Skip to content

Commit b089a79

Browse files
committed
Simplify wait_for; test util.replace directly
1 parent ff27b6e commit b089a79

File tree

2 files changed

+83
-65
lines changed

2 files changed

+83
-65
lines changed

mypy/test/testutil.py

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,78 @@
1-
import sys
2-
from typing import Type, Callable, List
1+
from typing import Iterator
32
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
3+
import os
4+
import sys
5+
import tempfile
6+
from contextlib import contextmanager
7+
from threading import Thread
88
from unittest import TestCase, main, skipUnless
99
from mypy import util
1010

1111

12-
def create_bad_function(lag: float, exc: BaseException) -> Callable[[], None]:
13-
start_time = time.perf_counter()
12+
WIN32 = sys.platform.startswith("win")
13+
14+
15+
@contextmanager
16+
def lock_file(filename: str, duration: float) -> Iterator[Thread]:
17+
'''
18+
Opens filename (which must exist) for reading
19+
After duration sec, releases the handle
20+
'''
21+
def _lock_file() -> None:
22+
with open(filename):
23+
time.sleep(duration)
24+
t = Thread(target=_lock_file, daemon=True)
25+
t.start()
26+
yield t
27+
t.join()
28+
29+
30+
@skipUnless(WIN32, "only relevant for Windows")
31+
class ReliableReplace(TestCase):
32+
tmpdir = tempfile.TemporaryDirectory(prefix='mypy-test-',
33+
dir=os.path.abspath('tmp-test-dirs'))
34+
src = os.path.join(tmpdir.name, 'tmp1')
35+
dest = os.path.join(tmpdir.name, 'tmp2')
36+
37+
@classmethod
38+
def tearDownClass(cls) -> None:
39+
cls.tmpdir.cleanup()
40+
41+
def setUp(self) -> None:
42+
# create two temporary files
43+
for fname in (self.src, self.dest):
44+
with open(fname, 'w') as f:
45+
f.write(fname)
46+
47+
def replace_ok(self) -> None:
48+
util._replace(self.src, self.dest, timeout=0.25)
49+
self.assertEqual(open(self.dest).read(), self.src, 'replace failed')
1450

15-
def f() -> None:
16-
if time.perf_counter() - start_time < lag:
17-
raise exc
18-
else:
19-
return
20-
return f
51+
def test_normal(self) -> None:
52+
self.replace_ok()
2153

54+
def test_problem_exists(self) -> None:
55+
with lock_file(self.src, 0.1):
56+
with self.assertRaises(PermissionError):
57+
os.replace(self.src, self.dest)
2258

23-
def create_funcs() -> List[Callable[[], None]]:
59+
def test_short_lock_src(self) -> None:
60+
with lock_file(self.src, 0.1):
61+
self.replace_ok()
2462

25-
def linux_function() -> None: pass
26-
windows_function1 = create_bad_function(0.1, PermissionError())
27-
windows_function2 = create_bad_function(0.2, FileExistsError())
28-
return [windows_function1, windows_function2, linux_function]
63+
def test_short_lock_dest(self) -> None:
64+
with lock_file(self.dest, 0.1):
65+
self.replace_ok()
2966

67+
def test_long_lock_src(self) -> None:
68+
with lock_file(self.src, 0.4):
69+
with self.assertRaises(PermissionError):
70+
self.replace_ok()
3071

31-
class WaitRetryTests(TestCase):
32-
def test_waitfor(self) -> None:
33-
with self.assertRaises(OSError):
34-
util.wait_for(create_funcs(), (PermissionError, FileExistsError), 0.1)
35-
util.wait_for(create_funcs(), (PermissionError, FileExistsError), 1)
36-
util.wait_for(create_funcs(), (OSError,), 1)
37-
with self.assertRaises(FileExistsError):
38-
util.wait_for(create_funcs(), (PermissionError,), 0.4)
72+
def test_long_lock_dest(self) -> None:
73+
with lock_file(self.dest, 0.4):
74+
with self.assertRaises(PermissionError):
75+
self.replace_ok()
3976

4077

4178
if __name__ == '__main__':

mypy/util.py

Lines changed: 19 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import sys
77
import math
88
import time
9-
from functools import partial
109
from xml.sax.saxutils import escape
1110
from typing import (
1211
TypeVar, List, Tuple, Optional, Sequence, Dict, Callable, Iterable, Type, Union, AnyStr, cast
@@ -143,49 +142,31 @@ def id(self, o: object) -> int:
143142
return self.id_map[o]
144143

145144

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-
180145
if sys.version_info >= (3, 6):
181146
PathType = Union[AnyStr, os.PathLike]
182147
else:
183148
PathType = AnyStr
184149

185150

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)
151+
def _replace(src: PathType, dest: PathType, timeout: float = 10) -> None:
152+
'''
153+
Replace src with dest using os.replace(src, dest)
154+
Wait and retry if OSError exception is raised
155+
Increase wait time exponentially until total wait of timeout sec
156+
On timeout, give up and reraise the last exception seen
157+
'''
158+
n_iter = max(1, math.ceil(math.log2(timeout / 0.001)))
159+
for i in range(n_iter):
160+
# last wait is ~ timeout/2, so that total wait ~ timeout
161+
wait = timeout / 2 ** (n_iter - i - 1)
162+
try:
163+
os.replace(src, dest)
164+
except PermissionError:
165+
if i == n_iter - 1:
166+
raise
167+
else:
168+
return
169+
time.sleep(wait)
189170

190171

191172
if sys.platform.startswith("win"):

0 commit comments

Comments
 (0)