Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/12186.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid downloading any dists in ``install --dry-run`` if PEP 658 ``.metadata`` files or lazy wheels are available.
1 change: 1 addition & 0 deletions news/12863.trivial.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Cache "concrete" dists by ``Distribution`` instead of ``InstallRequirement``.
1 change: 1 addition & 0 deletions news/12871.trivial.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Refactor much of ``RequirementPreparer`` to avoid duplicated code paths for metadata-only requirements.
5 changes: 3 additions & 2 deletions src/pip/_internal/commands/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ def run(self, options: Values, args: list[str]) -> int:
self.trace_basic_info(finder)

requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
preparer.finalize_linked_requirements(
requirement_set.requirements.values(), require_dist_files=True
)

downloaded: list[str] = []
for req in requirement_set.requirements.values():
Expand All @@ -137,8 +140,6 @@ def run(self, options: Values, args: list[str]) -> int:
preparer.save_linked_requirement(req)
downloaded.append(req.name)

preparer.prepare_linked_requirements_more(requirement_set.requirements.values())

if downloaded:
write_output("Successfully downloaded %s", " ".join(downloaded))

Expand Down
7 changes: 6 additions & 1 deletion src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ def add_options(self) -> None:
help=(
"Don't actually install anything, just print what would be. "
"Can be used in combination with --ignore-installed "
"to 'resolve' the requirements."
"to 'resolve' the requirements. If package metadata is available "
"or cached, --dry-run also avoids downloading the dependency at all."
),
)
self.cmd_opts.add_option(
Expand Down Expand Up @@ -393,6 +394,10 @@ def run(self, options: Values, args: list[str]) -> int:
requirement_set = resolver.resolve(
reqs, check_supported_wheels=not options.target_dir
)
preparer.finalize_linked_requirements(
requirement_set.requirements.values(),
require_dist_files=not options.dry_run,
)

if options.json_report_file:
report = InstallationReport(requirement_set.requirements_to_install)
Expand Down
5 changes: 3 additions & 2 deletions src/pip/_internal/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ def run(self, options: Values, args: list[str]) -> int:
self.trace_basic_info(finder)

requirement_set = resolver.resolve(reqs, check_supported_wheels=True)
preparer.finalize_linked_requirements(
requirement_set.requirements.values(), require_dist_files=True
)

reqs_to_build: list[InstallRequirement] = []
for req in requirement_set.requirements.values():
Expand All @@ -152,8 +155,6 @@ def run(self, options: Values, args: list[str]) -> int:
else:
reqs_to_build.append(req)

preparer.prepare_linked_requirements_more(requirement_set.requirements.values())

# build wheels
build_successes, build_failures = build(
reqs_to_build,
Expand Down
14 changes: 13 additions & 1 deletion src/pip/_internal/distributions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.distributions.installed import InstalledDistribution
from pip._internal.distributions.sdist import SourceDistribution
from pip._internal.distributions.wheel import WheelDistribution
from pip._internal.req.req_install import InstallRequirement
Expand All @@ -7,7 +8,18 @@
def make_distribution_for_install_requirement(
install_req: InstallRequirement,
) -> AbstractDistribution:
"""Returns a Distribution for the given InstallRequirement"""
"""Returns an AbstractDistribution for the given InstallRequirement.

As AbstractDistribution only covers installable artifacts, this method may only be
invoked at the conclusion of a resolve, when the RequirementPreparer has downloaded
the file corresponding to the resolved dist. Commands which intend to consume
metadata-only resolves without downloading should not call this method or
consume AbstractDistribution objects.
"""
# Only pre-installed requirements will have a .satisfied_by dist.
if install_req.satisfied_by:
return InstalledDistribution(install_req)

# Editable requirements will always be source distributions. They use the
# legacy logic until we create a modern standard for them.
if install_req.editable:
Expand Down
19 changes: 16 additions & 3 deletions src/pip/_internal/distributions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,17 @@ def build_tracker_id(self) -> str | None:

If None, then this dist has no work to do in the build tracker, and
``.prepare_distribution_metadata()`` will not be called."""
raise NotImplementedError()
...

@abc.abstractmethod
def get_metadata_distribution(self) -> BaseDistribution:
raise NotImplementedError()
"""Generate a concrete ``BaseDistribution`` instance for this artifact.

The implementation should also cache the result with
``self.req.cache_concrete_dist()`` so the distribution is available to other
users of the ``InstallRequirement``. This method is not called within the build
tracker context, so it should not identify any new setup requirements."""
...

@abc.abstractmethod
def prepare_distribution_metadata(
Expand All @@ -52,4 +58,11 @@ def prepare_distribution_metadata(
build_isolation: bool,
check_build_deps: bool,
) -> None:
raise NotImplementedError()
"""Generate the information necessary to extract metadata from the artifact.

This method will be executed within the context of ``BuildTracker#track()``, so
it needs to fully identify any setup requirements so they can be added to the
same active set of tracked builds, while ``.get_metadata_distribution()`` takes
care of generating and caching the ``BaseDistribution`` to expose to the rest of
the resolve."""
...
6 changes: 4 additions & 2 deletions src/pip/_internal/distributions/installed.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ def build_tracker_id(self) -> str | None:
return None

def get_metadata_distribution(self) -> BaseDistribution:
assert self.req.satisfied_by is not None, "not actually installed"
return self.req.satisfied_by
dist = self.req.satisfied_by
assert dist is not None, "not actually installed"
self.req.cache_concrete_dist(dist)
return dist

def prepare_distribution_metadata(
self,
Expand Down
18 changes: 14 additions & 4 deletions src/pip/_internal/distributions/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pip._internal.build_env import BuildEnvironment
from pip._internal.distributions.base import AbstractDistribution
from pip._internal.exceptions import InstallationError
from pip._internal.metadata import BaseDistribution
from pip._internal.metadata import BaseDistribution, get_directory_distribution
from pip._internal.utils.subprocess import runner_with_spinner_message

if TYPE_CHECKING:
Expand All @@ -24,13 +24,19 @@ class SourceDistribution(AbstractDistribution):
"""

@property
def build_tracker_id(self) -> str | None:
def build_tracker_id(self) -> str:
"""Identify this requirement uniquely by its link."""
assert self.req.link
return self.req.link.url_without_fragment

def get_metadata_distribution(self) -> BaseDistribution:
return self.req.get_dist()
assert (
self.req.metadata_directory
), "Set as part of .prepare_distribution_metadata()"
dist = get_directory_distribution(self.req.metadata_directory)
self.req.cache_concrete_dist(dist)
self.req.validate_sdist_metadata()
return dist

def prepare_distribution_metadata(
self,
Expand Down Expand Up @@ -69,7 +75,11 @@ def prepare_distribution_metadata(
self._raise_conflicts("the backend dependencies", conflicting)
if missing:
self._raise_missing_reqs(missing)
self.req.prepare_metadata()

# NB: we must still call .cache_concrete_dist() and .validate_sdist_metadata()
# before the InstallRequirement itself has been updated with the metadata from
# this directory!
self.req.prepare_metadata_directory()

def _prepare_build_backend(
self, build_env_installer: BuildEnvironmentInstaller
Expand Down
4 changes: 3 additions & 1 deletion src/pip/_internal/distributions/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ def get_metadata_distribution(self) -> BaseDistribution:
assert self.req.local_file_path, "Set as part of preparation during download"
assert self.req.name, "Wheels are never unnamed"
wheel = FilesystemWheel(self.req.local_file_path)
return get_wheel_distribution(wheel, canonicalize_name(self.req.name))
dist = get_wheel_distribution(wheel, canonicalize_name(self.req.name))
self.req.cache_concrete_dist(dist)
return dist

def prepare_distribution_metadata(
self,
Expand Down
21 changes: 21 additions & 0 deletions src/pip/_internal/metadata/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ class RequiresEntry(NamedTuple):


class BaseDistribution(Protocol):
@property
def is_concrete(self) -> bool:
"""Whether the distribution really exists somewhere on disk.

If this is false, it has been synthesized from metadata, e.g. via
``.from_metadata_file_contents()``, or ``.from_wheel()`` against
a ``MemoryWheel``."""
raise NotImplementedError()

@classmethod
def from_directory(cls, directory: str) -> BaseDistribution:
"""Load the distribution from a metadata directory.
Expand Down Expand Up @@ -664,6 +673,10 @@ def iter_installed_distributions(
class Wheel(Protocol):
location: str

@property
def is_concrete(self) -> bool:
raise NotImplementedError()

def as_zipfile(self) -> zipfile.ZipFile:
raise NotImplementedError()

Expand All @@ -672,6 +685,10 @@ class FilesystemWheel(Wheel):
def __init__(self, location: str) -> None:
self.location = location

@property
def is_concrete(self) -> bool:
return True

def as_zipfile(self) -> zipfile.ZipFile:
return zipfile.ZipFile(self.location, allowZip64=True)

Expand All @@ -681,5 +698,9 @@ def __init__(self, location: str, stream: IO[bytes]) -> None:
self.location = location
self.stream = stream

@property
def is_concrete(self) -> bool:
return False

def as_zipfile(self) -> zipfile.ZipFile:
return zipfile.ZipFile(self.stream, allowZip64=True)
19 changes: 16 additions & 3 deletions src/pip/_internal/metadata/importlib/_dists.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,22 @@ def __init__(
dist: importlib.metadata.Distribution,
info_location: BasePath | None,
installed_location: BasePath | None,
concrete: bool,
) -> None:
self._dist = dist
self._info_location = info_location
self._installed_location = installed_location
self._concrete = concrete

@property
def is_concrete(self) -> bool:
return self._concrete

@classmethod
def from_directory(cls, directory: str) -> BaseDistribution:
info_location = pathlib.Path(directory)
dist = importlib.metadata.Distribution.at(info_location)
return cls(dist, info_location, info_location.parent)
return cls(dist, info_location, info_location.parent, concrete=True)

@classmethod
def from_metadata_file_contents(
Expand All @@ -130,7 +136,7 @@ def from_metadata_file_contents(
metadata_path.write_bytes(metadata_contents)
# Construct dist pointing to the newly created directory.
dist = importlib.metadata.Distribution.at(metadata_path.parent)
return cls(dist, metadata_path.parent, None)
return cls(dist, metadata_path.parent, None, concrete=False)

@classmethod
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
Expand All @@ -139,7 +145,14 @@ def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
dist = WheelDistribution.from_zipfile(zf, name, wheel.location)
except zipfile.BadZipFile as e:
raise InvalidWheel(wheel.location, name) from e
return cls(dist, dist.info_location, pathlib.PurePosixPath(wheel.location))
except UnsupportedWheel as e:
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
return cls(
dist,
dist.info_location,
pathlib.PurePosixPath(wheel.location),
concrete=wheel.is_concrete,
)

@property
def location(self) -> str | None:
Expand Down
4 changes: 2 additions & 2 deletions src/pip/_internal/metadata/importlib/_envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def find(self, location: str) -> Iterator[BaseDistribution]:
installed_location: BasePath | None = None
else:
installed_location = info_location.parent
yield Distribution(dist, info_location, installed_location)
yield Distribution(dist, info_location, installed_location, concrete=True)

def find_legacy_editables(self, location: str) -> Iterator[BaseDistribution]:
"""Read location in egg-link files and return distributions in there.
Expand All @@ -110,7 +110,7 @@ def find_legacy_editables(self, location: str) -> Iterator[BaseDistribution]:
continue
target_location = str(path.joinpath(target_rel))
for dist, info_location in self._find_impl(target_location):
yield Distribution(dist, info_location, path)
yield Distribution(dist, info_location, path, concrete=True)


class Environment(BaseEnvironment):
Expand Down
15 changes: 10 additions & 5 deletions src/pip/_internal/metadata/pkg_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ def run_script(self, script_name: str, namespace: str) -> None:


class Distribution(BaseDistribution):
def __init__(self, dist: pkg_resources.Distribution) -> None:
def __init__(self, dist: pkg_resources.Distribution, concrete: bool) -> None:
self._dist = dist
self._concrete = concrete
# This is populated lazily, to avoid loading metadata for all possible
# distributions eagerly.
self.__extra_mapping: Mapping[NormalizedName, str] | None = None
Expand All @@ -93,6 +94,10 @@ def _extra_mapping(self) -> Mapping[NormalizedName, str]:

return self.__extra_mapping

@property
def is_concrete(self) -> bool:
return self._concrete

@classmethod
def from_directory(cls, directory: str) -> BaseDistribution:
dist_dir = directory.rstrip(os.sep)
Expand All @@ -111,7 +116,7 @@ def from_directory(cls, directory: str) -> BaseDistribution:
dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]

dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
return cls(dist)
return cls(dist, concrete=True)

@classmethod
def from_metadata_file_contents(
Expand All @@ -128,7 +133,7 @@ def from_metadata_file_contents(
metadata=InMemoryMetadata(metadata_dict, filename),
project_name=project_name,
)
return cls(dist)
return cls(dist, concrete=False)

@classmethod
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
Expand All @@ -149,7 +154,7 @@ def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
metadata=InMemoryMetadata(metadata_dict, wheel.location),
project_name=name,
)
return cls(dist)
return cls(dist, concrete=wheel.is_concrete)

@property
def location(self) -> str | None:
Expand Down Expand Up @@ -261,7 +266,7 @@ def from_paths(cls, paths: list[str] | None) -> BaseEnvironment:

def _iter_distributions(self) -> Iterator[BaseDistribution]:
for dist in self._ws:
yield Distribution(dist)
yield Distribution(dist, concrete=True)

def _search_distribution(self, name: str) -> BaseDistribution | None:
"""Find a distribution matching the ``name`` in the environment.
Expand Down
5 changes: 2 additions & 3 deletions src/pip/_internal/operations/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import Version

from pip._internal.distributions import make_distribution_for_install_requirement
from pip._internal.metadata import get_default_environment
from pip._internal.metadata.base import BaseDistribution
from pip._internal.req.req_install import InstallRequirement
Expand Down Expand Up @@ -148,8 +147,8 @@ def _simulate_installation_of(

# Modify it as installing requirement_set would (assuming no errors)
for inst_req in to_install:
abstract_dist = make_distribution_for_install_requirement(inst_req)
dist = abstract_dist.get_metadata_distribution()
assert inst_req.is_concrete
dist = inst_req.get_dist()
name = dist.canonical_name
package_set[name] = PackageDetails(dist.version, list(dist.iter_dependencies()))

Expand Down
Loading
Loading