diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index f6e6e291ca7..e729b82d958 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -181,8 +181,14 @@ def rmtree_errorhandler(func, path, exc_info): """On Windows, the files in .svn are read-only, so when rmtree() tries to remove them, an exception is thrown. We catch that here, remove the read-only attribute, and hopefully continue without problems.""" + try: + is_readonly = os.stat(path).st_mode & stat.S_IREAD + except (IOError, OSError): + # Since the path already removed we don't raise the error + return + # if file type currently read only - if os.stat(path).st_mode & stat.S_IREAD: + if is_readonly: # convert to read/write os.chmod(path, stat.S_IWRITE) # use the original function to repeat the operation diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 6c1ad16f807..00f17ac8324 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -59,6 +59,7 @@ redact_netloc, remove_auth_from_url, rmtree, + rmtree_errorhandler, split_auth_from_netloc, split_auth_netloc_from_url, untar_file, @@ -386,6 +387,61 @@ def test_unpack_zip(self, data): self.confirm_files() +def test_rmtree_errorhandler_not_existing_directory(tmpdir): + """ + Test `rmtree_errorhandler` ignores the given non-existing directory. + """ + not_existing_path = str(tmpdir / 'foo') + func_mock = Mock() + rmtree_errorhandler(func_mock, not_existing_path, None) + func_mock.assert_not_called() + + +def test_rmtree_errorhandler_readonly_directory(tmpdir): + """ + Test `rmtree_errorhandler` makes the given read-only directory writable. + """ + # Create read only directory + path = str((tmpdir / 'foo').mkdir()) + os.chmod(path, stat.S_IREAD) + + # Make sure func_mock is called with the given path + func_mock = Mock() + rmtree_errorhandler(func_mock, path, None) + func_mock.assert_called_with(path) + + # Make sure the path is became writable + assert os.stat(path).st_mode & stat.S_IWRITE + + +def test_rmtree_errorhandler_reraises_error(tmpdir): + """ + Test `rmtree_errorhandler` re-raises an error + by the given non-readonly directory. + """ + # Create write only directory + path = str((tmpdir / 'foo').mkdir()) + os.chmod(path, stat.S_IWRITE) + + # Make sure the handler re-raises an exception. + # Note that the raise statement without expression and + # active exception in the current scope throws + # the RuntimeError on python3 and the TypeError on python2. + func_mock = Mock() + with pytest.raises((RuntimeError, TypeError)): + rmtree_errorhandler(func_mock, path, None) + + func_mock.assert_not_called() + + +def test_rmtree_skips_not_existing_directory(): + """ + Test wrapped `rmtree` doesn't raise an error + by the given non-existing directory. + """ + rmtree.__wrapped__('foo') + + class Failer: def __init__(self, duration=1): self.succeed_after = time.time() + duration