Skip to content

Commit 245a8c2

Browse files
authored
Revamp good practices (#10206)
* Recommend importlib import mode for new projects * Recommend src layout more strongly * Switch to hatchling as the packaging tool in the example (following PyPA) * Add explanation about the different import modes
1 parent a9bbfb8 commit 245a8c2

File tree

3 files changed

+82
-101
lines changed

3 files changed

+82
-101
lines changed

doc/en/conf.py

+1
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@
393393
"tox": ("https://tox.wiki/en/stable", None),
394394
"virtualenv": ("https://virtualenv.pypa.io/en/stable", None),
395395
"setuptools": ("https://setuptools.pypa.io/en/stable", None),
396+
"packaging": ("https://packaging.python.org/en/latest", None),
396397
}
397398

398399

doc/en/explanation/goodpractices.rst

+79-89
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,26 @@ For development, we recommend you use :mod:`venv` for virtual environments and
1212
as well as the ``pytest`` package itself.
1313
This ensures your code and dependencies are isolated from your system Python installation.
1414

15-
Next, place a ``pyproject.toml`` file in the root of your package:
15+
Create a ``pyproject.toml`` file in the root of your repository as described in
16+
:doc:`packaging:tutorials/packaging-projects`.
17+
The first few lines should look like this:
1618

1719
.. code-block:: toml
1820
1921
[build-system]
20-
requires = ["setuptools >= 42"]
21-
build-backend = "setuptools.build_meta"
22-
23-
and a ``setup.cfg`` file containing your package's metadata with the following minimum content:
24-
25-
.. code-block:: ini
22+
requires = ["hatchling"]
23+
build-backend = "hatchling.build"
2624
2725
[metadata]
28-
name = PACKAGENAME
29-
30-
[options]
31-
packages = find:
26+
name = "PACKAGENAME"
3227
3328
where ``PACKAGENAME`` is the name of your package.
3429

35-
.. note::
36-
37-
If your pip version is older than ``21.3``, you'll also need a ``setup.py`` file:
38-
39-
.. code-block:: python
40-
41-
from setuptools import setup
42-
43-
setup()
44-
4530
You can then install your package in "editable" mode by running from the same directory:
4631

4732
.. code-block:: bash
4833
49-
pip install -e .
34+
pip install -e .
5035
5136
which lets you change your source code (both tests and application) and rerun tests at will.
5237

@@ -89,11 +74,11 @@ to keep tests separate from actual application code (often a good idea):
8974
.. code-block:: text
9075
9176
pyproject.toml
92-
setup.cfg
93-
mypkg/
94-
__init__.py
95-
app.py
96-
view.py
77+
src/
78+
mypkg/
79+
__init__.py
80+
app.py
81+
view.py
9782
tests/
9883
test_app.py
9984
test_view.py
@@ -112,72 +97,28 @@ This has the following benefits:
11297
See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and
11398
``python -m pytest``.
11499

115-
Note that this scheme has a drawback if you are using ``prepend`` :ref:`import mode <import-modes>`
116-
(which is the default): your test files must have **unique names**, because
117-
``pytest`` will import them as *top-level* modules since there are no packages
118-
to derive a full package name from. In other words, the test files in the example above will
119-
be imported as ``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to
120-
``sys.path``.
100+
For new projects, we recommend to use ``importlib`` :ref:`import mode <import-modes>`
101+
(see which-import-mode_ for a detailed explanation).
102+
To this end, add the following to your ``pyproject.toml``:
121103

122-
If you need to have test modules with the same name, you might add ``__init__.py`` files to your
123-
``tests`` folder and subfolders, changing them to packages:
124-
125-
.. code-block:: text
104+
.. code-block:: toml
126105
127-
pyproject.toml
128-
setup.cfg
129-
mypkg/
130-
...
131-
tests/
132-
__init__.py
133-
foo/
134-
__init__.py
135-
test_view.py
136-
bar/
137-
__init__.py
138-
test_view.py
106+
[tool.pytest.ini_options]
107+
addopts = [
108+
"--import-mode=importlib",
109+
]
139110
140-
Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test_view``, allowing
141-
you to have modules with the same name. But now this introduces a subtle problem: in order to load
142-
the test modules from the ``tests`` directory, pytest prepends the root of the repository to
143-
``sys.path``, which adds the side-effect that now ``mypkg`` is also importable.
111+
.. _src-layout:
144112

145-
This is problematic if you are using a tool like `tox`_ to test your package in a virtual environment,
146-
because you want to test the *installed* version of your package, not the local code from the repository.
113+
Generally, but especially if you use the default import mode ``prepend``,
114+
it is **strongly** suggested to use a ``src`` layout.
115+
Here, your application root package resides in a sub-directory of your root,
116+
i.e. ``src/mypkg/`` instead of ``mypkg``.
147117

148-
.. _`src-layout`:
118+
This layout prevents a lot of common pitfalls and has many benefits,
119+
which are better explained in this excellent `blog post`_ by Ionel Cristian Mărieș.
149120

