oprattr 0.6.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,19 @@
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
+
10
+ ## v0.7.0
11
+
12
+ - Add `oprattr.Object` and `oprattr.Quantity` to the public namespace
13
+ - Rename modules that should not be part of the public namespace
14
+ - Define the `OperationError` exception class
15
+ - Change default behavior when a metadata attribute does not implement an operation
16
+
3
17
  ## v0.6.0
4
18
 
5
19
  - Add `methods` module
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oprattr
3
- Version: 0.6.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.6.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
+
@@ -0,0 +1,123 @@
1
+ import collections.abc
2
+
3
+ import numpy.typing
4
+
5
+ from . import _abstract
6
+ from . import methods
7
+ from . import mixins
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
+ )
25
+
26
+
27
+ T = _typeface.TypeVar('T')
28
+
29
+ class Operand(_abstract.Object[T], mixins.Numpy):
30
+ """A concrete implementation of a real-valued object."""
31
+
32
+ def __abs__(self) -> _typeface.Self:
33
+ """Called for abs(self)."""
34
+
35
+ def __pos__(self) -> _typeface.Self:
36
+ """Called for +self."""
37
+
38
+ def __neg__(self) -> _typeface.Self:
39
+ """Called for -self."""
40
+
41
+ def __eq__(self, other) -> bool:
42
+ """Called for self == other."""
43
+
44
+ def __ne__(self, other) -> bool:
45
+ """Called for self != other."""
46
+
47
+ def __lt__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
48
+ """Called for self < other."""
49
+
50
+ def __le__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
51
+ """Called for self <= other."""
52
+
53
+ def __gt__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
54
+ """Called for self > other."""
55
+
56
+ def __ge__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
57
+ """Called for self >= other."""
58
+
59
+ def __add__(self, other) -> _typeface.Self:
60
+ """Called for self + other."""
61
+
62
+ def __radd__(self, other) -> _typeface.Self:
63
+ """Called for other + self."""
64
+
65
+ def __sub__(self, other) -> _typeface.Self:
66
+ """Called for self - other."""
67
+
68
+ def __rsub__(self, other) -> _typeface.Self:
69
+ """Called for other - self."""
70
+
71
+ def __mul__(self, other) -> _typeface.Self:
72
+ """Called for self * other."""
73
+
74
+ def __rmul__(self, other) -> _typeface.Self:
75
+ """Called for other * self."""
76
+
77
+ def __truediv__(self, other) -> _typeface.Self:
78
+ """Called for self / other."""
79
+
80
+ def __rtruediv__(self, other) -> _typeface.Self:
81
+ """Called for other / self."""
82
+
83
+ def __floordiv__(self, other) -> _typeface.Self:
84
+ """Called for self // other."""
85
+
86
+ def __rfloordiv__(self, other) -> _typeface.Self:
87
+ """Called for other // self."""
88
+
89
+ def __mod__(self, other) -> _typeface.Self:
90
+ """Called for self % other."""
91
+
92
+ def __rmod__(self, other) -> _typeface.Self:
93
+ """Called for other % self."""
94
+
95
+ def __pow__(self, other) -> _typeface.Self:
96
+ """Called for self ** other."""
97
+
98
+ def __rpow__(self, other):
99
+ """Called for other ** self."""
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
+
@@ -4,10 +4,10 @@ import numbers
4
4
  import numerical
5
5
  import numpy.typing
6
6
 
7
- from . import typeface
7
+ from . import _typeface
8
8
 
9
9
 
