tenseleyflow/sway / bec8a31

Browse files

sway(core): exception hierarchy

Authored by espadonne
SHA
bec8a31466479a0330787b5622416931e6f2c6ec
Parents
3732e0b
Tree
f42b7ec

2 changed files

StatusFile+-
A src/dlm_sway/core/errors.py 65 0
A tests/unit/test_errors.py 55 0
src/dlm_sway/core/errors.pyadded
@@ -0,0 +1,65 @@
1
+"""Exception hierarchy for dlm-sway.
2
+
3
+Every error sway raises inherits from :class:`SwayError` so callers can
4
+catch the whole family with a single ``except``. Subclasses carry enough
5
+context (spec paths, probe names, missing extras) for the CLI to render
6
+actionable messages without the caller having to introspect an exception
7
+chain.
8
+"""
9
+
10
+from __future__ import annotations
11
+
12
+
13
+class SwayError(Exception):
14
+    """Root of the dlm-sway exception hierarchy."""
15
+
16
+
17
+class SpecValidationError(SwayError):
18
+    """A ``sway.yaml`` (or equivalent) failed pydantic validation.
19
+
20
+    Parameters
21
+    ----------
22
+    message:
23
+        Human-readable summary of what went wrong.
24
+    source:
25
+        Path or identifier of the spec being validated, if known.
26
+    """
27
+
28
+    def __init__(self, message: str, *, source: str | None = None) -> None:
29
+        super().__init__(message)
30
+        self.source = source
31
+
32
+    def __str__(self) -> str:
33
+        base = super().__str__()
34
+        return f"{self.source}: {base}" if self.source else base
35
+
36
+
37
+class BackendNotAvailableError(SwayError):
38
+    """A requested backend's optional dependencies aren't installed.
39
+
40
+    The CLI turns this into a pointed ``pip install dlm-sway[<extra>]``
41
+    hint; programmatic callers can read :attr:`extra` directly.
42
+    """
43
+
44
+    def __init__(self, backend: str, *, extra: str, hint: str | None = None) -> None:
45
+        message = (
46
+            f"backend {backend!r} unavailable — install the extra: pip install 'dlm-sway[{extra}]'"
47
+        )
48
+        if hint:
49
+            message = f"{message}\n{hint}"
50
+        super().__init__(message)
51
+        self.backend = backend
52
+        self.extra = extra
53
+
54
+
55
+class ProbeError(SwayError):
56
+    """A probe failed to *execute* (as opposed to failing its assertion).
57
+
58
+    Distinct from a ``verdict=FAIL`` result — assertion failures are
59
+    normal and reported via :class:`ProbeResult`. This is for genuine
60
+    bugs: missing sections, mismatched tokenizers, NaN logits.
61
+    """
62
+
63
+    def __init__(self, probe: str, message: str) -> None:
64
+        super().__init__(f"probe {probe!r}: {message}")
65
+        self.probe = probe
tests/unit/test_errors.pyadded
@@ -0,0 +1,55 @@
1
+"""Tests for the exception hierarchy."""
2
+
3
+from __future__ import annotations
4
+
5
+import pytest
6
+
7
+from dlm_sway.core.errors import (
8
+    BackendNotAvailableError,
9
+    ProbeError,
10
+    SpecValidationError,
11
+    SwayError,
12
+)
13
+
14
+
15
+class TestSwayError:
16
+    def test_is_root_exception(self) -> None:
17
+        assert issubclass(SpecValidationError, SwayError)
18
+        assert issubclass(BackendNotAvailableError, SwayError)
19
+        assert issubclass(ProbeError, SwayError)
20
+
21
+    def test_raised_and_caught_as_sway_error(self) -> None:
22
+        with pytest.raises(SwayError):
23
+            raise ProbeError("delta_kl", "shape mismatch")
24
+
25
+
26
+class TestSpecValidationError:
27
+    def test_format_without_source(self) -> None:
28
+        err = SpecValidationError("unknown key 'topp'")
29
+        assert str(err) == "unknown key 'topp'"
30
+        assert err.source is None
31
+
32
+    def test_format_with_source(self) -> None:
33
+        err = SpecValidationError("unknown key 'topp'", source="sway.yaml")
34
+        assert str(err) == "sway.yaml: unknown key 'topp'"
35
+        assert err.source == "sway.yaml"
36
+
37
+
38
+class TestBackendNotAvailableError:
39
+    def test_hint_rendered_in_message(self) -> None:
40
+        err = BackendNotAvailableError("hf", extra="hf")
41
+        assert "pip install 'dlm-sway[hf]'" in str(err)
42
+        assert err.backend == "hf"
43
+        assert err.extra == "hf"
44
+
45
+    def test_appends_optional_hint(self) -> None:
46
+        err = BackendNotAvailableError("mlx", extra="mlx", hint="Apple Silicon only.")
47
+        assert "Apple Silicon only." in str(err)
48
+
49
+
50
+class TestProbeError:
51
+    def test_includes_probe_name(self) -> None:
52
+        err = ProbeError("delta_kl", "NaN logits")
53
+        assert "delta_kl" in str(err)
54
+        assert "NaN logits" in str(err)
55
+        assert err.probe == "delta_kl"