@@ -69,18 +69,6 @@ class LinkHash:
69
69
def __post_init__ (self ) -> None :
70
70
assert self .name in _SUPPORTED_HASHES
71
71
72
- @classmethod
73
- def parse_pep658_hash (cls , dist_info_metadata : str ) -> Optional ["LinkHash" ]:
74
- """Parse a PEP 658 data-dist-info-metadata hash."""
75
- if dist_info_metadata == "true" :
76
- return None
77
- name , sep , value = dist_info_metadata .partition ("=" )
78
- if not sep :
79
- return None
80
- if name not in _SUPPORTED_HASHES :
81
- return None
82
- return cls (name = name , value = value )
83
-
84
72
@classmethod
85
73
@functools .lru_cache (maxsize = None )
86
74
def find_hash_url_fragment (cls , url : str ) -> Optional ["LinkHash" ]:
@@ -107,6 +95,28 @@ def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
107
95
return hashes .is_hash_allowed (self .name , hex_digest = self .value )
108
96
109
97
98
+ @dataclass (frozen = True )
99
+ class MetadataFile :
100
+ """Information about a core metadata file associated with a distribution."""
101
+
102
+ hashes : Optional [Dict [str , str ]]
103
+
104
+ def __post_init__ (self ) -> None :
105
+ if self .hashes is not None :
106
+ assert all (name in _SUPPORTED_HASHES for name in self .hashes )
107
+
108
+
109
+ def supported_hashes (hashes : Optional [Dict [str , str ]]) -> Optional [Dict [str , str ]]:
110
+ # Remove any unsupported hash types from the mapping. If this leaves no
111
+ # supported hashes, return None
112
+ if hashes is None :
113
+ return None
114
+ hashes = {n : v for n , v in hashes .items () if n in _SUPPORTED_HASHES }
115
+ if not hashes :
116
+ return None
117
+ return hashes
118
+
119
+
110
120
def _clean_url_path_part (part : str ) -> str :
111
121
"""
112
122
Clean a "part" of a URL path (i.e. after splitting on "@" characters).
@@ -179,7 +189,7 @@ class Link(KeyBasedCompareMixin):
179
189
"comes_from" ,
180
190
"requires_python" ,
181
191
"yanked_reason" ,
182
- "dist_info_metadata " ,
192
+ "metadata_file_data " ,
183
193
"cache_link_parsing" ,
184
194
"egg_fragment" ,
185
195
]
@@ -190,7 +200,7 @@ def __init__(
190
200
comes_from : Optional [Union [str , "IndexContent" ]] = None ,
191
201
requires_python : Optional [str ] = None ,
192
202
yanked_reason : Optional [str ] = None ,
193
- dist_info_metadata : Optional [str ] = None ,
203
+ metadata_file_data : Optional [MetadataFile ] = None ,
194
204
cache_link_parsing : bool = True ,
195
205
hashes : Optional [Mapping [str , str ]] = None ,
196
206
) -> None :
@@ -208,18 +218,21 @@ def __init__(
208
218
a simple repository HTML link. If the file has been yanked but
209
219
no reason was provided, this should be the empty string. See
210
220
PEP 592 for more information and the specification.
211
- :param dist_info_metadata: the metadata attached to the file, or None if no such
212
- metadata is provided. This is the value of the "data-dist-info-metadata"
213
- attribute, if present, in a simple repository HTML link. This may be parsed
214
- into its own `Link` by `self.metadata_link()`. See PEP 658 for more
215
- information and the specification.
221
+ :param metadata_file_data: the metadata attached to the file, or None if
222
+ no such metadata is provided. This argument, if not None, indicates
223
+ that a separate metadata file exists, and also optionally supplies
224
+ hashes for that file.
216
225
:param cache_link_parsing: A flag that is used elsewhere to determine
217
226
whether resources retrieved from this link should be cached. PyPI
218
227
URLs should generally have this set to False, for example.
219
228
:param hashes: A mapping of hash names to digests to allow us to
220
229
determine the validity of a download.
221
230
"""
222
231
232
+ # The comes_from, requires_python, and metadata_file_data arguments are
233
+ # only used by classmethods of this class, and are not used in client
234
+ # code directly.
235
+
223
236
# url can be a UNC windows share
224
237
if url .startswith ("\\ \\ " ):
225
238
url = path_to_url (url )
@@ -239,7 +252,7 @@ def __init__(
239
252
self .comes_from = comes_from
240
253
self .requires_python = requires_python if requires_python else None
241
254
self .yanked_reason = yanked_reason
242
- self .dist_info_metadata = dist_info_metadata
255
+ self .metadata_file_data = metadata_file_data
243
256
244
257
super ().__init__ (key = url , defining_class = Link )
245
258
@@ -262,9 +275,25 @@ def from_json(
262
275
url = _ensure_quoted_url (urllib .parse .urljoin (page_url , file_url ))
263
276
pyrequire = file_data .get ("requires-python" )
264
277
yanked_reason = file_data .get ("yanked" )
265
- dist_info_metadata = file_data .get ("dist-info-metadata" )
266
278
hashes = file_data .get ("hashes" , {})
267
279
280
+ # PEP 714: Indexes must use the name core-metadata, but
281
+ # clients should support the old name as a fallback for compatibility.
282
+ metadata_info = file_data .get ("core-metadata" )
283
+ if metadata_info is None :
284
+ metadata_info = file_data .get ("dist-info-metadata" )
285
+
286
+ # The metadata info value may be a boolean, or a dict of hashes.
287
+ if isinstance (metadata_info , dict ):
288
+ # The file exists, and hashes have been supplied
289
+ metadata_file_data = MetadataFile (supported_hashes (metadata_info ))
290
+ elif metadata_info :
291
+ # The file exists, but there are no hashes
292
+ metadata_file_data = MetadataFile (None )
293
+ else :
294
+ # False or not present: the file does not exist
295
+ metadata_file_data = None
296
+
268
297
# The Link.yanked_reason expects an empty string instead of a boolean.
269
298
if yanked_reason and not isinstance (yanked_reason , str ):
270
299
yanked_reason = ""
@@ -278,7 +307,7 @@ def from_json(
278
307
requires_python = pyrequire ,
279
308
yanked_reason = yanked_reason ,
280
309
hashes = hashes ,
281
- dist_info_metadata = dist_info_metadata ,
310
+ metadata_file_data = metadata_file_data ,
282
311
)
283
312
284
313
@classmethod
@@ -298,14 +327,39 @@ def from_element(
298
327
url = _ensure_quoted_url (urllib .parse .urljoin (base_url , href ))
299
328
pyrequire = anchor_attribs .get ("data-requires-python" )
300
329
yanked_reason = anchor_attribs .get ("data-yanked" )
301
- dist_info_metadata = anchor_attribs .get ("data-dist-info-metadata" )
330
+
331
+ # PEP 714: Indexes must use the name data-core-metadata, but
332
+ # clients should support the old name as a fallback for compatibility.
333
+ metadata_info = anchor_attribs .get ("data-core-metadata" )
334
+ if metadata_info is None :
335
+ metadata_info = anchor_attribs .get ("data-dist-info-metadata" )
336
+ # The metadata info value may be the string "true", or a string of
337
+ # the form "hashname=hashval"
338
+ if metadata_info == "true" :
339
+ # The file exists, but there are no hashes
340
+ metadata_file_data = MetadataFile (None )
341
+ elif metadata_info is None :
342
+ # The file does not exist
343
+ metadata_file_data = None
344
+ else :
345
+ # The file exists, and hashes have been supplied
346
+ hashname , sep , hashval = metadata_info .partition ("=" )
347
+ if sep == "=" :
348
+ metadata_file_data = MetadataFile (supported_hashes ({hashname : hashval }))
349
+ else :
350
+ # Error - data is wrong. Treat as no hashes supplied.
351
+ logger .debug (
352
+ "Index returned invalid data-dist-info-metadata value: %s" ,
353
+ metadata_info ,
354
+ )
355
+ metadata_file_data = MetadataFile (None )
302
356
303
357
return cls (
304
358
url ,
305
359
comes_from = page_url ,
306
360
requires_python = pyrequire ,
307
361
yanked_reason = yanked_reason ,
308
- dist_info_metadata = dist_info_metadata ,
362
+ metadata_file_data = metadata_file_data ,
309
363
)
310
364
311
365
def __str__ (self ) -> str :
@@ -407,17 +461,13 @@ def subdirectory_fragment(self) -> Optional[str]:
407
461
return match .group (1 )
408
462
409
463
def metadata_link (self ) -> Optional ["Link" ]:
410
- """Implementation of PEP 658 parsing."""
411
- # Note that Link.from_element() parsing the "data-dist-info-metadata" attribute
412
- # from an HTML anchor tag is typically how the Link.dist_info_metadata attribute
413
- # gets set.
414
- if self .dist_info_metadata is None :
464
+ """Return a link to the associated core metadata file (if any)."""
465
+ if self .metadata_file_data is None :
415
466
return None
416
467
metadata_url = f"{ self .url_without_fragment } .metadata"
417
- metadata_link_hash = LinkHash .parse_pep658_hash (self .dist_info_metadata )
418
- if metadata_link_hash is None :
468
+ if self .metadata_file_data .hashes is None :
419
469
return Link (metadata_url )
420
- return Link (metadata_url , hashes = metadata_link_hash . as_dict () )
470
+ return Link (metadata_url , hashes = self . metadata_file_data . hashes )
421
471
422
472
def as_hashes (self ) -> Hashes :
423
473
return Hashes ({k : [v ] for k , v in self ._hashes .items ()})
0 commit comments