Skip to content

Commit e098905

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 7cdd367 commit e098905

File tree

1 file changed

+105
-47
lines changed

1 file changed

+105
-47
lines changed

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

Lines changed: 105 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
import logging
33
import os
44
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, cast
5+
from pip._internal.models.direct_url import VcsInfo
6+
from pip._internal.utils.direct_url_helpers import link_matches_direct_url
7+
from pip._internal.vcs.versioncontrol import vcs
58

69
from pip._vendor.packaging.utils import canonicalize_name
710
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible
@@ -67,6 +70,94 @@ def __init__(
6770
self.upgrade_strategy = upgrade_strategy
6871
self._result: Optional[Result] = None
6972

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

103194
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
195+
for identifier, candidate in result.mapping.items():
196+
# Whether the candidate was resolved from direct URL requirements.
197+
direct_url_requested = any(
198+
requirement.get_candidate_lookup()[0] is not None
199+
for requirement in result.criteria[identifier].iter_requirement()
200+
)
108201

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:
202+
ireq = self._get_ireq(candidate, direct_url_requested)
203+
if ireq is None:
140204
continue
141205

142206
link = candidate.source_link
143207
if link and link.is_yanked:
144-
# The reason can contain non-ASCII characters, Unicode
145-
# is required for Python 2.
146-
msg = (
147-
"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+
reason = link.yanked_reason or "<none given>"
209+
logger.warning(
210+
f"The candidate selected for download or install is a "
211+
f"yanked version: {candidate.name!r} candidate "
212+
f"(version {candidate.version} at {link})\n"
213+
f"Reason for being yanked: {reason}"
155214
)
156-
logger.warning(msg)
157215

158216
req_set.add_named_requirement(ireq)
159217

0 commit comments

Comments
 (0)