##// END OF EJS Templates
Shaperilio/autoreload verbosity (#13774)...
Fernando Perez -
r28121:0725b4e7 merge
parent child Browse files
Show More
@@ -0,0 +1,26 b''
1 Autoreload verbosity
2 ====================
3
4 We introduce more descriptive names for the ``%autoreload`` parameter:
5
6 - ``%autoreload now`` (also ``%autoreload``) - perform autoreload immediately.
7 - ``%autoreload off`` (also ``%autoreload 0``) - turn off autoreload.
8 - ``%autoreload explicit`` (also ``%autoreload 1``) - turn on autoreload only for modules
9 whitelisted by ``%aimport`` statements.
10 - ``%autoreload all`` (also ``%autoreload 2``) - turn on autoreload for all modules except those
11 blacklisted by ``%aimport`` statements.
12 - ``%autoreload complete`` (also ``%autoreload 3``) - all the fatures of ``all`` but also adding new
13 objects from the imported modules (see
14 IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects).
15
16 The original designations (e.g. "2") still work, and these new ones are case-insensitive.
17
18 Additionally, the option ``--print`` or ``-p`` can be added to the line to print the names of
19 modules being reloaded. Similarly, ``--log`` or ``-l`` will output the names to the logger at INFO
20 level. Both can be used simultaneously.
21
22 The parsing logic for ``%aimport`` is now improved such that modules can be whitelisted and
23 blacklisted in the same line, e.g. it's now possible to call ``%aimport os, -math`` to include
24 ``os`` for ``%autoreload explicit`` and exclude ``math`` for modes ``all`` and ``complete``.
25
26
@@ -1,631 +1,712 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 ``%autoreload``
32 ``%autoreload``, ``%autoreload now``
33 33
34 34 Reload all modules (except those excluded by ``%aimport``)
35 35 automatically now.
36 36
37 ``%autoreload 0``
37 ``%autoreload 0``, ``%autoreload off``
38 38
39 39 Disable automatic reloading.
40 40
41 ``%autoreload 1``
41 ``%autoreload 1``, ``%autoreload explicit``
42 42
43 43 Reload all modules imported with ``%aimport`` every time before
44 44 executing the Python code typed.
45 45
46 ``%autoreload 2``
46 ``%autoreload 2``, ``%autoreload all``
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``
51 ``%autoreload 3``, ``%autoreload complete``
52 52
53 Reload all modules AND autoload newly added objects
54 every time before executing the Python code typed.
53 Same as 2/all, but also adds any new objects in the module. See
54 unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects
55
56 Adding ``--print`` or ``-p`` to the ``%autoreload`` line will print autoreload activity to
57 standard out. ``--log`` or ``-l`` will do it to the log at INFO level; both can be used
58 simultaneously.
55 59
56 60 ``%aimport``
57 61
58 62 List modules which are to be automatically imported or not to be imported.
59 63
60 64 ``%aimport foo``
61 65
62 66 Import module 'foo' and mark it to be autoreloaded for ``%autoreload 1``
63 67
64 68 ``%aimport foo, bar``
65 69
66 70 Import modules 'foo', 'bar' and mark them to be autoreloaded for ``%autoreload 1``
67 71
68 72 ``%aimport -foo``
69 73
70 74 Mark module 'foo' to not be autoreloaded.
71 75
72 76 Caveats
73 77 =======
74 78
75 79 Reloading Python modules in a reliable way is in general difficult,
76 80 and unexpected things may occur. ``%autoreload`` tries to work around
77 81 common pitfalls by replacing function code objects and parts of
78 82 classes previously in the module with new versions. This makes the
79 83 following things to work:
80 84
81 85 - Functions and classes imported via 'from xxx import foo' are upgraded
82 86 to new versions when 'xxx' is reloaded.
83 87
84 88 - Methods and properties of classes are upgraded on reload, so that
85 89 calling 'c.foo()' on an object 'c' created before the reload causes
86 90 the new code for 'foo' to be executed.
87 91
88 92 Some of the known remaining caveats are:
89 93
90 94 - Replacing code objects does not always succeed: changing a @property
91 95 in a class to an ordinary method or a method to a member variable
92 96 can cause problems (but in old objects only).
93 97
94 98 - Functions that are removed (eg. via monkey-patching) from a module
95 99 before it is reloaded are not upgraded.
96 100
97 101 - C extension modules cannot be reloaded, and so cannot be autoreloaded.
98 102
99 103 - While comparing Enum and Flag, the 'is' Identity Operator is used (even in the case '==' has been used (Similar to the 'None' keyword)).
100 104
101 105 - Reloading a module, or importing the same module by a different name, creates new Enums. These may look the same, but are not.
102 106 """
103 107
108 from IPython.core import magic_arguments
109 from IPython.core.magic import Magics, magics_class, line_magic
110
104 111 __skip_doctest__ = True
105 112
106 113 # -----------------------------------------------------------------------------
107 114 # Copyright (C) 2000 Thomas Heller
108 115 # Copyright (C) 2008 Pauli Virtanen <pav@iki.fi>
109 116 # Copyright (C) 2012 The IPython Development Team
110 117 #
111 118 # Distributed under the terms of the BSD License. The full license is in
112 119 # the file COPYING, distributed as part of this software.
113 120 # -----------------------------------------------------------------------------
114 121 #
115 122 # This IPython module is written by Pauli Virtanen, based on the autoreload
116 123 # code by Thomas Heller.
117 124
118 125 # -----------------------------------------------------------------------------
119 126 # Imports
120 127 # -----------------------------------------------------------------------------
121 128
122 129 import os
123 130 import sys
124 131 import traceback
125 132 import types
126 133 import weakref
127 134 import gc
135 import logging
128 136 from importlib import import_module, reload
129 137 from importlib.util import source_from_cache
130 138
131 139 # ------------------------------------------------------------------------------
132 140 # Autoreload functionality
133 141 # ------------------------------------------------------------------------------
134 142
135 143
136 144 class ModuleReloader:
137 145 enabled = False
138 146 """Whether this reloader is enabled"""
139 147
140 148 check_all = True
141 149 """Autoreload all modules, not just those listed in 'modules'"""
142 150
143 151 autoload_obj = False
144 152 """Autoreload all modules AND autoload all new objects"""
145 153
146 154 def __init__(self, shell=None):
147 155 # Modules that failed to reload: {module: mtime-on-failed-reload, ...}
148 156 self.failed = {}
149 157 # Modules specially marked as autoreloadable.
150 158 self.modules = {}
151 159 # Modules specially marked as not autoreloadable.
152 160 self.skip_modules = {}
153 161 # (module-name, name) -> weakref, for replacing old code objects
154 162 self.old_objects = {}
155 163 # Module modification timestamps
156 164 self.modules_mtimes = {}
157 165 self.shell = shell
158 166
167 # Reporting callable for verbosity
168 self._report = lambda msg: None # by default, be quiet.
169
159 170 # Cache module modification times
160 171 self.check(check_all=True, do_reload=False)
161 172
162 173 def mark_module_skipped(self, module_name):
163 174 """Skip reloading the named module in the future"""
164 175 try:
165 176 del self.modules[module_name]
166 177 except KeyError:
167 178 pass
168 179 self.skip_modules[module_name] = True
169 180
170 181 def mark_module_reloadable(self, module_name):
171 182 """Reload the named module in the future (if it is imported)"""
172 183 try:
173 184 del self.skip_modules[module_name]
174 185 except KeyError:
175 186 pass
176 187 self.modules[module_name] = True
177 188
178 189 def aimport_module(self, module_name):
179 190 """Import a module, and mark it reloadable
180 191
181 192 Returns
182 193 -------
183 194 top_module : module
184 195 The imported module if it is top-level, or the top-level
185 196 top_name : module
186 197 Name of top_module
187 198
188 199 """
189 200 self.mark_module_reloadable(module_name)
190 201
191 202 import_module(module_name)
192 203 top_name = module_name.split(".")[0]
193 204 top_module = sys.modules[top_name]
194 205 return top_module, top_name
195 206
196 207 def filename_and_mtime(self, module):
197 208 if not hasattr(module, "__file__") or module.__file__ is None:
198 209 return None, None
199 210
200 211 if getattr(module, "__name__", None) in [None, "__mp_main__", "__main__"]:
201 212 # we cannot reload(__main__) or reload(__mp_main__)
202 213 return None, None
203 214
204 215 filename = module.__file__
205 216 path, ext = os.path.splitext(filename)
206 217
207 218 if ext.lower() == ".py":
208 219 py_filename = filename
209 220 else:
210 221 try:
211 222 py_filename = source_from_cache(filename)
212 223 except ValueError:
213 224 return None, None
214 225
215 226 try:
216 227 pymtime = os.stat(py_filename).st_mtime
217 228 except OSError:
218 229 return None, None
219 230
220 231 return py_filename, pymtime
221 232
222 233 def check(self, check_all=False, do_reload=True):
223 234 """Check whether some modules need to be reloaded."""
224 235
225 236 if not self.enabled and not check_all:
226 237 return
227 238
228 239 if check_all or self.check_all:
229 240 modules = list(sys.modules.keys())
230 241 else:
231 242 modules = list(self.modules.keys())
232 243
233 244 for modname in modules:
234 245 m = sys.modules.get(modname, None)
235 246
236 247 if modname in self.skip_modules:
237 248 continue
238 249
239 250 py_filename, pymtime = self.filename_and_mtime(m)
240 251 if py_filename is None:
241 252 continue
242 253
243 254 try:
244 255 if pymtime <= self.modules_mtimes[modname]:
245 256 continue
246 257 except KeyError:
247 258 self.modules_mtimes[modname] = pymtime
248 259 continue
249 260 else:
250 261 if self.failed.get(py_filename, None) == pymtime:
251 262 continue
252 263
253 264 self.modules_mtimes[modname] = pymtime
254 265
255 266 # If we've reached this point, we should try to reload the module
256 267 if do_reload:
268 self._report(f"Reloading '{modname}'.")
257 269 try:
258 270 if self.autoload_obj:
259 271 superreload(m, reload, self.old_objects, self.shell)
260 272 else:
261 273 superreload(m, reload, self.old_objects)
262 274 if py_filename in self.failed:
263 275 del self.failed[py_filename]
264 276 except:
265 277 print(
266 278 "[autoreload of {} failed: {}]".format(
267 279 modname, traceback.format_exc(10)
268 280 ),
269 281 file=sys.stderr,
270 282 )
271 283 self.failed[py_filename] = pymtime
272 284
273 285
274 286 # ------------------------------------------------------------------------------
275 287 # superreload
276 288 # ------------------------------------------------------------------------------
277 289
278 290
279 291 func_attrs = [
280 292 "__code__",
281 293 "__defaults__",
282 294 "__doc__",
283 295 "__closure__",
284 296 "__globals__",
285 297 "__dict__",
286 298 ]
287 299
288 300
289 301 def update_function(old, new):
290 302 """Upgrade the code object of a function"""
291 303 for name in func_attrs:
292 304 try:
293 305 setattr(old, name, getattr(new, name))
294 306 except (AttributeError, TypeError):
295 307 pass
296 308
297 309
298 310 def update_instances(old, new):
299 311 """Use garbage collector to find all instances that refer to the old
300 312 class definition and update their __class__ to point to the new class
301 313 definition"""
302 314
303 315 refs = gc.get_referrers(old)
304 316
305 317 for ref in refs:
306 318 if type(ref) is old:
307 319 object.__setattr__(ref, "__class__", new)
308 320
309 321
310 322 def update_class(old, new):
311 323 """Replace stuff in the __dict__ of a class, and upgrade
312 324 method code objects, and add new methods, if any"""
313 325 for key in list(old.__dict__.keys()):
314 326 old_obj = getattr(old, key)
315 327 try:
316 328 new_obj = getattr(new, key)
317 329 # explicitly checking that comparison returns True to handle
318 330 # cases where `==` doesn't return a boolean.
319 331 if (old_obj == new_obj) is True:
320 332 continue
321 333 except AttributeError:
322 334 # obsolete attribute: remove it
323 335 try:
324 336 delattr(old, key)
325 337 except (AttributeError, TypeError):
326 338 pass
327 339 continue
328 340 except ValueError:
329 341 # can't compare nested structures containing
330 342 # numpy arrays using `==`
331 343 pass
332 344
333 345 if update_generic(old_obj, new_obj):
334 346 continue
335 347
336 348 try:
337 349 setattr(old, key, getattr(new, key))
338 350 except (AttributeError, TypeError):
339 351 pass # skip non-writable attributes
340 352
341 353 for key in list(new.__dict__.keys()):
342 354 if key not in list(old.__dict__.keys()):
343 355 try:
344 356 setattr(old, key, getattr(new, key))
345 357 except (AttributeError, TypeError):
346 358 pass # skip non-writable attributes
347 359
348 360 # update all instances of class
349 361 update_instances(old, new)
350 362
351 363
352 364 def update_property(old, new):
353 365 """Replace get/set/del functions of a property"""
354 366 update_generic(old.fdel, new.fdel)
355 367 update_generic(old.fget, new.fget)
356 368 update_generic(old.fset, new.fset)
357 369
358 370
359 371 def isinstance2(a, b, typ):
360 372 return isinstance(a, typ) and isinstance(b, typ)
361 373
362 374
363 375 UPDATE_RULES = [
364 376 (lambda a, b: isinstance2(a, b, type), update_class),
365 377 (lambda a, b: isinstance2(a, b, types.FunctionType), update_function),
366 378 (lambda a, b: isinstance2(a, b, property), update_property),
367 379 ]
368 380 UPDATE_RULES.extend(
369 381 [
370 382 (
371 383 lambda a, b: isinstance2(a, b, types.MethodType),
372 384 lambda a, b: update_function(a.__func__, b.__func__),
373 385 ),
374 386 ]
375 387 )
376 388
377 389
378 390 def update_generic(a, b):
379 391 for type_check, update in UPDATE_RULES:
380 392 if type_check(a, b):
381 393 update(a, b)
382 394 return True
383 395 return False
384 396
385 397
386 398 class StrongRef:
387 399 def __init__(self, obj):
388 400 self.obj = obj
389 401
390 402 def __call__(self):
391 403 return self.obj
392 404
393 405
394 406 mod_attrs = [
395 407 "__name__",
396 408 "__doc__",
397 409 "__package__",
398 410 "__loader__",
399 411 "__spec__",
400 412 "__file__",
401 413 "__cached__",
402 414 "__builtins__",
403 415 ]
404 416
405 417
406 418 def append_obj(module, d, name, obj, autoload=False):
407 419 in_module = hasattr(obj, "__module__") and obj.__module__ == module.__name__
408 420 if autoload:
409 421 # check needed for module global built-ins
410 422 if not in_module and name in mod_attrs:
411 423 return False
412 424 else:
413 425 if not in_module:
414 426 return False
415 427
416 428 key = (module.__name__, name)
417 429 try:
418 430 d.setdefault(key, []).append(weakref.ref(obj))
419 431 except TypeError:
420 432 pass
421 433 return True
422 434
423 435
424 436 def superreload(module, reload=reload, old_objects=None, shell=None):
425 437 """Enhanced version of the builtin reload function.
426 438
427 439 superreload remembers objects previously in the module, and
428 440
429 441 - upgrades the class dictionary of every old class in the module
430 442 - upgrades the code object of every old function and method
431 443 - clears the module's namespace before reloading
432 444
433 445 """
434 446 if old_objects is None:
435 447 old_objects = {}
436 448
437 449 # collect old objects in the module
438 450 for name, obj in list(module.__dict__.items()):
439 451 if not append_obj(module, old_objects, name, obj):
440 452 continue
441 453 key = (module.__name__, name)
442 454 try:
443 455 old_objects.setdefault(key, []).append(weakref.ref(obj))
444 456 except TypeError:
445 457 pass
446 458
447 459 # reload module
448 460 try:
449 461 # clear namespace first from old cruft
450 462 old_dict = module.__dict__.copy()
451 463 old_name = module.__name__
452 464 module.__dict__.clear()
453 465 module.__dict__["__name__"] = old_name
454 466 module.__dict__["__loader__"] = old_dict["__loader__"]
455 467 except (TypeError, AttributeError, KeyError):
456 468 pass
457 469
458 470 try:
459 471 module = reload(module)
460 472 except:
461 473 # restore module dictionary on failed reload
462 474 module.__dict__.update(old_dict)
463 475 raise
464 476
465 477 # iterate over all objects and update functions & classes
466 478 for name, new_obj in list(module.__dict__.items()):
467 479 key = (module.__name__, name)
468 480 if key not in old_objects:
469 481 # here 'shell' acts both as a flag and as an output var
470 482 if (
471 483 shell is None
472 484 or name == "Enum"
473 485 or not append_obj(module, old_objects, name, new_obj, True)
474 486 ):
475 487 continue
476 488 shell.user_ns[name] = new_obj
477 489
478 490 new_refs = []
479 491 for old_ref in old_objects[key]:
480 492 old_obj = old_ref()
481 493 if old_obj is None:
482 494 continue
483 495 new_refs.append(old_ref)
484 496 update_generic(old_obj, new_obj)
485 497
486 498 if new_refs:
487 499 old_objects[key] = new_refs
488 500 else:
489 501 del old_objects[key]
490 502
491 503 return module
492 504
493 505
494 506 # ------------------------------------------------------------------------------
495 507 # IPython connectivity
496 508 # ------------------------------------------------------------------------------
497 509
498 from IPython.core.magic import Magics, magics_class, line_magic
499
500 510
501 511 @magics_class
502 512 class AutoreloadMagics(Magics):
503 513 def __init__(self, *a, **kw):
504 514 super().__init__(*a, **kw)
505 515 self._reloader = ModuleReloader(self.shell)
506 516 self._reloader.check_all = False
507 517 self._reloader.autoload_obj = False
508 518 self.loaded_modules = set(sys.modules)
509 519
510 520 @line_magic
511 def autoreload(self, parameter_s=""):
521 @magic_arguments.magic_arguments()
522 @magic_arguments.argument(
523 "mode",
524 type=str,
525 default="now",
526 nargs="?",
527 help="""blank or 'now' - Reload all modules (except those excluded by %%aimport)
528 automatically now.
529
530 '0' or 'off' - Disable automatic reloading.
531
532 '1' or 'explicit' - Reload only modules imported with %%aimport every
533 time before executing the Python code typed.
534
535 '2' or 'all' - Reload all modules (except those excluded by %%aimport)
536 every time before executing the Python code typed.
537
538 '3' or 'complete' - Same as 2/all, but also but also adds any new
539 objects in the module.
540 """,
541 )
542 @magic_arguments.argument(
543 "-p",
544 "--print",
545 action="store_true",
546 default=False,
547 help="Show autoreload activity using `print` statements",
548 )
549 @magic_arguments.argument(
550 "-l",
551 "--log",
552 action="store_true",
553 default=False,
554 help="Show autoreload activity using the logger",
555 )
556 def autoreload(self, line=""):
512 557 r"""%autoreload => Reload modules automatically
513 558
514 %autoreload
559 %autoreload or %autoreload now
515 560 Reload all modules (except those excluded by %aimport) automatically
516 561 now.
517 562
518 %autoreload 0
563 %autoreload 0 or %autoreload off
519 564 Disable automatic reloading.
520 565
521 %autoreload 1
522 Reload all modules imported with %aimport every time before executing
566 %autoreload 1 or %autoreload explicit
567 Reload only modules imported with %aimport every time before executing
523 568 the Python code typed.
524 569
525 %autoreload 2
570 %autoreload 2 or %autoreload all
526 571 Reload all modules (except those excluded by %aimport) every time
527 572 before executing the Python code typed.
528 573
574 %autoreload 3 or %autoreload complete
575 Same as 2/all, but also but also adds any new objects in the module. See
576 unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects
577
578 The optional arguments --print and --log control display of autoreload activity. The default
579 is to act silently; --print (or -p) will print out the names of modules that are being
580 reloaded, and --log (or -l) outputs them to the log at INFO level.
581
529 582 Reloading Python modules in a reliable way is in general
530 583 difficult, and unexpected things may occur. %autoreload tries to
531 584 work around common pitfalls by replacing function code objects and
532 585 parts of classes previously in the module with new versions. This
533 586 makes the following things to work:
534 587
535 588 - Functions and classes imported via 'from xxx import foo' are upgraded
536 589 to new versions when 'xxx' is reloaded.
537 590
538 591 - Methods and properties of classes are upgraded on reload, so that
539 592 calling 'c.foo()' on an object 'c' created before the reload causes
540 593 the new code for 'foo' to be executed.
541 594
542 595 Some of the known remaining caveats are:
543 596
544 597 - Replacing code objects does not always succeed: changing a @property
545 598 in a class to an ordinary method or a method to a member variable
546 599 can cause problems (but in old objects only).
547 600
548 601 - Functions that are removed (eg. via monkey-patching) from a module
549 602 before it is reloaded are not upgraded.
550 603
551 604 - C extension modules cannot be reloaded, and so cannot be
552 605 autoreloaded.
553 606
554 607 """
555 if parameter_s == "":
608 args = magic_arguments.parse_argstring(self.autoreload, line)
609 mode = args.mode.lower()
610
611 p = print
612
613 logger = logging.getLogger("autoreload")
614
615 l = logger.info
616
617 def pl(msg):
618 p(msg)
619 l(msg)
620
621 if args.print is False and args.log is False:
622 self._reloader._report = lambda msg: None
623 elif args.print is True:
624 if args.log is True:
625 self._reloader._report = pl
626 else:
627 self._reloader._report = p
628 elif args.log is True:
629 self._reloader._report = l
630
631 if mode == "" or mode == "now":
556 632 self._reloader.check(True)
557 elif parameter_s == "0":
633 elif mode == "0" or mode == "off":
558 634 self._reloader.enabled = False
559 elif parameter_s == "1":
635 elif mode == "1" or mode == "explicit":
636 self._reloader.enabled = True
560 637 self._reloader.check_all = False
638 self._reloader.autoload_obj = False
639 elif mode == "2" or mode == "all":
561 640 self._reloader.enabled = True
562 elif parameter_s == "2":
563 641 self._reloader.check_all = True
642 self._reloader.autoload_obj = False
643 elif mode == "3" or mode == "complete":
564 644 self._reloader.enabled = True
565 self._reloader.enabled = True
566 elif parameter_s == "3":
567 645 self._reloader.check_all = True
568 self._reloader.enabled = True
569 646 self._reloader.autoload_obj = True
647 else:
648 raise ValueError(f'Unrecognized autoreload mode "{mode}".')
570 649
571 650 @line_magic
572 651 def aimport(self, parameter_s="", stream=None):
573 652 """%aimport => Import modules for automatic reloading.
574 653
575 654 %aimport
576 655 List modules to automatically import and not to import.
577 656
578 657 %aimport foo
579 Import module 'foo' and mark it to be autoreloaded for %autoreload 1
658 Import module 'foo' and mark it to be autoreloaded for %autoreload explicit
580 659
581 660 %aimport foo, bar
582 Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload 1
661 Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload explicit
583 662
584 %aimport -foo
585 Mark module 'foo' to not be autoreloaded for %autoreload 1
663 %aimport -foo, bar
664 Mark module 'foo' to not be autoreloaded for %autoreload explicit, all, or complete, and 'bar'
665 to be autoreloaded for mode explicit.
586 666 """
587 667 modname = parameter_s
588 668 if not modname:
589 669 to_reload = sorted(self._reloader.modules.keys())
590 670 to_skip = sorted(self._reloader.skip_modules.keys())
591 671 if stream is None:
592 672 stream = sys.stdout
593 673 if self._reloader.check_all:
594 674 stream.write("Modules to reload:\nall-except-skipped\n")
595 675 else:
596 676 stream.write("Modules to reload:\n%s\n" % " ".join(to_reload))
597 677 stream.write("\nModules to skip:\n%s\n" % " ".join(to_skip))
598 elif modname.startswith("-"):
599 modname = modname[1:]
600 self._reloader.mark_module_skipped(modname)
601 678 else:
602 679 for _module in [_.strip() for _ in modname.split(",")]:
680 if _module.startswith("-"):
681 _module = _module[1:].strip()
682 self._reloader.mark_module_skipped(_module)
683 else:
603 684 top_module, top_name = self._reloader.aimport_module(_module)
604 685
605 686 # Inject module to user namespace
606 687 self.shell.push({top_name: top_module})
607 688
608 689 def pre_run_cell(self):
609 690 if self._reloader.enabled:
610 691 try:
611 692 self._reloader.check()
612 693 except:
613 694 pass
614 695
615 696 def post_execute_hook(self):
616 697 """Cache the modification times of any modules imported in this execution"""
617 698 newly_loaded_modules = set(sys.modules) - self.loaded_modules
618 699 for modname in newly_loaded_modules:
619 700 _, pymtime = self._reloader.filename_and_mtime(sys.modules[modname])
620 701 if pymtime is not None:
621 702 self._reloader.modules_mtimes[modname] = pymtime
622 703
623 704 self.loaded_modules.update(newly_loaded_modules)
624 705
625 706
626 707 def load_ipython_extension(ip):
627 708 """Load the extension in IPython."""
628 709 auto_reload = AutoreloadMagics(ip)
629 710 ip.register_magics(auto_reload)
630 711 ip.events.register("pre_run_cell", auto_reload.pre_run_cell)
631 712 ip.events.register("post_execute", auto_reload.post_execute_hook)
@@ -1,598 +1,689 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 platform
17 17 import pytest
18 18 import sys
19 19 import tempfile
20 20 import textwrap
21 21 import shutil
22 22 import random
23 23 import time
24 24 from io import StringIO
25 from dataclasses import dataclass
25 26
26 27 import IPython.testing.tools as tt
27 28
28 29 from unittest import TestCase
29 30
30 31 from IPython.extensions.autoreload import AutoreloadMagics
31 32 from IPython.core.events import EventManager, pre_run_cell
32 33 from IPython.testing.decorators import skipif_not_numpy
33 34
34 35 if platform.python_implementation() == "PyPy":
35 36 pytest.skip(
36 37 "Current autoreload implementation is extremely slow on PyPy",
37 38 allow_module_level=True,
38 39 )
39 40
40 41 # -----------------------------------------------------------------------------
41 42 # Test fixture
42 43 # -----------------------------------------------------------------------------
43 44
44 45 noop = lambda *a, **kw: None
45 46
46 47
47 48 class FakeShell:
48 49 def __init__(self):
49 50 self.ns = {}
50 51 self.user_ns = self.ns
51 52 self.user_ns_hidden = {}
52 53 self.events = EventManager(self, {"pre_run_cell", pre_run_cell})
53 54 self.auto_magics = AutoreloadMagics(shell=self)
54 55 self.events.register("pre_run_cell", self.auto_magics.pre_run_cell)
55 56
56 57 register_magics = set_hook = noop
57 58
58 59 def run_code(self, code):
59 60 self.events.trigger("pre_run_cell")
60 61 exec(code, self.user_ns)
61 62 self.auto_magics.post_execute_hook()
62 63
63 64 def push(self, items):
64 65 self.ns.update(items)
65 66
66 67 def magic_autoreload(self, parameter):
67 68 self.auto_magics.autoreload(parameter)
68 69
69 70 def magic_aimport(self, parameter, stream=None):
70 71 self.auto_magics.aimport(parameter, stream=stream)
71 72 self.auto_magics.post_execute_hook()
72 73
73 74
74 75 class Fixture(TestCase):
75 76 """Fixture for creating test module files"""
76 77
77 78 test_dir = None
78 79 old_sys_path = None
79 80 filename_chars = "abcdefghijklmopqrstuvwxyz0123456789"
80 81
81 82 def setUp(self):
82 83 self.test_dir = tempfile.mkdtemp()
83 84 self.old_sys_path = list(sys.path)
84 85 sys.path.insert(0, self.test_dir)
85 86 self.shell = FakeShell()
86 87
87 88 def tearDown(self):
88 89 shutil.rmtree(self.test_dir)
89 90 sys.path = self.old_sys_path
90 91
91 92 self.test_dir = None
92 93 self.old_sys_path = None
93 94 self.shell = None
94 95
95 96 def get_module(self):
96 97 module_name = "tmpmod_" + "".join(random.sample(self.filename_chars, 20))
97 98 if module_name in sys.modules:
98 99 del sys.modules[module_name]
99 100 file_name = os.path.join(self.test_dir, module_name + ".py")
100 101 return module_name, file_name
101 102
102 103 def write_file(self, filename, content):
103 104 """
104 105 Write a file, and force a timestamp difference of at least one second
105 106
106 107 Notes
107 108 -----
108 109 Python's .pyc files record the timestamp of their compilation
109 110 with a time resolution of one second.
110 111
111 112 Therefore, we need to force a timestamp difference between .py
112 113 and .pyc, without having the .py file be timestamped in the
113 114 future, and without changing the timestamp of the .pyc file
114 115 (because that is stored in the file). The only reliable way
115 116 to achieve this seems to be to sleep.
116 117 """
117 118 content = textwrap.dedent(content)
118 119 # Sleep one second + eps
119 120 time.sleep(1.05)
120 121
121 122 # Write
122 123 with open(filename, "w", encoding="utf-8") as f:
123 124 f.write(content)
124 125
125 126 def new_module(self, code):
126 127 code = textwrap.dedent(code)
127 128 mod_name, mod_fn = self.get_module()
128 129 with open(mod_fn, "w", encoding="utf-8") as f:
129 130 f.write(code)
130 131 return mod_name, mod_fn
131 132
132 133
133 134 # -----------------------------------------------------------------------------
134 135 # Test automatic reloading
135 136 # -----------------------------------------------------------------------------
136 137
137 138
138 139 def pickle_get_current_class(obj):
139 140 """
140 141 Original issue comes from pickle; hence the name.
141 142 """
142 143 name = obj.__class__.__name__
143 144 module_name = getattr(obj, "__module__", None)
144 145 obj2 = sys.modules[module_name]
145 146 for subpath in name.split("."):
146 147 obj2 = getattr(obj2, subpath)
147 148 return obj2
148 149
149 150
150 151 class TestAutoreload(Fixture):
151 152 def test_reload_enums(self):
152 153 mod_name, mod_fn = self.new_module(
153 154 textwrap.dedent(
154 155 """
155 156 from enum import Enum
156 157 class MyEnum(Enum):
157 158 A = 'A'
158 159 B = 'B'
159 160 """
160 161 )
161 162 )
162 163 self.shell.magic_autoreload("2")
163 164 self.shell.magic_aimport(mod_name)
164 165 self.write_file(
165 166 mod_fn,
166 167 textwrap.dedent(
167 168 """
168 169 from enum import Enum
169 170 class MyEnum(Enum):
170 171 A = 'A'
171 172 B = 'B'
172 173 C = 'C'
173 174 """
174 175 ),
175 176 )
176 177 with tt.AssertNotPrints(
177 178 ("[autoreload of %s failed:" % mod_name), channel="stderr"
178 179 ):
179 180 self.shell.run_code("pass") # trigger another reload
180 181
181 182 def test_reload_class_type(self):
182 183 self.shell.magic_autoreload("2")
183 184 mod_name, mod_fn = self.new_module(
184 185 """
185 186 class Test():
186 187 def meth(self):
187 188 return "old"
188 189 """
189 190 )
190 191 assert "test" not in self.shell.ns
191 192 assert "result" not in self.shell.ns
192 193
193 194 self.shell.run_code("from %s import Test" % mod_name)
194 195 self.shell.run_code("test = Test()")
195 196
196 197 self.write_file(
197 198 mod_fn,
198 199 """
199 200 class Test():
200 201 def meth(self):
201 202 return "new"
202 203 """,
203 204 )
204 205
205 206 test_object = self.shell.ns["test"]
206 207
207 208 # important to trigger autoreload logic !
208 209 self.shell.run_code("pass")
209 210
210 211 test_class = pickle_get_current_class(test_object)
211 212 assert isinstance(test_object, test_class)
212 213
213 214 # extra check.
214 215 self.shell.run_code("import pickle")
215 216 self.shell.run_code("p = pickle.dumps(test)")
216 217
217 218 def test_reload_class_attributes(self):
218 219 self.shell.magic_autoreload("2")
219 220 mod_name, mod_fn = self.new_module(
220 221 textwrap.dedent(
221 222 """
222 223 class MyClass:
223 224
224 225 def __init__(self, a=10):
225 226 self.a = a
226 227 self.b = 22
227 228 # self.toto = 33
228 229
229 230 def square(self):
230 231 print('compute square')
231 232 return self.a*self.a
232 233 """
233 234 )
234 235 )
235 236 self.shell.run_code("from %s import MyClass" % mod_name)
236 237 self.shell.run_code("first = MyClass(5)")
237 238 self.shell.run_code("first.square()")
238 239 with self.assertRaises(AttributeError):
239 240 self.shell.run_code("first.cube()")
240 241 with self.assertRaises(AttributeError):
241 242 self.shell.run_code("first.power(5)")
242 243 self.shell.run_code("first.b")
243 244 with self.assertRaises(AttributeError):
244 245 self.shell.run_code("first.toto")
245 246
246 247 # remove square, add power
247 248
248 249 self.write_file(
249 250 mod_fn,
250 251 textwrap.dedent(
251 252 """
252 253 class MyClass:
253 254
254 255 def __init__(self, a=10):
255 256 self.a = a
256 257 self.b = 11
257 258
258 259 def power(self, p):
259 260 print('compute power '+str(p))
260 261 return self.a**p
261 262 """
262 263 ),
263 264 )
264 265
265 266 self.shell.run_code("second = MyClass(5)")
266 267
267 268 for object_name in {"first", "second"}:
268 269 self.shell.run_code(f"{object_name}.power(5)")
269 270 with self.assertRaises(AttributeError):
270 271 self.shell.run_code(f"{object_name}.cube()")
271 272 with self.assertRaises(AttributeError):
272 273 self.shell.run_code(f"{object_name}.square()")
273 274 self.shell.run_code(f"{object_name}.b")
274 275 self.shell.run_code(f"{object_name}.a")
275 276 with self.assertRaises(AttributeError):
276 277 self.shell.run_code(f"{object_name}.toto")
277 278
278 279 @skipif_not_numpy
279 280 def test_comparing_numpy_structures(self):
280 281 self.shell.magic_autoreload("2")
281 282 mod_name, mod_fn = self.new_module(
282 283 textwrap.dedent(
283 284 """
284 285 import numpy as np
285 286 class MyClass:
286 287 a = (np.array((.1, .2)),
287 288 np.array((.2, .3)))
288 289 """
289 290 )
290 291 )
291 292 self.shell.run_code("from %s import MyClass" % mod_name)
292 293 self.shell.run_code("first = MyClass()")
293 294
294 295 # change property `a`
295 296 self.write_file(
296 297 mod_fn,
297 298 textwrap.dedent(
298 299 """
299 300 import numpy as np
300 301 class MyClass:
301 302 a = (np.array((.3, .4)),
302 303 np.array((.5, .6)))
303 304 """
304 305 ),
305 306 )
306 307
307 308 with tt.AssertNotPrints(
308 309 ("[autoreload of %s failed:" % mod_name), channel="stderr"
309 310 ):
310 311 self.shell.run_code("pass") # trigger another reload
311 312
312 313 def test_autoload_newly_added_objects(self):
314 # All of these fail with %autoreload 2
313 315 self.shell.magic_autoreload("3")
314 316 mod_code = """
315 317 def func1(): pass
316 318 """
317 319 mod_name, mod_fn = self.new_module(textwrap.dedent(mod_code))
318 320 self.shell.run_code(f"from {mod_name} import *")
319 321 self.shell.run_code("func1()")
320 322 with self.assertRaises(NameError):
321 323 self.shell.run_code("func2()")
322 324 with self.assertRaises(NameError):
323 325 self.shell.run_code("t = Test()")
324 326 with self.assertRaises(NameError):
325 327 self.shell.run_code("number")
326 328
327 329 # ----------- TEST NEW OBJ LOADED --------------------------
328 330
329 331 new_code = """
330 332 def func1(): pass
331 333 def func2(): pass
332 334 class Test: pass
333 335 number = 0
334 336 from enum import Enum
335 337 class TestEnum(Enum):
336 338 A = 'a'
337 339 """
338 340 self.write_file(mod_fn, textwrap.dedent(new_code))
339 341
340 342 # test function now exists in shell's namespace namespace
341 343 self.shell.run_code("func2()")
342 344 # test function now exists in module's dict
343 345 self.shell.run_code(f"import sys; sys.modules['{mod_name}'].func2()")
344 346 # test class now exists
345 347 self.shell.run_code("t = Test()")
346 348 # test global built-in var now exists
347 349 self.shell.run_code("number")
348 350 # test the enumerations gets loaded successfully
349 351 self.shell.run_code("TestEnum.A")
350 352
351 353 # ----------- TEST NEW OBJ CAN BE CHANGED --------------------
352 354
353 355 new_code = """
354 356 def func1(): return 'changed'
355 357 def func2(): return 'changed'
356 358 class Test:
357 359 def new_func(self):
358 360 return 'changed'
359 361 number = 1
360 362 from enum import Enum
361 363 class TestEnum(Enum):
362 364 A = 'a'
363 365 B = 'added'
364 366 """
365 367 self.write_file(mod_fn, textwrap.dedent(new_code))
366 368 self.shell.run_code("assert func1() == 'changed'")
367 369 self.shell.run_code("assert func2() == 'changed'")
368 370 self.shell.run_code("t = Test(); assert t.new_func() == 'changed'")
369 371 self.shell.run_code("assert number == 1")
370 372 if sys.version_info < (3, 12):
371 373 self.shell.run_code("assert TestEnum.B.value == 'added'")
372 374
373 375 # ----------- TEST IMPORT FROM MODULE --------------------------
374 376
375 377 new_mod_code = """
376 378 from enum import Enum
377 379 class Ext(Enum):
378 380 A = 'ext'
379 381 def ext_func():
380 382 return 'ext'
381 383 class ExtTest:
382 384 def meth(self):
383 385 return 'ext'
384 386 ext_int = 2
385 387 """
386 388 new_mod_name, new_mod_fn = self.new_module(textwrap.dedent(new_mod_code))
387 389 current_mod_code = f"""
388 390 from {new_mod_name} import *
389 391 """
390 392 self.write_file(mod_fn, textwrap.dedent(current_mod_code))
391 393 self.shell.run_code("assert Ext.A.value == 'ext'")
392 394 self.shell.run_code("assert ext_func() == 'ext'")
393 395 self.shell.run_code("t = ExtTest(); assert t.meth() == 'ext'")
394 396 self.shell.run_code("assert ext_int == 2")
395 397
398 def test_verbose_names(self):
399 # Asserts correspondense between original mode names and their verbose equivalents.
400 @dataclass
401 class AutoreloadSettings:
402 check_all: bool
403 enabled: bool
404 autoload_obj: bool
405
406 def gather_settings(mode):
407 self.shell.magic_autoreload(mode)
408 module_reloader = self.shell.auto_magics._reloader
409 return AutoreloadSettings(
410 module_reloader.check_all,
411 module_reloader.enabled,
412 module_reloader.autoload_obj,
413 )
414
415 assert gather_settings("0") == gather_settings("off")
416 assert gather_settings("0") == gather_settings("OFF") # Case insensitive
417 assert gather_settings("1") == gather_settings("explicit")
418 assert gather_settings("2") == gather_settings("all")
419 assert gather_settings("3") == gather_settings("complete")
420
421 # And an invalid mode name raises an exception.
422 with self.assertRaises(ValueError):
423 self.shell.magic_autoreload("4")
424
425 def test_aimport_parsing(self):
426 # Modules can be included or excluded all in one line.
427 module_reloader = self.shell.auto_magics._reloader
428 self.shell.magic_aimport("os") # import and mark `os` for auto-reload.
429 assert module_reloader.modules["os"] is True
430 assert "os" not in module_reloader.skip_modules.keys()
431
432 self.shell.magic_aimport("-math") # forbid autoreloading of `math`
433 assert module_reloader.skip_modules["math"] is True
434 assert "math" not in module_reloader.modules.keys()
435
436 self.shell.magic_aimport(
437 "-os, math"
438 ) # Can do this all in one line; wasn't possible before.
439 assert module_reloader.modules["math"] is True
440 assert "math" not in module_reloader.skip_modules.keys()
441 assert module_reloader.skip_modules["os"] is True
442 assert "os" not in module_reloader.modules.keys()
443
444 def test_autoreload_output(self):
445 self.shell.magic_autoreload("complete")
446 mod_code = """
447 def func1(): pass
448 """
449 mod_name, mod_fn = self.new_module(mod_code)
450 self.shell.run_code(f"import {mod_name}")
451 with tt.AssertPrints("", channel="stdout"): # no output; this is default
452 self.shell.run_code("pass")
453
454 self.shell.magic_autoreload("complete --print")
455 self.write_file(mod_fn, mod_code) # "modify" the module
456 with tt.AssertPrints(
457 f"Reloading '{mod_name}'.", channel="stdout"
458 ): # see something printed out
459 self.shell.run_code("pass")
460
461 self.shell.magic_autoreload("complete -p")
462 self.write_file(mod_fn, mod_code) # "modify" the module
463 with tt.AssertPrints(
464 f"Reloading '{mod_name}'.", channel="stdout"
465 ): # see something printed out
466 self.shell.run_code("pass")
467
468 self.shell.magic_autoreload("complete --print --log")
469 self.write_file(mod_fn, mod_code) # "modify" the module
470 with tt.AssertPrints(
471 f"Reloading '{mod_name}'.", channel="stdout"
472 ): # see something printed out
473 self.shell.run_code("pass")
474
475 self.shell.magic_autoreload("complete --print --log")
476 self.write_file(mod_fn, mod_code) # "modify" the module
477 with self.assertLogs(logger="autoreload") as lo: # see something printed out
478 self.shell.run_code("pass")
479 assert lo.output == [f"INFO:autoreload:Reloading '{mod_name}'."]
480
481 self.shell.magic_autoreload("complete -l")
482 self.write_file(mod_fn, mod_code) # "modify" the module
483 with self.assertLogs(logger="autoreload") as lo: # see something printed out
484 self.shell.run_code("pass")
485 assert lo.output == [f"INFO:autoreload:Reloading '{mod_name}'."]
486
396 487 def _check_smoketest(self, use_aimport=True):
397 488 """
398 489 Functional test for the automatic reloader using either
399 490 '%autoreload 1' or '%autoreload 2'
400 491 """
401 492
402 493 mod_name, mod_fn = self.new_module(
403 494 """
404 495 x = 9
405 496
406 497 z = 123 # this item will be deleted
407 498
408 499 def foo(y):
409 500 return y + 3
410 501
411 502 class Baz(object):
412 503 def __init__(self, x):
413 504 self.x = x
414 505 def bar(self, y):
415 506 return self.x + y
416 507 @property
417 508 def quux(self):
418 509 return 42
419 510 def zzz(self):
420 511 '''This method will be deleted below'''
421 512 return 99
422 513
423 514 class Bar: # old-style class: weakref doesn't work for it on Python < 2.7
424 515 def foo(self):
425 516 return 1
426 517 """
427 518 )
428 519
429 520 #
430 521 # Import module, and mark for reloading
431 522 #
432 523 if use_aimport:
433 524 self.shell.magic_autoreload("1")
434 525 self.shell.magic_aimport(mod_name)
435 526 stream = StringIO()
436 527 self.shell.magic_aimport("", stream=stream)
437 528 self.assertIn(("Modules to reload:\n%s" % mod_name), stream.getvalue())
438 529
439 530 with self.assertRaises(ImportError):
440 531 self.shell.magic_aimport("tmpmod_as318989e89ds")
441 532 else:
442 533 self.shell.magic_autoreload("2")
443 534 self.shell.run_code("import %s" % mod_name)
444 535 stream = StringIO()
445 536 self.shell.magic_aimport("", stream=stream)
446 537 self.assertTrue(
447 538 "Modules to reload:\nall-except-skipped" in stream.getvalue()
448 539 )
449 540 self.assertIn(mod_name, self.shell.ns)
450 541
451 542 mod = sys.modules[mod_name]
452 543
453 544 #
454 545 # Test module contents
455 546 #
456 547 old_foo = mod.foo
457 548 old_obj = mod.Baz(9)
458 549 old_obj2 = mod.Bar()
459 550
460 551 def check_module_contents():
461 552 self.assertEqual(mod.x, 9)
462 553 self.assertEqual(mod.z, 123)
463 554
464 555 self.assertEqual(old_foo(0), 3)
465 556 self.assertEqual(mod.foo(0), 3)
466 557
467 558 obj = mod.Baz(9)
468 559 self.assertEqual(old_obj.bar(1), 10)
469 560 self.assertEqual(obj.bar(1), 10)
470 561 self.assertEqual(obj.quux, 42)
471 562 self.assertEqual(obj.zzz(), 99)
472 563
473 564 obj2 = mod.Bar()
474 565 self.assertEqual(old_obj2.foo(), 1)
475 566 self.assertEqual(obj2.foo(), 1)
476 567
477 568 check_module_contents()
478 569
479 570 #
480 571 # Simulate a failed reload: no reload should occur and exactly
481 572 # one error message should be printed
482 573 #
483 574 self.write_file(
484 575 mod_fn,
485 576 """
486 577 a syntax error
487 578 """,
488 579 )
489 580
490 581 with tt.AssertPrints(
491 582 ("[autoreload of %s failed:" % mod_name), channel="stderr"
492 583 ):
493 584 self.shell.run_code("pass") # trigger reload
494 585 with tt.AssertNotPrints(
495 586 ("[autoreload of %s failed:" % mod_name), channel="stderr"
496 587 ):
497 588 self.shell.run_code("pass") # trigger another reload
498 589 check_module_contents()
499 590
500 591 #
501 592 # Rewrite module (this time reload should succeed)
502 593 #
503 594 self.write_file(
504 595 mod_fn,
505 596 """
506 597 x = 10
507 598
508 599 def foo(y):
509 600 return y + 4
510 601
511 602 class Baz(object):
512 603 def __init__(self, x):
513 604 self.x = x
514 605 def bar(self, y):
515 606 return self.x + y + 1
516 607 @property
517 608 def quux(self):
518 609 return 43
519 610
520 611 class Bar: # old-style class
521 612 def foo(self):
522 613 return 2
523 614 """,
524 615 )
525 616
526 617 def check_module_contents():
527 618 self.assertEqual(mod.x, 10)
528 619 self.assertFalse(hasattr(mod, "z"))
529 620
530 621 self.assertEqual(old_foo(0), 4) # superreload magic!
531 622 self.assertEqual(mod.foo(0), 4)
532 623
533 624 obj = mod.Baz(9)
534 625 self.assertEqual(old_obj.bar(1), 11) # superreload magic!
535 626 self.assertEqual(obj.bar(1), 11)
536 627
537 628 self.assertEqual(old_obj.quux, 43)
538 629 self.assertEqual(obj.quux, 43)
539 630
540 631 self.assertFalse(hasattr(old_obj, "zzz"))
541 632 self.assertFalse(hasattr(obj, "zzz"))
542 633
543 634 obj2 = mod.Bar()
544 635 self.assertEqual(old_obj2.foo(), 2)
545 636 self.assertEqual(obj2.foo(), 2)
546 637
547 638 self.shell.run_code("pass") # trigger reload
548 639 check_module_contents()
549 640
550 641 #
551 642 # Another failure case: deleted file (shouldn't reload)
552 643 #
553 644 os.unlink(mod_fn)
554 645
555 646 self.shell.run_code("pass") # trigger reload
556 647 check_module_contents()
557 648
558 649 #
559 650 # Disable autoreload and rewrite module: no reload should occur
560 651 #
561 652 if use_aimport:
562 653 self.shell.magic_aimport("-" + mod_name)
563 654 stream = StringIO()
564 655 self.shell.magic_aimport("", stream=stream)
565 656 self.assertTrue(("Modules to skip:\n%s" % mod_name) in stream.getvalue())
566 657
567 658 # This should succeed, although no such module exists
568 659 self.shell.magic_aimport("-tmpmod_as318989e89ds")
569 660 else:
570 661 self.shell.magic_autoreload("0")
571 662
572 663 self.write_file(
573 664 mod_fn,
574 665 """
575 666 x = -99
576 667 """,
577 668 )
578 669
579 670 self.shell.run_code("pass") # trigger reload
580 671 self.shell.run_code("pass")
581 672 check_module_contents()
582 673
583 674 #
584 675 # Re-enable autoreload: reload should now occur
585 676 #
586 677 if use_aimport:
587 678 self.shell.magic_aimport(mod_name)
588 679 else:
589 680 self.shell.magic_autoreload("")
590 681
591 682 self.shell.run_code("pass") # trigger reload
592 683 self.assertEqual(mod.x, -99)
593 684
594 685 def test_smoketest_aimport(self):
595 686 self._check_smoketest(use_aimport=True)
596 687
597 688 def test_smoketest_autoreload(self):
598 689 self._check_smoketest(use_aimport=False)
General Comments 0
You need to be logged in to leave comments. Login now