2323
2424import tomlkit
2525from packaging .requirements import Requirement
26- from packaging .specifiers import Specifier
26+ from packaging .specifiers import Specifier , SpecifierSet
27+ from packaging .version import Version
2728from tomlkit .items import String
2829
2930from .paths import PYPROJECT_PATH , STUBS_PATH , distribution_path
3334 "PackageDependencies" ,
3435 "StubMetadata" ,
3536 "StubtestSettings" ,
37+ "get_newest_supported_python" ,
3638 "get_oldest_supported_python" ,
3739 "get_recursive_requirements" ,
3840 "read_dependencies" ,
@@ -55,6 +57,51 @@ def _is_nested_dict(obj: object) -> TypeGuard[dict[str, dict[str, Any]]]:
5557 return isinstance (obj , dict ) and all (isinstance (k , str ) and isinstance (v , dict ) for k , v in obj .items ())
5658
5759
60+ def _bump_minor_version (version : str ) -> str :
61+ v = Version (version )
62+ major , minor , * _ = v .release
63+ minor += 1
64+ return str (Version (f"{ major } .{ minor } " ))
65+
66+
67+ def _is_specifier_subset (base : SpecifierSet , other : SpecifierSet ) -> bool :
68+ """
69+ Check `other` does not extend beyond `base`.
70+ `other` is allowed to have only one bound (>= or <).
71+ """
72+ base_lower = None
73+ base_upper = None
74+ for spec in base :
75+ if spec .operator .startswith (">" ):
76+ base_lower = Version (spec .version )
77+ elif spec .operator .startswith ("<" ):
78+ base_upper = Version (spec .version )
79+ assert isinstance (base_lower , Version )
80+ assert isinstance (base_upper , Version )
81+
82+ other_lower = None
83+ other_upper = None
84+ for spec in other :
85+ if spec .operator .startswith (">" ):
86+ other_lower = Version (spec .version )
87+ elif spec .operator .startswith ("<" ):
88+ other_upper = Version (spec .version )
89+
90+ # Check lower bound
91+ if other_lower and other_lower < base_lower :
92+ return False
93+ # Check upper bound
94+ return not (other_upper and other_upper > base_upper )
95+
96+
97+ @functools .cache
98+ def get_newest_supported_python () -> str :
99+ with PYPROJECT_PATH .open ("rb" ) as config :
100+ val = tomllib .load (config )["tool" ]["typeshed" ]["newest_supported_python" ]
101+ assert type (val ) is str
102+ return val
103+
104+
58105@functools .cache
59106def get_oldest_supported_python () -> str :
60107 with PYPROJECT_PATH .open ("rb" ) as config :
@@ -183,7 +230,7 @@ class StubMetadata:
183230 uploaded_to_pypi : Annotated [bool , "Whether or not a distribution is uploaded to PyPI" ]
184231 partial_stub : Annotated [bool , "Whether this is a partial type stub package as per PEP 561." ]
185232 stubtest_settings : StubtestSettings
186- requires_python : Annotated [Specifier , "Versions of Python supported by the stub package" ]
233+ requires_python : Annotated [SpecifierSet , "Versions of Python supported by the stub package" ]
187234
188235 @property
189236 def is_obsolete (self ) -> bool :
@@ -310,18 +357,22 @@ def read_metadata(distribution: str) -> StubMetadata:
310357 assert type (partial_stub ) is bool
311358 requires_python_str : object = data .get ("requires_python" ) # pyright: ignore[reportUnknownMemberType]
312359 oldest_supported_python = get_oldest_supported_python ()
313- oldest_supported_python_specifier = Specifier (f">={ oldest_supported_python } " )
360+ newest_supported_python = get_newest_supported_python ()
361+ newest_supported_python = _bump_minor_version (newest_supported_python )
362+ supported_python_specifier = SpecifierSet (f">={ oldest_supported_python } ,<{ newest_supported_python } " )
314363 if requires_python_str is None :
315- requires_python = oldest_supported_python_specifier
364+ requires_python = supported_python_specifier
316365 else :
317366 assert isinstance (requires_python_str , str )
318- requires_python = Specifier (requires_python_str )
319- assert requires_python != oldest_supported_python_specifier , f'requires_python="{ requires_python } " is redundant'
320- # Check minimum Python version is not less than the oldest version of Python supported by typeshed
321- assert oldest_supported_python_specifier .contains (
322- requires_python .version
323- ), f"'requires_python' contains versions lower than typeshed's oldest supported Python ({ oldest_supported_python } )"
324- assert requires_python .operator == ">=" , "'requires_python' should be a minimum version specifier, use '>=3.x'"
367+ requires_python = SpecifierSet (requires_python_str )
368+
369+ base_specs = {str (spec ) for spec in supported_python_specifier }
370+ metadata_specs = {str (spec ) for spec in requires_python }
371+ duplicates = base_specs & metadata_specs
372+ assert not duplicates , f"'requires_python' contains redundant specifier(s): { ',' .join (duplicates )} "
373+ assert _is_specifier_subset (
374+ supported_python_specifier , requires_python
375+ ), f"'requires_python' contains versions beyond the typeshed's supported Python ({ supported_python_specifier } )"
325376
326377 empty_tools : dict [object , object ] = {}
327378 tools_settings : object = data .get ("tool" , empty_tools ) # pyright: ignore[reportUnknownMemberType]
0 commit comments