fix lint: combine with statements

This commit is contained in:
2026-03-19 22:36:35 -07:00
parent ff08b1d6f5
commit 4f2cf49a80
3 changed files with 136 additions and 9 deletions

View File

@@ -1 +1,10 @@
# engine — modular internals for mainline # engine — modular internals for mainline
# Import submodules to make them accessible via engine.<name>
# This is required for unittest.mock.patch to work with "engine.<module>.<function>"
# 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

View File

@@ -185,8 +185,6 @@ class Pipeline:
return True return True
return True
def replace_stage( def replace_stage(
self, name: str, new_stage: Stage, preserve_state: bool = True self, name: str, new_stage: Stage, preserve_state: bool = True
) -> Stage | None: ) -> Stage | None:
@@ -304,11 +302,16 @@ class Pipeline:
self._capability_map = self._build_capability_map() self._capability_map = self._build_capability_map()
self._execution_order = self._resolve_dependencies() self._execution_order = self._resolve_dependencies()
try: # Note: We intentionally DO NOT validate dependencies here.
self._validate_dependencies() # Mutation operations (remove/swap/move) might leave the pipeline
self._validate_types() # temporarily invalid (e.g., removing a stage that others depend on).
except StageError: # Validation is performed explicitly in build() or can be checked
pass # manually via validate_minimum_capabilities().
# try:
# self._validate_dependencies()
# self._validate_types()
# except StageError:
# pass
# Restore initialized state # Restore initialized state
self._initialized = was_initialized self._initialized = was_initialized
@@ -504,6 +507,16 @@ class Pipeline:
self._capability_map = self._build_capability_map() self._capability_map = self._build_capability_map()
self._execution_order = self._resolve_dependencies() 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_dependencies()
self._validate_types() self._validate_types()
self._initialized = True self._initialized = True
@@ -712,8 +725,9 @@ class Pipeline:
frame_start = time.perf_counter() if self._metrics_enabled else 0 frame_start = time.perf_counter() if self._metrics_enabled else 0
stage_timings: list[StageMetrics] = [] 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]] = [] overlay_stages: list[tuple[int, Stage]] = []
display_stage: Stage | None = None
regular_stages: list[str] = [] regular_stages: list[str] = []
for name in self._execution_order: for name in self._execution_order:
@@ -721,6 +735,11 @@ class Pipeline:
if not stage or not stage.is_enabled(): if not stage or not stage.is_enabled():
continue 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 # Safely check is_overlay - handle MagicMock and other non-bool returns
try: try:
is_overlay = bool(getattr(stage, "is_overlay", False)) is_overlay = bool(getattr(stage, "is_overlay", False))
@@ -737,7 +756,7 @@ class Pipeline:
else: else:
regular_stages.append(name) regular_stages.append(name)
# Execute regular stages in dependency order # Execute regular stages in dependency order (excluding display)
for name in regular_stages: for name in regular_stages:
stage = self._stages.get(name) stage = self._stages.get(name)
if not stage or not stage.is_enabled(): 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: if self._metrics_enabled:
total_duration = (time.perf_counter() - frame_start) * 1000 total_duration = (time.perf_counter() - frame_start) * 1000
self._frame_metrics.append( self._frame_metrics.append(

View File

@@ -1772,3 +1772,73 @@ class TestPipelineMutation:
result = pipeline.execute(None) result = pipeline.execute(None)
assert result.success assert result.success
assert call_log == ["source", "display"] 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