python-introspect 0.1.8__tar.gz → 0.1.9__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.8/src/python_introspect.egg-info → python_introspect-0.1.9}/PKG-INFO +1 -1
- {python_introspect-0.1.8 → python_introspect-0.1.9}/pyproject.toml +1 -1
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/__init__.py +11 -1
- python_introspect-0.1.9/src/python_introspect/annotation_types.py +156 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9/src/python_introspect.egg-info}/PKG-INFO +1 -1
- {python_introspect-0.1.8 → python_introspect-0.1.9}/tests/test_annotation_types.py +33 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/tests/test_init.py +1 -1
- python_introspect-0.1.8/src/python_introspect/annotation_types.py +0 -87
- {python_introspect-0.1.8 → python_introspect-0.1.9}/LICENSE +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/README.md +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/setup.cfg +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/dataclass_projection.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/enableable.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/environment_projection.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/exceptions.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/signature_analyzer.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/unified_parameter_analyzer.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/validation.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect.egg-info/SOURCES.txt +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect.egg-info/dependency_links.txt +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect.egg-info/requires.txt +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect.egg-info/top_level.txt +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/tests/test_dataclass_projection.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/tests/test_environment_projection.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/tests/test_exceptions.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/tests/test_signature_analyzer.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/tests/test_unified_parameter_analyzer.py +0 -0
- {python_introspect-0.1.8 → python_introspect-0.1.9}/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.9"
|
|
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"
|
|
@@ -9,7 +9,7 @@ Extensibility:
|
|
|
9
9
|
type resolution for framework-specific types (lazy configs, proxies, etc.)
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
__version__ = "0.1.
|
|
12
|
+
__version__ = "0.1.9"
|
|
13
13
|
|
|
14
14
|
from .signature_analyzer import (
|
|
15
15
|
SignatureAnalyzer,
|
|
@@ -56,6 +56,11 @@ from .environment_projection import (
|
|
|
56
56
|
overlay_dataclass_from_environment,
|
|
57
57
|
)
|
|
58
58
|
from .annotation_types import (
|
|
59
|
+
coerce_enum_member,
|
|
60
|
+
declared_enum_type,
|
|
61
|
+
enum_import_path,
|
|
62
|
+
enum_input_values,
|
|
63
|
+
enum_member_names,
|
|
59
64
|
enum_member_type,
|
|
60
65
|
get_enum_from_list,
|
|
61
66
|
is_enum_type,
|
|
@@ -108,6 +113,11 @@ __all__ = [
|
|
|
108
113
|
"EnvironmentVariable",
|
|
109
114
|
"overlay_dataclass_from_environment",
|
|
110
115
|
# Annotation type operations
|
|
116
|
+
"coerce_enum_member",
|
|
117
|
+
"declared_enum_type",
|
|
118
|
+
"enum_import_path",
|
|
119
|
+
"enum_input_values",
|
|
120
|
+
"enum_member_names",
|
|
111
121
|
"enum_member_type",
|
|
112
122
|
"get_enum_from_list",
|
|
113
123
|
"is_enum_type",
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Generic operations derived directly from Python type annotations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import types
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Annotated, Union, get_args, get_origin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_union_type(annotation: object) -> bool:
|
|
11
|
+
"""Return whether ``annotation`` is a typing or PEP 604 union."""
|
|
12
|
+
|
|
13
|
+
return get_origin(annotation) in {Union, types.UnionType}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def optional_member_type(annotation: object) -> object | None:
|
|
17
|
+
"""Return ``T`` only for the simple optional declaration ``T | None``."""
|
|
18
|
+
|
|
19
|
+
if not is_union_type(annotation):
|
|
20
|
+
return None
|
|
21
|
+
members = get_args(annotation)
|
|
22
|
+
if len(members) != 2 or type(None) not in members:
|
|
23
|
+
return None
|
|
24
|
+
return next(member for member in members if member is not type(None))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def make_optional(annotation: object) -> object:
|
|
28
|
+
"""Return the same declaration with ``None`` admitted exactly once."""
|
|
29
|
+
|
|
30
|
+
if is_union_type(annotation) and type(None) in get_args(annotation):
|
|
31
|
+
return annotation
|
|
32
|
+
return Union[annotation, type(None)]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_optional(annotation: object) -> object:
|
|
36
|
+
"""Resolve an optional declaration to its non-None member declaration."""
|
|
37
|
+
|
|
38
|
+
member_type = optional_member_type(annotation)
|
|
39
|
+
return annotation if member_type is None else member_type
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_annotated(annotation: object) -> object:
|
|
43
|
+
"""Resolve an ``Annotated[T, ...]`` declaration to its owned type ``T``."""
|
|
44
|
+
|
|
45
|
+
if get_origin(annotation) is Annotated:
|
|
46
|
+
return get_args(annotation)[0]
|
|
47
|
+
return annotation
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_enum_type(annotation: object) -> bool:
|
|
51
|
+
"""Return whether the declaration is an enum type."""
|
|
52
|
+
|
|
53
|
+
return isinstance(annotation, type) and issubclass(annotation, Enum)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def enum_member_type(annotation: object) -> type[Enum] | None:
|
|
57
|
+
"""Return the enum type declared directly or as a simple optional."""
|
|
58
|
+
|
|
59
|
+
if is_enum_type(annotation):
|
|
60
|
+
return annotation
|
|
61
|
+
if not is_union_type(annotation):
|
|
62
|
+
return None
|
|
63
|
+
members = tuple(member for member in get_args(annotation) if member is not type(None))
|
|
64
|
+
if len(members) != 1 or not is_enum_type(members[0]):
|
|
65
|
+
return None
|
|
66
|
+
return members[0]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def declared_enum_type(annotation: object) -> type[Enum] | None:
|
|
70
|
+
"""Return the enum carried by transparent annotation wrappers.
|
|
71
|
+
|
|
72
|
+
Schema and transport projections need to recognise the same enum when it is
|
|
73
|
+
wrapped by ``Annotated``, ``Optional``, or a homogeneous container. Mixed
|
|
74
|
+
unions and heterogeneous containers admit values outside that enum, so they
|
|
75
|
+
deliberately have no single declared enum owner.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
resolved = resolve_annotated(annotation)
|
|
79
|
+
if is_enum_type(resolved):
|
|
80
|
+
return resolved
|
|
81
|
+
|
|
82
|
+
optional_member = optional_member_type(resolved)
|
|
83
|
+
if optional_member is not None:
|
|
84
|
+
return declared_enum_type(optional_member)
|
|
85
|
+
|
|
86
|
+
origin = get_origin(resolved)
|
|
87
|
+
members = get_args(resolved)
|
|
88
|
+
if origin in {list, set, frozenset} and len(members) == 1:
|
|
89
|
+
return declared_enum_type(members[0])
|
|
90
|
+
if origin is tuple and len(members) == 2 and members[1] is Ellipsis:
|
|
91
|
+
return declared_enum_type(members[0])
|
|
92
|
+
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def enum_input_values(annotation: object) -> tuple[str, ...]:
|
|
97
|
+
"""Return declaration-derived string inputs accepted for an enum.
|
|
98
|
+
|
|
99
|
+
String-valued members expose their value. Members whose values are not
|
|
100
|
+
strings expose their name, which keeps JSON and command-line projections
|
|
101
|
+
unambiguous, including enums whose concrete value is ``None``.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
enum_type = declared_enum_type(annotation)
|
|
105
|
+
if enum_type is None:
|
|
106
|
+
return ()
|
|
107
|
+
return tuple(
|
|
108
|
+
member.value if isinstance(member.value, str) else member.name for member in enum_type
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def enum_member_names(annotation: object) -> tuple[str, ...]:
|
|
113
|
+
"""Return member names for the single enum carried by an annotation."""
|
|
114
|
+
|
|
115
|
+
enum_type = declared_enum_type(annotation)
|
|
116
|
+
if enum_type is None:
|
|
117
|
+
return ()
|
|
118
|
+
return tuple(member.name for member in enum_type)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def enum_import_path(annotation: object) -> str | None:
|
|
122
|
+
"""Return the import path of the single enum carried by an annotation."""
|
|
123
|
+
|
|
124
|
+
enum_type = declared_enum_type(annotation)
|
|
125
|
+
if enum_type is None:
|
|
126
|
+
return None
|
|
127
|
+
return f"{enum_type.__module__}.{enum_type.__qualname__}"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def coerce_enum_member(annotation: object, value: object) -> Enum:
|
|
131
|
+
"""Coerce an enum value or declared member name through its annotation."""
|
|
132
|
+
|
|
133
|
+
enum_type = declared_enum_type(annotation)
|
|
134
|
+
if enum_type is None:
|
|
135
|
+
raise TypeError(f"Annotation does not declare one enum type: {annotation!r}")
|
|
136
|
+
if isinstance(value, enum_type):
|
|
137
|
+
return value
|
|
138
|
+
for member in enum_type:
|
|
139
|
+
if value == member.value or value == member.name:
|
|
140
|
+
return member
|
|
141
|
+
raise ValueError(f"{value!r} is not a valid {enum_type.__module__}.{enum_type.__name__}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def is_list_of_enums(annotation: object) -> bool:
|
|
145
|
+
"""Return whether the declaration is ``list[SomeEnum]``."""
|
|
146
|
+
|
|
147
|
+
members = get_args(annotation)
|
|
148
|
+
return get_origin(annotation) is list and len(members) == 1 and is_enum_type(members[0])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_enum_from_list(annotation: object) -> type[Enum] | None:
|
|
152
|
+
"""Return the enum type from ``list[SomeEnum]`` when declared."""
|
|
153
|
+
|
|
154
|
+
if not is_list_of_enums(annotation):
|
|
155
|
+
return None
|
|
156
|
+
return get_args(annotation)[0]
|
|
@@ -2,6 +2,11 @@ from enum import Enum
|
|
|
2
2
|
from typing import Annotated, Optional
|
|
3
3
|
|
|
4
4
|
from python_introspect import (
|
|
5
|
+
coerce_enum_member,
|
|
6
|
+
declared_enum_type,
|
|
7
|
+
enum_import_path,
|
|
8
|
+
enum_input_values,
|
|
9
|
+
enum_member_names,
|
|
5
10
|
enum_member_type,
|
|
6
11
|
get_enum_from_list,
|
|
7
12
|
is_list_of_enums,
|
|
@@ -18,6 +23,15 @@ class Mode(Enum):
|
|
|
18
23
|
SECOND = "second"
|
|
19
24
|
|
|
20
25
|
|
|
26
|
+
class OptionalMode(Enum):
|
|
27
|
+
ENABLED = "enabled"
|
|
28
|
+
INHERIT = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OtherMode(Enum):
|
|
32
|
+
THIRD = "third"
|
|
33
|
+
|
|
34
|
+
|
|
21
35
|
def test_optional_operations_derive_from_the_annotation() -> None:
|
|
22
36
|
annotated = Annotated[int, "units"]
|
|
23
37
|
optional = make_optional(annotated)
|
|
@@ -44,3 +58,22 @@ def test_enum_operations_derive_from_the_annotation() -> None:
|
|
|
44
58
|
assert is_list_of_enums(list[Mode])
|
|
45
59
|
assert get_enum_from_list(list[Mode]) is Mode
|
|
46
60
|
assert get_enum_from_list(list[str]) is None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_enum_schema_operations_follow_the_single_nested_declaration() -> None:
|
|
64
|
+
annotation = Annotated[list[Optional[Mode]], "modes"]
|
|
65
|
+
|
|
66
|
+
assert declared_enum_type(annotation) is Mode
|
|
67
|
+
assert enum_input_values(annotation) == ("first", "second")
|
|
68
|
+
assert enum_member_names(annotation) == ("FIRST", "SECOND")
|
|
69
|
+
assert enum_import_path(annotation) == f"{Mode.__module__}.{Mode.__qualname__}"
|
|
70
|
+
assert declared_enum_type(Mode | OtherMode) is None
|
|
71
|
+
assert declared_enum_type(Mode | str) is None
|
|
72
|
+
assert declared_enum_type(tuple[Mode, ...]) is Mode
|
|
73
|
+
assert declared_enum_type(tuple[Mode, str]) is None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_enum_input_coercion_accepts_values_and_non_string_member_names() -> None:
|
|
77
|
+
assert coerce_enum_member(OptionalMode, "enabled") is OptionalMode.ENABLED
|
|
78
|
+
assert coerce_enum_member(Optional[OptionalMode], "INHERIT") is OptionalMode.INHERIT
|
|
79
|
+
assert enum_input_values(OptionalMode) == ("enabled", "INHERIT")
|
|
@@ -11,7 +11,7 @@ class TestPackageImports:
|
|
|
11
11
|
"""Test that __version__ is available."""
|
|
12
12
|
assert hasattr(python_introspect, "__version__")
|
|
13
13
|
assert isinstance(python_introspect.__version__, str)
|
|
14
|
-
assert python_introspect.__version__ == "0.1.
|
|
14
|
+
assert python_introspect.__version__ == "0.1.9"
|
|
15
15
|
|
|
16
16
|
def test_signature_analyzer_import(self):
|
|
17
17
|
"""Test SignatureAnalyzer is importable."""
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
"""Generic operations derived directly from Python type annotations."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import types
|
|
6
|
-
from enum import Enum
|
|
7
|
-
from typing import Annotated, Union, get_args, get_origin
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def is_union_type(annotation: object) -> bool:
|
|
11
|
-
"""Return whether ``annotation`` is a typing or PEP 604 union."""
|
|
12
|
-
|
|
13
|
-
return get_origin(annotation) in {Union, types.UnionType}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def optional_member_type(annotation: object) -> object | None:
|
|
17
|
-
"""Return ``T`` only for the simple optional declaration ``T | None``."""
|
|
18
|
-
|
|
19
|
-
if not is_union_type(annotation):
|
|
20
|
-
return None
|
|
21
|
-
members = get_args(annotation)
|
|
22
|
-
if len(members) != 2 or type(None) not in members:
|
|
23
|
-
return None
|
|
24
|
-
return next(member for member in members if member is not type(None))
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def make_optional(annotation: object) -> object:
|
|
28
|
-
"""Return the same declaration with ``None`` admitted exactly once."""
|
|
29
|
-
|
|
30
|
-
if is_union_type(annotation) and type(None) in get_args(annotation):
|
|
31
|
-
return annotation
|
|
32
|
-
return Union[annotation, type(None)]
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def resolve_optional(annotation: object) -> object:
|
|
36
|
-
"""Resolve an optional declaration to its non-None member declaration."""
|
|
37
|
-
|
|
38
|
-
member_type = optional_member_type(annotation)
|
|
39
|
-
return annotation if member_type is None else member_type
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def resolve_annotated(annotation: object) -> object:
|
|
43
|
-
"""Resolve an ``Annotated[T, ...]`` declaration to its owned type ``T``."""
|
|
44
|
-
|
|
45
|
-
if get_origin(annotation) is Annotated:
|
|
46
|
-
return get_args(annotation)[0]
|
|
47
|
-
return annotation
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def is_enum_type(annotation: object) -> bool:
|
|
51
|
-
"""Return whether the declaration is an enum type."""
|
|
52
|
-
|
|
53
|
-
return isinstance(annotation, type) and issubclass(annotation, Enum)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def enum_member_type(annotation: object) -> type[Enum] | None:
|
|
57
|
-
"""Return the enum type declared directly or as a simple optional."""
|
|
58
|
-
|
|
59
|
-
if is_enum_type(annotation):
|
|
60
|
-
return annotation
|
|
61
|
-
if not is_union_type(annotation):
|
|
62
|
-
return None
|
|
63
|
-
members = tuple(
|
|
64
|
-
member for member in get_args(annotation) if member is not type(None)
|
|
65
|
-
)
|
|
66
|
-
if len(members) != 1 or not is_enum_type(members[0]):
|
|
67
|
-
return None
|
|
68
|
-
return members[0]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def is_list_of_enums(annotation: object) -> bool:
|
|
72
|
-
"""Return whether the declaration is ``list[SomeEnum]``."""
|
|
73
|
-
|
|
74
|
-
members = get_args(annotation)
|
|
75
|
-
return (
|
|
76
|
-
get_origin(annotation) is list
|
|
77
|
-
and len(members) == 1
|
|
78
|
-
and is_enum_type(members[0])
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def get_enum_from_list(annotation: object) -> type[Enum] | None:
|
|
83
|
-
"""Return the enum type from ``list[SomeEnum]`` when declared."""
|
|
84
|
-
|
|
85
|
-
if not is_list_of_enums(annotation):
|
|
86
|
-
return None
|
|
87
|
-
return get_args(annotation)[0]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/dataclass_projection.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/environment_projection.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect/signature_analyzer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect.egg-info/requires.txt
RENAMED
|
File without changes
|
{python_introspect-0.1.8 → python_introspect-0.1.9}/src/python_introspect.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.8 → python_introspect-0.1.9}/tests/test_unified_parameter_analyzer.py
RENAMED
|
File without changes
|
|
File without changes
|