Record the doctest core architecture as an ADR - #92
Draft
tony wants to merge 24 commits into
Draft
Conversation
why: Design decisions for the doctest engine have lived in commit bodies and pull request threads, where a later reader cannot tell a deliberate constraint from an accident. The project needs one place that records what was decided, what forced it, and what it rules out. what: - Add docs/adrs/index.md stating what a record is for - Fix numbering as sequential and permanent, and name the four statuses a record can carry - Require pinned source links, since a blob/master anchor rots silently onto unrelated code while still resolving - Wire the section into the docs toctree
why: The finder is one 320-line method spanning parsing, grouping and naming; the runner reaches CPython's name-mangled loop through a code-object clone; the plugin blocks pytest's doctest plugin and then imports its privates; and a fork of xdist guards state xdist cannot see. All four follow from one conflation: test identity and shared state are separate axes, coupled here into a single setting. what: - Decide the item is the group and the DocTest is the block, so N DocTests report N locations under one unsplittable node id - Record the three facts it rests on, each verified by running it: repr_failure reads locations per failure, _DocTestRunner__run is an ordinary attribute override, and an item-scoped mapping never crosses a process - Fix the vocabulary where doctest, pytest and Sphinx collide on globs, namespace, group, scope, skip and name - Tabulate the upstream constraints, each anchored at a pinned tag - Record what was rejected and why, including prefix replay, IntFlag option surfaces and a whole-file docstring - Stub the five deferred decisions as 0002 through 0006
why: ADR 0001 asserts constraints about six upstream projects. Those assertions need a reviewable derivation, or the next person to question one has to redo the reading. Keeping it beside the record rather than inside it also keeps the record about the decision. what: - Add a per-system structural doc for CPython doctest, _pytest.doctest, pytest-xdist, pytest-asyncio, sphinx.ext.doctest, and docutils with myst-parser, each in the same section order so they compare - Add asyncio as the stdlib's own worked example of a pluggable architecture, for contrast with doctest's four seams - Add prior art on Sybil, xdoctest, pytest-examples and typeshed - Add cross-cutting docs on data structures, data flows, extension seams, and the namespace-scope versus test-identity axis - Add a bibliography collecting every anchor in one place - Pin every citation to a tag, or to a trunk-reachable commit where a project publishes none
why: Review found the record described PR #87's unmerged design as the status quo, and several claims did not survive checking against source. Every corrected claim below was re-verified by reading a pinned tag or by executing it. what: - Rewrite Context: trunk collects one DocTest per page; the groups, merge, skip lifting, exec runner and xdist scheduler are PR #87's proposal, named as such - Drop "per-block SKIPPED" from what the shape buys free, and add an outcome contract: TestReport.outcome is one scalar per item, so a mixed group either erases the skip or over-reports the whole group. Record subtests as the only sanctioned alternative and why it is not adopted - Credit sphinx.ext.doctest with the execution shape; the pytest identity is what is novel, not N DocTests per namespace - Replace the bare DocTest tuple with PlannedBlock and GroupPlan, so run_group can order phases, evaluate the gate and guarantee cleanup - Replace the compile-mode literal with a private ExecutionProfile, since PR #59's top-level await is a second policy a mode string cannot express - Move docutils node vocabulary out of the stdlib-only leaf, and move settings below the layers that read it - Add the item lifecycle contract, since half-reusing DoctestItem reintroduces the clear_globs wipe - Correct Sphinx: :options: on testcode is an unknown-option error, not a silent discard; :pyversion: is the silent one; cleanup does not run after setup failure, so always-cleanup is a divergence - Correct the parsefactories claim: conftest autouse fixtures arrive via FixtureManager.pytest_plugin_registered - Correct the nominal-subclassing claim: stdlib accepts a duck-typed parser or finder; typeshed is what demands the class - Narrow the xdist sentence: identical collection still binds - Repin Sphinx anchors to v8.2.3, the version this project resolves
why: The record proposed deprecating doctest_docutils_namespace_items, but neither that setting nor its scope twin has shipped — both live on PR #87, in no release and on no tag. Deprecating an unshipped setting fails the Published-Release Test, and there is no downstream to warn. what: - Retitle and reslug: the question is whether the shape should ship, not how to retire it - State plainly that nothing shipped, so there is no migration path, no warning and no downstream grep - Give the shape its due first: merging costs node ids, fixture lifetime and gutter locality, which is what per-block answers - Then give the four reasons against, each with its guard: an id that NameErrors when selected, a mapping that cannot cross a worker, a mapping that cannot survive a rerun, and a crash tail that has no guard at all - Note that the guards are the cost of the shape, not incidental - Record the honest limit of the alternative: no per-block outcome and no per-block node id
why: The proposed mechanism does not work and was not needed. Nested state machines build from nested_sm_kwargs, so substituting a parser instance's state_classes never reaches the constructs with missing lines; and RSTState.nested_sm_cache is a shared class attribute, so the substitution is not scoped to one parse either. Meanwhile docutils 0.22 fixed the defect upstream. what: - Replace the substitution mechanism, the feature probe and the fallback design with a floor bump to docutils >=0.22 - Record why the mechanism failed, so it is not proposed again - Name the real cost: docutils >=0.22 requires Sphinx >=9.1 - Version-qualify the line conventions, and state that an unqualified claim about docutils line numbers is a bug in the claim - Keep the nullable line and the normalization layer, which serve .. include:: attribution regardless of version
why: Deselecting in pytest_collection_modifyitems runs too late. DoctestTextfile.collect() reads and parses the page inside collect(), so by then the built-in has already produced an item, or already reported a collection error that deselection cannot retract. what: - Filter the built-in's collector out of the pytest_collect_file result in a hookimpl wrapper, before it parses anything - Record that narrowing --doctest-glob cannot help either, because _is_doctest claims .rst initial paths before consulting the glob - Move the compatibility failure from plugin registration to the collection of an affected document, matching 0001 and 0002 on not taking down a session whose tests never touch a doctest - Replace the settled open question with the one the filter raises: scoping it to paths this plugin actually claims
why: TestResults is a two-field namedtuple carrying skipped off-tuple, so == compares two of three values and a skip-count regression passes the harness silently. attempted is also incremented before the SKIP check, so a skip that wrongly executes moves neither counter. what: - Assert (failed, attempted, skipped) explicitly, and say why the tuple comparison is insufficient - Add an exec-mode case to the matrix, and note it has no stock counterpart since a stock runner rejects a multi-statement body
why: Review checked these notes against source and found several assertions wrong. Each correction below was re-verified by reading a pinned tag or by executing it. what: - Repin to what the project resolves: docutils 0.21.2, Sphinx 8.2.3, and myst-parser split across two Python floors. Repin every Sphinx anchor, including the gated-drop line, which moved between tags - Sphinx runs one DocTest per BLOCK against one shared group namespace. Move it into the N-DocTests row of the product space; its "one node id" is really no id, since every block shares DocTest.name - Sphinx rejects :options: on a testcode with an unknown-option error rather than discarding it. The silent loss there is :pyversion:. Add the doctest-then-testoutput case, and the setup-failure short-circuit that skips cleanup - parsefactories collects fixtures from the .py being collected; conftest autouse fixtures arrive via pytest_plugin_registered - stdlib doctest accepts a duck-typed parser or finder. Typeshed is what names the class, so a nominal subclass buys type-checkability, not passability - A regex can parse directive options; Sybil does. Justify the docutils dependency on host fidelity instead - Version-qualify the line conventions, since docutils 0.22 changed them - Correct the pytest-asyncio note: it does own an item class, it does convert str to enum, and the None sentinel is used on three of six options rather than all - Record subtests as pytest's only sanctioned sub-item mechanism
why: The skipif-drop line moved between Sphinx tags, and the vocabulary table still pointed at the v9.1.0 offset under a v8.2.3 path — an anchor that resolves onto the wrong lines.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #92 +/- ##
=======================================
Coverage 76.68% 76.68%
=======================================
Files 15 15
Lines 1025 1025
=======================================
Hits 786 786
Misses 239 239 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
why: A second review round found the corrected baseline was still wrong, and three model claims did not survive execution. Trunk's _find appends one DocTest per matched node named page.md[k] and the collector yields one item per test, so released gp-libs already has per-block identity with isolated copied globals — it lacks only a sharing unit. Example.__eq__ gates on exact type identity, so a bare subclass compares unequal to a stock Example in both directions. what: - Restate the baseline from trunk's source: per-block identity ships today, and it is what this design preserves rather than invents - Restate the invariant as scheduling identity versus diagnostic identity, which is what the decoupling actually is - Keep the Example subclass but override __eq__ with an isinstance check and rebind __hash__, pinned by a doctest asserting symmetry both ways; record the stock-Example fallback - Correct Example.source: stdlib-normalized executable body, not the author's verbatim text, which is what Block.source holds - Drop the purity claim. Includes read transitive files, directives execute during parsing, the registry is process-global and MyST plugins alter the tree; the contract is determinism over source closure, normalized settings and a frozen registry - Split GroupPlan from GroupRun so planning stays immutable and the live mapping belongs to an attempt - Add BlockResult and GroupResult as the channel for partial-skip detail, with its visibility and its accepted loss stated - Record that a paired want depends on a run-time gate, that wildcard membership needs a distinct DocTest per group, and that the gate's namespace diverges from Sphinx's fresh context - Complete the item lifecycle: seed restore, cleanup failure precedence, and the runner behaviours that must be reimplemented
why: A stock DocTestRunner does not raise on a multi-statement body — the compile call sits inside the loop's own try, so it records an unexpected exception and returns failed=1. Only DebugRunner propagates. The harness also needs report_* events, since a skip that wrongly executes moves neither counter. what: - State that stock records rather than raises, and assert the exec-mode case as a pair: stock fails, this runner passes - Add report_* hook events and repeated runs to the matrix - Exclude cross-block FAIL_FAST and cleanup aggregation, which belong to run_group's lifecycle tests rather than the per-example loop
why: Both mechanism assumptions were wrong. A docutils system_message carries a level and text and nothing semantically stable, so keying suppression on a stable code is impossible for exactly the messages this record wants to suppress. And attaching an observer is additive — the message still reaches the warning stream. what: - Say codes exist only for diagnostics this project emits, and that docutils-originated messages must be classified instead - Give the full three-part recipe: halt_level above 4, report_level 5 or warning_stream disabled, then the observer - Open the classifier question, with the two candidate answers and the two docutils dialects a text table would have to handle - Replace the promotion rule: registered-name is inverted for a typo and never fires for a swallowed foreign container. Promote on body content matching the example regex, with near-miss as an additive rule, and record that this is reST-only - Mark the direction not yet implementable, rather than implying it is
why: 0005 said docutils >=0.22 requires Sphinx >=9.1. Sphinx 9.0 already permits it — but requires Python 3.11, and gp-sphinx's sphinx<9 cap makes the requirement unsatisfiable regardless. 0006 proposed wrapper=True, which is gated on pluggy >=1.2 rather than pytest 8, and pytest 7 permits a pluggy that raises at plugin import. what: - 0005: state the move as three parts, with the gp-sphinx cap named as the binding constraint and upstream of this repo; add the resolved matrix per interpreter; target >=0.22,<0.23 rather than an open-ended floor; correct "pins" to "resolves"; make dropping Python 3.10 the blocking open question - 0006: use old-style hookwrapper with force_result, which needs no floor and was verified on pytest 7 and 9; name the minimum supported pytest; record that DoctestItem is public and the filtered collector is not
why: Several notes still described PR #87 as shipped, carried the superseded GroupTest model, or repeated claims later verification falsified. The true baseline is now known from trunk's source: released gp-libs appends one DocTest per matched node named page.md[k] and gives each its own copied globs. what: - Split the taxonomy rows: released gp-libs is one block, one item, isolated globals; PR #87 is a separate proposed row - Move Sphinx into the N-DocTests row and drop "unoccupied", since Sphinx already executes that shape without addressable ids - Rename the identity rule to never-source-coordinate-derived, and say an ordinal among extracted blocks satisfies it — which is what the released finder already uses - Replace GroupTest with GroupPlan in the data-flow diagram, and attribute the clone and the synthetic page to PR #87 - Separate Sphinx's three units in 20: runner call per block, shared state per group, result as process-wide counters - Drop BlockKind.node_types from the seam list and replace the rejected compile-policy callable with the private ExecutionProfile, matching ADR 0001 - Record that :pyversion: is declared on both testcode and testoutput and honoured on neither - Correct the reporter section: an observer is additive and does not silence the stream, and a system_message carries no stable code - Record why the state_classes substitution is not parse-scoped - Fix the Sphinx heading and link that disagreed on version
why: A third review found the surrounding model incomplete or self-contradictory even though the invariant holds. The Example subclass was the worst of it: restoring equality with an isinstance check makes two UNRELATED subclasses compare equal, and equal to any third party's bare subclass. Verified by execution on 3.10 through 3.15. what: - Move execution policy off doctest.Example onto ProjectedBlock. The runner establishes an active execution request before delegating to stdlib run() and clears it in a finally, so stock Example objects stay stock and nothing in the kernel is subclassed for metadata - Replace the type set with the lifecycle it actually has: ParsedBlock/ParsedOutput (inert) -> ProjectedBlock (stock DocTest, phase, profile, gate, gateable ExpectedOutput) -> GroupPlan (immutable) -> RunContext (live globs) -> BlockResult/GroupResult - Drop want from the parsed layer: neither of its owners is the parsed block - Give BlockKind a profile NAME so a public type stops holding a private one - Add an error outcome and primary/secondary failures, with the precedence rule stated and pytest classification left to the adapter - Design Settings as three facets with a stated precedence chain, and resolve the frozen registry alongside them rather than pointing at ADR 0006, which defines no freeze - Record that entry-point discovery is not what breaks xdist determinism; nondeterministic contribution is - Split DocumentParser from extract_blocks so a Sphinx extension can pass its own resolved doctree, and say why that is not a builder - Move Python object discovery out of markup/: it takes an object, not text, and is DocTestFinder-shaped - Replace the share axis with ungrouped = default | block, which is the question actually being answered - Correct __lt__: it compares (name, filename, lineno, id), with name leading — the hazard holds, the old wording did not - Correct the Sphinx precedent: setup blocks are combined into one simulated DocTest, likewise cleanup; only test blocks are per-block
why: Review found remaining inaccuracies, and the ADR 0001 model changed under the notes. Each correction below was verified against a pinned tag or by execution. what: - __lt__ compares (name, filename, lineno, id), with name leading. The hazard survives, and it is LIVE: find() sorts blocks named page.md[k], so an eleven-block page runs its eleventh block second - parse() covers the NORMALIZED input, not the input exactly: tabs are expanded, common indent stripped, and a comment-only example is dropped outright - pytest_xdist_make_scheduler is a broader affinity seam than _split_scope; the honest claim is scoped to the shipped schedulers - myst_fence_as_directive runs a fence through the directive of the SAME name; python does not become testcode without an alias - pytest-asyncio's _get_asyncio_mode is called from several sites, so the lesson is one conversion site, not one resolution - pytest-examples stores Python string indices, and one indent scalar does not invert a dedent in general - Reconcile with the corrected ADR model: ParsedBlock/ParsedOutput, compile mode on the projected block, ungrouped instead of share - Replace note 22's reason for deferring entry points: discovery is identical across xdist workers, so nondeterministic contribution is the hazard, not discovery. Record that the freeze is real even though the registry object is not
why: A fourth review found the surrounding contracts did not compose. GroupPlan was called immutable while holding a mutable DocTest whose globs a run reassigns, so reruns would operate on last attempt's state. DocumentParser was said to subclass doctest.DocTestParser, whose parse(string, name) signature it cannot satisfy. And the record promised a pluggable core while declaring nothing public. what: - Hold ingredients, not a DocTest: ProjectedBlock is a recipe and RunContext materializes fresh stock Example and DocTest objects per attempt, so a plan can never carry run state - A gated testoutput makes its output ABSENT, not empty-with-options, which is what Sphinx does - Replace the nullable result record with a discriminated union, so a passed result carrying an exception is unrepresentable - Give exception precedence explicitly: control-flow, then body, then cleanup, with the loser recorded - Split ExecutionProfile (immutable factory) from ExecutionRuntime (per attempt, one per profile a group uses), so a group may mix prompt, exec and async blocks and an async runtime owns one loop - Run ordinary prompt blocks on CPython's untouched loop and reserve the owned __run for extended profiles, making the common lane compatible by construction - Give the parser three lanes, since DocumentParser cannot be a DocTestParser: strings, markup, and DocTestFinder-shaped objects - Define self.dtest as a synthetic zero-example group DocTest whose globs is the live mapping, and keep the darwin capture guard - Add the report-attribute channel, since the controller sees serialized reports rather than items - Give settings lifetimes (session, document, block) rather than one precedence sentence; move encoding to the loader and report style to the host; make wildcard and naming invariants, not knobs - Publish a contributor protocol while keeping the registry private, and record the conftest-timing and heterogeneous-worker hazards
why: ADR 0001 now runs ordinary prompt blocks on CPython's untouched loop, which shrinks what 0002 must prove and makes 0005 orthogonal to the architecture. Review also found the notes still generalized Sphinx's per-block execution and conflated xdist's two channels. what: - 0002: scope the harness to the extended lane; ordinary blocks are the reference rather than something to differentially prove - 0005: state plainly that the floor is support policy, not core architecture, and that no part of 0001 depends on the answer - 0005: correct the Sphinx version — the group fallback for a bare, unstamped doctest_block changed in 9.0, not 9.1, and directives always stamp groups so unargumented directives are unaffected - Notes: Sphinx is per-block for its TEST phase only; setup blocks are combined into one simulated DocTest and cleanup into another - Notes: separate xdist's scheduling channel from its reporting channel — the controller sees node-id strings when scheduling and serialized reports afterwards, and reports carry arbitrary extra attributes - Notes: a nullable line is not unique to this design; Sphinx's get_line_number returns None too. What is new is per-block propagation - Notes: narrow the affinity claim to the shipped schedulers, since pytest_xdist_make_scheduler substitutes a whole Scheduling - Notes: PR #87's per-block mode is proposed, not shipped - Notes: carry the block/runtime split into the data-flow diagram
why: Making the plan a recipe last round dropped information the recipe must carry. One prompt block yields SEVERAL doctest.Example objects — three for a block ending in a traceback — each with its own source, want, exc_msg, lineno, indent and options, and pytest's failure renderer needs the docstring besides. A single source string and one lineno cannot rebuild that. The result types lost outcomes for the same reason: continue_on_failure produces several failures from one block, and a PASSING block can report attempted=2 skipped=1. what: - Add ExampleRecipe and carry examples plus docstring on ProjectedBlock, so materialization reproduces get_doctest exactly rather than approximating it. Pinned by a doctest asserting the three-example case - Make Failed.failures plural, give Passed and Failed counts, and type SkipReason, since a skip may come from :skipif:, an inline flag, :pyversion: or a profile declining - Replace the single precedence ladder with a phase-aware table: process aborts always win; a cleanup failure — including a pytest.skip raised in cleanup — never outranks a body failure - Enter profile runtimes through an ExitStack so partial startup unwinds in reverse - Rename position to source_ordinal and define it as the ordinal among runnable candidates before gating, filtering and group expansion, so inserting prose renames nothing - Call GroupPlan structurally immutable and say the seed is shallow-copied per attempt, matching doctest's namespace semantics - Retire the last PlannedBlock and plan-held-DocTest language, and restate wildcard membership as per-group materialization
why: The remaining review items were the contracts named but never written: a registry that ADR 0001 depended on and ADR 0006 did not define, a compatibility promise stated as one phrase covering surfaces of very different strength, and an unknown-directive default that could swallow a body holding doctests. what: - 0006: define the freeze lifecycle — Contributor and Registrar signatures, build_registry, duplicate names as an error unless replace=True, deterministic contribution order, and a freeze point per host - 0006: nested conftests may not contribute, since they load after any freeze that precedes collection; their fixtures and hooks are unaffected - 0006: workers compare a registry manifest rather than node ids, since two workers can collect identical ids while resolving one profile name to different code — which is what makes SSH and socket workers safe rather than assumed homogeneous - 0001: decompose "vanilla-compatible" into a matrix, and narrow the Sphinx promise to consuming a resolved doctree, since no execution lifecycle or result channel is specified - 0001: name the registry as a fifth input collection is not pure over - 0001: correct the layer diagram, which still said the runner always owns the example loop - 0004: split unknown roles from unknown body-owning directives. A role cannot swallow a code block; a container can, so it is a collection error by default and a project registers legitimate containers explicitly - Notes: the execution profile is contributable, not private, matching 0001's public contributor protocol; entry-point discovery is optional behind it rather than out of scope - Notes: narrow the node-id claim to per-block ids over shared MUTABLE state, since ids over isolated state do keep the promise
why: The proposed core mixed settings with discovered capabilities and left source ordering, host outcomes, checker rendering, and result projection underspecified. what: - Separate contracts, settings, models, and registry snapshots - Define stable source identity and complete gate/result records - Specify pytest failure projection and phase-aware exceptions
why: Registry construction is a host-neutral extension contract, not a pytest private-API detail, and each supported host freezes contributors at a different point. what: - Add typed contributor, registrar, and snapshot contracts - Define direct, pytest, Sphinx, and xdist lifecycles - Keep mutable construction private and late registration explicit
why: Several supporting claims overstated Sphinx block granularity, xdist determinism, stdlib subclass compatibility, and source offset semantics. what: - Correct Sphinx 9.0 grouping and phase execution claims - Separate markup protocols from stdlib-shaped facades - Qualify xdist manifests and source rewrite metadata
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
doctest.Exampleanddoctest.DocTestobjects while supporting docutils, MyST, pytest, Sphinx doctrees, and direct library use.DocTestper source block for locations, gutters, and failure detail.RegistrySnapshot; keep the mutable registry builder private.This PR changes documentation only. The principal architecture record remains
Status: Proposed; implementation is separate work.Core architecture
The core has three compatibility lanes. Plain strings use the exact stdlib parser contract, Python objects use a
DocTestFinder-shaped adapter, and reStructuredText or MyST use a typedDocumentParserfollowed by one doctree extractor. The markup parser does not subclassDocTestParserbecause their method signatures and return types are incompatible.Parsed source keeps
document_orderfor block/output pairing andblock_ordinalfor stable runnable identity. Projection owns grouping, wildcard cloning, phase order, pairing, name minting,:skipif:, and:pyversion:. Plans hold recipes, not mutableDocTestinstances; each attempt materializes fresh stock objects against one item-owned mapping.Prompt doctests retain CPython's per-example loop. Extended execution profiles own only the loop required for
exec, top-level await, or later policies. Checker factories are contributable, and the checker that compares output also renders its difference.Host and result contracts
Direct callers pass
Contributorobjects tobuild_registry(). pytest publishes its hookspec throughpytest_addhooksand freezes initial-plugin contributions inpytest_configure(trylast=True), before xdist starts nodes or collection begins. Sphinx extensions contribute duringsetup(app)and freeze atconfig-inited, before documents are read.The xdist manifest includes normalized session settings, ordered registry entries with provider versions, and
doctest.OPTIONFLAGS_BY_NAME. It detects declared extension drift; it does not prove equal provider code or source closure. Version 1 therefore requires homogeneous worker environments.GroupResultretains typed per-block results locally. The pytest item flattens doctest failures in block order intoMultipleDoctestFailures, inherits pytest's representation for the default checker, and attaches only a versioned JSON-safe summary to reports. Only test-phase blocks determine pass versus skip; setup and cleanup are infrastructure. Process, debugger, and session aborts always propagate, while cleanup failures cannot erase a primary test outcome.Evidence corrections
DocTestper ordinary or paired test, but combines setup blocks into one simulatedDocTestand cleanup blocks into another.defaultgroup.Verification
uv run ruff format --check .uv run ruff check .uv run mypy src testsuv run pytest— 105 passedjust build-docs— warnings-as-errors build succeeded