Source code for mafw.devtools.toolchain.tools.pytest
# Copyright 2026 European Union
# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
# SPDX-License-Identifier: EUPL-1.2
"""
Concrete :class:`~mafw.devtools.toolchain.ProjectTool` implementation for **pytest**.
pytest is a project tool whose version lower bound is declared in
``project.optional-dependencies.test``. This module manages the ``>=``
specifier for pytest and validates updates by running the full test suite
via hatch.
Update logic (inherited from :class:`~mafw.devtools.toolchain.ProjectTool`):
1. Query PyPI for the latest stable pytest release.
2. Compare against the current lower bound in pyproject.toml.
3. If a newer version exists, update the ``>=`` specifier.
Post-update validation (overridden here):
4. Recreate the ``test`` hatch environment to force fresh dependency resolution.
5. Determine the highest supported Python version from ``tool.mafw.supported-python``.
6. Run the test suite via ``hatch test -py <version> --without-integration --without-slow-integration``.
7. If the test suite fails, revert pyproject.toml to its previous content.
The revert pattern uses ``_revert()``
which restores the raw pyproject.toml content saved before the update.
"""
from __future__ import annotations
import subprocess
from mafw.devtools import DevtoolsError
from mafw.devtools.toolchain import ProjectTool
[docs]
class PytestTool(ProjectTool):
"""Manage pytest's version lower bound in pyproject.toml.
pytest is categorized as a *project* tool because its version is
controlled through the ``test`` optional-dependencies group in
``pyproject.toml``. After an update, the ``test`` hatch environment
is recreated and the full test suite is executed at the highest
supported Python version to validate compatibility. If the tests
fail, pyproject.toml is reverted to its pre-update state.
All standard behaviour (version detection, update, verify, bootstrap)
is provided by :class:`~mafw.devtools.toolchain.ProjectTool`.
Only :meth:`post_update` is overridden to run the test suite.
:param project_root: Path to the project root directory containing
``pyproject.toml``. Defaults to the current working directory.
:type project_root: Path | None
"""
@property
def package_name(self) -> str:
"""PyPI package name.
:return: ``"pytest"``
:rtype: str
"""
return 'pytest'
@property
def section_path(self) -> str:
"""TOML section path where pytest is declared.
:return: ``"project.optional-dependencies.test"``
:rtype: str
"""
return 'project.optional-dependencies.test'
@property
def env_name(self) -> str:
"""Hatch environment where pytest is validated.
:return: ``"hatch-test"``
:rtype: str
"""
return 'hatch-test'
[docs]
def post_update(self) -> None:
"""Recreate the test environment, then run the test suite; revert on failure.
Steps:
1. Force recreation of the ``test`` hatch environment so that the
newly specified pytest version is resolved and installed fresh.
2. Determine the highest supported Python version dynamically from
``tool.mafw.supported-python`` in pyproject.toml.
3. Execute ``hatch test -py <version> --without-integration
--without-slow-integration`` from the project root.
4. If the test suite fails (non-zero exit code), attempt to extract
the conflicting package name via ``_parse_resolution_failure``,
revert pyproject.toml, and raise a descriptive error.
:raises DevtoolsError: If the test suite fails after the update, or
if environment recreation fails due to a resolution conflict.
"""
# Force fresh dependency resolution with the new pytest version.
# Determine the highest supported Python version dynamically.
py_version = self._get_highest_python_version()
# Validate dependency resolution via temporary clone.
self._recreate_environment(self.env_name, py_version)
# Run the test suite at the highest supported Python version.
result = subprocess.run(
[ # noqa: S603, S607
'hatch',
'test',
'-py',
py_version,
'--without-integration',
'--without-slow-integration',
],
capture_output=True,
text=True,
check=False,
cwd=self._project_root,
)
if result.returncode != 0:
# Try to identify which package caused a resolution failure.
conflict = self._parse_resolution_failure(result.stderr)
# Revert pyproject.toml to its pre-update state.
self._revert()
msg = f'Test suite failed after updating {self.name} — pyproject.toml has been reverted.'
if conflict:
msg += f'\nConflicting package: {conflict}'
msg += f'\n{result.stderr.strip()}'
raise DevtoolsError(msg)