oprattr 0.5.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.
- oprattr/__init__.py +214 -0
- oprattr/_operations.py +191 -0
- oprattr/abstract.py +60 -0
- oprattr/mixins.py +489 -0
- oprattr/py.typed +0 -0
- oprattr/typeface.py +42 -0
- oprattr/typeface.pyi +5 -0
- oprattr-0.5.0.dist-info/METADATA +13 -0
- oprattr-0.5.0.dist-info/RECORD +11 -0
- oprattr-0.5.0.dist-info/WHEEL +4 -0
- oprattr-0.5.0.dist-info/licenses/LICENSE +32 -0
oprattr/__init__.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import collections.abc
|
|
2
|
+
import functools
|
|
3
|
+
import numbers
|
|
4
|
+
|
|
5
|
+
from numerical import operators
|
|
6
|
+
import numpy
|
|
7
|
+
|
|
8
|
+
from . import abstract
|
|
9
|
+
from . import mixins
|
|
10
|
+
from . import typeface
|
|
11
|
+
from ._operations import (
|
|
12
|
+
unary,
|
|
13
|
+
equality,
|
|
14
|
+
ordering,
|
|
15
|
+
additive,
|
|
16
|
+
multiplicative,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
T = typeface.TypeVar('T')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Operand(abstract.Object[T], mixins.Numpy):
|
|
24
|
+
"""A concrete implementation of a real-valued object."""
|
|
25
|
+
|
|
26
|
+
def __abs__(self):
|
|
27
|
+
"""Called for abs(self)."""
|
|
28
|
+
return unary(operators.abs, self)
|
|
29
|
+
|
|
30
|
+
def __pos__(self):
|
|
31
|
+
"""Called for +self."""
|
|
32
|
+
return unary(operators.pos, self)
|
|
33
|
+
|
|
34
|
+
def __neg__(self):
|
|
35
|
+
"""Called for -self."""
|
|
36
|
+
return unary(operators.neg, self)
|
|
37
|
+
|
|
38
|
+
def __eq__(self, other):
|
|
39
|
+
"""Called for self == other."""
|
|
40
|
+
return equality(operators.eq, self, other)
|
|
41
|
+
|
|
42
|
+
def __ne__(self, other):
|
|
43
|
+
"""Called for self != other."""
|
|
44
|
+
return equality(operators.ne, self, other)
|
|
45
|
+
|
|
46
|
+
def __lt__(self, other):
|
|
47
|
+
"""Called for self < other."""
|
|
48
|
+
return ordering(operators.lt, self, other)
|
|
49
|
+
|
|
50
|
+
def __le__(self, other):
|
|
51
|
+
"""Called for self <= other."""
|
|
52
|
+
return ordering(operators.le, self, other)
|
|
53
|
+
|
|
54
|
+
def __gt__(self, other):
|
|
55
|
+
"""Called for self > other."""
|
|
56
|
+
return ordering(operators.gt, self, other)
|
|
57
|
+
|
|
58
|
+
def __ge__(self, other):
|
|
59
|
+
"""Called for self >= other."""
|
|
60
|
+
return ordering(operators.ge, self, other)
|
|
61
|
+
|
|
62
|
+
def __add__(self, other):
|
|
63
|
+
"""Called for self + other."""
|
|
64
|
+
return additive(operators.add, self, other)
|
|
65
|
+
|
|
66
|
+
def __radd__(self, other):
|
|
67
|
+
"""Called for other + self."""
|
|
68
|
+
return additive(operators.add, other, self)
|
|
69
|
+
|
|
70
|
+
def __sub__(self, other):
|
|
71
|
+
"""Called for self - other."""
|
|
72
|
+
return additive(operators.sub, self, other)
|
|
73
|
+
|
|
74
|
+
def __rsub__(self, other):
|
|
75
|
+
"""Called for other - self."""
|
|
76
|
+
return additive(operators.sub, other, self)
|
|
77
|
+
|
|
78
|
+
def __mul__(self, other):
|
|
79
|
+
"""Called for self * other."""
|
|
80
|
+
return multiplicative(operators.mul, self, other)
|
|
81
|
+
|
|
82
|
+
def __rmul__(self, other):
|
|
83
|
+
"""Called for other * self."""
|
|
84
|
+
return multiplicative(operators.mul, other, self)
|
|
85
|
+
|
|
86
|
+
def __truediv__(self, other):
|
|
87
|
+
"""Called for self / other."""
|
|
88
|
+
return multiplicative(operators.truediv, self, other)
|
|
89
|
+
|
|
90
|
+
def __rtruediv__(self, other):
|
|
91
|
+
"""Called for other / self."""
|
|
92
|
+
return multiplicative(operators.truediv, other, self)
|
|
93
|
+
|
|
94
|
+
def __floordiv__(self, other):
|
|
95
|
+
"""Called for self // other."""
|
|
96
|
+
return multiplicative(operators.floordiv, self, other)
|
|
97
|
+
|
|
98
|
+
def __rfloordiv__(self, other):
|
|
99
|
+
"""Called for other // self."""
|
|
100
|
+
return multiplicative(operators.floordiv, other, self)
|
|
101
|
+
|
|
102
|
+
def __mod__(self, other):
|
|
103
|
+
"""Called for self % other."""
|
|
104
|
+
return multiplicative(operators.mod, self, other)
|
|
105
|
+
|
|
106
|
+
def __rmod__(self, other):
|
|
107
|
+
"""Called for other % self."""
|
|
108
|
+
return multiplicative(operators.mod, other, self)
|
|
109
|
+
|
|
110
|
+
def __pow__(self, other):
|
|
111
|
+
"""Called for self ** other."""
|
|
112
|
+
if isinstance(other, numbers.Real):
|
|
113
|
+
return multiplicative(operators.pow, self, other)
|
|
114
|
+
return NotImplemented
|
|
115
|
+
|
|
116
|
+
def __rpow__(self, other):
|
|
117
|
+
"""Called for other ** self."""
|
|
118
|
+
return NotImplemented
|
|
119
|
+
|
|
120
|
+
def __array__(self, *args, **kwargs):
|
|
121
|
+
"""Called for numpy.array(self)."""
|
|
122
|
+
return numpy.array(self._data, *args, **kwargs)
|
|
123
|
+
|
|
124
|
+
def _apply_ufunc(self, ufunc, method, *args, **kwargs):
|
|
125
|
+
if ufunc in (numpy.equal, numpy.not_equal):
|
|
126
|
+
# NOTE: We are probably here because the left operand is a
|
|
127
|
+
# `numpy.ndarray`, which would otherwise take control and return the
|
|
128
|
+
# pure `numpy` result.
|
|
129
|
+
f = getattr(ufunc, method)
|
|
130
|
+
return equality(f, *args)
|
|
131
|
+
return super()._apply_ufunc(ufunc, method, *args, **kwargs)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@Operand.implementation(numpy.array_equal)
|
|
135
|
+
def array_equal(
|
|
136
|
+
x: numpy.typing.ArrayLike,
|
|
137
|
+
y: numpy.typing.ArrayLike,
|
|
138
|
+
**kwargs
|
|
139
|
+
) -> bool:
|
|
140
|
+
"""Called for numpy.array_equal(x, y)"""
|
|
141
|
+
return numpy.array_equal(numpy.array(x), numpy.array(y), **kwargs)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@Operand.implementation(numpy.gradient)
|
|
145
|
+
def gradient(x: Operand[T], *args, **kwargs):
|
|
146
|
+
"""Called for numpy.gradient(x)."""
|
|
147
|
+
data = numpy.gradient(x._data, *args, **kwargs)
|
|
148
|
+
meta = {}
|
|
149
|
+
for key, value in x._meta.items():
|
|
150
|
+
try:
|
|
151
|
+
v = numpy.gradient(value, **kwargs)
|
|
152
|
+
except TypeError as exc:
|
|
153
|
+
raise TypeError(
|
|
154
|
+
"Cannot compute numpy.gradient(x)"
|
|
155
|
+
f" because metadata attribute {key!r}"
|
|
156
|
+
" does not support this operation"
|
|
157
|
+
) from exc
|
|
158
|
+
else:
|
|
159
|
+
meta[key] = v
|
|
160
|
+
if isinstance(data, (list, tuple)):
|
|
161
|
+
r = [type(x)(array, **meta) for array in data]
|
|
162
|
+
if isinstance(data, tuple):
|
|
163
|
+
return tuple(r)
|
|
164
|
+
return r
|
|
165
|
+
return type(x)(data, **meta)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def wrapnumpy(f: collections.abc.Callable):
|
|
169
|
+
"""Implement a numpy function for objects with metadata."""
|
|
170
|
+
@functools.wraps(f)
|
|
171
|
+
def method(x: Operand[T], **kwargs):
|
|
172
|
+
"""Apply a numpy function to x."""
|
|
173
|
+
data = f(x._data, **kwargs)
|
|
174
|
+
meta = {}
|
|
175
|
+
for key, value in x._meta.items():
|
|
176
|
+
try:
|
|
177
|
+
v = f(value, **kwargs)
|
|
178
|
+
except TypeError as exc:
|
|
179
|
+
raise TypeError(
|
|
180
|
+
f"Cannot compute numpy.{f.__qualname__}(x)"
|
|
181
|
+
f" because metadata attribute {key!r}"
|
|
182
|
+
" does not support this operation"
|
|
183
|
+
) from exc
|
|
184
|
+
else:
|
|
185
|
+
meta[key] = v
|
|
186
|
+
return type(x)(data, **meta)
|
|
187
|
+
return method
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
_OPERAND_UFUNCS = (
|
|
191
|
+
numpy.sqrt,
|
|
192
|
+
numpy.sin,
|
|
193
|
+
numpy.cos,
|
|
194
|
+
numpy.tan,
|
|
195
|
+
numpy.log,
|
|
196
|
+
numpy.log2,
|
|
197
|
+
numpy.log10,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
_OPERAND_FUNCTIONS = (
|
|
202
|
+
numpy.squeeze,
|
|
203
|
+
numpy.mean,
|
|
204
|
+
numpy.sum,
|
|
205
|
+
numpy.cumsum,
|
|
206
|
+
numpy.transpose,
|
|
207
|
+
numpy.trapezoid,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
for f in _OPERAND_UFUNCS + _OPERAND_FUNCTIONS:
|
|
212
|
+
Operand.implement(f, wrapnumpy(f))
|
|
213
|
+
|
|
214
|
+
|
oprattr/_operations.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from numerical import operators
|
|
2
|
+
|
|
3
|
+
from .abstract import (
|
|
4
|
+
Quantity,
|
|
5
|
+
Object,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MetadataError(TypeError):
|
|
10
|
+
"""A metadata-related TypeError occurred."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
f: operators.Operator,
|
|
15
|
+
*args,
|
|
16
|
+
error: str | None = None,
|
|
17
|
+
key: str | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
super().__init__(*args)
|
|
20
|
+
self._f = f
|
|
21
|
+
self._error = error
|
|
22
|
+
self._key = key
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
"""Called when handling the exception."""
|
|
26
|
+
types = [type(arg) for arg in self.args]
|
|
27
|
+
return _build_error_message(
|
|
28
|
+
self._f,
|
|
29
|
+
*types,
|
|
30
|
+
error=self._error,
|
|
31
|
+
key=self._key,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_error_message(
|
|
36
|
+
f: operators.Operator,
|
|
37
|
+
*types: type,
|
|
38
|
+
error: str | None = None,
|
|
39
|
+
key: str | None = None,
|
|
40
|
+
) -> str:
|
|
41
|
+
"""Helper for `_raise_metadata_exception`.
|
|
42
|
+
|
|
43
|
+
This function should avoid raising an exception if at all possible, and
|
|
44
|
+
instead return the default error message, since it is already being called
|
|
45
|
+
as the result of an error elsewhere.
|
|
46
|
+
"""
|
|
47
|
+
errmsg = f"Cannot compute {f}"
|
|
48
|
+
errstr = error.lower() if isinstance(error, str) else ''
|
|
49
|
+
if errstr == 'unequal':
|
|
50
|
+
return f"{errmsg} between objects with unequal metadata"
|
|
51
|
+
if errstr in {'non-empty', 'nonempty'}:
|
|
52
|
+
if len(types) == 2:
|
|
53
|
+
a, b = types
|
|
54
|
+
endstr = "because {} has metadata"
|
|
55
|
+
if issubclass(a, Object):
|
|
56
|
+
return f"{errmsg} between {a} and {b} {endstr.format(str(a))}"
|
|
57
|
+
if issubclass(b, Object):
|
|
58
|
+
return f"{errmsg} between {a} and {b} {endstr.format(str(b))}"
|
|
59
|
+
if errstr == 'type':
|
|
60
|
+
if key is None:
|
|
61
|
+
keystr = "a metadata attribute"
|
|
62
|
+
else:
|
|
63
|
+
keystr = f"metadata attribute {key!r}"
|
|
64
|
+
midstr = f"because {keystr}"
|
|
65
|
+
endstr = "does not support this operation"
|
|
66
|
+
if len(types) == 1:
|
|
67
|
+
return f"{errmsg} of {types[0]} {midstr} {endstr}"
|
|
68
|
+
if len(types) == 2:
|
|
69
|
+
a, b = types
|
|
70
|
+
return f"{errmsg} between {a} and {b} {midstr} {endstr}"
|
|
71
|
+
return errmsg
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def unary(f: operators.Operator, a):
|
|
75
|
+
"""Compute the unary operation f(a)."""
|
|
76
|
+
if isinstance(a, Quantity):
|
|
77
|
+
meta = {}
|
|
78
|
+
for key, value in a._meta.items():
|
|
79
|
+
try:
|
|
80
|
+
v = f(value)
|
|
81
|
+
except TypeError as exc:
|
|
82
|
+
raise MetadataError(f, a, error='type', key=key) from exc
|
|
83
|
+
else:
|
|
84
|
+
meta[key] = v
|
|
85
|
+
return type(a)(f(a._data), **meta)
|
|
86
|
+
return f(a)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def equality(f: operators.Operator, a, b):
|
|
90
|
+
"""Compute the equality operation f(a, b)."""
|
|
91
|
+
x = a._data if isinstance(a, Quantity) else a
|
|
92
|
+
y = b._data if isinstance(b, Quantity) else b
|
|
93
|
+
fxy = f(x, y)
|
|
94
|
+
try:
|
|
95
|
+
iter(fxy)
|
|
96
|
+
except TypeError:
|
|
97
|
+
r = bool(fxy)
|
|
98
|
+
else:
|
|
99
|
+
r = all(fxy)
|
|
100
|
+
isne = f(1, 2)
|
|
101
|
+
if isinstance(a, Quantity) and isinstance(b, Quantity):
|
|
102
|
+
if a._meta != b._meta:
|
|
103
|
+
return isne
|
|
104
|
+
return r
|
|
105
|
+
if isinstance(a, Quantity):
|
|
106
|
+
if not a._meta:
|
|
107
|
+
return r
|
|
108
|
+
return isne
|
|
109
|
+
if isinstance(b, Quantity):
|
|
110
|
+
if not b._meta:
|
|
111
|
+
return r
|
|
112
|
+
return isne
|
|
113
|
+
return r
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def ordering(f: operators.Operator, a, b):
|
|
117
|
+
"""Compute the ordering operation f(a, b)."""
|
|
118
|
+
if isinstance(a, Quantity) and isinstance(b, Quantity):
|
|
119
|
+
if a._meta == b._meta:
|
|
120
|
+
return f(a._data, b._data)
|
|
121
|
+
raise MetadataError(f, a, b, error='unequal') from None
|
|
122
|
+
if isinstance(a, Quantity):
|
|
123
|
+
if not a._meta:
|
|
124
|
+
return f(a._data, b)
|
|
125
|
+
raise MetadataError(f, a, b, error='non-empty') from None
|
|
126
|
+
if isinstance(b, Quantity):
|
|
127
|
+
if not b._meta:
|
|
128
|
+
return f(a, b._data)
|
|
129
|
+
raise MetadataError(f, a, b, error='non-empty') from None
|
|
130
|
+
return f(a, b)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def additive(f: operators.Operator, a, b):
|
|
134
|
+
"""Compute the additive operation f(a, b)."""
|
|
135
|
+
if isinstance(a, Quantity) and isinstance(b, Quantity):
|
|
136
|
+
if a._meta == b._meta:
|
|
137
|
+
return type(a)(f(a._data, b._data), **a._meta)
|
|
138
|
+
raise MetadataError(f, a, b, error='unequal') from None
|
|
139
|
+
if isinstance(a, Quantity):
|
|
140
|
+
if not a._meta:
|
|
141
|
+
return type(a)(f(a._data, b))
|
|
142
|
+
raise MetadataError(f, a, b, error='non-empty') from None
|
|
143
|
+
if isinstance(b, Quantity):
|
|
144
|
+
if not b._meta:
|
|
145
|
+
return type(b)(f(a, b._data))
|
|
146
|
+
raise MetadataError(f, a, b, error='non-empty') from None
|
|
147
|
+
return f(a, b)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def multiplicative(f: operators.Operator, a, b):
|
|
151
|
+
"""Compute the multiplicative operation f(a, b)."""
|
|
152
|
+
if isinstance(a, Quantity) and isinstance(b, Quantity):
|
|
153
|
+
keys = set(a._meta) & set(b._meta)
|
|
154
|
+
meta = {}
|
|
155
|
+
for key in keys:
|
|
156
|
+
try:
|
|
157
|
+
v = f(a._meta[key], b._meta[key])
|
|
158
|
+
except TypeError as exc:
|
|
159
|
+
raise MetadataError(f, a, b, error='type', key=key) from exc
|
|
160
|
+
else:
|
|
161
|
+
meta[key] = v
|
|
162
|
+
for key, value in a._meta.items():
|
|
163
|
+
if key not in keys:
|
|
164
|
+
meta[key] = value
|
|
165
|
+
for key, value in b._meta.items():
|
|
166
|
+
if key not in keys:
|
|
167
|
+
meta[key] = value
|
|
168
|
+
return type(a)(f(a._data, b._data), **meta)
|
|
169
|
+
if isinstance(a, Quantity):
|
|
170
|
+
meta = {}
|
|
171
|
+
for key, value in a._meta.items():
|
|
172
|
+
try:
|
|
173
|
+
v = f(value, b)
|
|
174
|
+
except TypeError as exc:
|
|
175
|
+
raise MetadataError(f, a, b, error='type', key=key) from exc
|
|
176
|
+
else:
|
|
177
|
+
meta[key] = v
|
|
178
|
+
return type(a)(f(a._data, b), **meta)
|
|
179
|
+
if isinstance(b, Quantity):
|
|
180
|
+
meta = {}
|
|
181
|
+
for key, value in b._meta.items():
|
|
182
|
+
try:
|
|
183
|
+
v = f(a, value)
|
|
184
|
+
except TypeError as exc:
|
|
185
|
+
raise MetadataError(f, a, b, error='type', key=key) from exc
|
|
186
|
+
else:
|
|
187
|
+
meta[key] = v
|
|
188
|
+
return type(b)(f(a, b._data), **meta)
|
|
189
|
+
return f(a, b)
|
|
190
|
+
|
|
191
|
+
|
oprattr/abstract.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import collections.abc
|
|
2
|
+
import numbers
|
|
3
|
+
|
|
4
|
+
import numerical
|
|
5
|
+
import numpy.typing
|
|
6
|
+
|
|
7
|
+
from . import typeface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DataType = typeface.TypeVar(
|
|
11
|
+
'DataType',
|
|
12
|
+
int,
|
|
13
|
+
float,
|
|
14
|
+
numbers.Number,
|
|
15
|
+
numpy.number,
|
|
16
|
+
numpy.typing.ArrayLike,
|
|
17
|
+
numpy.typing.NDArray,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@typeface.runtime_checkable
|
|
22
|
+
class Quantity(numerical.Quantity[DataType], typeface.Protocol):
|
|
23
|
+
"""Protocol for numerical objects with metadata."""
|
|
24
|
+
|
|
25
|
+
_meta: collections.abc.Mapping[str, typeface.Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Object(numerical.Real, typeface.Generic[DataType]):
|
|
29
|
+
"""A real-valued object with metadata attributes."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
__data: DataType,
|
|
34
|
+
**metadata,
|
|
35
|
+
) -> None:
|
|
36
|
+
if not isinstance(__data, numerical.Real):
|
|
37
|
+
raise TypeError("Data input to Object must be real-valued")
|
|
38
|
+
self._data = __data
|
|
39
|
+
self._meta = metadata
|
|
40
|
+
|
|
41
|
+
def __str__(self):
|
|
42
|
+
"""Called for str(self)."""
|
|
43
|
+
try:
|
|
44
|
+
datastr = numpy.array2string(
|
|
45
|
+
self._data,
|
|
46
|
+
separator=", ",
|
|
47
|
+
threshold=6,
|
|
48
|
+
edgeitems=2,
|
|
49
|
+
prefix=f"{self.__class__.__qualname__}(",
|
|
50
|
+
suffix=")"
|
|
51
|
+
)
|
|
52
|
+
except Exception:
|
|
53
|
+
datastr = str(self._data)
|
|
54
|
+
metastr = ", ".join(f"{k}={str(v)!r}" for k, v in self._meta.items())
|
|
55
|
+
return f"{datastr}, {metastr}"
|
|
56
|
+
|
|
57
|
+
def __repr__(self):
|
|
58
|
+
"""Called for repr(self)."""
|
|
59
|
+
return f"{self.__class__.__qualname__}({self})"
|
|
60
|
+
|
oprattr/mixins.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
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
|
+
|
|
13
|
+
class Real:
|
|
14
|
+
"""Mixin for adding basic real-valued operator support."""
|
|
15
|
+
|
|
16
|
+
def __abs__(self):
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def __pos__(self):
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def __neg__(self):
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def __eq__(self, other):
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
def __ne__(self, other):
|
|
29
|
+
return not (self == other)
|
|
30
|
+
|
|
31
|
+
def __lt__(self, other):
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def __le__(self, other):
|
|
35
|
+
return (self < other) and (self == other)
|
|
36
|
+
|
|
37
|
+
def __gt__(self, other):
|
|
38
|
+
return not (self <= other)
|
|
39
|
+
|
|
40
|
+
def __ge__(self, other):
|
|
41
|
+
return not (self < other)
|
|
42
|
+
|
|
43
|
+
def __add__(self, other):
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __radd__(self, other):
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def __sub__(self, other):
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __rsub__(self, other):
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __mul__(self, other):
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __rmul__(self, other):
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __truediv__(self, other):
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def __rtruediv__(self, other):
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def __floordiv__(self, other):
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def __rfloordiv__(self, other):
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def __mod__(self, other):
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
def __rmod__(self, other):
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def __pow__(self, other):
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __rpow__(self, other):
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
UserFunction = collections.abc.Callable[..., T]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Numpy:
|
|
90
|
+
"""Mixin for adding support for `numpy` functions to numeric objects.
|
|
91
|
+
|
|
92
|
+
Classes that inherit from this class may implement support for `numpy`
|
|
93
|
+
universal functions ("ufuncs"; e.g., `numpy.sqrt`) by overloading
|
|
94
|
+
`_apply_ufunc`, and may implement support for `numpy` public functions
|
|
95
|
+
(e.g., `numpy.squeeze`) by overloading `_apply_function` and registering
|
|
96
|
+
individual function implementations via `implementation`.
|
|
97
|
+
|
|
98
|
+
It is important to note that the use cases of this class extend beyond
|
|
99
|
+
array-like objects. Both single- and multi-valued objects can benefit from
|
|
100
|
+
implementing support for `numpy` universal and public functions. For
|
|
101
|
+
example, it is possible to apply `numpy.sqrt` to both a real number and an
|
|
102
|
+
array
|
|
103
|
+
|
|
104
|
+
>>> numpy.sqrt(4)
|
|
105
|
+
2.0
|
|
106
|
+
>>> numpy.sqrt([4, 9])
|
|
107
|
+
array([2., 3.])
|
|
108
|
+
|
|
109
|
+
Even the trivial application of `numpy.mean` to a real number is defined:
|
|
110
|
+
|
|
111
|
+
>>> numpy.mean(2.5)
|
|
112
|
+
2.5
|
|
113
|
+
|
|
114
|
+
Notes
|
|
115
|
+
-----
|
|
116
|
+
- This class does not inherit from `numpy.lib.mixins.NDArrayOperatorsMixin`,
|
|
117
|
+
which implements most of the built-in Python numeric operators via
|
|
118
|
+
`__array_ufunc__`, because it assumes that subclasses independently
|
|
119
|
+
implement those methods.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init_subclass__(cls):
|
|
123
|
+
cls._UFUNC_TYPES |= {cls}
|
|
124
|
+
cls._FUNCTION_TYPES |= {cls}
|
|
125
|
+
cls._FUNCTIONS = {}
|
|
126
|
+
|
|
127
|
+
_UFUNC_TYPES = {
|
|
128
|
+
numpy.ndarray,
|
|
129
|
+
numbers.Number,
|
|
130
|
+
list,
|
|
131
|
+
abstract.Quantity,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def __array_ufunc__(self, ufunc, method, *args, **kwargs):
|
|
135
|
+
"""Provide support for `numpy` universal functions.
|
|
136
|
+
|
|
137
|
+
See https://numpy.org/doc/stable/reference/arrays.classes.html for more
|
|
138
|
+
information on use of this special method.
|
|
139
|
+
|
|
140
|
+
Notes
|
|
141
|
+
-----
|
|
142
|
+
- This method first ensures that the input types (as well as the type of
|
|
143
|
+
`out`, if passed via keyword) are supported types. It then checks for
|
|
144
|
+
a custom implementation of `ufunc`. If there is a custom
|
|
145
|
+
implementation, this method applies it and returns the result. If
|
|
146
|
+
there is no custom implementation, this method passes control to
|
|
147
|
+
`_apply_ufunc`, to allow subclass customization.
|
|
148
|
+
- See `implementation` for additional guidance on custom
|
|
149
|
+
implementations.
|
|
150
|
+
|
|
151
|
+
See Also
|
|
152
|
+
--------
|
|
153
|
+
`implementation`
|
|
154
|
+
Class method for registering custom ufunc implementations.
|
|
155
|
+
|
|
156
|
+
`_apply_ufunc`
|
|
157
|
+
Instance method that allows custom handling of ufuncs corresponding
|
|
158
|
+
to standard Python numerical operators.
|
|
159
|
+
"""
|
|
160
|
+
out = kwargs.get('out', ())
|
|
161
|
+
accepted = tuple(self._UFUNC_TYPES)
|
|
162
|
+
if not all(isinstance(x, accepted) for x in args + out):
|
|
163
|
+
return NotImplemented
|
|
164
|
+
if out:
|
|
165
|
+
kwargs['out'] = tuple(
|
|
166
|
+
x._data if isinstance(x, abstract.Quantity)
|
|
167
|
+
else x for x in out
|
|
168
|
+
)
|
|
169
|
+
if self._implements(ufunc):
|
|
170
|
+
operator = self._FUNCTIONS[ufunc]
|
|
171
|
+
return operator(*args, **kwargs)
|
|
172
|
+
return self._apply_ufunc(ufunc, method, *args, **kwargs)
|
|
173
|
+
|
|
174
|
+
def _apply_ufunc(self, ufunc, method, *args, **kwargs):
|
|
175
|
+
"""Apply a `numpy` universal function (a.k.a "ufunc") to data.
|
|
176
|
+
|
|
177
|
+
Notes
|
|
178
|
+
-----
|
|
179
|
+
- Subclasses that wish to customize support for ufuncs should overload
|
|
180
|
+
this method instead of `__array_ufunc__`.
|
|
181
|
+
- Subclasses should prefer to define custom implementations of specific
|
|
182
|
+
universal functions and register each via `implementation`, rather
|
|
183
|
+
than implementing function-specific logic in this method, since
|
|
184
|
+
`__array_ufunc__` will check for a custom implementation of a given
|
|
185
|
+
function before calling this method.
|
|
186
|
+
- The default implementation of this method applies the given ufunc to
|
|
187
|
+
real-valued data and directly returns the `numpy` result, without
|
|
188
|
+
attempting to create a new instance of the custom subclass.
|
|
189
|
+
|
|
190
|
+
See Also
|
|
191
|
+
--------
|
|
192
|
+
`implementation`
|
|
193
|
+
Class method for registering custom ufunc implementations.
|
|
194
|
+
|
|
195
|
+
`__array_ufunc__`
|
|
196
|
+
The entry point for `numpy` universal functions.
|
|
197
|
+
"""
|
|
198
|
+
operator = getattr(ufunc, method)
|
|
199
|
+
values = self._get_numpy_args(args)
|
|
200
|
+
try:
|
|
201
|
+
data = operator(*values, **kwargs)
|
|
202
|
+
except TypeError as err:
|
|
203
|
+
raise TypeError(
|
|
204
|
+
f"Unable to apply {ufunc} to {args}"
|
|
205
|
+
) from err
|
|
206
|
+
if method != 'at':
|
|
207
|
+
return data
|
|
208
|
+
|
|
209
|
+
_FUNCTION_TYPES = {
|
|
210
|
+
numpy.ndarray,
|
|
211
|
+
abstract.Object,
|
|
212
|
+
} | set(numpy.ScalarType)
|
|
213
|
+
|
|
214
|
+
def __array_function__(self, func, types, args, kwargs):
|
|
215
|
+
"""Provide support for functions in the `numpy` public API.
|
|
216
|
+
|
|
217
|
+
See https://numpy.org/doc/stable/reference/arrays.classes.html for more
|
|
218
|
+
information of use of this special method. The implementation shown here
|
|
219
|
+
is a combination of the example on that page and code from the
|
|
220
|
+
definition of `EncapsulateNDArray.__array_function__` in
|
|
221
|
+
https://github.com/dask/dask/blob/main/dask/array/tests/test_dispatch.py
|
|
222
|
+
|
|
223
|
+
Notes
|
|
224
|
+
-----
|
|
225
|
+
- This method first checks that all `types` are in
|
|
226
|
+
`self._FUNCTION_TYPES`, thereby allowing subclasses that don't
|
|
227
|
+
override `__array_function__` to handle objects of this type. It then
|
|
228
|
+
checks for a custom implementation of `func`. If there is a custom
|
|
229
|
+
implementation, this method applies it and returns the result. If
|
|
230
|
+
there is no custom implementation, this method passes control to
|
|
231
|
+
`_apply_function`, to allow subclass customization.
|
|
232
|
+
- See `implementation` for additional guidance on custom
|
|
233
|
+
implementations.
|
|
234
|
+
|
|
235
|
+
See Also
|
|
236
|
+
--------
|
|
237
|
+
`implementation`
|
|
238
|
+
Class method for registering custom function implementations.
|
|
239
|
+
|
|
240
|
+
`_apply_function`
|
|
241
|
+
Instance method that allows custom handling of `numpy` public
|
|
242
|
+
functions when there is no registered custom implementation.
|
|
243
|
+
"""
|
|
244
|
+
accepted = tuple(self._FUNCTION_TYPES)
|
|
245
|
+
if not all(issubclass(ti, accepted) for ti in types):
|
|
246
|
+
return NotImplemented
|
|
247
|
+
if self._implements(func):
|
|
248
|
+
return self._FUNCTIONS[func](*args, **kwargs)
|
|
249
|
+
return self._apply_function(func, types, args, kwargs)
|
|
250
|
+
|
|
251
|
+
def _apply_function(self, func, types, args, kwargs):
|
|
252
|
+
"""Apply a function in the `numpy` public API.
|
|
253
|
+
|
|
254
|
+
Notes
|
|
255
|
+
-----
|
|
256
|
+
- Subclasses that wish to customize support for public functions should
|
|
257
|
+
overload this method instead of `__array_function__`.
|
|
258
|
+
- Subclasses should prefer to define custom implementations of specific
|
|
259
|
+
public functions and register each via `implementation`, rather than
|
|
260
|
+
implementing function-specific logic in this method, since
|
|
261
|
+
`__array_function__` will check for a custom implementation of a given
|
|
262
|
+
function before calling this method.
|
|
263
|
+
- The default implementation calls `_get_numpy_array` for access to
|
|
264
|
+
real-valued data via an instance of `numpy.ndarray`, `_get_numpy_args`
|
|
265
|
+
to convert `args` to appropriate operands, and `_get_numpy_types` to
|
|
266
|
+
extract appropriate operand types. Subclasses may choose to overload
|
|
267
|
+
any of those individual methods instead of overloading this method.
|
|
268
|
+
|
|
269
|
+
See Also
|
|
270
|
+
--------
|
|
271
|
+
`implementation`
|
|
272
|
+
Class method for registering custom ufunc implementations.
|
|
273
|
+
|
|
274
|
+
`__array_function__`
|
|
275
|
+
The entry point for `numpy` public functions.
|
|
276
|
+
"""
|
|
277
|
+
array = self._get_numpy_array()
|
|
278
|
+
if array is None:
|
|
279
|
+
return NotImplemented
|
|
280
|
+
if not isinstance(array, numpy.ndarray):
|
|
281
|
+
raise TypeError(
|
|
282
|
+
f"{self.__class__.__qualname__}._get_numpy_array"
|
|
283
|
+
" did not return a numpy.ndarray"
|
|
284
|
+
) from None
|
|
285
|
+
args = self._get_numpy_args(args)
|
|
286
|
+
types = self._get_numpy_types(types)
|
|
287
|
+
return array.__array_function__(func, types, args, kwargs)
|
|
288
|
+
|
|
289
|
+
def _get_numpy_array(self) -> numpy.typing.NDArray | None:
|
|
290
|
+
"""Convert the data interface to an array for `numpy` mixin methods.
|
|
291
|
+
|
|
292
|
+
Notes
|
|
293
|
+
-----
|
|
294
|
+
- This method allows subclass implementations to control how they
|
|
295
|
+
convert their data interface to a `numpy.ndarray` for use with `numpy`
|
|
296
|
+
public functions.
|
|
297
|
+
- Returning `None` from this method will cause `_apply_function` to
|
|
298
|
+
return `NotImplemented`.
|
|
299
|
+
- The default implementation unconditionally returns `None`.
|
|
300
|
+
"""
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
def _get_numpy_args(self, args):
|
|
304
|
+
"""Convert `args` to operands of a `numpy` function.
|
|
305
|
+
|
|
306
|
+
This method will call `~_get_arg_data` on each member of `args` in order
|
|
307
|
+
to build a `tuple` of suitable operands. Subclasses may overload
|
|
308
|
+
`~_get_arg_data` to customize access to their data attribute.
|
|
309
|
+
"""
|
|
310
|
+
return tuple(self._get_arg_data(arg) for arg in args)
|
|
311
|
+
|
|
312
|
+
def _get_arg_data(self, arg):
|
|
313
|
+
"""Convert `arg` to an operand of a `numpy` function.
|
|
314
|
+
|
|
315
|
+
See Also
|
|
316
|
+
--------
|
|
317
|
+
`~_get_numpy_args`
|
|
318
|
+
The method that calls this method in a loop.
|
|
319
|
+
|
|
320
|
+
Notes
|
|
321
|
+
-----
|
|
322
|
+
- This method allows a subclass to customize how `numpy` functions
|
|
323
|
+
access its data attribute.
|
|
324
|
+
- The default implementation will return the `data` attribute of a of
|
|
325
|
+
`arg` if `arg` is an instance of the base object class; otherwise, it
|
|
326
|
+
will return the unmodified argument.
|
|
327
|
+
"""
|
|
328
|
+
if isinstance(arg, abstract.Quantity):
|
|
329
|
+
return arg._data
|
|
330
|
+
return arg
|
|
331
|
+
|
|
332
|
+
def _get_numpy_types(self, types):
|
|
333
|
+
"""Extract appropriate types for a `numpy` function.
|
|
334
|
+
|
|
335
|
+
Notes
|
|
336
|
+
-----
|
|
337
|
+
- This method allows subclasses to restrict the object types that they
|
|
338
|
+
pass to `numpy` public functions via `_apply_function`.
|
|
339
|
+
- The default implementation returns a tuple that contains all types
|
|
340
|
+
except for subtypes of `~_types.Quantity`.
|
|
341
|
+
"""
|
|
342
|
+
return tuple(
|
|
343
|
+
ti for ti in types
|
|
344
|
+
if not issubclass(ti, abstract.Object)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
@classmethod
|
|
348
|
+
def _implements(cls, operation: collections.abc.Callable):
|
|
349
|
+
"""True if this class defines a custom implementation for `operation`.
|
|
350
|
+
|
|
351
|
+
This is a helper methods that gracefully handles the case in which a
|
|
352
|
+
subclass does not support custom operator implementations.
|
|
353
|
+
"""
|
|
354
|
+
try:
|
|
355
|
+
result = operation in cls._FUNCTIONS
|
|
356
|
+
except TypeError:
|
|
357
|
+
return False
|
|
358
|
+
return result
|
|
359
|
+
|
|
360
|
+
_FUNCTIONS: dict[str, collections.abc.Callable]=None
|
|
361
|
+
"""Internal collection of custom `numpy` function implementations."""
|
|
362
|
+
|
|
363
|
+
@classmethod
|
|
364
|
+
def implementation(cls, numpy_function: collections.abc.Callable, /):
|
|
365
|
+
"""Register a custom implementation of this `numpy` function.
|
|
366
|
+
|
|
367
|
+
Parameters
|
|
368
|
+
----------
|
|
369
|
+
numpy_function : callable
|
|
370
|
+
The `numpy` universal or public function to implement.
|
|
371
|
+
|
|
372
|
+
Notes
|
|
373
|
+
-----
|
|
374
|
+
- Users may register `numpy` universal functions (a.k.a. ufuncs;
|
|
375
|
+
https://numpy.org/doc/stable/reference/ufuncs.html) as well as
|
|
376
|
+
functions in the public `numpy` API (e.g., `numpy.mean`). This may be
|
|
377
|
+
important if, for example, a class needs to implement a custom version
|
|
378
|
+
of `numpy.sqrt`, which is a ufunc.
|
|
379
|
+
- See https://numpy.org/doc/stable/reference/arrays.classes.html for the
|
|
380
|
+
suggestion on which this method is based.
|
|
381
|
+
|
|
382
|
+
Examples
|
|
383
|
+
--------
|
|
384
|
+
Overload `numpy.mean` for an existing class called `Array` with a
|
|
385
|
+
version that accepts no keyword arguments:
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
@Array.implementation(numpy.mean)
|
|
389
|
+
def mean(a: Array, **kwargs) -> Array:
|
|
390
|
+
if kwargs:
|
|
391
|
+
msg = "Cannot pass keywords to numpy.mean with Array" raise
|
|
392
|
+
TypeError(msg)
|
|
393
|
+
return numpy.sum(a) / len(a)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
This will compute the mean of the underlying data when called with no
|
|
397
|
+
arguments, but will raise an exception when called with arguments:
|
|
398
|
+
|
|
399
|
+
>>> v = Array([[1, 2], [3, 4]])
|
|
400
|
+
>>> numpy.mean(v)
|
|
401
|
+
5.0
|
|
402
|
+
>>> numpy.mean(v, axis=0)
|
|
403
|
+
...
|
|
404
|
+
TypeError: Cannot pass keywords to numpy.mean with Array
|
|
405
|
+
|
|
406
|
+
See Also
|
|
407
|
+
--------
|
|
408
|
+
`~implements`
|
|
409
|
+
"""
|
|
410
|
+
if not callable(numpy_function):
|
|
411
|
+
raise TypeError(
|
|
412
|
+
"The target operation of a custom numpy implementation"
|
|
413
|
+
" must be callable"
|
|
414
|
+
) from None
|
|
415
|
+
def decorator(user_function: UserFunction):
|
|
416
|
+
if cls._FUNCTIONS is None:
|
|
417
|
+
raise NotImplementedError(
|
|
418
|
+
f"Type {cls} does not support custom implementations"
|
|
419
|
+
" of numpy functions"
|
|
420
|
+
) from None
|
|
421
|
+
cls._FUNCTIONS[numpy_function] = user_function
|
|
422
|
+
return user_function
|
|
423
|
+
return decorator
|
|
424
|
+
|
|
425
|
+
@classmethod
|
|
426
|
+
def implement(
|
|
427
|
+
cls,
|
|
428
|
+
numpy_function: collections.abc.Callable,
|
|
429
|
+
user_function: UserFunction,
|
|
430
|
+
/,
|
|
431
|
+
) -> None:
|
|
432
|
+
"""Implement a `numpy` function via a given user function.
|
|
433
|
+
|
|
434
|
+
This method serves as an alternative to the class method
|
|
435
|
+
`implementation`, which is primarily meant to be used as a decorator.
|
|
436
|
+
This method allows the user to directly associate a custom
|
|
437
|
+
implementation with the target `numpy` function.
|
|
438
|
+
|
|
439
|
+
Parameters
|
|
440
|
+
----------
|
|
441
|
+
numpy_function : callable
|
|
442
|
+
The `numpy` universal or public function to implement.
|
|
443
|
+
|
|
444
|
+
user_function: callable
|
|
445
|
+
The custom implementation to associate with `numpy_function`.
|
|
446
|
+
|
|
447
|
+
Examples
|
|
448
|
+
--------
|
|
449
|
+
Here is an alternative to the `~implementation` example usage:
|
|
450
|
+
|
|
451
|
+
```
|
|
452
|
+
def mean(a: Array, **kwargs) -> Array:
|
|
453
|
+
if kwargs:
|
|
454
|
+
msg = "Cannot pass keywords to numpy.mean with Array" raise
|
|
455
|
+
TypeError(msg)
|
|
456
|
+
return numpy.sum(a) / len(a)
|
|
457
|
+
|
|
458
|
+
Array.implement(numpy.mean, mean)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
However, a more useful application may be to associate multiple `numpy`
|
|
462
|
+
functions with a single custom implementation:
|
|
463
|
+
|
|
464
|
+
```
|
|
465
|
+
def trig(f: numpy.ufunc):
|
|
466
|
+
def method(a: Array):
|
|
467
|
+
... # custom implementation
|
|
468
|
+
return method
|
|
469
|
+
|
|
470
|
+
for f in {numpy.sin, numpy.cos, numpy.tan}:
|
|
471
|
+
Array.implement(f, trig(f))
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
See Also
|
|
475
|
+
--------
|
|
476
|
+
`~implementation`
|
|
477
|
+
"""
|
|
478
|
+
if not callable(numpy_function):
|
|
479
|
+
raise TypeError(
|
|
480
|
+
"The target operation of a custom numpy implementation"
|
|
481
|
+
" must be callable"
|
|
482
|
+
) from None
|
|
483
|
+
if cls._FUNCTIONS is None:
|
|
484
|
+
raise NotImplementedError(
|
|
485
|
+
f"Type {cls} does not support custom implementations"
|
|
486
|
+
" of numpy functions"
|
|
487
|
+
) from None
|
|
488
|
+
cls._FUNCTIONS[numpy_function] = user_function
|
|
489
|
+
|
oprattr/py.typed
ADDED
|
File without changes
|
oprattr/typeface.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Support for type annotations.
|
|
3
|
+
|
|
4
|
+
This module provides a single interface to type annotations, including those
|
|
5
|
+
that are not defined by the operative Python version and those that this package
|
|
6
|
+
prefers to use from future versions.
|
|
7
|
+
|
|
8
|
+
Examples
|
|
9
|
+
--------
|
|
10
|
+
* Suppose `BestType` is available in the `typing` module starting with Python
|
|
11
|
+
version 3.X and is available in the `typing_extensions` module for earlier
|
|
12
|
+
versions. If the user is running with Python version <3.X, this module will
|
|
13
|
+
import `BestType` from `typing_extensions`. Otherwise, it will import
|
|
14
|
+
`BestType` from `typing`.
|
|
15
|
+
* Support `UpdatedType` is available in the `typing` module for the user's
|
|
16
|
+
version of Python, but this package wishes to take advantage of updates since
|
|
17
|
+
that version. This module will automatically import the version from
|
|
18
|
+
`typing_extensions`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import typing
|
|
22
|
+
import typing_extensions
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ()
|
|
26
|
+
|
|
27
|
+
EXTENDED = [
|
|
28
|
+
'Protocol',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def __getattr__(name: str) -> type:
|
|
32
|
+
"""Get a built-in type annotation."""
|
|
33
|
+
if name in EXTENDED:
|
|
34
|
+
return getattr(typing_extensions, name)
|
|
35
|
+
try:
|
|
36
|
+
attr = getattr(typing, name)
|
|
37
|
+
except AttributeError:
|
|
38
|
+
attr = getattr(typing_extensions, name)
|
|
39
|
+
return attr
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
oprattr/typeface.pyi
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oprattr
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: Matthew Young <myoung.space.science@gmail.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: numerical
|
|
9
|
+
Requires-Dist: numpy>=2.2.1
|
|
10
|
+
Requires-Dist: scipy>=1.15.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# oprattr: Self-Consistent Operations on Object Attributes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
oprattr/__init__.py,sha256=NPLnNea_NmyxQUX7DLqaC7YB67XATQ2gEhkPmBGpRpo,6044
|
|
2
|
+
oprattr/_operations.py,sha256=woQtV5R7ToZ5KY3OMJK7MQYHWPGFxQ3f8jTcqkmFdQI,6002
|
|
3
|
+
oprattr/abstract.py,sha256=RPGJ-jOz5bJoLvdbmP5eCP_NyGRj6yj5Iia5oJ0-gnE,1491
|
|
4
|
+
oprattr/mixins.py,sha256=_qqhReZu9Ta83irVihUrhggcObEj4lyp-3_S9K2hEog,16875
|
|
5
|
+
oprattr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
oprattr/typeface.py,sha256=FPGAdTZUmS_jd56PcZ5tCcUE_rXNubINBpVMQLRenvg,1214
|
|
7
|
+
oprattr/typeface.pyi,sha256=6gVdlDXtwl6Qyv07JWuRhM78VTGzds0pJ2KZUAAGcXs,113
|
|
8
|
+
oprattr-0.5.0.dist-info/METADATA,sha256=UjAmWY4kYQduP37H4KKENdWBMMLWBsz2KXxnIDsiOi0,375
|
|
9
|
+
oprattr-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
oprattr-0.5.0.dist-info/licenses/LICENSE,sha256=m2oXG0JDq5RzaKTS57TvGyNq5cWcV4_nfmLZjzLdYTg,1513
|
|
11
|
+
oprattr-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
BSD 3-Clause License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2025, Matt Young
|
|
6
|
+
All rights reserved.
|
|
7
|
+
|
|
8
|
+
Redistribution and use in source and binary forms, with or without
|
|
9
|
+
modification, are permitted provided that the following conditions are met:
|
|
10
|
+
|
|
11
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
12
|
+
list of conditions and the following disclaimer.
|
|
13
|
+
|
|
14
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
15
|
+
this list of conditions and the following disclaimer in the documentation
|
|
16
|
+
and/or other materials provided with the distribution.
|
|
17
|
+
|
|
18
|
+
* Neither the name of the copyright holder nor the names of its
|
|
19
|
+
contributors may be used to endorse or promote products derived from
|
|
20
|
+
this software without specific prior written permission.
|
|
21
|
+
|
|
22
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
23
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
24
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
25
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
26
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
27
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
28
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
29
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
30
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
31
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
32
|
+
|