Skip to content

Commit 0eb86b1

Browse files
committed
feat: Add check-eol-cached hook
1 parent accad06 commit 0eb86b1

File tree

5 files changed

+124
-29
lines changed

5 files changed

+124
-29
lines changed

.github/workflows/bump_cache.yml

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ name: Check Cached Release Cycle
33
on:
44
workflow_dispatch:
55
schedule:
6-
- cron: "0 14 14 1,4,7,10 *"
6+
# EOLs are currently scheduled for 20xx-10-01
7+
- cron: "0 14 2 1,4,7,10 *"
78

89
jobs:
910
bump-cache:
@@ -40,12 +41,14 @@ jobs:
4041
echo EOF
4142
} >> "$GITHUB_OUTPUT"
4243
43-
- name: Open Issue If Bumped
44-
if: ${{ steps.bump.outputs.RES != ''}}
45-
env:
46-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47-
run: |
48-
today=$(date +"%Y-%m-%d")
49-
gh issue create \
50-
--title "($today) Release Cycle Cache Outdated" \
51-
--body '\`\`\`diff\n${{ steps.report.outputs.RES }}\n\`\`\`'
44+
- name: Set Date
45+
id: date
46+
run:
47+
echo "today=$(date +"%Y-%m-%d")" >> "$GITHUB_OUTPUT"
48+
49+
- name: Create Pull Request If Bumped
50+
uses: peter-evans/create-pull-request@v7
51+
with:
52+
branch: "bump-cache"
53+
title: "chore: update release cycle cache (${{ steps.date.outputs.today }})"
54+
commit-message: "chore: update release cycle cache (${{ steps.date.outputs.today }})"

.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]

pre_commit_python_eol/bump_cache.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import json
22
import platform
3-
from pathlib import Path
43

54
from pre_commit_python_eol import __url__, __version__
5+
from pre_commit_python_eol.check_eol import CACHED_EOL_VERSIONS as LOCAL_CACHE_EOL_VERSIONS
6+
from pre_commit_python_eol.check_eol import CACHED_RELEASE_CYCLE as LOCAL_CACHE_CYCLE
7+
from pre_commit_python_eol.check_eol import (
8+
get_eol_versions,
9+
)
610

711
try:
812
import httpx
@@ -18,21 +22,30 @@
1822
)
1923

2024
CACHE_SOURCE = "https://peps.python.org/api/release-cycle.json"
21-
LOCAL_CACHE = Path("./pre_commit_python_eol/cached_release_cycle.json")
2225

2326

24-
def bump_cache() -> None:
27+
def bump_cached_release_cycle() -> None:
2528
"""Update the cached release cycle JSON from the source repository."""
2629
with httpx.Client(headers={"User-Agent": USER_AGENT}) as client:
2730
r = client.get(CACHE_SOURCE)
2831
r.raise_for_status()
2932

3033
rj = r.json()
3134

32-
with LOCAL_CACHE.open("w", encoding="utf8") as f:
35+
with LOCAL_CACHE_CYCLE.open("w", encoding="utf8") as f:
3336
json.dump(rj, f, indent=2, ensure_ascii=False)
3437
f.write("\n") # Add in trailing newline
3538

3639

40+
def bump_cached_eol_versions() -> None:
41+
"""Update the cached EOL versions JSON from the source repository."""
42+
eol_versions = dict(version.to_json() for version in get_eol_versions())
43+
44+
with LOCAL_CACHE_EOL_VERSIONS.open("w", encoding="utf8") as f:
45+
json.dump(eol_versions, f, indent=2, ensure_ascii=False)
46+
f.write("\n") # Add in trailing newline
47+
48+
3749
if __name__ == "__main__":
38-
bump_cache()
50+
bump_cached_release_cycle()
51+
bump_cached_eol_versions()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"3.9": {
3+
"status": "end-of-life",
4+
"end_of_life": "2025-10-31"
5+
},
6+
"3.8": {
7+
"status": "end-of-life",
8+
"end_of_life": "2024-10-07"
9+
},
10+
"3.7": {
11+
"status": "end-of-life",
12+
"end_of_life": "2023-06-27"
13+
},
14+
"3.6": {
15+
"status": "end-of-life",
16+
"end_of_life": "2021-12-23"
17+
},
18+
"3.5": {
19+
"status": "end-of-life",
20+
"end_of_life": "2020-09-30"
21+
},
22+
"3.4": {
23+
"status": "end-of-life",
24+
"end_of_life": "2019-03-18"
25+
},
26+
"3.3": {
27+
"status": "end-of-life",
28+
"end_of_life": "2017-09-29"
29+
},
30+
"3.2": {
31+
"status": "end-of-life",
32+
"end_of_life": "2016-02-20"
33+
},
34+
"3.1": {
35+
"status": "end-of-life",
36+
"end_of_life": "2012-04-09"
37+
},
38+
"3.0": {
39+
"status": "end-of-life",
40+
"end_of_life": "2009-06-27"
41+
},
42+
"2.7": {
43+
"status": "end-of-life",
44+
"end_of_life": "2020-01-01"
45+
},
46+
"2.6": {
47+
"status": "end-of-life",
48+
"end_of_life": "2013-10-29"
49+
}
50+
}

