Skip to content

Commit ab88392

Browse files
committed
Make sure metadata has Requires-Dist and Provides-Extra
egg-info distributions may not have the Requires-Dist and Provides-Extra fields in their metadata. For consistency and to provide an unsurprising metadata property, we emulate it by reading requires.txt.
1 parent e58a8a5 commit ab88392

File tree

3 files changed

+88
-87
lines changed

3 files changed

+88
-87
lines changed

src/pip/_internal/metadata/base.py

+78
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Iterable,
1414
Iterator,
1515
List,
16+
NamedTuple,
1617
Optional,
1718
Tuple,
1819
Union,
@@ -33,6 +34,7 @@
3334
from pip._internal.utils.compat import stdlib_pkgs # TODO: Move definition here.
3435
from pip._internal.utils.egg_link import egg_link_path_from_sys_path
3536
from pip._internal.utils.misc import is_local, normalize_path
37+
from pip._internal.utils.packaging import safe_extra
3638
from pip._internal.utils.urls import url_to_path
3739

3840
if TYPE_CHECKING:
@@ -91,6 +93,12 @@ def _convert_installed_files_path(
9193
return str(pathlib.Path(*info, *entry))
9294

9395

96+
class RequiresEntry(NamedTuple):
97+
requirement: str
98+
extra: str
99+
marker: str
100+
101+
94102
class BaseDistribution(Protocol):
95103
@classmethod
96104
def from_directory(cls, directory: str) -> "BaseDistribution":
@@ -451,6 +459,76 @@ def iter_declared_entries(self) -> Optional[Iterator[str]]:
451459
or self._iter_declared_entries_from_legacy()
452460
)
453461

462+
def _iter_requires_txt_entries(self) -> Iterator[RequiresEntry]:
463+
"""Parse a ``requires.txt`` in an egg-info directory.
464+
465+
This is an INI-ish format where an egg-info stores dependencies. A
466+
section name describes extra other environment markers, while each entry
467+
is an arbitrary string (not a key-value pair) representing a dependency
468+
as a requirement string (no markers).
469+
470+
There is a construct in ``importlib.metadata`` called ``Sectioned`` that
471+
does mostly the same, but the format is currently considered private.
472+
"""
473+
try:
474+
content = self.read_text("requires.txt")
475+
except FileNotFoundError:
476+
return
477+
extra = marker = "" # Section-less entries don't have markers.
478+
for line in content.splitlines():
479+
line = line.strip()
480+
if not line or line.startswith("#"): # Comment; ignored.
481+
continue
482+
if line.startswith("[") and line.endswith("]"): # A section header.
483+
extra, _, marker = line.strip("[]").partition(":")
484+
continue
485+
yield RequiresEntry(requirement=line, extra=extra, marker=marker)
486+
487+
def _iter_egg_info_extras(self) -> Iterable[str]:
488+
"""Get extras from the egg-info directory."""
489+
known_extras = {""}
490+
for entry in self._iter_requires_txt_entries():
491+
if entry.extra in known_extras:
492+
continue
493+
known_extras.add(entry.extra)
494+
yield entry.extra
495+
496+
def _iter_egg_info_dependencies(self) -> Iterable[str]:
497+
"""Get distribution dependencies from the egg-info directory.
498+
499+
To ease parsing, this converts a legacy dependency entry into a PEP 508
500+
requirement string. Like ``_iter_requires_txt_entries()``, there is code
501+
in ``importlib.metadata`` that does mostly the same, but not do exactly
502+
what we need.
503+
504+
Namely, ``importlib.metadata`` does not normalize the extra name before
505+
putting it into the requirement string, which causes marker comparison
506+
to fail because the dist-info format do normalize. This is consistent in
507+
all currently available PEP 517 backends, although not standardized.
508+
"""
509+
for entry in self._iter_requires_txt_entries():
510+
if entry.extra and entry.marker:
511+
marker = f'({entry.marker}) and extra == "{safe_extra(entry.extra)}"'
512+
elif entry.extra:
513+
marker = f'extra == "{safe_extra(entry.extra)}"'
514+
elif entry.marker:
515+
marker = entry.marker
516+
else:
517+
marker = ""
518+
if marker:
519+
yield f"{entry.requirement} ; {marker}"
520+
else:
521+
yield entry.requirement
522+
523+
def _add_egg_info_requires(self, metadata: email.message.Message) -> None:
524+
"""Add egg-info requires.txt information to the metadata."""
525+
if not metadata.get_all("Requires-Dist"):
526+
for dep in self._iter_egg_info_dependencies():
527+
metadata["Requires-Dist"] = dep
528+
if not metadata.get_all("Provides-Extra"):
529+
for extra in self._iter_egg_info_extras():
530+
metadata["Provides-Extra"] = extra
531+
454532

455533
class BaseEnvironment:
456534
"""An environment containing distributions to introspect."""

src/pip/_internal/metadata/importlib/_dists.py

