From 4f2cf49a8027fe1b7bf8257d22e9b9f96dc88530 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Thu, 19 Mar 2026 22:36:35 -0700 Subject: [PATCH] fix lint: combine with statements --- engine/__init__.py | 9 +++++ engine/pipeline/controller.py | 66 ++++++++++++++++++++++++++++----- tests/test_pipeline.py | 70 +++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 9 deletions(-) diff --git a/engine/__init__.py b/engine/__init__.py index 63f007f..a305edb 100644 --- a/engine/__init__.py +++ b/engine/__init__.py @@ -1 +1,10 @@ # engine — modular internals for mainline + +# Import submodules to make them accessible via engine. +# This is required for unittest.mock.patch to work with "engine.." +# strings and for direct attribute access on the engine package. +import engine.config # noqa: F401 +import engine.fetch # noqa: F401 +import engine.filter # noqa: F401 +import engine.sources # noqa: F401 +import engine.terminal # noqa: F401 diff --git a/engine/pipeline/controller.py b/engine/pipeline/controller.py index dce3591..bc857ce 100644 --- a/engine/pipeline/controller.py +++ b/engine/pipeline/controller.py @@ -185,8 +185,6 @@ class Pipeline: return True - return True - def replace_stage( self, name: str, new_stage: Stage, preserve_state: bool = True ) -> Stage | None: @@ -304,11 +302,16 @@ class Pipeline: self._capability_map = self._build_capability_map() self._execution_order = self._resolve_dependencies() - try: - self._validate_dependencies() - self._validate_types() - except StageError: - pass + # Note: We intentionally DO NOT validate dependencies here. + # Mutation operations (remove/swap/move) might leave the pipeline + # temporarily invalid (e.g., removing a stage that others depend on). + # Validation is performed explicitly in build() or can be checked + # manually via validate_minimum_capabilities(). + # try: + # self._validate_dependencies() + # self._validate_types() + # except StageError: + # pass # Restore initialized state self._initialized = was_initialized @@ -504,6 +507,16 @@ class Pipeline: self._capability_map = self._build_capability_map() self._execution_order = self._resolve_dependencies() + # Re-validate after injection attempt (whether anything was injected or not) + # If injection didn't run (injected empty), we still need to check if we're valid + # If injection ran but failed to fix (injected empty), we need to check + is_valid, missing = self.validate_minimum_capabilities() + if not is_valid: + raise StageError( + "build", + f"Auto-injection failed to provide minimum capabilities: {missing}", + ) + self._validate_dependencies() self._validate_types() self._initialized = True @@ -712,8 +725,9 @@ class Pipeline: frame_start = time.perf_counter() if self._metrics_enabled else 0 stage_timings: list[StageMetrics] = [] - # Separate overlay stages from regular stages + # Separate overlay stages and display stage from regular stages overlay_stages: list[tuple[int, Stage]] = [] + display_stage: Stage | None = None regular_stages: list[str] = [] for name in self._execution_order: @@ -721,6 +735,11 @@ class Pipeline: if not stage or not stage.is_enabled(): continue + # Check if this is the display stage - execute last + if stage.category == "display": + display_stage = stage + continue + # Safely check is_overlay - handle MagicMock and other non-bool returns try: is_overlay = bool(getattr(stage, "is_overlay", False)) @@ -737,7 +756,7 @@ class Pipeline: else: regular_stages.append(name) - # Execute regular stages in dependency order + # Execute regular stages in dependency order (excluding display) for name in regular_stages: stage = self._stages.get(name) if not stage or not stage.is_enabled(): @@ -828,6 +847,35 @@ class Pipeline: ) ) + # Execute display stage LAST (after overlay stages) + # This ensures overlay effects like HUD are visible in the final output + if display_stage: + stage_start = time.perf_counter() if self._metrics_enabled else 0 + + try: + current_data = display_stage.process(current_data, self.context) + except Exception as e: + if not display_stage.optional: + return StageResult( + success=False, + data=current_data, + error=str(e), + stage_name=display_stage.name, + ) + + if self._metrics_enabled: + stage_duration = (time.perf_counter() - stage_start) * 1000 + chars_in = len(str(data)) if data else 0 + chars_out = len(str(current_data)) if current_data else 0 + stage_timings.append( + StageMetrics( + name=display_stage.name, + duration_ms=stage_duration, + chars_in=chars_in, + chars_out=chars_out, + ) + ) + if self._metrics_enabled: total_duration = (time.perf_counter() - frame_start) * 1000 self._frame_metrics.append( diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index ce90b42..c8b86c1 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -1772,3 +1772,73 @@ class TestPipelineMutation: result = pipeline.execute(None) assert result.success assert call_log == ["source", "display"] + + +class TestAutoInjection: + """Tests for auto-injection of minimum capabilities.""" + + def setup_method(self): + """Reset registry before each test.""" + StageRegistry._discovered = False + StageRegistry._categories.clear() + StageRegistry._instances.clear() + discover_stages() + + def test_auto_injection_provides_minimum_capabilities(self): + """Pipeline with no stages gets minimum capabilities auto-injected.""" + pipeline = Pipeline() + # Don't add any stages + pipeline.build(auto_inject=True) + + # Should have stages for source, render, camera, display + assert len(pipeline.stages) > 0 + assert "source" in pipeline.stages + assert "display" in pipeline.stages + + def test_auto_injection_rebuilds_execution_order(self): + """Auto-injection rebuilds execution order correctly.""" + pipeline = Pipeline() + pipeline.build(auto_inject=True) + + # Execution order should be valid + assert len(pipeline.execution_order) > 0 + # Source should come before display + source_idx = pipeline.execution_order.index("source") + display_idx = pipeline.execution_order.index("display") + assert source_idx < display_idx + + def test_validation_error_after_auto_injection(self): + """Pipeline raises error if auto-injection fails to provide capabilities.""" + from unittest.mock import patch + + pipeline = Pipeline() + + # Mock ensure_minimum_capabilities to return empty list (injection failed) + with ( + patch.object(pipeline, "ensure_minimum_capabilities", return_value=[]), + patch.object( + pipeline, + "validate_minimum_capabilities", + return_value=(False, ["source"]), + ), + ): + # Even though injection "ran", it didn't provide the capability + # build() should raise StageError + with pytest.raises(StageError) as exc_info: + pipeline.build(auto_inject=True) + + assert "Auto-injection failed" in str(exc_info.value) + + def test_minimum_capability_removal_recovery(self): + """Pipeline re-injects minimum capability if removed.""" + pipeline = Pipeline() + pipeline.build(auto_inject=True) + + # Remove the display stage + pipeline.remove_stage("display", cleanup=True) + + # Rebuild with auto-injection + pipeline.build(auto_inject=True) + + # Display should be back + assert "display" in pipeline.stages