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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)