5
5
6
6
from __future__ import annotations
7
7
8
+ import datetime
8
9
import functools
9
10
import re
10
11
import urllib .parse
18
19
import tomlkit
19
20
from packaging .requirements import Requirement
20
21
from packaging .specifiers import Specifier
22
+ from tomlkit .items import String
21
23
22
24
from .paths import PYPROJECT_PATH , STUBS_PATH , distribution_path
23
25
@@ -140,6 +142,13 @@ def read_stubtest_settings(distribution: str) -> StubtestSettings:
140
142
)
141
143
142
144
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
+
143
152
@final
144
153
@dataclass (frozen = True )
145
154
class StubMetadata :
@@ -154,7 +163,7 @@ class StubMetadata:
154
163
extra_description : str | None
155
164
stub_distribution : Annotated [str , "The name under which the distribution is uploaded to PyPI" ]
156
165
upstream_repository : Annotated [str , "The URL of the upstream repository" ] | None
157
- obsolete_since : Annotated [str , "A string representing a specific version " ] | None
166
+ obsolete : Annotated [ObsoleteMetadata , "Metadata indicating when the stubs package became obsolete " ] | None
158
167
no_longer_updated : bool
159
168
uploaded_to_pypi : Annotated [bool , "Whether or not a distribution is uploaded to PyPI" ]
160
169
partial_stub : Annotated [bool , "Whether this is a partial type stub package as per PEP 561." ]
@@ -163,7 +172,7 @@ class StubMetadata:
163
172
164
173
@property
165
174
def is_obsolete (self ) -> bool :
166
- return self .obsolete_since is not None
175
+ return self .obsolete is not None
167
176
168
177
169
178
_KNOWN_METADATA_FIELDS : Final = frozenset (
@@ -214,27 +223,27 @@ def read_metadata(distribution: str) -> StubMetadata:
214
223
"""
215
224
try :
216
225
with metadata_path (distribution ).open ("rb" ) as f :
217
- data : dict [ str , object ] = tomli .load (f )
226
+ data = tomlkit .load (f )
218
227
except FileNotFoundError :
219
228
raise NoSuchStubError (f"Typeshed has no stubs for { distribution !r} !" ) from None
220
229
221
230
unknown_metadata_fields = data .keys () - _KNOWN_METADATA_FIELDS
222
231
assert not unknown_metadata_fields , f"Unexpected keys in METADATA.toml for { distribution !r} : { unknown_metadata_fields } "
223
232
224
233
assert "version" in data , f"Missing 'version' field in METADATA.toml for { distribution !r} "
225
- version = data [ "version" ]
234
+ version : object = data . get ( "version" ) # pyright: ignore[reportUnknownMemberType ]
226
235
assert isinstance (version , str ) and len (version ) > 0 , f"Invalid 'version' field in METADATA.toml for { distribution !r} "
227
236
# Check that the version spec parses
228
237
if version [0 ].isdigit ():
229
238
version = f"=={ version } "
230
239
version_spec = Specifier (version )
231
240
assert version_spec .operator in {"==" , "~=" }, f"Invalid 'version' field in METADATA.toml for { distribution !r} "
232
241
233
- requires_s : object = data .get ("requires" , [])
242
+ requires_s : object = data .get ("requires" , []) # pyright: ignore[reportUnknownMemberType]
234
243
assert isinstance (requires_s , list )
235
244
requires = [parse_requires (distribution , req ) for req in requires_s ]
236
245
237
- extra_description : object = data .get ("extra_description" )
246
+ extra_description : object = data .get ("extra_description" ) # pyright: ignore[reportUnknownMemberType]
238
247
assert isinstance (extra_description , (str , type (None )))
239
248
240
249
if "stub_distribution" in data :
@@ -244,7 +253,7 @@ def read_metadata(distribution: str) -> StubMetadata:
244
253
else :
245
254
stub_distribution = f"types-{ distribution } "
246
255
247
- upstream_repository : object = data .get ("upstream_repository" )
256
+ upstream_repository : object = data .get ("upstream_repository" ) # pyright: ignore[reportUnknownMemberType]
248
257
assert isinstance (upstream_repository , (str , type (None )))
249
258
if isinstance (upstream_repository , str ):
250
259
parsed_url = urllib .parse .urlsplit (upstream_repository )
@@ -268,21 +277,28 @@ def read_metadata(distribution: str) -> StubMetadata:
268
277
)
269
278
assert num_url_path_parts == 2 , bad_github_url_msg
270
279
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 )
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]
274
290
assert type (no_longer_updated ) is bool
275
- uploaded_to_pypi : object = data .get ("upload" , True )
291
+ uploaded_to_pypi : object = data .get ("upload" , True ) # pyright: ignore[reportUnknownMemberType]
276
292
assert type (uploaded_to_pypi ) is bool
277
- partial_stub : object = data .get ("partial_stub" , True )
293
+ partial_stub : object = data .get ("partial_stub" , True ) # pyright: ignore[reportUnknownMemberType]
278
294
assert type (partial_stub ) is bool
279
- requires_python_str : object = data .get ("requires_python" )
295
+ requires_python_str : object = data .get ("requires_python" ) # pyright: ignore[reportUnknownMemberType]
280
296
oldest_supported_python = get_oldest_supported_python ()
281
297
oldest_supported_python_specifier = Specifier (f">={ oldest_supported_python } " )
282
298
if requires_python_str is None :
283
299
requires_python = oldest_supported_python_specifier
284
300
else :
285
- assert type (requires_python_str ) is str
301
+ assert isinstance (requires_python_str , str )
286
302
requires_python = Specifier (requires_python_str )
287
303
assert requires_python != oldest_supported_python_specifier , f'requires_python="{ requires_python } " is redundant'
288
304
# Check minimum Python version is not less than the oldest version of Python supported by typeshed
@@ -292,7 +308,7 @@ def read_metadata(distribution: str) -> StubMetadata:
292
308
assert requires_python .operator == ">=" , "'requires_python' should be a minimum version specifier, use '>=3.x'"
293
309
294
310
empty_tools : dict [object , object ] = {}
295
- tools_settings : object = data .get ("tool" , empty_tools )
311
+ tools_settings : object = data .get ("tool" , empty_tools ) # pyright: ignore[reportUnknownMemberType]
296
312
assert isinstance (tools_settings , dict )
297
313
assert tools_settings .keys () <= _KNOWN_METADATA_TOOL_FIELDS .keys (), f"Unrecognised tool for { distribution !r} "
298
314
for tool , tk in _KNOWN_METADATA_TOOL_FIELDS .items ():
@@ -308,7 +324,7 @@ def read_metadata(distribution: str) -> StubMetadata:
308
324
extra_description = extra_description ,
309
325
stub_distribution = stub_distribution ,
310
326
upstream_repository = upstream_repository ,
311
- obsolete_since = obsolete_since ,
327
+ obsolete = obsolete ,
312
328
no_longer_updated = no_longer_updated ,
313
329
uploaded_to_pypi = uploaded_to_pypi ,
314
330
partial_stub = partial_stub ,
0 commit comments