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.
- {python_introspect-0.1.13/src/python_introspect.egg-info → python_introspect-0.1.14}/PKG-INFO +1 -1
- {python_introspect-0.1.13 → python_introspect-0.1.14}/pyproject.toml +1 -1
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/dataclass_projection.py +25 -17
- {python_introspect-0.1.13 → python_introspect-0.1.14/src/python_introspect.egg-info}/PKG-INFO +1 -1
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_dataclass_projection.py +54 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/LICENSE +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/README.md +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/setup.cfg +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/__init__.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/annotation_types.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/callable_declaration.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/docstring_annotations.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/enableable.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/environment_projection.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/exceptions.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/runtime_parameter.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/signature_analyzer.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/unified_parameter_analyzer.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/validation.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/SOURCES.txt +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/dependency_links.txt +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/requires.txt +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/top_level.txt +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_annotation_types.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_callable_declaration.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_environment_projection.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_exceptions.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_init.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_runtime_parameter.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_signature_analyzer.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_unified_parameter_analyzer.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_validate_python_release_action.py +0 -0
- {python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_validation.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-introspect"
|
|
7
|
-
version = "0.1.
|
|
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"
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/dataclass_projection.py
RENAMED
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,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,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/annotation_types.py
RENAMED
|
File without changes
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/callable_declaration.py
RENAMED
|
File without changes
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/docstring_annotations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/runtime_parameter.py
RENAMED
|
File without changes
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect/signature_analyzer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/requires.txt
RENAMED
|
File without changes
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/src/python_introspect.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_unified_parameter_analyzer.py
RENAMED
|
File without changes
|
{python_introspect-0.1.13 → python_introspect-0.1.14}/tests/test_validate_python_release_action.py
RENAMED
|
File without changes
|
|
File without changes
|