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.
- {typingkit-0.2.7 → typingkit-0.2.8}/PKG-INFO +1 -1
- {typingkit-0.2.7 → typingkit-0.2.8}/pyproject.toml +10 -6
- typingkit-0.2.8/src/typingkit/.DS_Store +0 -0
- typingkit-0.2.8/src/typingkit/core/.DS_Store +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/core/__init__.py +8 -2
- typingkit-0.2.8/src/typingkit/core/_options.py +195 -0
- typingkit-0.2.8/src/typingkit/core/dict.py +72 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/core/generics.py +150 -95
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/core/list.py +30 -16
- typingkit-0.2.8/src/typingkit/mypy_plugin.py +382 -0
- typingkit-0.2.8/src/typingkit/numpy/.DS_Store +0 -0
- typingkit-0.2.8/src/typingkit/numpy/_typed/.DS_Store +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/dimexpr.py +14 -7
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/ndarray.py +96 -79
- typingkit-0.2.7/src/typingkit/core/_config.py +0 -26
- typingkit-0.2.7/src/typingkit/core/dict.py +0 -55
- {typingkit-0.2.7 → typingkit-0.2.8}/README.md +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/__init__.py +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/core/_debug.py +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/core/_validators.py +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/__init__.py +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/__init__.py +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/context.py +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/factory.py +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/src/typingkit/numpy/_typed/helpers.py +0 -0
- {typingkit-0.2.7 → typingkit-0.2.8}/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.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.
|
|
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.
|
|
17
|
+
lint = ["ruff>=0.15.6"]
|
|
18
18
|
typecheck = [
|
|
19
|
-
"basedpyright>=1.38.
|
|
19
|
+
"basedpyright>=1.38.3",
|
|
20
20
|
"mypy[mypyc]>=1.19.1",
|
|
21
|
-
"pyrefly>=0.
|
|
21
|
+
"pyrefly>=0.57.0",
|
|
22
22
|
"pyright>=1.1.408",
|
|
23
|
-
"ty>=0.0.
|
|
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
|
|
Binary file
|
|
@@ -4,14 +4,20 @@ TypingKit core
|
|
|
4
4
|
"""
|
|
5
5
|
# src/typingkit/core/__init__.py
|
|
6
6
|
|
|
7
|
-
from typingkit.core.
|
|
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__()
|