typingkit 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- typingkit/__init__.py +11 -0
- typingkit/_typed/__init__.py +11 -0
- typingkit/_typed/_debug.py +37 -0
- typingkit/_typed/context.py +203 -0
- typingkit/_typed/dimexpr.py +154 -0
- typingkit/_typed/factory.py +71 -0
- typingkit/_typed/generics.py +50 -0
- typingkit/_typed/helpers.py +248 -0
- typingkit/_typed/list.py +206 -0
- typingkit/_typed/ndarray.py +513 -0
- typingkit/py.typed +0 -0
- typingkit-0.2.2.dist-info/METADATA +92 -0
- typingkit-0.2.2.dist-info/RECORD +14 -0
- typingkit-0.2.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NDArray
|
|
3
|
+
=======
|
|
4
|
+
"""
|
|
5
|
+
# src/typingkit/_typed/ndarray.py
|
|
6
|
+
|
|
7
|
+
# pyright: reportPrivateUsage = false
|
|
8
|
+
|
|
9
|
+
import builtins
|
|
10
|
+
from types import GenericAlias, UnionType
|
|
11
|
+
from typing import (
|
|
12
|
+
Any,
|
|
13
|
+
Iterator,
|
|
14
|
+
Literal,
|
|
15
|
+
NoReturn,
|
|
16
|
+
Protocol,
|
|
17
|
+
Self,
|
|
18
|
+
TypeAlias,
|
|
19
|
+
TypeVar,
|
|
20
|
+
TypeVarTuple,
|
|
21
|
+
cast,
|
|
22
|
+
get_args,
|
|
23
|
+
get_origin,
|
|
24
|
+
overload,
|
|
25
|
+
)
|
|
26
|
+
from typing import Literal as L
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
import numpy._typing as npt_
|
|
30
|
+
import numpy.typing as npt
|
|
31
|
+
|
|
32
|
+
## Typings
|
|
33
|
+
|
|
34
|
+
_ShapeRest = TypeVarTuple("_ShapeRest")
|
|
35
|
+
|
|
36
|
+
# `numpy` privates
|
|
37
|
+
_Shape: TypeAlias = tuple[int, ...]
|
|
38
|
+
_AnyShape: TypeAlias = tuple[Any, ...]
|
|
39
|
+
|
|
40
|
+
_ShapeT_co = TypeVar("_ShapeT_co", bound=_Shape, default=_AnyShape, covariant=True)
|
|
41
|
+
_DTypeT_co = TypeVar("_DTypeT_co", bound=np.dtype, default=np.dtype, covariant=True)
|
|
42
|
+
|
|
43
|
+
_ScalarT_co = TypeVar("_ScalarT_co", bound=np.generic, default=Any, covariant=True)
|
|
44
|
+
_NonObjectScalarT = TypeVar(
|
|
45
|
+
"_NonObjectScalarT",
|
|
46
|
+
bound=np.bool | np.number | np.flexible | np.datetime64 | np.timedelta64,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_ArrayT_co = TypeVar("_ArrayT_co", bound=np.ndarray, covariant=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _SupportsArray(Protocol[_ArrayT_co]):
|
|
53
|
+
def __array__(self, /) -> _ArrayT_co: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Exceptions
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ShapeError(Exception):
|
|
60
|
+
"""Raised when array shape doesn't match expected shape."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RankError(ShapeError):
|
|
64
|
+
"""Raised when array rank doesn't match expected dimensions."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DimensionError(ShapeError):
|
|
68
|
+
"""Raised when some dimension doesn't match expected."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DTypeError(Exception):
|
|
72
|
+
"""Raised when array dtype doesn't match expected dtype."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Runtime validation
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _validate_dtype(
|
|
79
|
+
expected: GenericAlias | TypeVar | type[np.dtype], actual: np.dtype
|
|
80
|
+
) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Validate dtype at runtime.
|
|
83
|
+
|
|
84
|
+
### References:
|
|
85
|
+
- https://numpy.org/doc/stable/reference/arrays.dtypes.html#checking-the-data-type
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# ~TypeVar
|
|
89
|
+
if isinstance(expected, TypeVar):
|
|
90
|
+
# [TODO] Verify bounds, contraints, default
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# np.dtype[...]
|
|
94
|
+
if isinstance(expected, GenericAlias):
|
|
95
|
+
if get_origin(expected) is np.dtype:
|
|
96
|
+
if not (args := get_args(expected)):
|
|
97
|
+
return None
|
|
98
|
+
assert len(args) == 1
|
|
99
|
+
exp = args[0]
|
|
100
|
+
|
|
101
|
+
# np.dtype[Any]
|
|
102
|
+
if exp is Any:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
# np.dtype[~TypeVar]
|
|
106
|
+
elif isinstance(exp, TypeVar):
|
|
107
|
+
# [TODO] Verify bounds, contraints, default
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
# np.dtype[A | B | ...]
|
|
111
|
+
elif get_origin(exp) is UnionType:
|
|
112
|
+
args = get_args(exp)
|
|
113
|
+
for arg in args:
|
|
114
|
+
if actual == arg:
|
|
115
|
+
break
|
|
116
|
+
if isinstance(arg, TypeVar):
|
|
117
|
+
return None
|
|
118
|
+
else:
|
|
119
|
+
raise DTypeError(f"expected {exp}, got {actual}")
|
|
120
|
+
|
|
121
|
+
# np.dtype[<subclass of np.generic>]
|
|
122
|
+
else:
|
|
123
|
+
if actual != exp:
|
|
124
|
+
raise DTypeError(f"expected {exp.__name__}, got {actual}")
|
|
125
|
+
else:
|
|
126
|
+
# [TODO]: Handle typing.Annotated
|
|
127
|
+
raise TypeError(f"Invalid dtype specification. {expected} is not a dtype")
|
|
128
|
+
|
|
129
|
+
# <class np.dtype>
|
|
130
|
+
if expected is np.dtype:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
return None # Fallback
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _resolve_shape(args: _AnyShape) -> _AnyShape:
|
|
137
|
+
from typingkit._typed.dimexpr import _resolve_dim
|
|
138
|
+
|
|
139
|
+
# [TODO]: Handle TypeAliasType
|
|
140
|
+
return tuple(_resolve_dim(arg) for arg in args)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _validate_shape(expected: _AnyShape, actual: _Shape) -> None:
|
|
144
|
+
"""Validate shapes at runtime."""
|
|
145
|
+
|
|
146
|
+
# tuple[T, ...] (variadic shape)
|
|
147
|
+
is_variadic = len(expected) == 2 and expected[1] is Ellipsis
|
|
148
|
+
|
|
149
|
+
## Rank enforcement
|
|
150
|
+
# In the variadic case, rank is not enforced.
|
|
151
|
+
if not is_variadic:
|
|
152
|
+
if len(expected) != len(actual):
|
|
153
|
+
raise RankError(f"Expected {len(expected)} dimensions, got {len(actual)}")
|
|
154
|
+
|
|
155
|
+
## Shape enforcement
|
|
156
|
+
|
|
157
|
+
if is_variadic:
|
|
158
|
+
dim_specs = tuple([expected[0]] * len(actual))
|
|
159
|
+
else:
|
|
160
|
+
dim_specs = expected
|
|
161
|
+
|
|
162
|
+
bindings = dict[TypeVar, tuple[int, int]]()
|
|
163
|
+
# store: TypeVar -> (first_index, first_value)
|
|
164
|
+
|
|
165
|
+
for idx, (exp, act) in enumerate(zip(dim_specs, actual)):
|
|
166
|
+
origin = get_origin(exp)
|
|
167
|
+
|
|
168
|
+
# Literal
|
|
169
|
+
if origin is Literal:
|
|
170
|
+
args = get_args(exp)
|
|
171
|
+
if act not in args:
|
|
172
|
+
expected_str = f"{args[0]}" if len(args) == 1 else f"one of {set(args)}"
|
|
173
|
+
raise DimensionError(
|
|
174
|
+
f"Dimension {idx}: expected {expected_str}, got {act}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# TypeVar
|
|
178
|
+
elif isinstance(exp, TypeVar):
|
|
179
|
+
if exp in bindings:
|
|
180
|
+
prev_index, prev_value = bindings[exp]
|
|
181
|
+
if prev_value != act:
|
|
182
|
+
raise ShapeError(
|
|
183
|
+
f"Inconsistent dimensions.\n"
|
|
184
|
+
f"Found Dimension {prev_index} to be {prev_value} and Dimension {idx} to be {act}"
|
|
185
|
+
f" but both were constrained to the same TypeVar {exp}."
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
bindings[exp] = (idx, act)
|
|
189
|
+
|
|
190
|
+
# (Concrete) int
|
|
191
|
+
elif isinstance(exp, int):
|
|
192
|
+
if exp != act:
|
|
193
|
+
raise ShapeError(f"Shape mismatch: expected {expected}, got {actual}")
|
|
194
|
+
|
|
195
|
+
# Relaxed dimension
|
|
196
|
+
elif exp is int or exp is Any:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _validate_shape_against_contexts(shape_spec: _AnyShape, actual: _Shape) -> None:
|
|
201
|
+
"""Validate shape against active TypeVar contexts (class-level and method-level)."""
|
|
202
|
+
from typingkit._typed.context import (
|
|
203
|
+
_active_class_context,
|
|
204
|
+
_method_typevar_context,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
method_context = _method_typevar_context.get()
|
|
208
|
+
class_context = _active_class_context.get()
|
|
209
|
+
typevar_bindings = dict[TypeVar, int]()
|
|
210
|
+
for idx, dim in enumerate(shape_spec):
|
|
211
|
+
if not isinstance(dim, TypeVar):
|
|
212
|
+
continue
|
|
213
|
+
if idx >= len(actual):
|
|
214
|
+
continue
|
|
215
|
+
actual_dim = actual[idx]
|
|
216
|
+
# Check method_context first before class_context
|
|
217
|
+
expected_dim = method_context.get(dim) or class_context.get(dim)
|
|
218
|
+
if expected_dim is not None and actual_dim != expected_dim:
|
|
219
|
+
raise ShapeError(
|
|
220
|
+
f"TypeVar {dim} mismatch at dimension {idx}: "
|
|
221
|
+
f"expected {expected_dim}, got {actual_dim}"
|
|
222
|
+
)
|
|
223
|
+
# Consistency check
|
|
224
|
+
if dim in typevar_bindings:
|
|
225
|
+
if actual_dim != typevar_bindings[dim]:
|
|
226
|
+
raise ShapeError(
|
|
227
|
+
f"TypeVar {dim} inconsistent: dimension {idx} is {actual_dim}, "
|
|
228
|
+
f"but previous occurrence required {typevar_bindings[dim]}"
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
typevar_bindings[dim] = actual_dim
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
## Typed NDArray
|
|
235
|
+
class TypedNDArray(np.ndarray[_ShapeT_co, _DTypeT_co]):
|
|
236
|
+
"""Generic `numpy.ndarray` subclass with static shape typing and runtime shape validation."""
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def __class_getitem__(cls, item: Any, /) -> GenericAlias:
|
|
240
|
+
# [HACK] Misuses __class_getitem__
|
|
241
|
+
# See https://docs.python.org/3/reference/datamodel.html#the-purpose-of-class-getitem
|
|
242
|
+
|
|
243
|
+
# This method is called when using `TypedNDArray` with generics as in `TypedNDArray[...]`.
|
|
244
|
+
# The arguments can be just a Shape GenericAlias such as `tuple[...]`,
|
|
245
|
+
# which is allowed since `_DTypeT_co` TypeVar defines a default.
|
|
246
|
+
# OR it is a Shape GenericAlias and a DType GenericAlias as `tuple[...], np.dtype[...]`
|
|
247
|
+
# which is passed in to `__class_getitem__` as a `tuple[GenericAlias, GenericAlias]`.
|
|
248
|
+
# Any other case would result in a static error already.
|
|
249
|
+
|
|
250
|
+
# We defer the arguments to `_TypedNDArrayGenericAlias` which is a subclass of `GenericAlias`. It has two roles:
|
|
251
|
+
# 1. Support handling further partial binding just as the type system expects.
|
|
252
|
+
# This is done through `_TypedNDArrayGenericAlias.__getitem__` which just ensures that
|
|
253
|
+
# `_TypedNDArrayGenericAlias` wraps the `GenericAlias` when partial binding.
|
|
254
|
+
# 2. Transfer the control back to `TypedNDArray` when its `_TypedNDArrayGenericAlias.__call__` method is called,
|
|
255
|
+
# which should then invoke `TypedNDArray.__new__`.
|
|
256
|
+
# Additionally we can use the bindings here to perform runtime validation.
|
|
257
|
+
|
|
258
|
+
ga = super().__class_getitem__(item)
|
|
259
|
+
return _TypedNDArrayGenericAlias.from_generic_alias(ga)
|
|
260
|
+
|
|
261
|
+
# [FIXME]: Can we skip this method? It just uses
|
|
262
|
+
# `np.asarray(object, dtype=dtype).view(cls)`
|
|
263
|
+
# all of which are regular `numpy`.
|
|
264
|
+
@overload
|
|
265
|
+
def __new__( # type: ignore[misc]
|
|
266
|
+
cls,
|
|
267
|
+
object: np._ArrayT,
|
|
268
|
+
dtype: None = None,
|
|
269
|
+
*,
|
|
270
|
+
copy: bool | np._CopyMode | None = True,
|
|
271
|
+
order: np._OrderKACF = "K",
|
|
272
|
+
subok: L[True],
|
|
273
|
+
ndmin: int = 0,
|
|
274
|
+
like: npt_._SupportsArrayFunc | None = None,
|
|
275
|
+
) -> np._ArrayT: ...
|
|
276
|
+
@overload
|
|
277
|
+
def __new__( # type: ignore[misc]
|
|
278
|
+
cls,
|
|
279
|
+
object: _SupportsArray[np._ArrayT],
|
|
280
|
+
dtype: None = None,
|
|
281
|
+
*,
|
|
282
|
+
copy: bool | np._CopyMode | None = True,
|
|
283
|
+
order: np._OrderKACF = "K",
|
|
284
|
+
subok: L[True],
|
|
285
|
+
ndmin: L[0] = 0,
|
|
286
|
+
like: npt_._SupportsArrayFunc | None = None,
|
|
287
|
+
) -> np._ArrayT: ...
|
|
288
|
+
@overload
|
|
289
|
+
def __new__(
|
|
290
|
+
cls,
|
|
291
|
+
object: npt_._ArrayLike[np._ScalarT],
|
|
292
|
+
dtype: None = None,
|
|
293
|
+
*,
|
|
294
|
+
copy: bool | np._CopyMode | None = True,
|
|
295
|
+
order: np._OrderKACF = "K",
|
|
296
|
+
subok: bool = False,
|
|
297
|
+
ndmin: int = 0,
|
|
298
|
+
like: npt_._SupportsArrayFunc | None = None,
|
|
299
|
+
) -> "TypedNDArray[_ShapeT_co, np.dtype[np._ScalarT]]": ...
|
|
300
|
+
@overload
|
|
301
|
+
# NOTE: This is prolly the best we can do without HKTs
|
|
302
|
+
def __new__(
|
|
303
|
+
cls,
|
|
304
|
+
object: Any,
|
|
305
|
+
dtype: npt_._DTypeLike[np._ScalarT],
|
|
306
|
+
*,
|
|
307
|
+
copy: bool | np._CopyMode | None = True,
|
|
308
|
+
order: np._OrderKACF = "K",
|
|
309
|
+
subok: bool = False,
|
|
310
|
+
ndmin: int = 0,
|
|
311
|
+
like: npt_._SupportsArrayFunc | None = None,
|
|
312
|
+
) -> "TypedNDArray[_ShapeT_co, np.dtype[np._ScalarT]]": ...
|
|
313
|
+
@overload
|
|
314
|
+
def __new__(
|
|
315
|
+
cls,
|
|
316
|
+
object: Any,
|
|
317
|
+
dtype: npt.DTypeLike | None = None,
|
|
318
|
+
*,
|
|
319
|
+
copy: bool | np._CopyMode | None = True,
|
|
320
|
+
order: np._OrderKACF = "K",
|
|
321
|
+
subok: bool = False,
|
|
322
|
+
ndmin: int = 0,
|
|
323
|
+
like: npt_._SupportsArrayFunc | None = None,
|
|
324
|
+
) -> "TypedNDArray[_ShapeT_co, np.dtype[Any]]": ...
|
|
325
|
+
#
|
|
326
|
+
def __new__( # type: ignore[misc]
|
|
327
|
+
cls,
|
|
328
|
+
object: npt.ArrayLike,
|
|
329
|
+
dtype: npt.DTypeLike | None = None,
|
|
330
|
+
# NOTE: This is prolly the best we can do without HKTs
|
|
331
|
+
*,
|
|
332
|
+
copy: bool | np._CopyMode | None = True,
|
|
333
|
+
order: np._OrderKACF = "K",
|
|
334
|
+
subok: bool = False,
|
|
335
|
+
ndmin: int = 0,
|
|
336
|
+
like: npt_._SupportsArrayFunc | None = None,
|
|
337
|
+
) -> Self:
|
|
338
|
+
# Overrides base; This doesn't follow `numpy.ndarray.__new__`,
|
|
339
|
+
# but rather tries to mimick `numpy.array(...)`;
|
|
340
|
+
|
|
341
|
+
arr = np.array(
|
|
342
|
+
object, dtype, copy=copy, order=order, subok=subok, ndmin=ndmin, like=like
|
|
343
|
+
)
|
|
344
|
+
# The regular `numpy.ndarray` machinery is put to use here,
|
|
345
|
+
# basically making `TypedNDArray` just a type wrapper around it, just as intended.
|
|
346
|
+
|
|
347
|
+
# The `.view(...)` method should be used when subclassing `numpy.ndarray`.
|
|
348
|
+
obj = arr.view(cls)
|
|
349
|
+
obj = cast(Self, obj) # pyright: ignore[reportUnnecessaryCast] # pyrefly: ignore [redundant-cast]
|
|
350
|
+
return obj
|
|
351
|
+
|
|
352
|
+
def __array_finalize__(self, obj: npt.NDArray[Any] | None, /) -> None:
|
|
353
|
+
if obj is None:
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
def __repr__(self) -> str:
|
|
357
|
+
return str(np.asarray(self).__repr__())
|
|
358
|
+
|
|
359
|
+
@overload # == 0D --> Not iterable
|
|
360
|
+
def __iter__(self: "TypedNDArray[tuple[()], _DTypeT_co]", /) -> NoReturn: ...
|
|
361
|
+
@overload # == 1-d & dtype[T \ object_]
|
|
362
|
+
def __iter__(
|
|
363
|
+
self: "TypedNDArray[tuple[int], np.dtype[_NonObjectScalarT]]", /
|
|
364
|
+
) -> Iterator[_NonObjectScalarT]: ...
|
|
365
|
+
@overload # == 1-d & StringDType
|
|
366
|
+
def __iter__(
|
|
367
|
+
self: "TypedNDArray[tuple[int], np.dtypes.StringDType]", /
|
|
368
|
+
) -> Iterator[str]: ...
|
|
369
|
+
@overload # >= 1D
|
|
370
|
+
# Currently, TypeVarTuple doesn't support bounds/constraints,
|
|
371
|
+
# which we'd want to bound/constrain to `_ShapeT_co` := `tuple[int, ...]`.
|
|
372
|
+
# We can relax `_ShapeT_co` to `tuple[Any, ...]` which would make the following overload type fine,
|
|
373
|
+
# but would relax bounds for each dimension, which is a trade off.
|
|
374
|
+
# So we just allow the following ~unsafely typed overload, which is just complained here
|
|
375
|
+
# and shouldn't affect when using `__iter__` in downstream code, hopefully.
|
|
376
|
+
def __iter__(
|
|
377
|
+
self: "TypedNDArray[tuple[int, *_ShapeRest], _DTypeT_co]", # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
378
|
+
/,
|
|
379
|
+
) -> Iterator["TypedNDArray[tuple[*_ShapeRest], _DTypeT_co]"]: ... # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
380
|
+
@overload # ?-d
|
|
381
|
+
# Not required, but can keep as a fallback
|
|
382
|
+
def __iter__(self, /) -> Iterator[Any]: ... # pyright: ignore[reportOverlappingOverload]
|
|
383
|
+
#
|
|
384
|
+
def __iter__(self, /) -> Iterator[Any]: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
385
|
+
return super().__iter__()
|
|
386
|
+
|
|
387
|
+
@overload
|
|
388
|
+
def astype(
|
|
389
|
+
self,
|
|
390
|
+
dtype: npt_._DTypeLike[np._ScalarT],
|
|
391
|
+
order: np._OrderKACF = ...,
|
|
392
|
+
casting: np._CastingKind = ...,
|
|
393
|
+
subok: builtins.bool = ...,
|
|
394
|
+
copy: builtins.bool | np._CopyMode = ...,
|
|
395
|
+
) -> "TypedNDArray[_ShapeT_co, np.dtype[np._ScalarT]]": ...
|
|
396
|
+
@overload
|
|
397
|
+
def astype(
|
|
398
|
+
self,
|
|
399
|
+
dtype: npt_.DTypeLike | None,
|
|
400
|
+
order: np._OrderKACF = ...,
|
|
401
|
+
casting: np._CastingKind = ...,
|
|
402
|
+
subok: builtins.bool = ...,
|
|
403
|
+
copy: builtins.bool | np._CopyMode = ...,
|
|
404
|
+
) -> "TypedNDArray[_ShapeT_co, np.dtype]": ...
|
|
405
|
+
#
|
|
406
|
+
def astype(
|
|
407
|
+
self,
|
|
408
|
+
dtype: npt_.DTypeLike | None,
|
|
409
|
+
order: np._OrderKACF = "K",
|
|
410
|
+
casting: np._CastingKind = "unsafe",
|
|
411
|
+
subok: builtins.bool = True,
|
|
412
|
+
copy: builtins.bool | np._CopyMode = True,
|
|
413
|
+
):
|
|
414
|
+
return super().astype(
|
|
415
|
+
dtype=dtype, order=order, casting=casting, subok=subok, copy=copy
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def flatten(
|
|
419
|
+
self, /, order: np._OrderKACF = "C"
|
|
420
|
+
) -> "TypedNDArray[tuple[int], _DTypeT_co]":
|
|
421
|
+
return super().flatten(order=order) # type: ignore[return-value]
|
|
422
|
+
|
|
423
|
+
@overload # type: ignore
|
|
424
|
+
def __getitem__(
|
|
425
|
+
self: "TypedNDArray[tuple[int], np.dtype[_ScalarT_co]]", key: int
|
|
426
|
+
) -> _ScalarT_co: ...
|
|
427
|
+
@overload
|
|
428
|
+
def __getitem__(
|
|
429
|
+
self: "TypedNDArray[tuple[int, *_ShapeRest], _DTypeT_co]", # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
430
|
+
key: int,
|
|
431
|
+
) -> "TypedNDArray[tuple[*_ShapeRest], _DTypeT_co]": ... # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
432
|
+
@overload
|
|
433
|
+
def __getitem__(
|
|
434
|
+
self: "TypedNDArray[tuple[int, *_ShapeRest], _DTypeT_co]", # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
435
|
+
key: slice,
|
|
436
|
+
) -> "TypedNDArray[tuple[int, *_ShapeRest], _DTypeT_co]": ... # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
437
|
+
@overload
|
|
438
|
+
def __getitem__(
|
|
439
|
+
self: "TypedNDArray[tuple[int, *_ShapeRest], np.dtype[_ScalarT_co]]", # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
440
|
+
key: tuple[int, *_ShapeRest],
|
|
441
|
+
) -> _ScalarT_co: ...
|
|
442
|
+
@overload
|
|
443
|
+
def __getitem__(
|
|
444
|
+
self: "TypedNDArray[tuple[int, int, *_ShapeRest], _DTypeT_co]", # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
445
|
+
key: tuple[int, slice],
|
|
446
|
+
) -> "TypedNDArray[tuple[int, *_ShapeRest], _DTypeT_co]": ... # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
447
|
+
@overload
|
|
448
|
+
def __getitem__(
|
|
449
|
+
self: "TypedNDArray[tuple[int, int, int, *_ShapeRest], _DTypeT_co]", # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
450
|
+
key: tuple[int, int],
|
|
451
|
+
) -> "TypedNDArray[tuple[int, *_ShapeRest], _DTypeT_co]": ... # type: ignore[type-var] # ty: ignore[unused-ignore-comment]
|
|
452
|
+
@overload
|
|
453
|
+
def __getitem__(self, key: Any) -> Any: ...
|
|
454
|
+
#
|
|
455
|
+
def __getitem__(self, key: Any) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
456
|
+
return super().__getitem__(key) # pyright: ignore[reportUnknownVariableType]
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
## Deferred shape binding
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class _TypedNDArrayGenericAlias(GenericAlias):
|
|
463
|
+
"""
|
|
464
|
+
Deferred TypedNDArray constructor for shapes with TypeVars.
|
|
465
|
+
Enables progressive type specialization, behaving like a type-level curry.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
@classmethod
|
|
469
|
+
def from_generic_alias(cls, alias: GenericAlias) -> Self:
|
|
470
|
+
return cls(alias.__origin__, alias.__args__) # pyright: ignore[reportArgumentType]
|
|
471
|
+
|
|
472
|
+
def __getitem__(self, typeargs: Any) -> Self:
|
|
473
|
+
ga = super().__getitem__(typeargs)
|
|
474
|
+
return type(self).from_generic_alias(ga)
|
|
475
|
+
|
|
476
|
+
def __call__(
|
|
477
|
+
self,
|
|
478
|
+
object: npt.ArrayLike,
|
|
479
|
+
dtype: npt.DTypeLike | None = None,
|
|
480
|
+
*,
|
|
481
|
+
copy: bool | np._CopyMode | None = True,
|
|
482
|
+
order: np._OrderKACF = "K",
|
|
483
|
+
subok: bool = False,
|
|
484
|
+
ndmin: int = 0,
|
|
485
|
+
like: npt_._SupportsArrayFunc | None = None,
|
|
486
|
+
) -> TypedNDArray:
|
|
487
|
+
# [NOTE] Should mimick `TypedNDArray.__new__` signature
|
|
488
|
+
|
|
489
|
+
base = cast(type[TypedNDArray], get_origin(self))
|
|
490
|
+
args = get_args(self)
|
|
491
|
+
|
|
492
|
+
if len(args) == 2:
|
|
493
|
+
shape_spec, dtype_spec = args
|
|
494
|
+
elif len(args) == 1:
|
|
495
|
+
(shape_spec,) = args
|
|
496
|
+
dtype_spec = _DTypeT_co.__default__
|
|
497
|
+
# The `dtype_spec` default here should match the default in `_DTypeT_co`.
|
|
498
|
+
else:
|
|
499
|
+
raise TypeError
|
|
500
|
+
|
|
501
|
+
# Create `numpy.ndarray` object
|
|
502
|
+
arr = base(
|
|
503
|
+
object, dtype, copy=copy, order=order, subok=subok, ndmin=ndmin, like=like
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Runtime validations
|
|
507
|
+
shape_args = _resolve_shape(get_args(shape_spec))
|
|
508
|
+
arr_shape, arr_dtype = arr.shape, arr.dtype
|
|
509
|
+
_validate_shape(shape_args, arr_shape)
|
|
510
|
+
_validate_shape_against_contexts(shape_args, arr_shape)
|
|
511
|
+
_validate_dtype(dtype_spec, arr_dtype)
|
|
512
|
+
|
|
513
|
+
return arr.view(TypedNDArray)
|
typingkit/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: typingkit
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Python strong typing suite, along with Typed NumPy: Static shape typing and runtime shape validation.
|
|
5
|
+
Author: Ashrith Sagar
|
|
6
|
+
Author-email: Ashrith Sagar <ashrith9sagar@gmail.com>
|
|
7
|
+
Requires-Dist: numpy>=2.2
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# typingkit
|
|
12
|
+
|
|
13
|
+
[](https://github.com/astral-sh/ruff)
|
|
14
|
+
|
|
15
|
+
Python strong typing suite, along with Typed NumPy: Static shape typing and runtime shape validation.
|
|
16
|
+
|
|
17
|
+
> [!WARNING]
|
|
18
|
+
> Experimental & WIP.
|
|
19
|
+
> See [USAGE.md](USAGE.md) for more details.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
<details>
|
|
24
|
+
|
|
25
|
+
<summary>Install uv (optional, recommended)</summary>
|
|
26
|
+
|
|
27
|
+
Install [`uv`](https://docs.astral.sh/uv/), if not already.
|
|
28
|
+
Check [here](https://docs.astral.sh/uv/getting-started/installation/) for installation instructions.
|
|
29
|
+
|
|
30
|
+
It is recommended to use `uv`, as it will automatically install the dependencies in a virtual environment.
|
|
31
|
+
If you don't want to use `uv`, skip to the next step.
|
|
32
|
+
|
|
33
|
+
**TL;DR: Just run**
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
</details>
|
|
40
|
+
|
|
41
|
+
<details>
|
|
42
|
+
|
|
43
|
+
<summary>Install the package</summary>
|
|
44
|
+
|
|
45
|
+
The dependencies are listed in the [pyproject.toml](pyproject.toml) file.
|
|
46
|
+
At present, the only required dependency is `numpy`.
|
|
47
|
+
|
|
48
|
+
Install the package from the PyPI release:
|
|
49
|
+
|
|
50
|
+
```shell
|
|
51
|
+
# Using uv
|
|
52
|
+
uv add typingkit
|
|
53
|
+
|
|
54
|
+
# Or with pip
|
|
55
|
+
pip3 install typingkit
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
To install from the latest commit:
|
|
59
|
+
|
|
60
|
+
```shell
|
|
61
|
+
uv add git+https://github.com/AshrithSagar/typingkit.git@main
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
</details>
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from typing import TypeVar
|
|
70
|
+
|
|
71
|
+
from typingkit._typed.ndarray import TypedNDArray
|
|
72
|
+
|
|
73
|
+
# Shape variables are just regular TypeVar's
|
|
74
|
+
N = TypeVar("N", bound=int, default=int)
|
|
75
|
+
M = TypeVar("M", bound=int, default=int)
|
|
76
|
+
|
|
77
|
+
# Create aliases such as these, or use TypedNDArray directly
|
|
78
|
+
Vector = TypedNDArray[tuple[N]]
|
|
79
|
+
Matrix = TypedNDArray[tuple[M, N]]
|
|
80
|
+
|
|
81
|
+
v1 = Vector([1, 2, 3]) # Passes
|
|
82
|
+
v2 = Vector([4, 5, 6, 7]) # Also passes
|
|
83
|
+
|
|
84
|
+
v3 = TypedNDArray[tuple[int]]([[8, 9]])
|
|
85
|
+
# Fails, since expected 1D array but passed in a 2D array
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
See [USAGE.md](USAGE.md) for more details.
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
This project falls under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
typingkit/__init__.py,sha256=nKaGmvJYFnD1rpYKWA459zUKzK1zneEHJn_sQp6KGLY,98
|
|
2
|
+
typingkit/_typed/__init__.py,sha256=ZFYm5nYxMFO2nPOM6aA-XRucU5HrbMXHmozFagopz7A,154
|
|
3
|
+
typingkit/_typed/_debug.py,sha256=X8riENmHb_G0QPicX0gaLdG5lxzyMelvdQK7L69jzu4,1154
|
|
4
|
+
typingkit/_typed/context.py,sha256=zl_WBhOvQOB8N-4weLG_EIuVd3srUkdNoJwS_BUjOWI,6334
|
|
5
|
+
typingkit/_typed/dimexpr.py,sha256=HoCRL7HUwMloTnKuWdS2ftwifMyiHv7f5I3Yxqggqac,3827
|
|
6
|
+
typingkit/_typed/factory.py,sha256=rcDQG0ihFpS34XQ6Sp_15ARqaTNYf0Gm7uLi8UKcjRM,2216
|
|
7
|
+
typingkit/_typed/generics.py,sha256=-gMilWx86rP5g_HT76PPaCYMCDLF-M1MAbEWof9w97M,1646
|
|
8
|
+
typingkit/_typed/helpers.py,sha256=mxrx4LasGTZ5aB37khrhMJaHcqFMM_gSRUJsQV4qcSg,12386
|
|
9
|
+
typingkit/_typed/list.py,sha256=q-peH3y72imqBauoab25lxCC6l3W0q8AqwvyUsFh2PM,6174
|
|
10
|
+
typingkit/_typed/ndarray.py,sha256=GX-YufpLmDN3Tb4PWX4Ku997x3Vc5Na780M9SNi7UXQ,18254
|
|
11
|
+
typingkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
typingkit-0.2.2.dist-info/WHEEL,sha256=Uo4e6VmJM8J_cLBTtiWBRVQkc1yUEkw94xaL0B_lH6c,80
|
|
13
|
+
typingkit-0.2.2.dist-info/METADATA,sha256=OCVD8GeirbjUY2Oekqlft9zp9Esi9SEZzHnhpBjbnKU,2258
|
|
14
|
+
typingkit-0.2.2.dist-info/RECORD,,
|