validating 0.0.3__tar.gz → 0.0.5__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.
- {validating-0.0.3 → validating-0.0.5}/PKG-INFO +7 -1
- {validating-0.0.3 → validating-0.0.5}/README.md +6 -0
- {validating-0.0.3 → validating-0.0.5}/pyproject.toml +1 -1
- {validating-0.0.3 → validating-0.0.5}/src/validating/datacls.py +2 -1
- {validating-0.0.3 → validating-0.0.5}/src/validating/valid_attr.py +26 -14
- {validating-0.0.3 → validating-0.0.5}/test.py +6 -0
- {validating-0.0.3 → validating-0.0.5}/.github/workflows/python-publish.yml +0 -0
- {validating-0.0.3 → validating-0.0.5}/.gitignore +0 -0
- {validating-0.0.3 → validating-0.0.5}/LICENSE +0 -0
- {validating-0.0.3 → validating-0.0.5}/install.py +0 -0
- {validating-0.0.3 → validating-0.0.5}/src/validating/__init__.py +0 -0
- {validating-0.0.3 → validating-0.0.5}/src/validating/_typing.py +0 -0
- {validating-0.0.3 → validating-0.0.5}/src/validating/core.py +0 -0
- {validating-0.0.3 → validating-0.0.5}/src/validating/valid_func.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: validating
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Provides a lightweight `attr()` descriptor factory that adds runtime validation to dataclass fields.
|
|
5
5
|
Project-URL: Documentation, https://github.com/Chitaoji/validating/blob/main/README.md
|
|
6
6
|
Project-URL: Repository, https://github.com/Chitaoji/validating/
|
|
@@ -181,6 +181,12 @@ class EvenNumber:
|
|
|
181
181
|
This project falls under the BSD 3-Clause License.
|
|
182
182
|
|
|
183
183
|
## History
|
|
184
|
+
### v0.0.5
|
|
185
|
+
* Added `typing.dataclass_transform` metadata to `validating.dataclass` (with `attr` and `dataclasses.Field` specifiers) to improve IDE/static-analysis support, including better Pylance hints.
|
|
186
|
+
|
|
187
|
+
### v0.0.4
|
|
188
|
+
* Updated error messages when validating typed dicts.
|
|
189
|
+
|
|
184
190
|
### v0.0.3
|
|
185
191
|
* Improved support for `PEP 585` generics with quoted builtin annotations (for example `list["int"]`) to ensure consistent runtime type validation.
|
|
186
192
|
|
|
@@ -165,6 +165,12 @@ class EvenNumber:
|
|
|
165
165
|
This project falls under the BSD 3-Clause License.
|
|
166
166
|
|
|
167
167
|
## History
|
|
168
|
+
### v0.0.5
|
|
169
|
+
* Added `typing.dataclass_transform` metadata to `validating.dataclass` (with `attr` and `dataclasses.Field` specifiers) to improve IDE/static-analysis support, including better Pylance hints.
|
|
170
|
+
|
|
171
|
+
### v0.0.4
|
|
172
|
+
* Updated error messages when validating typed dicts.
|
|
173
|
+
|
|
168
174
|
### v0.0.3
|
|
169
175
|
* Improved support for `PEP 585` generics with quoted builtin annotations (for example `list["int"]`) to ensure consistent runtime type validation.
|
|
170
176
|
|
|
@@ -10,7 +10,7 @@ from dataclasses import Field
|
|
|
10
10
|
from dataclasses import dataclass as _stdlib_dataclass
|
|
11
11
|
from functools import partial
|
|
12
12
|
from inspect import signature
|
|
13
|
-
from typing import Any, Callable, overload
|
|
13
|
+
from typing import Any, Callable, dataclass_transform, overload
|
|
14
14
|
|
|
15
15
|
from .valid_attr import AttrValidator, attr
|
|
16
16
|
from .valid_func import validate
|
|
@@ -52,6 +52,7 @@ def dataclass[T](
|
|
|
52
52
|
weakref_slot: bool = False,
|
|
53
53
|
validate_methods: bool = False,
|
|
54
54
|
) -> Callable[[type[T]], type[T]]: ...
|
|
55
|
+
@dataclass_transform(field_specifiers=(attr, Field))
|
|
55
56
|
def dataclass(
|
|
56
57
|
cls: type | None = None,
|
|
57
58
|
/,
|
|
@@ -7,8 +7,8 @@ NOTE: this module is private. All functions and objects are available in the mai
|
|
|
7
7
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
import sys
|
|
11
10
|
import builtins
|
|
11
|
+
import sys
|
|
12
12
|
from ast import (
|
|
13
13
|
Attribute,
|
|
14
14
|
If,
|
|
@@ -43,6 +43,7 @@ except ImportError: # pragma: no cover - Python < 3.11
|
|
|
43
43
|
try: # pragma: no cover - available on modern Python versions
|
|
44
44
|
from typing import is_typeddict
|
|
45
45
|
except ImportError: # pragma: no cover - compatibility fallback
|
|
46
|
+
|
|
46
47
|
def is_typeddict(type_hint: Any) -> bool:
|
|
47
48
|
return bool(
|
|
48
49
|
isinstance(type_hint, type)
|
|
@@ -51,6 +52,7 @@ except ImportError: # pragma: no cover - compatibility fallback
|
|
|
51
52
|
and hasattr(type_hint, "__optional_keys__")
|
|
52
53
|
)
|
|
53
54
|
|
|
55
|
+
|
|
54
56
|
__all__ = ["attr"]
|
|
55
57
|
|
|
56
58
|
|
|
@@ -686,7 +688,11 @@ class _TypeCheckingImportCollector(NodeVisitor):
|
|
|
686
688
|
if isinstance(test, Name):
|
|
687
689
|
return test.id == "TYPE_CHECKING"
|
|
688
690
|
if isinstance(test, Attribute):
|
|
689
|
-
return
|
|
691
|
+
return (
|
|
692
|
+
isinstance(test.value, Name)
|
|
693
|
+
and test.value.id == "typing"
|
|
694
|
+
and test.attr == "TYPE_CHECKING"
|
|
695
|
+
)
|
|
690
696
|
return False
|
|
691
697
|
|
|
692
698
|
def _consume_type_checking_stmt(self, stmt: Any) -> None:
|
|
@@ -774,7 +780,7 @@ def isoftype(
|
|
|
774
780
|
return None
|
|
775
781
|
except TypeError:
|
|
776
782
|
pass
|
|
777
|
-
return
|
|
783
|
+
return _format_expected_error(
|
|
778
784
|
path, f"{type_hint!r}, got {type(value)!r} instead"
|
|
779
785
|
)
|
|
780
786
|
|
|
@@ -782,7 +788,7 @@ def isoftype(
|
|
|
782
788
|
union_errors = [isoftype(value, arg, name, path) for arg in args]
|
|
783
789
|
if any(error is None for error in union_errors):
|
|
784
790
|
return None
|
|
785
|
-
return
|
|
791
|
+
return _format_expected_error(
|
|
786
792
|
path,
|
|
787
793
|
f"one of {args}, got {value.__class__!r} instead",
|
|
788
794
|
)
|
|
@@ -790,7 +796,7 @@ def isoftype(
|
|
|
790
796
|
if origin is Literal:
|
|
791
797
|
if value in args:
|
|
792
798
|
return None
|
|
793
|
-
return
|
|
799
|
+
return _format_expected_error(path, f"one of {args!r}, got {value!r} instead")
|
|
794
800
|
|
|
795
801
|
if origin is Unpack:
|
|
796
802
|
(unpacked_type,) = args
|
|
@@ -799,7 +805,7 @@ def isoftype(
|
|
|
799
805
|
if origin is list:
|
|
800
806
|
(elem_type,) = args
|
|
801
807
|
if not isinstance(value, list):
|
|
802
|
-
return
|
|
808
|
+
return _format_expected_error(path, f"a list, got {type(value)!r} instead")
|
|
803
809
|
for idx, elem in enumerate(value):
|
|
804
810
|
elem_error = isoftype(elem, elem_type, name, f"{name}[{idx}]")
|
|
805
811
|
if elem_error is not None:
|
|
@@ -810,7 +816,7 @@ def isoftype(
|
|
|
810
816
|
if len(args) == 2 and args[1] is ...:
|
|
811
817
|
(elem_type, _) = args
|
|
812
818
|
if not isinstance(value, tuple):
|
|
813
|
-
return
|
|
819
|
+
return _format_expected_error(
|
|
814
820
|
path, f"a tuple, got {type(value)!r} instead"
|
|
815
821
|
)
|
|
816
822
|
for idx, elem in enumerate(value):
|
|
@@ -820,7 +826,7 @@ def isoftype(
|
|
|
820
826
|
return None
|
|
821
827
|
|
|
822
828
|
if not isinstance(value, tuple) or len(value) != len(args):
|
|
823
|
-
return
|
|
829
|
+
return _format_expected_error(
|
|
824
830
|
path,
|
|
825
831
|
f"a tuple with {len(args)} elements, got {type(value)!r} with "
|
|
826
832
|
f"length {len(value) if isinstance(value, tuple) else 'N/A'} instead",
|
|
@@ -834,7 +840,7 @@ def isoftype(
|
|
|
834
840
|
if origin is dict:
|
|
835
841
|
key_t, val_t = args
|
|
836
842
|
if not isinstance(value, dict):
|
|
837
|
-
return
|
|
843
|
+
return _format_expected_error(path, f"a dict, got {type(value)!r} instead")
|
|
838
844
|
for key, val in value.items():
|
|
839
845
|
key_error = isoftype(key, key_t, name, f"{name}.keys() element {key!r}")
|
|
840
846
|
if key_error is not None:
|
|
@@ -847,7 +853,7 @@ def isoftype(
|
|
|
847
853
|
if origin is set:
|
|
848
854
|
(elem_type,) = args
|
|
849
855
|
if not isinstance(value, set):
|
|
850
|
-
return
|
|
856
|
+
return _format_expected_error(path, f"a set, got {type(value)!r} instead")
|
|
851
857
|
for elem in value:
|
|
852
858
|
elem_path = f"{path} element {elem!r}" if path else f"element {elem!r}"
|
|
853
859
|
elem_error = isoftype(elem, elem_type, name, elem_path)
|
|
@@ -858,12 +864,18 @@ def isoftype(
|
|
|
858
864
|
raise NotImplementedError(f"Unsupported type hint: {type_hint}")
|
|
859
865
|
|
|
860
866
|
|
|
861
|
-
def
|
|
867
|
+
def _format_expected_error(path: str, detail: str) -> str:
|
|
862
868
|
if path:
|
|
863
869
|
return f"{path} expected {detail}"
|
|
864
870
|
return f"expected {detail}"
|
|
865
871
|
|
|
866
872
|
|
|
873
|
+
def _format_error(path: str, detail: str) -> str:
|
|
874
|
+
if path:
|
|
875
|
+
return f"{path} {detail}"
|
|
876
|
+
return detail
|
|
877
|
+
|
|
878
|
+
|
|
867
879
|
def _validate_typed_dict(
|
|
868
880
|
value: object,
|
|
869
881
|
type_hint: Any,
|
|
@@ -871,7 +883,7 @@ def _validate_typed_dict(
|
|
|
871
883
|
path: str,
|
|
872
884
|
) -> Optional[str]:
|
|
873
885
|
if not isinstance(value, dict):
|
|
874
|
-
return
|
|
886
|
+
return _format_expected_error(path, f"a dict, got {type(value)!r} instead")
|
|
875
887
|
|
|
876
888
|
annotations = getattr(type_hint, "__annotations__", {})
|
|
877
889
|
required_keys = set(getattr(type_hint, "__required_keys__", set()))
|
|
@@ -881,12 +893,12 @@ def _validate_typed_dict(
|
|
|
881
893
|
missing = sorted(required_keys - set(value))
|
|
882
894
|
if missing:
|
|
883
895
|
keys = ", ".join(repr(k) for k in missing)
|
|
884
|
-
return
|
|
896
|
+
return _format_error(path, f"missing required keys: {keys}")
|
|
885
897
|
|
|
886
898
|
extra = sorted(set(value) - allowed_keys)
|
|
887
899
|
if extra:
|
|
888
900
|
keys = ", ".join(repr(k) for k in extra)
|
|
889
|
-
return
|
|
901
|
+
return _format_error(path, f"got unexpected keys: {keys}")
|
|
890
902
|
|
|
891
903
|
for key, annotated in annotations.items():
|
|
892
904
|
if key not in value:
|
|
@@ -22,6 +22,12 @@ from src.validating import (
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class TestAttrWithDataclasses(unittest.TestCase):
|
|
25
|
+
def test_validating_dataclass_exposes_dataclass_transform_metadata(self):
|
|
26
|
+
metadata = getattr(validating_dataclass, "__dataclass_transform__", None)
|
|
27
|
+
self.assertIsNotNone(metadata)
|
|
28
|
+
self.assertIn("field_specifiers", metadata)
|
|
29
|
+
self.assertIn(attr, metadata["field_specifiers"])
|
|
30
|
+
|
|
25
31
|
def test_validating_dataclass_can_validate_public_methods(self):
|
|
26
32
|
@validating_dataclass(validate_methods=True)
|
|
27
33
|
class Config:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|