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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: typingkit
3
- Version: 0.2.4
3
+ Version: 0.2.6
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.4"
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.7"]
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.4"]
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.20",
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.dict import TypedDict, TypedDictConfig
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, TypedListConfig
10
+ from typingkit.core.list import TypedList
10
11
 
11
12
  __all__ = [
12
13
  "RuntimeGeneric",
13
14
  "TypedList",
14
- "TypedListConfig",
15
15
  "TypedDict",
16
- "TypedDictConfig",
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: # type: ignore
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__()