Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
32 changes: 19 additions & 13 deletions pre_commit_python_eol/check_eol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
"""
Expand All @@ -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).

Expand All @@ -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
Expand All @@ -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
Expand Down
42 changes: 42 additions & 0 deletions tests/test_check_eol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)