Skip to content

Commit 7308bc7

Browse files
committed
feat: Add check-eol-cached hook
1 parent accad06 commit 7308bc7

File tree

4 files changed

+72
-13
lines changed

4 files changed

+72
-13
lines changed

.pre-commit-hooks.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,10 @@
44
language: python
55
files: '^pyproject.toml$'
66
types: [toml]
7+
8+
- id: check-eol-cached
9+
name: Check supported Python EOL (cached)
10+
entry: checkeol --cached
11+
language: python
12+
files: '^pyproject.toml$'
13+
types: [toml]

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,9 @@ Add this to your `.pre-commit-config.yaml`
2222
### `check-eol`
2323
Check `requires-python` against the current Python lifecycle & fail if an EOL version is included.
2424

25+
### `check-eol-cached`
26+
The same as `check-eol`, but deterministic. Caches EOL Python versions, which get updated when
27+
you update the hook.
28+
2529
## Python Version Support
2630
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/).

pre_commit_python_eol/check_eol.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ def from_json(cls, ver: str, metadata: dict[str, t.Any]) -> PythonRelease:
8181
end_of_life=_parse_eol_date(metadata["end_of_life"]),
8282
)
8383

84+
def is_eol(self, cached: bool) -> bool:
85+
"""Check if this version is end-of-life."""
86+
if self.status == ReleasePhase.EOL:
87+
return True
88+
89+
# When running cached, don't use the current date, and trust .status
90+
if not cached:
91+
utc_today = dt.datetime.now(dt.timezone.utc).date()
92+
if self.end_of_life <= utc_today:
93+
return True
94+
95+
return False
96+
8497

8598
def _get_cached_release_cycle(cache_json: Path) -> list[PythonRelease]:
8699
"""
@@ -100,7 +113,9 @@ def _get_cached_release_cycle(cache_json: Path) -> list[PythonRelease]:
100113
)
101114

102115

103-
def check_python_support(toml_file: Path, cache_json: Path = CACHED_RELEASE_CYCLE) -> None:
116+
def check_python_support(
117+
toml_file: Path, *, cached: bool = False, cache_json: Path = CACHED_RELEASE_CYCLE
118+
) -> None:
104119
"""
105120
Check the input TOML's `requires-python` for overlap with EOL Python version(s).
106121
@@ -116,18 +131,8 @@ def check_python_support(toml_file: Path, cache_json: Path = CACHED_RELEASE_CYCL
116131

117132
package_spec = specifiers.SpecifierSet(requires_python)
118133
release_cycle = _get_cached_release_cycle(cache_json)
119-
utc_today = dt.datetime.now(dt.timezone.utc).date()
120-
121-
eol_supported = []
122-
for r in release_cycle:
123-
if r.python_ver in package_spec:
124-
if r.status == ReleasePhase.EOL:
125-
eol_supported.append(r)
126-
continue
127134

128-
if r.end_of_life <= utc_today:
129-
eol_supported.append(r)
130-
continue
135+
eol_supported = [r for r in release_cycle if r.python_ver in package_spec and r.is_eol(cached)]
131136

132137
if eol_supported:
133138
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
138143
def main(argv: abc.Sequence[str] | None = None) -> int: # noqa: D103
139144
parser = argparse.ArgumentParser()
140145
parser.add_argument("filenames", nargs="*", type=Path)
146+
parser.add_argument("--cached", action="store_true")
141147
args = parser.parse_args(argv)
142148

143149
ec = 0
144150
for file in args.filenames:
145151
try:
146-
check_python_support(file)
152+
check_python_support(file, cached=args.cached)
147153
except EOLPythonError as e:
148154
print(f"{file}: {e}")
149155
ec = 1

tests/test_check_eol.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,45 @@ def test_check_python_support_multi_eol_raises(path_with_cache: tuple[Path, Path
213213
check_python_support(pyproject, cache_json=cache_path)
214214

215215
assert str(e.value).endswith("3.7, 3.8")
216+
217+
218+
def test_check_cached_python_support_no_eol(path_with_cache: tuple[Path, Path]) -> None:
219+
base_path, cache_path = path_with_cache
220+
pyproject = base_path / "pyproject.toml"
221+
pyproject.write_text(SAMPLE_PYPROJECT_NO_EOL)
222+
223+
check_python_support(
224+
pyproject,
225+
cache_json=cache_path,
226+
cached=True,
227+
)
228+
229+
230+
def test_check_cached_python_support_single_eol_raises(path_with_cache: tuple[Path, Path]) -> None:
231+
base_path, cache_path = path_with_cache
232+
pyproject = base_path / "pyproject.toml"
233+
pyproject.write_text(SAMPLE_PYPROJECT_SINGLE_EOL)
234+
235+
with pytest.raises(EOLPythonError) as e:
236+
check_python_support(
237+
pyproject,
238+
cache_json=cache_path,
239+
cached=True,
240+
)
241+
242+
assert str(e.value).endswith("3.8")
243+
244+
245+
def test_check_cached_python_support_single_eol_no_raises_by_date(
246+
path_with_cache: tuple[Path, Path],
247+
) -> None:
248+
base_path, cache_path = path_with_cache
249+
pyproject = base_path / "pyproject.toml"
250+
pyproject.write_text(SAMPLE_PYPROJECT_SINGLE_EOL_BY_DATE)
251+
252+
with time_machine.travel(dt.date(year=2031, month=11, day=1)):
253+
check_python_support(
254+
pyproject,
255+
cache_json=cache_path,
256+
cached=True,
257+
)

0 commit comments

Comments
 (0)