Skip to content

Commit ec4c413

Browse files
committed
Wait/retry when cannot delete file on Windows
1 parent 0a9f88d commit ec4c413

File tree

5 files changed

+104
-4
lines changed

5 files changed

+104
-4
lines changed

mypy/build.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -901,7 +901,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
901901
f.write(data_str)
902902
f.write('\n')
903903
data_mtime = os.path.getmtime(data_json_tmp)
904-
os.replace(data_json_tmp, data_json)
904+
util.replace(data_json_tmp, data_json)
905905
manager.trace("Interface for {} has changed".format(id))
906906

907907
mtime = st.st_mtime
@@ -927,7 +927,7 @@ def write_cache(id: str, path: str, tree: MypyFile,
927927
json.dump(meta, f, indent=2, sort_keys=True)
928928
else:
929929
json.dump(meta, f)
930-
os.replace(meta_json_tmp, meta_json)
930+
util.replace(meta_json_tmp, meta_json)
931931

932932
return interface_hash
933933

mypy/test/testutil.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import sys
2+
from typing import Type, Callable, List
3+
import time
4+
try:
5+
import collections.abc as collections_abc
6+
except ImportError:
7+
import collections as collections_abc # type: ignore # PY32 and earlier
8+
from unittest import TestCase, main, skipUnless
9+
from mypy import util
10+
11+
12+
def create_bad_function(lag: float, exc: BaseException) -> Callable[[], None]:
13+
start_time = time.perf_counter()
14+
15+
def f() -> None:
16+
if time.perf_counter() - start_time < lag:
17+
raise exc
18+
else:
19+
return
20+
return f
21+
22+
23+
def create_funcs() -> List[Callable[[], None]]:
24+
25+
def linux_function() -> None: pass
26+
windows_function1 = create_bad_function(0.01, PermissionError())
27+
windows_function2 = create_bad_function(0.02, FileExistsError())
28+
return [windows_function1, windows_function2, linux_function]
29+
30+
31+
class WaitRetryTests(TestCase):
32+
def test_waitfor(self) -> None:
33+
with self.assertRaises(OSError):
34+
util.wait_for(create_funcs(), (PermissionError, FileExistsError), 0.01)
35+
util.wait_for(create_funcs(), (PermissionError, FileExistsError), 1)
36+
with self.assertRaises(FileExistsError):
37+
util.wait_for(create_funcs(), (PermissionError,), 0.04)
38+
39+
40+
if __name__ == '__main__':
41+
main()

mypy/util.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@
22

33
import re
44
import subprocess
5+
import os
6+
import sys
7+
import math
8+
import time
9+
from functools import partial
510
from xml.sax.saxutils import escape
6-
from typing import TypeVar, List, Tuple, Optional, Sequence, Dict
11+
from typing import (
12+
TypeVar, List, Tuple, Optional, Sequence, Dict, Callable, Iterable, Type, Union, AnyStr, cast
13+
)
714

815

916
T = TypeVar('T')
@@ -134,3 +141,54 @@ def id(self, o: object) -> int:
134141
self.id_map[o] = self.next_id
135142
self.next_id += 1
136143
return self.id_map[o]
144+
145+
146+
# default timeout is short in case this functions is called individually for many files
147+
# batch processing results in better performance
148+
def wait_for(funcs: Iterable[Callable[[], None]],
149+
exc: Iterable[Type[BaseException]] = (),
150+
timeout: float = 0.1,
151+
msg: str = 'file operations') -> None:
152+
'''
153+
Execute functions in funcs (without arguments)
154+
Wait and retry all functions that raised exception that matches exc
155+
Increase wait time exponentially, give up after a total wait of timeout seconds
156+
Reraises the latest exception seen if timeout exceeded
157+
'''
158+
exc = tuple(exc)
159+
last_exc = None
160+
pending = set(funcs)
161+
n_iter = max(1, math.ceil(math.log2(timeout / 0.001)))
162+
for i in range(n_iter):
163+
if not pending:
164+
return
165+
# last wait is ~ timeout/2, so that total wait ~ timeout
166+
wait = timeout / 2 ** (n_iter - i)
167+
failed = set()
168+
for func in pending:
169+
try:
170+
func()
171+
except exc as e:
172+
last_exc = e
173+
failed.add(func)
174+
pending = failed
175+
time.sleep(wait)
176+
sys.stderr.write('timed out waiting for {}'.format(msg))
177+
raise last_exc
178+
179+
180+
if sys.version_info >= (3, 6):
181+
PathType = Union[AnyStr, os.PathLike]
182+
else:
183+
PathType = AnyStr
184+
185+
186+
def _replace(src: PathType, dest: PathType) -> None:
187+
repl = cast(Callable[[], None], partial(os.replace, src, dest))
188+
wait_for([repl], (OSError,), 0.1)
189+
190+
191+
if sys.platform.startswith("win"):
192+
replace = _replace
193+
else:
194+
replace = os.replace

runtests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ def add_imports(driver: Driver) -> None:
215215
'testdiff',
216216
'testfinegrained',
217217
'testmerge',
218+
'testutil',
218219
]]
219220

220221

0 commit comments

Comments
 (0)