python-introspect 0.1.13__tar.gz → 0.1.14__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 (33) hide show
  1. {python_introspect-0.1.13/src/python_introspect.egg-info → python_introspect-0.1.14}/PKG-INFO +1 -1
  2. {python_introspect-0.1.13 → python_introspect-0.1.14}/pyproject.toml +1 -1
  3. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/dataclass_projection.py +25 -17
  4. {python_introspect-0.1.13 → python_introspect-0.1.14/src/python_introspect.egg-info}/PKG-INFO +1 -1
  5. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_dataclass_projection.py +54 -0
  6. {python_introspect-0.1.13 → python_introspect-0.1.14}/LICENSE +0 -0
  7. {python_introspect-0.1.13 → python_introspect-0.1.14}/README.md +0 -0
  8. {python_introspect-0.1.13 → python_introspect-0.1.14}/setup.cfg +0 -0
  9. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/__init__.py +0 -0
  10. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/annotation_types.py +0 -0
  11. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/callable_declaration.py +0 -0
  12. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/docstring_annotations.py +0 -0
  13. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/enableable.py +0 -0
  14. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/environment_projection.py +0 -0
  15. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/exceptions.py +0 -0
  16. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/runtime_parameter.py +0 -0
  17. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/signature_analyzer.py +0 -0
  18. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/unified_parameter_analyzer.py +0 -0
  19. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/validation.py +0 -0
  20. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/SOURCES.txt +0 -0
  21. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/dependency_links.txt +0 -0
  22. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/requires.txt +0 -0
  23. {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/top_level.txt +0 -0
  24. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_annotation_types.py +0 -0
  25. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_callable_declaration.py +0 -0
  26. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_environment_projection.py +0 -0
  27. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_exceptions.py +0 -0
  28. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_init.py +0 -0
  29. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_runtime_parameter.py +0 -0
  30. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_signature_analyzer.py +0 -0
  31. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_unified_parameter_analyzer.py +0 -0
  32. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_validate_python_release_action.py +0 -0
  33. {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-introspect
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: Pure Python introspection toolkit for function signatures, dataclasses, and type hints
5
5
  Author-email: Tristan Simas <tristan.simas@mail.mcgill.ca>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-introspect"
7
- version = "0.1.13"
7
+ version = "0.1.14"
8
8
  description = "Pure Python introspection toolkit for function signatures, dataclasses, and type hints"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Mapping
5
+ from collections.abc import Mapping, Sequence
6
6
  from dataclasses import MISSING, fields, is_dataclass
7
7
  from enum import Enum
8
8
  from pathlib import Path
@@ -19,7 +19,6 @@ from typing import (
19
19
  from .annotation_types import is_union_type
20
20
  from .validation import validate_annotated_dataclass, validate_annotation_value
21
21
 
22
-
23
22
  DataclassT = TypeVar("DataclassT")
24
23
 
25
24
 
@@ -31,8 +30,7 @@ def dataclass_from_mapping(
31
30
 
32
31
  if not isinstance(target_type, type) or not is_dataclass(target_type):
33
32
  raise TypeError(
34
- "dataclass_from_mapping requires a dataclass type; "
35
- f"got {target_type!r}."
33
+ "dataclass_from_mapping requires a dataclass type; " f"got {target_type!r}."
36
34
  )
37
35
  if not isinstance(values, Mapping):
38
36
  raise TypeError("dataclass_from_mapping requires a mapping.")
@@ -58,7 +56,10 @@ def dataclass_from_mapping(
58
56
  missing: list[str] = []
59
57
  for declared_field in declared_fields:
60
58
  if declared_field.name not in values:
61
- if declared_field.default is MISSING and declared_field.default_factory is MISSING:
59
+ if (
60
+ declared_field.default is MISSING
61
+ and declared_field.default_factory is MISSING
62
+ ):
62
63
  missing.append(declared_field.name)
63
64
  continue
64
65
  annotation = annotations.get(declared_field.name, declared_field.type)
@@ -154,9 +155,7 @@ def _mapping_value_for_annotation(
154
155
  if len(successes) == 1:
155
156
  return successes[0]
156
157
  exact_matches = tuple(
157
- converted
158
- for converted in successes
159
- if type(converted) is type(value)
158
+ converted for converted in successes if type(converted) is type(value)
160
159
  )
161
160
  if len(exact_matches) == 1:
162
161
  return exact_matches[0]
@@ -164,9 +163,7 @@ def _mapping_value_for_annotation(
164
163
  raise TypeError(
165
164
  f"{path} ambiguously matches multiple members of {annotation!r}."
166
165
  )
167
- value_errors = tuple(
168
- error for error in errors if isinstance(error, ValueError)
169
- )
166
+ value_errors = tuple(error for error in errors if isinstance(error, ValueError))
170
167
  if len(value_errors) == 1:
171
168
  raise value_errors[0]
172
169
  if value_errors:
@@ -197,7 +194,9 @@ def _mapping_value_for_annotation(
197
194
  for index, item in enumerate(value)
198
195
  )
199
196
  if member_types and len(value) != len(member_types):
200
- raise ValueError(f"{path} must contain {len(member_types)} item(s); got {len(value)}.")
197
+ raise ValueError(
198
+ f"{path} must contain {len(member_types)} item(s); got {len(value)}."
199
+ )
201
200
  return tuple(
202
201
  _mapping_value_for_annotation(
203
202
  member_type,
@@ -206,13 +205,19 @@ def _mapping_value_for_annotation(
206
205
  )
207
206
  for index, (member_type, item) in enumerate(zip(member_types, value))
208
207
  )
209
- if origin is list:
210
- if not isinstance(value, list):
208
+ if origin in {list, Sequence}:
209
+ if (origin is list and not isinstance(value, list)) or (
210
+ origin is Sequence
211
+ and (
212
+ not isinstance(value, Sequence)
213
+ or isinstance(value, (str, bytes, bytearray))
214
+ )
215
+ ):
211
216
  raise TypeError(f"{path} must be an array.")
212
217
  member_types = get_args(annotation)
213
218
  if not member_types:
214
- return list(value)
215
- return [
219
+ return list(value) if origin is list else tuple(value)
220
+ converted = [
216
221
  _mapping_value_for_annotation(
217
222
  member_types[0],
218
223
  item,
@@ -220,6 +225,7 @@ def _mapping_value_for_annotation(
220
225
  )
221
226
  for index, item in enumerate(value)
222
227
  ]
228
+ return converted if origin is list else tuple(converted)
223
229
  if origin in {dict, Mapping}:
224
230
  if not isinstance(value, Mapping):
225
231
  raise TypeError(f"{path} must be an object.")
@@ -250,7 +256,9 @@ def _mapping_value_for_annotation(
250
256
  return annotation(value)
251
257
  except ValueError as error:
252
258
  choices = tuple(member.value for member in annotation)
253
- raise ValueError(f"{path} must be one of {choices!r}; got {value!r}.") from error
259
+ raise ValueError(
260
+ f"{path} must be one of {choices!r}; got {value!r}."
261
+ ) from error
254
262
  if isinstance(annotation, type) and is_dataclass(annotation):
255
263
  if isinstance(value, annotation):
256
264
  return value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-introspect
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: Pure Python introspection toolkit for function signatures, dataclasses, and type hints
5
5
  Author-email: Tristan Simas <tristan.simas@mail.mcgill.ca>
6
6
  License: MIT
@@ -1,5 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import Enum
3
+ from collections.abc import Sequence
3
4
 
4
5
  import pytest
5
6
 
@@ -36,6 +37,59 @@ class UntypedTupleEnvelope:
36
37
  values: tuple
37
38
 
38
39
 
40
+ @dataclass(frozen=True)
41
+ class RecursiveSequenceNode:
42
+ name: str
43
+ children: Sequence["RecursiveSequenceNode"] = ()
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class SequenceEnvelope:
48
+ nodes: Sequence[RecursiveSequenceNode]
49
+
50
+
51
+ @pytest.mark.parametrize("container", (list, tuple))
52
+ def test_dataclass_mapping_recurses_from_abstract_sequence_annotations(container):
53
+ result = dataclass_from_mapping(
54
+ SequenceEnvelope,
55
+ {
56
+ "nodes": container([{"name": "parent", "children": [{"name": "child"}]}]),
57
+ },
58
+ )
59
+ assert result == SequenceEnvelope(
60
+ (RecursiveSequenceNode("parent", (RecursiveSequenceNode("child"),)),)
61
+ )
62
+
63
+
64
+ @pytest.mark.parametrize(
65
+ "value", ("text", b"bytes", bytearray(b"bytes"), {"name": "mapping"}, 7)
66
+ )
67
+ def test_dataclass_mapping_sequence_rejects_non_array_values(value):
68
+ with pytest.raises(TypeError, match="must be an array"):
69
+ dataclass_from_mapping(SequenceEnvelope, {"nodes": value})
70
+
71
+
72
+ def test_dataclass_mapping_recursive_sequence_rejects_unknown_fields_and_wrong_elements():
73
+ with pytest.raises(ValueError, match="undeclared.*extra"):
74
+ dataclass_from_mapping(
75
+ SequenceEnvelope,
76
+ {
77
+ "nodes": [
78
+ {"name": "parent", "children": [{"name": "child", "extra": 1}]}
79
+ ]
80
+ },
81
+ )
82
+ with pytest.raises(TypeError, match="must be an object"):
83
+ dataclass_from_mapping(SequenceEnvelope, {"nodes": [42]})
84
+
85
+
86
+ def test_dataclass_mapping_abstract_sequence_preserves_typed_members():
87
+ node = RecursiveSequenceNode("typed")
88
+ assert dataclass_from_mapping(
89
+ SequenceEnvelope, {"nodes": [node]}
90
+ ) == SequenceEnvelope((node,))
91
+
92
+
39
93
  def test_dataclass_mapping_uses_nested_types_as_the_schema() -> None:
40
94
  result = dataclass_from_mapping(
41
95
  Envelope,