doctest_core: typed doctest core architecture spike - #97
Closed
tony wants to merge 31 commits into
Closed
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.
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
why: Test the ADR boundaries against stock prompt semantics, typed markup extraction, shared groups, and extended execution. what: - Add immutable contracts, settings, models, and registry snapshots - Extract and project reStructuredText and MyST into group recipes - Run prompt and exec lanes with typed results and cleanup policy - Cover projection, runtime, registry, and Sphinx doctree behavior
why: Keep the released direct API and command shape while exercising the same extraction, projection, and execution path as other hosts. what: - Adapt the finder and direct runner to typed block and group results - Preserve stock failure and summary accounting across Python versions - Add direct facade ordering, location, and reporting regressions
why: Preserve pytest fixtures, checker semantics, and reporting without blocking the built-in doctest plugin or splitting shared group state. what: - Add one item per group with fresh attempt-local doctest materialization - Quarantine private pytest compatibility and filter duplicate collectors - Exercise fixtures, reruns, xdist, async fixtures, and contributions - Package the typed core, adapters, tests, and corrected plugin entry point
why: Broad major-version constraints hid invalid dependency combinations and did not prove the declared Python and pytest floor. what: - Pin docutils, pytest, asyncio, and rerun plugin matrix versions - Select the matrix interpreter for every uv command - Check installed dependency consistency before static and runtime gates
why: The proposed architecture contained compatibility and lifecycle claims that changed when exercised against real hosts and version floors. what: - Record the typed-core and thin-adapter bakeoff result - Narrow vanilla, diagnostics, line, and pytest compatibility promises - Define runner and registry contracts proven by the implementation - List deferred host lifecycle, reporting, async, and facade work
why: Public guidance must distinguish the direct command from pytest's one-item-per-group behavior without promising full stdlib transcripts. what: - Describe direct doctest command and summary behavior - Explain built-in pytest doctest plugin composition - Clarify bare-block isolation and the Sphinx default directive group - State the supported docutils range
why: The rebase onto master dropped this branch's `docutils<0.22` cap, on the reasoning that master tests 0.22.4 successfully. That was wrong. Sphinx declares `docutils>=0.20,<0.22`, so 0.22.4 leaves the environment inconsistent, and this branch's `uv pip check` step fails on it. Master has no such step, which is why it never noticed. what: - Restore `docutils>=0.20.1,<0.22` - Return the matrix to docutils 0.20.1 and 0.21.2 - Drop the Python 3.10 exclusion, which only existed to keep 3.10 away from docutils 0.22 and cannot trigger under the ceiling Verified `uv pip check` clean and 227 tests passing on docutils 0.20.1 and 0.21.2 against pytest 8.4.2 and 9.1.1.
Member
Author
|
Duplicate — #93 already tracks this branch. The rebase onto master and the notes I wrote up here apply there; closing this in favour of it. |
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.
Spike branch, rebased onto master. Opening it for visibility — not ready to merge, see the failures below.
What's here
30 commits: ADRs recording the architecture decision and the research behind it, a new typed
doctest_corepackage (project,registry,runner,settings), anddoctest_docutils/pytest_doctest_docutilsrecomposed on top of it. ~10k insertions across 57 files.Rebase notes
Three files conflicted; the rest applied clean.
pyproject.toml— took master'spytest>=8.3.3over the branch'spytest>=7.2. That floor is why the branch's 9 mypy errors are gone: they were all pytest-7 legs, and pytest 7 is no longer supported. Kept the branch's added runtime deps (packaging,myst-parser>=2.0.0) and its tighterdocutils>=0.20.1.Dropped the branch's
docutils<0.22cap. Master now tests docutils 0.22.4, and it works here too — verified0.20.1/0.22.4against pytest8.4.2/9.1.1, all four identical. Restore the cap if it was pinning something the ADRs depend on that I missed..github/workflows/tests.yml— merged both sides:include:mapping of pytest topytest-asyncio/pytest-rerunfailures, minus the pytest 7.2.0 rowuv pip checkstepUV_PYTHONand--no-sync; deduplicated to oneenv:blockuv.lock— regenerated rather than hand-merged.CI will be red: 2 pre-existing failures
Both assert an absolute path and get a relative one (
../../../../../pytest-of-d/...vs/tmp/pytest-of-d/...). Not caused by the rebase — confirmed by running the same two tests on the pre-rebase branch at identical versions, where they fail the same way. Not an environment artifact either; they fail with--basetempinside the checkout.Left alone deliberately: this is in-flight spike work, not rebase fallout.
State
backup/spike-pre-rebase(8b887ad)Worth deciding
src/_pytest_doctest_compat.pystill carries pytest 7 branches. With the floor at 8.3.3 they're unreachable — that shim is now dead code and can likely go.