oprattr 0.7.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of oprattr might be problematic. Click here for more details.

@@ -1,5 +1,12 @@
1
1
  ## NEXT
2
2
 
3
+ ## v0.8.0
4
+
5
+ - Redefine `mixins.NumpyMixin` to extend `numerical.NumpyMixin`
6
+ - Redefine `Object` to extend `numerical.Object`
7
+ - Improve flexibility of `numpy` functions in `Operand`
8
+ - Update types in `__init__.pyi`
9
+
3
10
  ## v0.7.0
4
11
 
5
12
  - Add `oprattr.Object` and `oprattr.Quantity` to the public namespace
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oprattr
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Add your description here
5
5
  Author-email: Matthew Young <myoung.space.science@gmail.com>
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "oprattr"
3
- version = "0.7.0"
3
+ version = "0.8.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -24,4 +24,4 @@ dev = [
24
24
  ]
25
25
 
26
26
  [tool.uv.sources]
27
- numerical = { git = "https://github.com/myoung-space-science/numerical", rev = "v0.2.0" }
27
+ numerical = { git = "https://github.com/myoung-space-science/numerical", rev = "v0.3.0" }
@@ -0,0 +1,128 @@
1
+ import collections.abc
2
+ import functools
3
+
4
+ import numpy
5
+
6
+ from ._abstract import (
7
+ Quantity,
8
+ Object,
9
+ )
10
+ from ._exceptions import (
11
+ MetadataTypeError,
12
+ MetadataValueError,
13
+ OperationError,
14
+ )
15
+ from . import methods
16
+ from . import mixins
17
+ from . import _typeface
18
+ from ._operations import (
19
+ unary,
20
+ equality,
21
+ ordering,
22
+ additive,
23
+ multiplicative,
24
+ )
25
+
26
+
27
+ T = _typeface.TypeVar('T')
28
+
29
+ class Operand(Object[T], mixins.NumpyMixin):
30
+ """A concrete implementation of a real-valued object."""
31
+
32
+ __abs__ = methods.__abs__
33
+ __pos__ = methods.__pos__
34
+ __neg__ = methods.__neg__
35
+
36
+ __eq__ = methods.__eq__
37
+ __ne__ = methods.__ne__
38
+ __lt__ = methods.__lt__
39
+ __le__ = methods.__le__
40
+ __gt__ = methods.__gt__
41
+ __ge__ = methods.__ge__
42
+
43
+ __add__ = methods.__add__
44
+ __radd__ = methods.__radd__
45
+ __sub__ = methods.__sub__
46
+ __rsub__ = methods.__rsub__
47
+ __mul__ = methods.__mul__
48
+ __rmul__ = methods.__rmul__
49
+ __truediv__ = methods.__truediv__
50
+ __rtruediv__ = methods.__rtruediv__
51
+ __floordiv__ = methods.__floordiv__
52
+ __rfloordiv__ = methods.__rfloordiv__
53
+ __mod__ = methods.__mod__
54
+ __rmod__ = methods.__rmod__
55
+ __pow__ = methods.__pow__
56
+ __rpow__ = methods.__rpow__
57
+
58
+ def __array__(self, *args, **kwargs):
59
+ """Called for numpy.array(self)."""
60
+ return numpy.array(self._data, *args, **kwargs)
61
+
62
+ def _apply_ufunc(self, ufunc, method, *args, **kwargs):
63
+ if ufunc in (numpy.equal, numpy.not_equal):
64
+ # NOTE: We are probably here because the left operand is a
65
+ # `numpy.ndarray`, which would otherwise take control and return the
66
+ # pure `numpy` result.
67
+ f = getattr(ufunc, method)
68
+ return equality(f, *args)
69
+ data, meta = super()._apply_ufunc(ufunc, method, *args, **kwargs)
70
+ return self._from_numpy(data, **meta)
71
+
72
+ def _apply_function(self, func, types, args, kwargs):
73
+ data, meta = super()._apply_function(func, types, args, kwargs)
74
+ if data is NotImplemented:
75
+ return data
76
+ return self._from_numpy(data, **meta)
77
+
78
+ def _get_numpy_array(self):
79
+ return numpy.array(self._data)
80
+
81
+ def _from_numpy(self, data, **meta):
82
+ """Create a new instance after applying a numpy function."""
83
+ if isinstance(data, (list, tuple)):
84
+ r = [self._factory(array, **meta) for array in data]
85
+ if isinstance(data, tuple):
86
+ return tuple(r)
87
+ return r
88
+ return self._factory(data, **meta)
89
+
90
+ def _factory(self, data, **meta):
91
+ """Create a new instance from data and metadata.
92
+
93
+ The default implementation uses the standard `__new__` constructor.
94
+ Subclasses may overload this method to use a different constructor
95
+ (e.g., a module-defined factory function).
96
+ """
97
+ return type(self)(data, **meta)
98
+
99
+
100
+ @Operand.implementation(numpy.array_equal)
101
+ def array_equal(
102
+ x: numpy.typing.ArrayLike,
103
+ y: numpy.typing.ArrayLike,
104
+ **kwargs
105
+ ) -> bool:
106
+ """Called for numpy.array_equal(x, y)"""
107
+ return numpy.array_equal(numpy.array(x), numpy.array(y), **kwargs)
108
+
109
+
110
+ __all__ = [
111
+ # Modules
112
+ methods,
113
+ mixins,
114
+ # Object classes
115
+ Quantity,
116
+ Object,
117
+ # Error classes
118
+ MetadataTypeError,
119
+ MetadataValueError,
120
+ OperationError,
121
+ # Functions
122
+ additive,
123
+ equality,
124
+ multiplicative,
125
+ ordering,
126
+ unary,
127
+ ]
128
+
@@ -1,8 +1,27 @@
1
+ import collections.abc
2
+
1
3
  import numpy.typing
