99from collections import abc
1010from dataclasses import dataclass
1111from enum import StrEnum
12+ from operator import attrgetter
1213from pathlib import Path
1314
1415from packaging import specifiers , version
@@ -58,25 +59,12 @@ def _parse_eol_date(date_str: str) -> dt.date:
5859
5960
6061@dataclass (frozen = True )
61- class PythonRelease :
62- """
63- Represent the relevant metadata information for a Python release.
64-
65- For the purposes of this tool, instances of `PythonRelease` are considered equal if their
66- respective `python_ver` attributes are equal.
67- """
68-
62+ class PythonRelease : # noqa: D101
6963 python_ver : version .Version
7064 status : ReleasePhase
7165 end_of_life : dt .date
7266
73- def __eq__ (self , other : object ) -> bool :
74- if not isinstance (other , PythonRelease ):
75- return NotImplemented
76-
77- return self .python_ver == other .python_ver
78-
79- def __str__ (self ) -> str :
67+ def __str__ (self ) -> str : # pragma: no cover
8068 return f"Python { self .python_ver } - Status: { self .status } , EOL: { self .end_of_life } "
8169
8270 @classmethod
@@ -94,15 +82,25 @@ def from_json(cls, ver: str, metadata: dict[str, t.Any]) -> PythonRelease:
9482 )
9583
9684
97- def _get_cached_release_cycle (cache_json : Path = CACHED_RELEASE_CYCLE ) -> set [PythonRelease ]:
98- """Parse the locally cached Python release cycle into `PythonRelease` instance(s)."""
85+ def _get_cached_release_cycle (cache_json : Path ) -> list [PythonRelease ]:
86+ """
87+ Parse the locally cached Python release cycle into `PythonRelease` instance(s).
88+
89+ Results are sorted by Python version in descending order.
90+ """
9991 with cache_json .open ("r" , encoding = "utf-8" ) as f :
10092 contents = json .load (f )
10193
102- return {PythonRelease .from_json (v , m ) for v , m in contents .items ()}
94+ # The sorting is probably unnecessary since the JSON should already be sorted, but going to
95+ # retain since it's expected downstream
96+ return sorted (
97+ (PythonRelease .from_json (v , m ) for v , m in contents .items ()),
98+ key = attrgetter ("python_ver" ),
99+ reverse = True ,
100+ )
103101
104102
105- def check_python_support (toml_file : Path ) -> None :
103+ def check_python_support (toml_file : Path , cache_json : Path = CACHED_RELEASE_CYCLE ) -> None :
106104 with toml_file .open ("rb" ) as f :
107105 contents = tomllib .load (f )
108106
@@ -111,8 +109,24 @@ def check_python_support(toml_file: Path) -> None:
111109 raise RequiresPythonNotFoundError
112110
113111 package_spec = specifiers .SpecifierSet (requires_python )
112+ release_cycle = _get_cached_release_cycle (cache_json )
113+ utc_today = dt .datetime .now (dt .timezone .utc ).date ()
114+
115+ eol_supported = []
116+ for r in release_cycle :
117+ if r .python_ver in package_spec :
118+ if r .status == ReleasePhase .EOL :
119+ eol_supported .append (r )
120+ continue
121+
122+ if r .end_of_life <= utc_today :
123+ eol_supported .append (r )
124+ continue
114125
115- raise NotImplementedError
126+ if eol_supported :
127+ eol_supported .sort (key = attrgetter ("python_ver" )) # Sort ascending for error msg generation
128+ joined_vers = ", " .join (str (r .python_ver ) for r in eol_supported )
129+ raise EOLPythonError (f"EOL Python support found: { joined_vers } " )
116130
117131
118132def main (argv : abc .Sequence [str ] | None = None ) -> int : # noqa: D103
@@ -124,8 +138,8 @@ def main(argv: abc.Sequence[str] | None = None) -> int: # noqa: D103
124138 for file in args .filenames :
125139 try :
126140 check_python_support (file )
127- except EOLPythonError :
128- print (f"{ file } : Fail. " )
141+ except EOLPythonError as e :
142+ print (f"{ file } : { e } " )
129143 ec = 1
130144 except RequiresPythonNotFoundError :
131145 print (f"{ file } 'requires-python' could not be located, or it is empty." )
0 commit comments