Source code for tests.test_pyproject

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
"""Consistency checks tying the test matrix to the declared dependencies.

The ``click`` version axis of the test matrix is the one dependency whose whole
supported range is exercised release-by-release (a patch can change behavior
mid-stream). These tests keep that axis in sync with the ``click`` dependency
specifier, so a drift, like a new Click release or a raised floor, fails CI
instead of silently rotting.
"""

from __future__ import annotations

import sys
from pathlib import Path

import pytest
import requests
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
from packaging.version import Version

if sys.version_info >= (3, 11):
    import tomllib
else:
    import tomli as tomllib  # type: ignore[import-not-found]

PYPROJECT = Path(__file__).parent.parent / "pyproject.toml"
"""Path to the project's ``pyproject.toml``, relative to this test file."""

SENTINELS = ("released", "stable", "main")
"""Moving-reference values of the ``click-version`` axis: the lockfile-resolved
release, and Click's ``stable`` and ``main`` development branches. Everything
else in the axis is a pinned release number."""


[docs] def load_click_matrix() -> tuple[SpecifierSet, list[str], set[str]]: """Read the Click setup from ``pyproject.toml``. The ``click-version`` axis is spread across the forward-looking ``test-matrix.full-include`` rows (the pinned releases and the ``stable`` / ``main`` sentinels) and the ``released`` default declared in ``test-matrix.include``. :return: the ``click`` dependency specifier, the collected ``click-version`` axis values, and the subset that are pinned release numbers (sentinels start with a letter, pinned versions with a digit). """ config = tomllib.loads(PYPROJECT.read_text(encoding="UTF-8")) click = next( Requirement(dep) for dep in config["project"]["dependencies"] if Requirement(dep).name == "click" ) test_matrix = config["tool"]["repomatic"]["test-matrix"] versions = { row["click-version"] for row in test_matrix.get("full-include", ()) if "click-version" in row } versions.update( directive["click-version"] for directive in test_matrix.get("include", ()) if "click-version" in directive ) pinned = {entry for entry in versions if entry[0].isdigit()} return click.specifier, sorted(versions), pinned
[docs] def stable_pypi_versions(package: str) -> set[Version]: """Return all non-yanked, non-prerelease versions of ``package`` on PyPI.""" response = requests.get(f"https://pypi.org/pypi/{package}/json", timeout=30) response.raise_for_status() versions = set() for release, files in response.json()["releases"].items(): # Skip releases with no distribution files, or whose files are all yanked. if not files or all(f.get("yanked", False) for f in files): continue version = Version(release) if not version.is_prerelease: versions.add(version) return versions
[docs] @pytest.mark.once def test_click_floor_is_pinned_and_sentinels_present(): """The matrix floor stays in sync with the dependency floor (hermetic). The lowest pinned ``click-version`` must equal the lower bound of the ``click`` specifier, so raising or lowering the dependency floor without updating the matrix (or vice versa) fails here. Runs offline, unlike the PyPI cross-check below. """ specifier, variations, pinned = load_click_matrix() assert pinned, "No Click release pinned in test-matrix.full-include." floor = min( Version(spec.version) for spec in specifier if spec.operator in (">=", "==", "~=") ) lowest_pin = min(Version(entry) for entry in pinned) assert lowest_pin == floor, ( f"Lowest pinned Click version ({lowest_pin}) does not match the dependency " f"floor (`click{specifier}` => {floor}). Keep the pinned click-version rows " "in test-matrix.full-include in sync with the click specifier in [project] " "dependencies." ) missing_sentinels = set(SENTINELS) - set(variations) assert not missing_sentinels, ( f"The test matrix dropped the {sorted(missing_sentinels)} click-version " "sentinel(s) from test-matrix.full-include / include; the latest release and " "Click's dev branches would stop being tested." )
[docs] @pytest.mark.once def test_click_matrix_matches_authorized_releases(): """The pinned releases match exactly the Click releases the specifier allows. Every release allowed by the ``click`` specifier must be referenced in the matrix: the newest through the ``released`` sentinel, every earlier one by an explicit pin. This asserts the set equality in both directions: - a **missing** pin means Click published a release the matrix has not caught up to (the previous newest is no longer covered by ``released``); - a **stale** pin means a pinned version is no longer an authorized release (the floor was raised past it, or the release was yanked). Either way, the matrix needs an edit, and this is the signal. """ specifier, variations, pinned = load_click_matrix() try: available = stable_pypi_versions("click") except requests.RequestException as error: pytest.skip(f"Cannot reach PyPI to list Click releases: {error}") authorized = sorted(v for v in available if specifier.contains(v)) assert authorized, f"No Click release satisfies {specifier}." authorized_strings = {str(v) for v in authorized} # The newest authorized release is exercised through the `released` sentinel; # every earlier release must be pinned explicitly. covered = {str(authorized[-1])} if "released" in variations else set() required = authorized_strings - covered missing = required - pinned stale = pinned - authorized_strings assert not (missing or stale), "\n".join( message for message in ( f"Pin these new Click releases as test-matrix.full-include rows: " f"{sorted(missing, key=Version)} (and re-check which release `released` " f"now covers)." if missing else "", f"Drop these stale pins, no longer authorized by `click{specifier}`: " f"{sorted(stale, key=Version)}." if stale else "", ) if message )