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