Skip to content

Commit d7d15be

Browse files
Jonathan Helmuspradyunsg
authored andcommitted
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 - Update src/pip/_internal/commands/install.py Co-authored-by: Ed Morley <[email protected]> - Update src/pip/_internal/commands/install.py Co-authored-by: Pradyun Gedam <[email protected]>
1 parent 9f247c9 commit d7d15be

File tree

21 files changed

+289
-91
lines changed

21 files changed

+289
-91
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-1
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(), hydrate_virtual_reqs=True
135+
)
133136

134137
downloaded: List[str] = []
135138
for req in requirement_set.requirements.values():
@@ -138,7 +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())
142144
requirement_set.warn_legacy_versions_and_specifiers()
143145

144146
if downloaded:

src/pip/_internal/commands/install.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ def add_options(self) -> None:
8484
help=(
8585
"Don't actually install anything, just print what would be. "
8686
"Can be used in combination with --ignore-installed "
87-
"to 'resolve' the requirements."
87+
"to 'resolve' the requirements. If PEP 658 or fast-deps metadata is "
88+
"available, --dry-run also avoids downloading the dependency at all."
8889
),
8990
)
9091
self.cmd_opts.add_option(
@@ -377,6 +378,10 @@ def run(self, options: Values, args: List[str]) -> int:
377378
requirement_set = resolver.resolve(
378379
reqs, check_supported_wheels=not options.target_dir
379380
)
381+
preparer.finalize_linked_requirements(
382+
requirement_set.requirements.values(),
383+
hydrate_virtual_reqs=not options.dry_run,
384+
)
380385

381386
if options.json_report_file:
382387
report = InstallationReport(requirement_set.requirements_to_install)

src/pip/_internal/commands/wheel.py

+3-1
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(), hydrate_virtual_reqs=True
150+
)
148151

149152
reqs_to_build: List[InstallRequirement] = []
150153
for req in requirement_set.requirements.values():
@@ -153,7 +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())
157159
requirement_set.warn_legacy_versions_and_specifiers()
158160

159161
# build wheels

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

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def __init__(self, req: InstallRequirement) -> None:
2525
super().__init__()
2626
self.req = req
2727

28+
@abc.abstractproperty
29+
def add_to_build_tracker(self) -> bool:
30+
raise NotImplementedError()
31+
2832
@abc.abstractmethod
2933
def get_metadata_distribution(self) -> BaseDistribution:
3034
raise NotImplementedError()

src/pip/_internal/distributions/installed.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ class InstalledDistribution(AbstractDistribution):
1010
been computed.
1111
"""
1212

13+
@property
14+
def add_to_build_tracker(self) -> bool:
15+
return False
16+
1317
def get_metadata_distribution(self) -> BaseDistribution:
14-
assert self.req.satisfied_by is not None, "not actually installed"
15-
return self.req.satisfied_by
18+
dist = self.req.satisfied_by
19+
assert dist is not None, "not actually installed"
20+
self.req.cache_concrete_dist(dist)
21+
return dist
1622

1723
def prepare_distribution_metadata(
1824
self,

src/pip/_internal/distributions/sdist.py

+17-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pip._internal.distributions.base import AbstractDistribution
66
from pip._internal.exceptions import InstallationError
77
from pip._internal.index.package_finder import PackageFinder
8-
from pip._internal.metadata import BaseDistribution
8+
from pip._internal.metadata import BaseDistribution, get_directory_distribution
99
from pip._internal.utils.subprocess import runner_with_spinner_message
1010

1111
logger = logging.getLogger(__name__)
@@ -18,8 +18,18 @@ class SourceDistribution(AbstractDistribution):
1818
generated, either using PEP 517 or using the legacy `setup.py egg_info`.
1919
"""
2020

21+
@property
22+
def add_to_build_tracker(self) -> bool:
23+
return True
24+
2125
def get_metadata_distribution(self) -> BaseDistribution:
22-
return self.req.get_dist()
26+
assert (
27+
self.req.metadata_directory
28+
), "Set as part of .prepare_distribution_metadata()"
29+
dist = get_directory_distribution(self.req.metadata_directory)
30+
self.req.cache_concrete_dist(dist)
31+
self.req.validate_sdist_metadata()
32+
return dist
2333

