oprattr 0.1.0__py3-none-any.whl

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.

oprattr/__init__.py ADDED
@@ -0,0 +1,204 @@
1
+ import functools
2
+ import numbers
3
+ import typing
4
+
5
+ import numpy
6
+
7
+ from . import mixins
8
+ from . import operators
9
+ from . import _types
10
+ from ._operations import (
11
+ unary,
12
+ equality,
13
+ ordering,
14
+ additive,
15
+ multiplicative,
16
+ )
17
+
18
+
19
+ T = typing.TypeVar('T')
20
+
21
+
22
+ class Operand(_types.Object[T], mixins.Numpy):
23
+ """A concrete implementation of a real-valued object."""
24
+
25
+ def __abs__(self):
26
+ """Called for abs(self)."""
27
+ return unary(operators.abs, self)
28
+
29
+ def __pos__(self):
30
+ """Called for +self."""
31
+ return unary(operators.pos, self)
32
+
33
+ def __neg__(self):
34
+ """Called for -self."""
35
+ return unary(operators.neg, self)
36
+
37
+ def __eq__(self, other):
38
+ """Called for self == other."""
39
+ return equality(operators.eq, self, other)
40
+
41
+ def __ne__(self, other):
42
+ """Called for self != other."""
43
+ return equality(operators.ne, self, other)
44
+
45
+ def __lt__(self, other):
46
+ """Called for self < other."""
47
+ return ordering(operators.lt, self, other)
48
+
49
+ def __le__(self, other):
50
+ """Called for self <= other."""
51
+ return ordering(operators.le, self, other)
52
+
53
+ def __gt__(self, other):
54
+ """Called for self > other."""
55
+ return ordering(operators.gt, self, other)
56
+
57
+ def __ge__(self, other):
58
+ """Called for self >= other."""
59
+ return ordering(operators.ge, self, other)
60
+
61
+ def __add__(self, other):
62
+ """Called for self + other."""
63
+ return additive(operators.add, self, other)
64
+
65
+ def __radd__(self, other):
66
+ """Called for other + self."""
67
+ return additive(operators.add, other, self)
68
+
69
+ def __sub__(self, other):
70
+ """Called for self - other."""
71
+ return additive(operators.sub, self, other)
72
+
73
+ def __rsub__(self, other):
74
+ """Called for other - self."""
75
+ return additive(operators.sub, other, self)
76
+
77
+ def __mul__(self, other):
78
+ """Called for self * other."""
79
+ return multiplicative(operators.mul, self, other)
80
+
81
+ def __rmul__(self, other):
82
+ """Called for other * self."""
83
+ return multiplicative(operators.mul, other, self)
84
+
85
+ def __truediv__(self, other):
86
+ """Called for self / other."""
87
+ return multiplicative(operators.truediv, self, other)
88
+
89
+ def __rtruediv__(self, other):
90
+ """Called for other / self."""
91
+ return multiplicative(operators.truediv, other, self)
92
+
93
+ def __floordiv__(self, other):
94
+ """Called for self // other."""
95
+ return multiplicative(operators.floordiv, self, other)
96
+
97
+ def __rfloordiv__(self, other):
98
+ """Called for other // self."""
99
+ return multiplicative(operators.floordiv, other, self)
100
+
101
+ def __mod__(self, other):
102
+ """Called for self % other."""
103
+ return multiplicative(operators.mod, self, other)
104
+
105
+ def __rmod__(self, other):
106
+ """Called for other % self."""
107
+ return multiplicative(operators.mod, other, self)
108
+
109
+ def __pow__(self, other):
110
+ """Called for self ** other."""
111
+ if isinstance(other, numbers.Real):
112
+ return multiplicative(operators.pow, self, other)
113
+ return NotImplemented
114
+
115
+ def __rpow__(self, other):
116
+ """Called for other ** self."""
117
+ return super().__rpow__(other)
118
+
119
+ def __array__(self, *args, **kwargs):
120
+ """Called for numpy.array(self)."""
121
+ return numpy.array(self._data, *args, **kwargs)
122
+
123
+
124
+ @Operand.implementation(numpy.array_equal)
125
+ def array_equal(
126
+ x: numpy.typing.ArrayLike,
127
+ y: numpy.typing.ArrayLike,
128
+ **kwargs
129
+ ) -> bool:
130
+ """Called for numpy.array_equal(x, y)"""
131
+ return numpy.array_equal(numpy.array(x), numpy.array(y), **kwargs)
132
+
133
+
134
+ @Operand.implementation(numpy.gradient)
135
+ def gradient(x: Operand[T], *args, **kwargs):
136
+ """Called for numpy.gradient(x)."""
137
+ data = numpy.gradient(x._data, *args, **kwargs)
138
+ meta = {}
139
+ for key, value in x._meta.items():
140
+ try:
141
+ v = numpy.gradient(value, **kwargs)
142
+ except TypeError as exc:
143
+ raise TypeError(
144
+ "Cannot compute numpy.gradient(x)"
145
+ f" because metadata attribute {key!r}"
146
+ " does not support this operation"
147
+ ) from exc
148
+ else:
149
+ meta[key] = v
150
+ if isinstance(data, (list, tuple)):
151
+ r = [type(x)(array, **meta) for array in data]
152
+ if isinstance(data, tuple):
153
+ return tuple(r)
154
+ return r
155
+ return type(x)(data, **meta)
156
+
157
+
158
+ def wrapnumpy(f: typing.Callable):
159
+ """Implement a numpy function for objects with metadata."""
160
+ @functools.wraps(f)
161
+ def method(x: Operand[T], **kwargs):
162
+ """Apply a numpy function to x."""
163
+ data = f(x._data, **kwargs)
164
+ meta = {}
165
+ for key, value in x._meta.items():
166
+ try:
167
+ v = f(value, **kwargs)
168
+ except TypeError as exc:
169
+ raise TypeError(
170
+ f"Cannot compute numpy.{f.__qualname__}(x)"
171
+ f" because metadata attribute {key!r}"
172
+ " does not support this operation"
173
+ ) from exc
174
+ else:
175
+ meta[key] = v
176
+ return type(x)(data, **meta)
177
+ return method
178
+
179
+
180
+ _OPERAND_UFUNCS = (
181
+ numpy.sqrt,
182
+ numpy.sin,
183
+ numpy.cos,
184
+ numpy.tan,
185
+ numpy.log,
186
+ numpy.log2,
187
+ numpy.log10,
188
+ )
189
+
190
+
191
+ _OPERAND_FUNCTIONS = (
192
+ numpy.squeeze,
193
+ numpy.mean,
194
+ numpy.sum,
195
+ numpy.cumsum,
196
+ numpy.transpose,
197
+ numpy.trapezoid,
198
+ )
199
+
200
+
201
+ for f in _OPERAND_UFUNCS + _OPERAND_FUNCTIONS:
202
+ Operand.implement(f, wrapnumpy(f))
203
+
204
+
oprattr/_operations.py ADDED
@@ -0,0 +1,179 @@
1
+ import typing
2
+
3
+ from . import operators
4
+ from ._types import Object
5
+
6
+
7
+ class MetadataError(TypeError):
8
+ """A metadata-related TypeError occurred."""
9
+
10
+ def __init__(
11
+ self,
12
+ f: operators.Operator,
13
+ *args,
14
+ error: typing.Optional[str]=None,
15
+ key: typing.Optional[str]=None,
16
+ ) -> None:
17
+ super().__init__(*args)
18
+ self._f = f
19
+ self._error = error
20
+ self._key = key
21
+
22
+ def __str__(self):
23
+ """Called when handling the exception."""
24
+ types = [type(arg) for arg in self.args]
25
+ return _build_error_message(
26
+ self._f,
27
+ *types,
28
+ error=self._error,
29
+ key=self._key,
30
+ )
31
+
32
+
33
+ def _build_error_message(
34
+ f: operators.Operator,
35
+ *types: type,
36
+ error: typing.Optional[str]=None,
37
+ key: typing.Optional[str]=None,
38
+ ) -> str:
39
+ """Helper for `_raise_metadata_exception`.
40
+
41
+ This function should avoid raising an exception if at all possible, and
42
+ instead return the default error message, since it is already being called
43
+ as the result of an error elsewhere.
44
+ """
45
+ errmsg = f"Cannot compute {f}"
46
+ errstr = error.lower() if isinstance(error, str) else ''
47
+ if errstr == 'unequal':
48
+ return f"{errmsg} between objects with unequal metadata"
49
+ if errstr in {'non-empty', 'nonempty'}:
50
+ if len(types) == 2:
51
+ a, b = types
52
+ endstr = "because {} has metadata"
53
+ if issubclass(a, Object):
54
+ return f"{errmsg} between {a} and {b} {endstr.format(str(a))}"
55
+ if issubclass(b, Object):
56
+ return f"{errmsg} between {a} and {b} {endstr.format(str(b))}"
57
+ if errstr == 'type':
58
+ if key is None:
59
+ keystr = "a metadata attribute"
60
+ else:
61
+ keystr = f"metadata attribute {key!r}"
62
+ midstr = f"because {keystr}"
63
+ endstr = "does not support this operation"
64
+ if len(types) == 1:
65
+ return f"{errmsg} of {types[0]} {midstr} {endstr}"
66
+ if len(types) == 2:
67
+ a, b = types
68
+ return f"{errmsg} between {a} and {b} {midstr} {endstr}"
69
+ return errmsg
70
+
71
+
72
+ def unary(f: operators.Operator, a):
73
+ """Compute the unary operation f(a)."""
74
+ if isinstance(a, Object):
75
+ meta = {}
76
+ for key, value in a._meta.items():
77
+ try:
78
+ v = f(value)
79
+ except TypeError as exc:
80
+ raise MetadataError(f, a, error='type', key=key) from exc
81
+ else:
82
+ meta[key] = v
83
+ return type(a)(f(a._data), **meta)
84
+ return f(a)
85
+
86
+
87
+ def equality(f: operators.Operator, a, b):
88
+ """Compute the equality operation f(a, b)."""
89
+ if isinstance(a, Object) and isinstance(b, Object):
90
+ if a._meta != b._meta:
91
+ return f is operators.ne
92
+ return f(a._data, b._data)
93
+ if isinstance(a, Object):
94
+ if not a._meta:
95
+ return f(a._data, b)
96
+ return f is operators.ne
97
+ if isinstance(b, Object):
98
+ if not b._meta:
99
+ return f(a, b._data)
100
+ return f is operators.ne
101
+ return f(a, b)
102
+
103
+
104
+ def ordering(f: operators.Operator, a, b):
105
+ """Compute the ordering operation f(a, b)."""
106
+ if isinstance(a, Object) and isinstance(b, Object):
107
+ if a._meta == b._meta:
108
+ return f(a._data, b._data)
109
+ raise MetadataError(f, a, b, error='unequal') from None
110
+ if isinstance(a, Object):
111
+ if not a._meta:
112
+ return f(a._data, b)
113
+ raise MetadataError(f, a, b, error='non-empty') from None
114
+ if isinstance(b, Object):
115
+ if not b._meta:
116
+ return f(a, b._data)
117
+ raise MetadataError(f, a, b, error='non-empty') from None
118
+ return f(a, b)
119
+
120
+
121
+ def additive(f: operators.Operator, a, b):
122
+ """Compute the additive operation f(a, b)."""
123
+ if isinstance(a, Object) and isinstance(b, Object):
124
+ if a._meta == b._meta:
125
+ return type(a)(f(a._data, b._data), **a._meta)
126
+ raise MetadataError(f, a, b, error='unequal') from None
127
+ if isinstance(a, Object):
128
+ if not a._meta:
129
+ return type(a)(f(a._data, b))
130
+ raise MetadataError(f, a, b, error='non-empty') from None
131
+ if isinstance(b, Object):
132
+ if not b._meta:
133
+ return type(b)(f(a, b._data))
134
+ raise MetadataError(f, a, b, error='non-empty') from None
135
+ return f(a, b)
136
+
137
+
138
+ def multiplicative(f: operators.Operator, a, b):
139
+ """Compute the multiplicative operation f(a, b)."""
140
+ if isinstance(a, Object) and isinstance(b, Object):
141
+ keys = set(a._meta) & set(b._meta)
142
+ meta = {}
143
+ for key in keys:
144
+ try:
145
+ v = f(a._meta[key], b._meta[key])
146
+ except TypeError as exc:
147
+ raise MetadataError(f, a, b, error='type', key=key) from exc
148
+ else:
149
+ meta[key] = v
150
+ for key, value in a._meta.items():
151
+ if key not in keys:
152
+ meta[key] = value
153
+ for key, value in b._meta.items():
154
+ if key not in keys:
155
+ meta[key] = value
156
+ return type(a)(f(a._data, b._data), **meta)
157
+ if isinstance(a, Object):
158
+ meta = {}
159
+ for key, value in a._meta.items():
160
+ try:
161
+ v = f(value, b)
162
+ except TypeError as exc:
163
+ raise MetadataError(f, a, b, error='type', key=key) from exc
164
+ else:
165
+ meta[key] = v
166
+ return type(a)(f(a._data, b), **meta)
167
+ if isinstance(b, Object):
168
+ meta = {}
169
+ for key, value in b._meta.items():
170
+ try:
171
+ v = f(a, value)
172
+ except TypeError as exc:
173
+ raise MetadataError(f, a, b, error='type', key=key) from exc
174
+ else:
175
+ meta[key] = v
176
+ return type(b)(f(a, b._data), **meta)
177
+ return f(a, b)
178
+
179
+
oprattr/_types.py ADDED
@@ -0,0 +1,144 @@
1
+ import abc
2
+ import numbers
3
+ import typing
4
+
5
+ import numpy.typing
6
+
7
+
8
+ @typing.runtime_checkable
9
+ class Real(typing.Protocol):
10
+ """Abstract protocol for real-valued objects."""
11
+
12
+ @abc.abstractmethod
13
+ def __abs__(self):
14
+ return NotImplemented
15
+
16
+ @abc.abstractmethod
17
+ def __pos__(self):
18
+ return NotImplemented
19
+
20
+ @abc.abstractmethod
21
+ def __neg__(self):
22
+ return NotImplemented
23
+
24
+ @abc.abstractmethod
25
+ def __eq__(self, other):
26
+ return False
27
+
28
+ @abc.abstractmethod
29
+ def __ne__(self, other):
30
+ return True
31
+
32
+ @abc.abstractmethod
33
+ def __le__(self, other):
34
+ return NotImplemented
35
+
36
+ @abc.abstractmethod
37
+ def __lt__(self, other):
38
+ return NotImplemented
39
+
40
+ @abc.abstractmethod
41
+ def __ge__(self, other):
42
+ return NotImplemented
43
+
44
+ @abc.abstractmethod
45
+ def __gt__(self, other):
46
+ return NotImplemented
47
+
48
+ @abc.abstractmethod
49
+ def __add__(self, other):
50
+ return NotImplemented
51
+
52
+ @abc.abstractmethod
53
+ def __radd__(self, other):
54
+ return NotImplemented
55
+
56
+ @abc.abstractmethod
57
+ def __sub__(self, other):
58
+ return NotImplemented
59
+
60
+ @abc.abstractmethod
61
+ def __rsub__(self, other):
62
+ return NotImplemented
63
+
64
+ @abc.abstractmethod
65
+ def __mul__(self, other):
66
+ return NotImplemented
67
+
68
+ @abc.abstractmethod
69
+ def __rmul__(self, other):
70
+ return NotImplemented
71
+
72
+ @abc.abstractmethod
73
+ def __truediv__(self, other):
74
+ return NotImplemented
75
+
76
+ @abc.abstractmethod
77
+ def __rtruediv__(self, other):
78
+ return NotImplemented
79
+
80
+ @abc.abstractmethod
81
+ def __floordiv__(self, other):
82
+ return NotImplemented
83
+
84
+ @abc.abstractmethod
85
+ def __rfloordiv__(self, other):
86
+ return NotImplemented
87
+
88
+ @abc.abstractmethod
89
+ def __mod__(self, other):
90
+ return NotImplemented
91
+
92
+ @abc.abstractmethod
93
+ def __rmod__(self, other):
94
+ return NotImplemented
95
+
96
+ @abc.abstractmethod
97
+ def __pow__(self, other):
98
+ return NotImplemented
99
+
100
+ @abc.abstractmethod
101
+ def __rpow__(self, other):
102
+ return NotImplemented
103
+
104
+
105
+ DataType = typing.TypeVar(
106
+ 'DataType',
107
+ int,
108
+ float,
109
+ numbers.Number,
110
+ numpy.number,
111
+ numpy.typing.ArrayLike,
112
+ numpy.typing.NDArray,
113
+ )
114
+
115
+
116
+ class Object(Real, typing.Generic[DataType]):
117
+ """A real-valued object with metadata attributes."""
118
+
119
+ def __init__(
120
+ self,
121
+ __data: DataType,
122
+ **metadata,
123
+ ) -> None:
124
+ if not isinstance(__data, Real):
125
+ raise TypeError("Data input to Object must be real-valued")
126
+ self._data = __data
127
+ self._meta = metadata
128
+
129
+ def __repr__(self):
130
+ """Called for repr(self)."""
131
+ try:
132
+ datastr = numpy.array2string(
133
+ self._data,
134
+ separator=", ",
135
+ threshold=6,
136
+ edgeitems=2,
137
+ prefix=f"{self.__class__.__qualname__}(",
138
+ suffix=")"
139
+ )
140
+ except Exception:
141
+ datastr = str(self._data)
142
+ metastr = "metadata={" + ", ".join(f"{k!r}" for k in self._meta) + "}"
143
+ return f"{self.__class__.__qualname__}({datastr}, {metastr})"
144
+
oprattr/mixins.py ADDED
@@ -0,0 +1,415 @@
1
+ import numbers
2
+ import typing
3
+
4
+ import numpy
5
+
6
+ from . import _types
7
+
8
+
9
+ T = typing.TypeVar('T')
10
+
11
+
12
+ UserFunction = typing.Callable[..., T]
13
+
14
+
15
+ class Numpy:
16
+ """Mixin for adding support for `numpy` functions to numeric objects.
17
+
18
+ Classes that inherit from this class may implement support for `numpy`
19
+ universal functions ("ufuncs"; e.g., `numpy.sqrt`) by overloading
20
+ `_apply_ufunc`, and may implement support for `numpy` public functions
21
+ (e.g., `numpy.squeeze`) by overloading `_apply_function` and registering
22
+ individual function implementations via `implementation`.
23
+
24
+ It is important to note that the use cases of this class extend beyond
25
+ array-like objects. Both single- and multi-valued objects can benefit from
26
+ implementing support for `numpy` universal and public functions. For
27
+ example, it is possible to apply `numpy.sqrt` to both a real number and an
28
+ array
29
+
30
+ >>> numpy.sqrt(4)
31
+ 2.0
32
+ >>> numpy.sqrt([4, 9])
33
+ array([2., 3.])
34
+
35
+ Even the trivial application of `numpy.mean` to a real number is defined:
36
+
37
+ >>> numpy.mean(2.5)
38
+ 2.5
39
+
40
+ Notes
41
+ -----
42
+ - This class does not inherit from `numpy.lib.mixins.NDArrayOperatorsMixin`,
43
+ which implements most of the built-in Python numeric operators via
44
+ `__array_ufunc__`, because it assumes that subclasses independently
45
+ implement those methods.
46
+ """
47
+
48
+ def __init_subclass__(cls):
49
+ cls._UFUNC_TYPES |= {cls}
50
+ cls._FUNCTION_TYPES |= {cls}
51
+ cls._FUNCTIONS = {}
52
+
53
+ _UFUNC_TYPES = {
54
+ numpy.ndarray,
55
+ numbers.Number,
56
+ list,
57
+ _types.Object,
58
+ }
59
+
60
+ def __array_ufunc__(self, ufunc, method, *args, **kwargs):
61
+ """Provide support for `numpy` universal functions.
62
+
63
+ See https://numpy.org/doc/stable/reference/arrays.classes.html for more
64
+ information on use of this special method.
65
+
66
+ Notes
67
+ -----
68
+ - This method first ensures that the input types (as well as the type of
69
+ `out`, if passed via keyword) are supported types. It then checks for
70
+ a custom implementation of `ufunc`. If there is a custom
71
+ implementation, this method applies it and returns the result. If
72
+ there is no custom implementation, this method passes control to
73
+ `_apply_ufunc`, to allow subclass customization.
74
+ - See `implementation` for additional guidance on custom
75
+ implementations.
76
+
77
+ See Also
78
+ --------
79
+ `implementation`
80
+ Class method for registering custom ufunc implementations.
81
+
82
+ `_apply_ufunc`
83
+ Instance method that allows custom handling of ufuncs corresponding
84
+ to standard Python numerical operators.
85
+ """
86
+ out = kwargs.get('out', ())
87
+ accepted = tuple(self._UFUNC_TYPES)
88
+ if not all(isinstance(x, accepted) for x in args + out):
89
+ return NotImplemented
90
+ if out:
91
+ kwargs['out'] = tuple(
92
+ x._data if isinstance(x, _types.Object)
93
+ else x for x in out
94
+ )
95
+ if self._implements(ufunc):
96
+ operator = self._FUNCTIONS[ufunc]
97
+ return operator(*args, **kwargs)
98
+ return self._apply_ufunc(ufunc, method, *args, **kwargs)
99
+
100
+ def _apply_ufunc(self, ufunc, method, *args, **kwargs):
101
+ """Apply a `numpy` universal function (a.k.a "ufunc") to data.
102
+
103
+ Notes
104
+ -----
105
+ - Subclasses that wish to customize support for ufuncs should overload
106
+ this method instead of `__array_ufunc__`.
107
+ - Subclasses should prefer to define custom implementations of specific
108
+ universal functions and register each via `implementation`, rather
109
+ than implementing function-specific logic in this method, since
110
+ `__array_ufunc__` will check for a custom implementation of a given
111
+ function before calling this method.
112
+ - The default implementation of this method applies the given ufunc to
113
+ real-valued data and directly returns the `numpy` result, without
114
+ attempting to create a new instance of the custom subclass.
115
+
116
+ See Also
117
+ --------
118
+ `implementation`
119
+ Class method for registering custom ufunc implementations.
120
+
121
+ `__array_ufunc__`
122
+ The entry point for `numpy` universal functions.
123
+ """
124
+ operator = getattr(ufunc, method)
125
+ values = self._get_numpy_args(args)
126
+ try:
127
+ data = operator(*values, **kwargs)
128
+ except TypeError as err:
129
+ raise TypeError(
130
+ f"Unable to apply {ufunc} to {args}"
131
+ ) from err
132
+ if method != 'at':
133
+ return data
134
+
135
+ _FUNCTION_TYPES = {
136
+ numpy.ndarray,
137
+ _types.Object,
138
+ } | set(numpy.ScalarType)
139
+
140
+ def __array_function__(self, func, types, args, kwargs):
141
+ """Provide support for functions in the `numpy` public API.
142
+
143
+ See https://numpy.org/doc/stable/reference/arrays.classes.html for more
144
+ information of use of this special method. The implementation shown here
145
+ is a combination of the example on that page and code from the
146
+ definition of `EncapsulateNDArray.__array_function__` in
147
+ https://github.com/dask/dask/blob/main/dask/array/tests/test_dispatch.py
148
+
149
+ Notes
150
+ -----
151
+ - This method first checks that all `types` are in
152
+ `self._FUNCTION_TYPES`, thereby allowing subclasses that don't
153
+ override `__array_function__` to handle objects of this type. It then
154
+ checks for a custom implementation of `func`. If there is a custom
155
+ implementation, this method applies it and returns the result. If
156
+ there is no custom implementation, this method passes control to
157
+ `_apply_function`, to allow subclass customization.
158
+ - See `implementation` for additional guidance on custom
159
+ implementations.
160
+
161
+ See Also
162
+ --------
163
+ `implementation`
164
+ Class method for registering custom function implementations.
165
+
166
+ `_apply_function`
167
+ Instance method that allows custom handling of `numpy` public
168
+ functions when there is no registered custom implementation.
169
+ """
170
+ accepted = tuple(self._FUNCTION_TYPES)
171
+ if not all(issubclass(ti, accepted) for ti in types):
172
+ return NotImplemented
173
+ if self._implements(func):
174
+ return self._FUNCTIONS[func](*args, **kwargs)
175
+ return self._apply_function(func, types, args, kwargs)
176
+
177
+ def _apply_function(self, func, types, args, kwargs):
178
+ """Apply a function in the `numpy` public API.
179
+
180
+ Notes
181
+ -----
182
+ - Subclasses that wish to customize support for public functions should
183
+ overload this method instead of `__array_function__`.
184
+ - Subclasses should prefer to define custom implementations of specific
185
+ public functions and register each via `implementation`, rather than
186
+ implementing function-specific logic in this method, since
187
+ `__array_function__` will check for a custom implementation of a given
188
+ function before calling this method.
189
+ - The default implementation calls `_get_numpy_array` for access to
190
+ real-valued data via an instance of `numpy.ndarray`, `_get_numpy_args`
191
+ to convert `args` to appropriate operands, and `_get_numpy_types` to
192
+ extract appropriate operand types. Subclasses may choose to overload
193
+ any of those individual methods instead of overloading this method.
194
+
195
+ See Also
196
+ --------
197
+ `implementation`
198
+ Class method for registering custom ufunc implementations.
199
+
200
+ `__array_function__`
201
+ The entry point for `numpy` public functions.
202
+ """
203
+ array = self._get_numpy_array()
204
+ if array is None:
205
+ return NotImplemented
206
+ if not isinstance(array, numpy.ndarray):
207
+ raise TypeError(
208
+ f"{self.__class__.__qualname__}._get_numpy_array"
209
+ " did not return a numpy.ndarray"
210
+ ) from None
211
+ args = self._get_numpy_args(args)
212
+ types = self._get_numpy_types(types)
213
+ return array.__array_function__(func, types, args, kwargs)
214
+
215
+ def _get_numpy_array(self) -> typing.Optional[numpy.typing.NDArray]:
216
+ """Convert the data interface to an array for `numpy` mixin methods.
217
+
218
+ Notes
219
+ -----
220
+ - This method allows subclass implementations to control how they
221
+ convert their data interface to a `numpy.ndarray` for use with `numpy`
222
+ public functions.
223
+ - Returning `None` from this method will cause `_apply_function` to
224
+ return `NotImplemented`.
225
+ - The default implementation unconditionally returns `None`.
226
+ """
227
+ return
228
+
229
+ def _get_numpy_args(self, args):
230
+ """Convert `args` to operands of a `numpy` function.
231
+
232
+ This method will call `~_get_arg_data` on each member of `args` in order
233
+ to build a `tuple` of suitable operands. Subclasses may overload
234
+ `~_get_arg_data` to customize access to their data attribute.
235
+ """
236
+ return tuple(self._get_arg_data(arg) for arg in args)
237
+
238
+ def _get_arg_data(self, arg):
239
+ """Convert `arg` to an operand of a `numpy` function.
240
+
241
+ See Also
242
+ --------
243
+ `~_get_numpy_args`
244
+ The method that calls this method in a loop.
245
+
246
+ Notes
247
+ -----
248
+ - This method allows a subclass to customize how `numpy` functions
249
+ access its data attribute.
250
+ - The default implementation will return the `data` attribute of a of
251
+ `arg` if `arg` is an instance of the base object class; otherwise, it
252
+ will return the unmodified argument.
253
+ """
254
+ if isinstance(arg, _types.Object):
255
+ return arg._data
256
+ return arg
257
+
258
+ def _get_numpy_types(self, types):
259
+ """Extract appropriate types for a `numpy` function.
260
+
261
+ Notes
262
+ -----
263
+ - This method allows subclasses to restrict the object types that they
264
+ pass to `numpy` public functions via `_apply_function`.
265
+ - The default implementation returns a tuple that contains all types
266
+ except for subtypes of `~_types.Quantity`.
267
+ """
268
+ return tuple(
269
+ ti for ti in types
270
+ if not issubclass(ti, _types.Object)
271
+ )
272
+
273
+ @classmethod
274
+ def _implements(cls, operation: typing.Callable):
275
+ """True if this class defines a custom implementation for `operation`.
276
+
277
+ This is a helper methods that gracefully handles the case in which a
278
+ subclass does not support custom operator implementations.
279
+ """
280
+ try:
281
+ result = operation in cls._FUNCTIONS
282
+ except TypeError:
283
+ return False
284
+ return result
285
+
286
+ _FUNCTIONS: typing.Dict[str, typing.Callable]=None
287
+ """Internal collection of custom `numpy` function implementations."""
288
+
289
+ @classmethod
290
+ def implementation(cls, numpy_function: typing.Callable, /):
291
+ """Register a custom implementation of this `numpy` function.
292
+
293
+ Parameters
294
+ ----------
295
+ numpy_function : callable
296
+ The `numpy` universal or public function to implement.
297
+
298
+ Notes
299
+ -----
300
+ - Users may register `numpy` universal functions (a.k.a. ufuncs;
301
+ https://numpy.org/doc/stable/reference/ufuncs.html) as well as
302
+ functions in the public `numpy` API (e.g., `numpy.mean`). This may be
303
+ important if, for example, a class needs to implement a custom version
304
+ of `numpy.sqrt`, which is a ufunc.
305
+ - See https://numpy.org/doc/stable/reference/arrays.classes.html for the
306
+ suggestion on which this method is based.
307
+
308
+ Examples
309
+ --------
310
+ Overload `numpy.mean` for an existing class called `Array` with a
311
+ version that accepts no keyword arguments:
312
+
313
+ ```
314
+ @Array.implementation(numpy.mean)
315
+ def mean(a: Array, **kwargs) -> Array:
316
+ if kwargs:
317
+ msg = "Cannot pass keywords to numpy.mean with Array" raise
318
+ TypeError(msg)
319
+ return numpy.sum(a) / len(a)
320
+ ```
321
+
322
+ This will compute the mean of the underlying data when called with no
323
+ arguments, but will raise an exception when called with arguments:
324
+
325
+ >>> v = Array([[1, 2], [3, 4]])
326
+ >>> numpy.mean(v)
327
+ 5.0
328
+ >>> numpy.mean(v, axis=0)
329
+ ...
330
+ TypeError: Cannot pass keywords to numpy.mean with Array
331
+
332
+ See Also
333
+ --------
334
+ `~implements`
335
+ """
336
+ if not callable(numpy_function):
337
+ raise TypeError(
338
+ "The target operation of a custom numpy implementation"
339
+ " must be callable"
340
+ ) from None
341
+ def decorator(user_function: UserFunction):
342
+ if cls._FUNCTIONS is None:
343
+ raise NotImplementedError(
344
+ f"Type {cls} does not support custom implementations"
345
+ " of numpy functions"
346
+ ) from None
347
+ cls._FUNCTIONS[numpy_function] = user_function
348
+ return user_function
349
+ return decorator
350
+
351
+ @classmethod
352
+ def implement(
353
+ cls,
354
+ numpy_function: typing.Callable,
355
+ user_function: UserFunction,
356
+ /,
357
+ ) -> None:
358
+ """Implement a `numpy` function via a given user function.
359
+
360
+ This method serves as an alternative to the class method
361
+ `implementation`, which is primarily meant to be used as a decorator.
362
+ This method allows the user to directly associate a custom
363
+ implementation with the target `numpy` function.
364
+
365
+ Parameters
366
+ ----------
367
+ numpy_function : callable
368
+ The `numpy` universal or public function to implement.
369
+
370
+ user_function: callable
371
+ The custom implementation to associate with `numpy_function`.
372
+
373
+ Examples
374
+ --------
375
+ Here is an alternative to the `~implementation` example usage:
376
+
377
+ ```
378
+ def mean(a: Array, **kwargs) -> Array:
379
+ if kwargs:
380
+ msg = "Cannot pass keywords to numpy.mean with Array" raise
381
+ TypeError(msg)
382
+ return numpy.sum(a) / len(a)
383
+
384
+ Array.implement(numpy.mean, mean)
385
+ ```
386
+
387
+ However, a more useful application may be to associate multiple `numpy`
388
+ functions with a single custom implementation:
389
+
390
+ ```
391
+ def trig(f: numpy.ufunc):
392
+ def method(a: Array):
393
+ ... # custom implementation
394
+ return method
395
+
396
+ for f in {numpy.sin, numpy.cos, numpy.tan}:
397
+ Array.implement(f, trig(f))
398
+ ```
399
+
400
+ See Also
401
+ --------
402
+ `~implementation`
403
+ """
404
+ if not callable(numpy_function):
405
+ raise TypeError(
406
+ "The target operation of a custom numpy implementation"
407
+ " must be callable"
408
+ ) from None
409
+ if cls._FUNCTIONS is None:
410
+ raise NotImplementedError(
411
+ f"Type {cls} does not support custom implementations"
412
+ " of numpy functions"
413
+ ) from None
414
+ cls._FUNCTIONS[numpy_function] = user_function
415
+
oprattr/operators.py ADDED
@@ -0,0 +1,41 @@
1
+ """
2
+ A namespace for operators used by this package's `Object` class.
3
+ """
4
+
5
+ import builtins
6
+ import operator
7
+ import typing
8
+
9
+
10
+ class Operator:
11
+ """Base class for enhanced operators."""
12
+ def __init__(self, __f: typing.Callable, operation: str):
13
+ self._f = __f
14
+ self._operation = operation
15
+
16
+ def __repr__(self):
17
+ """Called for repr(self)."""
18
+ return self._operation
19
+
20
+ def __call__(self, *args, **kwds):
21
+ """Called for self(*args, **kwds)."""
22
+ return self._f(*args, **kwds)
23
+
24
+
25
+ eq = Operator(operator.eq, r'a == b')
26
+ ne = Operator(operator.ne, r'a != b')
27
+ lt = Operator(operator.lt, r'a < b')
28
+ le = Operator(operator.le, r'a <= b')
29
+ gt = Operator(operator.gt, r'a > b')
30
+ ge = Operator(operator.ge, r'a >= b')
31
+ abs = Operator(builtins.abs, r'abs(a)')
32
+ pos = Operator(operator.pos, r'+a')
33
+ neg = Operator(operator.neg, r'-a')
34
+ add = Operator(operator.add, r'a + b')
35
+ sub = Operator(operator.sub, r'a - b')
36
+ mul = Operator(operator.mul, r'a * b')
37
+ truediv = Operator(operator.truediv, r'a / b')
38
+ floordiv = Operator(operator.floordiv, r'a // b')
39
+ mod = Operator(operator.mod, r'a % b')
40
+ pow = Operator(builtins.pow, r'a ** b')
41
+
oprattr/py.typed ADDED
File without changes
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: oprattr
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author-email: Matthew Young <myoung.space.science@gmail.com>
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: numpy>=2.2.1
8
+ Requires-Dist: scipy>=1.15.0
9
+ Description-Content-Type: text/markdown
10
+
11
+ # oprattr: Self-Consistent Operations on Object Attributes
@@ -0,0 +1,9 @@
1
+ oprattr/__init__.py,sha256=_mEw0H7v3bpKXFPvM_koOVnvsUQLO3HK6zBmnKCOFQU,5553
2
+ oprattr/_operations.py,sha256=e8kTjc183mKR51EX2Ds22kPx0dQc5uqct2btgHzCgoY,5829
3
+ oprattr/_types.py,sha256=TzTt9GkQ5KZc5kqajdLnn8EcvxzMj3SALSI5iJrB6Vk,3182
4
+ oprattr/mixins.py,sha256=G-4aVbQ1wLDz15KSFL9yWqlU5HXQFwp4g7VoFjl7_24,15471
5
+ oprattr/operators.py,sha256=skqQpIezGSDbsmB2h-UNnxG_7aDGT6PsnvUkondpwOg,1154
6
+ oprattr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ oprattr-0.1.0.dist-info/METADATA,sha256=nwgndllGe5R1KX3UGxZjzvVcpdPG7jg9NQJDO8LtQhI,328
8
+ oprattr-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ oprattr-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any