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

+1
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

+3-2
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

+6-1
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

+3-2
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

+5
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

+16-3
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

+4-2
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

+15-5
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

+3-1
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

+21
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)

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

+16-3
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,22 @@ def __init__(
102102
dist: importlib.metadata.Distribution,
103103
info_location: Optional[BasePath],
104104
installed_location: Optional[BasePath],
105+
concrete: bool,
105106
) -> None:
106107
self._dist = dist
107108
self._info_location = info_location
108109
self._installed_location = installed_location
110+
self._concrete = concrete
111+
112+
@property
113+
def is_concrete(self) -> bool:
114+
return self._concrete
109115

110116
@classmethod
111117
def from_directory(cls, directory: str) -> BaseDistribution:
112118
info_location = pathlib.Path(directory)
113119
dist = importlib.metadata.Distribution.at(info_location)
114-
return cls(dist, info_location, info_location.parent)
120+
return cls(dist, info_location, info_location.parent, concrete=True)
115121

116122
@classmethod
117123
def from_metadata_file_contents(
@@ -128,7 +134,7 @@ def from_metadata_file_contents(
128134
metadata_path.write_bytes(metadata_contents)
129135
# Construct dist pointing to the newly created directory.
130136
dist = importlib.metadata.Distribution.at(metadata_path.parent)
131-
return cls(dist, metadata_path.parent, None)
137+
return cls(dist, metadata_path.parent, None, concrete=False)
132138

133139
@classmethod
134140
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
@@ -137,7 +143,14 @@ def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
137143
dist = WheelDistribution.from_zipfile(zf, name, wheel.location)
138144
except zipfile.BadZipFile as e:
139145
raise InvalidWheel(wheel.location, name) from e
140-
return cls(dist, dist.info_location, pathlib.PurePosixPath(wheel.location))
146+
except UnsupportedWheel as e:
147+
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
148+
return cls(
149+
dist,
150+
dist.info_location,
151+
pathlib.PurePosixPath(wheel.location),
152+
concrete=wheel.is_concrete,
153+
)
141154

142155
@property
143156
def location(self) -> Optional[str]:

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def find(self, location: str) -> Iterator[BaseDistribution]:
8080
installed_location: Optional[BasePath] = None
8181
else:
8282
installed_location = info_location.parent
83-
yield Distribution(dist, info_location, installed_location)
83+
yield Distribution(dist, info_location, installed_location, concrete=True)
8484

8585
def find_linked(self, location: str) -> Iterator[BaseDistribution]:
8686
"""Read location in egg-link files and return distributions in there.
@@ -104,7 +104,7 @@ def find_linked(self, location: str) -> Iterator[BaseDistribution]:
104104
continue
105105
target_location = str(path.joinpath(target_rel))
106106
for dist, info_location in self._find_impl(target_location):
107-
yield Distribution(dist, info_location, path)
107+
yield Distribution(dist, info_location, path, concrete=True)
108108

109109
def _find_eggs_in_dir(self, location: str) -> Iterator[BaseDistribution]:
110110
from pip._vendor.pkg_resources import find_distributions
@@ -116,7 +116,7 @@ def _find_eggs_in_dir(self, location: str) -> Iterator[BaseDistribution]:
116116
if not entry.name.endswith(".egg"):
117117
continue
118118
for dist in find_distributions(entry.path):
119-
yield legacy.Distribution(dist)
119+
yield legacy.Distribution(dist, concrete=True)
120120

121121
def _find_eggs_in_zip(self, location: str) -> Iterator[BaseDistribution]:
122122
from pip._vendor.pkg_resources import find_eggs_in_zip
@@ -128,7 +128,7 @@ def _find_eggs_in_zip(self, location: str) -> Iterator[BaseDistribution]:
128128
except zipimport.ZipImportError:
129129
return
130130
for dist in find_eggs_in_zip(importer, location):
131-
yield legacy.Distribution(dist)
131+
yield legacy.Distribution(dist, concrete=True)
132132

133133
def find_eggs(self, location: str) -> Iterator[BaseDistribution]:
134134
"""Find eggs in a location.

src/pip/_internal/metadata/pkg_resources.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ def run_script(self, script_name: str, namespace: str) -> None:
8181

8282

8383
class Distribution(BaseDistribution):
84-
def __init__(self, dist: pkg_resources.Distribution) -> None:
84+
def __init__(self, dist: pkg_resources.Distribution, concrete: bool) -> None:
8585
self._dist = dist
86+
self._concrete = concrete
8687
# This is populated lazily, to avoid loading metadata for all possible
8788
# distributions eagerly.
8889
self.__extra_mapping: Optional[Mapping[NormalizedName, str]] = None
@@ -96,6 +97,10 @@ def _extra_mapping(self) -> Mapping[NormalizedName, str]:
9697

9798
return self.__extra_mapping
9899

100+
@property
101+
def is_concrete(self) -> bool:
102+
return self._concrete
103+
99104
@classmethod
100105
def from_directory(cls, directory: str) -> BaseDistribution:
101106
dist_dir = directory.rstrip(os.sep)
@@ -114,7 +119,7 @@ def from_directory(cls, directory: str) -> BaseDistribution:
114119
dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
115120

116121
dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
117-
return cls(dist)
122+
return cls(dist, concrete=True)
118123

119124
@classmethod
120125
def from_metadata_file_contents(
@@ -131,7 +136,7 @@ def from_metadata_file_contents(
131136
metadata=InMemoryMetadata(metadata_dict, filename),
132137
project_name=project_name,
133138
)
134-
return cls(dist)
139+
return cls(dist, concrete=False)
135140

136141
@classmethod
137142
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
@@ -152,7 +157,7 @@ def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
152157
metadata=InMemoryMetadata(metadata_dict, wheel.location),
153158
project_name=name,
154159
)
155-
return cls(dist)
160+
return cls(dist, concrete=wheel.is_concrete)
156161

157162
@property
158163
def location(self) -> Optional[str]:
@@ -264,7 +269,7 @@ def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
264269

265270
def _iter_distributions(self) -> Iterator[BaseDistribution]:
266271
for dist in self._ws:
267-
yield Distribution(dist)
272+
yield Distribution(dist, concrete=True)
268273

269274
def _search_distribution(self, name: str) -> Optional[BaseDistribution]:
270275
"""Find a distribution matching the ``name`` in the environment.

src/pip/_internal/operations/check.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
2424
from pip._vendor.packaging.version import Version
2525

26-
from pip._internal.distributions import make_distribution_for_install_requirement
2726
from pip._internal.metadata import get_default_environment
2827
from pip._internal.metadata.base import BaseDistribution
2928
from pip._internal.req.req_install import InstallRequirement
@@ -154,8 +153,8 @@ def _simulate_installation_of(
154153

155154
# Modify it as installing requirement_set would (assuming no errors)
156155
for inst_req in to_install:
157-
abstract_dist = make_distribution_for_install_requirement(inst_req)
158-
dist = abstract_dist.get_metadata_distribution()
156+
assert inst_req.is_concrete
157+
dist = inst_req.get_dist()
159158
name = dist.canonical_name
160159
package_set[name] = PackageDetails(dist.version, list(dist.iter_dependencies()))
161160

0 commit comments

Comments
 (0)