##// END OF EJS Templates
Test for new parsing of `%aimport`
Emilio Graff -
Show More
@@ -1,624 +1,641 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 25 from dataclasses import dataclass
26 26
27 27 import IPython.testing.tools as tt
28 28
29 29 from unittest import TestCase
30 30
31 31 from IPython.extensions.autoreload import AutoreloadMagics
32 32 from IPython.core.events import EventManager, pre_run_cell
33 33 from IPython.testing.decorators import skipif_not_numpy
34 34
35 35 if platform.python_implementation() == "PyPy":
36 36 pytest.skip(
37 37 "Current autoreload implementation is extremely slow on PyPy",
38 38 allow_module_level=True,
39 39 )
40 40
41 41 # -----------------------------------------------------------------------------
42 42 # Test fixture
43 43 # -----------------------------------------------------------------------------
44 44
45 45 noop = lambda *a, **kw: None
46 46
47 47
48 48 class FakeShell:
49 49 def __init__(self):
50 50 self.ns = {}
51 51 self.user_ns = self.ns
52 52 self.user_ns_hidden = {}
53 53 self.events = EventManager(self, {"pre_run_cell", pre_run_cell})
54 54 self.auto_magics = AutoreloadMagics(shell=self)
55 55 self.events.register("pre_run_cell", self.auto_magics.pre_run_cell)
56 56
57 57 register_magics = set_hook = noop
58 58
59 59 def run_code(self, code):
60 60 self.events.trigger("pre_run_cell")
61 61 exec(code, self.user_ns)
62 62 self.auto_magics.post_execute_hook()
63 63
64 64 def push(self, items):
65 65 self.ns.update(items)
66 66
67 67 def magic_autoreload(self, parameter):
68 68 self.auto_magics.autoreload(parameter)
69 69
70 70 def magic_aimport(self, parameter, stream=None):
71 71 self.auto_magics.aimport(parameter, stream=stream)
72 72 self.auto_magics.post_execute_hook()
73 73
74 74
75 75 class Fixture(TestCase):
76 76 """Fixture for creating test module files"""
77 77
78 78 test_dir = None
79 79 old_sys_path = None
80 80 filename_chars = "abcdefghijklmopqrstuvwxyz0123456789"
81 81
82 82 def setUp(self):
83 83 self.test_dir = tempfile.mkdtemp()
84 84 self.old_sys_path = list(sys.path)
85 85 sys.path.insert(0, self.test_dir)
86 86 self.shell = FakeShell()
87 87
88 88 def tearDown(self):
89 89 shutil.rmtree(self.test_dir)
90 90 sys.path = self.old_sys_path
91 91
92 92 self.test_dir = None
93 93 self.old_sys_path = None
94 94 self.shell = None
95 95
96 96 def get_module(self):
97 97 module_name = "tmpmod_" + "".join(random.sample(self.filename_chars, 20))
98 98 if module_name in sys.modules:
99 99 del sys.modules[module_name]
100 100 file_name = os.path.join(self.test_dir, module_name + ".py")
101 101 return module_name, file_name
102 102
103 103 def write_file(self, filename, content):
104 104 """
105 105 Write a file, and force a timestamp difference of at least one second
106 106
107 107 Notes
108 108 -----
109 109 Python's .pyc files record the timestamp of their compilation
110 110 with a time resolution of one second.
111 111
112 112 Therefore, we need to force a timestamp difference between .py
113 113 and .pyc, without having the .py file be timestamped in the
114 114 future, and without changing the timestamp of the .pyc file
115 115 (because that is stored in the file). The only reliable way
116 116 to achieve this seems to be to sleep.
117 117 """
118 118 content = textwrap.dedent(content)
119 119 # Sleep one second + eps
120 120 time.sleep(1.05)
121 121
122 122 # Write
123 123 with open(filename, "w", encoding="utf-8") as f:
124 124 f.write(content)
125 125
126 126 def new_module(self, code):
127 127 code = textwrap.dedent(code)
128 128 mod_name, mod_fn = self.get_module()
129 129 with open(mod_fn, "w", encoding="utf-8") as f:
130 130 f.write(code)
131 131 return mod_name, mod_fn
132 132
133 133
134 134 # -----------------------------------------------------------------------------
135 135 # Test automatic reloading
136 136 # -----------------------------------------------------------------------------
137 137
138 138
139 139 def pickle_get_current_class(obj):
140 140 """
141 141 Original issue comes from pickle; hence the name.
142 142 """
143 143 name = obj.__class__.__name__
144 144 module_name = getattr(obj, "__module__", None)
145 145 obj2 = sys.modules[module_name]
146 146 for subpath in name.split("."):
147 147 obj2 = getattr(obj2, subpath)
148 148 return obj2
149 149
150 150
151 151 class TestAutoreload(Fixture):
152 152 def test_reload_enums(self):
153 153 mod_name, mod_fn = self.new_module(
154 154 textwrap.dedent(
155 155 """
156 156 from enum import Enum
157 157 class MyEnum(Enum):
158 158 A = 'A'
159 159 B = 'B'
160 160 """
161 161 )
162 162 )
163 163 self.shell.magic_autoreload("2")
164 164 self.shell.magic_aimport(mod_name)
165 165 self.write_file(
166 166 mod_fn,
167 167 textwrap.dedent(
168 168 """
169 169 from enum import Enum
170 170 class MyEnum(Enum):
171 171 A = 'A'
172 172 B = 'B'
173 173 C = 'C'
174 174 """
175 175 ),
176 176 )
177 177 with tt.AssertNotPrints(
178 178 ("[autoreload of %s failed:" % mod_name), channel="stderr"
179 179 ):
180 180 self.shell.run_code("pass") # trigger another reload
181 181
182 182 def test_reload_class_type(self):
183 183 self.shell.magic_autoreload("2")
184 184 mod_name, mod_fn = self.new_module(
185 185 """
186 186 class Test():
187 187 def meth(self):
188 188 return "old"
189 189 """
190 190 )
191 191 assert "test" not in self.shell.ns
192 192 assert "result" not in self.shell.ns
193 193
194 194 self.shell.run_code("from %s import Test" % mod_name)
195 195 self.shell.run_code("test = Test()")
196 196
197 197 self.write_file(
198 198 mod_fn,
199 199 """
200 200 class Test():
201 201 def meth(self):
202 202 return "new"
203 203 """,
204 204 )
205 205
206 206 test_object = self.shell.ns["test"]
207 207
208 208 # important to trigger autoreload logic !
209 209 self.shell.run_code("pass")
210 210
211 211 test_class = pickle_get_current_class(test_object)
212 212 assert isinstance(test_object, test_class)
213 213
214 214 # extra check.
215 215 self.shell.run_code("import pickle")
216 216 self.shell.run_code("p = pickle.dumps(test)")
217 217
218 218 def test_reload_class_attributes(self):
219 219 self.shell.magic_autoreload("2")
220 220 mod_name, mod_fn = self.new_module(
221 221 textwrap.dedent(
222 222 """
223 223 class MyClass:
224 224
225 225 def __init__(self, a=10):
226 226 self.a = a
227 227 self.b = 22
228 228 # self.toto = 33
229 229
230 230 def square(self):
231 231 print('compute square')
232 232 return self.a*self.a
233 233 """
234 234 )
235 235 )
236 236 self.shell.run_code("from %s import MyClass" % mod_name)
237 237 self.shell.run_code("first = MyClass(5)")
238 238 self.shell.run_code("first.square()")
239 239 with self.assertRaises(AttributeError):
240 240 self.shell.run_code("first.cube()")
241 241 with self.assertRaises(AttributeError):
242 242 self.shell.run_code("first.power(5)")
243 243 self.shell.run_code("first.b")
244 244 with self.assertRaises(AttributeError):
245 245 self.shell.run_code("first.toto")
246 246
247 247 # remove square, add power
248 248
249 249 self.write_file(
250 250 mod_fn,
251 251 textwrap.dedent(
252 252 """
253 253 class MyClass:
254 254
255 255 def __init__(self, a=10):
256 256 self.a = a
257 257 self.b = 11
258 258
259 259 def power(self, p):
260 260 print('compute power '+str(p))
261 261 return self.a**p
262 262 """
263 263 ),
264 264 )
265 265
266 266 self.shell.run_code("second = MyClass(5)")
267 267
268 268 for object_name in {"first", "second"}:
269 269 self.shell.run_code(f"{object_name}.power(5)")
270 270 with self.assertRaises(AttributeError):
271 271 self.shell.run_code(f"{object_name}.cube()")
272 272 with self.assertRaises(AttributeError):
273 273 self.shell.run_code(f"{object_name}.square()")
274 274 self.shell.run_code(f"{object_name}.b")
275 275 self.shell.run_code(f"{object_name}.a")
276 276 with self.assertRaises(AttributeError):
277 277 self.shell.run_code(f"{object_name}.toto")
278 278
279 279 @skipif_not_numpy
280 280 def test_comparing_numpy_structures(self):
281 281 self.shell.magic_autoreload("2")
282 282 mod_name, mod_fn = self.new_module(
283 283 textwrap.dedent(
284 284 """
285 285 import numpy as np
286 286 class MyClass:
287 287 a = (np.array((.1, .2)),
288 288 np.array((.2, .3)))
289 289 """
290 290 )
291 291 )
292 292 self.shell.run_code("from %s import MyClass" % mod_name)
293 293 self.shell.run_code("first = MyClass()")
294 294
295 295 # change property `a`
296 296 self.write_file(
297 297 mod_fn,
298 298 textwrap.dedent(
299 299 """
300 300 import numpy as np
301 301 class MyClass:
302 302 a = (np.array((.3, .4)),
303 303 np.array((.5, .6)))
304 304 """
305 305 ),
306 306 )
307 307
308 308 with tt.AssertNotPrints(
309 309 ("[autoreload of %s failed:" % mod_name), channel="stderr"
310 310 ):
311 311 self.shell.run_code("pass") # trigger another reload
312 312
313 313 def test_autoload_newly_added_objects(self):
314 314 # All of these fail with %autoreload 2
315 315 self.shell.magic_autoreload("3")
316 316 mod_code = """
317 317 def func1(): pass
318 318 """
319 319 mod_name, mod_fn = self.new_module(textwrap.dedent(mod_code))
320 320 self.shell.run_code(f"from {mod_name} import *")
321 321 self.shell.run_code("func1()")
322 322 with self.assertRaises(NameError):
323 323 self.shell.run_code("func2()")
324 324 with self.assertRaises(NameError):
325 325 self.shell.run_code("t = Test()")
326 326 with self.assertRaises(NameError):
327 327 self.shell.run_code("number")
328 328
329 329 # ----------- TEST NEW OBJ LOADED --------------------------
330 330
331 331 new_code = """
332 332 def func1(): pass
333 333 def func2(): pass
334 334 class Test: pass
335 335 number = 0
336 336 from enum import Enum
337 337 class TestEnum(Enum):
338 338 A = 'a'
339 339 """
340 340 self.write_file(mod_fn, textwrap.dedent(new_code))
341 341
342 342 # test function now exists in shell's namespace namespace
343 343 self.shell.run_code("func2()")
344 344 # test function now exists in module's dict
345 345 self.shell.run_code(f"import sys; sys.modules['{mod_name}'].func2()")
346 346 # test class now exists
347 347 self.shell.run_code("t = Test()")
348 348 # test global built-in var now exists
349 349 self.shell.run_code("number")
350 350 # test the enumerations gets loaded successfully
351 351 self.shell.run_code("TestEnum.A")
352 352
353 353 # ----------- TEST NEW OBJ CAN BE CHANGED --------------------
354 354
355 355 new_code = """
356 356 def func1(): return 'changed'
357 357 def func2(): return 'changed'
358 358 class Test:
359 359 def new_func(self):
360 360 return 'changed'
361 361 number = 1
362 362 from enum import Enum
363 363 class TestEnum(Enum):
364 364 A = 'a'
365 365 B = 'added'
366 366 """
367 367 self.write_file(mod_fn, textwrap.dedent(new_code))
368 368 self.shell.run_code("assert func1() == 'changed'")
369 369 self.shell.run_code("assert func2() == 'changed'")
370 370 self.shell.run_code("t = Test(); assert t.new_func() == 'changed'")
371 371 self.shell.run_code("assert number == 1")
372 372 self.shell.run_code("assert TestEnum.B.value == 'added'")
373 373
374 374 # ----------- TEST IMPORT FROM MODULE --------------------------
375 375
376 376 new_mod_code = """
377 377 from enum import Enum
378 378 class Ext(Enum):
379 379 A = 'ext'
380 380 def ext_func():
381 381 return 'ext'
382 382 class ExtTest:
383 383 def meth(self):
384 384 return 'ext'
385 385 ext_int = 2
386 386 """
387 387 new_mod_name, new_mod_fn = self.new_module(textwrap.dedent(new_mod_code))
388 388 current_mod_code = f"""
389 389 from {new_mod_name} import *
390 390 """
391 391 self.write_file(mod_fn, textwrap.dedent(current_mod_code))
392 392 self.shell.run_code("assert Ext.A.value == 'ext'")
393 393 self.shell.run_code("assert ext_func() == 'ext'")
394 394 self.shell.run_code("t = ExtTest(); assert t.meth() == 'ext'")
395 395 self.shell.run_code("assert ext_int == 2")
396 396
397 397 def test_verbose_names(self):
398 398 # Asserts correspondense between original mode names and their verbose equivalents.
399 399 @dataclass
400 400 class AutoreloadSettings:
401 401 check_all: bool
402 402 enabled: bool
403 403 autoload_obj: bool
404 404
405 405 def gather_settings(mode):
406 406 self.shell.magic_autoreload(mode)
407 407 module_reloader = self.shell.auto_magics._reloader
408 408 return AutoreloadSettings(module_reloader.check_all,
409 409 module_reloader.enabled,
410 410 module_reloader.autoload_obj
411 411 )
412 412 assert gather_settings("0") == gather_settings("off")
413 413 assert gather_settings("0") == gather_settings("OFF") # Case insensitive
414 414 assert gather_settings("1") == gather_settings("explicit")
415 415 assert gather_settings("2") == gather_settings("all")
416 416 assert gather_settings("3") == gather_settings("complete")
417 417
418 418 # And an invalid mode name raises an exception.
419 419 with self.assertRaises(ValueError):
420 420 self.shell.magic_autoreload('4')
421 421
422 def test_aimport_parsing(self):
423 # Modules can be included or excluded all in one line.
424 module_reloader = self.shell.auto_magics._reloader
425 self.shell.magic_aimport('os') # import and mark `os` for auto-reload.
426 assert module_reloader.modules['os'] is True
427 assert 'os' not in module_reloader.skip_modules.keys()
428
429 self.shell.magic_aimport('-math') # forbid autoreloading of `math`
430 assert module_reloader.skip_modules['math'] is True
431 assert 'math' not in module_reloader.modules.keys()
432
433 self.shell.magic_aimport('-os, math') # Can do this all in one line; wasn't possible before.
434 assert module_reloader.modules['math'] is True
435 assert 'math' not in module_reloader.skip_modules.keys()
436 assert module_reloader.skip_modules['os'] is True
437 assert 'os' not in module_reloader.modules.keys()
438
422 439 def _check_smoketest(self, use_aimport=True):
423 440 """
424 441 Functional test for the automatic reloader using either
425 442 '%autoreload 1' or '%autoreload 2'
426 443 """
427 444
428 445 mod_name, mod_fn = self.new_module(
429 446 """
430 447 x = 9
431 448
432 449 z = 123 # this item will be deleted
433 450
434 451 def foo(y):
435 452 return y + 3
436 453
437 454 class Baz(object):
438 455 def __init__(self, x):
439 456 self.x = x
440 457 def bar(self, y):
441 458 return self.x + y
442 459 @property
443 460 def quux(self):
444 461 return 42
445 462 def zzz(self):
446 463 '''This method will be deleted below'''
447 464 return 99
448 465
449 466 class Bar: # old-style class: weakref doesn't work for it on Python < 2.7
450 467 def foo(self):
451 468 return 1
452 469 """
453 470 )
454 471
455 472 #
456 473 # Import module, and mark for reloading
457 474 #
458 475 if use_aimport:
459 476 self.shell.magic_autoreload("1")
460 477 self.shell.magic_aimport(mod_name)
461 478 stream = StringIO()
462 479 self.shell.magic_aimport("", stream=stream)
463 480 self.assertIn(("Modules to reload:\n%s" % mod_name), stream.getvalue())
464 481
465 482 with self.assertRaises(ImportError):
466 483 self.shell.magic_aimport("tmpmod_as318989e89ds")
467 484 else:
468 485 self.shell.magic_autoreload("2")
469 486 self.shell.run_code("import %s" % mod_name)
470 487 stream = StringIO()
471 488 self.shell.magic_aimport("", stream=stream)
472 489 self.assertTrue(
473 490 "Modules to reload:\nall-except-skipped" in stream.getvalue()
474 491 )
475 492 self.assertIn(mod_name, self.shell.ns)
476 493
477 494 mod = sys.modules[mod_name]
478 495
479 496 #
480 497 # Test module contents
481 498 #
482 499 old_foo = mod.foo
483 500 old_obj = mod.Baz(9)
484 501 old_obj2 = mod.Bar()
485 502
486 503 def check_module_contents():
487 504 self.assertEqual(mod.x, 9)
488 505 self.assertEqual(mod.z, 123)
489 506
490 507 self.assertEqual(old_foo(0), 3)
491 508 self.assertEqual(mod.foo(0), 3)
492 509
493 510 obj = mod.Baz(9)
494 511 self.assertEqual(old_obj.bar(1), 10)
495 512 self.assertEqual(obj.bar(1), 10)
496 513 self.assertEqual(obj.quux, 42)
497 514 self.assertEqual(obj.zzz(), 99)
498 515
499 516 obj2 = mod.Bar()
500 517 self.assertEqual(old_obj2.foo(), 1)
501 518 self.assertEqual(obj2.foo(), 1)
502 519
503 520 check_module_contents()
504 521
505 522 #
506 523 # Simulate a failed reload: no reload should occur and exactly
507 524 # one error message should be printed
508 525 #
509 526 self.write_file(
510 527 mod_fn,
511 528 """
512 529 a syntax error
513 530 """,
514 531 )
515 532
516 533 with tt.AssertPrints(
517 534 ("[autoreload of %s failed:" % mod_name), channel="stderr"
518 535 ):
519 536 self.shell.run_code("pass") # trigger reload
520 537 with tt.AssertNotPrints(
521 538 ("[autoreload of %s failed:" % mod_name), channel="stderr"
522 539 ):
523 540 self.shell.run_code("pass") # trigger another reload
524 541 check_module_contents()
525 542
526 543 #
527 544 # Rewrite module (this time reload should succeed)
528 545 #
529 546 self.write_file(
530 547 mod_fn,
531 548 """
532 549 x = 10
533 550
534 551 def foo(y):
535 552 return y + 4
536 553
537 554 class Baz(object):
538 555 def __init__(self, x):
539 556 self.x = x
540 557 def bar(self, y):
541 558 return self.x + y + 1
542 559 @property
543 560 def quux(self):
544 561 return 43
545 562
546 563 class Bar: # old-style class
547 564 def foo(self):
548 565 return 2
549 566 """,
550 567 )
551 568
552 569 def check_module_contents():
553 570 self.assertEqual(mod.x, 10)
554 571 self.assertFalse(hasattr(mod, "z"))
555 572
556 573 self.assertEqual(old_foo(0), 4) # superreload magic!
557 574 self.assertEqual(mod.foo(0), 4)
558 575
559 576 obj = mod.Baz(9)
560 577 self.assertEqual(old_obj.bar(1), 11) # superreload magic!
561 578 self.assertEqual(obj.bar(1), 11)
562 579
563 580 self.assertEqual(old_obj.quux, 43)
564 581 self.assertEqual(obj.quux, 43)
565 582
566 583 self.assertFalse(hasattr(old_obj, "zzz"))
567 584 self.assertFalse(hasattr(obj, "zzz"))
568 585
569 586 obj2 = mod.Bar()
570 587 self.assertEqual(old_obj2.foo(), 2)
571 588 self.assertEqual(obj2.foo(), 2)
572 589
573 590 self.shell.run_code("pass") # trigger reload
574 591 check_module_contents()
575 592
576 593 #
577 594 # Another failure case: deleted file (shouldn't reload)
578 595 #
579 596 os.unlink(mod_fn)
580 597
581 598 self.shell.run_code("pass") # trigger reload
582 599 check_module_contents()
583 600
584 601 #
585 602 # Disable autoreload and rewrite module: no reload should occur
586 603 #
587 604 if use_aimport:
588 605 self.shell.magic_aimport("-" + mod_name)
589 606 stream = StringIO()
590 607 self.shell.magic_aimport("", stream=stream)
591 608 self.assertTrue(("Modules to skip:\n%s" % mod_name) in stream.getvalue())
592 609
593 610 # This should succeed, although no such module exists
594 611 self.shell.magic_aimport("-tmpmod_as318989e89ds")
595 612 else:
596 613 self.shell.magic_autoreload("0")
597 614
598 615 self.write_file(
599 616 mod_fn,
600 617 """
601 618 x = -99
602 619 """,
603 620 )
604 621
605 622 self.shell.run_code("pass") # trigger reload
606 623 self.shell.run_code("pass")
607 624 check_module_contents()
608 625
609 626 #
610 627 # Re-enable autoreload: reload should now occur
611 628 #
612 629 if use_aimport:
613 630 self.shell.magic_aimport(mod_name)
614 631 else:
615 632 self.shell.magic_autoreload("")
616 633
617 634 self.shell.run_code("pass") # trigger reload
618 635 self.assertEqual(mod.x, -99)
619 636
620 637 def test_smoketest_aimport(self):
621 638 self._check_smoketest(use_aimport=True)
622 639
623 640 def test_smoketest_autoreload(self):
624 641 self._check_smoketest(use_aimport=False)
General Comments 0
You need to be logged in to leave comments. Login now