python-introspect 0.2.3__tar.gz → 0.2.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.
Files changed (41) hide show
  1. {python_introspect-0.2.3/src/python_introspect.egg-info → python_introspect-0.2.5}/PKG-INFO +2 -1
  2. {python_introspect-0.2.3 → python_introspect-0.2.5}/pyproject.toml +2 -1
  3. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/__init__.py +1 -1
  4. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/annotation_types.py +31 -6
  5. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/signature_analyzer.py +27 -14
  6. {python_introspect-0.2.3 → python_introspect-0.2.5/src/python_introspect.egg-info}/PKG-INFO +2 -1
  7. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_dataclass_projection.py +35 -1
  8. {python_introspect-0.2.3 → python_introspect-0.2.5}/LICENSE +0 -0
  9. {python_introspect-0.2.3 → python_introspect-0.2.5}/README.md +0 -0
  10. {python_introspect-0.2.3 → python_introspect-0.2.5}/setup.cfg +0 -0
  11. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/callable_declaration.py +0 -0
  12. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/choices.py +0 -0
  13. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/dataclass_projection.py +0 -0
  14. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/docstring_annotations.py +0 -0
  15. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/enableable.py +0 -0
  16. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/environment_projection.py +0 -0
  17. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/exceptions.py +0 -0
  18. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/jsonable.py +0 -0
  19. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/public_api.py +0 -0
  20. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/runtime_parameter.py +0 -0
  21. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/unified_parameter_analyzer.py +0 -0
  22. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect/validation.py +0 -0
  23. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect.egg-info/SOURCES.txt +0 -0
  24. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect.egg-info/dependency_links.txt +0 -0
  25. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect.egg-info/requires.txt +0 -0
  26. {python_introspect-0.2.3 → python_introspect-0.2.5}/src/python_introspect.egg-info/top_level.txt +0 -0
  27. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_annotation_choices.py +0 -0
  28. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_annotation_types.py +0 -0
  29. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_callable_declaration.py +0 -0
  30. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_dataclass_source_preparation.py +0 -0
  31. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_environment_projection.py +0 -0
  32. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_exceptions.py +0 -0
  33. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_init.py +0 -0
  34. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_jsonable.py +0 -0
  35. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_public_api.py +0 -0
  36. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_release_readiness.py +0 -0
  37. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_runtime_parameter.py +0 -0
  38. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_signature_analyzer.py +0 -0
  39. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_unified_parameter_analyzer.py +0 -0
  40. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_validate_python_release_action.py +0 -0
  41. {python_introspect-0.2.3 → python_introspect-0.2.5}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-introspect
3
- Version: 0.2.3
3
+ Version: 0.2.5
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
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
20
  Classifier: Topic :: Utilities
20
21
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-introspect"
7
- version = "0.2.3"
7
+ version = "0.2.5"
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"
@@ -21,6 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.10",
22
22
  "Programming Language :: Python :: 3.11",
23
23
  "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
24
25
  "Topic :: Software Development :: Libraries :: Python Modules",
25
26
  "Topic :: Utilities",
26
27
  ]
@@ -9,7 +9,7 @@ Extensibility:
9
9
  type resolution for framework-specific types (lazy configs, proxies, etc.)
10
10
  """
11
11
 
12
- __version__ = "0.2.3"
12
+ __version__ = "0.2.5"
13
13
 
14
14
  from .signature_analyzer import (
15
15
  SignatureAnalyzer,
@@ -5,7 +5,15 @@ from __future__ import annotations
5
5
  import sys
6
6
  import types
7
7
  from enum import Enum
8
- from typing import Annotated, Literal, Union, get_args, get_origin, get_type_hints
8
+ from typing import (
9
+ Annotated,
10
+ ForwardRef,
11
+ Literal,
12
+ Union,
13
+ get_args,
14
+ get_origin,
15
+ get_type_hints,
16
+ )
9
17
 
10
18
 
11
19
  def resolved_class_annotations(owner: type) -> dict[str, object]:
@@ -13,6 +21,9 @@ def resolved_class_annotations(owner: type) -> dict[str, object]:
13
21
 
14
22
  Python 3.10 leaves strings inside PEP 585 aliases unresolved. Complete
15
23
  those declarations without interpreting Literal values or Annotated metadata.
24
+ A recursive alias such as ``V = int | list["V"]`` names itself; the inner
25
+ reference is left as a ``ForwardRef``, the form ``get_type_hints`` produces
26
+ for it on Python 3.11 and later.
16
27
  """
17
28
  annotations = get_type_hints(owner, include_extras=True)
18
29
  unresolved = set(annotations)
@@ -29,20 +40,34 @@ def resolved_class_annotations(owner: type) -> dict[str, object]:
29
40
  return annotations
30
41
 
31
42
 
32
- def _resolve_nested_type_strings(annotation: object, globalns: dict, localns: dict) -> object:
43
+ def _resolve_nested_type_strings(
44
+ annotation: object,
45
+ globalns: dict,
46
+ localns: dict,
47
+ expanding: frozenset[str] = frozenset(),
48
+ ) -> object:
33
49
  if isinstance(annotation, str):
34
- return _resolve_nested_type_strings(eval(annotation, globalns, localns), globalns, localns)
50
+ if annotation in expanding:
51
+ return ForwardRef(annotation)
52
+ return _resolve_nested_type_strings(
53
+ eval(annotation, globalns, localns), globalns, localns, expanding | {annotation}
54
+ )
35
55
  if isinstance(annotation, list):
36
- resolved = [_resolve_nested_type_strings(member, globalns, localns) for member in annotation]
56
+ resolved = [
57
+ _resolve_nested_type_strings(member, globalns, localns, expanding)
58
+ for member in annotation
59
+ ]
37
60
  return annotation if all(a is b for a, b in zip(resolved, annotation)) else resolved
