"""
Errors
======
Everything this package raises. All of it is reported against user code rather
than as an internal runtime fault, and the ones a design author can act on carry a
hint naming the remedy. `PartsError` is the base, so catching it catches all of
them.
Writing a provider, one of these is yours to raise: `StalePayload`, from
`rehydrate`, when a lock entry predates a change you cannot honour. It never
reaches the user -- the resolver records it and re-resolves the query from the
chain. Nothing else here is a provider's to raise; `ProviderContractError` is what
the core raises *about* a provider.
Reading a failure, the split worth knowing is whose fault it names. A fault in the
*design* comes with the remedy in its hint: `PartNotFound`, `PartDenied`,
`PolarityMismatch`. A fault in *configuration* means nothing has been asked yet or
the lock cannot be used at all: `NoChainConfigured`, `LockStale`. A fault in a
*provider* is not the design author's to fix: `ProviderContractError`,
`ProviderUnavailable`, and `NestedDeferral` where a provider's `materialize` is
what deferred. Only the first group should ever be the first thing a design author
reads.
`UnresolvedPart` and `ResolverLostQuery` sit apart from all three: they are raised
by a *read* rather than by resolution, when something touches a field on a
component whose query never produced a part. They are the only two that are also
`AttributeError`, because they come out of `__getattr__` and a probe there has to
be able to catch them. Which of the two you get says where to look: `UnresolvedPart`
means the run reported the failure and `parts_verdict()` explains it,
`ResolverLostQuery` means it did not, which is a bug here.
`DimensionMismatch` sits across that split on purpose. A query in the wrong unit
and a provider reporting one are equally likely, so it names the attribute and
both dimensionalities instead of picking a culprit.
`QueryUnlocked` is the odd one: a verdict category that has an exception class so a
driver can turn a failing verdict into a raise.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from jitx.error import UserCodeException
if TYPE_CHECKING:
pass
[docs]
class PartsError(UserCodeException):
"""Base for every error this package raises."""
[docs]
class PartNotFound(PartsError):
"""No provider in the chain matched the query.
Carries each provider's own reason, in chain order, because "no match" from a
four-member chain is four different facts.
"""
def __init__(self, description: str, reasons: list[tuple[str, str]]):
self.reasons = reasons
detail = "\n".join(f" {name}: {why}" for name, why in reasons)
super().__init__(
f"no part found for {description}\n{detail}",
hint="Relax a filter, or call `explain(query)` to see each "
"provider's reason",
)
[docs]
class PartDenied(PartsError):
"""A provider vetoed the query, so the chain stopped rather than falling through.
A veto is not an absence: something in the chain has an opinion about this
query being asked at all, and a later provider answering it would defeat the
point.
"""
def __init__(self, provider: str, reason: str, description: str):
self.provider = provider
super().__init__(
f"{provider} denied {description}: {reason}",
hint="This chain member vetoes the query rather than declining it; "
"the query has to change, or the chain does",
)
[docs]
class DimensionMismatch(PartsError):
"""A predicate and a value have different dimensionality.
A bug rather than a non-match, and deliberately does not presume whose: a
design querying the wrong unit and a provider reporting one are equally
likely, so the message names the attribute and both dimensions.
"""
def __init__(self, key: str, a: object, b: object):
self.key = key
super().__init__(
f"{key or 'value'}: cannot compare {_dim(a)} with {_dim(b)}",
hint="One side is in the wrong unit. Check the query's units and the "
"provider's reported attribute",
)
def _dim(value: object) -> str:
dimensionality = getattr(value, "dimensionality", None)
if dimensionality is not None:
return f"{value} ({dimensionality or 'dimensionless'})"
return f"{value!r} (dimensionless)"
[docs]
class StalePayload(PartsError):
"""A provider cannot rehydrate a payload it wrote earlier.
Raise this from `rehydrate` when a lock entry predates a change the provider
cannot honour. The resolver re-resolves the query from the chain and records
the event; it never reaches the user as an exception.
"""
[docs]
class ProviderUnavailable(PartsError):
"""A lock entry names a provider that cannot be reached.
Not a missing chain member: chain membership is in the key, so an unresolvable
chain fails before any entry is looked up. What reaches here is a delegating
provider whose inner routing names something absent, or a hand-edited entry.
Disposition is `StalePayload`'s when it arises during replay: it happens at
the same point in the same operation, so it is recorded as a replay failure
and the query re-resolves.
Also raised, and not caught, when a chain names a provider string that no
installed package supplies. That one is not a replay failure and there is
nothing to fall back to: the chain cannot be built at all.
"""
[docs]
class ProviderContractError(PartsError):
"""A provider returned something that violates the protocol.
A part missing its category's `value_key`, a `materialize` that replaced
ports, a `config()` that is not canonical JSON, an attribute in the wrong
unit. Names the provider, not the user.
"""
[docs]
class NestedDeferral(PartsError):
"""A part was deferred while parts were binding.
Raised when a provider's `materialize`, or any `PartsResolved` handler,
constructs a component that itself resolves through a chain. Build such a
part in the owning circuit and hand it to `materialize`, or give the provider
concrete geometry.
"""
[docs]
class PolarityMismatch(PartsError):
"""A plain `Capacitor` resolved to a part marked polarized.
Named against the user rather than the provider: the provider answered the
query it was given, and the query was the wrong shape. The message offers the
two remedies that exist: `PolarizedCapacitor`, or `polarized=Exactly(False)`
where the catalogue marks polarity on every row.
"""
def __init__(self, mpn: str, provider: str):
super().__init__(
f"{provider} resolved {mpn!r}, which is marked polarized, for a "
f"plain Capacitor whose ports are p1/p2",
hint="Use PolarizedCapacitor if you wanted a polarized part. Or, if "
"your catalogue marks polarity on every row, add "
"polarized=Exactly(False), against a catalogue that does not "
"mark it, that filter matches nothing",
)
[docs]
class LockStale(PartsError):
"""The lock predates the current digest_version, so nothing in it matches.
Tells the reader to regenerate rather than implying the design changed. Not
raised for an unrecognized chain: that is a stale-chain group, recorded and
reported at the end of the run.
"""
def __init__(self, found: int, expected: int):
super().__init__(
f"lockfile digest_version {found} predates the current {expected}; "
f"every digest in it was computed under different rules",
hint="Delete the lockfile and run once with a writing lock mode; "
"a stale digest_version cannot be read by any mode, so there is "
"nothing to regenerate it in place",
)
[docs]
class UnresolvedPart(PartsError, AttributeError):
"""A component's part never resolved, and something read a field off it.
Raised by the reading attribute access, not by the resolver: an unresolvable
query is recorded and the run continues to its summary, so the raise happens
wherever the design first touches the part that was never bound. The message
carries the query, the chain's own reason, and where the component was
constructed, because none of that is recoverable from the read site.
Both `PartsError` and `AttributeError` on purpose. It comes out of
`__getattr__`, where anything that is not an `AttributeError` breaks
`hasattr`, `getattr(obj, name, default)`, `copy`, `pickle` and every
structural probe that expects a missing name to be answerable rather than
fatal. Being a `PartsError` too is what lets a driver tell this apart from a
typo, which a bare `AttributeError` never could.
"""
def __init__(
self, component: str, name: str, description: str, reason: str, origin: str
):
self.attribute = name
self.reason = reason
detail = f": {reason}" if reason else ""
super().__init__(
f"{component}.{name} is unavailable: {description}, constructed at "
f"{origin}, never resolved{detail}",
hint="This query is in the parts report as unresolvable; "
"`parts_verdict().report()` has the chain's account of it",
)
[docs]
class ResolverLostQuery(PartsError, AttributeError):
"""A query reached no code path at all: a bug in this package, not the design.
Distinguished from `UnresolvedPart` because the remedies have nothing in
common. An unresolvable query was handled and failed, and the verdict explains
it. A query that was *dropped* produces no event of any kind, so the run's own
report cannot see the hole and reports success while a component sits unbound.
That has happened once already -- the adoption sweep keyed one state per
question and silently discarded duplicates -- and it surfaced as a missing
attribute far from its cause. Naming it makes the next one announce itself.
An `AttributeError` for the same reason `UnresolvedPart` is: it is raised from
`__getattr__`, and a probe there must not be handed something it cannot catch.
"""
def __init__(self, component: str, name: str, description: str, origin: str):
self.attribute = name
super().__init__(
f"{component}.{name} is unavailable: {description}, constructed at "
f"{origin}, was never resolved and never reported, which is a bug in "
f"jitxlib.parts",
hint="Please report this with the design that produced it; the parts "
"report will not mention the query, which is the symptom",
)
[docs]
class QueryUnlocked(PartsError):
"""A pinned mode met a query the lock cannot answer.
Under a pinned mode the miss is recorded and the run continues; the failure
arrives through `parts_verdict()`'s exit status. Raise this from a driver that
wants a failing verdict to become an exception.
"""
def __init__(self, description: str = ""):
super().__init__(
f"unlocked query{': ' + description if description else ''}",
hint='Run once with `lock-mode = "read-write"` (the default) to pin it',
)