2
4
 
3
5
  from . import _abstract
6
+ from . import methods
4
7
  from . import mixins
5
8
  from . import _typeface
9
+ from ._abstract import (
10
+ Quantity,
11
+ Object,
12
+ )
13
+ from ._exceptions import (
14
+ MetadataTypeError,
15
+ MetadataValueError,
16
+ OperationError,
17
+ )
18
+ from ._operations import (
19
+ unary,
20
+ equality,
21
+ ordering,
22
+ additive,
23
+ multiplicative,
24
+ )
6
25
 
7
26
 
8
27
  T = _typeface.TypeVar('T')
@@ -79,3 +98,26 @@ class Operand(_abstract.Object[T], mixins.Numpy):
79
98
  def __rpow__(self, other):
80
99
  """Called for other ** self."""
81
100
 
101
+ def __array__(self, *args, **kwargs) -> numpy.typing.NDArray:
102
+ """Called for numpy.array(self)."""
103
+
104
+
105
+ __all__ = [
106
+ # Modules
107
+ methods,
108
+ mixins,
109
+ # Object classes
110
+ Quantity,
111
+ Object,
112
+ # Functions
113
+ unary,
114
+ equality,
115
+ ordering,
116
+ additive,
117
+ multiplicative,
118
+ # Error classes
119
+ MetadataTypeError,
120
+ MetadataValueError,
121
+ OperationError,
122
+ ]
123
+
@@ -25,7 +25,7 @@ class Quantity(numerical.Quantity[DataType], _typeface.Protocol):
25
25
  _meta: collections.abc.Mapping[str, _typeface.Any]
26
26
 
27
27
 
28
- class Object(numerical.Real, _typeface.Generic[DataType]):
28
+ class Object(numerical.Object[DataType], numerical.Real):
29
29
  """A real-valued object with metadata attributes."""
30
30
 
31
31
  def __init__(
@@ -35,7 +35,7 @@ class Object(numerical.Real, _typeface.Generic[DataType]):
35
35
  ) -> None:
36
36
  if not isinstance(__data, numerical.Real):
37
37
  raise TypeError("Data input to Object must be real-valued")
38
- self._data = __data
38
+ super().__init__(__data)
39
39
  self._meta = metadata
40
40
 
41
41
  def __str__(self):
@@ -0,0 +1,17 @@
1
+ class MetadataTypeError(TypeError):
2
+ """A metadata-related TypeError occurred."""
3
+
4
+
5
+ class MetadataValueError(ValueError):
6
+ """A metadata-related ValueError occurred."""
7
+
8
+
9
+ class OperationError(NotImplementedError):
10
+ """A metadata attribute does not support this operation.
11
+
12
+ The default behavior when applying an operator to a metadata attribute of
13
+ `~Operand` is to copy the current value if the attribute does not define the
14
+ operation. Custom metadata attributes may raise this exception to indicate
15
+ that attempting to apply the operator is truly an error.
16
+ """
17
+
@@ -4,14 +4,10 @@ from ._abstract import (
4
4
  Quantity,
5
5
  Object,
6
6
  )
7
-
8
-
9
- class MetadataTypeError(TypeError):
10
- """A metadata-related TypeError occurred."""
11
-
12
-
13
- class MetadataValueError(ValueError):
14
- """A metadata-related ValueError occurred."""
7
+ from ._exceptions import (
8
+ MetadataTypeError,
9
+ MetadataValueError,
10
+ )
15
11
 
16
12
 
