55
66from __future__ import annotations
77
8+ import datetime
89import functools
910import re
11+ import sys
1012import urllib .parse
1113from collections .abc import Mapping
1214from dataclasses import dataclass
1315from pathlib import Path
1416from typing import Annotated , Any , Final , NamedTuple , final
1517from typing_extensions import TypeGuard
1618
17- import tomli
19+ if sys .version_info >= (3 , 11 ):
20+ import tomllib
21+ else :
22+ import tomli as tomllib
23+
1824import tomlkit
1925from packaging .requirements import Requirement
2026from packaging .specifiers import Specifier
27+ from tomlkit .items import String
2128
2229from .paths import PYPROJECT_PATH , STUBS_PATH , distribution_path
2330
@@ -50,7 +57,7 @@ def _is_nested_dict(obj: object) -> TypeGuard[dict[str, dict[str, Any]]]:
5057@functools .cache
5158def get_oldest_supported_python () -> str :
5259 with PYPROJECT_PATH .open ("rb" ) as config :
53- val = tomli .load (config )["tool" ]["typeshed" ]["oldest_supported_python" ]
60+ val = tomllib .load (config )["tool" ]["typeshed" ]["oldest_supported_python" ]
5461 assert type (val ) is str
5562 return val
5663
@@ -90,7 +97,7 @@ def system_requirements_for_platform(self, platform: str) -> list[str]:
9097def read_stubtest_settings (distribution : str ) -> StubtestSettings :
9198 """Return an object describing the stubtest settings for a single stubs distribution."""
9299 with metadata_path (distribution ).open ("rb" ) as f :
93- data : dict [str , object ] = tomli .load (f ).get ("tool" , {}).get ("stubtest" , {})
100+ data : dict [str , object ] = tomllib .load (f ).get ("tool" , {}).get ("stubtest" , {})
94101
95102 skip : object = data .get ("skip" , False )
96103 apt_dependencies : object = data .get ("apt_dependencies" , [])
@@ -140,6 +147,13 @@ def read_stubtest_settings(distribution: str) -> StubtestSettings:
140147 )
141148
142149
150+ @final
151+ @dataclass (frozen = True )
152+ class ObsoleteMetadata :
153+ since_version : Annotated [str , "A string representing a specific version" ]
154+ since_date : Annotated [datetime .date , "A date when the package became obsolete" ]
155+
156+
143157@final
144158@dataclass (frozen = True )
145159class StubMetadata :
@@ -154,7 +168,7 @@ class StubMetadata:
154168 extra_description : str | None
155169 stub_distribution : Annotated [str , "The name under which the distribution is uploaded to PyPI" ]
156170 upstream_repository : Annotated [str , "The URL of the upstream repository" ] | None
157- obsolete_since : Annotated [str , "A string representing a specific version " ] | None
171+ obsolete : Annotated [ObsoleteMetadata , "Metadata indicating when the stubs package became obsolete " ] | None
158172 no_longer_updated : bool
159173 uploaded_to_pypi : Annotated [bool , "Whether or not a distribution is uploaded to PyPI" ]
160174 partial_stub : Annotated [bool , "Whether this is a partial type stub package as per PEP 561." ]
@@ -163,7 +177,7 @@ class StubMetadata:
163177
164178 @property
165179 def is_obsolete (self ) -> bool :
166- return self .obsolete_since is not None
180+ return self .obsolete is not None
167181
168182
169183_KNOWN_METADATA_FIELDS : Final = frozenset (
@@ -214,27 +228,27 @@ def read_metadata(distribution: str) -> StubMetadata:
214228 """
215229 try :
216230 with metadata_path (distribution ).open ("rb" ) as f :
217- data : dict [ str , object ] = tomli .load (f )
231+ data = tomlkit .load (f )
218232 except FileNotFoundError :
219233 raise NoSuchStubError (f"Typeshed has no stubs for { distribution !r} !" ) from None
220234
221235 unknown_metadata_fields = data .keys () - _KNOWN_METADATA_FIELDS
222236 assert not unknown_metadata_fields , f"Unexpected keys in METADATA.toml for { distribution !r} : { unknown_metadata_fields } "
223237
224238 assert "version" in data , f"Missing 'version' field in METADATA.toml for { distribution !r} "
225- version = data [ "version" ]
239+ version : object = data . get ( "version" ) # pyright: ignore[reportUnknownMemberType ]
226240 assert isinstance (version , str ) and len (version ) > 0 , f"Invalid 'version' field in METADATA.toml for { distribution !r} "
227241 # Check that the version spec parses
228242 if version [0 ].isdigit ():
229243 version = f"=={ version } "
230244 version_spec = Specifier (version )
231245 assert version_spec .operator in {"==" , "~=" }, f"Invalid 'version' field in METADATA.toml for { distribution !r} "
232246
233- requires_s : object = data .get ("requires" , [])
247+ requires_s : object = data .get ("requires" , []) # pyright: ignore[reportUnknownMemberType]
234248 assert isinstance (requires_s , list )
235249 requires = [parse_requires (distribution , req ) for req in requires_s ]
236250
237- extra_description : object = data .get ("extra_description" )
251+ extra_description : object = data .get ("extra_description" ) # pyright: ignore[reportUnknownMemberType]
238252 assert isinstance (extra_description , (str , type (None )))
239253
240254 if "stub_distribution" in data :
@@ -244,7 +258,7 @@ def read_metadata(distribution: str) -> StubMetadata:
244258 else :
245259 stub_distribution = f"types-{ distribution } "
246260
247- upstream_repository : object = data .get ("upstream_repository" )
261+ upstream_repository : object = data .get ("upstream_repository" ) # pyright: ignore[reportUnknownMemberType]
248262 assert isinstance (upstream_repository , (str , type (None )))
249263 if isinstance (upstream_repository , str ):
250264 parsed_url = urllib .parse .urlsplit (upstream_repository )
@@ -268,21 +282,28 @@ def read_metadata(distribution: str) -> StubMetadata:
268282 )
269283 assert num_url_path_parts == 2 , bad_github_url_msg
270284
271- obsolete_since : object = data .get ("obsolete_since" )
272- assert isinstance (obsolete_since , (str , type (None )))
273- no_longer_updated : object = data .get ("no_longer_updated" , False )
285+ obsolete_since : object = data .get ("obsolete_since" ) # pyright: ignore[reportUnknownMemberType]
286+ assert isinstance (obsolete_since , (String , type (None )))
287+ if obsolete_since :
288+ comment = obsolete_since .trivia .comment
289+ since_date_string = comment .removeprefix ("# Released on " )
290+ since_date = datetime .date .fromisoformat (since_date_string )
291+ obsolete = ObsoleteMetadata (since_version = obsolete_since , since_date = since_date )
292+ else :
293+ obsolete = None
294+ no_longer_updated : object = data .get ("no_longer_updated" , False ) # pyright: ignore[reportUnknownMemberType]
274295 assert type (no_longer_updated ) is bool
275- uploaded_to_pypi : object = data .get ("upload" , True )
296+ uploaded_to_pypi : object = data .get ("upload" , True ) # pyright: ignore[reportUnknownMemberType]
276297 assert type (uploaded_to_pypi ) is bool
277- partial_stub : object = data .get ("partial_stub" , True )
298+ partial_stub : object = data .get ("partial_stub" , True ) # pyright: ignore[reportUnknownMemberType]
278299 assert type (partial_stub ) is bool
279- requires_python_str : object = data .get ("requires_python" )
300+ requires_python_str : object = data .get ("requires_python" ) # pyright: ignore[reportUnknownMemberType]
280301 oldest_supported_python = get_oldest_supported_python ()
281302 oldest_supported_python_specifier = Specifier (f">={ oldest_supported_python } " )
282303 if requires_python_str is None :
283304 requires_python = oldest_supported_python_specifier
284305 else :
285- assert type (requires_python_str ) is str
306+ assert isinstance (requires_python_str , str )
286307 requires_python = Specifier (requires_python_str )
287308 assert requires_python != oldest_supported_python_specifier , f'requires_python="{ requires_python } " is redundant'
288309 # Check minimum Python version is not less than the oldest version of Python supported by typeshed
@@ -292,7 +313,7 @@ def read_metadata(distribution: str) -> StubMetadata:
292313 assert requires_python .operator == ">=" , "'requires_python' should be a minimum version specifier, use '>=3.x'"
293314
294315 empty_tools : dict [object , object ] = {}
295- tools_settings : object = data .get ("tool" , empty_tools )
316+ tools_settings : object = data .get ("tool" , empty_tools ) # pyright: ignore[reportUnknownMemberType]
296317 assert isinstance (tools_settings , dict )
297318 assert tools_settings .keys () <= _KNOWN_METADATA_TOOL_FIELDS .keys (), f"Unrecognised tool for { distribution !r} "
298319 for tool , tk in _KNOWN_METADATA_TOOL_FIELDS .items ():
@@ -308,7 +329,7 @@ def read_metadata(distribution: str) -> StubMetadata:
308329 extra_description = extra_description ,
309330 stub_distribution = stub_distribution ,
310331 upstream_repository = upstream_repository ,
311- obsolete_since = obsolete_since ,
332+ obsolete = obsolete ,
312333 no_longer_updated = no_longer_updated ,
313334 uploaded_to_pypi = uploaded_to_pypi ,
314335 partial_stub = partial_stub ,
0 commit comments