pre_commit_python_eol/check_eol.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from packaging import specifiers, version
1616

1717
CACHED_RELEASE_CYCLE = Path(__file__).parent / "cached_release_cycle.json"
18+
CACHED_EOL_VERSIONS = Path(__file__).parent / "cached_eol_versions.json"
1819

1920

2021
class EOLPythonError(Exception): ... # noqa: D101
@@ -81,6 +82,13 @@ def from_json(cls, ver: str, metadata: dict[str, t.Any]) -> PythonRelease:
8182
end_of_life=_parse_eol_date(metadata["end_of_life"]),
8283
)
8384

85+
def to_json(self) -> tuple[str, dict[str, str]]:
86+
"""Recreates the original JSON object from the `PythonRelease` instance."""
87+
return str(self.python_ver), {
88+
"status": self.status.value,
89+
"end_of_life": self.end_of_life.isoformat(),
90+
}
91+
8492

8593
def _get_cached_release_cycle(cache_json: Path) -> list[PythonRelease]:
8694
"""
@@ -100,7 +108,27 @@ def _get_cached_release_cycle(cache_json: Path) -> list[PythonRelease]:
100108
)
101109

102110

103-
def check_python_support(toml_file: Path, cache_json: Path = CACHED_RELEASE_CYCLE) -> None:
111+
def get_cached_eol_versions() -> abc.Iterator[PythonRelease]:
112+
"""Parse the locally cached EOL Python versions into `PythonRelease` instance(s)."""
113+
with CACHED_EOL_VERSIONS.open("r", encoding="utf-8") as f:
114+
contents = json.load(f)
115+
116+
return (PythonRelease.from_json(v, m) for v, m in contents.items())
117+
118+
119+
def get_eol_versions(cache_json: Path = CACHED_RELEASE_CYCLE) -> abc.Iterator[PythonRelease]:
120+
"""Enumerate all EOL Python versions."""
121+
release_cycle = _get_cached_release_cycle(cache_json)
122+
utc_today = dt.datetime.now(dt.timezone.utc).date()
123+
124+
for r in release_cycle:
125+
if r.status == ReleasePhase.EOL or r.end_of_life <= utc_today:
126+
yield r
127+
128+
129+
def check_python_support(
130+
toml_file: Path, *, cached: bool = False, cache_json: Path = CACHED_RELEASE_CYCLE
131+
) -> None:
104132
"""
105133
Check the input TOML's `requires-python` for overlap with EOL Python version(s).
106134
@@ -115,19 +143,12 @@ def check_python_support(toml_file: Path, cache_json: Path = CACHED_RELEASE_CYCL
115143
raise RequiresPythonNotFoundError
116144

117145
package_spec = specifiers.SpecifierSet(requires_python)
118-
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
127146

128-
if r.end_of_life <= utc_today:
129-
eol_supported.append(r)
130-
continue
147+
if cached:
148+
eol_versions = get_cached_eol_versions()
149+
else:
150+
eol_versions = get_eol_versions(cache_json)
151+
eol_supported = [version for version in eol_versions if version.python_ver in package_spec]
131152

132153
if eol_supported:
133154
eol_supported.sort(key=attrgetter("python_ver")) # Sort ascending for error msg generation
@@ -138,12 +159,13 @@ def check_python_support(toml_file: Path, cache_json: Path = CACHED_RELEASE_CYCL
138159
def main(argv: abc.Sequence[str] | None = None) -> int: # noqa: D103
139160
parser = argparse.ArgumentParser()
140161
parser.add_argument("filenames", nargs="*", type=Path)
162+
parser.add_argument("--cached", action="store_true")
141163
args = parser.parse_args(argv)
142164

143165
ec = 0
144166
for file in args.filenames:
145167
try:
146-
check_python_support(file)
168+
check_python_support(file, cached=args.cached)
147169
except EOLPythonError as e:
148170
print(f"{file}: {e}")
149171
ec = 1

0 commit comments

Comments
 (0)