Skip to content

Commit da87fa3

Browse files
authored
fix: subclass all the exceptions (#6)
Closes #5.
1 parent 5bef6d9 commit da87fa3

3 files changed

Lines changed: 35 additions & 27 deletions

File tree

docs/reference/api.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,18 @@ non-default parent is added).
5656

5757
| Member | Purpose |
5858
| --- | --- |
59-
| `validate_room_id(room_id)` | enforce the room-id / path-segment rule (`ROOM_ID_RE`); raises `AddRoomError` |
59+
| `validate_room_id(room_id)` | enforce the room-id / path-segment rule (`ROOM_ID_RE`); raises `BadRoomId` |
6060
| `resolve_project(project_dir)` | resolve + verify the stack root (has `COMPOSE_FILE` and `INSTALLATION_FILE`) |
6161
| `resolve_package_name(project, override)` | the stack's own package (inferred from `src/<pkg>/tools.py`) or `DEFAULT_PACKAGE_NAME` |
6262
| `room_parent_candidates(project)` | the `room_paths` container entries a caller can offer as a `parent_path` (or `["./rooms"]` when absent) |
6363
| `install_room(project, room_id, *, config_text, prompt_text=None, parent_path, force=False, dry_run=False)` | write the room dir from a rendered `config_text` (+ optional prompt) under `parent_path` and wire `room_paths` |
6464
| `install_room_from(project, room_id, src_dir, *, parent_path, force=False, dry_run=False)` | the same, but *copy* the `src_dir` template tree (multi-file); the caller patches the copied files afterward |
6565
| `RoomInstalled(config_path, path_action)` | the install outcome (alias `RoomInstall` kept for back-compat) |
66-
| `AddRoomError` | user-facing error with message-factory classmethods (incl. `bad_room_id`, `parent_is_room`) |
66+
| `AddRoomError` | base class for user-facing errors |
67+
| `ComposeNotFound(AddRoomError)` | No `docker-compose.yaml` found |
68+
| `NotAStack(AddRoomError)` | target lacks required template-generated files |
69+
| `BadRoomId(AddRoomError)` | invalid room ID syntac |
70+
| `ParentIsRoom(AddRoomError)` | room directory exists |
6771
| `RoomExists(AddRoomError)` | Allows users to catch this error specifically |
6872
| `ADDED` / `UNCHANGED` / `COVERED` | the `installation.TargetAction` members, re-exported |
6973
| `ROOMS_PARENT_ENTRY` | the `./rooms` default-discovery container |

src/soliplex_plumber/rooms.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,36 +61,40 @@
6161

6262

6363
class AddRoomError(Exception):
64-
"""A user-facing error (printed without a traceback).
64+
"""Base class for user-facing errors."""
6565

66-
Message construction lives in these classmethod factories so call sites
67-
read ``raise AddRoomError.<reason>(...)`` with no inline message string.
68-
"""
6966

70-
@classmethod
71-
def compose_not_found(cls, path):
72-
return cls(
67+
class ComposeNotFound(AddRoomError):
68+
def __init__(self, path):
69+
self.path = path
70+
super().__init__(
7371
f"no {COMPOSE_FILE} at {path} "
7472
"(run with --project-dir pointing at the stack directory)"
7573
)
7674

77-
@classmethod
78-
def not_a_stack(cls, path):
79-
return cls(
75+
76+
class NotAStack(AddRoomError):
77+
def __init__(self, path):
78+
self.path = path
79+
super().__init__(
8080
f"{path} is not a generated Soliplex stack: missing "
8181
f"'{INSTALLATION_FILE}'"
8282
)
8383

84-
@classmethod
85-
def bad_room_id(cls, room_id):
86-
return cls(
84+
85+
class BadRoomId(AddRoomError):
86+
def __init__(self, room_id):
87+
self.room_id = room_id
88+
super().__init__(
8789
f"room id {room_id!r} must match {ROOM_ID_RE.pattern} "
8890
"(letters, digits, '.', '_', '-'; no leading dot)"
8991
)
9092

91-
@classmethod
92-
def parent_is_room(cls, path):
93-
return cls(
93+
94+
class ParentIsRoom(AddRoomError):
95+
def __init__(self, path):
96+
self.path = path
97+
super().__init__(
9498
f"parent_path {path} is itself a room (has a room_config.yaml); "
9599
"pass a container directory to install rooms into"
96100
)
@@ -104,16 +108,16 @@ def __init__(self, path):
104108

105109
def validate_room_id(room_id: str) -> None:
106110
if not ROOM_ID_RE.match(room_id):
107-
raise AddRoomError.bad_room_id(room_id)
111+
raise BadRoomId(room_id)
108112

109113

110114
def resolve_project(project_dir: str) -> pathlib.Path:
111115
"""Return the resolved stack root, or raise if it is not a stack."""
112116
project = pathlib.Path(project_dir).resolve()
113117
if not (project / COMPOSE_FILE).is_file():
114-
raise AddRoomError.compose_not_found(project / COMPOSE_FILE)
118+
raise ComposeNotFound(project / COMPOSE_FILE)
115119
if not (project / INSTALLATION_FILE).is_file():
116-
raise AddRoomError.not_a_stack(project)
120+
raise NotAStack(project)
117121
return project
118122

119123

@@ -246,7 +250,7 @@ def _install_room(
246250
room, or when the room dir already exists and ``force`` is false."""
247251
env = project / ENVIRONMENT_DIR
248252
if (env / parent_path / "room_config.yaml").is_file():
249-
raise AddRoomError.parent_is_room(env / parent_path)
253+
raise ParentIsRoom(env / parent_path)
250254
room_dir = env / parent_path / room_id
251255
config_path = room_dir / "room_config.yaml"
252256
if room_dir.exists() and not force:

tests/unit/test_rooms.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def test_validate_room_id_accepts(room_id):
106106

107107
@pytest.mark.parametrize("room_id", ["", ".hidden", "a/b", "a b", "../x"])
108108
def test_validate_room_id_rejects(room_id):
109-
with pytest.raises(rooms.AddRoomError, match="must match"):
109+
with pytest.raises(rooms.BadRoomId):
110110
rooms.validate_room_id(room_id)
111111

112112

@@ -121,17 +121,17 @@ def test_resolve_project_ok(tmp_path):
121121
assert result == project.resolve()
122122

123123

124-
def test_resolve_project_no_compose(tmp_path):
124+
def test_resolve_project_compose_not_found(tmp_path):
125125
project = _make_stack(tmp_path, compose=False)
126126

127-
with pytest.raises(rooms.AddRoomError, match="docker-compose.yml"):
127+
with pytest.raises(rooms.ComposeNotFound):
128128
rooms.resolve_project(str(project))
129129

130130

131131
def test_resolve_project_not_a_stack(tmp_path):
132132
project = _make_stack(tmp_path, installation=False)
133133

134-
with pytest.raises(rooms.AddRoomError, match="not a generated"):
134+
with pytest.raises(rooms.NotAStack):
135135
rooms.resolve_project(str(project))
136136

137137

@@ -381,7 +381,7 @@ def test_install_room_rejects_parent_that_is_a_room(tmp_path):
381381
# ./rooms is a room
382382
_write_text(rooms_dir / "room_config.yaml", rooms_id_yaml)
383383

384-
with pytest.raises(rooms.AddRoomError, match="itself a room"):
384+
with pytest.raises(rooms.ParentIsRoom):
385385
rooms.install_room(
386386
project, "handbook", config_text=X_ID_YAML, parent_path="./rooms"
387387
)

0 commit comments

Comments
 (0)