Skip to content

Commit b9feb19

Browse files
authored
Merge pull request #4642 from pradyunsg/feature/idempotent-uninstall
2 parents 4370a5a + fb47e18 commit b9feb19

File tree

6 files changed

+37
-10
lines changed

6 files changed

+37
-10
lines changed

news/3016.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pip uninstall now ignores the absence of a requirement and prints a warning.

news/4642.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pip uninstall now ignores the absence of a requirement and prints a warning.

src/pip/_internal/commands/uninstall.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ def run(self, options, args):
6464
'"pip help %(name)s")' % dict(name=self.name)
6565
)
6666
for req in reqs_to_uninstall.values():
67-
req.uninstall(
67+
uninstall_pathset = req.uninstall(
6868
auto_confirm=options.yes, verbose=options.verbose != 0
6969
)
70-
req.uninstalled_pathset.commit()
70+
if uninstall_pathset:
71+
uninstall_pathset.commit()

src/pip/_internal/req/req_install.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -648,13 +648,13 @@ def uninstall(self, auto_confirm=False, verbose=False):
648648
649649
"""
650650
if not self.check_if_exists():
651-
raise UninstallationError(
652-
"Cannot uninstall requirement %s, not installed" % (self.name,)
653-
)
651+
logger.warning("Skipping %s as it is not installed.", self.name)
652+
return
654653
dist = self.satisfied_by or self.conflicts_with
655654

656-
self.uninstalled_pathset = UninstallPathSet.from_dist(dist)
657-
self.uninstalled_pathset.remove(auto_confirm, verbose)
655+
uninstalled_pathset = UninstallPathSet.from_dist(dist)
656+
uninstalled_pathset.remove(auto_confirm, verbose)
657+
return uninstalled_pathset
658658

659659
def archive(self, build_dir):
660660
assert self.source_dir

src/pip/_internal/req/req_set.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,9 @@ def install(self, install_options, global_options=(), *args, **kwargs):
216216
requirement.conflicts_with,
217217
)
218218
with indent_log():
219-
requirement.uninstall(auto_confirm=True)
219+
uninstalled_pathset = requirement.uninstall(
220+
auto_confirm=True
221+
)
220222
try:
221223
requirement.install(
222224
install_options,
@@ -231,15 +233,15 @@ def install(self, install_options, global_options=(), *args, **kwargs):
231233
)
232234
# if install did not succeed, rollback previous uninstall
233235
if should_rollback:
234-
requirement.uninstalled_pathset.rollback()
236+
uninstalled_pathset.rollback()
235237
raise
236238
else:
237239
should_commit = (
238240
requirement.conflicts_with and
239241
requirement.install_succeeded
240242
)
241243
if should_commit:
242-
requirement.uninstalled_pathset.commit()
244+
uninstalled_pathset.commit()
243245
requirement.remove_temporary_source()
244246

245247
return to_install

tests/functional/test_uninstall.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,3 +468,25 @@ def test_uninstall_editable_and_pip_install(script, data):
468468
) in uninstall2.files_deleted, list(uninstall2.files_deleted.keys())
469469
list_result2 = script.pip('list', '--format=json')
470470
assert "FSPkg" not in {p["name"] for p in json.loads(list_result2.stdout)}
471+
472+
473+
def test_uninstall_ignores_missing_packages(script, data):
474+
"""Uninstall of a non existent package prints a warning and exits cleanly
475+
"""
476+
result = script.pip(
477+
'uninstall', '-y', 'non-existent-pkg', expect_stderr=True,
478+
)
479+
480+
assert "Skipping non-existent-pkg as it is not installed." in result.stderr
481+
assert result.returncode == 0, "Expected clean exit"
482+
483+
484+
def test_uninstall_ignores_missing_packages_and_uninstalls_rest(script, data):
485+
script.pip_install_local('simple')
486+
result = script.pip(
487+
'uninstall', '-y', 'non-existent-pkg', 'simple', expect_stderr=True,
488+
)
489+
490+
assert "Skipping non-existent-pkg as it is not installed." in result.stderr
491+
assert "Successfully uninstalled simple" in result.stdout
492+
assert result.returncode == 0, "Expected clean exit"

0 commit comments

Comments
 (0)