oprattr 0.2.0__tar.gz → 0.4.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.

@@ -0,0 +1,19 @@
1
+ ## NEXT
2
+
3
+ ## v0.4.0
4
+
5
+ - Replace local `operators` module with equivalent module from `numerical` package
6
+ - Redefine equality to always return a single boolean value
7
+
8
+ ## v0.3.0
9
+
10
+ - Incorporate `numerical` package
11
+ - Add `typeface` module
12
+
13
+ ## v0.2.0
14
+
15
+ - Rename `_types` submodule to `abstract`
16
+
17
+ ## v0.1.0
18
+
19
+ - Hello world!
oprattr-0.4.0/LICENSE ADDED
@@ -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
+
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oprattr
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Add your description here
5
5
  Author-email: Matthew Young <myoung.space.science@gmail.com>
6
+ License-File: LICENSE
6
7
  Requires-Python: >=3.10
8
+ Requires-Dist: numerical
7
9
  Requires-Dist: numpy>=2.2.1
8
10
  Requires-Dist: scipy>=1.15.0
9
11
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "oprattr"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -8,6 +8,7 @@ authors = [
8
8
  ]
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
11
+ "numerical",
11
12
  "numpy>=2.2.1",
12
13
  "scipy>=1.15.0",
13
14
  ]
@@ -21,3 +22,6 @@ dev = [
21
22
  "ipython>=8.31.0",
22
23
  "pytest>=8.3.4",
23
24
  ]
25
+
26
+ [tool.uv.sources]
27
+ numerical = { git = "https://github.com/myoung-space-science/numerical", rev = "v0.2.0" }
@@ -1,12 +1,13 @@
1
+ import collections.abc
1
2
  import functools
2
3
  import numbers
3
- import typing
4
4
 
5
+ from numerical import operators
5
6
  import numpy
6
7
 
7
- from . import mixins
8
- from . import operators
9
8
  from . import abstract
