55
66from __future__ import annotations
77
8+ import datetime
89import functools
910import re
1011import urllib .parse
1819import tomlkit
1920from packaging .requirements import Requirement
2021from packaging .specifiers import Specifier
22+ from tomlkit .items import String
2123
2224from .paths import PYPROJECT_PATH , STUBS_PATH , distribution_path
2325
2628 "PackageDependencies" ,
2729 "StubMetadata" ,
2830 "StubtestSettings" ,
31+ "get_oldest_supported_python" ,
2932 "get_recursive_requirements" ,
3033 "read_dependencies" ,
3134 "read_metadata" ,
@@ -139,6 +142,13 @@ def read_stubtest_settings(distribution: str) -> StubtestSettings:
139142 )
140143
141144
145+ @final
146+ @dataclass (frozen = True )
147+ class ObsoleteMetadata :
148+ since_version : Annotated [str , "A string representing a specific version" ]
149+ since_date : Annotated [datetime .date , "A date when the package became obsolete" ]
150+
151+
142152@final
143153@dataclass (frozen = True )
144154class StubMetadata :
@@ -153,7 +163,7 @@ class StubMetadata:
153163 extra_description : str | None
154164 stub_distribution : Annotated [str , "The name under which the distribution is uploaded to PyPI" ]
155165 upstream_repository : Annotated [str , "The URL of the upstream repository" ] | None
156- obsolete_since : Annotated [str , "A string representing a specific version " ] | None
166+ obsolete : Annotated [ObsoleteMetadata , "Metadata indicating when the stubs package became obsolete " ] | None
157167 no_longer_updated : bool
158168 uploaded_to_pypi : Annotated [bool , "Whether or not a distribution is uploaded to PyPI" ]
159169 partial_stub : Annotated [bool , "Whether this is a partial type stub package as per PEP 561." ]
@@ -162,7 +172,7 @@ class StubMetadata:
162172
163173 @property
164174 def is_obsolete (self ) -> bool :
165- return self .obsolete_since is not None
175+ return self .obsolete is not None
166176
167177
168178_KNOWN_METADATA_FIELDS : Final = frozenset (
@@ -213,27 +223,27 @@ def read_metadata(distribution: str) -> StubMetadata:
213223 """
214224 try :
215225 with metadata_path (distribution ).open ("rb" ) as f :
216- data : dict [ str , object ] = tomli .load (f )
226+ data = tomlkit .load (f )
217227 except FileNotFoundError :
218228 raise NoSuchStubError (f"Typeshed has no stubs for { distribution !r} !" ) from None
219229
220230 unknown_metadata_fields = data .keys () - _KNOWN_METADATA_FIELDS
221231 assert not unknown_metadata_fields , f"Unexpected keys in METADATA.toml for { distribution !r} : { unknown_metadata_fields } "
222232
223233 assert "version" in data , f"Missing 'version' field in METADATA.toml for { distribution !r} "
224- version = data [ "version" ]
234+ version : object = data . get ( "version" ) # pyright: ignore[reportUnknownMemberType ]
225235 assert isinstance (version , str ) and len (version ) > 0 , f"Invalid 'version' field in METADATA.toml for { distribution !r} "
226236 # Check that the version spec parses
227237 if version [0 ].isdigit ():
228238 version = f"=={ version } "
229239 version_spec = Specifier (version )
230240 assert version_spec .operator in {"==" , "~=" }, f"Invalid 'version' field in METADATA.toml for { distribution !r} "
231241
232- requires_s : object = data .get ("requires" , [])
242+ requires_s : object = data .get ("requires" , []) # pyright: ignore[reportUnknownMemberType]
233243 assert isinstance (requires_s , list )
234244 requires = [parse_requires (distribution , req ) for req in requires_s ]
235245
236- extra_description : object = data .get ("extra_description" )
246+ extra_description : object = data .get ("extra_description" ) # pyright: ignore[reportUnknownMemberType]
237247 assert isinstance (extra_description , (str , type (None )))
238248
239249 if "stub_distribution" in data :
@@ -243,7 +253,7 @@ def read_metadata(distribution: str) -> StubMetadata:
243253 else :
244254 stub_distribution = f"types-{ distribution } "
245255
246- upstream_repository : object = data .get ("upstream_repository" )
256+ upstream_repository : object = data .get ("upstream_repository" ) # pyright: ignore[reportUnknownMemberType]
247257 assert isinstance (upstream_repository , (str , type (None )))
248258 if isinstance (upstream_repository , str ):
249259 parsed_url = urllib .parse .urlsplit (upstream_repository )
@@ -267,21 +277,28 @@ def read_metadata(distribution: str) -> StubMetadata:
267277 )
268278 assert num_url_path_parts == 2 , bad_github_url_msg
269279
270- obsolete_since : object = data .get ("obsolete_since" )
271- assert isinstance (obsolete_since , (str , type (None )))
272- no_longer_updated : object = data .get ("no_longer_updated" , False )
280+ obsolete_since : object = data .get ("obsolete_since" ) # pyright: ignore[reportUnknownMemberType]
281+ assert isinstance (obsolete_since , (String , type (None )))
282+ if obsolete_since :
283+ comment = obsolete_since .trivia .comment
284+ since_date_string = comment .removeprefix ("# Released on " )
285+ since_date = datetime .date .fromisoformat (since_date_string )
286+ obsolete = ObsoleteMetadata (since_version = obsolete_since , since_date = since_date )
287+ else :
288+ obsolete = None
289+ no_longer_updated : object = data .get ("no_longer_updated" , False ) # pyright: ignore[reportUnknownMemberType]
273290 assert type (no_longer_updated ) is bool
274- uploaded_to_pypi : object = data .get ("upload" , True )
291+ uploaded_to_pypi : object = data .get ("upload" , True ) # pyright: ignore[reportUnknownMemberType]
275292 assert type (uploaded_to_pypi ) is bool
276- partial_stub : object = data .get ("partial_stub" , True )
293+ partial_stub : object = data .get ("partial_stub" , True ) # pyright: ignore[reportUnknownMemberType]
277294 assert type (partial_stub ) is bool
278- requires_python_str : object = data .get ("requires_python" )
295+ requires_python_str : object = data .get ("requires_python" ) # pyright: ignore[reportUnknownMemberType]
279296 oldest_supported_python = get_oldest_supported_python ()
280297 oldest_supported_python_specifier = Specifier (f">={ oldest_supported_python } " )
281298 if requires_python_str is None :
282299 requires_python = oldest_supported_python_specifier
283300 else :
284- assert type (requires_python_str ) is str
301+ assert isinstance (requires_python_str , str )
285302 requires_python = Specifier (requires_python_str )
286303 assert requires_python != oldest_supported_python_specifier , f'requires_python="{ requires_python } " is redundant'
287304 # Check minimum Python version is not less than the oldest version of Python supported by typeshed
@@ -291,7 +308,7 @@ def read_metadata(distribution: str) -> StubMetadata:
291308 assert requires_python .operator == ">=" , "'requires_python' should be a minimum version specifier, use '>=3.x'"
292309
293310 empty_tools : dict [object , object ] = {}
294- tools_settings : object = data .get ("tool" , empty_tools )
311+ tools_settings : object = data .get ("tool" , empty_tools ) # pyright: ignore[reportUnknownMemberType]
295312 assert isinstance (tools_settings , dict )
296313 assert tools_settings .keys () <= _KNOWN_METADATA_TOOL_FIELDS .keys (), f"Unrecognised tool for { distribution !r} "
297314 for tool , tk in _KNOWN_METADATA_TOOL_FIELDS .items ():
@@ -307,7 +324,7 @@ def read_metadata(distribution: str) -> StubMetadata:
307324 extra_description = extra_description ,
308325 stub_distribution = stub_distribution ,
309326 upstream_repository = upstream_repository ,
310- obsolete_since = obsolete_since ,
327+ obsolete = obsolete ,
311328 no_longer_updated = no_longer_updated ,
312329 uploaded_to_pypi = uploaded_to_pypi ,
313330 partial_stub = partial_stub ,
0 commit comments