typingkit 0.2.7__tar.gz → 0.2.8__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.
Files changed (26) hide show
  1. {typingkit-0.2.7 → typingkit-0.2.8}/PKG-INFO +1 -1
  2. {typingkit-0.2.7 → typingkit-0.2.8}/pyproject.toml +10 -6
  3. typingkit-0.2.8/src/typingkit/.DS_Store +0 -0
  4. typingkit-0.2.8/src/typingkit/core/.DS_Store +0 -0
  5. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/core/__init__.py +8 -2
  6. typingkit-0.2.8/src/typingkit/core/_options.py +195 -0
  7. typingkit-0.2.8/src/typingkit/core/dict.py +72 -0
  8. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/core/generics.py +150 -95
  9. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/core/list.py +30 -16
  10. typingkit-0.2.8/src/typingkit/mypy_plugin.py +382 -0
  11. typingkit-0.2.8/src/typingkit/numpy/.DS_Store +0 -0
  12. typingkit-0.2.8/src/typingkit/numpy/_typed/.DS_Store +0 -0
  13. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/dimexpr.py +14 -7
  14. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/ndarray.py +96 -79
  15. typingkit-0.2.7/src/typingkit/core/_config.py +0 -26
  16. typingkit-0.2.7/src/typingkit/core/dict.py +0 -55
  17. {typingkit-0.2.7 → typingkit-0.2.8}/README.md +0 -0
  18. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/__init__.py +0 -0
  19. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/core/_debug.py +0 -0
  20. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/core/_validators.py +0 -0
  21. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/__init__.py +0 -0
  22. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/__init__.py +0 -0
  23. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/context.py +0 -0
  24. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/factory.py +0 -0
  25. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/helpers.py +0 -0
  26. {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: typingkit
3
- Version: 0.2.7
3
+ Version: 0.2.8
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.7"
3
+ version = "0.2.8"
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,23 +8,27 @@ requires-python = ">=3.13"
8
8
  dependencies = ["numpy>=2.2"]
9
9
 
10
10
  [build-system]
11
- requires = ["uv_build>=0.10.9"]
11
+ requires = ["uv_build>=0.10.11,<0.11.0"]
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.5"]
17
+ lint = ["ruff>=0.15.6"]
18
18
  typecheck = [
19
- "basedpyright>=1.38.2",
19
+ "basedpyright>=1.38.3",
20
20
  "mypy[mypyc]>=1.19.1",
21
- "pyrefly>=0.56.0",
21
+ "pyrefly>=0.57.0",
22
22
  "pyright>=1.1.408",
23
- "ty>=0.0.21",
23
+ "ty>=0.0.23",
24
24
  ]
25
25
 
26
26
  [tool.mypy]
27
27
  enable_incomplete_feature = ["TypeForm"]
28
+ plugins = ["typingkit.mypy_plugin"]
29
+
30
+ [tool.pytest.ini_options]
31
+ norecursedirs = ["tests/mypy_plugin"]
28
32
 
29
33
  [tool.uv]
30
34
  default-groups = "all"
Binary file
@@ -4,14 +4,20 @@ TypingKit core
4
4
  """
5
5
  # src/typingkit/core/__init__.py
6
6
 
7
- from typingkit.core._config import TypedCollectionConfig
7
+ from typingkit.core._options import (
8
+ RuntimeOptions,
9
+ reset_global_default_runtime_options,
10
+ set_global_default_runtime_options,
11
+ )
8
12
  from typingkit.core.dict import TypedDict
9
13
  from typingkit.core.generics import RuntimeGeneric
10
14
  from typingkit.core.list import TypedList
11
15
 
12
16
  __all__ = [
13
17
  "RuntimeGeneric",
18
+ "RuntimeOptions",
19
+ "set_global_default_runtime_options",
20
+ "reset_global_default_runtime_options",
14
21
  "TypedList",
15
22
  "TypedDict",
16
- "TypedCollectionConfig",
17
23
  ]
@@ -0,0 +1,195 @@
1
+ """
2
+ RuntimeOptions
3
+ ==============
4
+
5
+ Per-class, inheritable, optionally context-scoped configuration for
6
+ ``RuntimeGeneric`` subclasses.
7
+
8
+ Public API
9
+ ----------
10
+ RuntimeOptions – base frozen-dataclass config attached to a class
11
+ RuntimeOptionsProxy – ContextVar-backed scoped override (context manager)
12
+
13
+ Attaching options to a class
14
+ -----------------------------
15
+ Pass an ``options=`` keyword to the class statement — handled by
16
+ ``RuntimeGeneric.__init_subclass__``::
17
+
18
+ class MyClass(RuntimeGeneric[A, B],
19
+ options=ListOptions(validate_a=False)):
20
+ ...
21
+
22
+ Options are inherited through the MRO; a subclass that does not pass
23
+ ``options=`` inherits its nearest parent's options unchanged.
24
+
25
+ Subclassing RuntimeOptions
26
+ --------------------------
27
+ Add domain-specific flags by subclassing::
28
+
29
+ @dataclass(frozen=True)
30
+ class MyClassRuntimeOptions(RuntimeOptions):
31
+ validate_a: bool = True
32
+ validate_b: bool = True
33
+
34
+ Temporary overrides
35
+ --------------------
36
+ Use ``RuntimeOptions.scoped(...)`` as a context manager::
37
+
38
+ with RuntimeOptions.scoped(validate=False):
39
+ MyClass[Literal[3], Literal[4]](...)
40
+
41
+ Scoped options take precedence over class-level options for any
42
+ ``__runtime_generic_post_init__`` call made within the ``with`` block,
43
+ across all ``RuntimeGeneric`` subclasses. The override is thread- and
44
+ async-safe (backed by a ``ContextVar``).
45
+ """
46
+ # src/typingkit/core/_options.py
47
+
48
+ from __future__ import annotations
49
+
50
+ from contextvars import ContextVar
51
+ from dataclasses import dataclass, replace
52
+ from typing import Any
53
+
54
+ __all__ = [
55
+ "RuntimeOptions",
56
+ ]
57
+
58
+ # ── Scoped override ───────────────────────────────────────────────────────────
59
+
60
+ _scoped_options: ContextVar[RuntimeOptions | None] = ContextVar(
61
+ "_scoped_options", default=None
62
+ )
63
+
64
+
65
+ # ── Options dataclass ─────────────────────────────────────────────────────────
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class RuntimeOptions:
70
+ """
71
+ Immutable per-class configuration for ``RuntimeGeneric``.
72
+
73
+ Fields
74
+ ------
75
+ validate : bool
76
+ Master switch. When ``False``, ``__runtime_generic_post_init__`` is
77
+ skipped entirely — equivalent to plain ``Generic`` behaviour. Takes
78
+ effect both at the alias-call level (``_RuntimeGenericAlias.__call__``)
79
+ and inside the post-init guard, so subclasses never see the call.
80
+
81
+ propagate : bool
82
+ Whether to walk ``__runtime_generic_iter_children__`` and propagate
83
+ resolved types into child ``RuntimeGeneric`` instances. Setting this
84
+ to ``False`` validates `self` but stops at the boundary — useful when
85
+ children manage their own lifecycle (e.g. lazy containers).
86
+ """
87
+
88
+ validate: bool = True
89
+ propagate: bool = True
90
+
91
+ # ── Merging ───────────────────────────────────────────────────────────────
92
+
93
+ def merged(self, **overrides: Any) -> RuntimeOptions:
94
+ """Return a new options object with `overrides` applied."""
95
+ return replace(self, **overrides)
96
+
97
+ # ── Scoped context manager ────────────────────────────────────────────────
98
+
99
+ @staticmethod
100
+ def scoped(**overrides: Any) -> _RuntimeOptionsScopedCtx:
101
+ """
102
+ Context manager for temporary option overrides.
103
+
104
+ Merges `overrides` into the currently-active scoped options (or the
105
+ sentinel ``RuntimeOptions()`` if none is active), so nested scopes
106
+ compose correctly::
107
+
108
+ with RuntimeOptions.scoped(validate=False):
109
+ with RuntimeOptions.scoped(propagate=False):
110
+ ... # both validate=False and propagate=False
111
+ """
112
+ return _RuntimeOptionsScopedCtx(overrides)
113
+
114
+ # ── Internal resolution ───────────────────────────────────────────────────
115
+
116
+ @staticmethod
117
+ def resolve(class_options: RuntimeOptions) -> RuntimeOptions:
118
+ """
119
+ Return the effective options for a call, merging scoped overrides.
120
+
121
+ Scoped options (from ``RuntimeOptions.scoped(...)``) always win over
122
+ class-level options. Only the fields present in *class_options*'s
123
+ type are considered from the scoped override, so domain-specific
124
+ fields on a subclass are not accidentally clobbered.
125
+ """
126
+ # Global default acts as a master override — if it disables validation,
127
+ # nothing can re-enable it except explicitly passing scoped().
128
+ effective = class_options
129
+ if not global_default_runtime_options.validate and effective.validate:
130
+ effective = replace(effective, validate=False)
131
+ if not global_default_runtime_options.propagate and effective.validate:
132
+ effective = replace(effective, propagate=False)
133
+
134
+ scoped = _scoped_options.get()
135
+ if scoped is None:
136
+ return effective
137
+
138
+ # Apply only the base RuntimeOptions fields from the scoped override;
139
+ # domain-specific fields on class_options are preserved.
140
+ base_fields = {"validate", "propagate"}
141
+ overrides: dict[str, Any] = {}
142
+ for field in base_fields:
143
+ scoped_val = getattr(scoped, field, None)
144
+ effective_val = getattr(effective, field, None)
145
+ if scoped_val is not None and scoped_val != effective_val:
146
+ overrides[field] = scoped_val
147
+ if not overrides:
148
+ return effective
149
+ return replace(effective, **overrides)
150
+
151
+
152
+ # ── Context manager implementation ────────────────────────────────────────────
153
+
154
+
155
+ class _RuntimeOptionsScopedCtx:
156
+ """Returned by ``RuntimeOptions.scoped()``; not part of the public API."""
157
+
158
+ __slots__ = ("_overrides", "_token")
159
+
160
+ def __init__(self, overrides: dict[str, Any]) -> None:
161
+ self._overrides = overrides
162
+ self._token = None
163
+
164
+ def __enter__(self) -> RuntimeOptions:
165
+ current = _scoped_options.get() or RuntimeOptions()
166
+ merged = replace(current, **self._overrides)
167
+ self._token = _scoped_options.set(merged) # type: ignore[assignment]
168
+ return merged
169
+
170
+ def __exit__(self, *_: Any) -> None:
171
+ if self._token is not None:
172
+ _scoped_options.reset(self._token)
173
+ self._token = None
174
+
175
+ # Support async with as well
176
+ async def __aenter__(self) -> RuntimeOptions:
177
+ return self.__enter__()
178
+
179
+ async def __aexit__(self, *args: Any) -> None:
180
+ self.__exit__(*args)
181
+
182
+
183
+ # ── Global default ────────────────────────────────────────────────────────────
184
+
185
+ global_default_runtime_options: RuntimeOptions = RuntimeOptions()
186
+
187
+
188
+ def set_global_default_runtime_options(options: RuntimeOptions) -> None:
189
+ global global_default_runtime_options
190
+ global_default_runtime_options = options
191
+
192
+
193
+ def reset_global_default_runtime_options() -> None:
194
+ global global_default_runtime_options
195
+ global_default_runtime_options = RuntimeOptions()
@@ -0,0 +1,72 @@
1
+ """
2
+ TypedDict
3
+ =======
4
+ A dict subclass with runtime length enforcement.
5
+ """
6
+ # src/typingkit/core/dict.py
7
+
8
+ from dataclasses import dataclass
9
+ from types import GenericAlias
10
+ from typing import Any, TypeVar, cast
11
+
12
+ from typingkit.core._options import RuntimeOptions
13
+ from typingkit.core._validators import LengthError, validate_length
14
+ from typingkit.core.generics import RuntimeGeneric, get_runtime_args, mapping_from_alias
15
+
16
+ __all__ = [
17
+ "TypedDict",
18
+ "TypedDictOptions",
19
+ "LengthError",
20
+ ]
21
+
22
+ Length = TypeVar("Length", bound=int, default=int)
23
+ Key = TypeVar("Key", default=Any)
24
+ Value = TypeVar("Value", default=Any)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class TypedDictOptions(RuntimeOptions):
29
+ validate_length: bool = True
30
+
31
+
32
+ _DEFAULT_DICT_OPTIONS = TypedDictOptions()
33
+
34
+
35
+ class TypedDict(
36
+ RuntimeGeneric[Length, Key, Value],
37
+ dict[Key, Value],
38
+ options=_DEFAULT_DICT_OPTIONS,
39
+ ):
40
+ """
41
+ A ``dict`` subclass with optional runtime length enforcement.
42
+
43
+ Usage::
44
+
45
+ TypedDict[Literal[2], str, int]({"a": 1, "b": 2}) # length checked
46
+ TypedDict[int, str, int]({"a": 1}) # unconstrained length
47
+ """
48
+
49
+ def __runtime_generic_validate__(self, alias: GenericAlias) -> None:
50
+ # get_runtime_args always returns a full-length tuple (defaults filled).
51
+ # Safe to unpack directly against the class's parameter list.
52
+ length, _key, _value = get_runtime_args(alias)
53
+
54
+ opts = self.__class__._runtime_options_
55
+ # Narrow to TypedDictOptions for the domain-specific flag.
56
+ # Falls back gracefully if a subclass switched to a plain RuntimeOptions.
57
+ if isinstance(opts, TypedDictOptions) and opts.validate_length:
58
+ validate_length(self, length)
59
+
60
+ # Propagate resolved types into child RuntimeGeneric instances.
61
+ # We call this here (rather than relying on the base post_init) because
62
+ # we've already resolved the mapping above — avoids a second build.
63
+ self.__runtime_generic_propagate_children__(
64
+ mapping_from_alias(alias, type(self))
65
+ )
66
+
67
+ def __len__(self) -> Length:
68
+ return cast(Length, super().__len__())
69
+
70
+ @property
71
+ def length(self) -> Length:
72
+ return self.__len__()