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)