Skip to content

Commit 89ee449

Browse files
authored
Merge pull request #11997 from nicoddemus/11475-importlib
Change importlib to first try to import modules using the standard mechanism
2 parents 8248946 + d6134bc commit 89ee449

16 files changed

+920
-152
lines changed

changelog/11475.feature.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added the new :confval:`consider_namespace_packages` configuration option, defaulting to ``False``.
2+
3+
If set to ``True``, pytest will attempt to identify modules that are part of `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__ when importing modules.

changelog/11475.improvement.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:ref:`--import-mode=importlib <import-mode-importlib>` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails.
2+
3+
This means that installed packages will be imported under their canonical name if possible first, for example ``app.core.models``, instead of having the module name always be derived from their path (for example ``.env310.lib.site_packages.app.core.models``).

doc/en/explanation/goodpractices.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ Within Python modules, ``pytest`` also discovers tests using the standard
6060
:ref:`unittest.TestCase <unittest.TestCase>` subclassing technique.
6161

6262

63-
Choosing a test layout / import rules
64-
-------------------------------------
63+
.. _`test layout`:
64+
65+
Choosing a test layout
66+
----------------------
6567

6668
``pytest`` supports two common test layouts:
6769

doc/en/explanation/pythonpath.rst

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,27 @@ Import modes
1010

1111
pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution.
1212

13-
Importing files in Python (at least until recently) is a non-trivial processes, often requiring
14-
changing :data:`sys.path`. Some aspects of the
13+
Importing files in Python is a non-trivial processes, so aspects of the
1514
import process can be controlled through the ``--import-mode`` command-line flag, which can assume
1615
these values:
1716

17+
.. _`import-mode-prepend`:
18+
1819
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
19-
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
20+
of :py:data:`sys.path` if not already there, and then imported with
21+
the :func:`importlib.import_module <importlib.import_module>` function.
22+
23+
It is highly recommended to arrange your test modules as packages by adding ``__init__.py`` files to your directories
24+
containing tests. This will make the tests part of a proper Python package, allowing pytest to resolve their full
25+
name (for example ``tests.core.test_core`` for ``test_core.py`` inside the ``tests.core`` package).
2026

21-
This requires test module names to be unique when the test directory tree is not arranged in
22-
packages, because the modules will put in :py:data:`sys.modules` after importing.
27+
If the test directory tree is not arranged as packages, then each test file needs to have a unique name
28+
compared to the other test files, otherwise pytest will raise an error if it finds two tests with the same name.
2329

2430
This is the classic mechanism, dating back from the time Python 2 was still supported.
2531

32+
.. _`import-mode-append`:
33+
2634
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
2735
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
2836

@@ -38,32 +46,78 @@ these values:
3846
the tests will run against the installed version
3947
of ``pkg_under_test`` when ``--import-mode=append`` is used whereas
4048
with ``prepend`` they would pick up the local version. This kind of confusion is why
41-
we advocate for using :ref:`src <src-layout>` layouts.
49+
we advocate for using :ref:`src-layouts <src-layout>`.
4250

4351
Same as ``prepend``, requires test module names to be unique when the test directory tree is
4452
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
4553

46-
* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
54+
.. _`import-mode-importlib`:
55+
56+
* ``importlib``: this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules, without changing :py:data:`sys.path`.
57+
58+
Advantages of this mode:
59+
60+
* pytest will not change :py:data:`sys.path` at all.
61+
* Test module names do not need to be unique -- pytest will generate a unique name automatically based on the ``rootdir``.
62+
63+
Disadvantages:
64+
65+
* Test modules can't import each other.
66+
* Testing utility modules in the tests directories (for example a ``tests.helpers`` module containing test-related functions/classes)
67+
are not importable. The recommendation in this case it to place testing utility modules together with the application/library
68+
code, for example ``app.testing.helpers``.
69+
70+
Important: by "test utility modules" we mean functions/classes which are imported by
71+
other tests directly; this does not include fixtures, which should be placed in ``conftest.py`` files, along
72+
with the test modules, and are discovered automatically by pytest.
73+
74+
It works like this:
75+
76+
1. Given a certain module path, for example ``tests/core/test_models.py``, derives a canonical name
77+
like ``tests.core.test_models`` and tries to import it.
4778

48-
For this reason this doesn't require test module names to be unique.
79+
For non-test modules this will work if they are accessible via :py:data:`sys.path`, so
80+
for example ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``.
81+
This is happens when plugins import non-test modules (for example doctesting).
4982

50-
One drawback however is that test modules are non-importable by each other. Also, utility
51-
modules in the tests directories are not automatically importable because the tests directory is no longer
52-
added to :py:data:`sys.path`.
83+
If this step succeeds, the module is returned.
5384

54-
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
55-
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
85+
For test modules, unless they are reachable from :py:data:`sys.path`, this step will fail.
86+
87+
2. If the previous step fails, we import the module directly using ``importlib`` facilities, which lets us import it without
88+
changing :py:data:`sys.path`.
89+
90+
Because Python requires the module to also be available in :py:data:`sys.modules`, pytest derives a unique name for it based
91+
on its relative location from the ``rootdir``, and adds the module to :py:data:`sys.modules`.
92+
93+
For example, ``tests/core/test_models.py`` will end up being imported as the module ``tests.core.test_models``.
94+
95+
.. versionadded:: 6.0
96+
97+
.. note::
98+
99+
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
100+
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
101+
102+
.. note::
103+
104+
By default, pytest will not attempt to resolve namespace packages automatically, but that can
105+
be changed via the :confval:`consider_namespace_packages` configuration variable.
56106

57107
.. seealso::
58108

59109
The :confval:`pythonpath` configuration variable.
60110

111+
The :confval:`consider_namespace_packages` configuration variable.
112+
113+
:ref:`test layout`.
114+
61115

62116
``prepend`` and ``append`` import modes scenarios
63117
-------------------------------------------------
64118

65119
Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to
66-
change ``sys.path`` in order to import test modules or ``conftest.py`` files, and the issues users
120+
change :py:data:`sys.path` in order to import test modules or ``conftest.py`` files, and the issues users
67121
might encounter because of that.
68122

69123
Test modules / ``conftest.py`` files inside packages
@@ -92,7 +146,7 @@ pytest will find ``foo/bar/tests/test_foo.py`` and realize it is part of a packa
92146
there's an ``__init__.py`` file in the same folder. It will then search upwards until it can find the
93147
last folder which still contains an ``__init__.py`` file in order to find the package *root* (in
94148
this case ``foo/``). To load the module, it will insert ``root/`` to the front of
95-
``sys.path`` (if not there already) in order to load
149+
:py:data:`sys.path` (if not there already) in order to load
96150
``test_foo.py`` as the *module* ``foo.bar.tests.test_foo``.
97151

98152
The same logic applies to the ``conftest.py`` file: it will be imported as ``foo.conftest`` module.
@@ -122,8 +176,8 @@ When executing:
122176
123177
pytest will find ``foo/bar/tests/test_foo.py`` and realize it is NOT part of a package given that
124178
there's no ``__init__.py`` file in the same folder. It will then add ``root/foo/bar/tests`` to
125-
``sys.path`` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done
126-
with the ``conftest.py`` file by adding ``root/foo`` to ``sys.path`` to import it as ``conftest``.
179+
:py:data:`sys.path` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done
180+
with the ``conftest.py`` file by adding ``root/foo`` to :py:data:`sys.path` to import it as ``conftest``.
127181

128182
For this reason this layout cannot have test modules with the same name, as they all will be
129183
imported in the global import namespace.
@@ -136,7 +190,7 @@ Invoking ``pytest`` versus ``python -m pytest``
136190
-----------------------------------------------
137191

138192
Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly
139-
equivalent behaviour, except that the latter will add the current directory to ``sys.path``, which
193+
equivalent behaviour, except that the latter will add the current directory to :py:data:`sys.path`, which
140194
is standard ``python`` behavior.
141195

142196
See also :ref:`invoke-python`.

doc/en/reference/reference.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,19 @@ passed multiple times. The expected format is ``name=value``. For example::
12741274
variables, that will be expanded. For more information about cache plugin
12751275
please refer to :ref:`cache_provider`.
12761276

1277+
.. confval:: consider_namespace_packages
1278+
1279+
Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__
1280+
when collecting Python modules. Default is ``False``.
1281+
1282+
Set to ``True`` if you are testing namespace packages installed into a virtual environment and it is important for
1283+
your packages to be imported using their full namespace package name.
1284+
1285+
Only `native namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages>`__
1286+
are supported, with no plans to support `legacy namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#legacy-namespace-packages>`__.
1287+
1288+
.. versionadded:: 8.1
1289+
12771290
.. confval:: console_output_style
12781291

