Skip to content

Commit 5569c47

Browse files
committed
Rewrite direct URL reinstallation logic
This entirely rewrites the logic when an incoming to-be-installed resolved candidate is resolved from a direct URL requirement. The current logic is: * Always reinstall on --upgrade or --force-reinstall. * Always reinstall locally available wheels. * Always reinstall editables or if an installed editable should be changed to be non-editable. * Always reinstall if local PEP 610 information does not match the incoming candidate. This includes cases where the URLs are not sufficiently similar, or if the resolved VCS revisions differ. * Do not reinstall otherwise. Note that this slightly differs from the proposal raised in previous discussions, where a local non-PEP 508 path would be reinstalled. This is due to pip does not actually carry this information to the resolver and it's not possible to distinguish PEP 508 requirements from bare path arguments. The logic does not change how version-specified candidates are reinstalled.
1 parent 89d9ad2 commit 5569c47

File tree

1 file changed

+109
-51
lines changed

1 file changed

+109
-51
lines changed

src/pip/_internal/resolution/resolvelib/resolver.py

Lines changed: 109 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@
1010

1111
from pip._internal.cache import WheelCache
1212
from pip._internal.index.package_finder import PackageFinder
13+
from pip._internal.models.direct_url import VcsInfo
1314
from pip._internal.operations.prepare import RequirementPreparer
1415
from pip._internal.req.req_install import InstallRequirement
1516
from pip._internal.req.req_set import RequirementSet
1617
from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
17-
from pip._internal.resolution.resolvelib.provider import PipProvider
18-
from pip._internal.resolution.resolvelib.reporter import (
19-
PipDebuggingReporter,
20-
PipReporter,
21-
)
18+
from pip._internal.utils.direct_url_helpers import link_matches_direct_url
19+
from pip._internal.vcs.versioncontrol import vcs
2220

2321
from .base import Candidate, Requirement
2422
from .factory import Factory
23+
from .provider import PipProvider
24+
from .reporter import PipDebuggingReporter, PipReporter
2525

