Skip to content

Commit b5dd279

Browse files
zoobacjerdonek
authored andcommitted
Make failed uninstalls roll back more reliably and better at avoiding naming conflicts (#6225)
1 parent 18be976 commit b5dd279

File tree

4 files changed

+28
-7
lines changed

4 files changed

+28
-7
lines changed

news/6194.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make failed uninstalls roll back more reliably and better at avoiding naming conflicts.

src/pip/_internal/req/req_uninstall.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pip._internal.utils.logging import indent_log
1616
from pip._internal.utils.misc import (
1717
FakeFile, ask, dist_in_usersite, dist_is_local, egg_link_path, is_local,
18-
normalize_path, renames,
18+
normalize_path, renames, rmtree,
1919
)
2020
from pip._internal.utils.temp_dir import AdjacentTempDirectory
2121

@@ -241,7 +241,10 @@ def _stash(self, path):
241241
best = AdjacentTempDirectory(os.path.dirname(path))
242242
best.create()
243243
self._save_dirs.append(best)
244-
return os.path.join(best.path, os.path.relpath(path, best.original))
244+
relpath = os.path.relpath(path, best.original)
245+
if not relpath or relpath == os.path.curdir:
246+
return best.path
247+
return os.path.join(best.path, relpath)
245248

246249
def remove(self, auto_confirm=False, verbose=False):
247250
"""Remove paths in ``self.paths`` with confirmation (unless
@@ -265,6 +268,13 @@ def remove(self, auto_confirm=False, verbose=False):
265268
new_path = self._stash(path)
266269
logger.debug('Removing file or directory %s', path)
267270
self._moved_paths.append((path, new_path))
271+
if os.path.isdir(path) and os.path.isdir(new_path):
272+
# If we're moving a directory, we need to
273+
# remove the destination first or else it will be
274+
# moved to inside the existing directory.
275+
# We just created new_path ourselves, so it will
276+
# be removable.
277+
os.rmdir(new_path)
268278
renames(path, new_path)
269279
for pth in self.pth.values():
270280
pth.remove()
@@ -311,9 +321,13 @@ def rollback(self):
311321
logger.info('Rolling back uninstall of %s', self.dist.project_name)
312322
for path, tmp_path in self._moved_paths:
313323
logger.debug('Replacing %s', path)
324+
if os.path.isdir(tmp_path) and os.path.isdir(path):
325+
rmtree(path)
314326
renames(tmp_path, path)
315327
for pth in self.pth.values():
316328
pth.rollback()
329+
for save_dir in self._save_dirs:
330+
save_dir.cleanup()
317331

318332
def commit(self):
319333
"""Remove temporary save dir: rollback will no longer be possible."""

src/pip/_internal/utils/temp_dir.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ class AdjacentTempDirectory(TempDirectory):
9898
9999
"""
100100
# The characters that may be used to name the temp directory
101-
LEADING_CHARS = "-~.+=%0123456789"
101+
# We always prepend a ~ and then rotate through these until
102+
# a usable name is found.
103+
# pkg_resources raises a different error for .dist-info folder
104+
# with leading '-' and invalid metadata
105+
LEADING_CHARS = "-~.=%0123456789"
102106

103107
def __init__(self, original, delete=None):
104108
super(AdjacentTempDirectory, self).__init__(delete=delete)
@@ -117,8 +121,8 @@ def _generate_names(cls, name):
117121
if name[i] in cls.LEADING_CHARS:
118122
continue
119123
for candidate in itertools.combinations_with_replacement(
120-
cls.LEADING_CHARS, i):
121-
new_name = ''.join(candidate) + name[i:]
124+
cls.LEADING_CHARS, i - 1):
125+
new_name = '~' + ''.join(candidate) + name[i:]
122126
if new_name != name:
123127
yield new_name
124128

tests/unit/test_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,10 @@ def names():
573573
assert not any(n == name for n in names())
574574

575575
# Check the first group are correct
576-
assert all(x == y for x, y in
577-
zip(some_names, [c + name[1:] for c in chars]))
576+
expected_names = ['~' + name[1:]]
577+
expected_names.extend('~' + c + name[2:] for c in chars)
578+
for x, y in zip(some_names, expected_names):
579+
assert x == y
578580

579581

580582
class TestGlibc(object):

0 commit comments

Comments
 (0)