1
1
import csv
2
2
import email .message
3
+ import functools
3
4
import json
4
5
import logging
5
6
import pathlib
13
14
Iterable ,
14
15
Iterator ,
15
16
List ,
17
+ NamedTuple ,
16
18
Optional ,
17
19
Tuple ,
18
20
Union ,
33
35
from pip ._internal .utils .compat import stdlib_pkgs # TODO: Move definition here.
34
36
from pip ._internal .utils .egg_link import egg_link_path_from_sys_path
35
37
from pip ._internal .utils .misc import is_local , normalize_path
38
+ from pip ._internal .utils .packaging import safe_extra
36
39
from pip ._internal .utils .urls import url_to_path
37
40
38
41
if TYPE_CHECKING :
@@ -91,6 +94,12 @@ def _convert_installed_files_path(
91
94
return str (pathlib .Path (* info , * entry ))
92
95
93
96
97
+ class RequiresEntry (NamedTuple ):
98
+ requirement : str
99
+ extra : str
100
+ marker : str
101
+
102
+
94
103
class BaseDistribution (Protocol ):
95
104
@classmethod
96
105
def from_directory (cls , directory : str ) -> "BaseDistribution" :
@@ -348,6 +357,15 @@ def read_text(self, path: InfoPath) -> str:
348
357
def iter_entry_points (self ) -> Iterable [BaseEntryPoint ]:
349
358
raise NotImplementedError ()
350
359
360
+ def _metadata_impl (self ) -> email .message .Message :
361
+ raise NotImplementedError ()
362
+
363
+ @functools .lru_cache (maxsize = 1 )
364
+ def _metadata (self ) -> email .message .Message :
365
+ metadata = self ._metadata_impl ()
366
+ self ._add_egg_info_requires (metadata )
367
+ return metadata
368
+
351
369
@property
352
370
def metadata (self ) -> email .message .Message :
353
371
"""Metadata of distribution parsed from e.g. METADATA or PKG-INFO.
@@ -357,7 +375,7 @@ def metadata(self) -> email.message.Message:
357
375
:raises NoneMetadataError: If the metadata file is available, but does
358
376
not contain valid metadata.
359
377
"""
360
- raise NotImplementedError ()
378
+ return self . _metadata ()
361
379
362
380
@property
363
381
def metadata_version (self ) -> Optional [str ]:
@@ -451,6 +469,76 @@ def iter_declared_entries(self) -> Optional[Iterator[str]]:
451
469
or self ._iter_declared_entries_from_legacy ()
452
470
)
453
471
472
+ def _iter_requires_txt_entries (self ) -> Iterator [RequiresEntry ]:
473
+ """Parse a ``requires.txt`` in an egg-info directory.
474
+
475
+ This is an INI-ish format where an egg-info stores dependencies. A
476
+ section name describes extra other environment markers, while each entry
477
+ is an arbitrary string (not a key-value pair) representing a dependency
478
+ as a requirement string (no markers).
479
+
480
+ There is a construct in ``importlib.metadata`` called ``Sectioned`` that
481
+ does mostly the same, but the format is currently considered private.
482
+ """
483
+ try :
484
+ content = self .read_text ("requires.txt" )
485
+ except FileNotFoundError :
486
+ return
487
+ extra = marker = "" # Section-less entries don't have markers.
488
+ for line in content .splitlines ():
489
+ line = line .strip ()
490
+ if not line or line .startswith ("#" ): # Comment; ignored.
491
+ continue
492
+ if line .startswith ("[" ) and line .endswith ("]" ): # A section header.
493
+ extra , _ , marker = line .strip ("[]" ).partition (":" )
494
+ continue
495
+ yield RequiresEntry (requirement = line , extra = extra , marker = marker )
496
+
497
+ def _iter_egg_info_extras (self ) -> Iterable [str ]:
498
+ """Get extras from the egg-info directory."""
499
+ known_extras = {"" }
500
+ for entry in self ._iter_requires_txt_entries ():
501
+ if entry .extra in known_extras :
502
+ continue
503
+ known_extras .add (entry .extra )
504
+ yield entry .extra
505
+
506
+ def _iter_egg_info_dependencies (self ) -> Iterable [str ]:
507
+ """Get distribution dependencies from the egg-info directory.
508
+
509
+ To ease parsing, this converts a legacy dependency entry into a PEP 508
510
+ requirement string. Like ``_iter_requires_txt_entries()``, there is code
511
+ in ``importlib.metadata`` that does mostly the same, but not do exactly
512
+ what we need.
513
+
514
+ Namely, ``importlib.metadata`` does not normalize the extra name before
515
+ putting it into the requirement string, which causes marker comparison
516
+ to fail because the dist-info format do normalize. This is consistent in
517
+ all currently available PEP 517 backends, although not standardized.
518
+ """
519
+ for entry in self ._iter_requires_txt_entries ():
520
+ if entry .extra and entry .marker :
521
+ marker = f'({ entry .marker } ) and extra == "{ safe_extra (entry .extra )} "'
522
+ elif entry .extra :
523
+ marker = f'extra == "{ safe_extra (entry .extra )} "'
524
+ elif entry .marker :
525
+ marker = entry .marker
526
+ else :
527
+ marker = ""
528
+ if marker :
529
+ yield f"{ entry .requirement } ; { marker } "
530
+ else :
531
+ yield entry .requirement
532
+
533
+ def _add_egg_info_requires (self , metadata : email .message .Message ) -> None :
534
+ """Add egg-info requires.txt information to the metadata."""
535
+ if not metadata .get_all ("Requires-Dist" ):
536
+ for dep in self ._iter_egg_info_dependencies ():
537
+ metadata ["Requires-Dist" ] = dep
538
+ if not metadata .get_all ("Provides-Extra" ):
539
+ for extra in self ._iter_egg_info_extras ():
540
+ metadata ["Provides-Extra" ] = extra
541
+
454
542
455
543
class BaseEnvironment :
456
544
"""An environment containing distributions to introspect."""
0 commit comments