38
61
  origin = get_origin(annotation)
39
62
  members = get_args(annotation)
40
63
  if not members or origin is Literal:
41
64
  return annotation
42
65
  if origin is Annotated:
43
- base = _resolve_nested_type_strings(members[0], globalns, localns)
66
+ base = _resolve_nested_type_strings(members[0], globalns, localns, expanding)
44
67
  return annotation if base is members[0] else Annotated[(base, *members[1:])]
45
- resolved = tuple(_resolve_nested_type_strings(member, globalns, localns) for member in members)
68
+ resolved = tuple(
69
+ _resolve_nested_type_strings(member, globalns, localns, expanding) for member in members
70
+ )
46
71
  if all(member is original for member, original in zip(resolved, members)):
47
72
  return annotation
48
73
  if isinstance(annotation, types.GenericAlias):
@@ -771,6 +771,26 @@ class CallableAnalysisContext:
771
771
  )
772
772
 
773
773
 
774
+ def _class_node_for_qualname(tree: ast.AST, qualname: str) -> Optional[ast.ClassDef]:
775
+ """Return the class statement whose qualified name is ``qualname``.
776
+
777
+ Function bodies contribute ``<locals>`` exactly as ``__qualname__`` does.
778
+ """
779
+ pending = [(node, ()) for node in reversed(getattr(tree, 'body', ()))]
780
+ while pending:
781
+ node, scope = pending.pop()
782
+ if isinstance(node, ast.ClassDef):
783
+ node_scope = (*scope, node.name)
784
+ if '.'.join(node_scope) == qualname:
785
+ return node
786
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
787
+ node_scope = (*scope, node.name, '<locals>')
788
+ else:
789
+ node_scope = scope
790
+ pending.extend((child, node_scope) for child in reversed(list(ast.iter_child_nodes(node))))
791
+ return None
792
+
793
+
774
794
  class ClassSourceDeclaration(NamedTuple):
775
795
  """Immutable located class source and its source-only documentation views."""
776
796
 
@@ -1196,20 +1216,13 @@ class SignatureAnalyzer:
1196
1216
  def _qualified_class_source(source: str, qualname: str) -> Optional[ClassSourceDeclaration]:
1197
1217
  """Use Python's qualified class/decorator locator on current source content."""
1198
1218
  tree = SignatureAnalyzer._module_source_tree(source)
1199
- finder = inspect._ClassFinder(qualname)
1200
- try:
1201
- finder.visit(tree)
1202
- except inspect.ClassFoundException as found:
1203
- lines = source.splitlines(keepends=True)
1204
- first_line = found.args[0] + 1
1205
- block = ''.join(inspect.getblock(lines[found.args[0]:]))
1206
- class_node = next(
1207
- node for node in ast.walk(tree)
1208
- if isinstance(node, ast.ClassDef)
1209
- and min([node.lineno, *(decorator.lineno for decorator in node.decorator_list)]) == first_line
1210
- )
1211
- return ClassSourceDeclaration.from_block(block, class_node, first_line)
1212
- return None
1219
+ class_node = _class_node_for_qualname(tree, qualname)
1220
+ if class_node is None:
1221
+ return None
1222
+ first_line = min([class_node.lineno, *(decorator.lineno for decorator in class_node.decorator_list)])
1223
+ lines = source.splitlines(keepends=True)
1224
+ block = ''.join(inspect.getblock(lines[first_line - 1:]))
1225
+ return ClassSourceDeclaration.from_block(block, class_node, first_line)
1213
1226
 
1214
1227
  @staticmethod
1215
1228
  def _extract_inline_field_docs(dataclass_type: type) -> Dict[str, str]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-introspect
3
- Version: 0.2.3
3
+ Version: 0.2.5
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
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
20
  Classifier: Topic :: Utilities
20
21
  Requires-Python: >=3.10
@@ -1,7 +1,7 @@
1
1
  from dataclasses import asdict, dataclass, field
2
2
  from enum import Enum
3
3
  from typing import Annotated, Literal
4
- from collections.abc import Sequence
4
+ from collections.abc import Mapping, Sequence
5
5
 
6
6
  import pytest
7
7
 
@@ -297,3 +297,37 @@ def test_inherited_nested_types_use_the_declaring_module_namespace(monkeypatch):
297
297
  )
298
298
  result = dataclass_from_mapping(child_module.Child, {"children": [{"name": "child"}]})
299
299
  assert isinstance(result.children[0], base_module.Leaf)
300
+
301
+
302
+ WireValue = (
303
+ str
304
+ | int
305
+ | float
306
+ | bool
307
+ | None
308
+ | list["WireValue"]
309
+ | tuple["WireValue", ...]
310
+ | dict[str, "WireValue"]
311
+ )
312
+
313
+
314
+ @dataclass(frozen=True)
315
+ class WireEnvelope:
316
+ sequence: int
317
+ event: Mapping[str, WireValue] | None
318
+
319
+
320
+ def test_recursive_type_alias_decodes_nested_values() -> None:
321
+ # Python 3.10 keeps "WireValue" as a string inside the PEP 585 aliases;
322
+ # resolving it must stop where the alias names itself.
323
+ payload = {
324
+ "sequence": 1,
325
+ "event": {
326
+ "phase": "compile",
327
+ "percent": 50.0,
328
+ "context": {"axes": ["A01"], "details": [{"ready": True, "error": None}]},
329
+ },
330
+ }
331
+ result = dataclass_from_mapping(WireEnvelope, payload)
332
+ assert result == WireEnvelope(1, payload["event"])
333
+ validate_annotated_dataclass(result)