Show More
@@ -0,0 +1,30 b'' | |||
|
1 | magic `%autoreload 2` now captures new methods added to classes. Earlier, only methods existing as of the initial import were being tracked and updated. | |
|
2 | ||
|
3 | This new feature helps dual environement development - Jupyter+IDE - where the code gradually moves from notebook cells to package files, as it gets structured. | |
|
4 | ||
|
5 | **Example**: An instance of the class `MyClass` will be able to access the method `cube()` after it is uncommented and the file `file1.py` saved on disk. | |
|
6 | ||
|
7 | ````python | |
|
8 | # notebook | |
|
9 | ||
|
10 | from mymodule import MyClass | |
|
11 | first = MyClass(5) | |
|
12 | ```` | |
|
13 | ||
|
14 | ````python | |
|
15 | # mymodule/file1.py | |
|
16 | ||
|
17 | class MyClass: | |
|
18 | ||
|
19 | def __init__(self, a=10): | |
|
20 | self.a = a | |
|
21 | ||
|
22 | def square(self): | |
|
23 | print('compute square') | |
|
24 | return self.a*self.a | |
|
25 | ||
|
26 | # def cube(self): | |
|
27 | # print('compute cube') | |
|
28 | # return self.a*self.a*self.a | |
|
29 | ```` | |
|
30 |
@@ -269,7 +269,7 b' def update_function(old, new):' | |||
|
269 | 269 | |
|
270 | 270 | def update_class(old, new): |
|
271 | 271 | """Replace stuff in the __dict__ of a class, and upgrade |
|
272 | method code objects""" | |
|
272 | method code objects, and add new methods, if any""" | |
|
273 | 273 | for key in list(old.__dict__.keys()): |
|
274 | 274 | old_obj = getattr(old, key) |
|
275 | 275 | try: |
@@ -291,6 +291,13 b' def update_class(old, new):' | |||
|
291 | 291 | except (AttributeError, TypeError): |
|
292 | 292 | pass # skip non-writable attributes |
|
293 | 293 | |
|
294 | for key in list(new.__dict__.keys()): | |
|
295 | if key not in list(old.__dict__.keys()): | |
|
296 | try: | |
|
297 | setattr(old, key, getattr(new, key)) | |
|
298 | except (AttributeError, TypeError): | |
|
299 | pass # skip non-writable attributes | |
|
300 | ||
|
294 | 301 | |
|
295 | 302 | def update_property(old, new): |
|
296 | 303 | """Replace get/set/del functions of a property""" |
@@ -151,6 +151,64 b' class TestAutoreload(Fixture):' | |||
|
151 | 151 | with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'): |
|
152 | 152 | self.shell.run_code("pass") # trigger another reload |
|
153 | 153 | |
|
154 | def test_reload_class_attributes(self): | |
|
155 | self.shell.magic_autoreload("2") | |
|
156 | mod_name, mod_fn = self.new_module(textwrap.dedent(""" | |
|
157 | class MyClass: | |
|
158 | ||
|
159 | def __init__(self, a=10): | |
|
160 | self.a = a | |
|
161 | self.b = 22 | |
|
162 | # self.toto = 33 | |
|
163 | ||
|
164 | def square(self): | |
|
165 | print('compute square') | |
|
166 | return self.a*self.a | |
|
167 | """ | |
|
168 | ) | |
|
169 | ) | |
|
170 | self.shell.run_code("from %s import MyClass" % mod_name) | |
|
171 | self.shell.run_code("first = MyClass(5)") | |
|
172 | self.shell.run_code("first.square()") | |
|
173 | with nt.assert_raises(AttributeError): | |
|
174 | self.shell.run_code("first.cube()") | |
|
175 | with nt.assert_raises(AttributeError): | |
|
176 | self.shell.run_code("first.power(5)") | |
|
177 | self.shell.run_code("first.b") | |
|
178 | with nt.assert_raises(AttributeError): | |
|
179 | self.shell.run_code("first.toto") | |
|
180 | ||
|
181 | # remove square, add power | |
|
182 | ||
|
183 | self.write_file( | |
|
184 | mod_fn, | |
|
185 | textwrap.dedent( | |
|
186 | """ | |
|
187 | class MyClass: | |
|
188 | ||
|
189 | def __init__(self, a=10): | |
|
190 | self.a = a | |
|
191 | self.b = 11 | |
|
192 | ||
|
193 | def power(self, p): | |
|
194 | print('compute power '+str(p)) | |
|
195 | return self.a**p | |
|
196 | """ | |
|
197 | ), | |
|
198 | ) | |
|
199 | ||
|
200 | self.shell.run_code("second = MyClass(5)") | |
|
201 | ||
|
202 | for object_name in {'first', 'second'}: | |
|
203 | self.shell.run_code("{object_name}.power(5)".format(object_name=object_name)) | |
|
204 | with nt.assert_raises(AttributeError): | |
|
205 | self.shell.run_code("{object_name}.cube()".format(object_name=object_name)) | |
|
206 | with nt.assert_raises(AttributeError): | |
|
207 | self.shell.run_code("{object_name}.square()".format(object_name=object_name)) | |
|
208 | self.shell.run_code("{object_name}.b".format(object_name=object_name)) | |
|
209 | self.shell.run_code("{object_name}.a".format(object_name=object_name)) | |
|
210 | with nt.assert_raises(AttributeError): | |
|
211 | self.shell.run_code("{object_name}.toto".format(object_name=object_name)) | |
|
154 | 212 | |
|
155 | 213 | def _check_smoketest(self, use_aimport=True): |
|
156 | 214 | """ |
@@ -340,3 +398,7 b' x = -99' | |||
|
340 | 398 | |
|
341 | 399 | def test_smoketest_autoreload(self): |
|
342 | 400 | self._check_smoketest(use_aimport=False) |
|
401 | ||
|
402 | ||
|
403 | ||
|
404 |
@@ -386,6 +386,12 b' def run_iptest():' | |||
|
386 | 386 | monkeypatch_xunit() |
|
387 | 387 | |
|
388 | 388 | arg1 = sys.argv[1] |
|
389 | if arg1.startswith('IPython/'): | |
|
390 | if arg1.endswith('.py'): | |
|
391 | arg1 = arg1[:-3] | |
|
392 | sys.argv[1] = arg1.replace('/', '.') | |
|
393 | ||
|
394 | arg1 = sys.argv[1] | |
|
389 | 395 | if arg1 in test_sections: |
|
390 | 396 | section = test_sections[arg1] |
|
391 | 397 | sys.argv[1:2] = section.includes |
General Comments 0
You need to be logged in to leave comments.
Login now