typingkit 0.2.3__tar.gz → 0.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: typingkit
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Python strong typing suite, along with Typed NumPy: Static shape typing and runtime shape validation.
5
5
  Author: Ashrith Sagar
6
6
  Author-email: Ashrith Sagar <ashrith9sagar@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "typingkit"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Python strong typing suite, along with Typed NumPy: Static shape typing and runtime shape validation."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Ashrith Sagar", email = "ashrith9sagar@gmail.com" }]
@@ -0,0 +1,5 @@
1
+ """
2
+ TypingKit
3
+ =======
4
+ """
5
+ # src/typingkit/__init__.py
@@ -0,0 +1,17 @@
1
+ """
2
+ TypingKit core
3
+ =======
4
+ """
5
+ # src/typingkit/core/__init__.py
6
+
7
+ from typingkit.core.dict import TypedDict, TypedDictConfig
8
+ from typingkit.core.generics import RuntimeGeneric
9
+ from typingkit.core.list import TypedList, TypedListConfig
10
+
11
+ __all__ = [
12
+ "RuntimeGeneric",
13
+ "TypedList",
14
+ "TypedListConfig",
15
+ "TypedDict",
16
+ "TypedDictConfig",
17
+ ]
@@ -2,7 +2,7 @@
2
2
  Debugging utilities
3
3
  =======
