diff --git a/engine/data_sources/pipeline_introspection.py b/engine/data_sources/pipeline_introspection.py index b7c372d..31c67fd 100644 --- a/engine/data_sources/pipeline_introspection.py +++ b/engine/data_sources/pipeline_introspection.py @@ -67,13 +67,13 @@ class PipelineIntrospectionSource(DataSource): @property def inlet_types(self) -> set: - from engine.pipeline.core import DataType + from engine.pipeline import DataType return {DataType.NONE} @property def outlet_types(self) -> set: - from engine.pipeline.core import DataType + from engine.pipeline import DataType return {DataType.SOURCE_ITEMS} diff --git a/engine/effects/plugins/tint.py b/engine/effects/plugins/tint.py index ce8c941..b983f77 100644 --- a/engine/effects/plugins/tint.py +++ b/engine/effects/plugins/tint.py @@ -20,13 +20,13 @@ class TintEffect(EffectPlugin): # Define inlet types for PureData-style typing @property def inlet_types(self) -> set: - from engine.pipeline.core import DataType + from engine.pipeline import DataType return {DataType.TEXT_BUFFER} @property def outlet_types(self) -> set: - from engine.pipeline.core import DataType + from engine.pipeline import DataType return {DataType.TEXT_BUFFER} diff --git a/engine/interfaces/__init__.py b/engine/interfaces/__init__.py index 2d8879f..8d5d899 100644 --- a/engine/interfaces/__init__.py +++ b/engine/interfaces/__init__.py @@ -28,7 +28,7 @@ from engine.effects.types import ( apply_param_bindings, create_effect_context, ) -from engine.pipeline.core import ( +from engine.pipeline import ( DataType, Stage, StageConfig, diff --git a/engine/pipeline/__init__.py b/engine/pipeline/__init__.py index ff740d0..22b4a81 100644 --- a/engine/pipeline/__init__.py +++ b/engine/pipeline/__init__.py @@ -26,6 +26,11 @@ from sideline.pipeline import ( register_source, ) +# Also re-export from sideline.core for compatibility +from sideline.pipeline.core import ( + DataType, +) + # Re-export from engine.pipeline.presets (Mainline-specific) from engine.pipeline.presets import ( DEMO_PRESET, @@ -91,4 +96,6 @@ __all__ = [ "register_effect", "register_display", "register_camera", + # Core types (from sideline) + "DataType", ] diff --git a/engine/pipeline/adapters/camera.py b/engine/pipeline/adapters/camera.py index d0fca41..17a7f3d 100644 --- a/engine/pipeline/adapters/camera.py +++ b/engine/pipeline/adapters/camera.py @@ -3,7 +3,7 @@ import time from typing import Any -from engine.pipeline.core import DataType, PipelineContext, Stage +from engine.pipeline import DataType, PipelineContext, Stage class CameraClockStage(Stage): diff --git a/engine/pipeline/adapters/data_source.py b/engine/pipeline/adapters/data_source.py index 04a59af..ecdfc9e 100644 --- a/engine/pipeline/adapters/data_source.py +++ b/engine/pipeline/adapters/data_source.py @@ -8,7 +8,7 @@ This module provides adapters that wrap existing components from typing import Any from engine.data_sources import SourceItem -from engine.pipeline.core import DataType, PipelineContext, Stage +from engine.pipeline import DataType, PipelineContext, Stage class DataSourceStage(Stage): diff --git a/engine/pipeline/adapters/display.py b/engine/pipeline/adapters/display.py index cdef260..f04b3e0 100644 --- a/engine/pipeline/adapters/display.py +++ b/engine/pipeline/adapters/display.py @@ -2,7 +2,7 @@ from typing import Any -from engine.pipeline.core import PipelineContext, Stage +from engine.pipeline import PipelineContext, Stage class DisplayStage(Stage): @@ -59,13 +59,13 @@ class DisplayStage(Stage): @property def inlet_types(self) -> set: - from engine.pipeline.core import DataType + from engine.pipeline import DataType return {DataType.TEXT_BUFFER} # Display consumes rendered text @property def outlet_types(self) -> set: - from engine.pipeline.core import DataType + from engine.pipeline import DataType return {DataType.NONE} # Display is a terminal stage (no output) diff --git a/engine/pipeline/adapters/effect_plugin.py b/engine/pipeline/adapters/effect_plugin.py index 1185021..6b038e7 100644 --- a/engine/pipeline/adapters/effect_plugin.py +++ b/engine/pipeline/adapters/effect_plugin.py @@ -2,7 +2,7 @@ from typing import Any -from engine.pipeline.core import PipelineContext, Stage +from engine.pipeline import PipelineContext, Stage class EffectPluginStage(Stage): @@ -69,13 +69,13 @@ class EffectPluginStage(Stage): @property def inlet_types(self) -> set: - from engine.pipeline.core import DataType + from engine.pipeline import DataType return {DataType.TEXT_BUFFER} @property def outlet_types(self) -> set: - from engine.pipeline.core import DataType + from engine.pipeline import DataType return {DataType.TEXT_BUFFER} diff --git a/engine/pipeline/adapters/frame_capture.py b/engine/pipeline/adapters/frame_capture.py index 03d909a..5fda41b 100644 --- a/engine/pipeline/adapters/frame_capture.py +++ b/engine/pipeline/adapters/frame_capture.py @@ -7,7 +7,7 @@ Wraps pipeline stages to capture frames for animation report generation. from typing import Any from engine.display.backends.animation_report import AnimationReportDisplay -from engine.pipeline.core import PipelineContext, Stage +from engine.pipeline import PipelineContext, Stage class FrameCaptureStage(Stage): diff --git a/engine/pipeline/adapters/message_overlay.py b/engine/pipeline/adapters/message_overlay.py index 2433a38..140e533 100644 --- a/engine/pipeline/adapters/message_overlay.py +++ b/engine/pipeline/adapters/message_overlay.py @@ -12,7 +12,7 @@ from datetime import datetime from engine import config from engine.effects.legacy import vis_trunc -from engine.pipeline.core import DataType, PipelineContext, Stage +from engine.pipeline import DataType, PipelineContext, Stage from engine.render.blocks import big_wrap from engine.render.gradient import msg_gradient diff --git a/engine/pipeline/adapters/positioning.py b/engine/pipeline/adapters/positioning.py index 40d48ec..35c410d 100644 --- a/engine/pipeline/adapters/positioning.py +++ b/engine/pipeline/adapters/positioning.py @@ -10,7 +10,7 @@ different ANSI positioning approaches: from enum import Enum from typing import Any -from engine.pipeline.core import DataType, PipelineContext, Stage +from engine.pipeline import DataType, PipelineContext, Stage class PositioningMode(Enum): diff --git a/engine/pipeline/adapters/transform.py b/engine/pipeline/adapters/transform.py index e1b6c08..7bab5e8 100644 --- a/engine/pipeline/adapters/transform.py +++ b/engine/pipeline/adapters/transform.py @@ -4,7 +4,7 @@ from typing import Any import engine.render from engine.data_sources import SourceItem -from engine.pipeline.core import DataType, PipelineContext, Stage +from engine.pipeline import DataType, PipelineContext, Stage def estimate_simple_height(text: str, width: int) -> int: diff --git a/engine/pipeline/controller.py b/engine/pipeline/controller.py index 62eeb49..c3b087d 100644 --- a/engine/pipeline/controller.py +++ b/engine/pipeline/controller.py @@ -9,7 +9,7 @@ import time from dataclasses import dataclass, field from typing import Any -from engine.pipeline.core import PipelineContext, Stage, StageError, StageResult +from engine.pipeline import PipelineContext, Stage, StageError, StageResult from engine.pipeline.params import PipelineParams from engine.pipeline.registry import StageRegistry @@ -640,7 +640,7 @@ class Pipeline: Raises StageError if type mismatch is detected. """ - from engine.pipeline.core import DataType + from engine.pipeline import DataType errors: list[str] = [] diff --git a/engine/pipeline/core.py b/engine/pipeline/core.py deleted file mode 100644 index 55ebf8c..0000000 --- a/engine/pipeline/core.py +++ /dev/null @@ -1,321 +0,0 @@ -""" -Pipeline core - Unified Stage abstraction and PipelineContext. - -This module provides the foundation for a clean, dependency-managed pipeline: -- Stage: Base class for all pipeline components (sources, effects, displays, cameras) -- PipelineContext: Dependency injection context for runtime data exchange -- Capability system: Explicit capability declarations with duck-typing support -- DataType: PureData-style inlet/outlet typing for validation -""" - -from abc import ABC, abstractmethod -from collections.abc import Callable -from dataclasses import dataclass, field -from enum import Enum, auto -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from engine.pipeline.params import PipelineParams - - -class DataType(Enum): - """PureData-style data types for inlet/outlet validation. - - Each type represents a specific data format that flows through the pipeline. - This enables compile-time-like validation of connections. - - Examples: - SOURCE_ITEMS: List[SourceItem] - raw items from sources - ITEM_TUPLES: List[tuple] - (title, source, timestamp) tuples - TEXT_BUFFER: List[str] - rendered ANSI buffer for display - RAW_TEXT: str - raw text strings - PIL_IMAGE: PIL Image object - """ - - SOURCE_ITEMS = auto() # List[SourceItem] - from DataSource - ITEM_TUPLES = auto() # List[tuple] - (title, source, ts) - TEXT_BUFFER = auto() # List[str] - ANSI buffer - RAW_TEXT = auto() # str - raw text - PIL_IMAGE = auto() # PIL Image object - ANY = auto() # Accepts any type - NONE = auto() # No data (terminator) - - -@dataclass -class StageConfig: - """Configuration for a single stage.""" - - name: str - category: str - enabled: bool = True - optional: bool = False - params: dict[str, Any] = field(default_factory=dict) - - -class Stage(ABC): - """Abstract base class for all pipeline stages. - - A Stage is a single component in the rendering pipeline. Stages can be: - - Sources: Data providers (headlines, poetry, pipeline viz) - - Effects: Post-processors (noise, fade, glitch, hud) - - Displays: Output backends (terminal, pygame, websocket) - - Cameras: Viewport controllers (vertical, horizontal, omni) - - Overlays: UI elements that compose on top (HUD) - - Stages declare: - - capabilities: What they provide to other stages - - dependencies: What they need from other stages - - stage_type: Category of stage (source, effect, overlay, display) - - render_order: Execution order within category - - is_overlay: If True, output is composited on top, not passed downstream - - Duck-typing is supported: any class with the required methods can act as a Stage. - """ - - name: str - category: str # "source", "effect", "overlay", "display", "camera" - optional: bool = False # If True, pipeline continues even if stage fails - - @property - def stage_type(self) -> str: - """Category of stage for ordering. - - Valid values: "source", "effect", "overlay", "display", "camera" - Defaults to category for backwards compatibility. - """ - return self.category - - @property - def render_order(self) -> int: - """Execution order within stage_type group. - - Higher values execute later. Useful for ordering overlays - or effects that need specific execution order. - """ - return 0 - - @property - def is_overlay(self) -> bool: - """If True, this stage's output is composited on top of the buffer. - - Overlay stages don't pass their output to the next stage. - Instead, their output is layered on top of the final buffer. - Use this for HUD, status displays, and similar UI elements. - """ - return False - - @property - def inlet_types(self) -> set[DataType]: - """Return set of data types this stage accepts. - - PureData-style inlet typing. If the connected upstream stage's - outlet_type is not in this set, the pipeline will raise an error. - - Examples: - - Source stages: {DataType.NONE} (no input needed) - - Transform stages: {DataType.ITEM_TUPLES, DataType.TEXT_BUFFER} - - Display stages: {DataType.TEXT_BUFFER} - """ - return {DataType.ANY} - - @property - def outlet_types(self) -> set[DataType]: - """Return set of data types this stage produces. - - PureData-style outlet typing. Downstream stages must accept - this type in their inlet_types. - - Examples: - - Source stages: {DataType.SOURCE_ITEMS} - - Transform stages: {DataType.TEXT_BUFFER} - - Display stages: {DataType.NONE} (consumes data) - """ - return {DataType.ANY} - - @property - def capabilities(self) -> set[str]: - """Return set of capabilities this stage provides. - - Examples: - - "source.headlines" - - "effect.noise" - - "display.output" - - "camera" - """ - return {f"{self.category}.{self.name}"} - - @property - def dependencies(self) -> set[str]: - """Return set of capability names this stage needs. - - Examples: - - {"display.output"} - - {"source.headlines"} - - {"camera"} - """ - return set() - - @property - def stage_dependencies(self) -> set[str]: - """Return set of stage names this stage must connect to directly. - - This allows explicit stage-to-stage dependencies, useful for enforcing - pipeline structure when capability matching alone is insufficient. - - Examples: - - {"viewport_filter"} # Must connect to viewport_filter stage - - {"camera_update"} # Must connect to camera_update stage - - NOTE: These are stage names (as added to pipeline), not capabilities. - """ - return set() - - def init(self, ctx: "PipelineContext") -> bool: - """Initialize stage with pipeline context. - - Args: - ctx: PipelineContext for accessing services - - Returns: - True if initialization succeeded, False otherwise - """ - return True - - @abstractmethod - def process(self, data: Any, ctx: "PipelineContext") -> Any: - """Process input data and return output. - - Args: - data: Input data from previous stage (or initial data for first stage) - ctx: PipelineContext for accessing services and state - - Returns: - Processed data for next stage - """ - ... - - def cleanup(self) -> None: # noqa: B027 - """Clean up resources when pipeline shuts down.""" - pass - - def get_config(self) -> StageConfig: - """Return current configuration of this stage.""" - return StageConfig( - name=self.name, - category=self.category, - optional=self.optional, - ) - - def set_enabled(self, enabled: bool) -> None: - """Enable or disable this stage.""" - self._enabled = enabled # type: ignore[attr-defined] - - def is_enabled(self) -> bool: - """Check if stage is enabled.""" - return getattr(self, "_enabled", True) - - -@dataclass -class StageResult: - """Result of stage processing, including success/failure info.""" - - success: bool - data: Any - error: str | None = None - stage_name: str = "" - - -class PipelineContext: - """Dependency injection context passed through the pipeline. - - Provides: - - services: Named services (display, config, event_bus, etc.) - - state: Runtime state shared between stages - - params: PipelineParams for animation-driven config - - Services can be injected at construction time or lazily resolved. - """ - - def __init__( - self, - services: dict[str, Any] | None = None, - initial_state: dict[str, Any] | None = None, - ): - self.services: dict[str, Any] = services or {} - self.state: dict[str, Any] = initial_state or {} - self._params: PipelineParams | None = None - - # Lazy resolvers for common services - self._lazy_resolvers: dict[str, Callable[[], Any]] = { - "config": self._resolve_config, - "event_bus": self._resolve_event_bus, - } - - def _resolve_config(self) -> Any: - from engine.config import get_config - - return get_config() - - def _resolve_event_bus(self) -> Any: - from engine.eventbus import get_event_bus - - return get_event_bus() - - def get(self, key: str, default: Any = None) -> Any: - """Get a service or state value by key. - - First checks services, then state, then lazy resolution. - """ - if key in self.services: - return self.services[key] - if key in self.state: - return self.state[key] - if key in self._lazy_resolvers: - try: - return self._lazy_resolvers[key]() - except Exception: - return default - return default - - def set(self, key: str, value: Any) -> None: - """Set a service or state value.""" - self.services[key] = value - - def set_state(self, key: str, value: Any) -> None: - """Set a runtime state value.""" - self.state[key] = value - - def get_state(self, key: str, default: Any = None) -> Any: - """Get a runtime state value.""" - return self.state.get(key, default) - - @property - def params(self) -> "PipelineParams | None": - """Get current pipeline params (for animation).""" - return self._params - - @params.setter - def params(self, value: "PipelineParams") -> None: - """Set pipeline params (from animation controller).""" - self._params = value - - def has_capability(self, capability: str) -> bool: - """Check if a capability is available.""" - return capability in self.services or capability in self._lazy_resolvers - - -class StageError(Exception): - """Raised when a stage fails to process.""" - - def __init__(self, stage_name: str, message: str, is_optional: bool = False): - self.stage_name = stage_name - self.message = message - self.is_optional = is_optional - super().__init__(f"Stage '{stage_name}' failed: {message}") - - -def create_stage_error( - stage_name: str, error: Exception, is_optional: bool = False -) -> StageError: - """Helper to create a StageError from an exception.""" - return StageError(stage_name, str(error), is_optional) diff --git a/engine/pipeline/registry.py b/engine/pipeline/registry.py index 6e9bcac..8d04d5c 100644 --- a/engine/pipeline/registry.py +++ b/engine/pipeline/registry.py @@ -8,10 +8,10 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, TypeVar -from engine.pipeline.core import Stage +from engine.pipeline import Stage if TYPE_CHECKING: - from engine.pipeline.core import Stage + from engine.pipeline import Stage T = TypeVar("T") diff --git a/engine/pipeline/stages/framebuffer.py b/engine/pipeline/stages/framebuffer.py index 790be34..3df5a8b 100644 --- a/engine/pipeline/stages/framebuffer.py +++ b/engine/pipeline/stages/framebuffer.py @@ -14,7 +14,7 @@ from dataclasses import dataclass from typing import Any from engine.display import _strip_ansi -from engine.pipeline.core import DataType, PipelineContext, Stage +from engine.pipeline import DataType, PipelineContext, Stage @dataclass diff --git a/engine/sensors/__init__.py b/engine/sensors/__init__.py index 24dd5ff..bac7844 100644 --- a/engine/sensors/__init__.py +++ b/engine/sensors/__init__.py @@ -25,7 +25,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from engine.pipeline.core import PipelineContext + from engine.pipeline import PipelineContext @dataclass @@ -166,13 +166,13 @@ class SensorStage: @property def inlet_types(self) -> set: - from engine.pipeline.core import DataType + from engine.pipeline import DataType return {DataType.ANY} @property def outlet_types(self) -> set: - from engine.pipeline.core import DataType + from engine.pipeline import DataType return {DataType.ANY} diff --git a/sideline/pipeline/controller.py b/sideline/pipeline/controller.py index 301c1ab..765ee3b 100644 --- a/sideline/pipeline/controller.py +++ b/sideline/pipeline/controller.py @@ -423,7 +423,7 @@ class Pipeline: List of stages that were injected """ from sideline.camera import Camera - from sideline.data_sources.sources import EmptyDataSource + from engine.data_sources.sources import EmptyDataSource from sideline.display import DisplayRegistry from sideline.pipeline.adapters import ( CameraClockStage, @@ -1033,7 +1033,7 @@ def create_pipeline_from_params(params: PipelineParams) -> Pipeline: def create_default_pipeline() -> Pipeline: """Create a default pipeline with all standard components.""" - from sideline.data_sources.sources import HeadlinesDataSource + from engine.data_sources.sources import HeadlinesDataSource from sideline.pipeline.adapters import ( DataSourceStage, SourceItemsToBufferStage, diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index 1e34287..dcc8148 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -15,7 +15,7 @@ from engine.pipeline import ( create_default_pipeline, discover_stages, ) -from engine.pipeline.core import DataType, StageError +from engine.pipeline import DataType, StageError class TestStageRegistry: @@ -118,7 +118,7 @@ class TestPipeline: def test_build_resolves_dependencies(self): """Pipeline.build resolves execution order.""" - from engine.pipeline.core import DataType + from engine.pipeline import DataType pipeline = Pipeline() mock_source = MagicMock(spec=Stage) @@ -153,7 +153,7 @@ class TestPipeline: def test_execute_runs_stages(self): """Pipeline.execute runs all stages in order.""" - from engine.pipeline.core import DataType + from engine.pipeline import DataType pipeline = Pipeline() @@ -283,7 +283,7 @@ class TestCapabilityBasedDependencies: def test_capability_wildcard_resolution(self): """Pipeline resolves dependencies using wildcard capabilities.""" from engine.pipeline.controller import Pipeline - from engine.pipeline.core import Stage + from engine.pipeline import Stage class SourceStage(Stage): name = "headlines" @@ -329,7 +329,7 @@ class TestCapabilityBasedDependencies: def test_missing_capability_raises_error(self): """Pipeline raises error when capability is missing.""" from engine.pipeline.controller import Pipeline - from engine.pipeline.core import Stage, StageError + from engine.pipeline import Stage, StageError class RenderStage(Stage): name = "render" @@ -359,7 +359,7 @@ class TestCapabilityBasedDependencies: def test_multiple_stages_same_capability(self): """Pipeline uses first registered stage for capability.""" from engine.pipeline.controller import Pipeline - from engine.pipeline.core import Stage + from engine.pipeline import Stage class SourceA(Stage): name = "headlines" @@ -458,8 +458,15 @@ class TestPipelineContext: """PipelineContext resolves lazy services.""" ctx = PipelineContext() + # Register a lazy service resolver + from unittest.mock import MagicMock + + mock_config = MagicMock() + ctx.register_service("config", lambda: mock_config) + config = ctx.get("config") assert config is not None + assert config == mock_config def test_has_capability(self): """PipelineContext.has_capability checks for services.""" @@ -608,7 +615,7 @@ class TestStageAdapters: """DisplayStage.init initializes display.""" from engine.display.backends.null import NullDisplay from engine.pipeline.adapters import DisplayStage - from engine.pipeline.core import PipelineContext + from engine.pipeline import PipelineContext from engine.pipeline.params import PipelineParams display = NullDisplay() @@ -623,7 +630,7 @@ class TestStageAdapters: """DisplayStage.process forwards to display.""" from engine.display.backends.null import NullDisplay from engine.pipeline.adapters import DisplayStage - from engine.pipeline.core import PipelineContext + from engine.pipeline import PipelineContext from engine.pipeline.params import PipelineParams display = NullDisplay() @@ -640,7 +647,7 @@ class TestStageAdapters: """CameraStage applies camera transform.""" from engine.camera import Camera, CameraMode from engine.pipeline.adapters import CameraStage - from engine.pipeline.core import PipelineContext + from engine.pipeline import PipelineContext camera = Camera(mode=CameraMode.FEED) stage = CameraStage(camera, name="vertical") @@ -658,7 +665,7 @@ class TestStageAdapters: """ from engine.camera import Camera, CameraMode from engine.pipeline.adapters import CameraStage - from engine.pipeline.core import PipelineContext + from engine.pipeline import PipelineContext from engine.pipeline.params import PipelineParams camera = Camera(mode=CameraMode.FEED) @@ -696,7 +703,7 @@ class TestDataSourceStage: from engine.data_sources.sources import HeadlinesDataSource from engine.pipeline.adapters import DataSourceStage - from engine.pipeline.core import PipelineContext + from engine.pipeline import PipelineContext mock_items = [ ("Test Headline 1", "TestSource", "12:00"), @@ -739,7 +746,7 @@ class TestEffectPluginStage: """EffectPluginStage applies sensor param bindings.""" from engine.effects.types import EffectConfig, EffectPlugin from engine.pipeline.adapters import EffectPluginStage - from engine.pipeline.core import PipelineContext + from engine.pipeline import PipelineContext from engine.pipeline.params import PipelineParams class SensorDrivenEffect(EffectPlugin): @@ -772,7 +779,7 @@ class TestFullPipeline: def test_pipeline_circular_dependency_detection(self): """Pipeline detects circular dependencies.""" from engine.pipeline.controller import Pipeline - from engine.pipeline.core import Stage + from engine.pipeline import Stage class StageA(Stage): name = "a" @@ -819,7 +826,7 @@ class TestPipelineMetrics: def test_metrics_collected(self): """Pipeline collects metrics when enabled.""" from engine.pipeline.controller import Pipeline, PipelineConfig - from engine.pipeline.core import Stage + from engine.pipeline import Stage class DummyStage(Stage): name = "dummy" @@ -842,7 +849,7 @@ class TestPipelineMetrics: def test_metrics_disabled(self): """Pipeline skips metrics when disabled.""" from engine.pipeline.controller import Pipeline, PipelineConfig - from engine.pipeline.core import Stage + from engine.pipeline import Stage class DummyStage(Stage): name = "dummy" @@ -864,7 +871,7 @@ class TestPipelineMetrics: def test_reset_metrics(self): """Pipeline.reset_metrics clears collected metrics.""" from engine.pipeline.controller import Pipeline, PipelineConfig - from engine.pipeline.core import Stage + from engine.pipeline import Stage class DummyStage(Stage): name = "dummy" @@ -894,7 +901,7 @@ class TestOverlayStages: def test_stage_is_overlay_property(self): """Stage has is_overlay property defaulting to False.""" - from engine.pipeline.core import Stage + from engine.pipeline import Stage class TestStage(Stage): name = "test" @@ -908,7 +915,7 @@ class TestOverlayStages: def test_stage_render_order_property(self): """Stage has render_order property defaulting to 0.""" - from engine.pipeline.core import Stage + from engine.pipeline import Stage class TestStage(Stage): name = "test" @@ -922,7 +929,7 @@ class TestOverlayStages: def test_stage_stage_type_property(self): """Stage has stage_type property defaulting to category.""" - from engine.pipeline.core import Stage + from engine.pipeline import Stage class TestStage(Stage): name = "test" @@ -937,7 +944,7 @@ class TestOverlayStages: def test_pipeline_get_overlay_stages(self): """Pipeline.get_overlay_stages returns overlay stages sorted by render_order.""" from engine.pipeline.controller import Pipeline - from engine.pipeline.core import Stage + from engine.pipeline import Stage class OverlayStageA(Stage): name = "overlay_a" @@ -991,7 +998,7 @@ class TestOverlayStages: def test_pipeline_executes_overlays_after_regular(self): """Pipeline executes overlays after regular stages.""" from engine.pipeline.controller import Pipeline - from engine.pipeline.core import Stage + from engine.pipeline import Stage call_order = [] @@ -1071,7 +1078,7 @@ class TestOverlayStages: def test_pipeline_get_stage_type(self): """Pipeline.get_stage_type returns stage_type for a stage.""" from engine.pipeline.controller import Pipeline - from engine.pipeline.core import Stage + from engine.pipeline import Stage class TestStage(Stage): name = "test" @@ -1093,7 +1100,7 @@ class TestOverlayStages: def test_pipeline_get_render_order(self): """Pipeline.get_render_order returns render_order for a stage.""" from engine.pipeline.controller import Pipeline - from engine.pipeline.core import Stage + from engine.pipeline import Stage class TestStage(Stage): name = "test" @@ -1341,7 +1348,7 @@ class TestPipelineMutation: dependencies: set | None = None, ): """Helper to create a mock stage.""" - from engine.pipeline.core import DataType + from engine.pipeline import DataType mock = MagicMock(spec=Stage) mock.name = name @@ -1693,7 +1700,7 @@ class TestPipelineMutation: def test_mutation_preserves_execution_for_remaining_stages(self): """Removing a stage doesn't break execution of remaining stages.""" - from engine.pipeline.core import DataType + from engine.pipeline import DataType call_log = []