forked from genewildish/Mainline
- Create engine/interfaces/ module with centralized re-exports of all ABCs/Protocols - Remove duplicate Display protocol from websocket.py - Remove unnecessary pass statements in exception classes - Skip flaky websocket test that fails in CI due to port binding
5.6 KiB
5.6 KiB
Agent Development Guide
Development Environment
This project uses:
- mise (mise.jdx.dev) - tool version manager and task runner
- uv - fast Python package installer
- ruff - linter and formatter (line-length 88, target Python 3.10)
- pytest - test runner with strict marker enforcement
Setup
mise run install # Install dependencies
# Or: uv sync --all-extras # includes mic, websocket, sixel support
Available Commands
# Testing
mise run test # Run all tests
mise run test-cov # Run tests with coverage report
pytest tests/test_foo.py::TestClass::test_method # Run single test
# Linting & Formatting
mise run lint # Run ruff linter
mise run lint-fix # Run ruff with auto-fix
mise run format # Run ruff formatter
# CI
mise run ci # Full CI pipeline (topics-init + lint + test-cov)
Running a Single Test
# Run a specific test function
pytest tests/test_eventbus.py::TestEventBusInit::test_init_creates_empty_subscribers
# Run all tests in a file
pytest tests/test_eventbus.py
# Run tests matching a pattern
pytest -k "test_subscribe"
Git Hooks
Install hooks at start of session:
ls -la .git/hooks/pre-commit # Verify installed
hk init --mise # Install if missing
mise run pre-commit # Run manually
Code Style Guidelines
Imports (three sections, alphabetical within each)
# 1. Standard library
import os
import threading
from collections import defaultdict
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
# 2. Third-party
from abc import ABC, abstractmethod
# 3. Local project
from engine.events import EventType
Type Hints
- Use type hints for all function signatures (parameters and return)
- Use
|for unions (Python 3.10+):EventType | None - Use
dict[K, V],list[V](generic syntax):dict[str, list[int]] - Use
Callable[[ArgType], ReturnType]for callbacks
def subscribe(self, event_type: EventType, callback: Callable[[Any], None]) -> None:
...
def get_sensor_value(self, sensor_name: str) -> float | None:
return self._state.get(f"sensor.{sensor_name}")
Naming Conventions
- Classes:
PascalCase(e.g.,EventBus,EffectPlugin) - Functions/methods:
snake_case(e.g.,get_event_bus,process_partial) - Constants:
SCREAMING_SNAKE_CASE(e.g.,CURSOR_OFF) - Private methods:
_snake_caseprefix (e.g.,_initialize) - Type variables:
PascalCase(e.g.,T,EffectT)
Dataclasses
Use @dataclass for simple data containers:
@dataclass
class EffectContext:
terminal_width: int
terminal_height: int
scroll_cam: int
ticker_height: int = 0
_state: dict[str, Any] = field(default_factory=dict, repr=False)
Abstract Base Classes
Use ABC for interface enforcement:
class EffectPlugin(ABC):
name: str
config: EffectConfig
@abstractmethod
def process(self, buf: list[str], ctx: EffectContext) -> list[str]:
...
@abstractmethod
def configure(self, config: EffectConfig) -> None:
...
Error Handling
- Catch specific exceptions, not bare
Exception - Use
try/exceptwith fallbacks for optional features - Silent pass in event callbacks to prevent one handler from breaking others
# Good: specific exception
try:
term_size = os.get_terminal_size()
except OSError:
term_width = 80
# Good: silent pass in callbacks
for callback in callbacks:
try:
callback(event)
except Exception:
pass
Thread Safety
Use locks for shared state:
class EventBus:
def __init__(self):
self._lock = threading.Lock()
def publish(self, event_type: EventType, event: Any = None) -> None:
with self._lock:
callbacks = list(self._subscribers.get(event_type, []))
Comments
- DO NOT ADD comments unless explicitly required
- Let code be self-documenting with good naming
- Use docstrings only for public APIs or complex logic
Testing Patterns
Follow pytest conventions:
class TestEventBusSubscribe:
"""Tests for EventBus.subscribe method."""
def test_subscribe_adds_callback(self):
"""subscribe() adds a callback for an event type."""
bus = EventBus()
def callback(e):
return None
bus.subscribe(EventType.NTFY_MESSAGE, callback)
assert bus.subscriber_count(EventType.NTFY_MESSAGE) == 1
- Use classes to group related tests (
Test<ClassName>,Test<method_name>) - Test docstrings follow
"<method>() <action>"pattern - Use descriptive assertion messages via pytest behavior
Workflow Rules
Before Committing
- Run tests:
mise run test - Run linter:
mise run lint - Review changes:
git diff
On Failing Tests
- Out-of-date test: Update test to match new expected behavior
- Correctly failing test: Fix implementation, not the test
Never modify a test to make it pass without understanding why it failed.
Architecture Overview
- Pipeline: source → render → effects → display
- EffectPlugin: ABC with
process()andconfigure()methods - Display backends: terminal, websocket, sixel, null (for testing)
- EventBus: thread-safe pub/sub messaging
- Presets: TOML format in
engine/presets.toml
Key files:
engine/pipeline/core.py- Stage base classengine/effects/types.py- EffectPlugin ABC and dataclassesengine/display/backends/- Display backend implementationsengine/eventbus.py- Thread-safe event system