Skip to content

Commit 2176c05

Browse files
committed
feat: support dynamic containers (SimpleNamespace) in _analyze_object_instance
1 parent a256795 commit 2176c05

File tree

1 file changed

+39
-4
lines changed

1 file changed

+39
-4
lines changed

src/python_introspect/unified_parameter_analyzer.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,22 @@ def _analyze_object_instance(instance: object) -> Dict[str, UnifiedParameterInfo
152152
Always returns CLASS signature defaults (not instance values).
153153
ObjectState extracts instance values separately via object.__getattribute__.
154154
155+
For dynamic containers like SimpleNamespace (which use **kwargs in __init__),
156+
falls back to inspecting __dict__ to discover attributes and their types.
157+
155158
Args:
156159
instance: Object instance to analyze
157160
"""
161+
from types import SimpleNamespace
162+
import logging
163+
_logger = logging.getLogger(__name__)
164+
158165
# Use MRO to get all constructor parameters from the inheritance chain
159166
instance_class = type(instance)
160167
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__]}")
161171

162172
# Traverse MRO from most specific to most general (like dual-axis resolver)
163173
for cls in instance_class.__mro__:
@@ -176,10 +186,15 @@ def _analyze_object_instance(instance: object) -> Dict[str, UnifiedParameterInfo
176186
if 'self' in class_params:
177187
del class_params['self']
178188

179-
# Special handling for **kwargs - if we see 'kwargs', skip this class
180-
# and let parent classes provide the actual parameters
181-
if 'kwargs' in class_params and len(class_params) <= 2:
182-
# This class uses **kwargs, skip it and let parent classes define parameters
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")
183198
continue
184199

185200
# Add parameters that haven't been seen yet (most specific wins)
@@ -200,6 +215,26 @@ def _analyze_object_instance(instance: object) -> Dict[str, UnifiedParameterInfo
200215
# in MRO might not have analyzable constructors (e.g., ABC, object)
201216
continue
202217

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+
203238
return all_params
204239

205240
@staticmethod

0 commit comments

Comments
 (0)