Skip to content

Commit 141f731

Browse files
committed
Backport new readonly removal features from python
- Backport python/cpython#10320 (not yet merged) for better cleanup on `shutil.rmtree` failure (using `os.chflags`) - Add compatibility shims for `PermissionError` and `IsADirectory` exceptions - General cleanup - Fixes #38 Signed-off-by: Dan Ryan <[email protected]>
1 parent de65a20 commit 141f731

File tree

6 files changed

+71
-17
lines changed

6 files changed

+71
-17
lines changed

README.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,14 @@ Shims are provided for full API compatibility from python 2.7 through 3.7 for th
113113
* ``vistir.compat.JSONDecodeError``
114114
* ``vistir.compat.ResourceWarning``
115115
* ``vistir.compat.FileNotFoundError``
116+
* ``vistir.compat.PermissionError``
117+
* ``vistir.compat.IsADirectoryError``
116118

117-
The following additional function is provided for encoding strings to the filesystem
118-
defualt encoding:
119+
The following additional functions are provided for encoding strings to the filesystem
120+
default encoding:
119121

120122
* ``vistir.compat.fs_str``
123+
* ``vistir.compat.to_native_string``
121124

122125

123126
🐉 Context Managers

docs/quickstart.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Install from `PyPI`_:
2929

3030
::
3131

32-
$ pipenv install --pre vistir
32+
$ pipenv install vistir
3333

3434
Install from `Github`_:
3535

@@ -113,11 +113,14 @@ Shims are provided for full API compatibility from python 2.7 through 3.7 for th
113113
* :class:`~vistir.compat.JSONDecodeError`
114114
* :exc:`~vistir.compat.ResourceWarning`
115115
* :exc:`~vistir.compat.FileNotFoundError`
116+
* :exc:`~vistir.compat.PermissionError`
117+
* :exc:`~vistir.compat.IsADirectoryError`
116118

117119
The following additional function is provided for encoding strings to the filesystem
118120
defualt encoding:
119121

120122
* :func:`~vistir.compat.fs_str`
123+
* :func:`~vistir.compat.to_native_string`
121124

122125

123126
🐉 Context Managers

news/38.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Backported compatibility shims from ``CPython`` for improved cleanup of readonly temporary directories on cleanup.

src/vistir/compat.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"JSONDecodeError",
2020
"FileNotFoundError",
2121
"ResourceWarning",
22-
"FileNotFoundError",
22+
"PermissionError",
23+
"IsADirectoryError",
2324
"fs_str",
2425
"lru_cache",
2526
"TemporaryDirectory",
@@ -69,8 +70,17 @@ def __init__(self, *args, **kwargs):
6970
self.errno = errno.ENOENT
7071
super(FileNotFoundError, self).__init__(*args, **kwargs)
7172

73+
class PermissionError(OSError):
74+
def __init__(self, *args, **kwargs):
75+
self.errno = errno.EACCES
76+
super(PermissionError, self).__init__(*args, **kwargs)
77+
78+
class IsADirectoryError(OSError):
79+
"""The command does not work on directories"""
80+
pass
81+
7282
else:
73-
from builtins import ResourceWarning, FileNotFoundError
83+
from builtins import ResourceWarning, FileNotFoundError, PermissionError, IsADirectoryError
7484

7585

7686
if not sys.warnoptions:
@@ -111,9 +121,39 @@ def __init__(self, suffix="", prefix=None, dir=None):
111121
)
112122

113123
@classmethod
114-
def _cleanup(cls, name, warn_message):
124+
def _rmtree(cls, name):
115125
from .path import rmtree
116-
rmtree(name)
126+
127+
def onerror(func, path, exc_info):
128+
if issubclass(exc_info[0], (PermissionError, OSError)):
129+
try:
130+
try:
131+
if path != name:
132+
os.chflags(os.path.dirname(path), 0)
133+
os.chflags(path, 0)
134+
except AttributeError:
135+
pass
136+
if path != name:
137+
os.chmod(os.path.dirname(path), 0o70)
138+
os.chmod(path, 0o700)
139+
140+
try:
141+
os.unlink(path)
142+
# PermissionError is raised on FreeBSD for directories
143+
except (IsADirectoryError, PermissionError, OSError):
144+
cls._rmtree(path)
145+
except FileNotFoundError:
146+
pass
147+
elif issubclass(exc_info[0], FileNotFoundError):
148+
pass
149+
else:
150+
raise
151+
152+
rmtree(name, onerror=onerror)
153+
154+
@classmethod
155+
def _cleanup(cls, name, warn_message):
156+
cls._rmtree(name)
117157
warnings.warn(warn_message, ResourceWarning)
118158

