Python · 4006 bytes Raw Blame History
1 """Exception hierarchy for 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 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
66
67
68 class MissingTrainingStateError(SwayError):
69 """The pre-run probes (S25 ``gradient_ghost``) couldn't find a
70 ``training_state.pt`` next to the adapter.
71
72 Distinguishes "the file legitimately doesn't exist for this adapter"
73 (probe SKIPs cleanly) from "the file exists but won't load"
74 (probe ERRORs). Pre-run probes catch this and emit SKIP rather
75 than letting the missing file kill the suite.
76 """
77
78 def __init__(self, adapter_path: object) -> None:
79 super().__init__(
80 f"no training_state.pt under {adapter_path} — adapter wasn't "
81 f"produced by dlm or the file was pruned. Pre-run diagnostics "
82 f"(gradient_ghost) will SKIP for this adapter."
83 )
84 self.adapter_path = adapter_path
85
86
87 class DlmCompatError(SwayError):
88 """The installed ``dlm`` package's public surface doesn't match what
89 sway's resolver expects.
90
91 Raised when e.g. ``dlm.base_models.resolve`` returns an object
92 without the ``hf_id`` attribute we depend on — historically dlm
93 used ``hf_id``; if it renames to ``repo_id`` we want a loud,
94 actionable error with both version strings in the message, not a
95 silent pass-through that hands the backend a registry key it can't
96 load.
97
98 The installed-dlm version is introspected best-effort; it's
99 informational, not a key for programmatic branching.
100 """
101
102 def __init__(self, message: str, *, installed_dlm_version: str | None = None) -> None:
103 full = message
104 if installed_dlm_version:
105 full = f"{message} (installed dlm version: {installed_dlm_version})"
106 full = (
107 f"{full}\n"
108 "Hint: pin a compatible dlm with: pip install 'dlm-sway[dlm]' "
109 "(resolves the tested dlm version range from pyproject.toml)."
110 )
111 super().__init__(full)
112 self.installed_dlm_version = installed_dlm_version