enforced 0.1.1__py3-none-any.whl
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.
- enforced/__init__.py +48 -0
- enforced/_compat.py +152 -0
- enforced/_dataclass/__init__.py +1 -0
- enforced/_dataclass/_assignment.py +509 -0
- enforced/_dataclass/_field_messages.py +85 -0
- enforced/_dataclass/_init.py +337 -0
- enforced/_dataclass/api.py +316 -0
- enforced/_dispatch/__init__.py +1 -0
- enforced/_dispatch/_candidates.py +291 -0
- enforced/_dispatch/_errors.py +176 -0
- enforced/_dispatch/_match.py +120 -0
- enforced/_dispatch/_reach.py +469 -0
- enforced/_dispatch/api.py +455 -0
- enforced/_environment.py +108 -0
- enforced/_formatting_tool.py +205 -0
- enforced/_json.py +145 -0
- enforced/_validator/__init__.py +9 -0
- enforced/_validator/_annotations.py +278 -0
- enforced/_validator/_checker.py +904 -0
- enforced/_validator/_checks.py +139 -0
- enforced/_validator/_class_rules.py +162 -0
- enforced/_validator/_codegen.py +424 -0
- enforced/_validator/_container.py +602 -0
- enforced/_validator/_deep.py +1236 -0
- enforced/_validator/_defaults.py +131 -0
- enforced/_validator/_errors.py +664 -0
- enforced/_validator/_file_types.py +184 -0
- enforced/_validator/_general_path.py +68 -0
- enforced/_validator/_lifetime.py +156 -0
- enforced/_validator/_messages.py +411 -0
- enforced/_validator/_problems.py +170 -0
- enforced/_validator/_record_strings.py +84 -0
- enforced/_validator/_remembered.py +154 -0
- enforced/_validator/_scope.py +384 -0
- enforced/_validator/_self_routing.py +153 -0
- enforced/_validator/_signatures.py +167 -0
- enforced/_validator/_slow_path.py +238 -0
- enforced/_validator/_specs.py +428 -0
- enforced/_validator/_text.py +201 -0
- enforced/_validator/_transform.py +268 -0
- enforced/_validator/_type_conversion.py +435 -0
- enforced/_validator/_type_data_model.py +161 -0
- enforced/_validator/_wrapping.py +250 -0
- enforced/_validator/api.py +760 -0
- enforced/data_types.py +330 -0
- enforced/py.typed +0 -0
- enforced/utils.py +54 -0
- enforced-0.1.1.dist-info/METADATA +907 -0
- enforced-0.1.1.dist-info/RECORD +52 -0
- enforced-0.1.1.dist-info/WHEEL +5 -0
- enforced-0.1.1.dist-info/licenses/LICENSE +21 -0
- enforced-0.1.1.dist-info/top_level.txt +1 -0
enforced/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from ._validator import *
|
|
2
|
+
from ._validator._type_data_model import TypeModel
|
|
3
|
+
from ._validator._transform import Transform
|
|
4
|
+
from ._environment import set_compile_after, turn_validation_off, turn_validation_on, validation_is_off
|
|
5
|
+
from ._formatting_tool import turn_colours_off, turn_colours_on
|
|
6
|
+
from ._json import to_json
|
|
7
|
+
from ._dispatch.api import dispatch, dispatchmethod
|
|
8
|
+
from ._dataclass.api import (
|
|
9
|
+
dataclass,
|
|
10
|
+
make_dataclass,
|
|
11
|
+
)
|
|
12
|
+
from .utils import protect, Ok, Err
|
|
13
|
+
from dataclasses import field, asdict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.1"
|
|
17
|
+
|
|
18
|
+
#* What the package offers. Type checkers read this list too: in a package that ships ``py.typed``,
|
|
19
|
+
#* a name imported above only counts as part of the package when it is named here.
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Check",
|
|
22
|
+
"Enforced",
|
|
23
|
+
"Err",
|
|
24
|
+
"Ok",
|
|
25
|
+
"InvalidDataError",
|
|
26
|
+
"InvalidDataWarning",
|
|
27
|
+
"Transform",
|
|
28
|
+
"TypeModel",
|
|
29
|
+
"dataclass",
|
|
30
|
+
"dispatch",
|
|
31
|
+
"dispatchmethod",
|
|
32
|
+
"enforce_class",
|
|
33
|
+
"enforce",
|
|
34
|
+
"make_dataclass",
|
|
35
|
+
"field",
|
|
36
|
+
"asdict",
|
|
37
|
+
"protect",
|
|
38
|
+
"to_json",
|
|
39
|
+
"set_compile_after",
|
|
40
|
+
"validate",
|
|
41
|
+
"validate_record",
|
|
42
|
+
"validate_function",
|
|
43
|
+
"turn_colours_off",
|
|
44
|
+
"turn_colours_on",
|
|
45
|
+
"turn_validation_off",
|
|
46
|
+
"turn_validation_on",
|
|
47
|
+
"validation_is_off",
|
|
48
|
+
]
|
enforced/_compat.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
What differs between Python 3.13 and the versions after it, in one place: the rest of the package
|
|
3
|
+
never asks ``sys.version_info``. Every branch reads the same way: 3.13 is the bridge, and whatever
|
|
4
|
+
comes after 3.13 follows 3.14's rules (the owner's decision, 2026-11-03).
|
|
5
|
+
|
|
6
|
+
Imports nothing from the package: any module may use it.
|
|
7
|
+
"""
|
|
8
|
+
from types import UnionType
|
|
9
|
+
from typing import Annotated, Any, Callable, Optional, Union
|
|
10
|
+
import dataclasses
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Format", "PUBLIC_MODULES", "UNION_TYPES",
|
|
15
|
+
"annotated_of", "annotation_text",
|
|
16
|
+
"class_repr", "get_annotations",
|
|
17
|
+
"standard_make_dataclass", "union_of"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
#* ``isinstance(x, UNION_TYPES)``: a union, however it was written. ``Union[int, str]`` is one object with
|
|
21
|
+
#* ``int | str`` from 3.14; on 3.13 another class, equal to it all the same.
|
|
22
|
+
UNION_TYPES: Any = (UnionType,) if sys.version_info >= (3, 14) else (UnionType, type(Optional[int]))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if sys.version_info >= (3, 14):
|
|
26
|
+
from annotationlib import Format, get_annotations
|
|
27
|
+
|
|
28
|
+
standard_make_dataclass = dataclasses.make_dataclass
|
|
29
|
+
|
|
30
|
+
#* A class's module as messages show it.
|
|
31
|
+
PUBLIC_MODULES: dict[str, str] = {}
|
|
32
|
+
|
|
33
|
+
def annotation_text(annotation: Any) -> str:
|
|
34
|
+
"""``str(annotation)``, as messages show it."""
|
|
35
|
+
return str(annotation)
|
|
36
|
+
|
|
37
|
+
def class_repr(cls: type) -> str:
|
|
38
|
+
"""``repr(cls)``, as messages show it."""
|
|
39
|
+
return repr(cls)
|
|
40
|
+
|
|
41
|
+
else:
|
|
42
|
+
from enum import IntEnum
|
|
43
|
+
from types import new_class, NoneType
|
|
44
|
+
from typing import get_args, get_origin, Literal
|
|
45
|
+
import inspect
|
|
46
|
+
import keyword
|
|
47
|
+
|
|
48
|
+
PUBLIC_MODULES = {"pathlib._local": "pathlib"}
|
|
49
|
+
|
|
50
|
+
class Format(IntEnum):
|
|
51
|
+
"""``annotationlib.Format``, for the names the package uses."""
|
|
52
|
+
VALUE = 1
|
|
53
|
+
FORWARDREF = 3
|
|
54
|
+
STRING = 4
|
|
55
|
+
|
|
56
|
+
def get_annotations(obj: Any, *, format: Format = Format.VALUE) -> dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
3.13 evaluated the annotations when it ran their ``def`` or class, so ``format`` changes nothing. Only
|
|
59
|
+
what has an ``__annotate__`` is asked for ``STRING`` (``_annotation_texts``), and nothing has one here.
|
|
60
|
+
"""
|
|
61
|
+
return inspect.get_annotations(obj)
|
|
62
|
+
|
|
63
|
+
def standard_make_dataclass(
|
|
64
|
+
cls_name: str,
|
|
65
|
+
fields: Any,
|
|
66
|
+
*,
|
|
67
|
+
bases: tuple[type, ...] = (),
|
|
68
|
+
namespace: dict[str, Any] | None = None,
|
|
69
|
+
module: str | None = None,
|
|
70
|
+
decorator: Callable[..., Any] = dataclasses.dataclass,
|
|
71
|
+
**options: Any,
|
|
72
|
+
) -> type:
|
|
73
|
+
"""``dataclasses.make_dataclass`` of 3.14: 3.13's calls its own ``dataclass``, and has no ``decorator``."""
|
|
74
|
+
seen: set[str] = set()
|
|
75
|
+
annotations: dict[str, Any] = {}
|
|
76
|
+
defaults: dict[str, Any] = {}
|
|
77
|
+
for item in fields:
|
|
78
|
+
if isinstance(item, str):
|
|
79
|
+
name, annotation = item, Any
|
|
80
|
+
elif len(item) == 2:
|
|
81
|
+
name, annotation = item
|
|
82
|
+
elif len(item) == 3:
|
|
83
|
+
name, annotation, defaults[item[0]] = item
|
|
84
|
+
else:
|
|
85
|
+
raise TypeError(f"Invalid field: {item!r}")
|
|
86
|
+
if not isinstance(name, str) or not name.isidentifier():
|
|
87
|
+
raise TypeError(f"Field names must be valid identifiers: {name!r}")
|
|
88
|
+
if keyword.iskeyword(name):
|
|
89
|
+
raise TypeError(f"Field names must not be keywords: {name!r}")
|
|
90
|
+
if name in seen:
|
|
91
|
+
raise TypeError(f"Field name duplicated: {name!r}")
|
|
92
|
+
seen.add(name)
|
|
93
|
+
annotations[name] = annotation
|
|
94
|
+
|
|
95
|
+
def body(class_namespace: dict[str, Any]) -> None:
|
|
96
|
+
class_namespace.update(namespace or {})
|
|
97
|
+
class_namespace.update(defaults)
|
|
98
|
+
class_namespace["__annotations__"] = annotations
|
|
99
|
+
|
|
100
|
+
cls = new_class(cls_name, bases, {}, body)
|
|
101
|
+
if module is not None:
|
|
102
|
+
cls.__module__ = module
|
|
103
|
+
return decorator(cls, **options) # type: ignore[no-any-return]
|
|
104
|
+
|
|
105
|
+
def annotation_text(annotation: Any) -> str:
|
|
106
|
+
"""
|
|
107
|
+
``str(annotation)`` as 3.14 prints it, where 3.13 says ``Optional[X]``, ``Union[X, Y]``, ``NoneType``
|
|
108
|
+
and ``pathlib._local.Path``. Made from the objects, which still know the order of a union (``None | X``).
|
|
109
|
+
"""
|
|
110
|
+
return annotation if isinstance(annotation, str) else _written(annotation)
|
|
111
|
+
|
|
112
|
+
def _written(annotation: Any) -> str:
|
|
113
|
+
if isinstance(annotation, UNION_TYPES):
|
|
114
|
+
return " | ".join(_written(member) for member in annotation.__args__)
|
|
115
|
+
if annotation is NoneType:
|
|
116
|
+
return "None"
|
|
117
|
+
if annotation is ...:
|
|
118
|
+
return "..."
|
|
119
|
+
if isinstance(annotation, list): # the parameters of a ``Callable``
|
|
120
|
+
return f"[{', '.join(_written(part) for part in annotation)}]"
|
|
121
|
+
args = get_args(annotation)
|
|
122
|
+
if isinstance(annotation, type) and not args:
|
|
123
|
+
module, name = annotation.__module__, annotation.__qualname__
|
|
124
|
+
return name if module == "builtins" else f"{PUBLIC_MODULES.get(module, module)}.{name}"
|
|
125
|
+
if not args or get_origin(annotation) is Literal:
|
|
126
|
+
return repr(annotation) # inside another annotation a string shows its quotes
|
|
127
|
+
head = repr(annotation).partition("[")[0]
|
|
128
|
+
if get_origin(annotation) is Annotated: # what follows the type is no annotation
|
|
129
|
+
return f"{head}[{_written(args[0])}, {', '.join(repr(part) for part in args[1:])}]"
|
|
130
|
+
return f"{head}[{', '.join(_written(part) for part in args)}]"
|
|
131
|
+
|
|
132
|
+
def class_repr(cls: type) -> str:
|
|
133
|
+
"""``repr(cls)``, without the module 3.13 keeps the classes of ``pathlib`` in."""
|
|
134
|
+
return repr(cls).replace(f"{cls.__module__}.", f"{PUBLIC_MODULES[cls.__module__]}.", 1)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
#* ``typing`` keeps what ``Union[...]`` (on 3.13) and ``Annotated[...]`` (on every version) made in a cache,
|
|
138
|
+
#* the 128 most recent, and with it the classes they name. An annotation enforced writes for a class made
|
|
139
|
+
#* at run time must not keep that class alive, so these two step past the cache. Private names, read
|
|
140
|
+
#* with ``getattr``: where they are gone the annotation comes from the cache, and is just as good.
|
|
141
|
+
_UNION_UNCACHED: Any = getattr(Union, "_getitem", None) if sys.version_info < (3, 14) else None
|
|
142
|
+
_ANNOTATED_UNCACHED: Any = getattr(getattr(Annotated, "_getitem", None), "__wrapped__", None)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def union_of(members: tuple[Any, ...]) -> Any:
|
|
146
|
+
"""``Union[members]``, with nothing kept by ``typing``."""
|
|
147
|
+
return Union[members] if _UNION_UNCACHED is None else _UNION_UNCACHED(Union, members)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def annotated_of(parts: tuple[Any, ...]) -> Any:
|
|
151
|
+
"""``Annotated[parts]``, with nothing kept by ``typing``."""
|
|
152
|
+
return Annotated[parts] if _ANNOTATED_UNCACHED is None else _ANNOTATED_UNCACHED(Annotated, *parts)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""enforced.dataclass: a type-checked drop-in for dataclasses.dataclass (the decorator is in ``api``)."""
|