Source code for jitxlib.parts.error

"""
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 NoChainConfigured(PartsError): """No provider chain is configured, at any precedence level. Not an empty chain and not a guess: discovery is opt-in, so this is the error a project gets when it has said nothing. """ def __init__(self, root: object = None): where = f" for {root}" if root is not None else "" super().__init__( f"no parts provider chain configured{where}", hint="Configure one of: a `parts` attribute on your Design, " "`[tool.jitx.parts] chain` in pyproject.toml, an installed " "configuration package, or `discover = true` to guess from the " "environment", )
[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', )