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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: validating
3
- Version: 0.0.3
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "validating"
7
- version = "0.0.3"
7
+ version = "0.0.5"
8
8
  dependencies = []
9
9
  requires-python = ">=3.13"
10
10
  authors = [
@@ -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 isinstance(test.value, Name) and test.value.id == "typing" and test.attr == "TYPE_CHECKING"
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 _format_isoftype_error(
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 _format_isoftype_error(
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 _format_isoftype_error(path, f"one of {args!r}, got {value!r} instead")
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 _format_isoftype_error(path, f"a list, got {type(value)!r} instead")
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 _format_isoftype_error(
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 _format_isoftype_error(
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 _format_isoftype_error(path, f"a dict, got {type(value)!r} instead")
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 _format_isoftype_error(path, f"a set, got {type(value)!r} instead")
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 _format_isoftype_error(path: str, detail: str) -> str:
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 _format_isoftype_error(path, f"a dict, got {type(value)!r} instead")
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 _format_isoftype_error(path, f"missing required keys: {keys}")
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 _format_isoftype_error(path, f"unexpected keys: {keys}")
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