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