Exception hierarchy¶
Every error the framework raises is a subclass of TripackError,
so consumers can catch the entire surface in one clause:
from tripack_contracts import TripackError
try:
container.resolve(Clock)
except TripackError:
# any Tripack failure: resolution, binding, scope, config, cycle
...
The hierarchy is intentionally shallow so the matrix of "what to catch" is short to memorise:
TripackError (base)
├── ResolutionError token cannot be resolved
│ └── CircularDependencyError cycle detected during resolution
├── BindingError binding registration failure / conflict
├── ScopeError scope unknown or expired
└── ConfigurationError declarative config invalid / unloadable
When each one fires¶
| Class | Raised when |
|---|---|
TripackError |
base class - never raised directly by the framework. Consumers MAY catch it to handle "any framework error". |
ResolutionError |
the runtime cannot resolve a token (no binding, or the binding's factory raised). |
CircularDependencyError |
resolution detected a cycle in the dependency graph. Subclass of ResolutionError. |
BindingError |
a binding cannot be registered: duplicate token with a different factory or lifecycle, or factory signature incompatible with the declared lifecycle. |
ScopeError |
resolving a SCOPED binding outside of any scope, or using a scope reference whose context manager has already exited. |
ConfigurationError |
TOML / JSON / YAML configuration is malformed, schema-invalid, or references a callable that cannot be imported. |
Catching strategy¶
# Coarse: handle anything Tripack-related uniformly.
try:
...
except TripackError:
...
# Mid: distinguish setup-time from resolution-time errors.
try:
...
except BindingError:
# bad registration - bug at startup
...
except ResolutionError:
# missing binding or cycle - bug in wiring
...
# Fine: dedicated path for cycles.
try:
...
except CircularDependencyError as exc:
cycle = " -> ".join(t.__qualname__ for t in exc.cycle if isinstance(t, type))
log.error("cycle: %s", cycle)
CircularDependencyError carries the cycle¶
This is the only subclass with attached state. The cycle attribute
is a tuple[DependencyToken, ...] describing the loop, where by
convention the first and last entries are the same token:
from tripack_contracts import CircularDependencyError
err = CircularDependencyError([Clock, Cache, Clock])
err.cycle # (<class 'Clock'>, <class 'Cache'>, <class 'Clock'>)
str(err) # "Circular dependency detected: Clock -> Cache -> Clock"
Class tokens render as __qualname__ in the formatted message;
non-class tokens (strings, tuples) fall back to repr.
All errors are picklable¶
Every class in the hierarchy round-trips through pickle, including
CircularDependencyError which uses a custom __reduce__ to
preserve the cycle tuple on the unpickled instance:
import pickle
from tripack_contracts import CircularDependencyError
err = CircularDependencyError(["a", "b", "a"])
revived = pickle.loads(pickle.dumps(err))
assert revived.cycle == ("a", "b", "a")
This matters for multiprocess workers, traceback serialisation, and test fixtures that snapshot exceptions.