4
4
  """
5
- # src/typingkit/_typed/debug.py
5
+ # src/typingkit/core/_debug.py
6
6
 
7
7
  from typing import Any, TypeVar, get_args, get_origin
8
8
 
@@ -0,0 +1,152 @@
1
+ """
2
+ TypedDict
3
+ =======
4
+ """
5
+ # src/typingkit/core/dict.py
6
+
7
+ from collections.abc import Mapping
8
+ from types import GenericAlias, NoneType, UnionType
9
+ from typing import Any, Literal, Self, TypeVar, cast, get_args, get_origin
10
+
11
+ from typingkit.core.generics import RuntimeGeneric, get_runtime_args
12
+
13
+ ## Typings
14
+
15
+ Length = TypeVar("Length", bound=int, default=int)
16
+ Key = TypeVar("Key", default=Any)
17
+ Value = TypeVar("Value", default=Any)
18
+
19
+
20
+ ## Exceptions
21
+
22
+
23
+ class LengthError(Exception):
24
+ """Raised when dict length doesn't match expected length."""
25
+
26
+
27
+ ## Runtime validation
28
+
29
+
30
+ class TypedDictConfig:
31
+ VALIDATE_LENGTH: bool = True
32
+
33
+ @classmethod
34
+ def enable_all(cls):
35
+ cls.VALIDATE_LENGTH = True
36
+
37
+ @classmethod
38
+ def disable_all(cls):
39
+ cls.VALIDATE_LENGTH = False
40
+
41
+
42
+ def _resolve_length(length: Any) -> Any:
43
+ # ~TypeVar
44
+ if isinstance(length, TypeVar):
45
+ # [TODO]: How should typing.NoDefault be handled?
46
+ length = _resolve_length(length.__default__)
47
+
48
+ origin = get_origin(length)
49
+
50
+ # Literal[A, B, ...]
51
+ if origin is Literal:
52
+ length = set(get_args(length))
53
+
54
+ # Union[A, B, ...]
55
+ if origin is UnionType:
56
+ resolved = (_resolve_length(arg) for arg in get_args(length))
57
+ result = set[Any]()
58
+ for r in resolved:
59
+ if isinstance(r, set):
60
+ result |= r
61
+ else:
62
+ result.add(r)
63
+ return result
64
+
65
+ return length
66
+
67
+
68
+ def _validate_length(object: Mapping[Key, Value], length: Any) -> None:
69
+ if not TypedDictConfig.VALIDATE_LENGTH:
70
+ return None
71
+
72
+ length = _resolve_length(length)
73
+
74
+ # type[Any]
75
+ if length is Any:
76
+ return None
77
+
78
+ # type[int]; Including <subclass of int>
79
+ if isinstance(length, type) and issubclass(length, int):
80
+ return None
81
+
82
+ # Should already be disallowed statically, here we just raise a runtime error
83
+ if length is NoneType:
84
+ raise LengthError("Invalid length")
85
+ # [TODO]: Others
86
+
87
+ # [NOTE]: From a statical perspective, being strict,
88
+ # prefer Literal[~int] over ~int, although we allow it here, for now.
89
+
90
+ actual = len(object)
91
+ if isinstance(length, set):
92
+ if len(length) > 1: # pyright: ignore[reportUnknownArgumentType]
93
+ for arg in length: # pyright: ignore[reportUnknownVariableType]
94
+ if isinstance(arg, int):
95
+ if arg == actual:
96
+ break
97
+
98
+ ## [TODO]: Similar to the outer validation, so prolly can refactor through a recursive function call
99
+ # type[Any]
100
+ if arg is Any:
101
+ break
102
+
103
+ # type[int]; Including <subclass of int>
104
+ if isinstance(arg, type) and issubclass(arg, int):
105
+ break
106
+ else:
107
+ raise LengthError(
108
+ f"Length mismatch: expected one of {length}, got {actual}"
109
+ )
110
+ elif len(length) == 1: # pyright: ignore[reportUnknownArgumentType]
111
+ # Defer to the single case below
112
+ length = length.pop() # pyright: ignore[reportUnknownVariableType]
113
+ else: # len(length) == 0
114
+ # This case should prolly never arise? Strictly speaking, statically.
115
+ return None
116
+ # (Concrete) int
117
+ if isinstance(length, int):
118
+ # This case is just for a minimal error message, we could already handle
119
+ # it in the `set` case above, provided `_resolve_length` resolves it accordingly.
120
+ if actual != length:
121
+ raise LengthError(f"Length mismatch: expected {length}, got {actual}")
122
+
123
+ return None # Fallback
124
+
125
+
126
+ ## TypedDict
127
+ class TypedDict(RuntimeGeneric[Length, Key, Value], dict[Key, Value]):
128
+ @classmethod
129
+ def __pre_new__(cls, alias: GenericAlias, *args: Any, **kwargs: Any) -> Self:
130
+ # Create `dict` object
131
+ obj = super().__pre_new__(alias, *args, **kwargs)
132
+
133
+ ## Runtime validations
134
+ typeargs = get_runtime_args(alias, cls)
135
+ if len(typeargs) == 3:
136
+ (length, _, _) = typeargs
137
+ elif len(typeargs) == 2:
138
+ (length, _) = typeargs
139
+ elif len(typeargs) == 1:
140
+ (length,) = typeargs
141
+ else:
142
+ raise TypeError
143
+ _validate_length(obj, length)
144
+
145
+ return obj
146
+
147
+ def __len__(self) -> Length:
148
+ return cast(Length, super().__len__())
149
+
150
+ @property
151
+ def length(self) -> Length:
152
+ return self.__len__()
@@ -0,0 +1,180 @@
1
+ """
2
+ Generics
3
+ =======
4
+ """
5
+ # src/typingkit/core/generics.py
6
+
7
+ from types import GenericAlias, get_original_bases
8
+ from typing import (
9
+ Any,
10
+ Generic,
11
+ Self,
12
+ TypeVar,
13
+ TypeVarTuple,
14
+ Unpack,
15
+ cast,
16
+ get_args,
17
+ get_origin,
18
+ )
19
+
20
+ Ts = TypeVarTuple("Ts")
21
+
22
+
23
+ class RuntimeGeneric(Generic[Unpack[Ts]]):
24
+ @classmethod
25
+ def __class_getitem__(cls, item: Any, /) -> GenericAlias:
26
+ # [HACK] Misuses __class_getitem__
27
+ # See https://docs.python.org/3/reference/datamodel.html#the-purpose-of-class-getitem
28
+
29
+ try:
30
+ ga = cast(GenericAlias, super().__class_getitem__(item)) # type: ignore[misc]
31
+ except: # noqa: E722
32
+ # Fallback if superclass does not implement `__class_getitem__`
33
+ ga = GenericAlias(cls, item)
34
+ return _RuntimeGenericAlias.from_generic_alias(ga)
35
+
36
+ @classmethod
37
+ def __pre_new__(cls, alias: GenericAlias, *args: Any, **kwargs: Any) -> Self:
38
+ return cls(*args, **kwargs)
39
+
40
+
41
+ class _RuntimeGenericAlias(GenericAlias):
42
+ """
43
+ Deferred RuntimeGeneric constructor.
44
+ Enables progressive type specialisation, behaving like a type-level curry.
45
+ """
46
+
47
+ @classmethod
48
+ def from_generic_alias(cls, alias: GenericAlias) -> Self:
49
+ origin = get_origin(alias)
50
+ typeargs = get_args(alias)
51
+ return cls(origin, typeargs)
52
+
53
+ def __getitem__(self, typeargs: Any) -> Self:
54
+ ga = super().__getitem__(typeargs)
55
+ return type(self).from_generic_alias(ga)
56
+
57
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
58
+ origin: type[RuntimeGeneric[Unpack[Ts]]] = get_origin(self) # type: ignore[valid-type]
59
+ obj = origin.__pre_new__(self, *args, **kwargs)
60
+ return obj
61
+
62
+
63
+ ## Generics resolution
64
+
65
+
66
+ def _substitute(tp: Any, mapping: dict[Any, Any]) -> Any:
67
+ if isinstance(tp, TypeVar):
68
+ return mapping.get(tp, tp)
69
+
70
+ if isinstance(tp, TypeVarTuple):
71
+ return mapping.get(tp, tp)
72
+
73
+ origin = get_origin(tp)
74
+ if origin is None:
75
+ return tp
76
+
77
+ args = get_args(tp)
78
+
79
+ if origin is Unpack:
80
+ (inner,) = args
81
+ value = _substitute(inner, mapping)
82
+
83
+ # If the inner resolved to a tuple (TypeVarTuple binding),
84
+ # return the tuple directly so the parent can expand it.
85
+ if isinstance(value, tuple):
86
+ return value # pyright: ignore[reportUnknownVariableType]
87
+
88
+ return Unpack[value]
89
+
90
+ new_args_list = list[Any]()
91
+ for arg in args:
92
+ val = _substitute(arg, mapping)
93
+ if isinstance(val, tuple):
94
+ new_args_list.extend(val) # pyright: ignore[reportUnknownArgumentType]
95
+ else:
96
+ new_args_list.append(val)
97
+ new_args = tuple(new_args_list)
98
+
99
+ try:
100
+ return origin[new_args]
101
+ except TypeError:
102
+ return tp
103
+
104
+
105
+ def _build_mapping(params: tuple[Any, ...], args: tuple[Any, ...]) -> dict[Any, Any]:
106
+ mapping = dict[Any, Any]()
107
+ i: int = 0
108
+ j: int = 0
109
+
110
+ while i < len(params):
111
+ p = params[i]
112
+
113
+ if isinstance(p, TypeVarTuple):
114
+ # Handle tuple unpacking
115
+ remaining_params = len(params) - i - 1
116
+ remaining_args = len(args) - j
117
+ size = remaining_args - remaining_params
118
+ if size < 0:
119
+ raise TypeError(
120
+ f"Not enough type arguments to bind TypeVarTuple {p}. "
121
+ f"Expected at least {len(params)} but got {len(args)}"
122
+ )
123
+ mapping[p] = args[j : j + size]
124
+ j += size
125
+ i += 1
126
+ continue
127
+
128
+ if j >= len(args):
129
+ # No argument supplied, try default
130
+ default = getattr(p, "__default__", None)
131
+ if default is not None:
132
+ mapping[p] = default
133
+ else:
134
+ raise TypeError(
135
+ f"Missing type argument for {p}. Expected {len(params)} args, got {len(args)}"
136
+ )
137
+ else:
138
+ mapping[p] = args[j]
139
+ j += 1
140
+
141
+ i += 1
142
+
143
+ if j < len(args):
144
+ # Extra arguments leftover
145
+ raise TypeError(
146
+ f"Too many type arguments. Expected {len(params)}, got {len(args)}"
147
+ )
148
+
149
+ return mapping
150
+
151
+
152
+ def get_runtime_args(tp: Any, cls: type) -> tuple[Any, ...]:
153
+ args = get_args(tp)
154
+
155
+ parameters: tuple[Any, ...] = getattr(cls, "__parameters__", ())
156
+ mapping = _build_mapping(parameters, args)
157
+
158
+ current_cls = cls
159
+ while True:
160
+ orig_bases = get_original_bases(current_cls)
161
+ for base in orig_bases:
162
+ origin = get_origin(base)
163
+ if origin is None:
164
+ continue
165
+
166
+ resolved = _substitute(base, mapping)
167
+
168
+ if get_origin(resolved) is RuntimeGeneric:
169
+ return get_args(resolved)
170
+
171
+ parent_params = getattr(origin, "__parameters__", ())
172
+ parent_args = get_args(resolved)
173
+
174
+ mapping = _build_mapping(parent_params, parent_args)
175
+ current_cls = origin
176
+ break
177
+ else:
178
+ break
179
+
180
+ return args
@@ -2,7 +2,7 @@
2
2
  TypedList
