From a6bb4084276cdfc9fd06b8af7c10c906f9dd9af9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 19 Mar 2023 11:35:46 +0100 Subject: [PATCH 1/3] WIP: throw together a initial halfassed proposal on rootspec import mode --- doc/en/proposals/importmode_root_spec.rst | 71 +++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 doc/en/proposals/importmode_root_spec.rst diff --git a/doc/en/proposals/importmode_root_spec.rst b/doc/en/proposals/importmode_root_spec.rst new file mode 100644 index 00000000000..ed8b3ac2881 --- /dev/null +++ b/doc/en/proposals/importmode_root_spec.rst @@ -0,0 +1,71 @@ +:orphan: + +=================================== +PROPOSAL: Parametrize with fixtures +=================================== + +.. warning:: + + This document outlines a proposal around creating a new import mode that supports pep420 and + supports the dissonance between installed and editable installed + + +Problem +======= + +test module discovery in pytest currently is is pre-pep40 and create pains for users +by adding anything things to sys.path and creating easy conflicts in sys.modules + +Additionally this will not solve the dissonance between doctest tests vs installed modules + +The importlib mode in contrast breaks all kinds of expectations by importing test modules in a magical way +that breaks relative imports and leaves them out of ``sys.modules`` + + +Proposed Solution +================= + +A new import mode take a definition of import roots. + +a import root is either a folder in the worktree that will no be installed, +or a folder that will be installed either normally or editable. + +In any case pytest will collect the file tree in the working directory. + +If normal install us used and the content of the imported file differs from the working directory, +pytest will fail collection with a "ContentMissmatch" error. + +When import roots are specified, nothing will be added to sys.path +instead installable content will rely in installs, and local content will create namespace packages that limit the path of subitems + +everything else will be discovered using normal imports + + + + +.. code-block:: + + # contents of pytest.ini + [pytest] + import-roots = + testing/ local + src/mypkg installed as mypkg + # alterntively + src/ installed + +## todo testcases + +* handle install vs editable install + + * when installed - imported name will differe from name in soruce tree + fail collection when content differs + (optional strictly check all files/missing files) + * when installed editable - import using the right name + + * when not installed: + +* testing folders + * if no __init__.py -> create fake namespace, use it for importing + * if __init__.py -> create normal package, dont change sys.path + +* ensure each folder gets a collection package From d77790e8d17b7195478faf908b0bd170884ffa7c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 24 Jul 2026 11:28:08 +0200 Subject: [PATCH 2/3] docs: rewrite import roots proposal against current pytest reality Rename the proposal from importmode_root_spec to import_roots and reposition it as a declarative alternative that replaces --import-mode instead of adding a fourth mode: * update the problem statement for the pytest 8 importlib rework (real module names, sys.modules insertion) and ground it in the modern import system (PEP 420/451/610/660, importlib.metadata) * specify implied roots for simple layouts with a hard failure on ambiguity instead of guessing * specify minimal editable vs real vs stale install classification via PEP 610 direct_url.json, with StaleInstallError on divergence * fix the copy-pasted title and turn the todo notes into a proper test case matrix Co-Authored-By: Claude Fable 5 --- doc/en/proposals/import_roots.rst | 182 ++++++++++++++++++++++ doc/en/proposals/importmode_root_spec.rst | 71 --------- 2 files changed, 182 insertions(+), 71 deletions(-) create mode 100644 doc/en/proposals/import_roots.rst delete mode 100644 doc/en/proposals/importmode_root_spec.rst diff --git a/doc/en/proposals/import_roots.rst b/doc/en/proposals/import_roots.rst new file mode 100644 index 00000000000..b3d7f025324 --- /dev/null +++ b/doc/en/proposals/import_roots.rst @@ -0,0 +1,182 @@ +:orphan: + +====================== +PROPOSAL: Import roots +====================== + +.. warning:: + + This document outlines a proposal for *import roots*: a declarative + replacement for the ``--import-mode`` option that aligns test importing + with the modern Python import system and detects mismatches between the + worktree and installed distributions. + +Problem +------- + +pytest currently offers three import modes, none of which is suitable as a +long-term default: + +* ``prepend`` and ``append`` permanently mutate ``sys.path``. They make the + repository layout dictate importability, allow same-named test modules to + clash, and silently shadow (or get shadowed by) installed distributions — + so it is easy to test a different copy of the code than intended. +* ``importlib`` no longer mutates ``sys.path`` and, since pytest 8, imports + modules under their real name when one can be resolved. However, when + resolution fails it falls back to synthetic module names derived from the + rootdir, and its resolution is *inferred* rather than declared, which keeps + it surprising in non-trivial layouts. + +All three modes share a deeper issue: pytest guesses package roots by walking +up from each file while ``__init__.py`` files are present, anchored on the +rootdir. This inference is the root cause of a long-standing family of +issues: ``ImportPathMismatchError`` between same-named test trees, conftest +modules being imported twice under different names, and the special-casing +needed to keep unrelated ``conftest`` modules from clobbering each other in +``sys.modules``. + +Separately, no mode addresses the dissonance between the worktree and an +installed distribution. When a project is tested against a non-editable +install (for example in a tox environment), a stale install means tests and +doctests may run against code that differs from the files being collected — +with no diagnostic whatsoever. + +Finally, the import machinery that these modes were designed around predates +much of the modern import system. Python has since standardized: + +* namespace packages (:pep:`420`) — packages no longer require + ``__init__.py``, +* spec-based imports (:pep:`451`) — finders, loaders, and + ``ModuleSpec`` as the single source of truth for a module's origin, +* recorded install provenance (:pep:`610`) — ``direct_url.json`` marks + whether a distribution is an editable install, +* standardized editable installs (:pep:`660`), +* ``importlib.metadata`` and ``importlib.resources`` in the standard library + for interrogating installed distributions and their files. + +A modern solution should be expressed in these terms instead of in +``sys.path`` manipulation. + +Proposed solution +----------------- + +Introduce *import roots* as an alternative to import modes — configuring +import roots replaces ``--import-mode`` entirely rather than adding a fourth +mode. + +An import root is a directory in the worktree together with a declaration of +how its content maps onto the import system: + +``local`` + Content that is not distributed (typically test folders). Modules are + imported under names anchored at the root, without any ``sys.path`` + mutation. A folder without ``__init__.py`` becomes a namespace package + whose search path is limited to the root; a folder with ``__init__.py`` + is imported as a regular package. + +``installed`` + The worktree source of a distribution that is installed into the current + environment. Modules are imported under their real, installed name. + pytest classifies the installation (see below) and verifies that what it + collects is what will be imported. + +In all cases pytest collects the file tree of the worktree, never touches +``sys.path``, and resolves imports through the standard spec-based machinery. +Conftest files are imported under proper dotted names derived from their +root, removing the need for ``sys.modules`` special-casing. + +Implied and explicit roots +-------------------------- + +pytest should imply import roots automatically in simple cases, so most +projects need no configuration: + +* an installed (or editable-installed) distribution whose recorded files map + back into the worktree implies an ``installed`` root, +* conventional layouts (``src/`` layout, a ``tests`` folder that is not part + of any distribution) imply their obvious classification. + +When the layout is ambiguous — multiple distributions, overlapping trees, +test folders inside installed packages, or content matching no known +distribution — pytest must not guess. Collection fails with an error that +explains which paths could not be classified and asks for explicit roots: + +.. code-block:: ini + + # contents of pytest.ini + [pytest] + import_roots = + tests local + src/mypkg installed as mypkg + +The configuration syntax shown here is a sketch; the concrete spelling +(including a TOML-native form in ``pyproject.toml``) is an open question. + +Editable versus real versus stale installs +------------------------------------------ + +For ``installed`` roots, pytest performs a minimal classification using +``importlib.metadata`` and :pep:`610` ``direct_url.json``: + +editable install + ``direct_url.json`` marks the distribution as editable. The worktree + itself is the import origin, so collection and import trivially agree. + No content verification is needed. + +real (non-editable) install + The import origin is the installed copy, not the worktree. pytest + verifies that the content of each imported file matches the + corresponding worktree file, and fails collection with a + ``StaleInstallError`` (naming both paths and suggesting a reinstall) + when they differ. An optional strict variant may verify the complete + file set of the distribution, including files missing on either side. + +not installed + A root declared (or implied) as ``installed`` whose distribution cannot + be found fails collection with a clear message, instead of silently + falling back to path-based importing. + +The detection is deliberately minimal: distributions installed through +mechanisms that record no usable provenance are treated best-effort, with an +explicit root declaration as the escape hatch. + +Migration +--------- + +* Configuring import roots and ``--import-mode`` together is an error. +* The long-term goal is for implied import roots to become pytest's default + importing behavior, with the legacy import modes deprecated afterwards — + something none of the existing modes could achieve. +* Collection integrates naturally with the directory collection nodes: + each collected directory belongs to exactly one root, which determines the + module names beneath it. + +Open questions +-------------- + +* the concrete configuration syntax (ini line format versus structured TOML), +* interaction with the ``pythonpath`` ini option, ``--pyargs``, and rootdir, +* which file (worktree or installed copy) appears in tracebacks and reports + for real installs, and how assertion rewriting applies to the installed + origin, +* how much layout inference is acceptable before requiring explicit roots. + +Test cases +---------- + +* real install: imported name differs from the path in the source tree; + collection succeeds when content matches and fails with + ``StaleInstallError`` when it differs; strict mode additionally detects + missing/extra files, +* editable install: modules import under the real name with the worktree as + origin, +* declared ``installed`` root without a matching distribution: collection + fails with a clear message, +* ``local`` root without ``__init__.py``: a namespace package anchored at the + root, with its search path limited to the root, +* ``local`` root with ``__init__.py``: a regular package, still without any + ``sys.path`` mutation, +* ambiguous layouts fail collection with a message naming the unclassified + paths, +* conftest files receive proper dotted module names in every case, +* every collected folder maps to exactly one root. diff --git a/doc/en/proposals/importmode_root_spec.rst b/doc/en/proposals/importmode_root_spec.rst deleted file mode 100644 index ed8b3ac2881..00000000000 --- a/doc/en/proposals/importmode_root_spec.rst +++ /dev/null @@ -1,71 +0,0 @@ -:orphan: - -=================================== -PROPOSAL: Parametrize with fixtures -=================================== - -.. warning:: - - This document outlines a proposal around creating a new import mode that supports pep420 and - supports the dissonance between installed and editable installed - - -Problem -======= - -test module discovery in pytest currently is is pre-pep40 and create pains for users -by adding anything things to sys.path and creating easy conflicts in sys.modules - -Additionally this will not solve the dissonance between doctest tests vs installed modules - -The importlib mode in contrast breaks all kinds of expectations by importing test modules in a magical way -that breaks relative imports and leaves them out of ``sys.modules`` - - -Proposed Solution -================= - -A new import mode take a definition of import roots. - -a import root is either a folder in the worktree that will no be installed, -or a folder that will be installed either normally or editable. - -In any case pytest will collect the file tree in the working directory. - -If normal install us used and the content of the imported file differs from the working directory, -pytest will fail collection with a "ContentMissmatch" error. - -When import roots are specified, nothing will be added to sys.path -instead installable content will rely in installs, and local content will create namespace packages that limit the path of subitems - -everything else will be discovered using normal imports - - - - -.. code-block:: - - # contents of pytest.ini - [pytest] - import-roots = - testing/ local - src/mypkg installed as mypkg - # alterntively - src/ installed - -## todo testcases - -* handle install vs editable install - - * when installed - imported name will differe from name in soruce tree - fail collection when content differs - (optional strictly check all files/missing files) - * when installed editable - import using the right name - - * when not installed: - -* testing folders - * if no __init__.py -> create fake namespace, use it for importing - * if __init__.py -> create normal package, dont change sys.path - -* ensure each folder gets a collection package From e690aff90c9df8ff2b75479950da0bc7f7c9c95e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 24 Jul 2026 11:38:10 +0200 Subject: [PATCH 3/3] docs: specify interaction between import roots and testpaths Collection targets must map to exactly one root, testpaths entries act as an inference signal for local roots, and conftest files above every root are recorded as an open naming question. Co-Authored-By: Claude Fable 5 --- doc/en/proposals/import_roots.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/doc/en/proposals/import_roots.rst b/doc/en/proposals/import_roots.rst index b3d7f025324..d111fcb2771 100644 --- a/doc/en/proposals/import_roots.rst +++ b/doc/en/proposals/import_roots.rst @@ -112,6 +112,35 @@ explains which paths could not be classified and asks for explicit roots: The configuration syntax shown here is a sketch; the concrete spelling (including a TOML-native form in ``pyproject.toml``) is an open question. +Interaction with ``testpaths`` +------------------------------ + +``testpaths`` and import roots answer different questions and stay separate: +``testpaths`` selects *what* is collected when no arguments are given, import +roots declare *how* collected files are imported. They interact in defined +ways: + +* every collection target — whether it comes from ``testpaths``, command line + arguments, or full-tree collection — must fall under exactly one import + root. A target that maps to no root is reported with the same + unclassified-path error as any other ambiguity, instead of falling back to + ``sys.path`` guessing. +* ``testpaths`` entries serve as an additional signal for implied roots: an + entry that is not part of any installed distribution implies a ``local`` + root. The common ``testpaths = tests`` layout therefore needs no explicit + root configuration. +* ``testpaths`` may point into an ``installed`` root — for example + ``testpaths = src`` together with ``--doctest-modules``. Collection still + walks the worktree while imports resolve to the installed name, so the + staleness verification applies exactly as for test-driven imports. +* command line arguments outside ``testpaths`` but inside a declared or + implied root behave normally; arguments outside every root fail with the + unclassified-path error. +* conftest files between the rootdir and the ``testpaths`` entries are + loaded today without belonging to any test package; under import roots + these files still participate and need a defined module name (see the + open questions). + Editable versus real versus stale installs ------------------------------------------ @@ -156,6 +185,8 @@ Open questions * the concrete configuration syntax (ini line format versus structured TOML), * interaction with the ``pythonpath`` ini option, ``--pyargs``, and rootdir, +* the module name for conftest files that live above every import root + (for example a ``conftest.py`` next to ``pyproject.toml``), * which file (worktree or installed copy) appears in tracebacks and reports for real installs, and how assertion rewriting applies to the installed origin,