119159
def __repr__(self):
@@ -126,16 +166,16 @@ def __exit__(self, exc, value, tb):
126166
self.cleanup()
127167

128168
def cleanup(self):
129-
from .path import rmtree
130169
if self._finalizer.detach():
131-
rmtree(self.name)
170+
self._rmtree(self.name)
132171

133172

134173
def fs_str(string):
135174
"""Encodes a string into the proper filesystem encoding
136175
137176
Borrowed from pip-tools
138177
"""
178+
139179
if isinstance(string, str):
140180
return string
141181
assert not isinstance(string, bytes)

src/vistir/path.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import posixpath
99
import shutil
1010
import stat
11-
import sys
1211
import warnings
1312

1413
import six
@@ -276,22 +275,26 @@ def set_write_bit(fn):
276275
file_stat = os.stat(fn).st_mode
277276
os.chmod(fn, file_stat | stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
278277
if not os.path.isdir(fn):
279-
return
278+
try:
279+
os.chflags(fn, 0)
280+
except AttributeError:
281+
pass
280282
for root, dirs, files in os.walk(fn, topdown=False):
281283
for dir_ in [os.path.join(root,d) for d in dirs]:
282284
set_write_bit(dir_)
283285
for file_ in [os.path.join(root, f) for f in files]:
284286
set_write_bit(file_)
285287

286288

287-
def rmtree(directory, ignore_errors=False):
289+
def rmtree(directory, ignore_errors=False, onerror=None):
288290
"""Stand-in for :func:`~shutil.rmtree` with additional error-handling.
289291
290292
This version of `rmtree` handles read-only paths, especially in the case of index
291293
files written by certain source control systems.
292294
293295
:param str directory: The target directory to remove
294296
:param bool ignore_errors: Whether to ignore errors, defaults to False
297+
:param func onerror: An error handling function, defaults to :func:`handle_remove_readonly`
295298
296299
.. note::
297300
@@ -301,9 +304,11 @@ def rmtree(directory, ignore_errors=False):
301304
from .compat import to_native_string
302305

303306
directory = to_native_string(directory)
307+
if onerror is None:
308+
onerror = handle_remove_readonly
304309
try:
305310
shutil.rmtree(
306-
directory, ignore_errors=ignore_errors, onerror=handle_remove_readonly
311+
directory, ignore_errors=ignore_errors, onerror=onerror
307312
)
308313
except (IOError, OSError, FileNotFoundError) as exc:
309314
# Ignore removal failures where the file doesn't exist
@@ -326,7 +331,9 @@ def handle_remove_readonly(func, path, exc):
326331
:func:`set_write_bit` on the target path and try again.
327332
"""
328333
# Check for read-only attribute
329-
from .compat import ResourceWarning, FileNotFoundError, to_native_string
334+
from .compat import (
335+
ResourceWarning, FileNotFoundError, PermissionError, to_native_string
336+
)
330337

331338
PERM_ERRORS = (errno.EACCES, errno.EPERM, errno.ENOENT)
332339
default_warning_message = (
@@ -340,7 +347,7 @@ def handle_remove_readonly(func, path, exc):
340347
set_write_bit(path)
341348
try:
342349
func(path)
343-
except (OSError, IOError, FileNotFoundError) as e:
350+
except (OSError, IOError, FileNotFoundError, PermissionError) as e:
344351
if e.errno == errno.ENOENT:
345352
return
346353
elif e.errno in PERM_ERRORS:
@@ -351,7 +358,7 @@ def handle_remove_readonly(func, path, exc):
351358
set_write_bit(path)
352359
try:
353360
func(path)
354-
except (OSError, IOError, FileNotFoundError) as e:
361+
except (OSError, IOError, FileNotFoundError, PermissionError) as e:
355362
if e.errno in PERM_ERRORS:
356363
warnings.warn(default_warning_message.format(path), ResourceWarning)
357364
pass

src/vistir/spin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,6 @@ def _clear_line():
292292
def create_spinner(*args, **kwargs):
293293
nospin = kwargs.pop("nospin", False)
294294
use_yaspin = kwargs.pop("use_yaspin", nospin)
295-
if nospin:
295+
if nospin or not use_yaspin:
296296
return DummySpinner(*args, **kwargs)
297297
return VistirSpinner(*args, **kwargs)

0 commit comments

Comments
 (0)