python-introspect 0.1.2__tar.gz → 0.1.4__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.2/src/python_introspect.egg-info → python_introspect-0.1.4}/PKG-INFO +4 -3
- {python_introspect-0.1.2 → python_introspect-0.1.4}/pyproject.toml +6 -5
- {python_introspect-0.1.2 → python_introspect-0.1.4}/src/python_introspect/__init__.py +20 -4
- python_introspect-0.1.4/src/python_introspect/enableable.py +79 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/src/python_introspect/signature_analyzer.py +84 -54
- {python_introspect-0.1.2 → python_introspect-0.1.4}/src/python_introspect/unified_parameter_analyzer.py +55 -40
- {python_introspect-0.1.2 → python_introspect-0.1.4/src/python_introspect.egg-info}/PKG-INFO +4 -3
- {python_introspect-0.1.2 → python_introspect-0.1.4}/src/python_introspect.egg-info/SOURCES.txt +1 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/src/python_introspect.egg-info/requires.txt +3 -2
- {python_introspect-0.1.2 → python_introspect-0.1.4}/LICENSE +0 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/README.md +0 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/setup.cfg +0 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/src/python_introspect/exceptions.py +0 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/src/python_introspect.egg-info/dependency_links.txt +0 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/src/python_introspect.egg-info/top_level.txt +0 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/tests/test_exceptions.py +0 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/tests/test_init.py +0 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/tests/test_signature_analyzer.py +0 -0
- {python_introspect-0.1.2 → python_introspect-0.1.4}/tests/test_unified_parameter_analyzer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-introspect
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -28,8 +28,9 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
|
28
28
|
Requires-Dist: black>=23.0; extra == "dev"
|
|
29
29
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
30
30
|
Provides-Extra: docs
|
|
31
|
-
Requires-Dist:
|
|
32
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: sphinx>=7.0.0; extra == "docs"
|
|
32
|
+
Requires-Dist: sphinx-rtd-theme>=2.0.0; extra == "docs"
|
|
33
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.24.0; extra == "docs"
|
|
33
34
|
Dynamic: license-file
|
|
34
35
|
|
|
35
36
|
# python-introspect
|
|
@@ -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.4"
|
|
8
8
|
description = "Pure Python introspection toolkit for function signatures, dataclasses, and type hints"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -37,8 +37,9 @@ dev = [
|
|
|
37
37
|
"mypy>=1.0",
|
|
38
38
|
]
|
|
39
39
|
docs = [
|
|
40
|
-
"
|
|
41
|
-
"
|
|
40
|
+
"sphinx>=7.0.0",
|
|
41
|
+
"sphinx-rtd-theme>=2.0.0",
|
|
42
|
+
"sphinx-autodoc-typehints>=1.24.0",
|
|
42
43
|
]
|
|
43
44
|
|
|
44
45
|
[project.urls]
|
|
@@ -57,14 +58,14 @@ python_functions = ["test_*"]
|
|
|
57
58
|
|
|
58
59
|
[tool.ruff]
|
|
59
60
|
line-length = 100
|
|
60
|
-
target-version = "
|
|
61
|
+
target-version = "0.1.4"
|
|
61
62
|
|
|
62
63
|
[tool.black]
|
|
63
64
|
line-length = 100
|
|
64
65
|
target-version = ["py39"]
|
|
65
66
|
|
|
66
67
|
[tool.mypy]
|
|
67
|
-
python_version = "
|
|
68
|
+
python_version = "0.1.4"
|
|
68
69
|
warn_return_any = true
|
|
69
70
|
warn_unused_configs = true
|
|
70
71
|
disallow_untyped_defs = true
|
|
@@ -3,15 +3,20 @@ python-introspect: Pure Python introspection toolkit
|
|
|
3
3
|
|
|
4
4
|
This package provides utilities for introspecting Python functions, methods,
|
|
5
5
|
dataclasses, and type hints.
|
|
6
|
+
|
|
7
|
+
Extensibility:
|
|
8
|
+
Use register_namespace_provider() and register_type_resolver() to extend
|
|
9
|
+
type resolution for framework-specific types (lazy configs, proxies, etc.)
|
|
6
10
|
"""
|
|
7
11
|
|
|
8
|
-
__version__ = "0.1.
|
|
12
|
+
__version__ = "0.1.4"
|
|
9
13
|
|
|
10
14
|
from .signature_analyzer import (
|
|
11
15
|
SignatureAnalyzer,
|
|
12
16
|
ParameterInfo,
|
|
13
17
|
DocstringInfo,
|
|
14
18
|
DocstringExtractor,
|
|
19
|
+
# Plugin registration
|
|
15
20
|
register_namespace_provider,
|
|
16
21
|
register_type_resolver,
|
|
17
22
|
)
|
|
@@ -25,6 +30,12 @@ from .exceptions import (
|
|
|
25
30
|
DocstringParsingError,
|
|
26
31
|
TypeResolutionError,
|
|
27
32
|
)
|
|
33
|
+
from .enableable import (
|
|
34
|
+
Enableable,
|
|
35
|
+
is_enableable,
|
|
36
|
+
mark_enableable,
|
|
37
|
+
ENABLED_FIELD,
|
|
38
|
+
)
|
|
28
39
|
|
|
29
40
|
__all__ = [
|
|
30
41
|
# Version
|
|
@@ -34,15 +45,20 @@ __all__ = [
|
|
|
34
45
|
"ParameterInfo",
|
|
35
46
|
"DocstringInfo",
|
|
36
47
|
"DocstringExtractor",
|
|
48
|
+
# Plugin registration
|
|
49
|
+
"register_namespace_provider",
|
|
50
|
+
"register_type_resolver",
|
|
37
51
|
# Unified analysis
|
|
38
52
|
"UnifiedParameterAnalyzer",
|
|
39
53
|
"UnifiedParameterInfo",
|
|
40
|
-
# Plugin system
|
|
41
|
-
"register_namespace_provider",
|
|
42
|
-
"register_type_resolver",
|
|
43
54
|
# Exceptions
|
|
44
55
|
"IntrospectionError",
|
|
45
56
|
"SignatureAnalysisError",
|
|
46
57
|
"DocstringParsingError",
|
|
47
58
|
"TypeResolutionError",
|
|
59
|
+
# Enableable
|
|
60
|
+
"Enableable",
|
|
61
|
+
"is_enableable",
|
|
62
|
+
"mark_enableable",
|
|
63
|
+
"ENABLED_FIELD",
|
|
48
64
|
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Nominal enable semantics as type-safe metadata.
|
|
2
|
+
|
|
3
|
+
This module provides a single, shared "axis" for objects and callables that
|
|
4
|
+
participate in enabled semantics.
|
|
5
|
+
|
|
6
|
+
Design goals:
|
|
7
|
+
- Nominal (not structural): only explicitly branded callables qualify.
|
|
8
|
+
- Dataclass-friendly: configs can inherit Enableable to get an enabled field.
|
|
9
|
+
- Callable-safe: branded callables must declare an `enabled` parameter.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import inspect
|
|
15
|
+
from abc import ABC, ABCMeta
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ENABLED_FIELD = 'enabled'
|
|
21
|
+
|
|
22
|
+
_ENABLEABLE_TAG = object()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EnableableMeta(ABCMeta):
|
|
26
|
+
"""Metaclass enabling nominal isinstance checks for branded callables."""
|
|
27
|
+
|
|
28
|
+
def __instancecheck__(cls, instance: Any) -> bool: # type: ignore[override]
|
|
29
|
+
if getattr(instance, '__enableable_tag__', None) is _ENABLEABLE_TAG:
|
|
30
|
+
return True
|
|
31
|
+
return super().__instancecheck__(instance)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Enableable(ABC, metaclass=EnableableMeta):
|
|
36
|
+
"""Mixin indicating an object participates in enabled semantics."""
|
|
37
|
+
|
|
38
|
+
enabled: bool = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_enableable(obj: Any) -> bool:
|
|
42
|
+
"""Return True iff obj is nominally Enableable.
|
|
43
|
+
|
|
44
|
+
Works for both instances (using isinstance) and classes (using issubclass).
|
|
45
|
+
This is needed because widget creation code needs to check if a type (class)
|
|
46
|
+
is enableable, not just instances.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
# Check if obj is a type/class
|
|
50
|
+
if isinstance(obj, type):
|
|
51
|
+
# obj is a class - check if it's a subclass of Enableable
|
|
52
|
+
try:
|
|
53
|
+
return issubclass(obj, Enableable)
|
|
54
|
+
except TypeError:
|
|
55
|
+
# obj is not a class or is not class-like (e.g., a generic type)
|
|
56
|
+
return False
|
|
57
|
+
else:
|
|
58
|
+
# obj is an instance - use isinstance
|
|
59
|
+
return isinstance(obj, Enableable)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def mark_enableable(obj: Any, *, enabled_default: bool = True) -> Any:
|
|
63
|
+
"""Nominally brand an object/callable as Enableable.
|
|
64
|
+
|
|
65
|
+
This does not wrap and does not change call semantics.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
_ = enabled_default # reserved for future: default enabled semantics
|
|
69
|
+
|
|
70
|
+
# If we're branding a callable, require the enabled kwarg to exist.
|
|
71
|
+
if callable(obj) and not isinstance(obj, type):
|
|
72
|
+
sig = inspect.signature(obj)
|
|
73
|
+
if ENABLED_FIELD not in sig.parameters:
|
|
74
|
+
raise TypeError(
|
|
75
|
+
f"Enableable callable '{getattr(obj, '__name__', obj)}' must have an '{ENABLED_FIELD}' parameter"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
setattr(obj, '__enableable_tag__', _ENABLEABLE_TAG)
|
|
79
|
+
return obj
|
{python_introspect-0.1.2 → python_introspect-0.1.4}/src/python_introspect/signature_analyzer.py
RENAMED
|
@@ -1,86 +1,82 @@
|
|
|
1
1
|
# File: python_introspect/signature_analyzer.py
|
|
2
|
+
"""
|
|
3
|
+
Signature analysis with extensible type resolution.
|
|
4
|
+
|
|
5
|
+
This module provides pure Python introspection with a plugin architecture
|
|
6
|
+
for framework-specific extensions. Register namespace providers and type
|
|
7
|
+
resolvers to extend functionality without modifying this code.
|
|
8
|
+
"""
|
|
2
9
|
|
|
3
10
|
import ast
|
|
4
11
|
import inspect
|
|
5
12
|
import dataclasses
|
|
6
13
|
import re
|
|
7
14
|
from typing import Any, Dict, Callable, get_type_hints, NamedTuple, Union, Optional, Type, List
|
|
15
|
+
|
|
8
16
|
from dataclasses import dataclass
|
|
9
17
|
|
|
10
|
-
#
|
|
11
|
-
#
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# PLUGIN REGISTRY - Allows frameworks to extend type resolution
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
# Namespace providers: functions that return Dict[str, Any] for get_type_hints()
|
|
23
|
+
# Used to resolve forward references like "GlobalPipelineConfig" -> actual class
|
|
12
24
|
_namespace_providers: List[Callable[[], Dict[str, Any]]] = []
|
|
25
|
+
|
|
26
|
+
# Type resolvers: functions that map types to their "real" types
|
|
27
|
+
# e.g., LazyWellFilterConfig -> WellFilterConfig
|
|
13
28
|
_type_resolvers: List[Callable[[type], Optional[type]]] = []
|
|
14
29
|
|
|
15
30
|
|
|
16
31
|
def register_namespace_provider(provider: Callable[[], Dict[str, Any]]) -> None:
|
|
17
32
|
"""Register a namespace provider for forward reference resolution.
|
|
18
33
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
to be used when resolving forward references in type hints.
|
|
34
|
+
The provider function should return a dict of names to types/values
|
|
35
|
+
that will be available during get_type_hints() resolution.
|
|
22
36
|
|
|
23
37
|
Example:
|
|
24
|
-
|
|
25
|
-
import mypackage
|
|
26
|
-
return vars(mypackage)
|
|
27
|
-
|
|
28
|
-
register_namespace_provider(my_namespace_provider)
|
|
38
|
+
register_namespace_provider(lambda: {'MyClass': MyClass, 'MyEnum': MyEnum})
|
|
29
39
|
"""
|
|
30
40
|
_namespace_providers.append(provider)
|
|
31
41
|
|
|
32
42
|
|
|
33
43
|
def register_type_resolver(resolver: Callable[[type], Optional[type]]) -> None:
|
|
34
|
-
"""Register a type resolver for
|
|
44
|
+
"""Register a type resolver for lazy/proxy type unwrapping.
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- A transformed type (e.g., LazyConfig -> Config)
|
|
39
|
-
- None to defer to other resolvers
|
|
46
|
+
The resolver function should return the resolved type if it can handle
|
|
47
|
+
the input type, or None to defer to other resolvers.
|
|
40
48
|
|
|
41
49
|
Example:
|
|
42
|
-
def
|
|
50
|
+
def resolve_lazy(t):
|
|
43
51
|
if t.__name__.startswith('Lazy'):
|
|
44
52
|
return get_base_type(t)
|
|
45
53
|
return None
|
|
46
|
-
|
|
47
|
-
register_type_resolver(my_type_resolver)
|
|
54
|
+
register_type_resolver(resolve_lazy)
|
|
48
55
|
"""
|
|
49
56
|
_type_resolvers.append(resolver)
|
|
50
57
|
|
|
51
58
|
|
|
52
|
-
def
|
|
53
|
-
"""Get all registered
|
|
54
|
-
result = {}
|
|
59
|
+
def _get_extended_namespace() -> Dict[str, Any]:
|
|
60
|
+
"""Get combined namespace from all registered providers."""
|
|
61
|
+
result: Dict[str, Any] = {}
|
|
55
62
|
for provider in _namespace_providers:
|
|
56
63
|
try:
|
|
57
|
-
|
|
58
|
-
if namespace:
|
|
59
|
-
result.update(namespace)
|
|
64
|
+
result.update(provider())
|
|
60
65
|
except Exception:
|
|
61
|
-
#
|
|
62
|
-
pass
|
|
66
|
+
pass # Ignore providers that fail
|
|
63
67
|
return result
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
def _resolve_type(t: type) -> type:
|
|
67
|
-
"""Resolve a type
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
t: The type to resolve
|
|
71
|
-
|
|
72
|
-
Returns:
|
|
73
|
-
The resolved type, or the original type if no resolver handled it
|
|
74
|
-
"""
|
|
71
|
+
"""Resolve a type through registered resolvers, returning the unwrapped type."""
|
|
75
72
|
for resolver in _type_resolvers:
|
|
76
73
|
try:
|
|
77
74
|
resolved = resolver(t)
|
|
78
75
|
if resolved is not None:
|
|
79
76
|
return resolved
|
|
80
77
|
except Exception:
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
return t
|
|
78
|
+
pass # Ignore resolvers that fail
|
|
79
|
+
return t # No resolver handled it, return as-is
|
|
84
80
|
|
|
85
81
|
|
|
86
82
|
@dataclass(frozen=True)
|
|
@@ -440,13 +436,14 @@ class SignatureAnalyzer:
|
|
|
440
436
|
"""
|
|
441
437
|
sig = inspect.signature(callable_obj)
|
|
442
438
|
# Build comprehensive namespace for forward reference resolution
|
|
443
|
-
# Start with registered
|
|
439
|
+
# Start with registered namespace providers, then add function's globals
|
|
440
|
+
extended_ns = _get_extended_namespace()
|
|
444
441
|
globalns = {
|
|
445
|
-
**
|
|
442
|
+
**extended_ns,
|
|
446
443
|
**getattr(callable_obj, '__globals__', {})
|
|
447
444
|
}
|
|
448
445
|
|
|
449
|
-
#
|
|
446
|
+
# Prioritize the function's actual module globals for type resolution
|
|
450
447
|
if hasattr(callable_obj, '__module__') and callable_obj.__module__:
|
|
451
448
|
try:
|
|
452
449
|
import sys
|
|
@@ -454,7 +451,7 @@ class SignatureAnalyzer:
|
|
|
454
451
|
if actual_module:
|
|
455
452
|
# Function's module globals should take precedence for type resolution
|
|
456
453
|
globalns = {
|
|
457
|
-
**
|
|
454
|
+
**extended_ns,
|
|
458
455
|
**vars(actual_module) # This overwrites with the actual module types
|
|
459
456
|
}
|
|
460
457
|
except Exception:
|
|
@@ -640,10 +637,10 @@ class SignatureAnalyzer:
|
|
|
640
637
|
# Use the class object itself as the key (classes are hashable and have stable identity)
|
|
641
638
|
cache_key = dataclass_type
|
|
642
639
|
if cache_key in SignatureAnalyzer._dataclass_analysis_cache:
|
|
643
|
-
logger.
|
|
640
|
+
logger.debug(f"✅ CACHE HIT for {dataclass_type.__name__} (id={id(dataclass_type)})")
|
|
644
641
|
return SignatureAnalyzer._dataclass_analysis_cache[cache_key]
|
|
645
642
|
|
|
646
|
-
logger.
|
|
643
|
+
logger.debug(f"❌ CACHE MISS for {dataclass_type.__name__} (id={id(dataclass_type)}), cache has {len(SignatureAnalyzer._dataclass_analysis_cache)} entries")
|
|
647
644
|
|
|
648
645
|
try:
|
|
649
646
|
# Try to get type hints, fall back to __annotations__ if resolution fails
|
|
@@ -1013,7 +1010,8 @@ class SignatureAnalyzer:
|
|
|
1013
1010
|
def _resolve_lazy_dataclass_for_docs(dataclass_type: type) -> type:
|
|
1014
1011
|
"""Resolve lazy dataclasses to their base classes for documentation extraction.
|
|
1015
1012
|
|
|
1016
|
-
Uses registered type resolvers to
|
|
1013
|
+
Uses registered type resolvers to unwrap lazy/proxy types.
|
|
1014
|
+
Falls back to heuristics if no resolver handles the type.
|
|
1017
1015
|
|
|
1018
1016
|
Args:
|
|
1019
1017
|
dataclass_type: The dataclass type (potentially lazy)
|
|
@@ -1021,7 +1019,40 @@ class SignatureAnalyzer:
|
|
|
1021
1019
|
Returns:
|
|
1022
1020
|
The resolved dataclass type for documentation extraction
|
|
1023
1021
|
"""
|
|
1024
|
-
|
|
1022
|
+
try:
|
|
1023
|
+
# First, try registered type resolvers (framework-specific)
|
|
1024
|
+
resolved = _resolve_type(dataclass_type)
|
|
1025
|
+
if resolved is not dataclass_type:
|
|
1026
|
+
return resolved
|
|
1027
|
+
|
|
1028
|
+
# Fallback heuristics for common patterns (framework-agnostic)
|
|
1029
|
+
class_name = dataclass_type.__name__
|
|
1030
|
+
|
|
1031
|
+
# Handle LazyXxxConfig -> XxxConfig by looking in same module
|
|
1032
|
+
if class_name.startswith('Lazy') and class_name.endswith('Config'):
|
|
1033
|
+
try:
|
|
1034
|
+
base_class_name = class_name[4:] # Remove 'Lazy' prefix
|
|
1035
|
+
module = __import__(dataclass_type.__module__, fromlist=[base_class_name])
|
|
1036
|
+
if hasattr(module, base_class_name):
|
|
1037
|
+
return getattr(module, base_class_name)
|
|
1038
|
+
except (ImportError, AttributeError):
|
|
1039
|
+
pass
|
|
1040
|
+
|
|
1041
|
+
# Try to find GlobalXxxConfig version in same module
|
|
1042
|
+
if not class_name.startswith('Global') and class_name.endswith('Config'):
|
|
1043
|
+
try:
|
|
1044
|
+
global_class_name = f'Global{class_name}'
|
|
1045
|
+
module = __import__(dataclass_type.__module__, fromlist=[global_class_name])
|
|
1046
|
+
if hasattr(module, global_class_name):
|
|
1047
|
+
return getattr(module, global_class_name)
|
|
1048
|
+
except (ImportError, AttributeError):
|
|
1049
|
+
pass
|
|
1050
|
+
|
|
1051
|
+
# If no resolution found, return the original type
|
|
1052
|
+
return dataclass_type
|
|
1053
|
+
|
|
1054
|
+
except Exception:
|
|
1055
|
+
return dataclass_type
|
|
1025
1056
|
|
|
1026
1057
|
@staticmethod
|
|
1027
1058
|
def _extract_all_field_docs(dataclass_type: type) -> Dict[str, str]:
|
|
@@ -1120,16 +1151,12 @@ class SignatureAnalyzer:
|
|
|
1120
1151
|
parameters = SignatureAnalyzer._analyze_dataclass(dataclass_type)
|
|
1121
1152
|
|
|
1122
1153
|
# Update default values with current instance values
|
|
1123
|
-
#
|
|
1154
|
+
# CRITICAL: Always use object.__getattribute__ to bypass __getattribute__ overrides
|
|
1155
|
+
# This ensures we get the raw stored value, not a resolved/computed value
|
|
1124
1156
|
for name, param_info in parameters.items():
|
|
1125
|
-
|
|
1126
|
-
#
|
|
1127
|
-
|
|
1128
|
-
# This is a lazy dataclass - use object.__getattribute__ to get stored value
|
|
1129
|
-
current_value = object.__getattribute__(instance, name)
|
|
1130
|
-
else:
|
|
1131
|
-
# Regular dataclass - use normal getattr
|
|
1132
|
-
current_value = getattr(instance, name)
|
|
1157
|
+
try:
|
|
1158
|
+
# Bypass __getattribute__ to get raw stored value (not resolved)
|
|
1159
|
+
current_value = object.__getattribute__(instance, name)
|
|
1133
1160
|
|
|
1134
1161
|
# Create new ParameterInfo with current value as default
|
|
1135
1162
|
parameters[name] = ParameterInfo(
|
|
@@ -1139,6 +1166,9 @@ class SignatureAnalyzer:
|
|
|
1139
1166
|
is_required=param_info.is_required,
|
|
1140
1167
|
description=param_info.description
|
|
1141
1168
|
)
|
|
1169
|
+
except AttributeError:
|
|
1170
|
+
# Field doesn't exist on instance, keep signature default
|
|
1171
|
+
pass
|
|
1142
1172
|
|
|
1143
1173
|
return parameters
|
|
1144
1174
|
|
|
@@ -82,12 +82,11 @@ class UnifiedParameterAnalyzer:
|
|
|
82
82
|
if dataclasses.is_dataclass(target):
|
|
83
83
|
result = UnifiedParameterAnalyzer._analyze_dataclass_type(target)
|
|
84
84
|
else:
|
|
85
|
-
#
|
|
86
|
-
# This traverses MRO to get all inherited parameters with signature defaults
|
|
85
|
+
# For classes, use _analyze_object_instance to traverse MRO
|
|
87
86
|
# Create a dummy instance just to get the class hierarchy analyzed
|
|
88
87
|
try:
|
|
89
88
|
dummy_instance = target.__new__(target)
|
|
90
|
-
result = UnifiedParameterAnalyzer._analyze_object_instance(dummy_instance
|
|
89
|
+
result = UnifiedParameterAnalyzer._analyze_object_instance(dummy_instance)
|
|
91
90
|
except:
|
|
92
91
|
# If we can't create a dummy instance, fall back to just analyzing __init__
|
|
93
92
|
result = UnifiedParameterAnalyzer._analyze_callable(target.__init__)
|
|
@@ -147,16 +146,28 @@ class UnifiedParameterAnalyzer:
|
|
|
147
146
|
return unified_params
|
|
148
147
|
|
|
149
148
|
@staticmethod
|
|
150
|
-
def _analyze_object_instance(instance: object
|
|
149
|
+
def _analyze_object_instance(instance: object) -> Dict[str, UnifiedParameterInfo]:
|
|
151
150
|
"""Analyze a regular object instance by examining its full inheritance hierarchy.
|
|
152
151
|
|
|
152
|
+
Always returns CLASS signature defaults (not instance values).
|
|
153
|
+
ObjectState extracts instance values separately via object.__getattribute__.
|
|
154
|
+
|
|
155
|
+
For dynamic containers like SimpleNamespace (which use **kwargs in __init__),
|
|
156
|
+
falls back to inspecting __dict__ to discover attributes and their types.
|
|
157
|
+
|
|
153
158
|
Args:
|
|
154
159
|
instance: Object instance to analyze
|
|
155
|
-
use_signature_defaults: If True, use signature defaults instead of instance values
|
|
156
160
|
"""
|
|
161
|
+
from types import SimpleNamespace
|
|
162
|
+
import logging
|
|
163
|
+
_logger = logging.getLogger(__name__)
|
|
164
|
+
|
|
157
165
|
# Use MRO to get all constructor parameters from the inheritance chain
|
|
158
166
|
instance_class = type(instance)
|
|
159
167
|
all_params = {}
|
|
168
|
+
found_kwargs_only = False
|
|
169
|
+
|
|
170
|
+
_logger.debug(f"🔧 _analyze_object_instance: instance_class={instance_class.__name__}, MRO={[c.__name__ for c in instance_class.__mro__]}")
|
|
160
171
|
|
|
161
172
|
# Traverse MRO from most specific to most general (like dual-axis resolver)
|
|
162
173
|
for cls in instance_class.__mro__:
|
|
@@ -175,29 +186,27 @@ class UnifiedParameterAnalyzer:
|
|
|
175
186
|
if 'self' in class_params:
|
|
176
187
|
del class_params['self']
|
|
177
188
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
_logger.debug(f"🔧 _analyze_object_instance: cls={cls.__name__}, class_params after removing self={list(class_params.keys())}")
|
|
190
|
+
|
|
191
|
+
# Special handling for *args/**kwargs - if params are only args/kwargs, skip this class
|
|
192
|
+
# This handles dynamic containers like SimpleNamespace(self, /, *args, **kwargs)
|
|
193
|
+
variadic_only = set(class_params.keys()) <= {'args', 'kwargs'}
|
|
194
|
+
if variadic_only and class_params:
|
|
195
|
+
# This class uses only *args/**kwargs, skip it and use __dict__ fallback
|
|
196
|
+
found_kwargs_only = True
|
|
197
|
+
_logger.debug(f"🔧 _analyze_object_instance: cls={cls.__name__} has only variadic params {list(class_params.keys())}, skipping, found_kwargs_only=True")
|
|
182
198
|
continue
|
|
183
199
|
|
|
184
200
|
# Add parameters that haven't been seen yet (most specific wins)
|
|
185
201
|
for param_name, param_info in class_params.items():
|
|
186
202
|
if param_name not in all_params and param_name != 'kwargs':
|
|
187
|
-
#
|
|
188
|
-
if use_signature_defaults:
|
|
189
|
-
default_value = param_info.default_value
|
|
190
|
-
else:
|
|
191
|
-
# Get current value from instance if it exists
|
|
192
|
-
default_value = getattr(instance, param_name, param_info.default_value)
|
|
193
|
-
|
|
194
|
-
# Create parameter info with appropriate default value
|
|
203
|
+
# Always use signature defaults - ObjectState extracts instance values separately
|
|
195
204
|
all_params[param_name] = UnifiedParameterInfo(
|
|
196
205
|
name=param_name,
|
|
197
206
|
param_type=param_info.param_type,
|
|
198
|
-
default_value=default_value,
|
|
207
|
+
default_value=param_info.default_value,
|
|
199
208
|
is_required=param_info.is_required,
|
|
200
|
-
description=param_info.description,
|
|
209
|
+
description=param_info.description,
|
|
201
210
|
source_type="object_instance"
|
|
202
211
|
)
|
|
203
212
|
|
|
@@ -206,32 +215,38 @@ class UnifiedParameterAnalyzer:
|
|
|
206
215
|
# in MRO might not have analyzable constructors (e.g., ABC, object)
|
|
207
216
|
continue
|
|
208
217
|
|
|
218
|
+
# Fallback for dynamic containers (SimpleNamespace, etc.): inspect __dict__
|
|
219
|
+
# This handles objects that store attrs via **kwargs and have no static signature
|
|
220
|
+
_logger.debug(f"🔧 _analyze_object_instance: after MRO loop, all_params={list(all_params.keys())}, found_kwargs_only={found_kwargs_only}")
|
|
221
|
+
if not all_params and found_kwargs_only and hasattr(instance, '__dict__'):
|
|
222
|
+
_logger.debug(f"🔧 _analyze_object_instance: FALLBACK triggered, inspecting __dict__={list(instance.__dict__.keys())}")
|
|
223
|
+
for attr_name, attr_value in instance.__dict__.items():
|
|
224
|
+
if attr_name.startswith('_'):
|
|
225
|
+
continue
|
|
226
|
+
# Infer type from value
|
|
227
|
+
attr_type = type(attr_value) if attr_value is not None else type(None)
|
|
228
|
+
all_params[attr_name] = UnifiedParameterInfo(
|
|
229
|
+
name=attr_name,
|
|
230
|
+
param_type=attr_type,
|
|
231
|
+
default_value=attr_value,
|
|
232
|
+
is_required=False,
|
|
233
|
+
description=None,
|
|
234
|
+
source_type="dynamic_attr"
|
|
235
|
+
)
|
|
236
|
+
_logger.debug(f"🔧 _analyze_object_instance: after fallback, all_params={list(all_params.keys())}")
|
|
237
|
+
|
|
209
238
|
return all_params
|
|
210
239
|
|
|
211
240
|
@staticmethod
|
|
212
241
|
def _analyze_dataclass_instance(instance: object) -> Dict[str, UnifiedParameterInfo]:
|
|
213
|
-
"""Analyze a dataclass instance.
|
|
214
|
-
# Get the type and analyze it
|
|
215
|
-
dataclass_type = type(instance)
|
|
216
|
-
unified_params = UnifiedParameterAnalyzer._analyze_dataclass_type(dataclass_type)
|
|
217
|
-
|
|
218
|
-
# Update default values with current instance values
|
|
219
|
-
for name, param_info in unified_params.items():
|
|
220
|
-
if hasattr(instance, name):
|
|
221
|
-
# For regular dataclasses, use normal getattr
|
|
222
|
-
current_value = getattr(instance, name)
|
|
242
|
+
"""Analyze a dataclass instance.
|
|
223
243
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
description=param_info.description,
|
|
231
|
-
source_type="dataclass_instance"
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
return unified_params
|
|
244
|
+
Always returns CLASS signature defaults (not instance values).
|
|
245
|
+
ObjectState extracts instance values separately via object.__getattribute__.
|
|
246
|
+
"""
|
|
247
|
+
# Get the type and analyze it - returns CLASS signature defaults
|
|
248
|
+
dataclass_type = type(instance)
|
|
249
|
+
return UnifiedParameterAnalyzer._analyze_dataclass_type(dataclass_type)
|
|
235
250
|
|
|
236
251
|
@staticmethod
|
|
237
252
|
def analyze_nested(target: Union[Callable, Type, object], parent_info: Dict[str, UnifiedParameterInfo] = None) -> Dict[str, UnifiedParameterInfo]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-introspect
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -28,8 +28,9 @@ Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
|
28
28
|
Requires-Dist: black>=23.0; extra == "dev"
|
|
29
29
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
30
30
|
Provides-Extra: docs
|
|
31
|
-
Requires-Dist:
|
|
32
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: sphinx>=7.0.0; extra == "docs"
|
|
32
|
+
Requires-Dist: sphinx-rtd-theme>=2.0.0; extra == "docs"
|
|
33
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.24.0; extra == "docs"
|
|
33
34
|
Dynamic: license-file
|
|
34
35
|
|
|
35
36
|
# python-introspect
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.2 → python_introspect-0.1.4}/src/python_introspect.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.2 → python_introspect-0.1.4}/tests/test_unified_parameter_analyzer.py
RENAMED
|
File without changes
|