12791292
Sets the console output style while running tests:

src/_pytest/config/__init__.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,8 @@ def _set_initial_conftests(
547547
confcutdir: Optional[Path],
548548
invocation_dir: Path,
549549
importmode: Union[ImportMode, str],
550+
*,
551+
consider_namespace_packages: bool,
550552
) -> None:
551553
"""Load initial conftest files given a preparsed "namespace".
552554
@@ -572,10 +574,20 @@ def _set_initial_conftests(
572574
# Ensure we do not break if what appears to be an anchor
573575
# is in fact a very long option (#10169, #11394).
574576
if safe_exists(anchor):
575-
self._try_load_conftest(anchor, importmode, rootpath)
577+
self._try_load_conftest(
578+
anchor,
579+
importmode,
580+
rootpath,
581+
consider_namespace_packages=consider_namespace_packages,
582+
)
576583
foundanchor = True
577584
if not foundanchor:
578-
self._try_load_conftest(invocation_dir, importmode, rootpath)
585+
self._try_load_conftest(
586+
invocation_dir,
587+
importmode,
588+
rootpath,
589+
consider_namespace_packages=consider_namespace_packages,
590+
)
579591

580592
def _is_in_confcutdir(self, path: Path) -> bool:
581593
"""Whether to consider the given path to load conftests from."""
@@ -593,17 +605,37 @@ def _is_in_confcutdir(self, path: Path) -> bool:
593605
return path not in self._confcutdir.parents
594606

595607
def _try_load_conftest(
596-
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
608+
self,
609+
anchor: Path,
610+
importmode: Union[str, ImportMode],
611+
rootpath: Path,
612+
*,
613+
consider_namespace_packages: bool,
597614
) -> None:
598-
self._loadconftestmodules(anchor, importmode, rootpath)
615+
self._loadconftestmodules(
616+
anchor,
617+
importmode,
618+
rootpath,
619+
consider_namespace_packages=consider_namespace_packages,
620+
)
599621
# let's also consider test* subdirs
600622
if anchor.is_dir():
601623
for x in anchor.glob("test*"):
602624
if x.is_dir():
603-
self._loadconftestmodules(x, importmode, rootpath)
625+
self._loadconftestmodules(
626+
x,
627+
importmode,
628+
rootpath,
629+
consider_namespace_packages=consider_namespace_packages,
630+
)
604631

605632
def _loadconftestmodules(
606-
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
633+
self,
634+
path: Path,
635+
importmode: Union[str, ImportMode],
636+
rootpath: Path,
637+
*,
638+
consider_namespace_packages: bool,
607639
) -> None:
608640
if self._noconftest:
609641
return
@@ -620,7 +652,12 @@ def _loadconftestmodules(
620652
if self._is_in_confcutdir(parent):
621653
conftestpath = parent / "conftest.py"
622654
if conftestpath.is_file():
623-
mod = self._importconftest(conftestpath, importmode, rootpath)
655+
mod = self._importconftest(
656+
conftestpath,
657+
importmode,
658+
rootpath,
659+
consider_namespace_packages=consider_namespace_packages,
660+
)
624661
clist.append(mod)
625662
self._dirpath2confmods[directory] = clist
626663

@@ -642,7 +679,12 @@ def _rget_with_confmod(
642679
raise KeyError(name)
643680

644681
def _importconftest(
645-
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
682+
self,
683+
conftestpath: Path,
684+
importmode: Union[str, ImportMode],
685+
rootpath: Path,
686+
*,
687+
consider_namespace_packages: bool,
646688
) -> types.ModuleType:
647689
conftestpath_plugin_name = str(conftestpath)
648690
existing = self.get_plugin(conftestpath_plugin_name)
@@ -661,7 +703,12 @@ def _importconftest(
661703
pass
662704

663705
try:
664-
mod = import_path(conftestpath, mode=importmode, root=rootpath)
706+
mod = import_path(
707+
conftestpath,
708+
mode=importmode,
709+
root=rootpath,
710+
consider_namespace_packages=consider_namespace_packages,
711+
)
665712
except Exception as e:
666713
assert e.__traceback__ is not None
667714
raise ConftestImportFailure(conftestpath, cause=e) from e
@@ -1177,6 +1224,9 @@ def pytest_load_initial_conftests(self, early_config: "Config") -> None:
11771224
confcutdir=early_config.known_args_namespace.confcutdir,
11781225
invocation_dir=early_config.invocation_params.dir,
11791226
importmode=early_config.known_args_namespace.importmode,
1227+
consider_namespace_packages=early_config.getini(
1228+
"consider_namespace_packages"
1229+
),
11801230
)
11811231

11821232
def _initini(self, args: Sequence[str]) -> None:

src/_pytest/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,12 @@ def pytest_addoption(parser: Parser) -> None:
222222
help="Prepend/append to sys.path when importing test modules and conftest "
223223
"files. Default: prepend.",
224224
)
225+
parser.addini(
226+
"consider_namespace_packages",
227+
type="bool",
228+
default=False,
229+
help="Consider namespace packages when resolving module names during import",
230+
)
225231

226232
group = parser.getgroup("debugconfig", "test session debugging and configuration")
227233
group.addoption(

0 commit comments

Comments
 (0)