10
- DataType = typeface.TypeVar(
10
+ DataType = _typeface.TypeVar(
11
11
  'DataType',
12
12
  int,
13
13
  float,
@@ -18,14 +18,14 @@ DataType = typeface.TypeVar(
18
18
  )
19
19
 
20
20
 
21
- @typeface.runtime_checkable
22
- class Quantity(numerical.Quantity[DataType], typeface.Protocol):
21
+ @_typeface.runtime_checkable
22
+ class Quantity(numerical.Quantity[DataType], _typeface.Protocol):
23
23
  """Protocol for numerical objects with metadata."""
24
24
 
25
- _meta: collections.abc.Mapping[str, typeface.Any]
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
+
@@ -1,17 +1,13 @@
1
1
  from numerical import operators
2
2
 
3
- from .abstract import (
3
+ 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):
@@ -157,11 +158,6 @@ def symbol_gradient(x: Symbol, **kwargs):
157
158
  return f"numpy.gradient({x})"
158
159
 
159
160
 
160
- @Symbol.implementation(numpy.trapezoid)
161
- def symbol_trapezoid(x: Symbol, **kwargs):
162
- return f"numpy.trapezoid({x})"
163
-
164
-
165
161
  def symbolic_binary(a, op, b):
166
162
  if isinstance(a, (Symbol, str)) and isinstance(b, (Symbol, str)):
167
163
  return f"{a} {op} {b}"
@@ -502,26 +498,29 @@ def test_gradient():
502
498
  grad = numpy.gradient(v)
503
499
  for t, g in zip(that, grad):
504
500
  assert isinstance(t, oprattr.Operand)
505
- assert numpy.array_equal(t, x(g, name=nR))
501
+ assert numpy.array_equal(t, g)
502
+ assert t._meta['name'] == nR
506
503
  for axis in range(v.ndim):
507
504
  that = numpy.gradient(this, axis=axis)
508
505
  assert isinstance(that, oprattr.Operand)
509
506
  grad = numpy.gradient(v, axis=axis)
510
- assert numpy.array_equal(that, x(grad, name=nR))
507
+ assert numpy.array_equal(that, grad)
508
+ assert t._meta['name'] == nR
511
509
 
512
510
 
513
511
  def test_trapezoid():
514
- """Test `numpy.trapezoid`."""
512
+ """Test `numpy.trapezoid`, which `Symbol` does not implement."""
515
513
  nA = Symbol('A')
516
- nR = Symbol('numpy.trapezoid(A)')
517
514
  v = numpy.arange(3*4*5).reshape(3, 4, 5)
518
515
  this = x(v, name=nA)
519
516
  that = numpy.trapezoid(this)
520
517
  assert isinstance(that, oprattr.Operand)
521
- assert numpy.array_equal(that, x(numpy.trapezoid(v), name=nR))
518
+ assert numpy.array_equal(that, numpy.trapezoid(v))
519
+ assert that._meta['name'] == nA
522
520
  for axis in range(v.ndim):
523
521
  that = numpy.trapezoid(this, axis=axis)
524
522
  assert isinstance(that, oprattr.Operand)
525
523
  trap = numpy.trapezoid(v, axis=axis)
526
- assert numpy.array_equal(that, x(trap, name=nR))
524
+ assert numpy.array_equal(that, trap)
525
+ assert that._meta['name'] == nA
527
526
 
@@ -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,139 +0,0 @@
1
- import collections.abc
2
- import functools
3
-
4
- import numpy
5
-
6
- from . import abstract
7
- from . import methods
8
- from . import mixins
9
- from . import typeface
10
- from ._operations import equality
11
-
12
-
13
- T = typeface.TypeVar('T')
14
-
15
-
16
- class Operand(abstract.Object[T], mixins.Numpy):
17
- """A concrete implementation of a real-valued object."""
18
-
19
- __abs__ = methods.__abs__
20
- __pos__ = methods.__pos__
21
- __neg__ = methods.__neg__
22
-
23
- __eq__ = methods.__eq__
24
- __ne__ = methods.__ne__
25
- __lt__ = methods.__lt__
26
- __le__ = methods.__le__
27
- __gt__ = methods.__gt__
28
- __ge__ = methods.__ge__
29
-
30
- __add__ = methods.__add__
31
- __radd__ = methods.__radd__
32
- __sub__ = methods.__sub__
33
- __rsub__ = methods.__rsub__
34
- __mul__ = methods.__mul__
35
- __rmul__ = methods.__rmul__
36
- __truediv__ = methods.__truediv__
37
- __rtruediv__ = methods.__rtruediv__
38
- __floordiv__ = methods.__floordiv__
39
- __rfloordiv__ = methods.__rfloordiv__
40
- __mod__ = methods.__mod__
41
- __rmod__ = methods.__rmod__
42
- __pow__ = methods.__pow__
43
- __rpow__ = methods.__rpow__
44
-
45
- def __array__(self, *args, **kwargs):
46
- """Called for numpy.array(self)."""
47
- return numpy.array(self._data, *args, **kwargs)
48
-
49
- def _apply_ufunc(self, ufunc, method, *args, **kwargs):
50
- if ufunc in (numpy.equal, numpy.not_equal):
51
- # NOTE: We are probably here because the left operand is a
52
- # `numpy.ndarray`, which would otherwise take control and return the
53
- # pure `numpy` result.
54
- f = getattr(ufunc, method)
55
- return equality(f, *args)
56
- return super()._apply_ufunc(ufunc, method, *args, **kwargs)
57
-
58
-
59
- @Operand.implementation(numpy.array_equal)
60
- def array_equal(
61
- x: numpy.typing.ArrayLike,
62
- y: numpy.typing.ArrayLike,
63
- **kwargs
64
- ) -> bool:
65
- """Called for numpy.array_equal(x, y)"""
66
- return numpy.array_equal(numpy.array(x), numpy.array(y), **kwargs)
67
-
68
-
69
- @Operand.implementation(numpy.gradient)
70
- def gradient(x: Operand[T], *args, **kwargs):
71
- """Called for numpy.gradient(x)."""
72
- data = numpy.gradient(x._data, *args, **kwargs)
73
- meta = {}
74
- for key, value in x._meta.items():
75
- try:
76
- v = numpy.gradient(value, **kwargs)
77
- except TypeError as exc:
78
- raise TypeError(
79
- "Cannot compute numpy.gradient(x)"
80
- f" because metadata attribute {key!r}"
81
- " does not support this operation"
82
- ) from exc
83
- else:
84
- meta[key] = v
85
- if isinstance(data, (list, tuple)):
86
- r = [type(x)(array, **meta) for array in data]
87
- if isinstance(data, tuple):
88
- return tuple(r)
89
- return r
90
- return type(x)(data, **meta)
91
-
92
-
93
- def wrapnumpy(f: collections.abc.Callable):
94
- """Implement a numpy function for objects with metadata."""
95
- @functools.wraps(f)
96
- def method(x: Operand[T], **kwargs):
97
- """Apply a numpy function to x."""
98
- data = f(x._data, **kwargs)
99
- meta = {}
100
- for key, value in x._meta.items():
101
- try:
102
- v = f(value, **kwargs)
103
- except TypeError as exc:
104
- raise TypeError(
105
- f"Cannot compute numpy.{f.__qualname__}(x)"
106
- f" because metadata attribute {key!r}"
107
- " does not support this operation"
108
- ) from exc
109
- else:
110
- meta[key] = v
111
- return type(x)(data, **meta)
112
- return method
113
-
114
-
115
- _OPERAND_UFUNCS = (
116
- numpy.sqrt,
117
- numpy.sin,
118
- numpy.cos,
119
- numpy.tan,
120
- numpy.log,
121
- numpy.log2,
122
- numpy.log10,
123
- )
124
-
125
-
126
- _OPERAND_FUNCTIONS = (
127
- numpy.squeeze,
128
- numpy.mean,
129
- numpy.sum,
130
- numpy.cumsum,
131
- numpy.transpose,
132
- numpy.trapezoid,
133
- )
134
-
135
-
136
- for f in _OPERAND_UFUNCS + _OPERAND_FUNCTIONS:
137
- Operand.implement(f, wrapnumpy(f))
138
-
139
-
@@ -1,81 +0,0 @@
1
- import numpy.typing
2
-
3
- from . import abstract
4
- from . import mixins
5
- from . import typeface
6
-
7
-
8
- T = typeface.TypeVar('T')
9
-
10
- class Operand(abstract.Object[T], mixins.Numpy):
11
- """A concrete implementation of a real-valued object."""
12
-
13
- def __abs__(self) -> typeface.Self:
14
- """Called for abs(self)."""
15
-
16
- def __pos__(self) -> typeface.Self:
17
- """Called for +self."""
18
-
19
- def __neg__(self) -> typeface.Self:
20
- """Called for -self."""
21
-
22
- def __eq__(self, other) -> bool:
23
- """Called for self == other."""
24
-
25
- def __ne__(self, other) -> bool:
26
- """Called for self != other."""
27
-
28
- def __lt__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
29
- """Called for self < other."""
30
-
31
- def __le__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
32
- """Called for self <= other."""
33
-
34
- def __gt__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
35
- """Called for self > other."""
36
-
37
- def __ge__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
38
- """Called for self >= other."""
39
-
40
- def __add__(self, other) -> typeface.Self:
41
- """Called for self + other."""
42
-
43
- def __radd__(self, other) -> typeface.Self:
44
- """Called for other + self."""
45
-
46
- def __sub__(self, other) -> typeface.Self:
47
- """Called for self - other."""
48
-
49
- def __rsub__(self, other) -> typeface.Self:
50
- """Called for other - self."""
51
-
52
- def __mul__(self, other) -> typeface.Self:
53
- """Called for self * other."""
54
-
55
- def __rmul__(self, other) -> typeface.Self:
56
- """Called for other * self."""
57
-
58
- def __truediv__(self, other) -> typeface.Self:
59
- """Called for self / other."""
60
-
61
- def __rtruediv__(self, other) -> typeface.Self:
62
- """Called for other / self."""
63
-
64
- def __floordiv__(self, other) -> typeface.Self:
65
- """Called for self // other."""
66
-
67
- def __rfloordiv__(self, other) -> typeface.Self:
68
- """Called for other // self."""
69
-
70
- def __mod__(self, other) -> typeface.Self:
71
- """Called for self % other."""
72
-
73
- def __rmod__(self, other) -> typeface.Self:
74
- """Called for other % self."""
75
-
76
- def __pow__(self, other) -> typeface.Self:
77
- """Called for self ** other."""
78
-
79
- def __rpow__(self, other):
80
- """Called for other ** self."""
81
-
@@ -1,489 +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
-
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
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes