ndlike 0.1.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.
- ndlike-0.1.0/LICENSE +21 -0
- ndlike-0.1.0/PKG-INFO +160 -0
- ndlike-0.1.0/README.md +134 -0
- ndlike-0.1.0/pyproject.toml +98 -0
- ndlike-0.1.0/setup.cfg +4 -0
- ndlike-0.1.0/src/ndlike/__init__.py +1 -0
- ndlike-0.1.0/src/ndlike/_switchdispatch.py +208 -0
- ndlike-0.1.0/src/ndlike/array_like.py +471 -0
- ndlike-0.1.0/src/ndlike/jax_functions.py +999 -0
- ndlike-0.1.0/src/ndlike/jax_like.py +547 -0
- ndlike-0.1.0/src/ndlike/jax_operators.py +104 -0
- ndlike-0.1.0/src/ndlike/protected_axis/__init__.py +0 -0
- ndlike-0.1.0/src/ndlike/protected_axis/_common_functions.py +374 -0
- ndlike-0.1.0/src/ndlike/protected_axis/jax.py +517 -0
- ndlike-0.1.0/src/ndlike/protected_axis/jax_functions.py +737 -0
- ndlike-0.1.0/src/ndlike/protected_axis/numpy.py +514 -0
- ndlike-0.1.0/src/ndlike/protected_axis/numpy_functions.py +474 -0
- ndlike-0.1.0/src/ndlike/protected_axis/torch.py +387 -0
- ndlike-0.1.0/src/ndlike/protected_axis/torch_functions.py +681 -0
- ndlike-0.1.0/src/ndlike/py.typed +0 -0
- ndlike-0.1.0/src/ndlike/torch_functions.py +154 -0
- ndlike-0.1.0/src/ndlike/torch_like.py +346 -0
- ndlike-0.1.0/src/ndlike.egg-info/PKG-INFO +160 -0
- ndlike-0.1.0/src/ndlike.egg-info/SOURCES.txt +25 -0
- ndlike-0.1.0/src/ndlike.egg-info/dependency_links.txt +1 -0
- ndlike-0.1.0/src/ndlike.egg-info/requires.txt +8 -0
- ndlike-0.1.0/src/ndlike.egg-info/top_level.txt +1 -0
ndlike-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the probly team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ndlike-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ndlike
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A toolkit for defining custom array types on top of NumPy, JAX, and PyTorch.
|
|
5
|
+
Author-email: Clemens Damke <clemens@cortys.de>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: source, https://github.com/Cortys/ndlike
|
|
8
|
+
Project-URL: tracker, https://github.com/Cortys/ndlike/issues
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: numpy>=2.1.0
|
|
20
|
+
Requires-Dist: flextype>=0.2.0
|
|
21
|
+
Provides-Extra: torch
|
|
22
|
+
Requires-Dist: torch>=2.6.0; extra == "torch"
|
|
23
|
+
Provides-Extra: jax
|
|
24
|
+
Requires-Dist: jax>=0.8.1; extra == "jax"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# ndlike
|
|
28
|
+
|
|
29
|
+
NumPy arrays, JAX arrays, and PyTorch tensors provide similar primitives for
|
|
30
|
+
indexing, reshaping, stacking, and numerical operations. `ndlike` provides tools
|
|
31
|
+
to build wrappers around these types that behave like the underlying arrays,
|
|
32
|
+
but whose elements represent custom composite values—such as vectors or
|
|
33
|
+
distributions—instead of individual numbers.
|
|
34
|
+
|
|
35
|
+
Protocols and reusable base classes handle array interfaces and backend function
|
|
36
|
+
dispatch. Protected axes distinguish collection dimensions from each element's
|
|
37
|
+
internal dimensions, so indexing, reshaping, or stacking a collection preserves
|
|
38
|
+
the structure of its elements.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Requires Python 3.12 or newer.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install ndlike
|
|
46
|
+
pip install 'ndlike[torch]'
|
|
47
|
+
pip install 'ndlike[jax]'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
NumPy and flextype are core dependencies. Torch and JAX are optional and are
|
|
51
|
+
never imported by the backend-independent modules. Import backend modules explicitly.
|
|
52
|
+
|
|
53
|
+
## Array protocols and base classes
|
|
54
|
+
|
|
55
|
+
`ndlike.array_like.ArrayLike` describes indexing, shape, and metadata rather than
|
|
56
|
+
just convertibility to an array (as in `numpy.typing.ArrayLike`). It does not
|
|
57
|
+
require the full numerical Array API. `ArrayLikeWithoutSize` covers the shared
|
|
58
|
+
interface without NumPy/JAX's integer `size` property, since Torch uses a method.
|
|
59
|
+
|
|
60
|
+
Backend-specific APIs build on these protocols:
|
|
61
|
+
|
|
62
|
+
| Module | Protocol | Conversion helper | Conversion hook |
|
|
63
|
+
| --- | --- | --- | --- |
|
|
64
|
+
| `ndlike.array_like` | `NumpyLike` | `to_numpy_like` | `__array_like__` |
|
|
65
|
+
| `ndlike.torch_like` | `TorchLike` | `to_torch_like` | `__torch_like__` |
|
|
66
|
+
| `ndlike.jax_like` | `JaxLike` | `to_jax_like` | `__jax_like__` |
|
|
67
|
+
|
|
68
|
+
Each module also provides matching `Implementation` and `Convertible` classes,
|
|
69
|
+
such as `NumpyLikeImplementation` and `NumpyLikeConvertible`.
|
|
70
|
+
|
|
71
|
+
Use the array protocols to type inputs, or subclass an implementation base to
|
|
72
|
+
reuse convenience methods while supplying your representation's metadata and
|
|
73
|
+
operation-dispatch hooks. NumPy uses `__array_function__` / `__array_ufunc__`,
|
|
74
|
+
Torch uses `__torch_function__`, and ndlike supplies `__jax_function__` for JAX.
|
|
75
|
+
|
|
76
|
+
Conversion helpers call the conversion hook when an input satisfies the matching
|
|
77
|
+
convertible protocol: the core array interface plus the hook, not the hook alone.
|
|
78
|
+
This lets custom types preserve their structure instead of becoming raw arrays.
|
|
79
|
+
Otherwise, the helpers fall back to `numpy.asanyarray`, `torch.as_tensor`, and
|
|
80
|
+
`jax.numpy.asarray`, respectively. NumPy retains `__array_like__` for backward
|
|
81
|
+
compatibility and consistency with NumPy's `__array__`.
|
|
82
|
+
|
|
83
|
+
## Protected axes
|
|
84
|
+
|
|
85
|
+
Protected-axis base classes give structured objects array-like behavior over
|
|
86
|
+
their batch dimensions while preserving trailing internal dimensions. For
|
|
87
|
+
example, a batch of six 3D vectors should have logical shape `(6,)`, not `(6, 3)`:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from dataclasses import dataclass
|
|
91
|
+
from typing import ClassVar
|
|
92
|
+
|
|
93
|
+
import numpy as np
|
|
94
|
+
|
|
95
|
+
from ndlike.protected_axis.numpy import NumpyAxisProtected
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class Vectors(NumpyAxisProtected[np.ndarray]):
|
|
100
|
+
array: np.ndarray
|
|
101
|
+
protected_axes: ClassVar[dict[str, int]] = {"array": 1}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
vectors = Vectors(np.ones((6, 3)))
|
|
105
|
+
assert vectors.shape == (6,)
|
|
106
|
+
assert vectors.protected_shape == (3,)
|
|
107
|
+
assert vectors[0].array.shape == (3,)
|
|
108
|
+
|
|
109
|
+
grid = vectors.reshape(2, 3)
|
|
110
|
+
assert grid.shape == (2, 3)
|
|
111
|
+
assert grid.array.shape == (2, 3, 3)
|
|
112
|
+
|
|
113
|
+
stacked = np.stack([vectors, vectors])
|
|
114
|
+
assert isinstance(stacked, Vectors)
|
|
115
|
+
assert stacked.shape == (2, 6)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`protected_axes` maps field names to the number of trailing axes to preserve.
|
|
119
|
+
Multiple fields can have different internal shapes while sharing the same batch
|
|
120
|
+
shape; indexing and supported shape operations update them together. Dataclasses
|
|
121
|
+
work out of the box; other classes can override `with_protected_values` to
|
|
122
|
+
reconstruct results.
|
|
123
|
+
|
|
124
|
+
Numerical operations are opt-in: use `permitted_functions` (for example,
|
|
125
|
+
`{np.mean}`) and, for NumPy ufuncs, `permitted_ufuncs` (for example,
|
|
126
|
+
`{np.add: ["__call__", "reduce"]}`). Supported reductions operate on batch axes,
|
|
127
|
+
leaving internal axes intact.
|
|
128
|
+
|
|
129
|
+
The corresponding backend classes are `TorchAxisProtected` in
|
|
130
|
+
`ndlike.protected_axis.torch` and `JaxAxisProtected` in
|
|
131
|
+
`ndlike.protected_axis.jax`.
|
|
132
|
+
|
|
133
|
+
## JAX function dispatch
|
|
134
|
+
|
|
135
|
+
JAX has no native equivalent of NumPy's `__array_function__`. For custom objects,
|
|
136
|
+
use wrappers from `ndlike.jax_functions`, such as `jax_reshape`, `jax_stack`, and
|
|
137
|
+
`jax_mean`, instead of the corresponding `jax.numpy` functions. They dispatch to
|
|
138
|
+
`__jax_function__` on custom types and call JAX normally for native arrays.
|
|
139
|
+
`jax_function_dispatch` lets you add the same dispatch behavior to your own
|
|
140
|
+
functions. `JaxLikeImplementation` also provides dataclass-based pytree support
|
|
141
|
+
for JAX transformations such as `jax.jit` and `jax.vmap`.
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
uv sync --all-extras
|
|
147
|
+
uv run pytest
|
|
148
|
+
uv run prek install
|
|
149
|
+
uv run prek run --all-files
|
|
150
|
+
uv build
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Dependency groups follow flextype: `lint` contains Ruff and ty, `test` contains
|
|
154
|
+
pytest, and `dev` includes both groups plus prek. Development Torch wheels use
|
|
155
|
+
the CPU index on Linux and Windows. Type checking requires both backend extras.
|
|
156
|
+
|
|
157
|
+
`scripts/check_backend_naming.py` enforces backend prefixes on definitions
|
|
158
|
+
(BKN001) and restricts runtime Torch/JAX imports to their respective
|
|
159
|
+
backend-prefixed modules (BKN002). Type-only imports are exempt. Intentional
|
|
160
|
+
exceptions use an explicit `# noqa: BKN001` or `# noqa: BKN002` on the opening line.
|
ndlike-0.1.0/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# ndlike
|
|
2
|
+
|
|
3
|
+
NumPy arrays, JAX arrays, and PyTorch tensors provide similar primitives for
|
|
4
|
+
indexing, reshaping, stacking, and numerical operations. `ndlike` provides tools
|
|
5
|
+
to build wrappers around these types that behave like the underlying arrays,
|
|
6
|
+
but whose elements represent custom composite values—such as vectors or
|
|
7
|
+
distributions—instead of individual numbers.
|
|
8
|
+
|
|
9
|
+
Protocols and reusable base classes handle array interfaces and backend function
|
|
10
|
+
dispatch. Protected axes distinguish collection dimensions from each element's
|
|
11
|
+
internal dimensions, so indexing, reshaping, or stacking a collection preserves
|
|
12
|
+
the structure of its elements.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Requires Python 3.12 or newer.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install ndlike
|
|
20
|
+
pip install 'ndlike[torch]'
|
|
21
|
+
pip install 'ndlike[jax]'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
NumPy and flextype are core dependencies. Torch and JAX are optional and are
|
|
25
|
+
never imported by the backend-independent modules. Import backend modules explicitly.
|
|
26
|
+
|
|
27
|
+
## Array protocols and base classes
|
|
28
|
+
|
|
29
|
+
`ndlike.array_like.ArrayLike` describes indexing, shape, and metadata rather than
|
|
30
|
+
just convertibility to an array (as in `numpy.typing.ArrayLike`). It does not
|
|
31
|
+
require the full numerical Array API. `ArrayLikeWithoutSize` covers the shared
|
|
32
|
+
interface without NumPy/JAX's integer `size` property, since Torch uses a method.
|
|
33
|
+
|
|
34
|
+
Backend-specific APIs build on these protocols:
|
|
35
|
+
|
|
36
|
+
| Module | Protocol | Conversion helper | Conversion hook |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| `ndlike.array_like` | `NumpyLike` | `to_numpy_like` | `__array_like__` |
|
|
39
|
+
| `ndlike.torch_like` | `TorchLike` | `to_torch_like` | `__torch_like__` |
|
|
40
|
+
| `ndlike.jax_like` | `JaxLike` | `to_jax_like` | `__jax_like__` |
|
|
41
|
+
|
|
42
|
+
Each module also provides matching `Implementation` and `Convertible` classes,
|
|
43
|
+
such as `NumpyLikeImplementation` and `NumpyLikeConvertible`.
|
|
44
|
+
|
|
45
|
+
Use the array protocols to type inputs, or subclass an implementation base to
|
|
46
|
+
reuse convenience methods while supplying your representation's metadata and
|
|
47
|
+
operation-dispatch hooks. NumPy uses `__array_function__` / `__array_ufunc__`,
|
|
48
|
+
Torch uses `__torch_function__`, and ndlike supplies `__jax_function__` for JAX.
|
|
49
|
+
|
|
50
|
+
Conversion helpers call the conversion hook when an input satisfies the matching
|
|
51
|
+
convertible protocol: the core array interface plus the hook, not the hook alone.
|
|
52
|
+
This lets custom types preserve their structure instead of becoming raw arrays.
|
|
53
|
+
Otherwise, the helpers fall back to `numpy.asanyarray`, `torch.as_tensor`, and
|
|
54
|
+
`jax.numpy.asarray`, respectively. NumPy retains `__array_like__` for backward
|
|
55
|
+
compatibility and consistency with NumPy's `__array__`.
|
|
56
|
+
|
|
57
|
+
## Protected axes
|
|
58
|
+
|
|
59
|
+
Protected-axis base classes give structured objects array-like behavior over
|
|
60
|
+
their batch dimensions while preserving trailing internal dimensions. For
|
|
61
|
+
example, a batch of six 3D vectors should have logical shape `(6,)`, not `(6, 3)`:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from dataclasses import dataclass
|
|
65
|
+
from typing import ClassVar
|
|
66
|
+
|
|
67
|
+
import numpy as np
|
|
68
|
+
|
|
69
|
+
from ndlike.protected_axis.numpy import NumpyAxisProtected
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Vectors(NumpyAxisProtected[np.ndarray]):
|
|
74
|
+
array: np.ndarray
|
|
75
|
+
protected_axes: ClassVar[dict[str, int]] = {"array": 1}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
vectors = Vectors(np.ones((6, 3)))
|
|
79
|
+
assert vectors.shape == (6,)
|
|
80
|
+
assert vectors.protected_shape == (3,)
|
|
81
|
+
assert vectors[0].array.shape == (3,)
|
|
82
|
+
|
|
83
|
+
grid = vectors.reshape(2, 3)
|
|
84
|
+
assert grid.shape == (2, 3)
|
|
85
|
+
assert grid.array.shape == (2, 3, 3)
|
|
86
|
+
|
|
87
|
+
stacked = np.stack([vectors, vectors])
|
|
88
|
+
assert isinstance(stacked, Vectors)
|
|
89
|
+
assert stacked.shape == (2, 6)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`protected_axes` maps field names to the number of trailing axes to preserve.
|
|
93
|
+
Multiple fields can have different internal shapes while sharing the same batch
|
|
94
|
+
shape; indexing and supported shape operations update them together. Dataclasses
|
|
95
|
+
work out of the box; other classes can override `with_protected_values` to
|
|
96
|
+
reconstruct results.
|
|
97
|
+
|
|
98
|
+
Numerical operations are opt-in: use `permitted_functions` (for example,
|
|
99
|
+
`{np.mean}`) and, for NumPy ufuncs, `permitted_ufuncs` (for example,
|
|
100
|
+
`{np.add: ["__call__", "reduce"]}`). Supported reductions operate on batch axes,
|
|
101
|
+
leaving internal axes intact.
|
|
102
|
+
|
|
103
|
+
The corresponding backend classes are `TorchAxisProtected` in
|
|
104
|
+
`ndlike.protected_axis.torch` and `JaxAxisProtected` in
|
|
105
|
+
`ndlike.protected_axis.jax`.
|
|
106
|
+
|
|
107
|
+
## JAX function dispatch
|
|
108
|
+
|
|
109
|
+
JAX has no native equivalent of NumPy's `__array_function__`. For custom objects,
|
|
110
|
+
use wrappers from `ndlike.jax_functions`, such as `jax_reshape`, `jax_stack`, and
|
|
111
|
+
`jax_mean`, instead of the corresponding `jax.numpy` functions. They dispatch to
|
|
112
|
+
`__jax_function__` on custom types and call JAX normally for native arrays.
|
|
113
|
+
`jax_function_dispatch` lets you add the same dispatch behavior to your own
|
|
114
|
+
functions. `JaxLikeImplementation` also provides dataclass-based pytree support
|
|
115
|
+
for JAX transformations such as `jax.jit` and `jax.vmap`.
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
uv sync --all-extras
|
|
121
|
+
uv run pytest
|
|
122
|
+
uv run prek install
|
|
123
|
+
uv run prek run --all-files
|
|
124
|
+
uv build
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Dependency groups follow flextype: `lint` contains Ruff and ty, `test` contains
|
|
128
|
+
pytest, and `dev` includes both groups plus prek. Development Torch wheels use
|
|
129
|
+
the CPU index on Linux and Windows. Type checking requires both backend extras.
|
|
130
|
+
|
|
131
|
+
`scripts/check_backend_naming.py` enforces backend prefixes on definitions
|
|
132
|
+
(BKN001) and restricts runtime Torch/JAX imports to their respective
|
|
133
|
+
backend-prefixed modules (BKN002). Type-only imports are exempt. Intentional
|
|
134
|
+
exceptions use an explicit `# noqa: BKN001` or `# noqa: BKN002` on the opening line.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ndlike"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A toolkit for defining custom array types on top of NumPy, JAX, and PyTorch."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
dependencies = ["numpy>=2.1.0", "flextype>=0.2.0"]
|
|
13
|
+
authors = [{ name = "Clemens Damke", email = "clemens@cortys.de" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Programming Language :: Python :: 3.15",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
torch = ["torch>=2.6.0"]
|
|
26
|
+
jax = ["jax>=0.8.1"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
source = "https://github.com/Cortys/ndlike"
|
|
30
|
+
tracker = "https://github.com/Cortys/ndlike/issues"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
include = ["ndlike", "ndlike.*"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
ndlike = ["py.typed"]
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
lint = ["ruff>=0.17.1", "ty>=0.0.89"]
|
|
41
|
+
test = ["pytest>=8.0", "typing-extensions>=4.7.0"]
|
|
42
|
+
dev = [
|
|
43
|
+
{ include-group = "lint" },
|
|
44
|
+
{ include-group = "test" },
|
|
45
|
+
"prek>=0.5.6",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 120
|
|
50
|
+
target-version = "py312"
|
|
51
|
+
output-format = "full"
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = ["ALL"]
|
|
55
|
+
external = ["BKN001", "BKN002"]
|
|
56
|
+
ignore = ["COM812", "CPY001", "FBT", "FIX002", "N812", "PLR0913", "PLR0917", "PLR2004", "RET504"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.pydocstyle]
|
|
59
|
+
convention = "google"
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint.per-file-ignores]
|
|
62
|
+
"tests/**/*.py" = [
|
|
63
|
+
"ANN001", "ANN201", "ANN202", "C901", "D100", "D101", "D102", "D103", "D404",
|
|
64
|
+
"FBT001", "INP001", "N803", "PD901", "PLR2004", "S101", "TCH",
|
|
65
|
+
]
|
|
66
|
+
"__init__.py" = ["I002", "PLC0414"]
|
|
67
|
+
# Previously private modules retain their original documentation during extraction.
|
|
68
|
+
"src/ndlike/protected_axis/*.py" = ["D102", "D103", "D104", "D105"]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint.mccabe]
|
|
71
|
+
max-complexity = 15
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint.isort]
|
|
74
|
+
known-first-party = ["ndlike"]
|
|
75
|
+
force-sort-within-sections = true
|
|
76
|
+
combine-as-imports = true
|
|
77
|
+
force-single-line = false
|
|
78
|
+
no-lines-before = ["future"]
|
|
79
|
+
required-imports = ["from __future__ import annotations"]
|
|
80
|
+
extra-standard-library = ["typing_extensions"]
|
|
81
|
+
|
|
82
|
+
[tool.pytest.ini_options]
|
|
83
|
+
testpaths = ["tests/ndlike"]
|
|
84
|
+
minversion = "8.0"
|
|
85
|
+
pythonpath = ["src"]
|
|
86
|
+
addopts = ["--import-mode=importlib"]
|
|
87
|
+
|
|
88
|
+
[tool.ty.src]
|
|
89
|
+
include = ["src/ndlike", "tests/ndlike", "scripts"]
|
|
90
|
+
|
|
91
|
+
# Use small CPU wheels in development and CI; installed users choose their own index.
|
|
92
|
+
[[tool.uv.index]]
|
|
93
|
+
name = "pytorch-cpu"
|
|
94
|
+
url = "https://download.pytorch.org/whl/cpu"
|
|
95
|
+
explicit = true
|
|
96
|
+
|
|
97
|
+
[tool.uv.sources]
|
|
98
|
+
torch = [{ index = "pytorch-cpu", marker = "sys_platform != 'darwin'" }]
|
ndlike-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tools for defining custom array types across NumPy, PyTorch, and JAX."""
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""An extensible switch dispatch mechanism.
|
|
2
|
+
|
|
3
|
+
Akin to the builtin functools.singledispatch mechanism,
|
|
4
|
+
but with an equality-based dispatcher.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Iterator, MutableMapping
|
|
10
|
+
from functools import update_wrapper
|
|
11
|
+
from typing import TYPE_CHECKING, Concatenate, Protocol, overload, override
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Callable, Iterable
|
|
15
|
+
from types import NotImplementedType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _RegistrationSink[K, V](Protocol):
|
|
19
|
+
"""The contravariant write interface used to constrain registered values."""
|
|
20
|
+
|
|
21
|
+
def _register_value(self, key: K, value: V) -> None: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _Registration[V]:
|
|
25
|
+
"""Store a value while preserving its exact type and identity."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, store: Callable[[V], None]) -> None:
|
|
28
|
+
self._store = store
|
|
29
|
+
|
|
30
|
+
# A bound such as ``W: V`` is not legal: TypeVar bounds cannot be generic.
|
|
31
|
+
# Contravariant storage constrains W to accepted values without widening it
|
|
32
|
+
# to V, preserving generic classes, overloads, and callable-object attributes.
|
|
33
|
+
def __call__[W](self: _Registration[W], value: W) -> W:
|
|
34
|
+
self._store(value)
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _FunctionRegistration[F]:
|
|
39
|
+
"""Identity-preserving registration with the function decorator's ``f`` keyword."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, store: Callable[[F], None]) -> None:
|
|
42
|
+
self._store = store
|
|
43
|
+
|
|
44
|
+
def __call__[W](self: _FunctionRegistration[W], f: W) -> W:
|
|
45
|
+
self._store(f)
|
|
46
|
+
return f
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class switchdispatch[T, **In, Out]: # noqa: N801
|
|
50
|
+
"""A switch dispatch decorator.
|
|
51
|
+
|
|
52
|
+
Similar to functools.singledispatch, but dispatches based on equality
|
|
53
|
+
rather than type.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> @switchdispatch
|
|
57
|
+
>>> def func(x):
|
|
58
|
+
>>> return "default"
|
|
59
|
+
>>>
|
|
60
|
+
>>> @func.register(1)
|
|
61
|
+
>>> def _(x):
|
|
62
|
+
>>> return "one"
|
|
63
|
+
>>>
|
|
64
|
+
>>> @func.register(2)
|
|
65
|
+
>>> def _(x):
|
|
66
|
+
>>> return "two"
|
|
67
|
+
>>>
|
|
68
|
+
>>> print(func(1)) # Output: "one"
|
|
69
|
+
>>> print(func(2)) # Output: "two"
|
|
70
|
+
>>> print(func(3)) # Output: "default"
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, func: Callable[Concatenate[T, In], Out]) -> None:
|
|
74
|
+
"""Initialize the switchdispatch with the default function."""
|
|
75
|
+
self._func = func
|
|
76
|
+
self._registry: dict[object, Callable[Concatenate[T, In], Out]] = {}
|
|
77
|
+
update_wrapper(self, func, updated=())
|
|
78
|
+
|
|
79
|
+
@overload
|
|
80
|
+
def register(self, key: T) -> _FunctionRegistration[Callable[Concatenate[T, In], Out]]: ...
|
|
81
|
+
|
|
82
|
+
@overload
|
|
83
|
+
def register[F](self: _RegistrationSink[T, F], key: T, f: F) -> F: ...
|
|
84
|
+
|
|
85
|
+
def register(
|
|
86
|
+
self,
|
|
87
|
+
key: object,
|
|
88
|
+
f: Callable[Concatenate[T, In], Out] | None = None,
|
|
89
|
+
) -> Callable[Concatenate[T, In], Out] | _FunctionRegistration[Callable[Concatenate[T, In], Out]]:
|
|
90
|
+
"""Register a function for the given key, returning it unchanged."""
|
|
91
|
+
if f is None:
|
|
92
|
+
return self.multi_register([key])
|
|
93
|
+
return self.multi_register([key], f)
|
|
94
|
+
|
|
95
|
+
@overload
|
|
96
|
+
def multi_register(self, keys: Iterable[object]) -> _FunctionRegistration[Callable[Concatenate[T, In], Out]]: ...
|
|
97
|
+
|
|
98
|
+
@overload
|
|
99
|
+
def multi_register[F](self: _RegistrationSink[object, F], keys: Iterable[object], f: F) -> F: ...
|
|
100
|
+
|
|
101
|
+
def multi_register(
|
|
102
|
+
self,
|
|
103
|
+
keys: Iterable[object],
|
|
104
|
+
f: Callable[Concatenate[T, In], Out] | None = None,
|
|
105
|
+
) -> Callable[Concatenate[T, In], Out] | _FunctionRegistration[Callable[Concatenate[T, In], Out]]:
|
|
106
|
+
"""Register a function for the given keys, returning it unchanged."""
|
|
107
|
+
if f is not None:
|
|
108
|
+
for key in keys:
|
|
109
|
+
self._register_value(key, f)
|
|
110
|
+
return f
|
|
111
|
+
|
|
112
|
+
def store(f: Callable[Concatenate[T, In], Out]) -> None:
|
|
113
|
+
for key in keys:
|
|
114
|
+
self._register_value(key, f)
|
|
115
|
+
|
|
116
|
+
return _FunctionRegistration(store)
|
|
117
|
+
|
|
118
|
+
def _register_value(self, key: object, value: Callable[Concatenate[T, In], Out]) -> None:
|
|
119
|
+
self._registry[key] = value
|
|
120
|
+
|
|
121
|
+
def __call__(self, arg: T, *args: In.args, **kwargs: In.kwargs) -> Out:
|
|
122
|
+
"""Call the appropriate function based on the argument."""
|
|
123
|
+
func_impl = self._registry.get(arg, self._func)
|
|
124
|
+
return func_impl(arg, *args, **kwargs)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class switch[K, V](MutableMapping[K, V]): # noqa: N801
|
|
128
|
+
"""A simple switch-case mechanism based on a dictionary."""
|
|
129
|
+
|
|
130
|
+
_registry: dict[K, V]
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
cases: dict[K, V] | None = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Initialize the switch with the given cases and default value."""
|
|
137
|
+
self._registry = cases or {}
|
|
138
|
+
|
|
139
|
+
@overload
|
|
140
|
+
def register(self, key: K) -> _Registration[V]: ...
|
|
141
|
+
|
|
142
|
+
@overload
|
|
143
|
+
def register[W](self: _RegistrationSink[K, W], key: K, value: W) -> W: ...
|
|
144
|
+
|
|
145
|
+
def register(
|
|
146
|
+
self,
|
|
147
|
+
key: K,
|
|
148
|
+
value: V | NotImplementedType = NotImplemented,
|
|
149
|
+
) -> _Registration[V] | V:
|
|
150
|
+
"""Register a value for the given key, returning it unchanged."""
|
|
151
|
+
if value is NotImplemented:
|
|
152
|
+
return self.multi_register([key])
|
|
153
|
+
return self.multi_register([key], value)
|
|
154
|
+
|
|
155
|
+
@overload
|
|
156
|
+
def multi_register(self, keys: Iterable[K]) -> _Registration[V]: ...
|
|
157
|
+
|
|
158
|
+
@overload
|
|
159
|
+
def multi_register[W](self: _RegistrationSink[K, W], keys: Iterable[K], value: W) -> W: ...
|
|
160
|
+
|
|
161
|
+
def multi_register(
|
|
162
|
+
self,
|
|
163
|
+
keys: Iterable[K],
|
|
164
|
+
value: V | NotImplementedType = NotImplemented,
|
|
165
|
+
) -> _Registration[V] | V:
|
|
166
|
+
"""Register a value for the given keys, returning it unchanged."""
|
|
167
|
+
if value is not NotImplemented:
|
|
168
|
+
for key in keys:
|
|
169
|
+
self._register_value(key, value)
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
def store(value: V) -> None:
|
|
173
|
+
for key in keys:
|
|
174
|
+
self._register_value(key, value)
|
|
175
|
+
|
|
176
|
+
return _Registration(store)
|
|
177
|
+
|
|
178
|
+
def _register_value(self, key: K, value: V) -> None:
|
|
179
|
+
self._registry[key] = value
|
|
180
|
+
|
|
181
|
+
@override
|
|
182
|
+
def __getitem__(self, key: K) -> V:
|
|
183
|
+
"""Return the value corresponding to the given key."""
|
|
184
|
+
return self._registry[key]
|
|
185
|
+
|
|
186
|
+
def __call__(self, key: K) -> V:
|
|
187
|
+
"""Return the value corresponding to the given key."""
|
|
188
|
+
return self._registry[key]
|
|
189
|
+
|
|
190
|
+
@override
|
|
191
|
+
def __setitem__(self, key: K, value: V) -> None:
|
|
192
|
+
"""Set the value for the given key."""
|
|
193
|
+
self._registry[key] = value
|
|
194
|
+
|
|
195
|
+
@override
|
|
196
|
+
def __delitem__(self, key: K) -> None:
|
|
197
|
+
"""Delete the value for the given key."""
|
|
198
|
+
del self._registry[key]
|
|
199
|
+
|
|
200
|
+
@override
|
|
201
|
+
def __iter__(self) -> Iterator[K]:
|
|
202
|
+
"""Return an iterator over the keys in the switch cases."""
|
|
203
|
+
return iter(self._registry)
|
|
204
|
+
|
|
205
|
+
@override
|
|
206
|
+
def __len__(self) -> int:
|
|
207
|
+
"""Return the number of cases in the switch."""
|
|
208
|
+
return len(self._registry)
|