diff options
Diffstat (limited to 'src/pip/_internal/metadata/base.py')
-rw-r--r-- | src/pip/_internal/metadata/base.py | 92 |
1 files changed, 91 insertions, 1 deletions
diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index f1a1ee62f..d226dec8b 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -1,5 +1,6 @@ import csv import email.message +import functools import json import logging import pathlib @@ -13,6 +14,7 @@ from typing import ( Iterable, Iterator, List, + NamedTuple, Optional, Tuple, Union, @@ -33,6 +35,7 @@ from pip._internal.models.direct_url import ( from pip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here. from pip._internal.utils.egg_link import egg_link_path_from_sys_path from pip._internal.utils.misc import is_local, normalize_path +from pip._internal.utils.packaging import safe_extra from pip._internal.utils.urls import url_to_path if TYPE_CHECKING: @@ -91,6 +94,12 @@ def _convert_installed_files_path( return str(pathlib.Path(*info, *entry)) +class RequiresEntry(NamedTuple): + requirement: str + extra: str + marker: str + + class BaseDistribution(Protocol): @classmethod def from_directory(cls, directory: str) -> "BaseDistribution": @@ -348,6 +357,17 @@ class BaseDistribution(Protocol): def iter_entry_points(self) -> Iterable[BaseEntryPoint]: raise NotImplementedError() + def _metadata_impl(self) -> email.message.Message: + raise NotImplementedError() + + @functools.lru_cache(maxsize=1) + def _metadata_cached(self) -> email.message.Message: + # When we drop python 3.7 support, move this to the metadata property and use + # functools.cached_property instead of lru_cache. + metadata = self._metadata_impl() + self._add_egg_info_requires(metadata) + return metadata + @property def metadata(self) -> email.message.Message: """Metadata of distribution parsed from e.g. METADATA or PKG-INFO. @@ -357,7 +377,7 @@ class BaseDistribution(Protocol): :raises NoneMetadataError: If the metadata file is available, but does not contain valid metadata. """ - raise NotImplementedError() + return self._metadata_cached() @property def metadata_version(self) -> Optional[str]: @@ -451,6 +471,76 @@ class BaseDistribution(Protocol): or self._iter_declared_entries_from_legacy() ) + def _iter_requires_txt_entries(self) -> Iterator[RequiresEntry]: + """Parse a ``requires.txt`` in an egg-info directory. + + This is an INI-ish format where an egg-info stores dependencies. A + section name describes extra other environment markers, while each entry + is an arbitrary string (not a key-value pair) representing a dependency + as a requirement string (no markers). + + There is a construct in ``importlib.metadata`` called ``Sectioned`` that + does mostly the same, but the format is currently considered private. + """ + try: + content = self.read_text("requires.txt") + except FileNotFoundError: + return + extra = marker = "" # Section-less entries don't have markers. + for line in content.splitlines(): + line = line.strip() + if not line or line.startswith("#"): # Comment; ignored. + continue + if line.startswith("[") and line.endswith("]"): # A section header. + extra, _, marker = line.strip("[]").partition(":") + continue + yield RequiresEntry(requirement=line, extra=extra, marker=marker) + + def _iter_egg_info_extras(self) -> Iterable[str]: + """Get extras from the egg-info directory.""" + known_extras = {""} + for entry in self._iter_requires_txt_entries(): + if entry.extra in known_extras: + continue + known_extras.add(entry.extra) + yield entry.extra + + def _iter_egg_info_dependencies(self) -> Iterable[str]: + """Get distribution dependencies from the egg-info directory. + + To ease parsing, this converts a legacy dependency entry into a PEP 508 + requirement string. Like ``_iter_requires_txt_entries()``, there is code + in ``importlib.metadata`` that does mostly the same, but not do exactly + what we need. + + Namely, ``importlib.metadata`` does not normalize the extra name before + putting it into the requirement string, which causes marker comparison + to fail because the dist-info format do normalize. This is consistent in + all currently available PEP 517 backends, although not standardized. + """ + for entry in self._iter_requires_txt_entries(): + if entry.extra and entry.marker: + marker = f'({entry.marker}) and extra == "{safe_extra(entry.extra)}"' + elif entry.extra: + marker = f'extra == "{safe_extra(entry.extra)}"' + elif entry.marker: + marker = entry.marker + else: + marker = "" + if marker: + yield f"{entry.requirement} ; {marker}" + else: + yield entry.requirement + + def _add_egg_info_requires(self, metadata: email.message.Message) -> None: + """Add egg-info requires.txt information to the metadata.""" + if not metadata.get_all("Requires-Dist"): + for dep in self._iter_egg_info_dependencies(): + metadata["Requires-Dist"] = dep + if not metadata.get_all("Provides-Extra"): + for extra in self._iter_egg_info_extras(): + metadata["Provides-Extra"] = extra + class BaseEnvironment: """An environment containing distributions to introspect.""" |