##// END OF EJS Templates
Merge branch 'autoreload': port autoreload extension to current API...
Fernando Perez -
r4867:088eb7d0 merge
parent child Browse files
Show More
@@ -0,0 +1,483 b''
1 """
2 ``autoreload`` is an IPython extension that reloads modules
3 automatically before executing the line of code typed.
4
5 This makes for example the following workflow possible:
6
7 .. sourcecode:: ipython
8
9 In [1]: %load_ext autoreload
10
11 In [2]: %autoreload 2
12
13 In [3]: from foo import some_function
14
15 In [4]: some_function()
16 Out[4]: 42
17
18 In [5]: # open foo.py in an editor and change some_function to return 43
19
20 In [6]: some_function()
21 Out[6]: 43
22
23 The module was reloaded without reloading it explicitly, and the
24 object imported with ``from foo import ...`` was also updated.
25
26 Usage
27 =====
28
29 The following magic commands are provided:
30
31 ``%autoreload``
32
33 Reload all modules (except those excluded by ``%aimport``)
34 automatically now.
35
36 ``%autoreload 0``
37
38 Disable automatic reloading.
39
40 ``%autoreload 1``
41
42 Reload all modules imported with ``%aimport`` every time before
43 executing the Python code typed.
44
45 ``%autoreload 2``
46
47 Reload all modules (except those excluded by ``%aimport``) every
48 time before executing the Python code typed.
49
50 ``%aimport``
51
52 List modules which are to be automatically imported or not to be imported.
53
54 ``%aimport foo``
55
56 Import module 'foo' and mark it to be autoreloaded for ``%autoreload 1``
57
58 ``%aimport -foo``
59
60 Mark module 'foo' to not be autoreloaded.
61
62 Caveats
63 =======
64
65 Reloading Python modules in a reliable way is in general difficult,
66 and unexpected things may occur. ``%autoreload`` tries to work around
67 common pitfalls by replacing function code objects and parts of
68 classes previously in the module with new versions. This makes the
69 following things to work:
70
71 - Functions and classes imported via 'from xxx import foo' are upgraded
72 to new versions when 'xxx' is reloaded.
73
74 - Methods and properties of classes are upgraded on reload, so that
75 calling 'c.foo()' on an object 'c' created before the reload causes
76 the new code for 'foo' to be executed.
77
78 Some of the known remaining caveats are:
79
80 - Replacing code objects does not always succeed: changing a @property
81 in a class to an ordinary method or a method to a member variable
82 can cause problems (but in old objects only).
83
84 - Functions that are removed (eg. via monkey-patching) from a module
85 before it is reloaded are not upgraded.
86
87 - C extension modules cannot be reloaded, and so cannot be
88 autoreloaded.
89
90 """
91
92 skip_doctest = True
93
94 # Pauli Virtanen <pav@iki.fi>, 2008.
95 # Thomas Heller, 2000.
96 #
97 # This IPython module is written by Pauli Virtanen, based on the autoreload
98 # code by Thomas Heller.
99
100 #------------------------------------------------------------------------------
101 # Autoreload functionality
102 #------------------------------------------------------------------------------
103
104 import time, os, threading, sys, types, imp, inspect, traceback, atexit
105 import weakref
106
107 def _get_compiled_ext():
108 """Official way to get the extension of compiled files (.pyc or .pyo)"""
109 for ext, mode, typ in imp.get_suffixes():
110 if typ == imp.PY_COMPILED:
111 return ext
112
113 PY_COMPILED_EXT = _get_compiled_ext()
114
115 class ModuleReloader(object):
116 enabled = False
117 """Whether this reloader is enabled"""
118
119 failed = {}
120 """Modules that failed to reload: {module: mtime-on-failed-reload, ...}"""
121
122 modules = {}
123 """Modules specially marked as autoreloadable."""
124
125 skip_modules = {}
126 """Modules specially marked as not autoreloadable."""
127
128 check_all = True
129 """Autoreload all modules, not just those listed in 'modules'"""
130
131 old_objects = {}
132 """(module-name, name) -> weakref, for replacing old code objects"""
133
134 def mark_module_skipped(self, module_name):
135 """Skip reloading the named module in the future"""
136 try:
137 del self.modules[module_name]
138 except KeyError:
139 pass
140 self.skip_modules[module_name] = True
141
142 def mark_module_reloadable(self, module_name):
143 """Reload the named module in the future (if it is imported)"""
144 try:
145 del self.skip_modules[module_name]
146 except KeyError:
147 pass
148 self.modules[module_name] = True
149
150 def aimport_module(self, module_name):
151 """Import a module, and mark it reloadable
152
153 Returns
154 -------
155 top_module : module
156 The imported module if it is top-level, or the top-level
157 top_name : module
158 Name of top_module
159
160 """
161 self.mark_module_reloadable(module_name)
162
163 __import__(module_name)
164 top_name = module_name.split('.')[0]
165 top_module = sys.modules[top_name]
166 return top_module, top_name
167
168 def check(self, check_all=False):
169 """Check whether some modules need to be reloaded."""
170
171 if not self.enabled and not check_all:
172 return
173
174 if check_all or self.check_all:
175 modules = sys.modules.keys()
176 else:
177 modules = self.modules.keys()
178
179 for modname in modules:
180 m = sys.modules.get(modname, None)
181
182 if modname in self.skip_modules:
183 continue
184
185 if not hasattr(m, '__file__'):
186 continue
187
188 if m.__name__ == '__main__':
189 # we cannot reload(__main__)
190 continue
191
192 filename = m.__file__
193 path, ext = os.path.splitext(filename)
194
195 if ext.lower() == '.py':
196 ext = PY_COMPILED_EXT
197 pyc_filename = path + PY_COMPILED_EXT
198 py_filename = filename
199 else:
200 pyc_filename = filename
201 py_filename = filename[:-1]
202
203 if ext != PY_COMPILED_EXT:
204 continue
205
206 try:
207 pymtime = os.stat(py_filename).st_mtime
208 if pymtime <= os.stat(pyc_filename).st_mtime:
209 continue
210 if self.failed.get(py_filename, None) == pymtime:
211 continue
212 except OSError:
213 continue
214
215 try:
216 superreload(m, reload, self.old_objects)
217 if py_filename in self.failed:
218 del self.failed[py_filename]
219 except:
220 print >> sys.stderr, "[autoreload of %s failed: %s]" % (
221 modname, traceback.format_exc(1))
222 self.failed[py_filename] = pymtime
223
224 #------------------------------------------------------------------------------
225 # superreload
226 #------------------------------------------------------------------------------
227
228 def update_function(old, new):
229 """Upgrade the code object of a function"""
230 for name in ['func_code', 'func_defaults', 'func_doc',
231 'func_closure', 'func_globals', 'func_dict']:
232 try:
233 setattr(old, name, getattr(new, name))
234 except (AttributeError, TypeError):
235 pass
236
237 def update_class(old, new):
238 """Replace stuff in the __dict__ of a class, and upgrade
239 method code objects"""
240 for key in old.__dict__.keys():
241 old_obj = getattr(old, key)
242
243 try:
244 new_obj = getattr(new, key)
245 except AttributeError:
246 # obsolete attribute: remove it
247 try:
248 delattr(old, key)
249 except (AttributeError, TypeError):
250 pass
251 continue
252
253 if update_generic(old_obj, new_obj): continue
254
255 try:
256 setattr(old, key, getattr(new, key))
257 except (AttributeError, TypeError):
258 pass # skip non-writable attributes
259
260 def update_property(old, new):
261 """Replace get/set/del functions of a property"""
262 update_generic(old.fdel, new.fdel)
263 update_generic(old.fget, new.fget)
264 update_generic(old.fset, new.fset)
265
266 def isinstance2(a, b, typ):
267 return isinstance(a, typ) and isinstance(b, typ)
268
269 UPDATE_RULES = [
270 (lambda a, b: isinstance2(a, b, types.ClassType),
271 update_class),
272 (lambda a, b: isinstance2(a, b, types.TypeType),
273 update_class),
274 (lambda a, b: isinstance2(a, b, types.FunctionType),
275 update_function),
276 (lambda a, b: isinstance2(a, b, property),
277 update_property),
278 (lambda a, b: isinstance2(a, b, types.MethodType),
279 lambda a, b: update_function(a.im_func, b.im_func)),
280 ]
281
282 def update_generic(a, b):
283 for type_check, update in UPDATE_RULES:
284 if type_check(a, b):
285 update(a, b)
286 return True
287 return False
288
289 class StrongRef(object):
290 def __init__(self, obj):
291 self.obj = obj
292 def __call__(self):
293 return self.obj
294
295 def superreload(module, reload=reload, old_objects={}):
296 """Enhanced version of the builtin reload function.
297
298 superreload remembers objects previously in the module, and
299
300 - upgrades the class dictionary of every old class in the module
301 - upgrades the code object of every old function and method
302 - clears the module's namespace before reloading
303
304 """
305
306 # collect old objects in the module
307 for name, obj in module.__dict__.items():
308 if not hasattr(obj, '__module__') or obj.__module__ != module.__name__:
309 continue
310 key = (module.__name__, name)
311 try:
312 old_objects.setdefault(key, []).append(weakref.ref(obj))
313 except TypeError:
314 # weakref doesn't work for all types;
315 # create strong references for 'important' cases
316 if isinstance(obj, types.ClassType):
317 old_objects.setdefault(key, []).append(StrongRef(obj))
318
319 # reload module
320 try:
321 # clear namespace first from old cruft
322 old_dict = module.__dict__.copy()
323 old_name = module.__name__
324 module.__dict__.clear()
325 module.__dict__['__name__'] = old_name
326 except (TypeError, AttributeError, KeyError):
327 pass
328
329 try:
330 module = reload(module)
331 except:
332 # restore module dictionary on failed reload
333 module.__dict__.update(old_dict)
334 raise
335
336 # iterate over all objects and update functions & classes
337 for name, new_obj in module.__dict__.items():
338 key = (module.__name__, name)
339 if key not in old_objects: continue
340
341 new_refs = []
342 for old_ref in old_objects[key]:
343 old_obj = old_ref()
344 if old_obj is None: continue
345 new_refs.append(old_ref)
346 update_generic(old_obj, new_obj)
347
348 if new_refs:
349 old_objects[key] = new_refs
350 else:
351 del old_objects[key]
352
353 return module
354
355 #------------------------------------------------------------------------------
356 # IPython connectivity
357 #------------------------------------------------------------------------------
358
359 from IPython.core.plugin import Plugin
360 from IPython.core.hooks import TryNext
361
362 class AutoreloadInterface(object):
363 def __init__(self, *a, **kw):
364 super(AutoreloadInterface, self).__init__(*a, **kw)
365 self._reloader = ModuleReloader()
366 self._reloader.check_all = False
367
368 def magic_autoreload(self, ipself, parameter_s=''):
369 r"""%autoreload => Reload modules automatically
370
371 %autoreload
372 Reload all modules (except those excluded by %aimport) automatically
373 now.
374
375 %autoreload 0
376 Disable automatic reloading.
377
378 %autoreload 1
379 Reload all modules imported with %aimport every time before executing
380 the Python code typed.
381
382 %autoreload 2
383 Reload all modules (except those excluded by %aimport) every time
384 before executing the Python code typed.
385
386 Reloading Python modules in a reliable way is in general
387 difficult, and unexpected things may occur. %autoreload tries to
388 work around common pitfalls by replacing function code objects and
389 parts of classes previously in the module with new versions. This
390 makes the following things to work:
391
392 - Functions and classes imported via 'from xxx import foo' are upgraded
393 to new versions when 'xxx' is reloaded.
394
395 - Methods and properties of classes are upgraded on reload, so that
396 calling 'c.foo()' on an object 'c' created before the reload causes
397 the new code for 'foo' to be executed.
398
399 Some of the known remaining caveats are:
400
401 - Replacing code objects does not always succeed: changing a @property
402 in a class to an ordinary method or a method to a member variable
403 can cause problems (but in old objects only).
404
405 - Functions that are removed (eg. via monkey-patching) from a module
406 before it is reloaded are not upgraded.
407
408 - C extension modules cannot be reloaded, and so cannot be
409 autoreloaded.
410
411 """
412 if parameter_s == '':
413 self._reloader.check(True)
414 elif parameter_s == '0':
415 self._reloader.enabled = False
416 elif parameter_s == '1':
417 self._reloader.check_all = False
418 self._reloader.enabled = True
419 elif parameter_s == '2':
420 self._reloader.check_all = True
421 self._reloader.enabled = True
422
423 def magic_aimport(self, ipself, parameter_s='', stream=None):
424 """%aimport => Import modules for automatic reloading.
425
426 %aimport
427 List modules to automatically import and not to import.
428
429 %aimport foo
430 Import module 'foo' and mark it to be autoreloaded for %autoreload 1
431
432 %aimport -foo
433 Mark module 'foo' to not be autoreloaded for %autoreload 1
434
435 """
436
437 modname = parameter_s
438 if not modname:
439 to_reload = self._reloader.modules.keys()
440 to_reload.sort()
441 to_skip = self._reloader.skip_modules.keys()
442 to_skip.sort()
443 if stream is None:
444 stream = sys.stdout
445 if self._reloader.check_all:
446 stream.write("Modules to reload:\nall-except-skipped\n")
447 else:
448 stream.write("Modules to reload:\n%s\n" % ' '.join(to_reload))
449 stream.write("\nModules to skip:\n%s\n" % ' '.join(to_skip))
450 elif modname.startswith('-'):
451 modname = modname[1:]
452 self._reloader.mark_module_skipped(modname)
453 else:
454 top_module, top_name = self._reloader.aimport_module(modname)
455
456 # Inject module to user namespace
457 ipself.push({top_name: top_module})
458
459 def pre_run_code_hook(self, ipself):
460 if not self._reloader.enabled:
461 raise TryNext
462 try:
463 self._reloader.check()
464 except:
465 pass
466
467 class AutoreloadPlugin(AutoreloadInterface, Plugin):
468 def __init__(self, shell=None, config=None):
469 super(AutoreloadPlugin, self).__init__(shell=shell, config=config)
470
471 self.shell.define_magic('autoreload', self.magic_autoreload)
472 self.shell.define_magic('aimport', self.magic_aimport)
473 self.shell.set_hook('pre_run_code_hook', self.pre_run_code_hook)
474
475 _loaded = False
476
477 def load_ipython_extension(ip):
478 """Load the extension in IPython."""
479 global _loaded
480 if not _loaded:
481 plugin = AutoreloadPlugin(shell=ip, config=ip.config)
482 ip.plugin_manager.register_plugin('autoreload', plugin)
483 _loaded = True
@@ -0,0 +1,307 b''
1 import os
2 import sys
3 import tempfile
4 import shutil
5 import random
6 import time
7 from StringIO import StringIO
8
9 import nose.tools as nt
10
11 from IPython.extensions.autoreload import AutoreloadInterface
12 from IPython.core.hooks import TryNext
13
14 #-----------------------------------------------------------------------------
15 # Test fixture
16 #-----------------------------------------------------------------------------
17
18 class FakeShell(object):
19 def __init__(self):
20 self.ns = {}
21 self.reloader = AutoreloadInterface()
22
23 def run_code(self, code):
24 try:
25 self.reloader.pre_run_code_hook(self)
26 except TryNext:
27 pass
28 exec code in self.ns
29
30 def push(self, items):
31 self.ns.update(items)
32
33 def magic_autoreload(self, parameter):
34 self.reloader.magic_autoreload(self, parameter)
35
36 def magic_aimport(self, parameter, stream=None):
37 self.reloader.magic_aimport(self, parameter, stream=stream)
38
39
40 class Fixture(object):
41 """Fixture for creating test module files"""
42
43 test_dir = None
44 old_sys_path = None
45 filename_chars = "abcdefghijklmopqrstuvwxyz0123456789"
46
47 def setUp(self):
48 self.test_dir = tempfile.mkdtemp()
49 self.old_sys_path = list(sys.path)
50 sys.path.insert(0, self.test_dir)
51 self.shell = FakeShell()
52
53 def tearDown(self):
54 shutil.rmtree(self.test_dir)
55 sys.path = self.old_sys_path
56 self.shell.reloader.enabled = False
57
58 self.test_dir = None
59 self.old_sys_path = None
60 self.shell = None
61
62 def get_module(self):
63 module_name = "tmpmod_" + "".join(random.sample(self.filename_chars,20))
64 if module_name in sys.modules:
65 del sys.modules[module_name]
66 file_name = os.path.join(self.test_dir, module_name + ".py")
67 return module_name, file_name
68
69 def write_file(self, filename, content):
70 """
71 Write a file, and force a timestamp difference of at least one second
72
73 Notes
74 -----
75 Python's .pyc files record the timestamp of their compilation
76 with a time resolution of one second.
77
78 Therefore, we need to force a timestamp difference between .py
79 and .pyc, without having the .py file be timestamped in the
80 future, and without changing the timestamp of the .pyc file
81 (because that is stored in the file). The only reliable way
82 to achieve this seems to be to sleep.
83
84 """
85
86 # Sleep one second + eps
87 time.sleep(1.05)
88
89 # Write
90 f = open(filename, 'w')
91 try:
92 f.write(content)
93 finally:
94 f.close()
95
96 def new_module(self, code):
97 mod_name, mod_fn = self.get_module()
98 f = open(mod_fn, 'w')
99 try:
100 f.write(code)
101 finally:
102 f.close()
103 return mod_name, mod_fn
104
105 #-----------------------------------------------------------------------------
106 # Test automatic reloading
107 #-----------------------------------------------------------------------------
108
109 class TestAutoreload(Fixture):
110 def _check_smoketest(self, use_aimport=True):
111 """
112 Functional test for the automatic reloader using either
113 '%autoreload 1' or '%autoreload 2'
114 """
115
116 mod_name, mod_fn = self.new_module("""
117 x = 9
118
119 z = 123 # this item will be deleted
120
121 def foo(y):
122 return y + 3
123
124 class Baz(object):
125 def __init__(self, x):
126 self.x = x
127 def bar(self, y):
128 return self.x + y
129 @property
130 def quux(self):
131 return 42
132 def zzz(self):
133 '''This method will be deleted below'''
134 return 99
135
136 class Bar: # old-style class: weakref doesn't work for it on Python < 2.7
137 def foo(self):
138 return 1
139 """)
140
141 #
142 # Import module, and mark for reloading
143 #
144 if use_aimport:
145 self.shell.magic_autoreload("1")
146 self.shell.magic_aimport(mod_name)
147 stream = StringIO()
148 self.shell.magic_aimport("", stream=stream)
149 nt.assert_true(("Modules to reload:\n%s" % mod_name) in
150 stream.getvalue())
151
152 nt.assert_raises(
153 ImportError,
154 self.shell.magic_aimport, "tmpmod_as318989e89ds")
155 else:
156 self.shell.magic_autoreload("2")
157 self.shell.run_code("import %s" % mod_name)
158 stream = StringIO()
159 self.shell.magic_aimport("", stream=stream)
160 nt.assert_true("Modules to reload:\nall-except-skipped" in
161 stream.getvalue())
162 nt.assert_true(mod_name in self.shell.ns)
163
164 mod = sys.modules[mod_name]
165
166 #
167 # Test module contents
168 #
169 old_foo = mod.foo
170 old_obj = mod.Baz(9)
171 old_obj2 = mod.Bar()
172
173 def check_module_contents():
174 nt.assert_equal(mod.x, 9)
175 nt.assert_equal(mod.z, 123)
176
177 nt.assert_equal(old_foo(0), 3)
178 nt.assert_equal(mod.foo(0), 3)
179
180 obj = mod.Baz(9)
181 nt.assert_equal(old_obj.bar(1), 10)
182 nt.assert_equal(obj.bar(1), 10)
183 nt.assert_equal(obj.quux, 42)
184 nt.assert_equal(obj.zzz(), 99)
185
186 obj2 = mod.Bar()
187 nt.assert_equal(old_obj2.foo(), 1)
188 nt.assert_equal(obj2.foo(), 1)
189
190 check_module_contents()
191
192 #
193 # Simulate a failed reload: no reload should occur and exactly
194 # one error message should be printed
195 #
196 self.write_file(mod_fn, """
197 a syntax error
198 """)
199
200 old_stderr = sys.stderr
201 new_stderr = StringIO()
202 sys.stderr = new_stderr
203 try:
204 self.shell.run_code("pass") # trigger reload
205 self.shell.run_code("pass") # trigger another reload
206 check_module_contents()
207 finally:
208 sys.stderr = old_stderr
209
210 nt.assert_true(('[autoreload of %s failed:' % mod_name) in
211 new_stderr.getvalue())
212 nt.assert_equal(new_stderr.getvalue().count('[autoreload of'), 1)
213
214 #
215 # Rewrite module (this time reload should succeed)
216 #
217 self.write_file(mod_fn, """
218 x = 10
219
220 def foo(y):
221 return y + 4
222
223 class Baz(object):
224 def __init__(self, x):
225 self.x = x
226 def bar(self, y):
227 return self.x + y + 1
228 @property
229 def quux(self):
230 return 43
231
232 class Bar: # old-style class
233 def foo(self):
234 return 2
235 """)
236
237 def check_module_contents():
238 nt.assert_equal(mod.x, 10)
239 nt.assert_false(hasattr(mod, 'z'))
240
241 nt.assert_equal(old_foo(0), 4) # superreload magic!
242 nt.assert_equal(mod.foo(0), 4)
243
244 obj = mod.Baz(9)
245 nt.assert_equal(old_obj.bar(1), 11) # superreload magic!
246 nt.assert_equal(obj.bar(1), 11)
247
248 nt.assert_equal(old_obj.quux, 43)
249 nt.assert_equal(obj.quux, 43)
250
251 nt.assert_false(hasattr(old_obj, 'zzz'))
252 nt.assert_false(hasattr(obj, 'zzz'))
253
254 obj2 = mod.Bar()
255 nt.assert_equal(old_obj2.foo(), 2)
256 nt.assert_equal(obj2.foo(), 2)
257
258 self.shell.run_code("pass") # trigger reload
259 check_module_contents()
260
261 #
262 # Another failure case: deleted file (shouldn't reload)
263 #
264 os.unlink(mod_fn)
265
266 self.shell.run_code("pass") # trigger reload
267 check_module_contents()
268
269 #
270 # Disable autoreload and rewrite module: no reload should occur
271 #
272 if use_aimport:
273 self.shell.magic_aimport("-" + mod_name)
274 stream = StringIO()
275 self.shell.magic_aimport("", stream=stream)
276 nt.assert_true(("Modules to skip:\n%s" % mod_name) in
277 stream.getvalue())
278
279 # This should succeed, although no such module exists
280 self.shell.magic_aimport("-tmpmod_as318989e89ds")
281 else:
282 self.shell.magic_autoreload("0")
283
284 self.write_file(mod_fn, """
285 x = -99
286 """)
287
288 self.shell.run_code("pass") # trigger reload
289 self.shell.run_code("pass")
290 check_module_contents()
291
292 #
293 # Re-enable autoreload: reload should now occur
294 #
295 if use_aimport:
296 self.shell.magic_aimport(mod_name)
297 else:
298 self.shell.magic_autoreload("")
299
300 self.shell.run_code("pass") # trigger reload
301 nt.assert_equal(mod.x, -99)
302
303 def test_smoketest_aimport(self):
304 self._check_smoketest(use_aimport=True)
305
306 def test_smoketest_autoreload(self):
307 self._check_smoketest(use_aimport=False)
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now