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