9
+ from . import mixins
10
+ from . import typeface
10
11
  from ._operations import (
11
12
  unary,
12
13
  equality,
@@ -16,7 +17,7 @@ from ._operations import (
16
17
  )
17
18
 
18
19
 
19
- T = typing.TypeVar('T')
20
+ T = typeface.TypeVar('T')
20
21
 
21
22
 
22
23
  class Operand(abstract.Object[T], mixins.Numpy):
@@ -114,12 +115,21 @@ class Operand(abstract.Object[T], mixins.Numpy):
114
115
 
115
116
  def __rpow__(self, other):
116
117
  """Called for other ** self."""
117
- return super().__rpow__(other)
118
+ return NotImplemented
118
119
 
119
120
  def __array__(self, *args, **kwargs):
120
121
  """Called for numpy.array(self)."""
121
122
  return numpy.array(self._data, *args, **kwargs)
122
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
+
123
133
 
124
134
  @Operand.implementation(numpy.array_equal)
125
135
  def array_equal(
@@ -155,7 +165,7 @@ def gradient(x: Operand[T], *args, **kwargs):
155
165
  return type(x)(data, **meta)
156
166
 
157
167
 
158
- def wrapnumpy(f: typing.Callable):
168
+ def wrapnumpy(f: collections.abc.Callable):
159
169
  """Implement a numpy function for objects with metadata."""
160
170
  @functools.wraps(f)
161
171
  def method(x: Operand[T], **kwargs):
@@ -1,7 +1,6 @@
1
- import typing
1
+ from numerical import operators
2
2
 
3
- from . import operators
4
- from .abstract import Object
3
+ from .abstract import Quantity
5
4
 
6
5
 
7
6
  class MetadataError(TypeError):
@@ -11,8 +10,8 @@ class MetadataError(TypeError):
11
10
  self,
12
11
  f: operators.Operator,
13
12
  *args,
14
- error: typing.Optional[str]=None,
15
- key: typing.Optional[str]=None,
13
+ error: str | None = None,
14
+ key: str | None = None,
16
15
  ) -> None:
17
16
  super().__init__(*args)
18
17
  self._f = f
@@ -33,8 +32,8 @@ class MetadataError(TypeError):
33
32
  def _build_error_message(
34
33
  f: operators.Operator,
35
34
  *types: type,
36
- error: typing.Optional[str]=None,
37
- key: typing.Optional[str]=None,
35
+ error: str | None = None,
36
+ key: str | None = None,
38
37
  ) -> str:
39
38
  """Helper for `_raise_metadata_exception`.
40
39
 
@@ -50,9 +49,9 @@ def _build_error_message(
50
49
  if len(types) == 2:
51
50
  a, b = types
52
51
  endstr = "because {} has metadata"
53
- if issubclass(a, Object):
52
+ if issubclass(a, Quantity):
54
53
  return f"{errmsg} between {a} and {b} {endstr.format(str(a))}"
55
- if issubclass(b, Object):
54
+ if issubclass(b, Quantity):
56
55
  return f"{errmsg} between {a} and {b} {endstr.format(str(b))}"
57
56
  if errstr == 'type':
58
57
  if key is None:
@@ -71,7 +70,7 @@ def _build_error_message(
71
70
 
72
71
  def unary(f: operators.Operator, a):
73
72
  """Compute the unary operation f(a)."""
74
- if isinstance(a, Object):
73
+ if isinstance(a, Quantity):
75
74
  meta = {}
76
75
  for key, value in a._meta.items():
77
76
  try:
@@ -86,32 +85,42 @@ def unary(f: operators.Operator, a):
86
85
 
87
86
  def equality(f: operators.Operator, a, b):
88
87
  """Compute the equality operation f(a, b)."""
89
- if isinstance(a, Object) and isinstance(b, Object):
88
+ x = a._data if isinstance(a, Quantity) else a
89
+ y = b._data if isinstance(b, Quantity) else b
90
+ fxy = f(x, y)
91
+ try:
92
+ iter(fxy)
93
+ except TypeError:
94
+ r = bool(fxy)
95
+ else:
96
+ r = all(fxy)
97
+ isne = f(1, 2)
98
+ if isinstance(a, Quantity) and isinstance(b, Quantity):
90
99
  if a._meta != b._meta:
91
- return f is operators.ne
92
- return f(a._data, b._data)
93
- if isinstance(a, Object):
100
+ return isne
101
+ return r
102
+ if isinstance(a, Quantity):
94
103
  if not a._meta:
95
- return f(a._data, b)
96
- return f is operators.ne
97
- if isinstance(b, Object):
104
+ return r
105
+ return isne
106
+ if isinstance(b, Quantity):
98
107
  if not b._meta:
99
- return f(a, b._data)
100
- return f is operators.ne
101
- return f(a, b)
108
+ return r
109
+ return isne
110
+ return r
102
111
 
103
112
 
104
113
  def ordering(f: operators.Operator, a, b):
105
114
  """Compute the ordering operation f(a, b)."""
106
- if isinstance(a, Object) and isinstance(b, Object):
115
+ if isinstance(a, Quantity) and isinstance(b, Quantity):
107
116
  if a._meta == b._meta:
108
117
  return f(a._data, b._data)
109
118
  raise MetadataError(f, a, b, error='unequal') from None
110
- if isinstance(a, Object):
119
+ if isinstance(a, Quantity):
111
120
  if not a._meta:
112
121
  return f(a._data, b)
113
122
  raise MetadataError(f, a, b, error='non-empty') from None
114
- if isinstance(b, Object):
123
+ if isinstance(b, Quantity):
115
124
  if not b._meta:
116
125
  return f(a, b._data)
117
126
  raise MetadataError(f, a, b, error='non-empty') from None
@@ -120,15 +129,15 @@ def ordering(f: operators.Operator, a, b):
120
129
 
121
130
  def additive(f: operators.Operator, a, b):
122
131
  """Compute the additive operation f(a, b)."""
123
- if isinstance(a, Object) and isinstance(b, Object):
132
+ if isinstance(a, Quantity) and isinstance(b, Quantity):
124
133
  if a._meta == b._meta:
125
134
  return type(a)(f(a._data, b._data), **a._meta)
126
135
  raise MetadataError(f, a, b, error='unequal') from None
127
- if isinstance(a, Object):
136
+ if isinstance(a, Quantity):
128
137
  if not a._meta:
129
138
  return type(a)(f(a._data, b))
130
139
  raise MetadataError(f, a, b, error='non-empty') from None
131
- if isinstance(b, Object):
140
+ if isinstance(b, Quantity):
132
141
  if not b._meta:
133
142
  return type(b)(f(a, b._data))
134
143
  raise MetadataError(f, a, b, error='non-empty') from None
@@ -137,7 +146,7 @@ def additive(f: operators.Operator, a, b):
137
146
 
138
147
  def multiplicative(f: operators.Operator, a, b):
139
148
  """Compute the multiplicative operation f(a, b)."""
140
- if isinstance(a, Object) and isinstance(b, Object):
149
+ if isinstance(a, Quantity) and isinstance(b, Quantity):
141
150
  keys = set(a._meta) & set(b._meta)
142
151
  meta = {}
143
152
  for key in keys:
@@ -154,7 +163,7 @@ def multiplicative(f: operators.Operator, a, b):
154
163
  if key not in keys:
155
164
  meta[key] = value
156
165
  return type(a)(f(a._data, b._data), **meta)
157
- if isinstance(a, Object):
166
+ if isinstance(a, Quantity):
158
167
  meta = {}
159
168
  for key, value in a._meta.items():
160
169
  try:
@@ -164,7 +173,7 @@ def multiplicative(f: operators.Operator, a, b):
164
173
  else:
165
174
  meta[key] = v
166
175
  return type(a)(f(a._data, b), **meta)
167
- if isinstance(b, Object):
176
+ if isinstance(b, Quantity):
168
177
  meta = {}
169
178
  for key, value in b._meta.items():
170
179
  try:
@@ -0,0 +1,56 @@
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 __repr__(self):
42
+ """Called for repr(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 = "metadata={" + ", ".join(f"{k!r}" for k in self._meta) + "}"
55
+ return f"{self.__class__.__qualname__}({datastr}, {metastr})"
56
+
@@ -1,12 +1,13 @@
1
+ import collections.abc
1
2
  import numbers
2
- import typing
3
3
 
4
4
  import numpy
5
5
 
6
6
  from . import abstract
7
+ from . import typeface
7
8
 
8
9
 
9
- T = typing.TypeVar('T')
10
+ T = typeface.TypeVar('T')
10
11
 
11
12
 
12
13
  class Real:
@@ -82,7 +83,7 @@ class Real:
82
83
  return self
83
84
 
84
85
 
85
- UserFunction = typing.Callable[..., T]
86
+ UserFunction = collections.abc.Callable[..., T]
86
87
 
87
88
 
88
89
  class Numpy:
@@ -127,7 +128,7 @@ class Numpy:
127
128
  numpy.ndarray,
128
129
  numbers.Number,
129
130
  list,
130
- abstract.Object,
131
+ abstract.Quantity,
131
132
  }
132
133
 
133
134
  def __array_ufunc__(self, ufunc, method, *args, **kwargs):
@@ -162,7 +163,7 @@ class Numpy:
162
163
  return NotImplemented
163
164
  if out:
164
165
  kwargs['out'] = tuple(
165
- x._data if isinstance(x, abstract.Object)
166
+ x._data if isinstance(x, abstract.Quantity)
166
167
  else x for x in out
167
168
  )
168
169
  if self._implements(ufunc):
@@ -285,7 +286,7 @@ class Numpy:
285
286
  types = self._get_numpy_types(types)
286
287
  return array.__array_function__(func, types, args, kwargs)
287
288
 
288
- def _get_numpy_array(self) -> typing.Optional[numpy.typing.NDArray]:
289
+ def _get_numpy_array(self) -> numpy.typing.NDArray | None:
289
290
  """Convert the data interface to an array for `numpy` mixin methods.
290
291
 
291
292
  Notes
@@ -324,7 +325,7 @@ class Numpy:
324
325
  `arg` if `arg` is an instance of the base object class; otherwise, it
325
326
  will return the unmodified argument.
326
327
  """
327
- if isinstance(arg, abstract.Object):
328
+ if isinstance(arg, abstract.Quantity):
328
329
  return arg._data
329
330
  return arg
330
331
 
@@ -344,7 +345,7 @@ class Numpy:
344
345
  )
