Skip to content

Commit 57a33d6

Browse files
use .metadata distribution info when possible
When performing `install --dry-run` and PEP 658 .metadata files are available to guide the resolve, do not download the associated wheels. Rather use the distribution information directly from the .metadata files when reporting the results on the CLI and in the --report file. - describe the new --dry-run behavior - finalize linked requirements immediately after resolve - introduce is_concrete - funnel InstalledDistribution through _get_prepared_distribution() too - add test for new install --dry-run functionality (no downloading)
1 parent 8eadcab commit 57a33d6

File tree

21 files changed

+569
-105
lines changed

21 files changed

+569
-105
lines changed

news/12186.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Avoid downloading any dists in ``install --dry-run`` if PEP 658 ``.metadata`` files or lazy wheels are available.

src/pip/_internal/commands/download.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ def run(self, options: Values, args: List[str]) -> int:
130130
self.trace_basic_info(finder)
131131

132132
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
133+
preparer.finalize_linked_requirements(
134+
requirement_set.requirements.values(), require_dist_files=True
135+
)
133136

134137
downloaded: List[str] = []
135138
for req in requirement_set.requirements.values():
@@ -138,8 +141,6 @@ def run(self, options: Values, args: List[str]) -> int:
138141
preparer.save_linked_requirement(req)
139142
downloaded.append(req.name)
140143

141-
preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
142-
143144
if downloaded:
144145
write_output("Successfully downloaded %s", " ".join(downloaded))
145146