150-
In this situation, it is **strongly** suggested to use a ``src`` layout where application root package resides in a
151-
sub-directory of your root:
152-
153-
.. code-block:: text
154-
155-
pyproject.toml
156-
setup.cfg
157-
src/
158-
mypkg/
159-
__init__.py
160-
app.py
161-
view.py
162-
tests/
163-
__init__.py
164-
foo/
165-
__init__.py
166-
test_view.py
167-
bar/
168-
__init__.py
169-
test_view.py
170-
171-
172-
This layout prevents a lot of common pitfalls and has many benefits, which are better explained in this excellent
173-
`blog post by Ionel Cristian Mărieș <https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure>`_.
174-
175-
.. note::
176-
The ``--import-mode=importlib`` option (see :ref:`import-modes`) does not have
177-
any of the drawbacks above because ``sys.path`` is not changed when importing
178-
test modules, so users that run into this issue are strongly encouraged to try it.
179-
180-
The ``src`` directory layout is still strongly recommended however.
121+
.. _blog post: https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure>
181122

182123

183124
Tests as part of application code
@@ -190,8 +131,7 @@ want to distribute them along with your application:
190131
.. code-block:: text
191132
192133
pyproject.toml
193-
setup.cfg
194-
mypkg/
134+
[src/]mypkg/
195135
__init__.py
196136
app.py
197137
view.py
@@ -253,6 +193,56 @@ Note that this layout also works in conjunction with the ``src`` layout mentione
253193
much less surprising.
254194

255195

196+
.. _which-import-mode:
197+
198+
Choosing an import mode
199+
^^^^^^^^^^^^^^^^^^^^^^^
200+
201+
For historical reasons, pytest defaults to the ``prepend`` :ref:`import mode <import-modes>`
202+
instead of the ``importlib`` import mode we recommend for new projects.
203+
The reason lies in the way the ``prepend`` mode works:
204+
205+
Since there are no packages to derive a full package name from,
206+
``pytest`` will import your test files as *top-level* modules.
207+
The test files in the first example (:ref:`src layout <src-layout>`) would be imported as
208+
``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to ``sys.path``.
209+
210+
This results in a drawback compared to the import mode ``importlib``:
211+
your test files must have **unique names**.
212+
213+
If you need to have test modules with the same name,
214+
as a workaround you might add ``__init__.py`` files to your ``tests`` folder and subfolders,
215+
changing them to packages:
216+
217+
.. code-block:: text
218+
219+
pyproject.toml
220+
mypkg/
221+
...
222+
tests/
223+
__init__.py
224+
foo/
225+
__init__.py
226+
test_view.py
227+
bar/
228+
__init__.py
229+
test_view.py
230+
231+
Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test_view``,
232+
allowing you to have modules with the same name.
233+
But now this introduces a subtle problem:
234+
in order to load the test modules from the ``tests`` directory,
235+
pytest prepends the root of the repository to ``sys.path``,
236+
which adds the side-effect that now ``mypkg`` is also importable.
237+
238+
This is problematic if you are using a tool like tox_ to test your package in a virtual environment,
239+
because you want to test the *installed* version of your package,
240+
not the local code from the repository.
241+
242+
The ``importlib`` import mode does not have any of the drawbacks above,
243+
because ``sys.path`` is not changed when importing test modules.
244+
245+
256246
.. _`buildout`: http://www.buildout.org/en/latest/
257247

258248
.. _`use tox`:

doc/en/how-to/fixtures.rst

+2-12
Original file line numberDiff line numberDiff line change
@@ -1766,8 +1766,6 @@ Given the tests file structure is:
17661766
::
17671767

17681768
tests/
1769-
__init__.py
1770-
17711769
conftest.py
17721770
# content of tests/conftest.py
17731771
import pytest
@@ -1782,8 +1780,6 @@ Given the tests file structure is:
17821780
assert username == 'username'
17831781

17841782
subfolder/
1785-
__init__.py
1786-
17871783
conftest.py
17881784
# content of tests/subfolder/conftest.py
17891785
import pytest
@@ -1792,8 +1788,8 @@ Given the tests file structure is:
17921788
def username(username):
17931789
return 'overridden-' + username
17941790

1795-
test_something.py
1796-
# content of tests/subfolder/test_something.py
1791+
test_something_else.py
1792+
# content of tests/subfolder/test_something_else.py
17971793
def test_username(username):
17981794
assert username == 'overridden-username'
17991795

@@ -1809,8 +1805,6 @@ Given the tests file structure is:
18091805
::
18101806

18111807
tests/
1812-
__init__.py
1813-
18141808
conftest.py
18151809
# content of tests/conftest.py
18161810
import pytest
@@ -1852,8 +1846,6 @@ Given the tests file structure is:
18521846
::
18531847

18541848
tests/
1855-
__init__.py
1856-
18571849
conftest.py
18581850
# content of tests/conftest.py
18591851
import pytest
@@ -1890,8 +1882,6 @@ Given the tests file structure is:
18901882
::
18911883

18921884
tests/
1893-
__init__.py
1894-
18951885
conftest.py
18961886
# content of tests/conftest.py
18971887
import pytest

0 commit comments

Comments
 (0)