Skip to content

Commit 92cc72b

Browse files
committed
fix: Use extraction_target for delegation in saved_object and to_object
- Add saved_object property that returns _extraction_target (handles delegation) - Fix to_object() to return reconstructed delegate when delegation is used - Fix mark_saved() to update delegate, not lifecycle object - Fix registry methods to use saved_object for use_saved=True - Fix _compute_resolved_values() to use saved_object for saved baseline This fixes PipelineConfig editor showing empty form when opened via orchestrator delegation (__objectstate_delegate__).
1 parent 4854e11 commit 92cc72b

1 file changed

Lines changed: 46 additions & 16 deletions

File tree

src/objectstate/object_state.py

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -383,8 +383,8 @@ def get_ancestor_objects(cls, scope_id: Optional[str], use_saved: bool = False)
383383
state = cls._states.get(ancestor_key)
384384
if state:
385385
if use_saved:
386-
# Return saved baseline (object_instance is updated in mark_saved)
387-
objects.append(state.object_instance)
386+
# Return saved baseline (saved_object handles delegation correctly)
387+
objects.append(state.saved_object)
388388
else:
389389
# Return live state with current edits
390390
objects.append(state.to_object())
@@ -420,7 +420,7 @@ def get_ancestor_objects_with_scopes(cls, scope_id: Optional[str], use_saved: bo
420420
for ancestor_key in ancestors:
421421
state = cls._states.get(ancestor_key)
422422
if state:
423-
obj = state.object_instance if use_saved else state.to_object()
423+
obj = state.saved_object if use_saved else state.to_object()
424424
results.append((ancestor_key, obj))
425425

426426
return results
@@ -1563,6 +1563,18 @@ def context_obj(self) -> Optional[Any]:
15631563
"""Derive context_obj from parent_state (no separate attribute needed)."""
15641564
return self._parent_state.object_instance if self._parent_state else None
15651565

1566+
@property
1567+
def saved_object(self) -> Any:
1568+
"""Get the saved baseline object with the correct type.
1569+
1570+
For delegation: returns _extraction_target (the delegate/config)
1571+
For non-delegation: returns object_instance
1572+
1573+
This is the object that should be used for context resolution when
1574+
use_saved=True. It represents the "saved" state of the editable object.
1575+
"""
1576+
return self._extraction_target
1577+
15661578
@property
15671579
def fields(self) -> FieldProxy:
15681580
"""Type-safe field access via FieldProxy.
@@ -2282,7 +2294,8 @@ def _compute_resolved_snapshot(self, use_saved: bool = False) -> Dict[str, Any]:
22822294

22832295
# Use saved baseline or live state for this object
22842296
if use_saved:
2285-
current_obj = self.object_instance
2297+
# Use saved_object which handles delegation correctly
2298+
current_obj = self.saved_object
22862299
else:
22872300
# CRITICAL: Use to_object() to get CURRENT state with user edits,
22882301
# not object_instance which is the original/saved baseline.
@@ -2392,16 +2405,17 @@ def mark_saved(self) -> None:
23922405
if is_container:
23932406
continue
23942407

2395-
# Get the old value from object_instance by navigating dotted path
2408+
# Get the old value by navigating dotted path on the extraction target
2409+
# For delegation, parameters are on the delegate, not object_instance
23962410
try:
23972411
# Navigate through nested attributes for dotted paths
2398-
obj = self.object_instance
2412+
obj = self._extraction_target
23992413
parts = param_name.split('.')
24002414
for part in parts:
24012415
obj = object.__getattribute__(obj, part)
24022416
old_instance_values[param_name] = obj
24032417
except AttributeError:
2404-
# Field doesn't exist on object_instance, skip it
2418+
# Field doesn't exist on extraction target, skip it
24052419
pass
24062420

24072421
# Find parameters that differ between old object_instance and new live parameters
@@ -2419,12 +2433,18 @@ def mark_saved(self) -> None:
24192433
if old_value != new_value:
24202434
changed_params.append(param_name)
24212435

2422-
# CRITICAL: Rebuild object_instance BEFORE invalidating descendants
2423-
# Descendants will recompute using parent's object_instance, so it must have new values!
2436+
# CRITICAL: Rebuild extraction target BEFORE invalidating descendants
2437+
# Descendants will recompute using parent's extraction target, so it must have new values!
24242438
if not isinstance(self.object_instance, type):
2425-
# Update object_instance with current parameters
2426-
# to_object() already handles all types uniformly
2427-
self.object_instance = self.to_object()
2439+
if self._delegate_attr is not None:
2440+
# DELEGATION: to_object() returns the delegate and updates it on object_instance
2441+
# as a side effect. Keep object_instance unchanged (it's the lifecycle object).
2442+
# _extraction_target is updated to point to the new delegate.
2443+
self._extraction_target = self.to_object()
2444+
else:
2445+
# NON-DELEGATION: to_object() returns the reconstructed object_instance
2446+
self.object_instance = self.to_object()
2447+
self._extraction_target = self.object_instance # Keep in sync
24282448

24292449
# Update saved parameters (after object_instance update, before invalidation)
24302450
self._saved_parameters = copy.deepcopy(self.parameters)
@@ -2624,8 +2644,15 @@ def to_object(self) -> Any:
26242644
- Python functions: can't copy, return original
26252645
- Everything else: shallow copy + reconstruct nested dataclass fields
26262646
2627-
DELEGATION: If __objectstate_delegate__ was used, reconstructs the delegate
2628-
and updates it on the original object_instance, returning object_instance.
2647+
DELEGATION: If __objectstate_delegate__ was used:
2648+
- Reconstructs the delegate (e.g., pipeline_config)
2649+
- Updates the delegate attribute on object_instance as a side effect
2650+
- Returns the reconstructed delegate (NOT object_instance)
2651+
- Callers needing the lifecycle object (orchestrator) should use state.object_instance
2652+
2653+
Returns:
2654+
The reconstructed object that matches the stored parameters.
2655+
For delegation, this is the delegate type (config), not the lifecycle object.
26292656
"""
26302657
if self._cached_object is not None:
26312658
return self._cached_object
@@ -2694,10 +2721,13 @@ def to_object(self) -> Any:
26942721
reconstructed = obj_copy
26952722

26962723
# DELEGATION: If using delegation, update the delegate attribute on object_instance
2697-
# and return the object_instance (which now has the updated delegate)
2724+
# as a side effect, but return the reconstructed delegate (not object_instance).
2725+
# This ensures callers get the correct type (config, not orchestrator).
2726+
# Callers who need the lifecycle object should access state.object_instance directly.
26982727
if self._delegate_attr is not None:
26992728
setattr(self.object_instance, self._delegate_attr, reconstructed)
2700-
self._cached_object = self.object_instance
2729+
# Return the reconstructed delegate - this is what the parameters represent
2730+
self._cached_object = reconstructed
27012731
else:
27022732
self._cached_object = reconstructed
27032733

0 commit comments

Comments
 (0)