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