2434
def prepare_distribution_metadata(
2535
self,
@@ -58,7 +68,11 @@ def prepare_distribution_metadata(
5868
self._raise_conflicts("the backend dependencies", conflicting)
5969
if missing:
6070
self._raise_missing_reqs(missing)
61-
self.req.prepare_metadata()
71+
72+
# NB: we must still call .cache_concrete_dist() and .validate_sdist_metadata()
73+
# before the InstallRequirement itself has been updated with the metadata from
74+
# this directory!
75+
self.req.prepare_metadata_directory()
6276

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

src/pip/_internal/distributions/wheel.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ class WheelDistribution(AbstractDistribution):
1515
This does not need any preparation as wheels can be directly unpacked.
1616
"""
1717

18+
@property
19+
def add_to_build_tracker(self) -> bool:
20+
return True
21+
1822
def get_metadata_distribution(self) -> BaseDistribution:
1923
"""Loads the metadata from the wheel file into memory and returns a
2024
Distribution that uses it, not relying on the wheel file or
@@ -23,7 +27,9 @@ def get_metadata_distribution(self) -> BaseDistribution:
2327
assert self.req.local_file_path, "Set as part of preparation during download"
2428
assert self.req.name, "Wheels are never unnamed"
2529
wheel = FilesystemWheel(self.req.local_file_path)
26-
return get_wheel_distribution(wheel, canonicalize_name(self.req.name))
30+
dist = get_wheel_distribution(wheel, canonicalize_name(self.req.name))
31+
self.req.cache_concrete_dist(dist)
32+
return dist
2733

2834
def prepare_distribution_metadata(
2935
self,

src/pip/_internal/metadata/base.py

+20
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ class RequiresEntry(NamedTuple):
105105

106106

107107
class BaseDistribution(Protocol):
108+
@property
109+
def is_concrete(self) -> bool:
110+
"""Whether the distribution really exists somewhere on disk.
111+
112+
If this is false, it has been synthesized from metadata via
113+
``.from_metadata_file_contents()``."""
114+
raise NotImplementedError()
115+
108116
@classmethod
109117
def from_directory(cls, directory: str) -> "BaseDistribution":
110118
"""Load the distribution from a metadata directory.
@@ -667,6 +675,10 @@ def iter_installed_distributions(
667675
class Wheel(Protocol):
668676
location: str
669677

678+
@property
679+
def is_concrete(self) -> bool:
680+
raise NotImplementedError()
681+
670682
def as_zipfile(self) -> zipfile.ZipFile:
671683
raise NotImplementedError()
672684

@@ -675,6 +687,10 @@ class FilesystemWheel(Wheel):
675687
def __init__(self, location: str) -> None:
676688
self.location = location
677689

690+
@property
691+
def is_concrete(self) -> bool:
692+
return True
693+
678694
def as_zipfile(self) -> zipfile.ZipFile:
679695
return zipfile.ZipFile(self.location, allowZip64=True)
680696

@@ -684,5 +700,9 @@ def __init__(self, location: str, stream: IO[bytes]) -> None:
684700
self.location = location
685701
self.stream = stream
686702

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

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,22 @@ def __init__(
9999
dist: importlib.metadata.Distribution,
100100
info_location: Optional[BasePath],
101101
installed_location: Optional[BasePath],
102+
concrete: bool,
102103
) -> None:
103104
self._dist = dist
104105
self._info_location = info_location
105106
self._installed_location = installed_location
107+
self._concrete = concrete
108+
109+
@property
110+
def is_concrete(self) -> bool:
111+
return self._concrete
106112

107113
@classmethod
108114
def from_directory(cls, directory: str) -> BaseDistribution:
109115
info_location = pathlib.Path(directory)
110116
dist = importlib.metadata.Distribution.at(info_location)
111-
return cls(dist, info_location, info_location.parent)
117+
return cls(dist, info_location, info_location.parent, concrete=True)
112118

113119
@classmethod
114120
def from_metadata_file_contents(
@@ -125,7 +131,7 @@ def from_metadata_file_contents(
125131
metadata_path.write_bytes(metadata_contents)
126132
# Construct dist pointing to the newly created directory.
127133
dist = importlib.metadata.Distribution.at(metadata_path.parent)
128-
return cls(dist, metadata_path.parent, None)
134+
return cls(dist, metadata_path.parent, None, concrete=False)
129135

130136
@classmethod
131137
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
@@ -136,7 +142,12 @@ def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
136142
raise InvalidWheel(wheel.location, name) from e
137143
except UnsupportedWheel as e:
138144
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
139-
return cls(dist, dist.info_location, pathlib.PurePosixPath(wheel.location))
145+
return cls(
146+
dist,
147+
dist.info_location,
148+
pathlib.PurePosixPath(wheel.location),
149+
concrete=wheel.is_concrete,
150+
)
140151

141152
@property
142153
def location(self) -> Optional[str]:

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

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

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

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

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

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

src/pip/_internal/metadata/pkg_resources.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,13 @@ def run_script(self, script_name: str, namespace: str) -> None:
6969

7070

7171
class Distribution(BaseDistribution):
72-
def __init__(self, dist: pkg_resources.Distribution) -> None:
72+
def __init__(self, dist: pkg_resources.Distribution, concrete: bool) -> None:
7373
self._dist = dist
74+
self._concrete = concrete
75+
76+
@property
77+
def is_concrete(self) -> bool:
78+
return self._concrete
7479

7580
@classmethod
7681
def from_directory(cls, directory: str) -> BaseDistribution:
@@ -90,7 +95,7 @@ def from_directory(cls, directory: str) -> BaseDistribution:
9095
dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
9196

9297
dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
93-
return cls(dist)
98+
return cls(dist, concrete=True)
9499

95100
@classmethod
96101
def from_metadata_file_contents(
@@ -107,7 +112,7 @@ def from_metadata_file_contents(
107112
metadata=InMemoryMetadata(metadata_dict, filename),
108113
project_name=project_name,
109114
)
110-
return cls(dist)
115+
return cls(dist, concrete=False)
111116

112117
@classmethod
113118
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
@@ -128,7 +133,7 @@ def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
128133
metadata=InMemoryMetadata(metadata_dict, wheel.location),
129134
project_name=name,
130135
)
131-
return cls(dist)
136+
return cls(dist, concrete=wheel.is_concrete)
132137

133138
@property
134139
def location(self) -> Optional[str]:
@@ -233,7 +238,7 @@ def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
233238

234239
def _iter_distributions(self) -> Iterator[BaseDistribution]:
235240
for dist in self._ws:
236-
yield Distribution(dist)
241+
yield Distribution(dist, concrete=True)
237242

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

src/pip/_internal/models/installation_report.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def _install_req_to_dict(cls, ireq: InstallRequirement) -> Dict[str, Any]:
2929
"requested": ireq.user_supplied,
3030
# PEP 566 json encoding for metadata
3131
# https://www.python.org/dev/peps/pep-0566/#json-compatible-metadata
32-
"metadata": ireq.get_dist().metadata_dict,
32+
"metadata": ireq.cached_dist.metadata_dict,
3333
}
3434
if ireq.user_supplied and ireq.extras:
3535
# For top level requirements, the list of requested extras, if any.

src/pip/_internal/operations/check.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
1010
from pip._vendor.packaging.version import LegacyVersion
1111

12-
from pip._internal.distributions import make_distribution_for_install_requirement
1312
from pip._internal.metadata import get_default_environment
1413
from pip._internal.metadata.base import DistributionVersion
1514
from pip._internal.req.req_install import InstallRequirement
@@ -127,8 +126,8 @@ def _simulate_installation_of(
127126

128127
# Modify it as installing requirement_set would (assuming no errors)
129128
for inst_req in to_install:
130-
abstract_dist = make_distribution_for_install_requirement(inst_req)
131-
dist = abstract_dist.get_metadata_distribution()
129+
assert inst_req.is_concrete
130+
dist = inst_req.cached_dist
132131
name = dist.canonical_name
133132
package_set[name] = PackageDetails(dist.version, list(dist.iter_dependencies()))
134133

0 commit comments

Comments
 (0)