##// END OF EJS Templates
Add new '%autoreload 3' option...
Spas Kalaydzhisyki -
Show More
@@ -0,0 +1,14 b''
1 Autoreload 3 feature
2 ====================
3
4 Example: When an IPython session is ran with the 'autoreload' extension loaded,
5 you will now have the option '3' to select which means the following:
6
7 1. replicate all functionality from option 2
8 2. autoload all new funcs/classes/enums/globals from the module when they're added
9 3. autoload all newly imported funcs/classes/enums/globals from external modules
10
11 Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload``
12
13 For more information please see unit test -
14 extensions/tests/test_autoreload.py : 'test_autoload_newly_added_objects'
@@ -1,550 +1,595 b''
1 1 """IPython extension to reload modules before executing user code.
2 2
3 3 ``autoreload`` reloads modules automatically before entering the execution of
4 4 code typed at the IPython prompt.
5 5
6 6 This makes for example the following workflow possible:
7 7
8 8 .. sourcecode:: ipython
9 9
10 10 In [1]: %load_ext autoreload
11 11
12 12 In [2]: %autoreload 2
13 13
14 14 In [3]: from foo import some_function
15 15
16 16 In [4]: some_function()
17 17 Out[4]: 42
18 18
19 19 In [5]: # open foo.py in an editor and change some_function to return 43
20 20
21 21 In [6]: some_function()
22 22 Out[6]: 43
23 23
24 24 The module was reloaded without reloading it explicitly, and the object
25 25 imported with ``from foo import ...`` was also updated.
26 26
27 27 Usage
28 28 =====
29 29
30 30 The following magic commands are provided:
31 31
32 32 ``%autoreload``
33 33
34 34 Reload all modules (except those excluded by ``%aimport``)
35 35 automatically now.
36 36
37 37 ``%autoreload 0``
38 38
39 39 Disable automatic reloading.
40 40
41 41 ``%autoreload 1``
42 42
43 43 Reload all modules imported with ``%aimport`` every time before
44 44 executing the Python code typed.
45 45
46 46 ``%autoreload 2``
47 47
48 48 Reload all modules (except those excluded by ``%aimport``) every
49 49 time before executing the Python code typed.
50 50
51 ``%autoreload 3``
52
53 Reload all modules AND autoload newly added objects
54 (except those excluded by ``%aimport``)
55 every time before executing the Python code typed.
56
51 57 ``%aimport``
52 58
53 59 List modules which are to be automatically imported or not to be imported.
54 60
55 61 ``%aimport foo``
56 62
57 63 Import module 'foo' and mark it to be autoreloaded for ``%autoreload 1``
58 64
59 65 ``%aimport foo, bar``
60 66
61 67 Import modules 'foo', 'bar' and mark them to be autoreloaded for ``%autoreload 1``
62 68
63 69 ``%aimport -foo``
64 70
65 71 Mark module 'foo' to not be autoreloaded.
66 72
67 73 Caveats
68 74 =======
69 75
70 76 Reloading Python modules in a reliable way is in general difficult,
71 77 and unexpected things may occur. ``%autoreload`` tries to work around
72 78 common pitfalls by replacing function code objects and parts of
73 79 classes previously in the module with new versions. This makes the
74 80 following things to work:
75 81
76 82 - Functions and classes imported via 'from xxx import foo' are upgraded
77 83 to new versions when 'xxx' is reloaded.
78 84
79 85 - Methods and properties of classes are upgraded on reload, so that
80 86 calling 'c.foo()' on an object 'c' created before the reload causes
81 87 the new code for 'foo' to be executed.
82 88
83 89 Some of the known remaining caveats are:
84 90
85 91 - Replacing code objects does not always succeed: changing a @property
86 92 in a class to an ordinary method or a method to a member variable
87 93 can cause problems (but in old objects only).
88 94
89 95 - Functions that are removed (eg. via monkey-patching) from a module
90 96 before it is reloaded are not upgraded.
91 97
92 98 - C extension modules cannot be reloaded, and so cannot be autoreloaded.
93 99 """
94 100
95 101 skip_doctest = True
96 102
97 103 #-----------------------------------------------------------------------------
98 104 # Copyright (C) 2000 Thomas Heller
99 105 # Copyright (C) 2008 Pauli Virtanen <pav@iki.fi>
100 106 # Copyright (C) 2012 The IPython Development Team
101 107 #
102 108 # Distributed under the terms of the BSD License. The full license is in
103 109 # the file COPYING, distributed as part of this software.
104 110 #-----------------------------------------------------------------------------
105 111 #
106 112 # This IPython module is written by Pauli Virtanen, based on the autoreload
107 113 # code by Thomas Heller.
108 114
109 115 #-----------------------------------------------------------------------------
110 116 # Imports
111 117 #-----------------------------------------------------------------------------
112 118
113 119 import os
114 120 import sys
115 121 import traceback
116 122 import types
117 123 import weakref
118 124 import gc
119 125 from importlib import import_module
120 126 from importlib.util import source_from_cache
121 127 from imp import reload
122 128
123 129 #------------------------------------------------------------------------------
124 130 # Autoreload functionality
125 131 #------------------------------------------------------------------------------
126 132
127 133 class ModuleReloader(object):
128 134 enabled = False
129 135 """Whether this reloader is enabled"""
130 136
131 137 check_all = True
132 138 """Autoreload all modules, not just those listed in 'modules'"""
133 139
134 def __init__(self):
140 autoload_obj = False
141 """Autoreload all modules AND autoload all new objects"""
142
143 def __init__(self, shell=None):
135 144 # Modules that failed to reload: {module: mtime-on-failed-reload, ...}
136 145 self.failed = {}
137 146 # Modules specially marked as autoreloadable.
138 147 self.modules = {}
139 148 # Modules specially marked as not autoreloadable.
140 149 self.skip_modules = {}
141 150 # (module-name, name) -> weakref, for replacing old code objects
142 151 self.old_objects = {}
143 152 # Module modification timestamps
144 153 self.modules_mtimes = {}
154 self.shell = shell
145 155
146 156 # Cache module modification times
147 157 self.check(check_all=True, do_reload=False)
148 158
149 159 def mark_module_skipped(self, module_name):
150 160 """Skip reloading the named module in the future"""
151 161 try:
152 162 del self.modules[module_name]
153 163 except KeyError:
154 164 pass
155 165 self.skip_modules[module_name] = True
156 166
157 167 def mark_module_reloadable(self, module_name):
158 168 """Reload the named module in the future (if it is imported)"""
159 169 try:
160 170 del self.skip_modules[module_name]
161 171 except KeyError:
162 172 pass
163 173 self.modules[module_name] = True
164 174
165 175 def aimport_module(self, module_name):
166 176 """Import a module, and mark it reloadable
167 177
168 178 Returns
169 179 -------
170 180 top_module : module
171 181 The imported module if it is top-level, or the top-level
172 182 top_name : module
173 183 Name of top_module
174 184
175 185 """
176 186 self.mark_module_reloadable(module_name)
177 187
178 188 import_module(module_name)
179 189 top_name = module_name.split('.')[0]
180 190 top_module = sys.modules[top_name]
181 191 return top_module, top_name
182 192
183 193 def filename_and_mtime(self, module):
184 194 if not hasattr(module, '__file__') or module.__file__ is None:
185 195 return None, None
186 196
187 197 if getattr(module, '__name__', None) in [None, '__mp_main__', '__main__']:
188 198 # we cannot reload(__main__) or reload(__mp_main__)
189 199 return None, None
190 200
191 201 filename = module.__file__
192 202 path, ext = os.path.splitext(filename)
193 203
194 204 if ext.lower() == '.py':
195 205 py_filename = filename
196 206 else:
197 207 try:
198 208 py_filename = source_from_cache(filename)
199 209 except ValueError:
200 210 return None, None
201 211
202 212 try:
203 213 pymtime = os.stat(py_filename).st_mtime
204 214 except OSError:
205 215 return None, None
206 216
207 217 return py_filename, pymtime
208 218
209 219 def check(self, check_all=False, do_reload=True):
210 220 """Check whether some modules need to be reloaded."""
211 221
212 222 if not self.enabled and not check_all:
213 223 return
214 224
215 225 if check_all or self.check_all:
216 226 modules = list(sys.modules.keys())
217 227 else:
218 228 modules = list(self.modules.keys())
219 229
220 230 for modname in modules:
221 231 m = sys.modules.get(modname, None)
222 232
223 233 if modname in self.skip_modules:
224 234 continue
225 235
226 236 py_filename, pymtime = self.filename_and_mtime(m)
227 237 if py_filename is None:
228 238 continue
229 239
230 240 try:
231 241 if pymtime <= self.modules_mtimes[modname]:
232 242 continue
233 243 except KeyError:
234 244 self.modules_mtimes[modname] = pymtime
235 245 continue
236 246 else:
237 247 if self.failed.get(py_filename, None) == pymtime:
238 248 continue
239 249
240 250 self.modules_mtimes[modname] = pymtime
241 251
242 252 # If we've reached this point, we should try to reload the module
243 253 if do_reload:
244 254 try:
245 superreload(m, reload, self.old_objects)
255 if self.autoload_obj:
256 superreload(m, reload, self.old_objects, self.shell)
257 else:
258 superreload(m, reload, self.old_objects)
246 259 if py_filename in self.failed:
247 260 del self.failed[py_filename]
248 261 except:
249 262 print("[autoreload of %s failed: %s]" % (
250 263 modname, traceback.format_exc(10)), file=sys.stderr)
251 264 self.failed[py_filename] = pymtime
252 265
253 266 #------------------------------------------------------------------------------
254 267 # superreload
255 268 #------------------------------------------------------------------------------
256 269
257 270
258 271 func_attrs = ['__code__', '__defaults__', '__doc__',
259 272 '__closure__', '__globals__', '__dict__']
260 273
261 274
262 275 def update_function(old, new):
263 276 """Upgrade the code object of a function"""
264 277 for name in func_attrs:
265 278 try:
266 279 setattr(old, name, getattr(new, name))
267 280 except (AttributeError, TypeError):
268 281 pass
269 282
270 283
271 284 def update_instances(old, new):
272 285 """Use garbage collector to find all instances that refer to the old
273 286 class definition and update their __class__ to point to the new class
274 287 definition"""
275 288
276 289 refs = gc.get_referrers(old)
277 290
278 291 for ref in refs:
279 292 if type(ref) is old:
280 293 ref.__class__ = new
281 294
282 295
283 296 def update_class(old, new):
284 297 """Replace stuff in the __dict__ of a class, and upgrade
285 298 method code objects, and add new methods, if any"""
286 299 for key in list(old.__dict__.keys()):
287 300 old_obj = getattr(old, key)
288 301 try:
289 302 new_obj = getattr(new, key)
290 303 # explicitly checking that comparison returns True to handle
291 304 # cases where `==` doesn't return a boolean.
292 305 if (old_obj == new_obj) is True:
293 306 continue
294 307 except AttributeError:
295 308 # obsolete attribute: remove it
296 309 try:
297 310 delattr(old, key)
298 311 except (AttributeError, TypeError):
299 312 pass
300 313 continue
301 314
302 315 if update_generic(old_obj, new_obj): continue
303 316
304 317 try:
305 318 setattr(old, key, getattr(new, key))
306 319 except (AttributeError, TypeError):
307 320 pass # skip non-writable attributes
308 321
309 322 for key in list(new.__dict__.keys()):
310 323 if key not in list(old.__dict__.keys()):
311 324 try:
312 325 setattr(old, key, getattr(new, key))
313 326 except (AttributeError, TypeError):
314 327 pass # skip non-writable attributes
315 328
316 329 # update all instances of class
317 330 update_instances(old, new)
318 331
319 332
320 333 def update_property(old, new):
321 334 """Replace get/set/del functions of a property"""
322 335 update_generic(old.fdel, new.fdel)
323 336 update_generic(old.fget, new.fget)
324 337 update_generic(old.fset, new.fset)
325 338
326 339
327 340 def isinstance2(a, b, typ):
328 341 return isinstance(a, typ) and isinstance(b, typ)
329 342
330 343
331 344 UPDATE_RULES = [
332 345 (lambda a, b: isinstance2(a, b, type),
333 346 update_class),
334 347 (lambda a, b: isinstance2(a, b, types.FunctionType),
335 348 update_function),
336 349 (lambda a, b: isinstance2(a, b, property),
337 350 update_property),
338 351 ]
339 352 UPDATE_RULES.extend([(lambda a, b: isinstance2(a, b, types.MethodType),
340 353 lambda a, b: update_function(a.__func__, b.__func__)),
341 354 ])
342 355
343 356
344 357 def update_generic(a, b):
345 358 for type_check, update in UPDATE_RULES:
346 359 if type_check(a, b):
347 360 update(a, b)
348 361 return True
349 362 return False
350 363
351 364
352 365 class StrongRef(object):
353 366 def __init__(self, obj):
354 367 self.obj = obj
355 368 def __call__(self):
356 369 return self.obj
357 370
358 371
359 def superreload(module, reload=reload, old_objects=None):
372 def append_obj(module, d, name, obj, autoload=False):
373 not_in_mod = not hasattr(obj, '__module__') or obj.__module__ != module.__name__
374 if autoload:
375 # check needed for module global built-ins (int, str, dict,..)
376 if name.startswith('__') and not_in_mod:
377 return False
378 else:
379 if not_in_mod:
380 return False
381
382 key = (module.__name__, name)
383 try:
384 d.setdefault(key, []).append(weakref.ref(obj))
385 except TypeError:
386 pass
387 return True
388
389
390 def superreload(module, reload=reload, old_objects=None, shell=None):
360 391 """Enhanced version of the builtin reload function.
361 392
362 393 superreload remembers objects previously in the module, and
363 394
364 395 - upgrades the class dictionary of every old class in the module
365 396 - upgrades the code object of every old function and method
366 397 - clears the module's namespace before reloading
367 398
368 399 """
369 400 if old_objects is None:
370 401 old_objects = {}
371 402
372 403 # collect old objects in the module
373 404 for name, obj in list(module.__dict__.items()):
374 if not hasattr(obj, '__module__') or obj.__module__ != module.__name__:
405 if not append_obj(module, old_objects, name, obj):
375 406 continue
376 407 key = (module.__name__, name)
377 408 try:
378 409 old_objects.setdefault(key, []).append(weakref.ref(obj))
379 410 except TypeError:
380 411 pass
381 412
382 413 # reload module
383 414 try:
384 415 # clear namespace first from old cruft
385 416 old_dict = module.__dict__.copy()
386 417 old_name = module.__name__
387 418 module.__dict__.clear()
388 419 module.__dict__['__name__'] = old_name
389 420 module.__dict__['__loader__'] = old_dict['__loader__']
390 421 except (TypeError, AttributeError, KeyError):
391 422 pass
392 423
393 424 try:
394 425 module = reload(module)
395 426 except:
396 427 # restore module dictionary on failed reload
397 428 module.__dict__.update(old_dict)
398 429 raise
399 430
400 431 # iterate over all objects and update functions & classes
401 432 for name, new_obj in list(module.__dict__.items()):
402 433 key = (module.__name__, name)
403 if key not in old_objects: continue
434 if key not in old_objects:
435 # here 'shell' acts both as a flag and as an output var
436 if (
437 shell is None or
438 name == 'Enum' or
439 not append_obj(module, old_objects, name, new_obj, True)
440 ):
441 continue
442 shell.user_ns[name] = new_obj
404 443
405 444 new_refs = []
406 445 for old_ref in old_objects[key]:
407 446 old_obj = old_ref()
408 447 if old_obj is None: continue
409 448 new_refs.append(old_ref)
410 449 update_generic(old_obj, new_obj)
411 450
412 451 if new_refs:
413 452 old_objects[key] = new_refs
414 453 else:
415 454 del old_objects[key]
416 455
417 456 return module
418 457
419 458 #------------------------------------------------------------------------------
420 459 # IPython connectivity
421 460 #------------------------------------------------------------------------------
422 461
423 462 from IPython.core.magic import Magics, magics_class, line_magic
424 463
425 464 @magics_class
426 465 class AutoreloadMagics(Magics):
427 466 def __init__(self, *a, **kw):
428 467 super(AutoreloadMagics, self).__init__(*a, **kw)
429 self._reloader = ModuleReloader()
468 self._reloader = ModuleReloader(self.shell)
430 469 self._reloader.check_all = False
470 self._reloader.autoload_obj = False
431 471 self.loaded_modules = set(sys.modules)
432 472
433 473 @line_magic
434 474 def autoreload(self, parameter_s=''):
435 475 r"""%autoreload => Reload modules automatically
436 476
437 477 %autoreload
438 478 Reload all modules (except those excluded by %aimport) automatically
439 479 now.
440 480
441 481 %autoreload 0
442 482 Disable automatic reloading.
443 483
444 484 %autoreload 1
445 485 Reload all modules imported with %aimport every time before executing
446 486 the Python code typed.
447 487
448 488 %autoreload 2
449 489 Reload all modules (except those excluded by %aimport) every time
450 490 before executing the Python code typed.
451 491
452 492 Reloading Python modules in a reliable way is in general
453 493 difficult, and unexpected things may occur. %autoreload tries to
454 494 work around common pitfalls by replacing function code objects and
455 495 parts of classes previously in the module with new versions. This
456 496 makes the following things to work:
457 497
458 498 - Functions and classes imported via 'from xxx import foo' are upgraded
459 499 to new versions when 'xxx' is reloaded.
460 500
461 501 - Methods and properties of classes are upgraded on reload, so that
462 502 calling 'c.foo()' on an object 'c' created before the reload causes
463 503 the new code for 'foo' to be executed.
464 504
465 505 Some of the known remaining caveats are:
466 506
467 507 - Replacing code objects does not always succeed: changing a @property
468 508 in a class to an ordinary method or a method to a member variable
469 509 can cause problems (but in old objects only).
470 510
471 511 - Functions that are removed (eg. via monkey-patching) from a module
472 512 before it is reloaded are not upgraded.
473 513
474 514 - C extension modules cannot be reloaded, and so cannot be
475 515 autoreloaded.
476 516
477 517 """
478 518 if parameter_s == '':
479 519 self._reloader.check(True)
480 520 elif parameter_s == '0':
481 521 self._reloader.enabled = False
482 522 elif parameter_s == '1':
483 523 self._reloader.check_all = False
484 524 self._reloader.enabled = True
485 525 elif parameter_s == '2':
486 526 self._reloader.check_all = True
487 527 self._reloader.enabled = True
528 self._reloader.enabled = True
529 elif parameter_s == '3':
530 self._reloader.check_all = True
531 self._reloader.enabled = True
532 self._reloader.autoload_obj = True
488 533
489 534 @line_magic
490 535 def aimport(self, parameter_s='', stream=None):
491 536 """%aimport => Import modules for automatic reloading.
492 537
493 538 %aimport
494 539 List modules to automatically import and not to import.
495 540
496 541 %aimport foo
497 542 Import module 'foo' and mark it to be autoreloaded for %autoreload 1
498 543
499 544 %aimport foo, bar
500 545 Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload 1
501 546
502 547 %aimport -foo
503 548 Mark module 'foo' to not be autoreloaded for %autoreload 1
504 549 """
505 550 modname = parameter_s
506 551 if not modname:
507 552 to_reload = sorted(self._reloader.modules.keys())
508 553 to_skip = sorted(self._reloader.skip_modules.keys())
509 554 if stream is None:
510 555 stream = sys.stdout
511 556 if self._reloader.check_all:
512 557 stream.write("Modules to reload:\nall-except-skipped\n")
513 558 else:
514 559 stream.write("Modules to reload:\n%s\n" % ' '.join(to_reload))
515 560 stream.write("\nModules to skip:\n%s\n" % ' '.join(to_skip))
516 561 elif modname.startswith('-'):
517 562 modname = modname[1:]
518 563 self._reloader.mark_module_skipped(modname)
519 564 else:
520 565 for _module in ([_.strip() for _ in modname.split(',')]):
521 566 top_module, top_name = self._reloader.aimport_module(_module)
522 567
523 568 # Inject module to user namespace
524 569 self.shell.push({top_name: top_module})
525 570
526 571 def pre_run_cell(self):
527 572 if self._reloader.enabled:
528 573 try:
529 574 self._reloader.check()
530 575 except:
531 576 pass
532 577
533 578 def post_execute_hook(self):
534 579 """Cache the modification times of any modules imported in this execution
535 580 """
536 581 newly_loaded_modules = set(sys.modules) - self.loaded_modules
537 582 for modname in newly_loaded_modules:
538 583 _, pymtime = self._reloader.filename_and_mtime(sys.modules[modname])
539 584 if pymtime is not None:
540 585 self._reloader.modules_mtimes[modname] = pymtime
541 586
542 587 self.loaded_modules.update(newly_loaded_modules)
543 588
544 589
545 590 def load_ipython_extension(ip):
546 591 """Load the extension in IPython."""
547 592 auto_reload = AutoreloadMagics(ip)
548 593 ip.register_magics(auto_reload)
549 594 ip.events.register('pre_run_cell', auto_reload.pre_run_cell)
550 595 ip.events.register('post_execute', auto_reload.post_execute_hook)
@@ -1,447 +1,530 b''
1 1 """Tests for autoreload extension.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (c) 2012 IPython Development Team.
5 5 #
6 6 # Distributed under the terms of the Modified BSD License.
7 7 #
8 8 # The full license is in the file COPYING.txt, distributed with this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 import os
16 16 import sys
17 17 import tempfile
18 18 import textwrap
19 19 import shutil
20 20 import random
21 21 import time
22 22 from io import StringIO
23 23
24 24 import nose.tools as nt
25 25 import IPython.testing.tools as tt
26 26
27 27 from unittest import TestCase
28 28
29 29 from IPython.extensions.autoreload import AutoreloadMagics
30 30 from IPython.core.events import EventManager, pre_run_cell
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Test fixture
34 34 #-----------------------------------------------------------------------------
35 35
36 36 noop = lambda *a, **kw: None
37 37
38 38 class FakeShell:
39 39
40 40 def __init__(self):
41 41 self.ns = {}
42 42 self.user_ns = self.ns
43 43 self.user_ns_hidden = {}
44 44 self.events = EventManager(self, {'pre_run_cell', pre_run_cell})
45 45 self.auto_magics = AutoreloadMagics(shell=self)
46 46 self.events.register('pre_run_cell', self.auto_magics.pre_run_cell)
47 47
48 48 register_magics = set_hook = noop
49 49
50 50 def run_code(self, code):
51 51 self.events.trigger('pre_run_cell')
52 52 exec(code, self.user_ns)
53 53 self.auto_magics.post_execute_hook()
54 54
55 55 def push(self, items):
56 56 self.ns.update(items)
57 57
58 58 def magic_autoreload(self, parameter):
59 59 self.auto_magics.autoreload(parameter)
60 60
61 61 def magic_aimport(self, parameter, stream=None):
62 62 self.auto_magics.aimport(parameter, stream=stream)
63 63 self.auto_magics.post_execute_hook()
64 64
65 65
66 66 class Fixture(TestCase):
67 67 """Fixture for creating test module files"""
68 68
69 69 test_dir = None
70 70 old_sys_path = None
71 71 filename_chars = "abcdefghijklmopqrstuvwxyz0123456789"
72 72
73 73 def setUp(self):
74 74 self.test_dir = tempfile.mkdtemp()
75 75 self.old_sys_path = list(sys.path)
76 76 sys.path.insert(0, self.test_dir)
77 77 self.shell = FakeShell()
78 78
79 79 def tearDown(self):
80 80 shutil.rmtree(self.test_dir)
81 81 sys.path = self.old_sys_path
82 82
83 83 self.test_dir = None
84 84 self.old_sys_path = None
85 85 self.shell = None
86 86
87 87 def get_module(self):
88 88 module_name = "tmpmod_" + "".join(random.sample(self.filename_chars,20))
89 89 if module_name in sys.modules:
90 90 del sys.modules[module_name]
91 91 file_name = os.path.join(self.test_dir, module_name + ".py")
92 92 return module_name, file_name
93 93
94 94 def write_file(self, filename, content):
95 95 """
96 96 Write a file, and force a timestamp difference of at least one second
97 97
98 98 Notes
99 99 -----
100 100 Python's .pyc files record the timestamp of their compilation
101 101 with a time resolution of one second.
102 102
103 103 Therefore, we need to force a timestamp difference between .py
104 104 and .pyc, without having the .py file be timestamped in the
105 105 future, and without changing the timestamp of the .pyc file
106 106 (because that is stored in the file). The only reliable way
107 107 to achieve this seems to be to sleep.
108 108 """
109 109 content = textwrap.dedent(content)
110 110 # Sleep one second + eps
111 111 time.sleep(1.05)
112 112
113 113 # Write
114 114 with open(filename, 'w') as f:
115 115 f.write(content)
116 116
117 117 def new_module(self, code):
118 118 code = textwrap.dedent(code)
119 119 mod_name, mod_fn = self.get_module()
120 120 with open(mod_fn, 'w') as f:
121 121 f.write(code)
122 122 return mod_name, mod_fn
123 123
124 124 #-----------------------------------------------------------------------------
125 125 # Test automatic reloading
126 126 #-----------------------------------------------------------------------------
127 127
128 128 def pickle_get_current_class(obj):
129 129 """
130 130 Original issue comes from pickle; hence the name.
131 131 """
132 132 name = obj.__class__.__name__
133 133 module_name = getattr(obj, "__module__", None)
134 134 obj2 = sys.modules[module_name]
135 135 for subpath in name.split("."):
136 136 obj2 = getattr(obj2, subpath)
137 137 return obj2
138 138
139 139 class TestAutoreload(Fixture):
140 140
141 141 def test_reload_enums(self):
142 142 mod_name, mod_fn = self.new_module(textwrap.dedent("""
143 143 from enum import Enum
144 144 class MyEnum(Enum):
145 145 A = 'A'
146 146 B = 'B'
147 147 """))
148 148 self.shell.magic_autoreload("2")
149 149 self.shell.magic_aimport(mod_name)
150 150 self.write_file(mod_fn, textwrap.dedent("""
151 151 from enum import Enum
152 152 class MyEnum(Enum):
153 153 A = 'A'
154 154 B = 'B'
155 155 C = 'C'
156 156 """))
157 157 with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'):
158 158 self.shell.run_code("pass") # trigger another reload
159 159
160 160 def test_reload_class_type(self):
161 161 self.shell.magic_autoreload("2")
162 162 mod_name, mod_fn = self.new_module(
163 163 """
164 164 class Test():
165 165 def meth(self):
166 166 return "old"
167 167 """
168 168 )
169 169 assert "test" not in self.shell.ns
170 170 assert "result" not in self.shell.ns
171 171
172 172 self.shell.run_code("from %s import Test" % mod_name)
173 173 self.shell.run_code("test = Test()")
174 174
175 175 self.write_file(
176 176 mod_fn,
177 177 """
178 178 class Test():
179 179 def meth(self):
180 180 return "new"
181 181 """,
182 182 )
183 183
184 184 test_object = self.shell.ns["test"]
185 185
186 186 # important to trigger autoreload logic !
187 187 self.shell.run_code("pass")
188 188
189 189 test_class = pickle_get_current_class(test_object)
190 190 assert isinstance(test_object, test_class)
191 191
192 192 # extra check.
193 193 self.shell.run_code("import pickle")
194 194 self.shell.run_code("p = pickle.dumps(test)")
195 195
196 196 def test_reload_class_attributes(self):
197 197 self.shell.magic_autoreload("2")
198 198 mod_name, mod_fn = self.new_module(textwrap.dedent("""
199 199 class MyClass:
200 200
201 201 def __init__(self, a=10):
202 202 self.a = a
203 203 self.b = 22
204 204 # self.toto = 33
205 205
206 206 def square(self):
207 207 print('compute square')
208 208 return self.a*self.a
209 209 """
210 210 )
211 211 )
212 212 self.shell.run_code("from %s import MyClass" % mod_name)
213 213 self.shell.run_code("first = MyClass(5)")
214 214 self.shell.run_code("first.square()")
215 215 with nt.assert_raises(AttributeError):
216 216 self.shell.run_code("first.cube()")
217 217 with nt.assert_raises(AttributeError):
218 218 self.shell.run_code("first.power(5)")
219 219 self.shell.run_code("first.b")
220 220 with nt.assert_raises(AttributeError):
221 221 self.shell.run_code("first.toto")
222 222
223 223 # remove square, add power
224 224
225 225 self.write_file(
226 226 mod_fn,
227 227 textwrap.dedent(
228 228 """
229 229 class MyClass:
230 230
231 231 def __init__(self, a=10):
232 232 self.a = a
233 233 self.b = 11
234 234
235 235 def power(self, p):
236 236 print('compute power '+str(p))
237 237 return self.a**p
238 238 """
239 239 ),
240 240 )
241 241
242 242 self.shell.run_code("second = MyClass(5)")
243 243
244 244 for object_name in {'first', 'second'}:
245 245 self.shell.run_code("{object_name}.power(5)".format(object_name=object_name))
246 246 with nt.assert_raises(AttributeError):
247 247 self.shell.run_code("{object_name}.cube()".format(object_name=object_name))
248 248 with nt.assert_raises(AttributeError):
249 249 self.shell.run_code("{object_name}.square()".format(object_name=object_name))
250 250 self.shell.run_code("{object_name}.b".format(object_name=object_name))
251 251 self.shell.run_code("{object_name}.a".format(object_name=object_name))
252 252 with nt.assert_raises(AttributeError):
253 253 self.shell.run_code("{object_name}.toto".format(object_name=object_name))
254 254
255 def test_autoload_newly_added_objects(self):
256 self.shell.magic_autoreload("3")
257 mod_code = """
258 def func1(): pass
259 """
260 mod_name, mod_fn = self.new_module(textwrap.dedent(mod_code))
261 self.shell.run_code(f"from {mod_name} import *")
262 self.shell.run_code("func1()")
263 with nt.assert_raises(NameError):
264 self.shell.run_code('func2()')
265 with nt.assert_raises(NameError):
266 self.shell.run_code('t = Test()')
267 with nt.assert_raises(NameError):
268 self.shell.run_code('number')
269
270 # ----------- TEST NEW OBJ LOADED --------------------------
271
272 new_code = """
273 def func1(): pass
274 def func2(): pass
275 class Test: pass
276 number = 0
277 from enum import Enum
278 class TestEnum(Enum):
279 A = 'a'
280 """
281 self.write_file(mod_fn, textwrap.dedent(new_code))
282
283 # test function now exists in shell's namespace namespace
284 self.shell.run_code("func2()")
285 # test function now exists in module's dict
286 self.shell.run_code(f"import sys; sys.modules['{mod_name}'].func2()")
287 # test class now exists
288 self.shell.run_code("t = Test()")
289 # test global built-in var now exists
290 self.shell.run_code('number')
291 # test the enumerations gets loaded succesfully
292 self.shell.run_code("TestEnum.A")
293
294 # ----------- TEST NEW OBJ CAN BE CHANGED --------------------
295
296 new_code = """
297 def func1(): return 'changed'
298 def func2(): return 'changed'
299 class Test:
300 def new_func(self):
301 return 'changed'
302 number = 1
303 from enum import Enum
304 class TestEnum(Enum):
305 A = 'a'
306 B = 'added'
307 """
308 self.write_file(mod_fn, textwrap.dedent(new_code))
309 self.shell.run_code("assert func1() == 'changed'")
310 self.shell.run_code("assert func2() == 'changed'")
311 self.shell.run_code("t = Test(); assert t.new_func() == 'changed'")
312 self.shell.run_code("assert number == 1")
313 self.shell.run_code("assert TestEnum.B.value == 'added'")
314
315 # ----------- TEST IMPORT FROM MODULE --------------------------
316
317 new_mod_code = '''
318 from enum import Enum
319 class Ext(Enum):
320 A = 'ext'
321 def ext_func():
322 return 'ext'
323 class ExtTest:
324 def meth(self):
325 return 'ext'
326 ext_int = 2
327 '''
328 new_mod_name, new_mod_fn = self.new_module(textwrap.dedent(new_mod_code))
329 current_mod_code = f'''
330 from {new_mod_name} import *
331 '''
332 self.write_file(mod_fn, textwrap.dedent(current_mod_code))
333 self.shell.run_code("assert Ext.A.value == 'ext'")
334 self.shell.run_code("assert ext_func() == 'ext'")
335 self.shell.run_code("t = ExtTest(); assert t.meth() == 'ext'")
336 self.shell.run_code("assert ext_int == 2")
337
255 338 def _check_smoketest(self, use_aimport=True):
256 339 """
257 340 Functional test for the automatic reloader using either
258 341 '%autoreload 1' or '%autoreload 2'
259 342 """
260 343
261 344 mod_name, mod_fn = self.new_module("""
262 345 x = 9
263 346
264 347 z = 123 # this item will be deleted
265 348
266 349 def foo(y):
267 350 return y + 3
268 351
269 352 class Baz(object):
270 353 def __init__(self, x):
271 354 self.x = x
272 355 def bar(self, y):
273 356 return self.x + y
274 357 @property
275 358 def quux(self):
276 359 return 42
277 360 def zzz(self):
278 361 '''This method will be deleted below'''
279 362 return 99
280 363
281 364 class Bar: # old-style class: weakref doesn't work for it on Python < 2.7
282 365 def foo(self):
283 366 return 1
284 367 """)
285 368
286 369 #
287 370 # Import module, and mark for reloading
288 371 #
289 372 if use_aimport:
290 373 self.shell.magic_autoreload("1")
291 374 self.shell.magic_aimport(mod_name)
292 375 stream = StringIO()
293 376 self.shell.magic_aimport("", stream=stream)
294 377 nt.assert_in(("Modules to reload:\n%s" % mod_name), stream.getvalue())
295 378
296 379 with nt.assert_raises(ImportError):
297 380 self.shell.magic_aimport("tmpmod_as318989e89ds")
298 381 else:
299 382 self.shell.magic_autoreload("2")
300 383 self.shell.run_code("import %s" % mod_name)
301 384 stream = StringIO()
302 385 self.shell.magic_aimport("", stream=stream)
303 386 nt.assert_true("Modules to reload:\nall-except-skipped" in
304 387 stream.getvalue())
305 388 nt.assert_in(mod_name, self.shell.ns)
306 389
307 390 mod = sys.modules[mod_name]
308 391
309 392 #
310 393 # Test module contents
311 394 #
312 395 old_foo = mod.foo
313 396 old_obj = mod.Baz(9)
314 397 old_obj2 = mod.Bar()
315 398
316 399 def check_module_contents():
317 400 nt.assert_equal(mod.x, 9)
318 401 nt.assert_equal(mod.z, 123)
319 402
320 403 nt.assert_equal(old_foo(0), 3)
321 404 nt.assert_equal(mod.foo(0), 3)
322 405
323 406 obj = mod.Baz(9)
324 407 nt.assert_equal(old_obj.bar(1), 10)
325 408 nt.assert_equal(obj.bar(1), 10)
326 409 nt.assert_equal(obj.quux, 42)
327 410 nt.assert_equal(obj.zzz(), 99)
328 411
329 412 obj2 = mod.Bar()
330 413 nt.assert_equal(old_obj2.foo(), 1)
331 414 nt.assert_equal(obj2.foo(), 1)
332 415
333 416 check_module_contents()
334 417
335 418 #
336 419 # Simulate a failed reload: no reload should occur and exactly
337 420 # one error message should be printed
338 421 #
339 422 self.write_file(mod_fn, """
340 423 a syntax error
341 424 """)
342 425
343 426 with tt.AssertPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'):
344 427 self.shell.run_code("pass") # trigger reload
345 428 with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'):
346 429 self.shell.run_code("pass") # trigger another reload
347 430 check_module_contents()
348 431
349 432 #
350 433 # Rewrite module (this time reload should succeed)
351 434 #
352 435 self.write_file(mod_fn, """
353 436 x = 10
354 437
355 438 def foo(y):
356 439 return y + 4
357 440
358 441 class Baz(object):
359 442 def __init__(self, x):
360 443 self.x = x
361 444 def bar(self, y):
362 445 return self.x + y + 1
363 446 @property
364 447 def quux(self):
365 448 return 43
366 449
367 450 class Bar: # old-style class
368 451 def foo(self):
369 452 return 2
370 453 """)
371 454
372 455 def check_module_contents():
373 456 nt.assert_equal(mod.x, 10)
374 457 nt.assert_false(hasattr(mod, 'z'))
375 458
376 459 nt.assert_equal(old_foo(0), 4) # superreload magic!
377 460 nt.assert_equal(mod.foo(0), 4)
378 461
379 462 obj = mod.Baz(9)
380 463 nt.assert_equal(old_obj.bar(1), 11) # superreload magic!
381 464 nt.assert_equal(obj.bar(1), 11)
382 465
383 466 nt.assert_equal(old_obj.quux, 43)
384 467 nt.assert_equal(obj.quux, 43)
385 468
386 469 nt.assert_false(hasattr(old_obj, 'zzz'))
387 470 nt.assert_false(hasattr(obj, 'zzz'))
388 471
389 472 obj2 = mod.Bar()
390 473 nt.assert_equal(old_obj2.foo(), 2)
391 474 nt.assert_equal(obj2.foo(), 2)
392 475
393 476 self.shell.run_code("pass") # trigger reload
394 477 check_module_contents()
395 478
396 479 #
397 480 # Another failure case: deleted file (shouldn't reload)
398 481 #
399 482 os.unlink(mod_fn)
400 483
401 484 self.shell.run_code("pass") # trigger reload
402 485 check_module_contents()
403 486
404 487 #
405 488 # Disable autoreload and rewrite module: no reload should occur
406 489 #
407 490 if use_aimport:
408 491 self.shell.magic_aimport("-" + mod_name)
409 492 stream = StringIO()
410 493 self.shell.magic_aimport("", stream=stream)
411 494 nt.assert_true(("Modules to skip:\n%s" % mod_name) in
412 495 stream.getvalue())
413 496
414 497 # This should succeed, although no such module exists
415 498 self.shell.magic_aimport("-tmpmod_as318989e89ds")
416 499 else:
417 500 self.shell.magic_autoreload("0")
418 501
419 502 self.write_file(mod_fn, """
420 503 x = -99
421 504 """)
422 505
423 506 self.shell.run_code("pass") # trigger reload
424 507 self.shell.run_code("pass")
425 508 check_module_contents()
426 509
427 510 #
428 511 # Re-enable autoreload: reload should now occur
429 512 #
430 513 if use_aimport:
431 514 self.shell.magic_aimport(mod_name)
432 515 else:
433 516 self.shell.magic_autoreload("")
434 517
435 518 self.shell.run_code("pass") # trigger reload
436 519 nt.assert_equal(mod.x, -99)
437 520
438 521 def test_smoketest_aimport(self):
439 522 self._check_smoketest(use_aimport=True)
440 523
441 524 def test_smoketest_autoreload(self):
442 525 self._check_smoketest(use_aimport=False)
443 526
444 527
445 528
446 529
447 530
General Comments 0
You need to be logged in to leave comments. Login now