diff --git a/news/1370.bugfix.md b/news/1370.bugfix.md new file mode 100644 index 0000000000..0a9bf877cc --- /dev/null +++ b/news/1370.bugfix.md @@ -0,0 +1 @@ +Fix unnecessary package downloads and VCS clones for certain commands. diff --git a/src/pdm/installers/synchronizers.py b/src/pdm/installers/synchronizers.py index 81acde1437..da73f471c1 100644 --- a/src/pdm/installers/synchronizers.py +++ b/src/pdm/installers/synchronizers.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import functools import multiprocessing import traceback @@ -11,7 +12,7 @@ from pdm import termui from pdm.exceptions import InstallationError from pdm.installers.manager import InstallManager -from pdm.models.candidates import Candidate +from pdm.models.candidates import Candidate, make_candidate from pdm.models.environment import Environment from pdm.models.requirements import parse_requirement, strip_extras from pdm.utils import is_editable @@ -140,8 +141,13 @@ def __init__( if editables is not None: candidates["editables"] = editables for key in keys: - if key in candidates: - candidates[key].req.editable = False + if key in candidates and candidates[key].req.editable: + # We do not do in-place update, which will break the caches + candidate = candidates[key] + req = dataclasses.replace(candidate.req, editable=False) + candidates[key] = make_candidate( + req, candidate.name, candidate.version, candidate.link + ) self.candidates = candidates self._manager: InstallManager | None = None diff --git a/src/pdm/models/requirements.py b/src/pdm/models/requirements.py index 7fbb6504bf..abfcd01456 100644 --- a/src/pdm/models/requirements.py +++ b/src/pdm/models/requirements.py @@ -60,7 +60,7 @@ def strip_extras(line: str) -> tuple[str, tuple[str, ...] | None]: return name, extras -@dataclasses.dataclass +@dataclasses.dataclass(eq=False) class Requirement: """Base class of a package requirement. A requirement is a (virtual) specification of a package which contains @@ -119,7 +119,7 @@ def __hash__(self) -> int: return hash(self._hash_key()) def __eq__(self, o: object) -> bool: - return isinstance(o, Requirement) and hash(self) == hash(o) + return isinstance(o, Requirement) and self._hash_key() == o._hash_key() @functools.lru_cache(maxsize=None) def identify(self) -> str: @@ -233,17 +233,14 @@ def _format_marker(self) -> str: return "" -@dataclasses.dataclass +@dataclasses.dataclass(eq=False) class NamedRequirement(Requirement): - def __hash__(self) -> int: - return hash(self._hash_key()) - def as_line(self) -> str: extras = f"[{','.join(sorted(self.extras))}]" if self.extras else "" return f"{self.project_name}{extras}{self.specifier}{self._format_marker()}" -@dataclasses.dataclass +@dataclasses.dataclass(eq=False) class FileRequirement(Requirement): url: str = "" path: Path | None = None @@ -260,9 +257,6 @@ def __post_init__(self) -> None: def _hash_key(self) -> tuple: return super()._hash_key() + (self.url, self.editable) - def __hash__(self) -> int: - return hash(self._hash_key()) - @classmethod def create(cls: type[T], **kwargs: Any) -> T: if kwargs.get("path"): @@ -380,15 +374,12 @@ def _check_installable(self) -> None: self.name = result.name -@dataclasses.dataclass +@dataclasses.dataclass(eq=False) class VcsRequirement(FileRequirement): vcs: str = "" ref: str | None = None revision: str | None = None - def __hash__(self) -> int: - return hash(self._hash_key()) - def __post_init__(self) -> None: super().__post_init__() if not self.vcs: diff --git a/src/pdm/resolver/providers.py b/src/pdm/resolver/providers.py index 6b08d17574..643ffe4218 100644 --- a/src/pdm/resolver/providers.py +++ b/src/pdm/resolver/providers.py @@ -6,6 +6,7 @@ from resolvelib import AbstractProvider from pdm.models.candidates import Candidate, make_candidate +from pdm.models.repositories import LockedRepository from pdm.models.requirements import parse_requirement, strip_extras from pdm.resolver.python import ( PythonCandidate, @@ -120,9 +121,12 @@ def get_override_candidates(self, identifier: str) -> Iterable[Candidate]: return self._find_candidates(parse_requirement(req)) def _find_candidates(self, requirement: Requirement) -> Iterable[Candidate]: - if not requirement.is_named: + if not requirement.is_named and not isinstance( + self.repository, LockedRepository + ): can = make_candidate(requirement) - can.prepare(self.repository.environment).metadata + if not can.name: + can.prepare(self.repository.environment).metadata return [can] else: return self.repository.find_candidates( @@ -165,6 +169,9 @@ def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> boo requirement.url # type: ignore ) version = candidate.version + if version is None: + # This should be a URL candidate, consider it to be matching + return True # Allow prereleases if: 1) it is not specified in the tool settings or # 2) the candidate doesn't come from PyPI index. allow_prereleases = (