345
346
 
346
347
  @classmethod
347
- def _implements(cls, operation: typing.Callable):
348
+ def _implements(cls, operation: collections.abc.Callable):
348
349
  """True if this class defines a custom implementation for `operation`.
349
350
 
350
351
  This is a helper methods that gracefully handles the case in which a
@@ -356,11 +357,11 @@ class Numpy:
356
357
  return False
357
358
  return result
358
359
 
359
- _FUNCTIONS: typing.Dict[str, typing.Callable]=None
360
+ _FUNCTIONS: dict[str, collections.abc.Callable]=None
360
361
  """Internal collection of custom `numpy` function implementations."""
361
362
 
362
363
  @classmethod
363
- def implementation(cls, numpy_function: typing.Callable, /):
364
+ def implementation(cls, numpy_function: collections.abc.Callable, /):
364
365
  """Register a custom implementation of this `numpy` function.
365
366
 
366
367
  Parameters
@@ -424,7 +425,7 @@ class Numpy:
424
425
  @classmethod
425
426
  def implement(
426
427
  cls,
427
- numpy_function: typing.Callable,
428
+ numpy_function: collections.abc.Callable,
428
429
  user_function: UserFunction,
429
430
  /,
430
431
  ) -> None:
@@ -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
+
@@ -0,0 +1,5 @@
1
+ """
2
+ Type help for our custom type-annotation interface.
3
+ """
4
+ from typing_extensions import *
5
+ from typing import *
@@ -196,10 +196,20 @@ def test_equality():
196
196
  assert x(1) != x(-1)
197
197
  assert x(1) == 1
198
198
  assert x(1) != -1
199
- assert x(1, name=Symbol('A')) == x(1, name=Symbol('A'))
200
- assert x(1, name=Symbol('A')) != x(1, name=Symbol('B'))
201
- assert x(1, name=Symbol('A')) != 1
202
- assert 1 != x(1, name=Symbol('A'))
199
+ sA = Symbol('A')
200
+ sB = Symbol('B')
201
+ assert x(1, name=sA) == x(1, name=sA)
202
+ assert x(1, name=sA) != x(1, name=sB)
203
+ assert x(1, name=sA) != 1
204
+ assert 1 != x(1, name=sA)
205
+ array = numpy.array([-1, +1])
206
+ assert x(array) == x(array)
207
+ assert x(array, name=sA) == x(array, name=sA)
208
+ assert x(array, name=sA) != x(array, name=sB)
209
+ assert x(array, name=sA) != array
210
+ assert x(array) == array
211
+ assert array == x(array)
212
+ assert array != x(array, name=sA)
203
213
 
204
214
 
205
215
  def test_ordering():