python-introspect 0.1.8__tar.gz → 0.1.10__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 (30) hide show
  1. {python_introspect-0.1.8/src/python_introspect.egg-info → python_introspect-0.1.10}/PKG-INFO +1 -1
  2. {python_introspect-0.1.8 → python_introspect-0.1.10}/pyproject.toml +1 -1
  3. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect/__init__.py +14 -1
  4. python_introspect-0.1.10/src/python_introspect/annotation_types.py +156 -0
  5. python_introspect-0.1.10/src/python_introspect/callable_declaration.py +37 -0
  6. {python_introspect-0.1.8 → python_introspect-0.1.10/src/python_introspect.egg-info}/PKG-INFO +1 -1
  7. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect.egg-info/SOURCES.txt +2 -0
  8. {python_introspect-0.1.8 → python_introspect-0.1.10}/tests/test_annotation_types.py +33 -0
  9. python_introspect-0.1.10/tests/test_callable_declaration.py +40 -0
  10. {python_introspect-0.1.8 → python_introspect-0.1.10}/tests/test_init.py +1 -1
  11. python_introspect-0.1.8/src/python_introspect/annotation_types.py +0 -87
  12. {python_introspect-0.1.8 → python_introspect-0.1.10}/LICENSE +0 -0
  13. {python_introspect-0.1.8 → python_introspect-0.1.10}/README.md +0 -0
  14. {python_introspect-0.1.8 → python_introspect-0.1.10}/setup.cfg +0 -0
  15. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect/dataclass_projection.py +0 -0
  16. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect/enableable.py +0 -0
  17. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect/environment_projection.py +0 -0
  18. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect/exceptions.py +0 -0
  19. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect/signature_analyzer.py +0 -0
  20. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect/unified_parameter_analyzer.py +0 -0
  21. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect/validation.py +0 -0
  22. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect.egg-info/dependency_links.txt +0 -0
  23. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect.egg-info/requires.txt +0 -0
  24. {python_introspect-0.1.8 → python_introspect-0.1.10}/src/python_introspect.egg-info/top_level.txt +0 -0
  25. {python_introspect-0.1.8 → python_introspect-0.1.10}/tests/test_dataclass_projection.py +0 -0
  26. {python_introspect-0.1.8 → python_introspect-0.1.10}/tests/test_environment_projection.py +0 -0
  27. {python_introspect-0.1.8 → python_introspect-0.1.10}/tests/test_exceptions.py +0 -0
  28. {python_introspect-0.1.8 → python_introspect-0.1.10}/tests/test_signature_analyzer.py +0 -0
  29. {python_introspect-0.1.8 → python_introspect-0.1.10}/tests/test_unified_parameter_analyzer.py +0 -0
  30. {python_introspect-0.1.8 → python_introspect-0.1.10}/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.8
3
+ Version: 0.1.10
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.8"
7
+ version = "0.1.10"
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.8"
12
+ __version__ = "0.1.10"
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,
@@ -66,6 +71,7 @@ from .annotation_types import (
66
71
  resolve_annotated,
67
72
  resolve_optional,
68
73
  )
74
+ from .callable_declaration import callable_declaration_kwargs
69
75
 
70
76
  __all__ = [
71
77
  # Version
@@ -108,6 +114,11 @@ __all__ = [
108
114
  "EnvironmentVariable",
109
115
  "overlay_dataclass_from_environment",
110
116
  # Annotation type operations
117
+ "coerce_enum_member",
118
+ "declared_enum_type",
119
+ "enum_import_path",
120
+ "enum_input_values",
121
+ "enum_member_names",
111
122
  "enum_member_type",
112
123
  "get_enum_from_list",
113
124
  "is_enum_type",
@@ -117,4 +128,6 @@ __all__ = [
117
128
  "optional_member_type",
118
129
  "resolve_annotated",
119
130
  "resolve_optional",
131
+ # Callable declarations
132
+ "callable_declaration_kwargs",
120
133
  ]
@@ -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]
@@ -0,0 +1,37 @@
1
+ """Declaration projections derived from callable signatures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Callable, Mapping
7
+ from typing import TypeVar
8
+
9
+
10
+ ParameterValue = TypeVar("ParameterValue")
11
+
12
+
13
+ def callable_declaration_kwargs(
14
+ func: Callable,
15
+ kwargs: Mapping[str, ParameterValue],
16
+ *,
17
+ values_equal: Callable[[object, object], bool],
18
+ ) -> dict[str, ParameterValue]:
19
+ """Return declared kwargs whose values differ from signature defaults.
20
+
21
+ Equality remains caller-owned so frameworks can supply semantics for
22
+ arrays, lazy values, and other values whose ``==`` result is not boolean.
23
+ """
24
+
25
+ try:
26
+ defaults = {
27
+ name: parameter.default
28
+ for name, parameter in inspect.signature(func).parameters.items()
29
+ if parameter.default is not inspect.Parameter.empty
30
+ }
31
+ except (TypeError, ValueError):
32
+ defaults = {}
33
+ return {
34
+ name: value
35
+ for name, value in kwargs.items()
36
+ if name not in defaults or not values_equal(value, defaults[name])
37
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-introspect
3
- Version: 0.1.8
3
+ Version: 0.1.10
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
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  src/python_introspect/__init__.py
5
5
  src/python_introspect/annotation_types.py
6
+ src/python_introspect/callable_declaration.py
6
7
  src/python_introspect/dataclass_projection.py
7
8
  src/python_introspect/enableable.py
8
9
  src/python_introspect/environment_projection.py
@@ -16,6 +17,7 @@ src/python_introspect.egg-info/dependency_links.txt
16
17
  src/python_introspect.egg-info/requires.txt
17
18
  src/python_introspect.egg-info/top_level.txt
18
19
  tests/test_annotation_types.py
20
+ tests/test_callable_declaration.py
19
21
  tests/test_dataclass_projection.py
20
22
  tests/test_environment_projection.py
21
23
  tests/test_exceptions.py
@@ -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")
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from operator import attrgetter
4
+
5
+ from python_introspect import callable_declaration_kwargs
6
+
7
+
8
+ def sample_function(value, threshold: float = 1.0, enabled: bool = True):
9
+ return value
10
+
11
+
12
+ def test_callable_declaration_kwargs_projects_signature_defaults() -> None:
13
+ kwargs = {
14
+ "threshold": 1,
15
+ "enabled": False,
16
+ "extension_parameter": "preserved",
17
+ }
18
+
19
+ declaration = callable_declaration_kwargs(
20
+ sample_function,
21
+ kwargs,
22
+ values_equal=lambda left, right: left == right,
23
+ )
24
+
25
+ assert declaration == {
26
+ "enabled": False,
27
+ "extension_parameter": "preserved",
28
+ }
29
+
30
+
31
+ def test_callable_declaration_kwargs_preserves_values_for_opaque_callable() -> None:
32
+ kwargs = {"extension_parameter": "preserved"}
33
+
34
+ declaration = callable_declaration_kwargs(
35
+ attrgetter("value"),
36
+ kwargs,
37
+ values_equal=lambda left, right: left == right,
38
+ )
39
+
40
+ assert declaration == kwargs
@@ -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.8"
14
+ assert python_introspect.__version__ == "0.1.10"
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]