Skip to content

Commit 254414b

Browse files
authored
Merge pull request #9147 from pradyunsg/reinstall-local-distributions
Always reinstall local distributions provided by the user
2 parents 8438271 + b2785d8 commit 254414b

File tree

5 files changed

+101
-54
lines changed

5 files changed

+101
-54
lines changed

src/pip/_internal/index/collector.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from pip._internal.models.search_scope import SearchScope
2323
from pip._internal.network.utils import raise_for_status
2424
from pip._internal.utils.compat import lru_cache
25-
from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS
25+
from pip._internal.utils.filetypes import is_archive_file
2626
from pip._internal.utils.misc import pairwise, redact_auth_from_url
2727
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
2828
from pip._internal.utils.urls import path_to_url, url_to_path
@@ -65,17 +65,6 @@ def _match_vcs_scheme(url):
6565
return None
6666

6767

68-
def _is_url_like_archive(url):
69-
# type: (str) -> bool
70-
"""Return whether the URL looks like an archive.
71-
"""
72-
filename = Link(url).filename
73-
for bad_ext in ARCHIVE_EXTENSIONS:
74-
if filename.endswith(bad_ext):
75-
return True
76-
return False
77-
78-
7968
class _NotHTML(Exception):
8069
def __init__(self, content_type, request_desc):
8170
# type: (str, str) -> None
@@ -130,7 +119,7 @@ def _get_html_response(url, session):
130119
3. Check the Content-Type header to make sure we got HTML, and raise
131120
`_NotHTML` otherwise.
132121
"""
133-
if _is_url_like_archive(url):
122+
if is_archive_file(Link(url).filename):
134123
_ensure_html_response(url, session=session)
135124

136125
logger.debug('Getting page %s', redact_auth_from_url(url))

src/pip/_internal/req/constructors.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
from pip._internal.pyproject import make_pyproject_path
2525
from pip._internal.req.req_install import InstallRequirement
2626
from pip._internal.utils.deprecation import deprecated
27-
from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS
28-
from pip._internal.utils.misc import is_installable_dir, splitext
27+
from pip._internal.utils.filetypes import is_archive_file
28+
from pip._internal.utils.misc import is_installable_dir
2929
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
3030
from pip._internal.utils.urls import path_to_url
3131
from pip._internal.vcs import is_url, vcs
@@ -45,15 +45,6 @@
4545
operators = Specifier._operators.keys()
4646

4747

48-
def is_archive_file(name):
49-
# type: (str) -> bool
50-
"""Return True if `name` is a considered as an archive file."""
51-
ext = splitext(name)[1].lower()
52-
if ext in ARCHIVE_EXTENSIONS:
53-
return True
54-
return False
55-
56-
5748
def _strip_extras(path):
5849
# type: (str) -> Tuple[str, Optional[str]]
5950
m = re.match(r'^(.+)(\[[^\]]+\])$', path)

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
PipDebuggingReporter,
1717
PipReporter,
1818
)
19+
from pip._internal.utils.deprecation import deprecated
20+
from pip._internal.utils.filetypes import is_archive_file
1921
from pip._internal.utils.misc import dist_is_editable
2022
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
2123

@@ -132,12 +134,14 @@ def resolve(self, root_reqs, check_supported_wheels):
132134

133135
# Check if there is already an installation under the same name,
134136
# and set a flag for later stages to uninstall it, if needed.
135-
# * There isn't, good -- no uninstalltion needed.
137+
#
138+
# * There is no existing installation. Nothing to uninstall.
136139
# * The --force-reinstall flag is set. Always reinstall.
137140
# * The installation is different in version or editable-ness, so
138141
# we need to uninstall it to install the new distribution.
139-
# * The installed version is the same as the pending distribution.
140-
# Skip this distrubiton altogether to save work.
142+
# * The candidate is a local wheel. Do nothing.
143+
# * The candidate is a local sdist. Print a deprecation warning.
144+
# * The candidate is a local path. Always reinstall.
141145
installed_dist = self.factory.get_dist_to_uninstall(candidate)
142146
if installed_dist is None:
143147
ireq.should_reinstall = False
@@ -147,6 +151,34 @@ def resolve(self, root_reqs, check_supported_wheels):
147151
ireq.should_reinstall = True
148152
elif dist_is_editable(installed_dist) != candidate.is_editable:
149153
ireq.should_reinstall = True
154+
elif candidate.source_link.is_file:
155+
if candidate.source_link.is_wheel:
156+
logger.info(
157+
"%s is already installed with the same version as the "
158+
"provided wheel. Use --force-reinstall to force an "
159+
"installation of the wheel.",
160+
ireq.name,
161+
)
162+
continue
163+
164+
looks_like_sdist = (
165+
is_archive_file(candidate.source_link.file_path)
166+
and candidate.source_link.ext != ".zip"
167+
)
168+
if looks_like_sdist:
169+
reason = (
170+
"Source distribution is being reinstalled despite an "
171+
"installed package having the same name and version as "
172+
"the installed package."
173+
)
174+
replacement = "use --force-reinstall"
175+
deprecated(
176+
reason=reason,
177+
replacement=replacement,
178+
gone_in="21.1",
179+
issue=8711,
180+
)
181+
ireq.should_reinstall = True
150182
else:
151183
continue
152184

src/pip/_internal/utils/filetypes.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Filetype information.
22
"""
3+
from pip._internal.utils.misc import splitext
34
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
45