17
13
  def _build_error_message(
@@ -0,0 +1,148 @@
1
+ import collections.abc
2
+
3
+ import numerical
4
+
5
+ from ._abstract import Quantity
6
+ from ._exceptions import OperationError
7
+ from . import _typeface
8
+
9
+
10
+ T = _typeface.TypeVar('T')
11
+
12
+ class Real:
13
+ """Mixin for adding basic real-valued operator support."""
14
+
15
+ def __abs__(self):
16
+ return self
17
+
18
+ def __pos__(self):
19
+ return self
20
+
21
+ def __neg__(self):
22
+ return self
23
+
24
+ def __eq__(self, other):
25
+ return False
26
+
27
+ def __ne__(self, other):
28
+ return not (self == other)
29
+
30
+ def __lt__(self, other):
31
+ return False
32
+
33
+ def __le__(self, other):
34
+ return (self < other) and (self == other)
35
+
36
+ def __gt__(self, other):
37
+ return not (self <= other)
38
+
39
+ def __ge__(self, other):
40
+ return not (self < other)
41
+
42
+ def __add__(self, other):
43
+ return self
44
+
45
+ def __radd__(self, other):
46
+ return self
47
+
48
+ def __sub__(self, other):
49
+ return self
50
+
51
+ def __rsub__(self, other):
52
+ return self
53
+
54
+ def __mul__(self, other):
55
+ return self
56
+
57
+ def __rmul__(self, other):
58
+ return self
59
+
60
+ def __truediv__(self, other):
61
+ return self
62
+
63
+ def __rtruediv__(self, other):
64
+ return self
65
+
66
+ def __floordiv__(self, other):
67
+ return self
68
+
69
+ def __rfloordiv__(self, other):
70
+ return self
71
+
72
+ def __mod__(self, other):
73
+ return self
74
+
75
+ def __rmod__(self, other):
76
+ return self
77
+
78
+ def __pow__(self, other):
79
+ return self
80
+
81
+ def __rpow__(self, other):
82
+ return self
83
+
84
+
85
+ UserFunction = collections.abc.Callable[..., T]
86
+
87
+ class NumpyMixin(numerical.mixins.NumpyMixin):
88
+ """Mixin for adding `numpy` support to objects with metadata.
89
+
90
+ Notes
91
+ -----
92
+ - This class extends `numerical.mixins.NumpyMixin`. See that class for
93
+ further documentation.
94
+ """
95
+
96
+ def _apply_ufunc(self, ufunc, method, *args, **kwargs):
97
+ data = super()._apply_ufunc(ufunc, method, *args, **kwargs)
98
+ f = getattr(ufunc, method)
99
+ types = [type(arg) for arg in args]
100
+ try:
101
+ meta = self._apply_operator_to_metadata(f, *args, **kwargs)
102
+ except OperationError as err:
103
+ raise TypeError(
104
+ f"Cannot apply numpy.{ufunc} to arguments with type(s) {types}"
105
+ ) from err
106
+ if method != 'at':
107
+ return data, meta
108
+
109
+ def _apply_function(self, func, types, args, kwargs):
110
+ data = super()._apply_function(func, types, args, kwargs)
111
+ try:
112
+ meta = self._apply_operator_to_metadata(func, *args, **kwargs)
113
+ except OperationError as err:
114
+ raise TypeError(
115
+ f"Cannot apply numpy.{func} to arguments with type(s) {types}"
116
+ ) from err
117
+ return data, meta
118
+
119
+ def _apply_operator_to_metadata(self, f, *args, **kwargs):
120
+ """Apply a numpy universal or public function to arguments."""
121
+ keys = {
122
+ k
123
+ for x in args
124
+ if isinstance(x, Quantity)
125
+ for k in x._meta.keys()
126
+ }
127
+ meta = {}
128
+ for key in keys:
129
+ values = [
130
+ x._meta[key]
131
+ if isinstance(x, Quantity) else x
132
+ for x in args
133
+ ]
134
+ try:
135
+ result = f(*values, **kwargs)
136
+ except TypeError as err:
137
+ if len(values) > 1 and any(v != values[0] for v in values):
138
+ raise err # TODO: More descriptive error message
139
+ else:
140
+ meta[key] = values[0]
141
+ except OperationError as err:
142
+ raise TypeError(
143
+ f"Attribute {key!r} does not support this operation"
144
+ ) from err
145
+ else:
146
+ meta[key] = result
147
+ return meta.copy()
148
+
@@ -1,13 +1,14 @@
1
1
  import itertools
2
2
  import numbers
3
3
 
4
+ import numerical
4
5
  import numpy
5
6
  import pytest
6
7
 
7
8
  import oprattr
8
9
 
9
10
 
10
- class Symbol(oprattr.mixins.Numpy):
11
+ class Symbol(numerical.mixins.NumpyMixin):
11
12
  """A symbolic test attribute."""
12
13
 
13
14
  def __init__(self, __x: str):
@@ -108,8 +108,8 @@ wheels = [
108
108
 
109
109
  [[package]]
110
110
  name = "numerical"
111
- version = "0.2.0"
112
- source = { git = "https://github.com/myoung-space-science/numerical?rev=v0.2.0#a6e3af04c8815fc82cd06b66696392daf50b4f2c" }
111
+ version = "0.3.0"
112
+ source = { git = "https://github.com/myoung-space-science/numerical?rev=v0.3.0#b4a46e5e93e8d256c1198aec22d5556ea041673b" }
113
113
  dependencies = [
114
114
  { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
115
115
  { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
@@ -267,7 +267,7 @@ wheels = [
267
267
 
268
268
  [[package]]
269
269
  name = "oprattr"
270
- version = "0.2.0"
270
+ version = "0.7.0"
271
271
  source = { editable = "." }
272
272
  dependencies = [
273
273
  { name = "numerical" },
@@ -284,7 +284,7 @@ dev = [
284
284
 
285
285
  [package.metadata]
286
286
  requires-dist = [
287
- { name = "numerical", git = "https://github.com/myoung-space-science/numerical?rev=v0.2.0" },
287
+ { name = "numerical", git = "https://github.com/myoung-space-science/numerical?rev=v0.3.0" },
288
288
  { name = "numpy", specifier = ">=2.2.1" },
289
289
  { name = "scipy", specifier = ">=1.15.0" },
290
290
  ]
@@ -1,179 +0,0 @@
1
- import collections.abc
2
- import functools
3
-
4
- import numpy
5
-
6
- from ._abstract import (
7
- Quantity,
8
- Object,
9
- )
10
- from . import methods
11
- from . import mixins
12
- from . import _typeface
13
- from ._operations import (
14
- unary,
15
- equality,
16
- ordering,
17
- additive,
18
- multiplicative,
19
- MetadataTypeError,
20
- MetadataValueError,
21
- )
22
-
23
-
24
- T = _typeface.TypeVar('T')
25
-
26
- class Operand(Object[T], mixins.Numpy):
27
- """A concrete implementation of a real-valued object."""
28
-
29
- __abs__ = methods.__abs__
30
- __pos__ = methods.__pos__
31
- __neg__ = methods.__neg__
32
-
33
- __eq__ = methods.__eq__
34
- __ne__ = methods.__ne__
35
- __lt__ = methods.__lt__
36
- __le__ = methods.__le__
37
- __gt__ = methods.__gt__
38
- __ge__ = methods.__ge__
39
-
40
- __add__ = methods.__add__
41
- __radd__ = methods.__radd__
42
- __sub__ = methods.__sub__
43
- __rsub__ = methods.__rsub__
44
- __mul__ = methods.__mul__
45
- __rmul__ = methods.__rmul__
46
- __truediv__ = methods.__truediv__
47
- __rtruediv__ = methods.__rtruediv__
48
- __floordiv__ = methods.__floordiv__
49
- __rfloordiv__ = methods.__rfloordiv__
50
- __mod__ = methods.__mod__
51
- __rmod__ = methods.__rmod__
52
- __pow__ = methods.__pow__
53
- __rpow__ = methods.__rpow__
54
-
55
- def __array__(self, *args, **kwargs):
56
- """Called for numpy.array(self)."""
57
- return numpy.array(self._data, *args, **kwargs)
58
-
59
- def _apply_ufunc(self, ufunc, method, *args, **kwargs):
60
- if ufunc in (numpy.equal, numpy.not_equal):
61
- # NOTE: We are probably here because the left operand is a
62
- # `numpy.ndarray`, which would otherwise take control and return the
63
- # pure `numpy` result.
64
- f = getattr(ufunc, method)
65
- return equality(f, *args)
66
- return super()._apply_ufunc(ufunc, method, *args, **kwargs)
67
-
68
-
69
- @Operand.implementation(numpy.array_equal)
70
- def array_equal(
71
- x: numpy.typing.ArrayLike,
72
- y: numpy.typing.ArrayLike,
73
- **kwargs
74
- ) -> bool:
75
- """Called for numpy.array_equal(x, y)"""
76
- return numpy.array_equal(numpy.array(x), numpy.array(y), **kwargs)
77
-
78
-
79
- class OperationError(NotImplementedError):
80
- """A metadata attribute does not support this operation.
81
-
82
- The default behavior when applying an operator to a metadata attribute of
83
- `~Operand` is to copy the current value if the attribute does not define the
84
- operation. Custom metadata attributes may raise this exception to indicate
85
- that attempting to apply the operator is truly an error.
86
- """
87
-
88
-
89
- @Operand.implementation(numpy.gradient)
90
- def gradient(x: Operand[T], *args, **kwargs):
91
- """Called for numpy.gradient(x)."""
92
- f = numpy.gradient
93
- data = f(x._data, *args, **kwargs)
94
- meta = _apply_to_metadata(f, x, **kwargs)
95
- if isinstance(data, (list, tuple)):
96
- r = [type(x)(array, **meta) for array in data]
97
- if isinstance(data, tuple):
98
- return tuple(r)
99
- return r
100
- return type(x)(data, **meta)
101
-
102
-
103
- def wrapnumpy(f: collections.abc.Callable):
104
- """Implement a numpy function for objects with metadata."""
105
- @functools.wraps(f)
106
- def method(x: Operand[T], **kwargs):
107
- """Apply a numpy function to x."""
108
- data = f(x._data, **kwargs)
109
- meta = _apply_to_metadata(f, x, **kwargs)
110
- return type(x)(data, **meta)
111
- return method
112
-
113
-
114
- def _apply_to_metadata(
115
- f: collections.abc.Callable,
116
- x: Operand,
117
- **kwargs,
118
- ) -> dict[str, _typeface.Any]:
119
- """Apply `f` to metadata attributes."""
120
- processed = {}
121
- for key, value in x._meta.items():
122
- try:
123
- v = f(value, **kwargs)
124
- except TypeError:
125
- processed[key] = value
126
- except OperationError as exc:
127
- raise TypeError(
128
- f"Cannot compute numpy.{f.__qualname__}(x)"
129
- f" because metadata attribute {key!r} of {type(x)}"
130
- " does not support this operation"
131
- ) from exc
132
- else:
133
- processed[key] = v
134
- return processed.copy()
135
-
136
-
137
- _OPERAND_UFUNCS = (
138
- numpy.sqrt,
139
- numpy.sin,
140
- numpy.cos,
141
- numpy.tan,
142
- numpy.log,
143
- numpy.log2,
144
- numpy.log10,
145
- )
146
-
147
-
148
- _OPERAND_FUNCTIONS = (
149
- numpy.squeeze,
150
- numpy.mean,
151
- numpy.sum,
152
- numpy.cumsum,
153
- numpy.transpose,
154
- numpy.trapezoid,
155
- )
156
-
157
-
158
- for _func in _OPERAND_UFUNCS + _OPERAND_FUNCTIONS:
159
- Operand.implement(_func, wrapnumpy(_func))
160
-
161
-
162
- __all__ = [
163
- # Modules
164
- methods,
165
- mixins,
166
- # Object classes
167
- Quantity,
168
- Object,
169
- # Functions
170
- unary,
171
- equality,
172
- ordering,
173
- additive,
174
- multiplicative,
175
- # Error classes
176
- MetadataTypeError,
177
- MetadataValueError,
178
- OperationError,
179
- ]
@@ -1,487 +0,0 @@
1
- import collections.abc
2
- import numbers
3
-
4
- import numpy
5
-
6
- from . import _abstract
7
- from . import _typeface
8
-
9
-
10
- T = _typeface.TypeVar('T')
11
-
12
- class Real:
13
- """Mixin for adding basic real-valued operator support."""
14
-
15
- def __abs__(self):
16
- return self
17
-
18
- def __pos__(self):
19
- return self
20
-
21
- def __neg__(self):
22
- return self
23
-
24
- def __eq__(self, other):
25
- return False
26
-
27
- def __ne__(self, other):
28
- return not (self == other)
29
-
30
- def __lt__(self, other):
31
- return False
32
-
33
- def __le__(self, other):
34
- return (self < other) and (self == other)
35
-
36
- def __gt__(self, other):
37
- return not (self <= other)
38
-
39
- def __ge__(self, other):
40
- return not (self < other)
41
-
42
- def __add__(self, other):
43
- return self
44
-
45
- def __radd__(self, other):
46
- return self
47
-
48
- def __sub__(self, other):
49
- return self
50
-
51
- def __rsub__(self, other):
52
- return self
53
-
54
- def __mul__(self, other):
55
- return self
56
-
57
- def __rmul__(self, other):
58
- return self
59
-
60
- def __truediv__(self, other):
61
- return self
62
-
63
- def __rtruediv__(self, other):
64
- return self
65
-
66
- def __floordiv__(self, other):
67
- return self
68
-
69
- def __rfloordiv__(self, other):
70
- return self
71
-
72
- def __mod__(self, other):
73
- return self
74
-
75
- def __rmod__(self, other):
76
- return self
77
-
78
- def __pow__(self, other):
79
- return self
80
-
81
- def __rpow__(self, other):
82
- return self
83
-
84
-
85
- UserFunction = collections.abc.Callable[..., T]
86
-
87
- class Numpy:
88
- """Mixin for adding support for `numpy` functions to numeric objects.
89
-
90
- Classes that inherit from this class may implement support for `numpy`
91
- universal functions ("ufuncs"; e.g., `numpy.sqrt`) by overloading
92
- `_apply_ufunc`, and may implement support for `numpy` public functions
93
- (e.g., `numpy.squeeze`) by overloading `_apply_function` and registering
94
- individual function implementations via `implementation`.
95
-
96
- It is important to note that the use cases of this class extend beyond
97
- array-like objects. Both single- and multi-valued objects can benefit from
98
- implementing support for `numpy` universal and public functions. For
99
- example, it is possible to apply `numpy.sqrt` to both a real number and an
100
- array
101
-
102
- >>> numpy.sqrt(4)
103
- 2.0
104
- >>> numpy.sqrt([4, 9])
105
- array([2., 3.])
106
-
107
- Even the trivial application of `numpy.mean` to a real number is defined:
108
-
109
- >>> numpy.mean(2.5)
110
- 2.5
111
-
112
- Notes
113
- -----
114
- - This class does not inherit from `numpy.lib.mixins.NDArrayOperatorsMixin`,
115
- which implements most of the built-in Python numeric operators via
116
- `__array_ufunc__`, because it assumes that subclasses independently
117
- implement those methods.
118
- """
119
-
120
- def __init_subclass__(cls):
121
- cls._UFUNC_TYPES |= {cls}
122
- cls._FUNCTION_TYPES |= {cls}
123
- cls._FUNCTIONS = {}
124
-
125
- _UFUNC_TYPES = {
126
- numpy.ndarray,
127
- numbers.Number,
128
- list,
129
- _abstract.Quantity,
130
- }
131
-
132
- def __array_ufunc__(self, ufunc, method, *args, **kwargs):
133
- """Provide support for `numpy` universal functions.
134
-
135
- See https://numpy.org/doc/stable/reference/arrays.classes.html for more
136
- information on use of this special method.
137
-
138
- Notes
139
- -----
140
- - This method first ensures that the input types (as well as the type of
141
- `out`, if passed via keyword) are supported types. It then checks for
142
- a custom implementation of `ufunc`. If there is a custom
143
- implementation, this method applies it and returns the result. If
144
- there is no custom implementation, this method passes control to
145
- `_apply_ufunc`, to allow subclass customization.
146
- - See `implementation` for additional guidance on custom
147
- implementations.
148
-
149
- See Also
150
- --------
151
- `implementation`
152
- Class method for registering custom ufunc implementations.
153
-
154
- `_apply_ufunc`
155
- Instance method that allows custom handling of ufuncs corresponding
156
- to standard Python numerical operators.
157
- """
158
- out = kwargs.get('out', ())
159
- accepted = tuple(self._UFUNC_TYPES)
160
- if not all(isinstance(x, accepted) for x in args + out):
161
- return NotImplemented
162
- if out:
163
- kwargs['out'] = tuple(
164
- x._data if isinstance(x, _abstract.Quantity)
165
- else x for x in out
166
- )
167
- if self._implements(ufunc):
168
- operator = self._FUNCTIONS[ufunc]
169
- return operator(*args, **kwargs)
170
- return self._apply_ufunc(ufunc, method, *args, **kwargs)
171
-
172
- def _apply_ufunc(self, ufunc, method, *args, **kwargs):
173
- """Apply a `numpy` universal function (a.k.a "ufunc") to data.
174
-
175
- Notes
176
- -----
177
- - Subclasses that wish to customize support for ufuncs should overload
178
- this method instead of `__array_ufunc__`.
179
- - Subclasses should prefer to define custom implementations of specific
180
- universal functions and register each via `implementation`, rather
181
- than implementing function-specific logic in this method, since
182
- `__array_ufunc__` will check for a custom implementation of a given
183
- function before calling this method.
184
- - The default implementation of this method applies the given ufunc to
185
- real-valued data and directly returns the `numpy` result, without
186
- attempting to create a new instance of the custom subclass.
187
-
188
- See Also
189
- --------
190
- `implementation`
191
- Class method for registering custom ufunc implementations.
192
-
193
- `__array_ufunc__`
194
- The entry point for `numpy` universal functions.
195
- """
196
- operator = getattr(ufunc, method)
197
- values = self._get_numpy_args(args)
198
- try:
199
- data = operator(*values, **kwargs)
200
- except TypeError as err:
201
- raise TypeError(
202
- f"Unable to apply {ufunc} to {args}"
203
- ) from err
204
- if method != 'at':
205
- return data
206
-
207
- _FUNCTION_TYPES = {
208
- numpy.ndarray,
209
- _abstract.Object,
210
- } | set(numpy.ScalarType)
211
-
212
- def __array_function__(self, func, types, args, kwargs):
213
- """Provide support for functions in the `numpy` public API.
214
-
215
- See https://numpy.org/doc/stable/reference/arrays.classes.html for more
216
- information of use of this special method. The implementation shown here
217
- is a combination of the example on that page and code from the
218
- definition of `EncapsulateNDArray.__array_function__` in
219
- https://github.com/dask/dask/blob/main/dask/array/tests/test_dispatch.py
220
-
221
- Notes
222
- -----
223
- - This method first checks that all `types` are in
224
- `self._FUNCTION_TYPES`, thereby allowing subclasses that don't
225
- override `__array_function__` to handle objects of this type. It then
226
- checks for a custom implementation of `func`. If there is a custom
227
- implementation, this method applies it and returns the result. If
228
- there is no custom implementation, this method passes control to
229
- `_apply_function`, to allow subclass customization.
230
- - See `implementation` for additional guidance on custom
231
- implementations.
232
-
233
- See Also
234
- --------
235
- `implementation`
236
- Class method for registering custom function implementations.
237
-
238
- `_apply_function`
239
- Instance method that allows custom handling of `numpy` public
240
- functions when there is no registered custom implementation.
241
- """
242
- accepted = tuple(self._FUNCTION_TYPES)
243
- if not all(issubclass(ti, accepted) for ti in types):
244
- return NotImplemented
245
- if self._implements(func):
246
- return self._FUNCTIONS[func](*args, **kwargs)
247
- return self._apply_function(func, types, args, kwargs)
248
-
249
- def _apply_function(self, func, types, args, kwargs):
250
- """Apply a function in the `numpy` public API.
251
-
252
- Notes
253
- -----
254
- - Subclasses that wish to customize support for public functions should
255
- overload this method instead of `__array_function__`.
256
- - Subclasses should prefer to define custom implementations of specific
257
- public functions and register each via `implementation`, rather than
258
- implementing function-specific logic in this method, since
259
- `__array_function__` will check for a custom implementation of a given
260
- function before calling this method.
261
- - The default implementation calls `_get_numpy_array` for access to
262
- real-valued data via an instance of `numpy.ndarray`, `_get_numpy_args`
263
- to convert `args` to appropriate operands, and `_get_numpy_types` to
264
- extract appropriate operand types. Subclasses may choose to overload
265
- any of those individual methods instead of overloading this method.
266
-
267
- See Also
268
- --------
269
- `implementation`
270
- Class method for registering custom ufunc implementations.
271
-
272
- `__array_function__`
273
- The entry point for `numpy` public functions.
274
- """
275
- array = self._get_numpy_array()
276
- if array is None:
277
- return NotImplemented
278
- if not isinstance(array, numpy.ndarray):
279
- raise TypeError(
280
- f"{self.__class__.__qualname__}._get_numpy_array"
281
- " did not return a numpy.ndarray"
282
- ) from None
283
- args = self._get_numpy_args(args)
284
- types = self._get_numpy_types(types)
285
- return array.__array_function__(func, types, args, kwargs)
286
-
287
- def _get_numpy_array(self) -> numpy.typing.NDArray | None:
288
- """Convert the data interface to an array for `numpy` mixin methods.
289
-
290
- Notes
291
- -----
292
- - This method allows subclass implementations to control how they
293
- convert their data interface to a `numpy.ndarray` for use with `numpy`
294
- public functions.
295
- - Returning `None` from this method will cause `_apply_function` to
296
- return `NotImplemented`.
297
- - The default implementation unconditionally returns `None`.
298
- """
299
- return
300
-
301
- def _get_numpy_args(self, args):
302
- """Convert `args` to operands of a `numpy` function.
303
-
304
- This method will call `~_get_arg_data` on each member of `args` in order
305
- to build a `tuple` of suitable operands. Subclasses may overload
306
- `~_get_arg_data` to customize access to their data attribute.
307
- """
308
- return tuple(self._get_arg_data(arg) for arg in args)
309
-
310
- def _get_arg_data(self, arg):
311
- """Convert `arg` to an operand of a `numpy` function.
312
-
313
- See Also
314
- --------
315
- `~_get_numpy_args`
316
- The method that calls this method in a loop.
317
-
318
- Notes
319
- -----
320
- - This method allows a subclass to customize how `numpy` functions
321
- access its data attribute.
322
- - The default implementation will return the `data` attribute of a of
323
- `arg` if `arg` is an instance of the base object class; otherwise, it
324
- will return the unmodified argument.
325
- """
326
- if isinstance(arg, _abstract.Quantity):
327
- return arg._data
328
- return arg
329
-
330
- def _get_numpy_types(self, types):
331
- """Extract appropriate types for a `numpy` function.
332
-
333
- Notes
334
- -----
335
- - This method allows subclasses to restrict the object types that they
336
- pass to `numpy` public functions via `_apply_function`.
337
- - The default implementation returns a tuple that contains all types
338
- except for subtypes of `~_types.Quantity`.
339
- """
340
- return tuple(
341
- ti for ti in types
342
- if not issubclass(ti, _abstract.Object)
343
- )
344
-
345
- @classmethod
346
- def _implements(cls, operation: collections.abc.Callable):
347
- """True if this class defines a custom implementation for `operation`.
348
-
349
- This is a helper methods that gracefully handles the case in which a
350
- subclass does not support custom operator implementations.
351
- """
352
- try:
353
- result = operation in cls._FUNCTIONS
354
- except TypeError:
355
- return False
356
- return result
357
-
358
- _FUNCTIONS: dict[str, collections.abc.Callable]=None
359
- """Internal collection of custom `numpy` function implementations."""
360
-
361
- @classmethod
362
- def implementation(cls, numpy_function: collections.abc.Callable, /):
363
- """Register a custom implementation of this `numpy` function.
364
-
365
- Parameters
366
- ----------
367
- numpy_function : callable
368
- The `numpy` universal or public function to implement.
369
-
370
- Notes
371
- -----
372
- - Users may register `numpy` universal functions (a.k.a. ufuncs;
373
- https://numpy.org/doc/stable/reference/ufuncs.html) as well as
374
- functions in the public `numpy` API (e.g., `numpy.mean`). This may be
375
- important if, for example, a class needs to implement a custom version
376
- of `numpy.sqrt`, which is a ufunc.
377
- - See https://numpy.org/doc/stable/reference/arrays.classes.html for the
378
- suggestion on which this method is based.
379
-
380
- Examples
381
- --------
382
- Overload `numpy.mean` for an existing class called `Array` with a
383
- version that accepts no keyword arguments:
384
-
385
- ```
386
- @Array.implementation(numpy.mean)
387
- def mean(a: Array, **kwargs) -> Array:
388
- if kwargs:
389
- msg = "Cannot pass keywords to numpy.mean with Array" raise
390
- TypeError(msg)
391
- return numpy.sum(a) / len(a)
392
- ```
393
-
394
- This will compute the mean of the underlying data when called with no
395
- arguments, but will raise an exception when called with arguments:
396
-
397
- >>> v = Array([[1, 2], [3, 4]])
398
- >>> numpy.mean(v)
399
- 5.0
400
- >>> numpy.mean(v, axis=0)
401
- ...
402
- TypeError: Cannot pass keywords to numpy.mean with Array
403
-
404
- See Also
405
- --------
406
- `~implements`
407
- """
408
- if not callable(numpy_function):
409
- raise TypeError(
410
- "The target operation of a custom numpy implementation"
411
- " must be callable"
412
- ) from None
413
- def decorator(user_function: UserFunction):
414
- if cls._FUNCTIONS is None:
415
- raise NotImplementedError(
416
- f"Type {cls} does not support custom implementations"
417
- " of numpy functions"
418
- ) from None
419
- cls._FUNCTIONS[numpy_function] = user_function
420
- return user_function
421
- return decorator
422
-
423
- @classmethod
424
- def implement(
425
- cls,
426
- numpy_function: collections.abc.Callable,
427
- user_function: UserFunction,
428
- /,
429
- ) -> None:
430
- """Implement a `numpy` function via a given user function.
431
-
432
- This method serves as an alternative to the class method
433
- `implementation`, which is primarily meant to be used as a decorator.
434
- This method allows the user to directly associate a custom
435
- implementation with the target `numpy` function.
436
-
437
- Parameters
438
- ----------
439
- numpy_function : callable
440
- The `numpy` universal or public function to implement.
441
-
442
- user_function: callable
443
- The custom implementation to associate with `numpy_function`.
444
-
445
- Examples
446
- --------
447
- Here is an alternative to the `~implementation` example usage:
448
-
449
- ```
450
- def mean(a: Array, **kwargs) -> Array:
451
- if kwargs:
452
- msg = "Cannot pass keywords to numpy.mean with Array" raise
453
- TypeError(msg)
454
- return numpy.sum(a) / len(a)
455
-
456
- Array.implement(numpy.mean, mean)
457
- ```
458
-
459
- However, a more useful application may be to associate multiple `numpy`
460
- functions with a single custom implementation:
461
-
462
- ```
463
- def trig(f: numpy.ufunc):
464
- def method(a: Array):
465
- ... # custom implementation
466
- return method
467
-
468
- for f in {numpy.sin, numpy.cos, numpy.tan}:
469
- Array.implement(f, trig(f))
470
- ```
471
-
472
- See Also
473
- --------
474
- `~implementation`
475
- """
476
- if not callable(numpy_function):
477
- raise TypeError(
478
- "The target operation of a custom numpy implementation"
479
- " must be callable"
480
- ) from None
481
- if cls._FUNCTIONS is None:
482
- raise NotImplementedError(
483
- f"Type {cls} does not support custom implementations"
484
- " of numpy functions"
485
- ) from None
486
- cls._FUNCTIONS[numpy_function] = user_function
487
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes