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