2626
if TYPE_CHECKING:
2727
from pip._vendor.resolvelib.resolvers import Result as RLResult
@@ -67,6 +67,94 @@ def __init__(
6767
self.upgrade_strategy = upgrade_strategy
6868
self._result: Optional[Result] = None
6969

70+
def _get_ireq(
71+
self,
72+
candidate: Candidate,
73+
direct_url_requested: bool,
74+
) -> Optional[InstallRequirement]:
75+
ireq = candidate.get_install_requirement()
76+
77+
# No ireq to install (e.g. extra-ed candidate). Skip.
78+
if ireq is None:
79+
return None
80+
81+
# The currently installed distribution of the same identifier.
82+
installed_dist = self.factory.get_dist_to_uninstall(candidate)
83+
84+
if installed_dist is None: # Not installed. Install incoming candidate.
85+
return ireq
86+
87+
# If we return this ireq, it should trigger uninstallation of the
88+
# existing distribution for reinstallation.
89+
ireq.should_reinstall = True
90+
91+
# Reinstall if --force-reinstall is set.
92+
if self.factory.force_reinstall:
93+
return ireq
94+
95+
# The artifact represented by the incoming candidate.
96+
cand_link = candidate.source_link
97+
98+
# The candidate does not point to an artifact. This means the resolver
99+
# has already decided the installed distribution is good enough.
100+
if cand_link is None:
101+
return None
102+
103+
# The incoming candidate was produced only from version requirements.
104+
# Reinstall if the installed distribution's version does not match.
105+
if not direct_url_requested:
106+
if installed_dist.version == candidate.version:
107+
return None
108+
return ireq
109+
110+
# At this point, the incoming candidate was produced from a direct URL.
111+
# Determine whether to upgrade based on flags and whether the installed
112+
# distribution was done via a direct URL.
113+
114+
# Always reinstall an incoming wheel candidate on the local filesystem.
115+
# This is quite fast anyway, and we can avoid drama when users want
116+
# their in-development direct URL requirement automatically reinstalled.
117+
if cand_link.is_file and cand_link.is_wheel:
118+
return ireq
119+
120+
# Reinstall if --upgrade is specified.
121+
if self.upgrade_strategy != "to-satisfy-only":
122+
return ireq
123+
124+
# The PEP 610 direct URL representation of the installed distribution.
125+
dist_direct_url = installed_dist.direct_url
126+
127+
# The incoming candidate was produced from a direct URL, but the
128+
# currently installed distribution was not. Always reinstall to be sure.
129+
if dist_direct_url is None:
130+
return ireq
131+
132+
# Editable candidate always triggers reinstallation.
133+
if candidate.is_editable:
134+
return ireq
135+
136+
# The currently installed distribution is editable, but the incoming
137+
# candidate is not. Uninstall the editable one to match.
138+
if installed_dist.editable:
139+
return ireq
140+
141+
# Reinstall if the direct URLs don't match.
142+
if not link_matches_direct_url(cand_link, dist_direct_url):
143+
return ireq
144+
145+
# If VCS, only reinstall if the resolved revisions don't match.
146+
cand_vcs = vcs.get_backend_for_scheme(cand_link.scheme)
147+
dist_direct_info = dist_direct_url.info
148+
if cand_vcs and ireq.source_dir and isinstance(dist_direct_info, VcsInfo):
149+
candidate_rev = cand_vcs.get_revision(ireq.source_dir)
150+
if candidate_rev != dist_direct_info.commit_id:
151+
return ireq
152+
153+
# Now we know both the installed distribution and incoming candidate
154+
# are based on direct URLs, neither are editable nor VCS, and point to
155+
# equivalent targets. They are probably the same, don't reinstall.
156+
return None
157+
70158
def resolve(
71159
self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
72160
) -> RequirementSet:
@@ -101,59 +189,29 @@ def resolve(
101189
raise error from e
102190

103191
req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
104-
for candidate in result.mapping.values():
105-
ireq = candidate.get_install_requirement()
106-
if ireq is None:
107-
continue
192+
for identifier, candidate in result.mapping.items():
193+
# Whether the candidate was resolved from direct URL requirements.
194+
direct_url_requested = any(
195+
requirement.get_candidate_lookup()[0] is not None
196+
for requirement in result.criteria[identifier].iter_requirement()
197+
)
108198

109-
# Check if there is already an installation under the same name,
110-
# and set a flag for later stages to uninstall it, if needed.
111-
installed_dist = self.factory.get_dist_to_uninstall(candidate)
112-
if installed_dist is None:
113-
# There is no existing installation -- nothing to uninstall.
114-
ireq.should_reinstall = False
115-
elif self.factory.force_reinstall:
116-
# The --force-reinstall flag is set -- reinstall.
117-
ireq.should_reinstall = True
118-
elif installed_dist.version != candidate.version:
119-
# The installation is different in version -- reinstall.
120-
ireq.should_reinstall = True
121-
elif candidate.is_editable or installed_dist.editable:
122-
# The incoming distribution is editable, or different in
123-
# editable-ness to installation -- reinstall.
124-
ireq.should_reinstall = True
125-
elif candidate.source_link and candidate.source_link.is_file:
126-
# The incoming distribution is under file://
127-
if candidate.source_link.is_wheel:
128-
# is a local wheel -- do nothing.
129-
logger.info(
130-
"%s is already installed with the same version as the "
131-
"provided wheel. Use --force-reinstall to force an "
132-
"installation of the wheel.",
133-
ireq.name,
134-
)
135-
continue
136-
137-
# is a local sdist or path -- reinstall
138-
ireq.should_reinstall = True
139-
else:
199+
ireq = self._get_ireq(candidate, direct_url_requested)
200+
if ireq is None:
140201
continue
141202

142203
link = candidate.source_link
143204
if link and link.is_yanked:
144-
# The reason can contain non-ASCII characters, Unicode
145-
# is required for Python 2.
146-
msg = (
205+
reason = link.yanked_reason or "<none given>"
206+
logger.warning(
147207
"The candidate selected for download or install is a "
148-
"yanked version: {name!r} candidate (version {version} "
149-
"at {link})\nReason for being yanked: {reason}"
150-
).format(
151-
name=candidate.name,
152-
version=candidate.version,
153-
link=link,
154-
reason=link.yanked_reason or "<none given>",
208+
"yanked version: %r candidate (version %s at %s)\n"
209+
"Reason for being yanked: %s",
210+
candidate.name,
211+
candidate.version,
212+
link,
213+
reason,
155214
)
156-
logger.warning(msg)
157215

158216
req_set.add_named_requirement(ireq)
159217

0 commit comments

Comments
 (0)