diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index b42decc..faac883 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,3 +4,10 @@ language: python files: '^pyproject.toml$' types: [toml] + +- id: check-eol-cached + name: Check supported Python EOL (cached) + entry: checkeol --cached + language: python + files: '^pyproject.toml$' + types: [toml] diff --git a/README.md b/README.md index 1db283a..601437c 100644 --- a/README.md +++ b/README.md @@ -22,5 +22,9 @@ Add this to your `.pre-commit-config.yaml` ### `check-eol` Check `requires-python` against the current Python lifecycle & fail if an EOL version is included. +### `check-eol-cached` +The same as `check-eol`, but deterministic. Caches EOL Python versions, which get updated when +you update the hook. + ## Python Version Support Starting with Python 3.11, a best attempt is made to support Python versions until they reach EOL, after which support will be formally dropped by the next minor or major release of this package, whichever arrives first. The status of Python versions can be found [here](https://devguide.python.org/versions/). diff --git a/pre_commit_python_eol/check_eol.py b/pre_commit_python_eol/check_eol.py index d5ed55d..4e9665d 100644 --- a/pre_commit_python_eol/check_eol.py +++ b/pre_commit_python_eol/check_eol.py @@ -81,6 +81,19 @@ def from_json(cls, ver: str, metadata: dict[str, t.Any]) -> PythonRelease: end_of_life=_parse_eol_date(metadata["end_of_life"]), ) + def is_eol(self, cached: bool) -> bool: + """Check if this version is end-of-life.""" + if self.status == ReleasePhase.EOL: + return True + + # When running cached, don't use the current date, and trust .status + if not cached: + utc_today = dt.datetime.now(dt.timezone.utc).date() + if self.end_of_life <= utc_today: + return True + + return False + def _get_cached_release_cycle(cache_json: Path) -> list[PythonRelease]: """ @@ -100,7 +113,9 @@ def _get_cached_release_cycle(cache_json: Path) -> list[PythonRelease]: ) -def check_python_support(toml_file: Path, cache_json: Path = CACHED_RELEASE_CYCLE) -> None: +def check_python_support( + toml_file: Path, *, cached: bool = False, cache_json: Path = CACHED_RELEASE_CYCLE +) -> None: """ Check the input TOML's `requires-python` for overlap with EOL Python version(s). @@ -116,18 +131,8 @@ def check_python_support(toml_file: Path, cache_json: Path = CACHED_RELEASE_CYCL package_spec = specifiers.SpecifierSet(requires_python) release_cycle = _get_cached_release_cycle(cache_json) - utc_today = dt.datetime.now(dt.timezone.utc).date() - - eol_supported = [] - for r in release_cycle: - if r.python_ver in package_spec: - if r.status == ReleasePhase.EOL: - eol_supported.append(r) - continue - if r.end_of_life <= utc_today: - eol_supported.append(r) - continue + eol_supported = [r for r in release_cycle if r.python_ver in package_spec and r.is_eol(cached)] if eol_supported: eol_supported.sort(key=attrgetter("python_ver")) # Sort ascending for error msg generation @@ -138,12 +143,13 @@ def check_python_support(toml_file: Path, cache_json: Path = CACHED_RELEASE_CYCL def main(argv: abc.Sequence[str] | None = None) -> int: # noqa: D103 parser = argparse.ArgumentParser() parser.add_argument("filenames", nargs="*", type=Path) + parser.add_argument("--cached", action="store_true") args = parser.parse_args(argv) ec = 0 for file in args.filenames: try: - check_python_support(file) + check_python_support(file, cached=args.cached) except EOLPythonError as e: print(f"{file}: {e}") ec = 1 diff --git a/tests/test_check_eol.py b/tests/test_check_eol.py index cf7fc4e..7424c72 100644 --- a/tests/test_check_eol.py +++ b/tests/test_check_eol.py @@ -213,3 +213,45 @@ def test_check_python_support_multi_eol_raises(path_with_cache: tuple[Path, Path check_python_support(pyproject, cache_json=cache_path) assert str(e.value).endswith("3.7, 3.8") + + +def test_check_cached_python_support_no_eol(path_with_cache: tuple[Path, Path]) -> None: + base_path, cache_path = path_with_cache + pyproject = base_path / "pyproject.toml" + pyproject.write_text(SAMPLE_PYPROJECT_NO_EOL) + + check_python_support( + pyproject, + cache_json=cache_path, + cached=True, + ) + + +def test_check_cached_python_support_single_eol_raises(path_with_cache: tuple[Path, Path]) -> None: + base_path, cache_path = path_with_cache + pyproject = base_path / "pyproject.toml" + pyproject.write_text(SAMPLE_PYPROJECT_SINGLE_EOL) + + with pytest.raises(EOLPythonError) as e: + check_python_support( + pyproject, + cache_json=cache_path, + cached=True, + ) + + assert str(e.value).endswith("3.8") + + +def test_check_cached_python_support_single_eol_no_raises_by_date( + path_with_cache: tuple[Path, Path], +) -> None: + base_path, cache_path = path_with_cache + pyproject = base_path / "pyproject.toml" + pyproject.write_text(SAMPLE_PYPROJECT_SINGLE_EOL_BY_DATE) + + with time_machine.travel(dt.date(year=2031, month=11, day=1)): + check_python_support( + pyproject, + cache_json=cache_path, + cached=True, + )