First attempt to overhaul goodpractices.rst

This commit is contained in:
Philipp A 2022-08-19 14:47:23 +02:00
parent b3c1377693
commit 6c60840aae
2 changed files with 65 additions and 69 deletions

View File

@ -393,6 +393,7 @@ intersphinx_mapping = {
"tox": ("https://tox.wiki/en/stable", None), "tox": ("https://tox.wiki/en/stable", None),
"virtualenv": ("https://virtualenv.pypa.io/en/stable", None), "virtualenv": ("https://virtualenv.pypa.io/en/stable", None),
"setuptools": ("https://setuptools.pypa.io/en/stable", None), "setuptools": ("https://setuptools.pypa.io/en/stable", None),
"packaging": ("https://packaging.python.org/en/latest", None),
} }

View File

@ -12,41 +12,26 @@ For development, we recommend you use :mod:`venv` for virtual environments and
as well as the ``pytest`` package itself. as well as the ``pytest`` package itself.
This ensures your code and dependencies are isolated from your system Python installation. This ensures your code and dependencies are isolated from your system Python installation.
Next, place a ``pyproject.toml`` file in the root of your package: Create a ``pyproject.toml`` file in the root of your repository as described in
:doc:`packaging:tutorials/packaging-projects`.
The first few lines should look like this:
.. code-block:: toml .. code-block:: toml
[build-system] [build-system]
requires = ["setuptools>=42", "wheel"] requires = ["hatchling"]
build-backend = "setuptools.build_meta" build-backend = "hatchling.build"
and a ``setup.cfg`` file containing your package's metadata with the following minimum content:
.. code-block:: ini
[metadata] [metadata]
name = PACKAGENAME name = "PACKAGENAME"
[options]
packages = find:
where ``PACKAGENAME`` is the name of your package. where ``PACKAGENAME`` is the name of your package.
.. note::
If your pip version is older than ``21.3``, you'll also need a ``setup.py`` file:
.. code-block:: python
from setuptools import setup
setup()
You can then install your package in "editable" mode by running from the same directory: You can then install your package in "editable" mode by running from the same directory:
.. code-block:: bash .. code-block:: bash
pip install -e . pip install -e .
which lets you change your source code (both tests and application) and rerun tests at will. which lets you change your source code (both tests and application) and rerun tests at will.
@ -112,43 +97,24 @@ This has the following benefits:
See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and
``python -m pytest``. ``python -m pytest``.
Note that this scheme has a drawback if you are using ``prepend`` :ref:`import mode <import-modes>` For the most convenient experience,
(which is the default): your test files must have **unique names**, because choose the ``import-lib`` :ref:`import mode <import-modes>`.
``pytest`` will import them as *top-level* modules since there are no packages To this end, add the following to your ``pyproject.toml``:
to derive a full package name from. In other words, the test files in the example above will
be imported as ``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to
``sys.path``.
If you need to have test modules with the same name, you might add ``__init__.py`` files to your .. code-block:: toml
``tests`` folder and subfolders, changing them to packages:
.. code-block:: text [tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
]
pyproject.toml The default :ref:`import mode <import-modes>` ``prepend`` has several drawbacks [1]_.
setup.cfg
mypkg/
...
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test_view``, allowing
you to have modules with the same name. But now this introduces a subtle problem: in order to load
the test modules from the ``tests`` directory, pytest prepends the root of the repository to
``sys.path``, which adds the side-effect that now ``mypkg`` is also importable.
This is problematic if you are using a tool like `tox`_ to test your package in a virtual environment,
because you want to test the *installed* version of your package, not the local code from the repository.
.. _`src-layout`: .. _`src-layout`:
In this situation, it is **strongly** suggested to use a ``src`` layout where application root package resides in a Generally, but especially if you use the default import mode,
sub-directory of your root: it is **strongly** suggested to use a ``src`` layout.
Here, your application root package resides in a sub-directory of your root:
.. code-block:: text .. code-block:: text
@ -159,26 +125,11 @@ sub-directory of your root:
__init__.py __init__.py
app.py app.py
view.py view.py
tests/ tests/...
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
This layout prevents a lot of common pitfalls and has many benefits, which are better explained in this excellent This layout prevents a lot of common pitfalls and has many benefits, which are better explained in this excellent
`blog post by Ionel Cristian Mărieș <https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure>`_. `blog post by Ionel Cristian Mărieș <https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure>`_.
.. note::
The ``--import-mode=importlib`` option (see :ref:`import-modes`) does not have
any of the drawbacks above because ``sys.path`` is not changed when importing
test modules, so users that run into this issue are strongly encouraged to try it.
The ``src`` directory layout is still strongly recommended however.
Tests as part of application code Tests as part of application code
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -285,3 +236,47 @@ See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`
setuptools intends to setuptools intends to
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_. `remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
.. [1] The default ``prepend`` :ref:`import mode <import-modes>` works as follows:
Since there are no packages to derive a full package name from,
``pytest`` will import your test files as *top-level* modules.
The test files in the first example would be imported as
``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to ``sys.path``.
This results in the following drawback compared to the import mode ``import-lib``:
your test files must have **unique names**.
If you need to have test modules with the same name,
as a workaround you might add ``__init__.py`` files to your ``tests`` folder and subfolders,
changing them to packages:
.. code-block:: text
pyproject.toml
setup.cfg
mypkg/
...
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test_view``,
allowing you to have modules with the same name.
But now this introduces a subtle problem:
in order to load the test modules from the ``tests`` directory,
pytest prepends the root of the repository to ``sys.path``,
which adds the side-effect that now ``mypkg`` is also importable.
This is problematic if you are using a tool like `tox`_ to test your package in a virtual environment,
because you want to test the *installed* version of your package,
not the local code from the repository.
The ``importlib`` import mode does not have any of the drawbacks above,
because ``sys.path`` is not changed when importing test modules.