3
3
  =======
4
4
  """
5
- # src/typingkit/_typed/list.py
5
+ # src/typingkit/core/list.py
6
6
 
7
7
  import copy
8
8
  import numbers
@@ -10,7 +10,7 @@ from collections.abc import Iterable, Sequence
10
10
  from types import GenericAlias, NoneType, UnionType
11
11
  from typing import Any, Callable, Literal, Self, TypeVar, cast, get_args, get_origin
12
12
 
13
- from typingkit._typed.generics import RuntimeGeneric
13
+ from typingkit.core.generics import RuntimeGeneric, get_runtime_args
14
14
 
15
15
  ## Typings
16
16
 
@@ -168,7 +168,7 @@ class TypedList(RuntimeGeneric[Length, Item], list[Item]):
168
168
  obj = super().__pre_new__(alias, *args, **kwargs)
169
169
 
170
170
  ## Runtime validations
171
- typeargs = get_args(alias)
171
+ typeargs = get_runtime_args(alias, cls)
172
172
  if len(typeargs) == 2:
173
173
  length, item_type = typeargs
174
174
  elif len(typeargs) == 1:
@@ -193,11 +193,7 @@ class TypedList(RuntimeGeneric[Length, Item], list[Item]):
193
193
  return self.__len__()
194
194
 
195
195
  @classmethod
196
- def full(
197
- cls: "type[TypedList[Length, Item]]",
198
- length: Length,
199
- fill_value: Item | Callable[[int], Item],
200
- ) -> "TypedList[Length, Item]":
196
+ def full(cls, length: Length, fill_value: Item | Callable[[int], Item]) -> Self:
201
197
  data: list[Item]
202
198
  if callable(fill_value):
203
199
  data = [cast(Item, fill_value(i)) for i in range(length)]
@@ -0,0 +1,15 @@
1
+ """
2
+ Typed NumPy
3
+ =======
4
+ """
5
+ # src/typingkit/numpy/__init__.py
6
+
7
+ import numpy
8
+
9
+ from typingkit.numpy._typed import TypedNDArray, enforce_shapes
10
+
11
+ __all__ = [
12
+ "numpy",
13
+ "TypedNDArray",
14
+ "enforce_shapes",
15
+ ]
@@ -0,0 +1,13 @@
1
+ """
2
+ Typed NumPy core
3
+ =======
4
+ """
5
+ # src/typingkit/numpy/_typed/__init__.py
6
+
7
+ from typingkit.numpy._typed.context import enforce_shapes
8
+ from typingkit.numpy._typed.ndarray import TypedNDArray
9
+
10
+ __all__ = [
11
+ "TypedNDArray",
12
+ "enforce_shapes",
13
+ ]
@@ -3,7 +3,7 @@ Context binding
3
3
  =======
4
4
  Manages TypeVar binding contexts for shape validation.
5
5
  """
