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
2 changes: 1 addition & 1 deletion .bumper.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.bumper]
current_version = "2025.12.0"
current_version = "2025.12.1"
versioning_type = "calver"

[[tool.bumper.files]]
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ ci:

repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.11.0
rev: 25.12.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
Expand Down
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 (cache only)
entry: checkeol --cache_only
language: python
files: '^pyproject.toml$'
types: [toml]
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog
Versions follow [Calendar Versioning](https://calver.org/) (`<YYYY>`.`<MM>`.`<MICRO>`)

## [v2025.12.1]
### Added
* #8 Add the `check-eol-cached` hook, which utilizies only the cached EOL information and does not incorporate a date-based check

## [v2025.12.0]
### Fixed
* #6 Fix EOL cache file not being included in source distribution
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,25 @@ Add this to your `.pre-commit-config.yaml`

```yaml
- repo: https://github.com/sco1/pre-commit-python-eol
rev: v2025.12.0
rev: v2025.12.1
hooks:
- id: check-eol
- id: check-eol-cached
```

While both hooks are technically compatible with each other, it's advised to choose a single hook behavior that best fits your needs.

### EOL Status Cache
To avoid requiring network connectivity at runtime, EOL status is cached to [a local JSON file](./pre_commit_python_eol/cached_release_cycle.json) distributed alongside this hook. The cache is updated quarterly & a changed cache will result in a version bump for this hook.

## Hooks
**NOTE:** Only pyproject.toml is currently inspected. It is assumed that project metadata is specified per [PyPA Guidance](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/)
Only `pyproject.toml` is currently inspected. It is assumed that project metadata is specified per [PyPA Guidance](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/)

### `check-eol`
Check `requires-python` against the current Python lifecycle & fail if an EOL version is included.
Check `requires-python` against the current Python lifecycle & fail if an EOL version is included; this includes a date-based check using the system's time for versions that have not yet explicitly been declared EOL.

### `check-eol-cached`
Check `requires-python` against the current Python lifecycle & fail if an EOL version is included; this hook utilizes only the cached release cycle information.

## 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/).
2 changes: 1 addition & 1 deletion pre_commit_python_eol/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = "2025.12.0"
__version__ = "2025.12.1"
__url__ = "https://github.com/sco1/pre-commit-check-eol"
41 changes: 28 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,23 @@ 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, use_system_date: bool) -> bool:
"""
Check if this version is end-of-life.

If `use_system_date` is `True`, an additional date-based check is performed for versions
that are not explicitly EOL.
"""
if self.status == ReleasePhase.EOL:
return True

if use_system_date:
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,12 +117,17 @@ 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, cache_json: Path = CACHED_RELEASE_CYCLE, use_system_date: bool = True
) -> None:
"""
Check the input TOML's `requires-python` for overlap with EOL Python version(s).

If overlap(s) are present, an exception is raised whose message enumerates all EOL Python
versions supported by the TOML file.

If `use_system_date` is `True`, an additional date-based check is performed for versions that
are not explicitly EOL.
"""
with toml_file.open("rb") as f:
contents = tomllib.load(f)
Expand All @@ -116,18 +138,10 @@ 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(use_system_date))
]

if eol_supported:
eol_supported.sort(key=attrgetter("python_ver")) # Sort ascending for error msg generation
Expand All @@ -138,12 +152,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("--cache_only", action="store_true")
args = parser.parse_args(argv)

ec = 0
for file in args.filenames:
try:
check_python_support(file)
check_python_support(file, use_system_date=(not args.cache_only))
except EOLPythonError as e:
print(f"{file}: {e}")
ec = 1
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pre-commit-python-eol"
version = "2025.12.0"
version = "2025.12.1"
description = "A pre-commit hook for enforcing supported Python EOL"
license = "MIT"
license-files = ["LICENSE"]
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_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,
use_system_date=False,
)


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,
use_system_date=False,
)


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,
use_system_date=False,
)

assert str(e.value).endswith("3.8")
Loading