|
11 | 11 | import sys
|
12 | 12 | import sysconfig
|
13 | 13 | import urllib.parse
|
| 14 | +from functools import partial |
14 | 15 | from io import StringIO
|
15 | 16 | from itertools import filterfalse, tee, zip_longest
|
16 | 17 | from types import TracebackType
|
@@ -123,33 +124,66 @@ def get_prog() -> str:
|
123 | 124 | # Retry every half second for up to 3 seconds
|
124 | 125 | # Tenacity raises RetryError by default, explicitly raise the original exception
|
125 | 126 | @retry(reraise=True, stop=stop_after_delay(3), wait=wait_fixed(0.5))
|
126 |
| -def rmtree(dir: str, ignore_errors: bool = False) -> None: |
| 127 | +def rmtree( |
| 128 | + dir: str, |
| 129 | + ignore_errors: bool = False, |
| 130 | + onexc: Optional[Callable[[Any, Any, Any], Any]] = None, |
| 131 | +) -> None: |
| 132 | + if ignore_errors: |
| 133 | + onexc = _onerror_ignore |
| 134 | + elif onexc is None: |
| 135 | + onexc = _onerror_reraise |
127 | 136 | if sys.version_info >= (3, 12):
|
128 |
| - shutil.rmtree(dir, ignore_errors=ignore_errors, onexc=rmtree_errorhandler) |
| 137 | + shutil.rmtree(dir, onexc=partial(rmtree_errorhandler, onexc=onexc)) |
129 | 138 | else:
|
130 |
| - shutil.rmtree(dir, ignore_errors=ignore_errors, onerror=rmtree_errorhandler) |
| 139 | + shutil.rmtree(dir, onerror=partial(rmtree_errorhandler, onexc=onexc)) |
| 140 | + |
| 141 | + |
| 142 | +def _onerror_ignore(*_args: Any) -> None: |
| 143 | + pass |
| 144 | + |
| 145 | + |
| 146 | +def _onerror_reraise(*_args: Any) -> None: |
| 147 | + raise |
131 | 148 |
|
132 | 149 |
|
133 | 150 | def rmtree_errorhandler(
|
134 |
| - func: Callable[..., Any], path: str, exc_info: Union[ExcInfo, BaseException] |
| 151 | + func: Callable[..., Any], |
| 152 | + path: str, |
| 153 | + exc_info: Union[ExcInfo, BaseException], |
| 154 | + *, |
| 155 | + onexc: Callable[..., Any] = _onerror_reraise, |
135 | 156 | ) -> None:
|
136 |
| - """On Windows, the files in .svn are read-only, so when rmtree() tries to |
137 |
| - remove them, an exception is thrown. We catch that here, remove the |
138 |
| - read-only attribute, and hopefully continue without problems.""" |
| 157 | + """ |
| 158 | + `rmtree` error handler to 'force' a file remove (i.e. like `rm -f`). |
| 159 | +
|
| 160 | + * If a file is readonly then it's write flag is set and operation is |
| 161 | + retried. |
| 162 | +
|
| 163 | + * `onerror` is the original callback from `rmtree(... onerror=onerror)` |
| 164 | + that is chained at the end if the "rm -f" still fails. |
| 165 | + """ |
139 | 166 | try:
|
140 |
| - has_attr_readonly = not (os.stat(path).st_mode & stat.S_IWRITE) |
| 167 | + st_mode = os.stat(path).st_mode |
141 | 168 | except OSError:
|
142 | 169 | # it's equivalent to os.path.exists
|
143 | 170 | return
|
144 | 171 |
|
145 |
| - if has_attr_readonly: |
| 172 | + if not st_mode & stat.S_IWRITE: |
146 | 173 | # convert to read/write
|
147 |
| - os.chmod(path, stat.S_IWRITE) |
148 |
| - # use the original function to repeat the operation |
149 |
| - func(path) |
150 |
| - return |
151 |
| - else: |
152 |
| - raise |
| 174 | + try: |
| 175 | + os.chmod(path, st_mode | stat.S_IWRITE) |
| 176 | + except OSError: |
| 177 | + pass |
| 178 | + else: |
| 179 | + # use the original function to repeat the operation |
| 180 | + try: |
| 181 | + func(path) |
| 182 | + return |
| 183 | + except OSError: |
| 184 | + pass |
| 185 | + |
| 186 | + onexc(func, path, exc_info) |
153 | 187 |
|
154 | 188 |
|
155 | 189 | def display_path(path: str) -> str:
|
|
0 commit comments