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 | def update_class(old, new): |
|
270 | def update_class(old, new): | |
271 | """Replace stuff in the __dict__ of a class, and upgrade |
|
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 | for key in list(old.__dict__.keys()): |
|
273 | for key in list(old.__dict__.keys()): | |
274 | old_obj = getattr(old, key) |
|
274 | old_obj = getattr(old, key) | |
275 | try: |
|
275 | try: | |
@@ -291,6 +291,13 b' def update_class(old, new):' | |||||
291 | except (AttributeError, TypeError): |
|
291 | except (AttributeError, TypeError): | |
292 | pass # skip non-writable attributes |
|
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 | def update_property(old, new): |
|
302 | def update_property(old, new): | |
296 | """Replace get/set/del functions of a property""" |
|
303 | """Replace get/set/del functions of a property""" |
@@ -151,6 +151,64 b' class TestAutoreload(Fixture):' | |||||
151 | with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'): |
|
151 | with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'): | |
152 | self.shell.run_code("pass") # trigger another reload |
|
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 | def _check_smoketest(self, use_aimport=True): |
|
213 | def _check_smoketest(self, use_aimport=True): | |
156 | """ |
|
214 | """ | |
@@ -340,3 +398,7 b' x = -99' | |||||
340 |
|
398 | |||
341 | def test_smoketest_autoreload(self): |
|
399 | def test_smoketest_autoreload(self): | |
342 | self._check_smoketest(use_aimport=False) |
|
400 | self._check_smoketest(use_aimport=False) | |
|
401 | ||||
|
402 | ||||
|
403 | ||||
|
404 |
@@ -386,6 +386,12 b' def run_iptest():' | |||||
386 | monkeypatch_xunit() |
|
386 | monkeypatch_xunit() | |
387 |
|
387 | |||
388 | arg1 = sys.argv[1] |
|
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 | if arg1 in test_sections: |
|
395 | if arg1 in test_sections: | |
390 | section = test_sections[arg1] |
|
396 | section = test_sections[arg1] | |
391 | sys.argv[1:2] = section.includes |
|
397 | sys.argv[1:2] = section.includes |
General Comments 0
You need to be logged in to leave comments.
Login now