Skip to content

Commit 00f52c2

Browse files
Kasper Jungeclaude
authored andcommitted
refactor: introduce Primitive protocol to type the generic discovery and display functions
The Named protocol only captured `name`, leaving _discover_and_filter_enabled (engine.py) and _print_primitives_section (cli.py) completely untyped. Evolve it to Primitive with both `name` and `enabled`, and add full type annotations to these two key generic functions so the shared primitive pattern is explicit and IDE-verifiable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 523953b commit 00f52c2

3 files changed

Lines changed: 29 additions & 11 deletions

File tree

src/ralphify/_discovery.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,23 @@
1616
from ralphify._frontmatter import PRIMITIVES_DIR, parse_frontmatter
1717

1818

19-
class Named(Protocol):
20-
"""Protocol for objects with a ``name`` attribute.
19+
class Primitive(Protocol):
20+
"""Protocol for the shared interface of all primitive types.
2121
2222
All primitive dataclasses (:class:`~ralphify.checks.Check`,
2323
:class:`~ralphify.contexts.Context`, :class:`~ralphify.instructions.Instruction`,
2424
:class:`~ralphify.ralphs.Ralph`) satisfy this protocol, enabling type-safe
25-
merging in :func:`merge_by_name`.
25+
discovery, filtering, merging, and display.
2626
"""
2727

2828
@property
2929
def name(self) -> str: ...
3030

31+
@property
32+
def enabled(self) -> bool: ...
33+
3134

32-
_N = TypeVar("_N", bound=Named)
35+
_P = TypeVar("_P", bound=Primitive)
3336

3437

3538
class PrimitiveEntry(NamedTuple):
@@ -100,12 +103,12 @@ def discover_local_primitives(
100103
return _scan_dir(base_dir / kind, marker)
101104

102105

103-
def merge_by_name(global_list: list[_N], local_list: list[_N]) -> list[_N]:
106+
def merge_by_name(global_list: list[_P], local_list: list[_P]) -> list[_P]:
104107
"""Merge global and prompt-local primitives; local wins on name conflict.
105108
106109
Used by the engine to overlay prompt-scoped primitives on top of
107-
global ones. Both lists must contain objects with a ``.name`` attribute
108-
(see :class:`Named`). Results are sorted alphabetically by name.
110+
global ones. Both lists must contain objects satisfying the
111+
:class:`Primitive` protocol. Results are sorted alphabetically by name.
109112
"""
110113
by_name = {p.name: p for p in global_list}
111114
for p in local_list:

src/ralphify/cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
import sys
1010
import tomllib
1111
import uuid
12+
from collections.abc import Callable
1213
from pathlib import Path
14+
from typing import TypeVar
1315

1416
import typer
1517
from rich.console import Console
1618

1719
from ralphify import __version__
1820
from ralphify._console_emitter import ConsoleEmitter
21+
from ralphify._discovery import Primitive
1922
from ralphify._frontmatter import CHECK_MARKER, CONFIG_FILENAME, CONTEXT_MARKER, INSTRUCTION_MARKER, PRIMITIVES_DIR, RALPH_MARKER
2023
from ralphify.checks import discover_checks
2124
from ralphify.contexts import discover_contexts
@@ -77,7 +80,10 @@ def resolve_command(self, ctx, args):
7780
]
7881

7982

80-
def _print_primitives_section(label: str, items: list, detail_fn) -> None:
83+
_P = TypeVar("_P", bound=Primitive)
84+
85+
86+
def _print_primitives_section(label: str, items: list[_P], detail_fn: Callable[[_P], str]) -> None:
8187
"""Print a status section for discovered primitives."""
8288
if items:
8389
rprint(f"\n[bold]{label}:[/bold] {len(items)} found")

src/ralphify/engine.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
import traceback
1818
from datetime import datetime, timezone
1919
from pathlib import Path
20-
from typing import Any, NamedTuple
20+
from collections.abc import Callable as _Callable
21+
from typing import Any, NamedTuple, TypeVar
2122
from ralphify._agent import execute_agent
22-
from ralphify._discovery import merge_by_name
23+
from ralphify._discovery import Primitive, merge_by_name
2324
from ralphify._events import Event, EventEmitter, EventType, NullEmitter
2425
from ralphify._output import format_duration
2526
from ralphify._run_types import RunConfig, RunState, RunStatus
@@ -66,7 +67,15 @@ def _resolve_prompt_dir(config: RunConfig) -> Path | None:
6667
return None
6768

6869

69-
def _discover_and_filter_enabled(root, prompt_dir, discover, discover_local):
70+
_P = TypeVar("_P", bound=Primitive)
71+
72+
73+
def _discover_and_filter_enabled(
74+
root: Path,
75+
prompt_dir: Path | None,
76+
discover: _Callable[[Path], list[_P]],
77+
discover_local: _Callable[[Path], list[_P]],
78+
) -> list[_P]:
7079
"""Discover primitives, merge local overrides (if any), and return only enabled ones.
7180
7281
Encapsulates the three-step pattern shared by all primitive types:

0 commit comments

Comments
 (0)