+7-86
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,7 @@
33
import os
44
import pathlib
55
import zipfile
6-
from typing import (
7-
Collection,
8-
Dict,
9-
Iterable,
10-
Iterator,
11-
Mapping,
12-
NamedTuple,
13-
Optional,
14-
Sequence,
15-
)
6+
from typing import Collection, Dict, Iterable, Iterator, Mapping, Optional, Sequence
167

178
from pip._vendor.packaging.requirements import Requirement
189
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
@@ -92,12 +83,6 @@ def read_text(self, filename: str) -> Optional[str]:
9283
return text
9384

9485

95-
class RequiresEntry(NamedTuple):
96-
requirement: str
97-
extra: str
98-
marker: str
99-
100-
10186
class Distribution(BaseDistribution):
10287
def __init__(
10388
self,
@@ -189,82 +174,18 @@ def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
189174

190175
@property
191176
def metadata(self) -> email.message.Message:
192-
return self._dist.metadata
193-
194-
def _iter_requires_txt_entries(self) -> Iterator[RequiresEntry]:
195-
"""Parse a ``requires.txt`` in an egg-info directory.
196-
197-
This is an INI-ish format where an egg-info stores dependencies. A
198-
section name describes extra other environment markers, while each entry
199-
is an arbitrary string (not a key-value pair) representing a dependency
200-
as a requirement string (no markers).
201-
202-
There is a construct in ``importlib.metadata`` called ``Sectioned`` that
203-
does mostly the same, but the format is currently considered private.
204-
"""
205-
content = self._dist.read_text("requires.txt")
206-
if content is None:
207-
return
208-
extra = marker = "" # Section-less entries don't have markers.
209-
for line in content.splitlines():
210-
line = line.strip()
211-
if not line or line.startswith("#"): # Comment; ignored.
212-
continue
213-
if line.startswith("[") and line.endswith("]"): # A section header.
214-
extra, _, marker = line.strip("[]").partition(":")
215-
continue
216-
yield RequiresEntry(requirement=line, extra=extra, marker=marker)
217-
218-
def _iter_egg_info_extras(self) -> Iterable[str]:
219-
"""Get extras from the egg-info directory."""
220-
known_extras = {""}
221-
for entry in self._iter_requires_txt_entries():
222-
if entry.extra in known_extras:
223-
continue
224-
known_extras.add(entry.extra)
225-
yield entry.extra
177+
metadata = self._dist.metadata
178+
self._add_egg_info_requires(metadata)
179+
return metadata
226180

227181
def iter_provided_extras(self) -> Iterable[str]:
228-
iterator = (
229-
self._dist.metadata.get_all("Provides-Extra")
230-
or self._iter_egg_info_extras()
182+
return (
183+
safe_extra(extra) for extra in self.metadata.get_all("Provides-Extra", [])
231184
)
232-
return (safe_extra(extra) for extra in iterator)
233-
234-
def _iter_egg_info_dependencies(self) -> Iterable[str]:
235-
"""Get distribution dependencies from the egg-info directory.
236-
237-
To ease parsing, this converts a legacy dependency entry into a PEP 508
238-
requirement string. Like ``_iter_requires_txt_entries()``, there is code
239-
in ``importlib.metadata`` that does mostly the same, but not do exactly
240-
what we need.
241-
242-
Namely, ``importlib.metadata`` does not normalize the extra name before
243-
putting it into the requirement string, which causes marker comparison
244-
to fail because the dist-info format do normalize. This is consistent in
245-
all currently available PEP 517 backends, although not standardized.
246-
"""
247-
for entry in self._iter_requires_txt_entries():
248-
if entry.extra and entry.marker:
249-
marker = f'({entry.marker}) and extra == "{safe_extra(entry.extra)}"'
250-
elif entry.extra:
251-
marker = f'extra == "{safe_extra(entry.extra)}"'
252-
elif entry.marker:
253-
marker = entry.marker
254-
else:
255-
marker = ""
256-
if marker:
257-
yield f"{entry.requirement} ; {marker}"
258-
else:
259-
yield entry.requirement
260185

261186
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
262-
req_string_iterator = (
263-
self._dist.metadata.get_all("Requires-Dist")
264-
or self._iter_egg_info_dependencies()
265-
)
266187
contexts: Sequence[Dict[str, str]] = [{"extra": safe_extra(e)} for e in extras]
267-
for req_string in req_string_iterator:
188+
for req_string in self.metadata.get_all("Requires-Dist", []):
268189
req = Requirement(req_string)
269190
if not req.marker:
270191
yield req

src/pip/_internal/metadata/pkg_resources.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ def metadata(self) -> email.message.Message:
192192
metadata = ""
193193
feed_parser = email.parser.FeedParser()
194194
feed_parser.feed(metadata)
195-
return feed_parser.close()
195+
metadata_msg = feed_parser.close()
196+
self._add_egg_info_requires(metadata_msg)
197+
return metadata_msg
196198

197199
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
198200
if extras: # pkg_resources raises on invalid extras, so we sanitize.

0 commit comments

Comments
 (0)