|
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