src/pip/_internal/commands/install.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ def add_options(self) -> None:
8585
help=(
8686
"Don't actually install anything, just print what would be. "
8787
"Can be used in combination with --ignore-installed "
88-
"to 'resolve' the requirements."
88+
"to 'resolve' the requirements. If package metadata is available "
89+
"or cached, --dry-run also avoids downloading the dependency at all."
8990
),
9091
)
9192
self.cmd_opts.add_option(
@@ -379,6 +380,10 @@ def run(self, options: Values, args: List[str]) -> int:
379380
requirement_set = resolver.resolve(
380381
reqs, check_supported_wheels=not options.target_dir
381382
)
383+
preparer.finalize_linked_requirements(
384+
requirement_set.requirements.values(),
385+
require_dist_files=not options.dry_run,
386+
)
382387

383388
if options.json_report_file:
384389
report = InstallationReport(requirement_set.requirements_to_install)

src/pip/_internal/commands/wheel.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ def run(self, options: Values, args: List[str]) -> int:
145145
self.trace_basic_info(finder)
146146

147147
requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
148+
preparer.finalize_linked_requirements(
149+
requirement_set.requirements.values(), require_dist_files=True
150+
)
148151

149152
reqs_to_build: List[InstallRequirement] = []
150153
for req in requirement_set.requirements.values():
@@ -153,8 +156,6 @@ def run(self, options: Values, args: List[str]) -> int:
153156
elif should_build_for_wheel_command(req):
154157
reqs_to_build.append(req)
155158

156-
preparer.prepare_linked_requirements_more(requirement_set.requirements.values())
157-
158159
# build wheels
159160
build_successes, build_failures = build(
160161
reqs_to_build,

src/pip/_internal/distributions/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pip._internal.distributions.base import AbstractDistribution
2+
from pip._internal.distributions.installed import InstalledDistribution
23
from pip._internal.distributions.sdist import SourceDistribution
34
from pip._internal.distributions.wheel import WheelDistribution
45
from pip._internal.req.req_install import InstallRequirement
@@ -8,6 +9,10 @@ def make_distribution_for_install_requirement(
89
install_req: InstallRequirement,
910
) -> AbstractDistribution:
1011
"""Returns a Distribution for the given InstallRequirement"""
12+
# Only pre-installed requirements will have a .satisfied_by dist.
13+
if install_req.satisfied_by:
14+
return InstalledDistribution(install_req)
15+
1116
# Editable requirements will always be source distributions. They use the
1217
# legacy logic until we create a modern standard for them.
1318
if install_req.editable:

src/pip/_internal/distributions/base.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,17 @@ def build_tracker_id(self) -> Optional[str]:
3737
3838
If None, then this dist has no work to do in the build tracker, and
3939
``.prepare_distribution_metadata()`` will not be called."""
40-
raise NotImplementedError()
40+
...
4141

4242
@abc.abstractmethod
4343
def get_metadata_distribution(self) -> BaseDistribution:
44-
raise NotImplementedError()
44+
"""Generate a concrete ``BaseDistribution`` instance for this artifact.
45+
46+
The implementation should also cache the result with
47+
``self.req.cache_concrete_dist()`` so the distribution is available to other
48+
users of the ``InstallRequirement``. This method is not called within the build
49+
tracker context, so it should not identify any new setup requirements."""
50+
...
4551

4652
@abc.abstractmethod
4753
def prepare_distribution_metadata(
@@ -50,4 +56,11 @@ def prepare_distribution_metadata(
5056
build_isolation: bool,
5157
check_build_deps: bool,
5258
) -> None:
53-
raise NotImplementedError()
59+
"""Generate the information necessary to extract metadata from the artifact.
60+
61+
This method will be executed within the context of ``BuildTracker#track()``, so
62+
it needs to fully identify any setup requirements so they can be added to the
63+
same active set of tracked builds, while ``.get_metadata_distribution()`` takes
64+
care of generating and caching the ``BaseDistribution`` to expose to the rest of
65+
the resolve."""
66+
...

src/pip/_internal/distributions/installed.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ def build_tracker_id(self) -> Optional[str]:
1717
return None
1818

1919
def get_metadata_distribution(self) -> BaseDistribution:
20-
assert self.req.satisfied_by is not None, "not actually installed"
21-
return self.req.satisfied_by
20+
dist = self.req.satisfied_by
21+
assert dist is not None, "not actually installed"
22+
self.req.cache_concrete_dist(dist)
23+
return dist
2224

2325
def prepare_distribution_metadata(
2426
self,

src/pip/_internal/distributions/sdist.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import logging
2-
from typing import TYPE_CHECKING, Iterable, Optional, Set, Tuple
2+
from typing import TYPE_CHECKING, Iterable, Set, Tuple
33

44
from pip._internal.build_env import BuildEnvironment
55
from pip._internal.distributions.base import AbstractDistribution
66
from pip._internal.exceptions import InstallationError
7-
from pip._internal.metadata import BaseDistribution
7+
from pip._internal.metadata import BaseDistribution, get_directory_distribution
88
from pip._internal.utils.subprocess import runner_with_spinner_message
99

1010
if TYPE_CHECKING:
@@ -21,13 +21,19 @@ class SourceDistribution(AbstractDistribution):
2121
"""
2222

2323
@property
24-
def build_tracker_id(self) -> Optional[str]:
24+
def build_tracker_id(self) -> str:
2525
"""Identify this requirement uniquely by its link."""
2626
assert self.req.link
2727
return self.req.link.url_without_fragment
2828

2929
def get_metadata_distribution(self) -> BaseDistribution:
30-
return self.req.get_dist()
30+
assert (
31+
self.req.metadata_directory
32+
), "Set as part of .prepare_distribution_metadata()"
33+
dist = get_directory_distribution(self.req.metadata_directory)
34+
self.req.cache_concrete_dist(dist)
35+
self.req.validate_sdist_metadata()
36+
return dist
3137

3238
def prepare_distribution_metadata(
3339
self,
@@ -66,7 +72,11 @@ def prepare_distribution_metadata(
6672
self._raise_conflicts("the backend dependencies", conflicting)
6773
if missing:
6874
self._raise_missing_reqs(missing)
69-
self.req.prepare_metadata()
75+
76+
# NB: we must still call .cache_concrete_dist() and .validate_sdist_metadata()
77+
# before the InstallRequirement itself has been updated with the metadata from
78+
# this directory!
79+
self.req.prepare_metadata_directory()
7080

7181
def _prepare_build_backend(self, finder: "PackageFinder") -> None:
7282
# Isolate in a BuildEnvironment and install the build-time

src/pip/_internal/distributions/wheel.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ def get_metadata_distribution(self) -> BaseDistribution:
3131
assert self.req.local_file_path, "Set as part of preparation during download"
3232
assert self.req.name, "Wheels are never unnamed"
3333
wheel = FilesystemWheel(self.req.local_file_path)
34-
return get_wheel_distribution(wheel, canonicalize_name(self.req.name))
34+
dist = get_wheel_distribution(wheel, canonicalize_name(self.req.name))
35+
self.req.cache_concrete_dist(dist)
36+
return dist
3537

3638
def prepare_distribution_metadata(
3739
self,

src/pip/_internal/metadata/base.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ class RequiresEntry(NamedTuple):
9797

9898

9999
class BaseDistribution(Protocol):
100+
@property
101+
def is_concrete(self) -> bool:
102+
"""Whether the distribution really exists somewhere on disk.
103+
104+
If this is false, it has been synthesized from metadata, e.g. via
105+
``.from_metadata_file_contents()``, or ``.from_wheel()`` against
106+
a ``MemoryWheel``."""
107+
raise NotImplementedError()
108+
100109
@classmethod
101110
def from_directory(cls, directory: str) -> "BaseDistribution":
102111
"""Load the distribution from a metadata directory.
@@ -667,6 +676,10 @@ def iter_installed_distributions(
667676
class Wheel(Protocol):
668677
location: str
669678

679+
@property
680+
def is_concrete(self) -> bool:
681+
raise NotImplementedError()
682+
670683
def as_zipfile(self) -> zipfile.ZipFile:
671684
raise NotImplementedError()
672685

@@ -675,6 +688,10 @@ class FilesystemWheel(Wheel):
675688
def __init__(self, location: str) -> None:
676689
self.location = location
677690

691+
@property
692+
def is_concrete(self) -> bool:
693+
return True
694+
678695
def as_zipfile(self) -> zipfile.ZipFile:
679696
return zipfile.ZipFile(self.location, allowZip64=True)
680697

@@ -684,5 +701,9 @@ def __init__(self, location: str, stream: IO[bytes]) -> None:
684701
self.location = location
685702
self.stream = stream
686703

704+
@property
705+
def is_concrete(self) -> bool:
706+
return False
707+
687708
def as_zipfile(self) -> zipfile.ZipFile:
688709
return zipfile.ZipFile(self.stream, allowZip64=True)

0 commit comments

Comments
 (0)