typingkit 0.2.4__tar.gz → 0.2.6__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.
- {typingkit-0.2.4 → typingkit-0.2.6}/PKG-INFO +1 -1
- {typingkit-0.2.4 → typingkit-0.2.6}/pyproject.toml +7 -4
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/core/__init__.py +4 -4
- typingkit-0.2.6/src/typingkit/core/_config.py +26 -0
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/core/_debug.py +4 -3
- typingkit-0.2.6/src/typingkit/core/_validators.py +105 -0
- typingkit-0.2.6/src/typingkit/core/dict.py +60 -0
- typingkit-0.2.6/src/typingkit/core/generics.py +515 -0
- typingkit-0.2.6/src/typingkit/core/list.py +130 -0
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/numpy/_typed/ndarray.py +1 -1
- typingkit-0.2.4/src/typingkit/core/dict.py +0 -152
- typingkit-0.2.4/src/typingkit/core/generics.py +0 -180
- typingkit-0.2.4/src/typingkit/core/list.py +0 -202
- {typingkit-0.2.4 → typingkit-0.2.6}/README.md +0 -0
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/__init__.py +0 -0
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/numpy/__init__.py +0 -0
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/numpy/_typed/__init__.py +0 -0
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/numpy/_typed/context.py +0 -0
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/numpy/_typed/dimexpr.py +0 -0
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/numpy/_typed/factory.py +0 -0
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/numpy/_typed/helpers.py +0 -0
- {typingkit-0.2.4 → typingkit-0.2.6}/src/typingkit/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "typingkit"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.6"
|
|
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" }]
|
|
@@ -8,20 +8,23 @@ requires-python = ">=3.13"
|
|
|
8
8
|
dependencies = ["numpy>=2.2"]
|
|
9
9
|
|
|
10
10
|
[build-system]
|
|
11
|
-
requires = ["uv_build>=0.10.
|
|
11
|
+
requires = ["uv_build>=0.10.9"]
|
|
12
12
|
build-backend = "uv_build"
|
|
13
13
|
|
|
14
14
|
[dependency-groups]
|
|
15
15
|
dev = ["rich>=14.3.3"]
|
|
16
16
|
test = ["pytest>=9.0.2", "pytest-memray>=1.8.0"]
|
|
17
|
-
lint = ["ruff>=0.15.
|
|
17
|
+
lint = ["ruff>=0.15.5"]
|
|
18
18
|
typecheck = [
|
|
19
19
|
"basedpyright>=1.38.2",
|
|
20
20
|
"mypy[mypyc]>=1.19.1",
|
|
21
21
|
"pyrefly>=0.55.0",
|
|
22
22
|
"pyright>=1.1.408",
|
|
23
|
-
"ty>=0.0.
|
|
23
|
+
"ty>=0.0.21",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
+
[tool.mypy]
|
|
27
|
+
enable_incomplete_feature = ["TypeForm"]
|
|
28
|
+
|
|
26
29
|
[tool.uv]
|
|
27
30
|
default-groups = "all"
|
|
@@ -4,14 +4,14 @@ TypingKit core
|
|
|
4
4
|
"""
|
|
5
5
|
# src/typingkit/core/__init__.py
|
|
6
6
|
|
|
7
|
-
from typingkit.core.
|
|
7
|
+
from typingkit.core._config import TypedCollectionConfig
|
|
8
|
+
from typingkit.core.dict import TypedDict
|
|
8
9
|
from typingkit.core.generics import RuntimeGeneric
|
|
9
|
-
from typingkit.core.list import TypedList
|
|
10
|
+
from typingkit.core.list import TypedList
|
|
10
11
|
|
|
11
12
|
__all__ = [
|
|
12
13
|
"RuntimeGeneric",
|
|
13
14
|
"TypedList",
|
|
14
|
-
"TypedListConfig",
|
|
15
15
|
"TypedDict",
|
|
16
|
-
"
|
|
16
|
+
"TypedCollectionConfig",
|
|
17
17
|
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime validation configuration
|
|
3
|
+
=======
|
|
4
|
+
"""
|
|
5
|
+
# src/typingkit/core/_config.py
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"TypedCollectionConfig",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TypedCollectionConfig:
|
|
13
|
+
"""Global switches for runtime validation in TypedList and TypedDict."""
|
|
14
|
+
|
|
15
|
+
VALIDATE_LENGTH: bool = True
|
|
16
|
+
VALIDATE_ITEM: bool = True
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def enable_all(cls) -> None:
|
|
20
|
+
cls.VALIDATE_LENGTH = True
|
|
21
|
+
cls.VALIDATE_ITEM = True
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def disable_all(cls) -> None:
|
|
25
|
+
cls.VALIDATE_LENGTH = False
|
|
26
|
+
cls.VALIDATE_ITEM = False
|
|
@@ -7,9 +7,10 @@ Debugging utilities
|
|
|
7
7
|
from typing import Any, TypeVar, get_args, get_origin
|
|
8
8
|
|
|
9
9
|
from rich.tree import Tree
|
|
10
|
+
from typing_extensions import TypeForm
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def diagnostic(obj: Any, pfx: str | None = None) -> Tree:
|
|
13
|
+
def diagnostic(obj: object | TypeForm[Any], pfx: str | None = None) -> Tree:
|
|
13
14
|
_pfx = f"[bold cyan]{pfx}[/] " if pfx else ""
|
|
14
15
|
tree = Tree(
|
|
15
16
|
f"{_pfx}[yellow]obj[/]=[green]{obj!r}[/], "
|
|
@@ -17,8 +18,8 @@ def diagnostic(obj: Any, pfx: str | None = None) -> Tree:
|
|
|
17
18
|
)
|
|
18
19
|
match obj:
|
|
19
20
|
case tuple():
|
|
20
|
-
for x in obj: #
|
|
21
|
-
tree.add(diagnostic(x))
|
|
21
|
+
for x in obj: # pyright: ignore[reportUnknownVariableType]
|
|
22
|
+
tree.add(diagnostic(x)) # pyright: ignore[reportUnknownArgumentType]
|
|
22
23
|
|
|
23
24
|
case TypeVar():
|
|
24
25
|
tree.add(diagnostic(obj.__bound__, "__bound__:"))
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime validators
|
|
3
|
+
=======
|
|
4
|
+
"""
|
|
5
|
+
# src/typingkit/core/_validators.py
|
|
6
|
+
|
|
7
|
+
from collections.abc import Sized
|
|
8
|
+
from types import NoneType, UnionType
|
|
9
|
+
from typing import Any, Literal, TypeVar, get_args, get_origin
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"LengthError",
|
|
13
|
+
"validate_length",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LengthError(Exception):
|
|
18
|
+
"""Raised when an object's length doesn't match the expected length."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Sentinel returned by _coerce_length when a length constraint is trivially satisfied.
|
|
22
|
+
_UNCONSTRAINED = object()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _coerce_length(length: Any) -> Any:
|
|
26
|
+
"""
|
|
27
|
+
Normalise a length annotation into one of:
|
|
28
|
+
- ``_UNCONSTRAINED`` — no check needed (TypeVar with no default, Any, bare int type)
|
|
29
|
+
- ``int`` — exact required length
|
|
30
|
+
- ``frozenset[int]`` — one of several allowed lengths (from Literal / Union)
|
|
31
|
+
|
|
32
|
+
Raises ``LengthError`` for explicitly invalid annotations (e.g. ``NoneType``).
|
|
33
|
+
"""
|
|
34
|
+
# Unbound TypeVar — fall back to its default, or treat as unconstrained
|
|
35
|
+
if isinstance(length, TypeVar):
|
|
36
|
+
default = getattr(length, "__default__", None)
|
|
37
|
+
if default is None or default is length:
|
|
38
|
+
return _UNCONSTRAINED
|
|
39
|
+
return _coerce_length(default)
|
|
40
|
+
|
|
41
|
+
# Any / bare int supertype — unconstrained
|
|
42
|
+
if length is Any:
|
|
43
|
+
return _UNCONSTRAINED
|
|
44
|
+
if isinstance(length, type):
|
|
45
|
+
if length is NoneType:
|
|
46
|
+
raise LengthError(f"Invalid length annotation: {length!r}")
|
|
47
|
+
if issubclass(length, int):
|
|
48
|
+
return _UNCONSTRAINED # e.g. Length=int means "any integer length"
|
|
49
|
+
|
|
50
|
+
origin = get_origin(length)
|
|
51
|
+
|
|
52
|
+
# Literal[a, b, ...] -> frozenset of ints
|
|
53
|
+
if origin is Literal:
|
|
54
|
+
return _coerce_union_args(get_args(length))
|
|
55
|
+
|
|
56
|
+
# X | Y | Z -> union of coerced results
|
|
57
|
+
if origin is UnionType:
|
|
58
|
+
return _coerce_union_args(get_args(length))
|
|
59
|
+
|
|
60
|
+
# Concrete int (e.g. Literal[3] already resolved, or bare 3)
|
|
61
|
+
if isinstance(length, int):
|
|
62
|
+
return length
|
|
63
|
+
|
|
64
|
+
return _UNCONSTRAINED # Unknown annotation shape — Skip
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _coerce_union_args(args: tuple[Any, ...]) -> Any:
|
|
68
|
+
"""Collapse a sequence of length annotations into a single ``frozenset`` or scalar."""
|
|
69
|
+
collected = set[int]()
|
|
70
|
+
for arg in args:
|
|
71
|
+
coerced = _coerce_length(arg)
|
|
72
|
+
if coerced is _UNCONSTRAINED:
|
|
73
|
+
return _UNCONSTRAINED # Any branch being unconstrained -> whole union is
|
|
74
|
+
if isinstance(coerced, frozenset):
|
|
75
|
+
collected |= coerced
|
|
76
|
+
else:
|
|
77
|
+
collected.add(coerced)
|
|
78
|
+
return frozenset(collected) if len(collected) != 1 else next(iter(collected)) # type: ignore[arg-type]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_length(obj: Sized, length: Any) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Assert that ``len(obj)`` satisfies the ``length`` annotation.
|
|
84
|
+
|
|
85
|
+
``length`` may be a concrete ``int``, a ``TypeVar``, ``Literal[...]``,
|
|
86
|
+
a ``Union`` of the above, or ``Any``. Unconstrained annotations (bare
|
|
87
|
+
``int`` type, unbound ``TypeVar``, ``Any``) are accepted without a check.
|
|
88
|
+
"""
|
|
89
|
+
constraint = _coerce_length(length)
|
|
90
|
+
if constraint is _UNCONSTRAINED:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
actual = len(obj)
|
|
94
|
+
|
|
95
|
+
if isinstance(constraint, frozenset):
|
|
96
|
+
if actual not in constraint:
|
|
97
|
+
allowed = ", ".join(str(val) for val in sorted(constraint)) # pyright: ignore[reportUnknownArgumentType, reportUnknownVariableType]
|
|
98
|
+
raise LengthError(
|
|
99
|
+
f"Length mismatch: expected one of {{{allowed}}}, got {actual}"
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
if actual != constraint:
|
|
103
|
+
raise LengthError(f"Length mismatch: expected {constraint}, got {actual}")
|
|
104
|
+
|
|
105
|
+
return None
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TypedDict
|
|
3
|
+
=======
|
|
4
|
+
"""
|
|
5
|
+
# src/typingkit/core/dict.py
|
|
6
|
+
|
|
7
|
+
from types import GenericAlias
|
|
8
|
+
from typing import Any, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
from typingkit.core._config import TypedCollectionConfig
|
|
11
|
+
from typingkit.core._validators import LengthError, validate_length
|
|
12
|
+
from typingkit.core.generics import RuntimeGeneric, get_runtime_args
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"TypedDict",
|
|
16
|
+
"LengthError",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# ── Type parameters ───────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
Length = TypeVar("Length", bound=int, default=int)
|
|
22
|
+
Key = TypeVar("Key", default=Any)
|
|
23
|
+
Value = TypeVar("Value", default=Any)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── TypedDict ─────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TypedDict(RuntimeGeneric[Length, Key, Value], dict[Key, Value]):
|
|
30
|
+
"""
|
|
31
|
+
A ``dict`` subclass whose length is enforced at runtime via the
|
|
32
|
+
``RuntimeGeneric`` machinery.
|
|
33
|
+
|
|
34
|
+
Usage::
|
|
35
|
+
|
|
36
|
+
TypedDict[Literal[2], str, int]({"a": 1, "b": 2}) # length checked
|
|
37
|
+
TypedDict[int, str, int]({"a": 1}) # no length constraint
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# ── RuntimeGeneric hooks ──────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
def __runtime_generic_post_init__(self, alias: GenericAlias) -> None:
|
|
43
|
+
args = get_runtime_args(alias)
|
|
44
|
+
length: Any = args[0] if args else Length.__default__ # type: ignore[misc]
|
|
45
|
+
|
|
46
|
+
if TypedCollectionConfig.VALIDATE_LENGTH:
|
|
47
|
+
validate_length(self, length)
|
|
48
|
+
|
|
49
|
+
# Propagate into nested RuntimeGenerics — delegate to the base class
|
|
50
|
+
# which calls __runtime_generic_iter_children__ for us.
|
|
51
|
+
return super().__runtime_generic_post_init__(alias)
|
|
52
|
+
|
|
53
|
+
# ── dict API ──────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
def __len__(self) -> Length:
|
|
56
|
+
return cast(Length, super().__len__())
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def length(self) -> Length:
|
|
60
|
+
return self.__len__()
|