forked from BetaRavener/uPyLoader
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathhotswap.py
More file actions
374 lines (323 loc) · 12.5 KB
/
hotswap.py
File metadata and controls
374 lines (323 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
#!/usr/bin/env python
"""Automatic replacement of imported Python modules.
The hotswap module watches the source files of imported modules which are
replaced by its new version when the respective source files change.
The need for a program restart during development of long-running programs
like GUI applications for example is reduced.
Additionally this module can be called as a wrapper script:
hotswap.py [OPTIONS] <module.py> [args]
In this case module.py is imported as module and the function
module.main() is called. Hotswapping is enabled so that changes
in the source code take effect without restarting the program.
"""
version = "0.3.1"
__author__ = "Michael Krause"
__email__ = "michael@krause-software.com"
#
# CREDITS
# The idea and first implementation of the mechanism used by this module
# was first made public by Thomas Heller in a Usenet posting
# to comp.lang.python in 2001 (named autoreload.py).
# Updates for new-style classes were taken from a Usenet posting
# by Jeremy Fincher.
__all__ = ['run', 'stop', 'superreload']
import time
import os
import threading
import sys
import types
import imp
import getopt
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
try:
reload
except NameError:
from importlib import reload
if PY2:
TypeType = types.TypeType
ClassType = types.ClassType
else:
TypeType = type
ClassType = type
def _get_compiled_ext():
for ext, mode, typ in imp.get_suffixes():
if typ == imp.PY_COMPILED:
return ext
# the official way to get the extension of compiled files (.pyc or .pyo)
PY_COMPILED_EXT = _get_compiled_ext()
class ModuleWatcher:
SECONDS_BETWEEN_CHECKS = 0.1
SKIP_SYSTEM_MODULES = False
NOTIFYFUNC = None
VERBOSE = False
running = 0
def __init__(self):
# If we don't do this, there may be tracebacks
# when shutting down python.
import atexit
atexit.register(self.stop)
def run(self, skipsystem=SKIP_SYSTEM_MODULES,
seconds=SECONDS_BETWEEN_CHECKS,
notifyfunc=NOTIFYFUNC,
verbose=VERBOSE):
if self.running:
if verbose:
print("# hotswap already running")
return
self.SKIP_SYSTEM_MODULES = skipsystem
self.SECONDS_BETWEEN_CHECKS = seconds
self.NOTIFYFUNC = notifyfunc
self.VERBOSE = verbose
if self.VERBOSE:
print("# starting hotswap seconds=%s, skipsystem=%s" \
% (self.SECONDS_BETWEEN_CHECKS, self.SKIP_SYSTEM_MODULES))
self.running = 1
self.thread = threading.Thread(target=self._check_modules)
self.thread.setDaemon(1)
self.thread.start()
def stop(self):
if not self.running:
if self.VERBOSE:
print("# hotswap not running")
return
self.running = 0
self.thread.join()
if self.VERBOSE:
print("# hotswap stopped")
def _check_modules(self):
last_modified = {}
while self.running:
time.sleep(self.SECONDS_BETWEEN_CHECKS)
for m in list(sys.modules.values()):
if not hasattr(m, '__file__'):
# We only check modules that have a plain file
# as Python source.
continue
if m.__name__ == '__main__':
# __main__ cannot be reloaded without executing
# its code a second time, so we skip it.
continue
file = m.__file__
path, ext = os.path.splitext(file)
if self.SKIP_SYSTEM_MODULES:
# do not check system modules
sysprefix = sys.prefix + os.sep
if file.startswith(sysprefix):
continue
if ext.lower() == '.py':
ext = PY_COMPILED_EXT
if ext != PY_COMPILED_EXT:
continue
sourcefile = path + '.py'
try:
source_mtime = os.stat(sourcefile)[8]
if sourcefile not in last_modified:
last_modified[sourcefile] = source_mtime
continue
else:
if source_mtime <= last_modified[sourcefile]:
continue
last_modified[sourcefile] = source_mtime
except OSError:
continue
try:
superreload(m, verbose=self.VERBOSE)
except:
import traceback
traceback.print_exc(0)
try:
if hasattr(m, 'onHotswap') and callable(m.onHotswap):
# The module can invalidate cached results or post
# redisplay operations by defining function named
# onHotswap that is called after a reload.
m.onHotswap()
if callable(self.NOTIFYFUNC):
self.NOTIFYFUNC(module=m)
except:
import traceback
traceback.print_exc(0)
def update_function(old, new, attrnames):
for name in attrnames:
try:
setattr(old, name, getattr(new, name))
except AttributeError:
pass
def superreload(module,
reload=reload,
_old_objects = {},
verbose=True):
"""superreload (module) -> module
Enhanced version of the builtin reload function.
superreload replaces the class dictionary of every top-level
class in the module with the new one automatically,
as well as every function's code object.
"""
# retrieve the attributes from the module before the reload,
# and remember them in _old_objects.
for name, object in module.__dict__.items():
key = (module.__name__, name)
_old_objects.setdefault(key, []).append(object)
if verbose:
print("# reloading module %r" % module)
newmodule = reload(module)
if newmodule is None:
return module
# XXX We have a problem here if importing the module fails!
# iterate over all objects and update them
for name, new_obj in newmodule.__dict__.items():
# print "updating", `name`, type(new_obj), `new_obj`
key = (newmodule.__name__, name)
if key in _old_objects:
for old_obj in _old_objects[key]:
if type(new_obj) == ClassType:
if hasattr(old_obj.__dict__, 'update'):
old_obj.__dict__.update(new_obj.__dict__)
elif type(new_obj) == types.FunctionType:
update_function(old_obj,
new_obj,
"func_code func_defaults func_doc".split())
elif type(new_obj) == types.MethodType:
update_function(old_obj.im_func,
new_obj.im_func,
"func_code func_defaults func_doc".split())
return newmodule
_watcher = ModuleWatcher()
run = _watcher.run
stop = _watcher.stop
def modulename(path):
return os.path.splitext(path)[0].replace(os.sep, '.')
def importmodule(filename):
"""Returns the imported module of this source file.
This function tries to find this source file as module
on the Python path, so that its typical module name is used.
If this does not work, the directory of this file is inserted
at the beginning of sys.path and the import is attempted again.
"""
sourcefile = os.path.abspath(filename)
modfile = os.path.basename(sourcefile)
# Given an absolute filename of a python source file,
# we need to find it on the Python path to calculate its
# proper module name.
candidates = []
for p in sys.path:
pdir = p + os.sep
checkfile = os.path.join(p, modfile)
if os.path.normcase(sourcefile).startswith(os.path.normcase(pdir)):
relmodfile = sourcefile[len(pdir):]
candidates.append((len(relmodfile), relmodfile))
if candidates:
# Pick the most specific module path from all candidates
candidates.sort()
modname = modulename(candidates[0][1])
else:
modname = modulename(os.path.basename(sourcefile))
try:
# In case the source file was in the Python path
# it can be imported now.
module = __import__(modname, globals(), locals(), [])
except ImportError as e:
failed_modname = str(e).split()[-1]
failed_modname = failed_modname.replace("'", "")
if failed_modname == modname:
# The ImportError wasn't caused by some nested import
# but our module was not found, so we add the source files
# directory to the path and import it again.
modname = modulename(os.path.basename(sourcefile))
sys.path.insert(0, os.path.dirname(sourcefile))
module = __import__(modname, globals(), locals(), [])
else:
import traceback
tb = sys.exc_traceback
if tb:
tb = tb.tb_next
traceback.print_exception(sys.exc_type, sys.exc_value, tb)
# The module to be imported could be found but raised an
# ImportError itself.
raise e
# We have to deal module nesting like logging.handlers
# before calling the modules main function.
components = modname.split('.')
for comp in components[1:]:
module = getattr(module, comp)
return module
#----------------------------------------------------------------------------
class Usage(Exception):
def __init__(self, msg):
self.msg = msg
def usage(argv0):
print >>sys.stderr, """Usage: %s [OPTIONS] <module.py>
Import module and call module.main() with hotswap enabled.
Subsequent modifications in module.py and other source files of
modules being used are monitored periodically and put into effect
without restarting the program.
Options:
-h, --help Display this help then exit.
-w, --wait Wait number of seconds between checks. [0.1]
-s, --skipsystem Skip check of system modules beneath (%s). [False]
-v, --verbose Display diagnostic messages. [False]
""" % (argv0, sys.prefix)
#----------------------------------------------------------------------------
def main(argv=None):
if argv is None:
argv = sys.argv
wait = ModuleWatcher.SECONDS_BETWEEN_CHECKS
skipsystem = ModuleWatcher.SKIP_SYSTEM_MODULES
verbose = ModuleWatcher.VERBOSE
# parse command line arguments
try:
try:
opts, args = getopt.getopt(argv[1:], "hw:sv",
["help", "wait",
"skipsystem", "verbose"])
except getopt.error as msg:
raise Usage(msg)
for o, a in opts:
if o in ("-h", "--help"):
usage(argv[0])
return 0
if o in ("-w", "--wait"):
try:
wait = float(a)
except ValueError:
raise Usage("Parameter -w/--wait expects a float value")
if o in ("-s", "--skipsystem"):
skipsystem = True
if o in ("-v", "--verbose"):
verbose = True
except Usage as err:
print >>sys.stderr, "%s:" % argv[0],
print >>sys.stderr, err.msg
print >>sys.stderr, "for help use --help"
return 2
# Remove hotswap options from arguments
if args:
del argv[1:-len(args)]
else:
del argv[1:]
if len(argv) <= 1:
usage(argv[0])
sys.exit(1)
firstarg = argv[1]
sourcefile = os.path.abspath(firstarg)
if not os.path.isfile(sourcefile):
print("%s: File '%s' does not exist." % (os.path.basename(argv[0]),
sourcefile))
sys.exit(1)
try:
module = importmodule(sourcefile)
except ImportError as e:
print("%s: Unable to import '%s' as module: %s" % (os.path.basename(argv[0]),
sourcefile, e))
sys.exit(1)
# Remove hotswap.py from arguments that argv looks as
# if no additional wrapper was present.
del argv[0]
# Start hotswapping
run(skipsystem=skipsystem,
seconds=wait,
verbose=verbose)
# Run the Python source file with hotswapping enabled.
module.main()
if __name__ == '__main__':
main()