Experimental _pytest.scenario API: nested configs + in-memory collection (PoC) - #14809
Experimental _pytest.scenario API: nested configs + in-memory collection (PoC)#14809RonnyPfannschmidt wants to merge 1 commit into
Conversation
…lection pytest's own testsuite is heavily integration-based: most tests write files via pytester and run a full session, then glob-match terminal output. This adds an experimental internal API to test pytest with pytest hermetically instead: * ConfigSpec/configured() build a parsed+configured nested Config from declarative data through the real parse phases -- no rootdir discovery, config files, conftests, plugin autoload, or env vars. Config.parse() is split into behavior-preserving phase methods so the programmatic path shares code with command line parsing, and ArgsSource.SPEC marks verbatim args. * fake_module()/collect_tests()/run_tests() collect test items from in-memory objects (functions, classes, module namespaces) through the standard pytest_collection flow via a ScenarioModule whose _getobj serves the preset object; nodeids derive from synthetic rootdir-relative paths that are never touched on disk. * RunRecord/ItemRunRecord provide typed structured results derived from real report objects via pytest_report_teststatus, with a pytester-compatible assert_outcomes(). The default scenario plugin set deliberately excludes capture, terminal, assertion, cacheprovider and the process-global-state plugins; capture nesting is the documented follow-up. Supporting core fixes: * Class.from_parent() no longer silently discards a passed obj. * get_config() accepts an explicit invocation dir=. * debugging/unittest guard cross-plugin option reads (trace/usepdb) so they work when the debugging plugin is not registered. Validated by 33 self-tests (0.3s, hermeticity asserted) and by converting seven existing tests in testing/python/ to the new API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
714974e to
8845122
Compare
|
Nice initiative. Anything that brings more isolation, hermeticity and control of side-effects seems like a good direction to me. I haven't dug into the code or details yet, so just some small comments: The name "scenario" sounds possibly too generic for this, it doesn't sound like sometime that is intended for testing pytest itself, but like an end-user feature, like pytest-bdd has
This sounds possibly nice but didn't look at the details. In due time, it would be nice to have this in a separate commit or PR to be considered separately.
👍 on the general idea.
What's the reason to use separate types than the TestReport etc. themselves? I do believe pytester has some helpers to grab those, although we don't use them as much as we should. Regarding these:
These all sound (just based on the description) look nice independent improvements. Consider submitting as separate PRs, to reduce the changes here.
These sound like they can share the benefits with the threading work.
Ideally we can have this nice stuff in pytester from the start, instead of a separate API, just to keep things simple for users (mostly plugin authors). But maybe there's good reason to have a separate API, I don't know yet. |
|
This is a initial poc It includes a number of precursor enhancements that need to be extracted and landed first As to why new types/apis - pytester is very intertwined with the problematic way to run things and completely dependent on capture- i wanted to start without that as limit to explore the details first |
Context
pytest's own testsuite is heavily integration-based: of ~3200 real test functions in
testing/, ~61% usepytester, ~91% of those write files to disk, and roughly 1400 assertion sites glob-match rendered terminal output (vs ~270 structured ones). Tests need a full session because stdout is the only observable most of them have — there is no API to collect an item from an in-memory object, or to build aConfigwithout a real rootdir/cwd.testing/conftest.pyeven reorders tests by decompiling__code__.co_namesbecause "uses pytester" is the only available proxy for "slow".This PR is a proof of concept for fixing that at the API level. Draft — the API shape and naming are up for discussion.
What's included
New experimental package
_pytest/scenario/(internal, not exported frompytest):ConfigSpec/configured()— a parsed+configured nestedConfigfrom declarative data, built through the real parse phases but without rootdir discovery, config files, conftests, plugin autoload, or env consultation. Hermetic by default; teardown is paired via_ensure_unconfigure().fake_module(name, *members)— group loose functions/classes into an in-memory module with an explicit name;collect_tests()/run_tests()/ stepwisescenario()collect through the standardpytest_collectionflow (so-k/-m, parametrize, fixture scoping andpytest_collection_modifyitemsall work) via aScenarioModulewhose_getobjserves the preset object. Nodeids derive from synthetic rootdir-relative paths that are never touched on disk.RunRecord/ItemRunRecord— typed results derived from real report objects viapytest_report_teststatus, withassert_outcomes()signature-compatible with pytester and per-test lookup (record["test_x"].setup.longreprtext).Core changes in support:
Config.parse()split into behavior-preserving phase methods (_preparse_addopts,_apply_rootdir,_register_core_ini_options,_load_plugins_phase,_load_initial_conftests_phase,_finalize_parse(decide_args=)), plusArgsSource.SPEC. The programmatic path shares real code with command line parsing instead of bypassing it.Class.from_parent()no longer silently discards a passedobj(it was accepted and dropped).get_config()gained an explicit invocationdir=(was hardcodedPath.cwd()).debugging/unittestguard their cross-plugin option reads (trace/usepdb) so they work when the debugging plugin is not registered.Validation: 33 self-tests in
testing/test_scenario.pyrun in ~0.3s with hermeticity asserted (no sys.path/sys.modules/cwd/environ mutation, no files created); seven existing tests intesting/python/{collect,fixtures,metafunc}.pyconverted, including the hand-rolledmake_functionharness.Deliberate scope cuts (documented follow-ups)
capture,terminal,assertion,cacheproviderand the process-global-state plugins.capsys/capfdinside scenarios are unavailable untilCaptureManagerbecomes stack-aware (suspend/resume is currently absolute, restoring values memoised at__init__— nested managers work only by LIFO luck today). That is the next pillar.load_conftests=True) raisesNotImplementedError; plugin objects viaConfigSpec.extra_pluginsare the replacement.filterwarnings = error) are inherited by scenarios; a scenario'sinicfg={"filterwarnings": [...]}takes precedence.inline_runonto this API, migrate stdout outcome-assertions toRunRecord, and considerpytest.scenariograduation.The changelog fragment is
14809.feature.rst.Per our AI contribution policy: this was researched, designed and implemented with Claude Code under my direction and review.
🤖 Generated with Claude Code