lock module#
The lockfile#
Which part answered which query, recorded so a later run gives the same board.
Lock is what a caller touches: it says where the file is and what to
do with it. The modes are LockMode: read-write (the default –
replay what is there, resolve and record the rest), read (replay, and resolve a
miss loudly), check (replay, re-resolve, and report differences), replay
(hermetic – replay and adopt, and never ask the chain to resolve) and off.
Under the three pinned modes a miss does not raise where it happens: it is
recorded and the run continues, and the failure arrives through
parts_verdict()’s exit status at the end. A project sets the file and the mode
under [tool.jitx.parts]:
[tool.jitx.parts]
lock = "parts.lock.json"
lock-mode = "read-write"
An unset lock-mode is the default; one that does not name a mode logs a warning and falls back to read, so a typo cannot silently re-pin the design.
Lock is a context, so anything wrapping a run – a driver, a test harness, a CLI verb if one is ever built – selects a mode by entering one, and a Design that declares its own overrides the project default. Nothing has to be called by hand: an ordinary run pins at the end of elaboration.
A lock exists to stop external change from moving a design, not internal change. That principle is what the keying follows from: an entry is keyed on the query digest, the active chain, and a user-supplied lock_scope, so editing the chain re-resolves (internal, deliberate) while a catalogue gaining rows does not (external, the world moving on).
A digest miss is recoverable, which is why editing one query does not churn the
parts nobody touched. Whether a locked part still satisfies an edited query is
checkable locally (the entry stores its attributes and the core has match)
so at the final elaboration flush orphaned entries are matched against unmatched
queries by the tiered sweep in adopt_orphans().
The rest of the module is the file itself. Lockfile reads and writes
it; encode_payload() and decode_payload() are the provider-facing
helpers for getting quantities through JSON and back.
- class LockMode(*values)[source]#
Bases:
StrEnumWhat a run does with the lockfile.
A StrEnum, so the member is the config spelling: LockMode(“read-write”) parses what [tool.jitx.parts] lock-mode holds, and a design that spells a mode as a plain string still compares equal.
- READ_WRITE = 'read-write'#
Replay what is there, resolve and record the rest. The default.
- READ = 'read'#
Replay only. A miss still resolves, so the board is complete and the report is, but it is recorded and fails the run at the end.
- CHECK = 'check'#
Run normally, then resolve the whole design cleanly and report where the board this run built differs from what the clean run would build. Every bound part is compared, adoptions included. The run’s own part is always the one that gets built; this mode only reports.
- REPLAY = 'replay'#
the chain is never asked to resolve. A miss is recorded as unresolvable and its component is left unbound. Replay and adoption still work – both reach their answer from the lockfile and build it with rehydrate, which is local.
- Type:
Hermetic
- OFF = 'off'#
Ignore the lock entirely.
- PINNED_MODES = frozenset({LockMode.CHECK, LockMode.READ, LockMode.REPLAY})#
Modes where a miss is a failure. off and read-write grow a design; these three defend one.
- class Lock(path=None, mode=LockMode.READ_WRITE)[source]#
Bases:
ContextWhich lockfile to use, and how.
A context, so anything wrapping a run selects a mode by entering one, and a Design that declares its own lock overrides the project default like any other context.
read-write is the default: running a design twice with no changes should give the same board, and the only way to get that is for the first run to pin.
- force_mode(mode)[source]#
Run every lock in the process under mode, or stop doing so with None.
For a driver that must select a mode around a design it cannot wrap in a Lock context, such as an exporter. Each lock keeps its path and only its mode is replaced, and the override applies to a design that declares its own Lock as well.
- LOCK_OFF = Lock(path=None, mode=<LockMode.OFF: 'off'>)#
The no-project default. A module-level singleton, not an inline constructor: get() must hand back the same object every time.
- discovered_lock(project)[source]#
The project’s lock, or off when there is no project to put one in.
Takes the project from
jitxlib.parts.policy.current_project(), which has already read its config. Keyed and cached exactly as discovered_source is, and for the same two reasons: identity stability, and not re-reading config for every part in the design. Reset withclear_discovered_locks().- Return type:
- Parameters:
project (JitxProject | None)
- clear_discovered_locks()[source]#
Drop every cached lock. For tests, and for a fresh run in one process.
- Return type:
- class LockKey(digest, chain, lock_scope='')[source]#
Bases:
objectWhat an entry is keyed on.
The digest gives query identity, the chain says what was asked, and lock_scope disambiguates queries that should be pinned separately.
- class LockEntry(key, query, filters, provider, identity, payload, adopted_from=None)[source]#
Bases:
objectOne pinned selection.
provider is the chain member, which is what is installed and what implements rehydrate; identity.provider records where the part actually came from, which for a delegating provider is not the same thing.
- Parameters:
- identity: PartIdentity#
- encode_value(value)[source]#
Encode an attribute or bound for JSON.
Quantities use pint’s own string form, tagged so nothing has to be sniffed back. A dimensionless bound stays a plain number, which is why [“Range”, null, 0.01] is right for AtMost(1 * pct) rather than an inconsistency with the encoder.
- decode_value(value)[source]#
Inverse of
encode_value(), through JITX’s shared registry.
- encode_payload(value)[source]#
Render a provider payload as JSON, with quantities encoded.
- Overloads:
- Parameters:
value (object)
- Return type:
For ResolvedPart.payload, which the core stores verbatim and never serializes. Walks dicts and lists, and takes a bare value too:
payload = encode_payload({“rated”: 35 * V, “series”: [“E24”, “E96”]})
decode_payload is the inverse, so rehydrate gets its quantities back.
Two asymmetries to know about. A dimensionless quantity encodes as its plain magnitude, so 1 * pct returns as 0.01 rather than as a quantity, matching how attributes and filters are stored. And a tuple encodes as a list, so it returns as a list.
Refuses a two-item list or tuple starting with “q”, at any depth, since that is how a quantity is encoded and decoding would hand back one. Store such data under a key instead, as {“tag”: “q”, “value”: …}.
- decode_payload(value)[source]#
Inverse of encode_payload, restoring quantities.
A two-item list starting with “q” is read back as a quantity. encode_payload refuses that shape as raw data, so anything it produced round-trips.
- decode_predicate(blob)[source]#
Rebuild a predicate from its encoded form.
Written out per type rather than as cls(*values). Two reasons: a splat over a decoded list is untypeable without a cast, and an entry with the wrong arity for its predicate name should be a clear error rather than a TypeError out of a constructor.
- class Lockfile(path=None, chains=dict(), entries=dict(), seen_keys=set())[source]#
Bases:
objectA parsed lockfile, plus the chain interning table.
Chains are interned at the top of the file rather than repeated per entry, because a design may resolve under more than one: any with PartSource(…) block introduces another.
- Parameters:
- seen_keys: set[tuple[str, str, str]]#
Keys claimed by a query this run, as (digest, chain, lock_scope).
Run state rather than file content: never read from or written to the JSON, and excluded from comparison and repr so it cannot be mistaken for either.
It lives here because it is keyed the way entries are, and because it is shared exactly when the file is. Two designs in one process writing one lockfile must share their claims, or the second adopts entries the first is holding; two designs writing different lockfiles must not.
- classmethod read(path)[source]#
Parse a lockfile, or return an empty one if it does not exist.
Raises LockStale on a digest_version mismatch, which is the only lock failure detectable before elaboration and the only one that is total.
- label_for(digest, preferred=None)[source]#
The stable short handle for a chain digest, assigning one if needed.
Deterministic so that regenerating a lock does not reshuffle every entry: main for the design-level chain, then whatever label a with block was given, else chain-2, chain-3.
- class Tier(*values)[source]#
Bases:
EnumAdoption tiers, strongest signal first.
What adoption protects is the layout, not the bill of materials: a part is a footprint that has been placed, routed around and fitted next to its neighbours, so re-resolution handing back a different size invalidates work built on top of it.
- EXACT = 1#
The orphan’s recorded filters equal the query’s current filters. The rank-edit case, where the BOM would churn.
- SUBSUMED = 2#
The query’s filters are a decidable relaxation of the orphan’s. The widening case, where the layout would churn, which is worse.
- SATISFIED = 3#
No filter relationship, but the part satisfies the query. Weakest signal, broadest safety net.
- subsumes(query_filters, entry_filters)[source]#
Whether the query’s filter set is a decidable relaxation of the entry’s.
Every key relaxed-or-equal, and at least one strictly relaxed.
- tier_of(entry, query_filters)[source]#
The strongest relation between an orphan and a query, or None.
The tiers are disjoint by construction: this returns the strongest that holds.
- confirmed(query_filters, entry)[source]#
Whether the entry’s recorded part satisfies the query’s current filters.
Part of candidacy, not a check on a chosen candidate: an entry whose recorded part does not satisfy the query was never a candidate for it, and letting it manufacture an ambiguity would let one hand-edited entry unpin a legitimate match.
A raise means “not a candidate” rather than aborting the sweep. Per candidate, not per pass: one unusable entry must not cost every other query its pin.
- class AdoptionCandidate(digest, chain, lock_scope, filters, query, payload_key=None)[source]#
Bases:
objectA query looking for an orphan to adopt, as the sweep sees it.
- Parameters:
- adopt_orphans(orphans, unmatched)[source]#
Match orphaned entries against unmatched queries.
Returns a mapping from a candidate’s lock key triple (joined) to the entry it adopted. Tier-major, and it returns to tier 1 after every weak adoption: a removal can make a stronger tier decidable for another query, and a loop that only restarts between passes enforces the tiering invariant between passes rather than at every moment.
- rekey(entry, candidate)[source]#
Re-key an adopted entry to the query that adopted it.
Rewriting the filters is not optional: leave them and the file holds an entry whose digest describes one query and whose filters describe another, so the next edit matches against stale filters, wrong for exactly the entries that have been adopted once.
- Return type:
- Parameters:
entry (LockEntry)
candidate (AdoptionCandidate)