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

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 

5 

6import inspect 

7import types 

8from collections.abc import Iterable, Mapping 

9from typing import Any, TypeVar 

10 

11from ._scipp import core 

12from .core.cpp_classes import DataArray, Variable 

13 

14_T = TypeVar('_T') 

15 

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] 

25 

26 

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 ] 

54 

55 

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 

67 

68 

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 

79 

80 

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}.') 

84 

85 

86def _expect_no_variance(x: Variable | DataArray) -> None: 

87 if x.variance is not None: 

88 raise core.VariancesError('Expected input without variances.') 

89 

90 

91def _int_dunder(self: Variable | DataArray) -> int: 

92 _expect_dimensionless_or_unitless(self) 

93 _expect_no_variance(self) 

94 return int(self.value) 

95 

96 

97def _float_dunder(self: Variable | DataArray) -> float: 

98 _expect_dimensionless_or_unitless(self) 

99 _expect_no_variance(self) 

100 return float(self.value) 

101 

102 

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) 

106 

107 

108class _NoDefaultType: 

109 def __repr__(self) -> str: 

110 return 'NotSpecified' 

111 

112 

113_NoDefault = _NoDefaultType() 

114 

115 

116def _pop(self, key, default=_NoDefault): # type: ignore[no-untyped-def] # see _get 

117 """ 

118 Remove and return an element. 

119 

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) 

125 

126 

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 

134 

135 

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) 

141 

142 

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 ) 

156 

157 

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