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,17 @@ 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
+ # When we drop python 3.7 support, move this to the metadata property and use
366
+ # functools.cached_property instead of lru_cache.
367
+ metadata = self ._metadata_impl ()
368
+ self ._add_egg_info_requires (metadata )
369
+ return metadata
370
+
351
371
@property
352
372
def metadata (self ) -> email .message .Message :
353
373
"""Metadata of distribution parsed from e.g. METADATA or PKG-INFO.
@@ -357,7 +377,7 @@ def metadata(self) -> email.message.Message:
357
377
:raises NoneMetadataError: If the metadata file is available, but does
358
378
not contain valid metadata.
359
379
"""
360
- raise NotImplementedError ()
380
+ return self . _metadata ()
361
381
362
382
@property
363
383
def metadata_version (self ) -> Optional [str ]:
@@ -451,6 +471,76 @@ def iter_declared_entries(self) -> Optional[Iterator[str]]:
451
471
or self ._iter_declared_entries_from_legacy ()
452
472
)
453
473
474
+ def _iter_requires_txt_entries (self ) -> Iterator [RequiresEntry ]:
475
+ """Parse a ``requires.txt`` in an egg-info directory.
476
+
477
+ This is an INI-ish format where an egg-info stores dependencies. A
478
+ section name describes extra other environment markers, while each entry
479
+ is an arbitrary string (not a key-value pair) representing a dependency
480
+ as a requirement string (no markers).
481
+
482
+ There is a construct in ``importlib.metadata`` called ``Sectioned`` that
483
+ does mostly the same, but the format is currently considered private.
484
+ """
485
+ try :
486
+ content = self .read_text ("requires.txt" )
487
+ except FileNotFoundError :
488
+ return
489
+ extra = marker = "" # Section-less entries don't have markers.
490
+ for line in content .splitlines ():
491
+ line = line .strip ()
492
+ if not line or line .startswith ("#" ): # Comment; ignored.
493
+ continue
494
+ if line .startswith ("[" ) and line .endswith ("]" ): # A section header.
495
+ extra , _ , marker = line .strip ("[]" ).partition (":" )
496
+ continue
497
+ yield RequiresEntry (requirement = line , extra = extra , marker = marker )
498
+
499
+ def _iter_egg_info_extras (self ) -> Iterable [str ]:
500
+ """Get extras from the egg-info directory."""
501
+ known_extras = {"" }
502
+ for entry in self ._iter_requires_txt_entries ():
503
+ if entry .extra in known_extras :
504
+ continue
505
+ known_extras .add (entry .extra )
506
+ yield entry .extra
507
+
508
+ def _iter_egg_info_dependencies (self ) -> Iterable [str ]:
509
+ """Get distribution dependencies from the egg-info directory.
510
+
511
+ To ease parsing, this converts a legacy dependency entry into a PEP 508
512
+ requirement string. Like ``_iter_requires_txt_entries()``, there is code
513
+ in ``importlib.metadata`` that does mostly the same, but not do exactly
514
+ what we need.
515
+
516
+ Namely, ``importlib.metadata`` does not normalize the extra name before
517
+ putting it into the requirement string, which causes marker comparison
518
+ to fail because the dist-info format do normalize. This is consistent in
519
+ all currently available PEP 517 backends, although not standardized.
520
+ """
521
+ for entry in self ._iter_requires_txt_entries ():
522
+ if entry .extra and entry .marker :
523
+ marker = f'({ entry .marker } ) and extra == "{ safe_extra (entry .extra )} "'
524
+ elif entry .extra :
525
+ marker = f'extra == "{ safe_extra (entry .extra )} "'
526
+ elif entry .marker :
527
+ marker = entry .marker
528
+ else :
529
+ marker = ""
530
+ if marker :
531
+ yield f"{ entry .requirement } ; { marker } "
532
+ else :
533
+ yield entry .requirement
534
+
535
+ def _add_egg_info_requires (self , metadata : email .message .Message ) -> None :
536
+ """Add egg-info requires.txt information to the metadata."""
537
+ if not metadata .get_all ("Requires-Dist" ):
538
+ for dep in self ._iter_egg_info_dependencies ():
539
+ metadata ["Requires-Dist" ] = dep
540
+ if not metadata .get_all ("Provides-Extra" ):
541
+ for extra in self ._iter_egg_info_extras ():
542
+ metadata ["Provides-Extra" ] = extra
543
+
454
544
455
545
class BaseEnvironment :
456
546
"""An environment containing distributions to introspect."""
0 commit comments