6
- # src/typingkit/_typed/context.py
6
+ # src/typingkit/numpy/_typed/context.py
7
7
 
8
8
  # pyright: reportPrivateUsage = false
9
9
 
@@ -21,7 +21,7 @@ from typing import (
21
21
  get_type_hints,
22
22
  )
23
23
 
24
- from typingkit._typed.ndarray import (
24
+ from typingkit.numpy._typed.ndarray import (
25
25
  DimensionError,
26
26
  _validate_shape,
27
27
  _validate_shape_against_contexts,
@@ -2,7 +2,7 @@
2
2
  DimExpr
3
3
  =======
4
4
  """
5
- # src/typingkit/_typed/dimexpr.py
5
+ # src/typingkit/numpy/_typed/dimexpr.py
6
6
 
7
7
  import math
8
8
  from typing import (
@@ -2,7 +2,7 @@
2
2
  Shape factory for TypedNDArray
3
3
  =======
4
4
  """
5
- # src/typingkit/_typed/factory.py
5
+ # src/typingkit/numpy/_typed/factory.py
6
6
 
7
7
  # pyright: reportPrivateUsage = false
8
8
 
@@ -12,7 +12,7 @@ from typing import Any, Generic, TypeVar
12
12
  import numpy as np
13
13
  import numpy.typing as npt
14
14
 
15
- from typingkit._typed.ndarray import TypedNDArray, _AnyShape
15
+ from typingkit.numpy._typed.ndarray import TypedNDArray, _AnyShape
16
16
 
17
17
  _ShapeT = TypeVar("_ShapeT", bound=_AnyShape)
18
18
  _ScalarT = TypeVar("_ScalarT", bound=np.generic)
@@ -2,13 +2,13 @@
2
2
  Helpers for TypedNDArray
3
3
  =======
4
4
  """
5
- # src/typingkit/_typed/helpers.py
5
+ # src/typingkit/numpy/_typed/helpers.py
6
6
 
7
7
  from typing import Literal, TypeAlias, TypeVar
8
8
 
9
9
  import numpy as np
10
10
 
11
- from typingkit._typed.ndarray import TypedNDArray
11
+ from typingkit.numpy._typed.ndarray import TypedNDArray
12
12
 
13
13
  ## Helpers
14
14
 
@@ -2,7 +2,7 @@
2
2
  NDArray
3
3
  =======
4
4
  """
5
- # src/typingkit/_typed/ndarray.py
5
+ # src/typingkit/numpy/_typed/ndarray.py
6
6
 
7
7
  # pyright: reportPrivateUsage = false
8
8
 
@@ -134,7 +134,7 @@ def _validate_dtype(
134
134
 
135
135
 
136
136
  def _resolve_shape(args: _AnyShape) -> _AnyShape:
137
- from typingkit._typed.dimexpr import _resolve_dim
137
+ from typingkit.numpy._typed.dimexpr import _resolve_dim
138
138
 
139
139
  # [TODO]: Handle TypeAliasType
140
140
  return tuple(_resolve_dim(arg) for arg in args)
@@ -199,7 +199,7 @@ def _validate_shape(expected: _AnyShape, actual: _Shape) -> None:
199
199
 
200
200
  def _validate_shape_against_contexts(shape_spec: _AnyShape, actual: _Shape) -> None:
201
201
  """Validate shape against active TypeVar contexts (class-level and method-level)."""
202
- from typingkit._typed.context import (
202
+ from typingkit.numpy._typed.context import (
203
203
  _active_class_context,
204
204
  _method_typevar_context,
205
205
  )
@@ -1,11 +0,0 @@
1
- """
2
- Typed NumPy
3
- =======
4
- """
5
- # src/typingkit/__init__.py
6
-
7
- import numpy
8
-
9
- __all__ = [
10
- "numpy",
11
- ]
@@ -1,11 +0,0 @@
1
- """
2
- Typed NumPy core
3
- =======
4
- """
5
- # src/typingkit/_typed/__init__.py
6
-
7
- from typingkit._typed.ndarray import TypedNDArray
8
-
9
- __all__ = [
10
- "TypedNDArray",
11
- ]
@@ -1,50 +0,0 @@
1
- """
2
- Generics
3
- =======
4
- """
5
- # src/typingkit/_typed/generics.py
6
-
7
- from types import GenericAlias
8
- from typing import Any, Generic, Self, TypeVarTuple, Unpack, cast, get_args, get_origin
9
-
10
- Ts = TypeVarTuple("Ts", default=Unpack[tuple[Any, ...]])
11
-
12
-
13
- class RuntimeGeneric(Generic[Unpack[Ts]]):
14
- @classmethod
15
- def __class_getitem__(cls, item: Any, /) -> GenericAlias:
16
- # [HACK] Misuses __class_getitem__
17
- # See https://docs.python.org/3/reference/datamodel.html#the-purpose-of-class-getitem
18
-
19
- try:
20
- ga = cast(GenericAlias, super().__class_getitem__(item)) # type: ignore[misc]
21
- except: # noqa: E722
22
- # Fallback if superclass does not implement `__class_getitem__`
23
- ga = GenericAlias(cls, item)
24
- return _RuntimeGenericAlias.from_generic_alias(ga)
25
-
26
- @classmethod
27
- def __pre_new__(cls, alias: GenericAlias, *args: Any, **kwargs: Any) -> Self:
28
- return cls(*args, **kwargs)
29
-
30
-
31
- class _RuntimeGenericAlias(GenericAlias):
32
- """
33
- Deferred RuntimeGeneric constructor.
34
- Enables progressive type specialisation, behaving like a type-level curry.
35
- """
36
-
37
- @classmethod
38
- def from_generic_alias(cls, alias: GenericAlias) -> Self:
39
- origin = get_origin(alias)
40
- typeargs = get_args(alias)
41
- return cls(origin, typeargs)
42
-
43
- def __getitem__(self, typeargs: Any) -> Self:
44
- ga = super().__getitem__(typeargs)
45
- return type(self).from_generic_alias(ga)
46
-
47
- def __call__(self, *args: Any, **kwargs: Any) -> Any:
48
- origin: type[RuntimeGeneric] = get_origin(self)
49
- obj = origin.__pre_new__(self, *args, **kwargs)
50
- return obj
File without changes