Skip to content

Commit fd5134d

Browse files
committed
Rework the reloader to keep the client code in the
main thread.
1 parent de6111c commit fd5134d

File tree

1 file changed

+92
-59
lines changed

1 file changed

+92
-59
lines changed

pyzen/core.py

Lines changed: 92 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,74 +3,109 @@
33
import time
44
import traceback
55
from multiprocessing import Process, Queue
6-
from threading import Thread
6+
from threading import Thread, Lock
77
from Queue import Empty
88

99
from pyzen.ui import load_ui
1010

1111
_SLEEP_TIME = 1
1212

13-
def _reloader_thread():
14-
"""When this function is run from the main thread, it will force other
15-
threads to exit when any modules currently loaded change.
16-
17-
@param modification_callback: a function taking a single argument, the
18-
modified file, which is called every time a modification is detected
13+
MAGIC_RETURN_CODE = 254
14+
15+
class ReloaderThread(Thread):
16+
def __init__(self):
17+
super(ReloaderThread, self).__init__()
18+
self.daemon = False
19+
self._quit = False
20+
self._quit_lock = Lock()
21+
self.do_reload = False
22+
23+
def run(self):
24+
"""When this is run, it will force other
25+
threads to exit when any modules currently loaded change.
26+
"""
27+
mtimes = {}
28+
while True:
29+
with self._quit_lock:
30+
if self._quit:
31+
return
32+
33+
for filename in filter(None, [getattr(module, '__file__', None)
34+
for module in sys.modules.values()]):
35+
while not os.path.isfile(filename): # Probably in an egg or zip file
36+
filename = os.path.dirname(filename)
37+
if not filename:
38+
break
39+
if not filename: # Couldn't map to physical file, so just ignore
40+
continue
41+
42+
if filename.endswith('.pyc') or filename.endswith('.pyo'):
43+
filename = filename[:-1]
44+
45+
if not os.path.isfile(filename):
46+
# Compiled file for non-existant source
47+
continue
48+
49+
mtime = os.stat(filename).st_mtime
50+
if filename not in mtimes:
51+
mtimes[filename] = mtime
52+
continue
53+
if mtime > mtimes[filename]:
54+
print >> sys.stderr, 'Detected modification of %s, restarting.' % filename
55+
self.do_reload = True
56+
sys.exit(MAGIC_RETURN_CODE)
57+
time.sleep(_SLEEP_TIME)
58+
59+
def quit(self):
60+
with self._quit_lock():
61+
self._quit = True
62+
63+
64+
class RunnerThread(Thread):
65+
"""A thread to run the provided function and push the test results
66+
back to a Queue.
1967
"""
20-
mtimes = {}
21-
while True:
22-
for filename in filter(None, [getattr(module, '__file__', None)
23-
for module in sys.modules.values()]):
24-
while not os.path.isfile(filename): # Probably in an egg or zip file
25-
filename = os.path.dirname(filename)
26-
if not filename:
27-
break
28-
if not filename: # Couldn't map to physical file, so just ignore
29-
continue
30-
31-
if filename.endswith('.pyc') or filename.endswith('.pyo'):
32-
filename = filename[:-1]
33-
34-
if not os.path.isfile(filename):
35-
# Compiled file for non-existant source
36-
continue
37-
38-
mtime = os.stat(filename).st_mtime
39-
if filename not in mtimes:
40-
mtimes[filename] = mtime
41-
continue
42-
if mtime > mtimes[filename]:
43-
print >> sys.stderr, 'Detected modification of %s, restarting.' % filename
44-
sys.exit(3)
45-
time.sleep(_SLEEP_TIME)
46-
47-
def _runner_thread(q, func, args, kwargs):
48-
try:
49-
start_time = time.clock()
50-
result = func(*args, **kwargs)
51-
end_time = time.clock()
52-
q.put({
53-
'failures': len(result.failures),
54-
'errors': len(result.errors),
55-
'total': result.testsRun,
56-
'time': end_time - start_time,
57-
})
58-
except Exception:
59-
traceback.print_exc()
60-
q.put({
61-
'failures': -1,
62-
'errors': -1,
63-
'total': -1,
64-
'time': 0,
65-
})
68+
69+
def __init__(self, q, func, args, kwargs):
70+
super(RunnerThread, self).__init__()
71+
self.q = q
72+
self.func = func
73+
self.args = args
74+
self.kwargs = kwargs
75+
76+
def run(self):
77+
try:
78+
start_time = time.clock()
79+
result = self.func(*self.args, **self.kwargs)
80+
end_time = time.clock()
81+
self.q.put({
82+
'failures': len(result.failures),
83+
'errors': len(result.errors),
84+
'total': result.testsRun,
85+
'time': end_time - start_time,
86+
})
87+
except Exception:
88+
traceback.print_exc()
89+
self.q.put({
90+
'failures': -1,
91+
'errors': -1,
92+
'total': -1,
93+
'time': 0,
94+
})
95+
6696

6797
def reloader(q, func, args, kwargs):
68-
t = Thread(target=_runner_thread, args=(q, func, args, kwargs))
98+
t = ReloaderThread()
6999
t.start()
70100
try:
71-
_reloader_thread()
101+
RunnerThread(q, func, args, kwargs).run()
72102
except KeyboardInterrupt:
73-
pass
103+
t.quit()
104+
finally:
105+
t.join()
106+
if t.do_reload:
107+
sys.exit(MAGIC_RETURN_CODE)
108+
74109

75110
def main(ui_override, func, *args, **kwargs):
76111
p = None
@@ -89,7 +124,7 @@ def main(ui_override, func, *args, **kwargs):
89124
except Empty:
90125
# Timed out, check if we need to restart
91126
if not p.is_alive():
92-
if p.exitcode == 3:
127+
if p.exitcode == MAGIC_RETURN_CODE:
93128
break # This means we need to restart it
94129
else:
95130
return p.exitcode # Any other return code should be considered real
@@ -98,5 +133,3 @@ def main(ui_override, func, *args, **kwargs):
98133
ui.shutdown()
99134
if p is not None:
100135
p.terminate()
101-
102-

0 commit comments

Comments
 (0)