forked from genewildish/Mainline
fix lint: combine with statements
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user