56
if MYPY_CHECK_RUNNING:
@@ -14,3 +15,12 @@
1415
ARCHIVE_EXTENSIONS = (
1516
ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS
1617
)
18+
19+
20+
def is_archive_file(name):
21+
# type: (str) -> bool
22+
"""Return True if `name` is a considered as an archive file."""
23+
ext = splitext(name)[1].lower()
24+
if ext in ARCHIVE_EXTENSIONS:
25+
return True
26+
return False

tests/functional/test_new_resolver.py

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,44 +1152,69 @@ def test_new_resolver_check_wheel_version_normalized(
11521152
assert_installed(script, simple="0.1.0+local.1")
11531153

11541154

1155-
def test_new_resolver_contraint_on_dep_with_extra(script):
1156-
create_basic_wheel_for_package(
1155+
def test_new_resolver_does_reinstall_local_sdists(script):
1156+
archive_path = create_basic_sdist_for_package(
11571157
script,
1158-
name="simple",
1159-
version="1",
1160-
depends=["dep[x]"],
1158+
"pkg",
1159+
"1.0",
11611160
)
1162-
create_basic_wheel_for_package(
1163-
script,
1164-
name="dep",
1165-
version="1",
1166-
extras={"x": ["depx==1"]},
1161+
script.pip(
1162+
"install", "--no-cache-dir", "--no-index",
1163+
archive_path,
11671164
)
1168-
create_basic_wheel_for_package(
1169-
script,
1170-
name="dep",
1171-
version="2",
1172-
extras={"x": ["depx==2"]},
1165+
assert_installed(script, pkg="1.0")
1166+
1167+
result = script.pip(
1168+
"install", "--no-cache-dir", "--no-index",
1169+
archive_path,
1170+
expect_stderr=True,
11731171
)
1174-
create_basic_wheel_for_package(
1172+
assert "Installing collected packages: pkg" in result.stdout, str(result)
1173+
assert "DEPRECATION" in result.stderr, str(result)
1174+
assert_installed(script, pkg="1.0")
1175+
1176+
1177+
def test_new_resolver_does_reinstall_local_paths(script):
1178+
pkg = create_test_package_with_setup(
11751179
script,
1176-
name="depx",
1177-
version="1",
1180+
name="pkg",
1181+
version="1.0"
11781182
)
1179-
create_basic_wheel_for_package(
1180-
script,
1181-
name="depx",
1182-
version="2",
1183+
script.pip(
1184+
"install", "--no-cache-dir", "--no-index",
1185+
pkg,
11831186
)
1187+
assert_installed(script, pkg="1.0")
1188+
1189+
result = script.pip(
1190+
"install", "--no-cache-dir", "--no-index",
1191+
pkg,
1192+
)
1193+
assert "Installing collected packages: pkg" in result.stdout, str(result)
1194+
assert_installed(script, pkg="1.0")
11841195

1185-
constraints_txt = script.scratch_path / "constraints.txt"
1186-
constraints_txt.write_text("dep==1")
11871196

1197+
def test_new_resolver_does_not_reinstall_when_from_a_local_index(script):
1198+
create_basic_sdist_for_package(
1199+
script,
1200+
"simple",
1201+
"0.1.0",
1202+
)
11881203
script.pip(
11891204
"install",
11901205
"--no-cache-dir", "--no-index",
11911206
"--find-links", script.scratch_path,
1192-
"--constraint", constraints_txt,
1193-
"simple",
1207+
"simple"
1208+
)
1209+
assert_installed(script, simple="0.1.0")
1210+
1211+
result = script.pip(
1212+
"install",
1213+
"--no-cache-dir", "--no-index",
1214+
"--find-links", script.scratch_path,
1215+
"simple"
11941216
)
1195-
assert_installed(script, simple="1", dep="1", depx="1")
1217+
# Should not reinstall!
1218+
assert "Installing collected packages: simple" not in result.stdout, str(result)
1219+
assert "Requirement already satisfied: simple" in result.stdout, str(result)
1220+
assert_installed(script, simple="0.1.0")

0 commit comments

Comments
 (0)