Coverage for install/scipp/_binding.py: 24%
70 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-17 01:51 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-17 01:51 +0000
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3# @author Jan-Lukas Wynen
4from __future__ import annotations
6import inspect
7import types
8from collections.abc import Iterable, Mapping
9from typing import Any, TypeVar
11from ._scipp import core
12from .core.cpp_classes import DataArray, Variable
14_T = TypeVar('_T')
16_dict_likes = [
17 (core.Dataset, core.DataArray),
18 (core.Coords, core.Variable),
19 (core.Masks, core.Variable),
20 (core._BinsMeta, core.Variable),
21 (core._BinsCoords, core.Variable),
22 (core._BinsMasks, core.Variable),
23 (core._BinsAttrs, core.Variable),
24]
27def _make_dict_accessor_signature(value_type: type) -> list[inspect.Signature]:
28 base_params = [
29 inspect.Parameter(name='self', kind=inspect.Parameter.POSITIONAL_OR_KEYWORD),
30 inspect.Parameter(
31 name='key',
32 kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
33 annotation=str,
34 ),
35 ]
36 params_with_default = [
37 *base_params,
38 inspect.Parameter(
39 name='default',
40 kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
41 annotation=_T,
42 ),
43 ]
44 return [
45 inspect.Signature(
46 parameters=base_params,
47 return_annotation=value_type,
48 ),
49 inspect.Signature(
50 parameters=params_with_default,
51 return_annotation=value_type | _T,
52 ),
53 ]
56# Using type annotations here would lead to problems with Sphinx autodoc.
57# Type checkers anyway use the stub file
58# which is generated from a custom signature override.
59def _get(self, key, default=None): # type: ignore[no-untyped-def]
60 """
61 Return the value for key if key is in present, else default.
62 """
63 try:
64 return self[key]
65 except KeyError:
66 return default
69def bind_get() -> None:
70 for cls, value_type in _dict_likes:
71 method = _convert_to_method(name='get', func=_get, abbreviate_doc=False)
72 method.__doc__ = (
73 "Get the value associated with the provided key or the default value."
74 )
75 method.__signature__ = _make_dict_accessor_signature( # type: ignore[attr-defined]
76 value_type
77 )
78 cls.get = method
81def _expect_dimensionless_or_unitless(x: Variable | DataArray) -> None:
82 if x.unit is not None and x.unit != core.units.dimensionless:
83 raise core.UnitError(f'Expected unit dimensionless or no unit, got {x.unit}.')
86def _expect_no_variance(x: Variable | DataArray) -> None:
87 if x.variance is not None:
88 raise core.VariancesError('Expected input without variances.')
91def _int_dunder(self: Variable | DataArray) -> int:
92 _expect_dimensionless_or_unitless(self)
93 _expect_no_variance(self)
94 return int(self.value)
97def _float_dunder(self: Variable | DataArray) -> float:
98 _expect_dimensionless_or_unitless(self)
99 _expect_no_variance(self)
100 return float(self.value)
103def bind_conversion_to_builtin(cls: Any) -> None:
104 cls.__int__ = _convert_to_method(name='__int__', func=_int_dunder)
105 cls.__float__ = _convert_to_method(name='__float__', func=_float_dunder)
108class _NoDefaultType:
109 def __repr__(self) -> str:
110 return 'NotSpecified'
113_NoDefault = _NoDefaultType()
116def _pop(self, key, default=_NoDefault): # type: ignore[no-untyped-def] # see _get
117 """
118 Remove and return an element.
120 If key is not found, default is returned if given, otherwise KeyError is raised.
121 """
122 if key not in self and default is not _NoDefault:
123 return default
124 return self._pop(key)
127def bind_pop() -> None:
128 for cls, value_type in _dict_likes:
129 method = _convert_to_method(name='pop', func=_pop, abbreviate_doc=False)
130 method.__signature__ = _make_dict_accessor_signature( # type: ignore[attr-defined]
131 value_type
132 )
133 cls.pop = method
136def bind_functions_as_methods(
137 cls: type, namespace: Mapping[str, types.FunctionType], func_names: Iterable[str]
138) -> None:
139 for func_name, func in ((n, namespace[n]) for n in func_names):
140 bind_function_as_method(cls=cls, name=func_name, func=func)
143# Ideally, `func` would be annotated as `types.FunctionType`.
144# But that makes mypy flag calls to `bind_function_as_method` as errors
145# because functions are instances of `Callable` but not of `FunctionType`.
146# Note, using `Callable` does not work because it only defines `__call__`,
147# but not define the required attributes.
148def bind_function_as_method(
149 *, cls: type, name: str, func: Any, abbreviate_doc: bool = True
150) -> None:
151 setattr(
152 cls,
153 name,
154 _convert_to_method(name=name, func=func, abbreviate_doc=abbreviate_doc),
155 )
158def _convert_to_method(
159 *, name: str, func: Any, abbreviate_doc: bool = True
160) -> types.FunctionType:
161 method = types.FunctionType(
162 func.__code__, func.__globals__, name, func.__defaults__, func.__closure__
163 )
164 method.__kwdefaults__ = func.__kwdefaults__
165 method.__annotations__ = func.__annotations__
166 if func.__doc__ is not None:
167 # Extract the summary from the docstring.
168 # This relies on check W293 in flake8 to avoid implementing a more
169 # sophisticate / expensive parser that running during import of scipp.
170 # Line feeds are replaced because they mess with the
171 # reST parser of autosummary.
172 if abbreviate_doc:
173 method.__doc__ = (
174 func.__doc__.split('\n\n', 1)[0].replace('\n', ' ')
175 + f'\n\n:seealso: Details in :py:meth:`scipp.{name}`'
176 )
177 else:
178 method.__doc__ = func.__doc__
179 if hasattr(func, '__wrapped__'):
180 method.__wrapped__ = func.__wrapped__ # type: ignore[attr-defined]
181 return method