Skip to content

Commit 96cea15

Browse files
committed
Expose public manager workflow hooks
1 parent ced67a3 commit 96cea15

1 file changed

Lines changed: 70 additions & 22 deletions

File tree

src/pyqt_reactive/widgets/shared/abstract_manager_widget.py

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ def __init__(self, service_adapter, color_scheme=None, gui_config=None, parent=N
231231
self.gui_config = gui_config or self._get_default_gui_config()
232232
self.style_generator = StyleSheetGenerator(self.color_scheme) # Create internally
233233
self.event_bus = service_adapter.get_event_bus() if service_adapter else None
234+
self.code_execution_workflow = None
235+
self.deletion_workflow = None
234236

235237
# UI components (created in setup_ui)
236238
self.buttons: Dict[str, QPushButton] = {}
@@ -278,7 +280,7 @@ def __init__(self, service_adapter, color_scheme=None, gui_config=None, parent=N
278280
self._init_cross_window_preview_mixin()
279281

280282
# Register time travel callback for list refresh
281-
ObjectStateRegistry.add_time_travel_complete_callback(self._on_time_travel_complete)
283+
ObjectStateRegistry.add_time_travel_complete_callback(self.on_time_travel_complete)
282284

283285
def _get_default_gui_config(self):
284286
"""Get default GUI config fallback."""
@@ -463,7 +465,7 @@ def _on_registry_register(self, scope_key: str, state: 'ObjectState') -> None:
463465

464466
backing_items = self._get_backing_items()
465467
if item not in backing_items:
466-
insert_idx = self._get_item_insert_index(item, scope_key)
468+
insert_idx = self.get_item_insert_index(item, scope_key)
467469
if insert_idx is not None:
468470
backing_items.insert(insert_idx, item)
469471
else:
@@ -499,20 +501,28 @@ def _find_backing_item_by_scope(self, scope_key: str) -> Optional[Any]:
499501
return item
500502
return None
501503

502-
def _on_time_travel_complete(self, dirty_states, triggering_scope):
504+
def on_time_travel_complete(self, dirty_states, triggering_scope):
503505
"""Refresh list after time travel. Subclasses can override for custom reloads."""
504506
self.update_item_list()
505507
if hasattr(self, "update_button_states"):
506508
self.update_button_states()
507509

508-
def _get_item_insert_index(self, item: Any, scope_key: str) -> Optional[int]:
510+
def _on_time_travel_complete(self, dirty_states, triggering_scope):
511+
"""Compatibility shim for callers still bound to the old private hook."""
512+
self.on_time_travel_complete(dirty_states, triggering_scope)
513+
514+
def get_item_insert_index(self, item: Any, scope_key: str) -> Optional[int]:
509515
"""Get the index at which to insert item during time-travel re-registration.
510516
511517
Subclass can override to maintain correct ordering.
512518
Default: returns None (append to end).
513519
"""
514520
return None
515521

522+
def _get_item_insert_index(self, item: Any, scope_key: str) -> Optional[int]:
523+
"""Compatibility shim for subclasses still overriding the old private hook."""
524+
return self.get_item_insert_index(item, scope_key)
525+
516526
# ========== Action Dispatch (Concrete) ==========
517527

518528
def handle_button_action(self, action: str) -> None:
@@ -565,8 +575,8 @@ def action_delete(self) -> None:
565575
self.service_adapter.show_error_dialog(f"No {self.ITEM_NAME_PLURAL} selected")
566576
return
567577

568-
if self._validate_delete(items):
569-
self._perform_delete(items)
578+
if self.validate_delete(items):
579+
self.perform_delete(items)
570580
self.update_item_list()
571581
self._emit_items_changed()
572582
self.status_message.emit(f"Deleted {len(items)} {self.ITEM_NAME_PLURAL}")
@@ -582,7 +592,7 @@ def action_edit(self) -> None:
582592
self.service_adapter.show_error_dialog(f"No {self.ITEM_NAME_SINGULAR} selected")
583593
return
584594

585-
self._show_item_editor(items[0])
595+
self.show_item_editor(items[0])
586596

587597
def action_code(self) -> None:
588598
"""
@@ -1016,8 +1026,8 @@ def _handle_edited_code(self, code: str) -> None:
10161026
10171027
Subclasses implement hooks:
10181028
- _pre_code_execution() - Pre-processing (optional, default no-op)
1019-
- _handle_code_execution_error(code, error, namespace) - Migration fallback (optional)
1020-
- _apply_executed_code(namespace) -> bool - Extract and apply variables (REQUIRED)
1029+
- handle_code_execution_error(code, error, namespace) - Migration fallback (optional)
1030+
- apply_executed_code(namespace) -> bool - Extract and apply variables (REQUIRED)
10211031
- _post_code_execution() - Post-processing (optional, default no-op)
10221032
"""
10231033
code_type = self._get_code_type()
@@ -1038,14 +1048,14 @@ def _handle_edited_code(self, code: str) -> None:
10381048
exec(code, namespace)
10391049
except TypeError as e:
10401050
# Migration fallback hook (returns new namespace or None to re-raise)
1041-
migrated_namespace = self._handle_code_execution_error(code, e, namespace)
1051+
migrated_namespace = self.handle_code_execution_error(code, e, namespace)
10421052
if migrated_namespace is not None:
10431053
namespace = migrated_namespace
10441054
else:
10451055
raise
10461056

10471057
# Apply extracted variables to state (subclass hook)
1048-
if not self._apply_executed_code(namespace):
1058+
if not self.apply_executed_code(namespace):
10491059
raise ValueError(self._get_code_missing_error_message())
10501060

10511061
# Post-processing: broadcast, trigger refresh
@@ -1069,7 +1079,7 @@ def _pre_code_execution(self) -> None:
10691079
"""
10701080
pass # Default: no-op
10711081

1072-
def _handle_code_execution_error(self, code: str, error: Exception, namespace: dict) -> Optional[dict]:
1082+
def handle_code_execution_error(self, code: str, error: Exception, namespace: dict) -> Optional[dict]:
10731083
"""
10741084
Handle code execution error, optionally returning migrated namespace.
10751085
@@ -1078,9 +1088,16 @@ def _handle_code_execution_error(self, code: str, error: Exception, namespace: d
10781088
PipelineEditor: Handle old-format step constructors (group_by/variable_components)
10791089
PlateManager: Return None (no migration support)
10801090
"""
1091+
workflow = self.code_execution_workflow
1092+
if workflow is not None and hasattr(workflow, "migration_namespace"):
1093+
return workflow.migration_namespace(code, error)
10811094
return None # Default: re-raise error
10821095

1083-
def _apply_executed_code(self, namespace: dict) -> bool:
1096+
def _handle_code_execution_error(self, code: str, error: Exception, namespace: dict) -> Optional[dict]:
1097+
"""Compatibility shim for subclasses still overriding the old private hook."""
1098+
return self.handle_code_execution_error(code, error, namespace)
1099+
1100+
def apply_executed_code(self, namespace: dict) -> bool:
10841101
"""
10851102
Apply executed code namespace to widget state.
10861103
@@ -1090,9 +1107,16 @@ def _apply_executed_code(self, namespace: dict) -> bool:
10901107
PipelineEditor: Extract 'pipeline_steps', update self.pipeline_steps
10911108
PlateManager: Extract 'plate_paths', 'pipeline_data', etc.
10921109
"""
1093-
logger.warning(f"{type(self).__name__}._apply_executed_code not implemented")
1110+
workflow = self.code_execution_workflow
1111+
if workflow is not None and hasattr(workflow, "apply_namespace"):
1112+
return workflow.apply_namespace(namespace)
1113+
logger.warning(f"{type(self).__name__}.apply_executed_code not implemented")
10941114
return False # Default: fail (subclass must override)
10951115

1116+
def _apply_executed_code(self, namespace: dict) -> bool:
1117+
"""Compatibility shim for subclasses still overriding the old private hook."""
1118+
return self.apply_executed_code(namespace)
1119+
10961120
def _get_code_missing_error_message(self) -> str:
10971121
"""
10981122
Error message when expected code variables are missing.
@@ -1407,7 +1431,7 @@ def update_item_list(self) -> None:
14071431
return
14081432

14091433
# Pre-update hook (collect live context, normalize state)
1410-
update_context = self._pre_update_list()
1434+
update_context = self.prepare_list_update()
14111435

14121436
# Clear scope cache at start of update cycle - will be populated lazily
14131437
self._item_scope_cache.clear()
@@ -1527,22 +1551,38 @@ def _get_item_from_list_item(self, list_item: QListWidgetItem) -> Any:
15271551
# Data is the item itself
15281552
return data
15291553

1530-
def _validate_delete(self, items: List[Any]) -> bool:
1554+
def validate_delete(self, items: List[Any]) -> bool:
15311555
"""Check if delete is allowed. Default: True. Override for restrictions."""
1556+
workflow = self.deletion_workflow
1557+
if workflow is not None and hasattr(workflow, "validate"):
1558+
return workflow.validate(items)
15321559
return True
15331560

1534-
@abstractmethod
1535-
def _perform_delete(self, items: List[Any]) -> None:
1561+
def _validate_delete(self, items: List[Any]) -> bool:
1562+
"""Compatibility shim for subclasses still overriding the old private hook."""
1563+
return self.validate_delete(items)
1564+
1565+
def perform_delete(self, items: List[Any]) -> None:
15361566
"""
15371567
Remove items from internal list.
15381568
15391569
PlateManager: Remove from self.plates, cleanup orchestrators
15401570
PipelineEditor: Remove from self.pipeline_steps, update orchestrator
15411571
"""
1542-
...
1572+
workflow = self.deletion_workflow
1573+
if workflow is not None and hasattr(workflow, "delete"):
1574+
workflow.delete(items)
1575+
return
1576+
raise NotImplementedError(
1577+
f"{type(self).__name__}.perform_delete requires a deletion workflow or override"
1578+
)
1579+
1580+
def _perform_delete(self, items: List[Any]) -> None:
1581+
"""Compatibility shim for callers still bound to the old private hook."""
1582+
self.perform_delete(items)
15431583

15441584
@abstractmethod
1545-
def _show_item_editor(self, item: Any) -> None:
1585+
def show_item_editor(self, item: Any) -> None:
15461586
"""
15471587
Show editor for item.
15481588
@@ -1551,6 +1591,10 @@ def _show_item_editor(self, item: Any) -> None:
15511591
"""
15521592
...
15531593

1594+
def _show_item_editor(self, item: Any) -> None:
1595+
"""Compatibility shim for callers still bound to the old private hook."""
1596+
self.show_item_editor(item)
1597+
15541598
# === UI Hooks (declarative via ITEM_HOOKS) ===
15551599

15561600
def _get_item_id(self, item: Any) -> str:
@@ -1680,7 +1724,7 @@ def _get_list_placeholder(self) -> Optional[Tuple[str, Any]]:
16801724
"""
16811725
return None # Default: no placeholder
16821726

1683-
def _pre_update_list(self) -> Any:
1727+
def prepare_list_update(self) -> Any:
16841728
"""
16851729
Pre-update hook: normalize state, collect context.
16861730
@@ -1691,6 +1735,10 @@ def _pre_update_list(self) -> Any:
16911735
"""
16921736
return None # Default: no context
16931737

1738+
def _pre_update_list(self) -> Any:
1739+
"""Compatibility shim for subclasses still overriding the old private hook."""
1740+
return self.prepare_list_update()
1741+
16941742
def _post_update_list(self) -> None:
16951743
"""
16961744
Post-update hook: auto-select first if needed.
@@ -2313,5 +2361,5 @@ def _handle_full_preview_refresh(self) -> None:
23132361

23142362
def closeEvent(self, event):
23152363
"""Ensure time travel callbacks are unregistered."""
2316-
ObjectStateRegistry.remove_time_travel_complete_callback(self._on_time_travel_complete)
2364+
ObjectStateRegistry.remove_time_travel_complete_callback(self.on_time_travel_complete)
23172365
super().closeEvent(event)

0 commit comments

Comments
 (0)