Show More
@@ -0,0 +1,342 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | """Test Parallel magics | |
|
3 | ||
|
4 | Authors: | |
|
5 | ||
|
6 | * Min RK | |
|
7 | """ | |
|
8 | #------------------------------------------------------------------------------- | |
|
9 | # Copyright (C) 2011 The IPython Development Team | |
|
10 | # | |
|
11 | # Distributed under the terms of the BSD License. The full license is in | |
|
12 | # the file COPYING, distributed as part of this software. | |
|
13 | #------------------------------------------------------------------------------- | |
|
14 | ||
|
15 | #------------------------------------------------------------------------------- | |
|
16 | # Imports | |
|
17 | #------------------------------------------------------------------------------- | |
|
18 | ||
|
19 | import sys | |
|
20 | import time | |
|
21 | ||
|
22 | import zmq | |
|
23 | from nose import SkipTest | |
|
24 | ||
|
25 | from IPython.testing import decorators as dec | |
|
26 | from IPython.testing.ipunittest import ParametricTestCase | |
|
27 | ||
|
28 | from IPython import parallel as pmod | |
|
29 | from IPython.parallel import error | |
|
30 | from IPython.parallel import AsyncResult | |
|
31 | from IPython.parallel.util import interactive | |
|
32 | ||
|
33 | from IPython.parallel.tests import add_engines | |
|
34 | ||
|
35 | from .clienttest import ClusterTestCase, capture_output, generate_output | |
|
36 | ||
|
37 | def setup(): | |
|
38 | add_engines(3, total=True) | |
|
39 | ||
|
40 | class TestParallelMagics(ClusterTestCase, ParametricTestCase): | |
|
41 | ||
|
42 | def test_px_blocking(self): | |
|
43 | ip = get_ipython() | |
|
44 | v = self.client[-1:] | |
|
45 | v.activate() | |
|
46 | v.block=True | |
|
47 | ||
|
48 | ip.magic('px a=5') | |
|
49 | self.assertEquals(v['a'], [5]) | |
|
50 | ip.magic('px a=10') | |
|
51 | self.assertEquals(v['a'], [10]) | |
|
52 | # just 'print a' works ~99% of the time, but this ensures that | |
|
53 | # the stdout message has arrived when the result is finished: | |
|
54 | with capture_output() as io: | |
|
55 | ip.magic( | |
|
56 | 'px import sys,time;print(a);sys.stdout.flush();time.sleep(0.2)' | |
|
57 | ) | |
|
58 | out = io.stdout | |
|
59 | self.assertTrue('[stdout:' in out, out) | |
|
60 | self.assertTrue(out.rstrip().endswith('10')) | |
|
61 | self.assertRaisesRemote(ZeroDivisionError, ip.magic, 'px 1/0') | |
|
62 | ||
|
63 | def test_cellpx_block_args(self): | |
|
64 | """%%px --[no]block flags work""" | |
|
65 | ip = get_ipython() | |
|
66 | v = self.client[-1:] | |
|
67 | v.activate() | |
|
68 | v.block=False | |
|
69 | ||
|
70 | for block in (True, False): | |
|
71 | v.block = block | |
|
72 | ||
|
73 | with capture_output() as io: | |
|
74 | ip.run_cell_magic("px", "", "1") | |
|
75 | if block: | |
|
76 | self.assertTrue(io.stdout.startswith("Parallel"), io.stdout) | |
|
77 | else: | |
|
78 | self.assertTrue(io.stdout.startswith("Async"), io.stdout) | |
|
79 | ||
|
80 | with capture_output() as io: | |
|
81 | ip.run_cell_magic("px", "--block", "1") | |
|
82 | self.assertTrue(io.stdout.startswith("Parallel"), io.stdout) | |
|
83 | ||
|
84 | with capture_output() as io: | |
|
85 | ip.run_cell_magic("px", "--noblock", "1") | |
|
86 | self.assertTrue(io.stdout.startswith("Async"), io.stdout) | |
|
87 | ||
|
88 | def test_cellpx_groupby_engine(self): | |
|
89 | """%%px --group-outputs=engine""" | |
|
90 | ip = get_ipython() | |
|
91 | v = self.client[:] | |
|
92 | v.block = True | |
|
93 | v.activate() | |
|
94 | ||
|
95 | v['generate_output'] = generate_output | |
|
96 | ||
|
97 | with capture_output() as io: | |
|
98 | ip.run_cell_magic('px', '--group-outputs=engine', 'generate_output()') | |
|
99 | ||
|
100 | lines = io.stdout.strip().splitlines()[1:] | |
|
101 | expected = [ | |
|
102 | ('[stdout:', '] stdout'), | |
|
103 | 'stdout2', | |
|
104 | 'IPython.core.display.HTML', | |
|
105 | 'IPython.core.display.Math', | |
|
106 | ('] Out[', 'IPython.core.display.Math') | |
|
107 | ] * len(v) | |
|
108 | ||
|
109 | self.assertEquals(len(lines), len(expected), io.stdout) | |
|
110 | for line,expect in zip(lines, expected): | |
|
111 | if isinstance(expect, str): | |
|
112 | expect = [expect] | |
|
113 | for ex in expect: | |
|
114 | self.assertTrue(ex in line, "Expected %r in %r" % (ex, line)) | |
|
115 | ||
|
116 | expected = [ | |
|
117 | ('[stderr:', '] stderr'), | |
|
118 | 'stderr2', | |
|
119 | ] * len(v) | |
|
120 | ||
|
121 | lines = io.stderr.strip().splitlines() | |
|
122 | self.assertEquals(len(lines), len(expected), io.stderr) | |
|
123 | for line,expect in zip(lines, expected): | |
|
124 | if isinstance(expect, str): | |
|
125 | expect = [expect] | |
|
126 | for ex in expect: | |
|
127 | self.assertTrue(ex in line, "Expected %r in %r" % (ex, line)) | |
|
128 | ||
|
129 | ||
|
130 | def test_cellpx_groupby_order(self): | |
|
131 | """%%px --group-outputs=order""" | |
|
132 | ip = get_ipython() | |
|
133 | v = self.client[:] | |
|
134 | v.block = True | |
|
135 | v.activate() | |
|
136 | ||
|
137 | v['generate_output'] = generate_output | |
|
138 | ||
|
139 | with capture_output() as io: | |
|
140 | ip.run_cell_magic('px', '--group-outputs=order', 'generate_output()') | |
|
141 | ||
|
142 | lines = io.stdout.strip().splitlines()[1:] | |
|
143 | expected = [] | |
|
144 | expected.extend([ | |
|
145 | ('[stdout:', '] stdout'), | |
|
146 | 'stdout2', | |
|
147 | ] * len(v)) | |
|
148 | expected.extend([ | |
|
149 | 'IPython.core.display.HTML', | |
|
150 | ] * len(v)) | |
|
151 | expected.extend([ | |
|
152 | 'IPython.core.display.Math', | |
|
153 | ] * len(v)) | |
|
154 | expected.extend([ | |
|
155 | ('] Out[', 'IPython.core.display.Math') | |
|
156 | ] * len(v)) | |
|
157 | ||
|
158 | self.assertEquals(len(lines), len(expected), io.stdout) | |
|
159 | for line,expect in zip(lines, expected): | |
|
160 | if isinstance(expect, str): | |
|
161 | expect = [expect] | |
|
162 | for ex in expect: | |
|
163 | self.assertTrue(ex in line, "Expected %r in %r" % (ex, line)) | |
|
164 | ||
|
165 | expected = [ | |
|
166 | ('[stderr:', '] stderr'), | |
|
167 | 'stderr2', | |
|
168 | ] * len(v) | |
|
169 | ||
|
170 | lines = io.stderr.strip().splitlines() | |
|
171 | self.assertEquals(len(lines), len(expected), io.stderr) | |
|
172 | for line,expect in zip(lines, expected): | |
|
173 | if isinstance(expect, str): | |
|
174 | expect = [expect] | |
|
175 | for ex in expect: | |
|
176 | self.assertTrue(ex in line, "Expected %r in %r" % (ex, line)) | |
|
177 | ||
|
178 | def test_cellpx_groupby_atype(self): | |
|
179 | """%%px --group-outputs=type""" | |
|
180 | ip = get_ipython() | |
|
181 | v = self.client[:] | |
|
182 | v.block = True | |
|
183 | v.activate() | |
|
184 | ||
|
185 | v['generate_output'] = generate_output | |
|
186 | ||
|
187 | with capture_output() as io: | |
|
188 | ip.run_cell_magic('px', '--group-outputs=type', 'generate_output()') | |
|
189 | ||
|
190 | lines = io.stdout.strip().splitlines()[1:] | |
|
191 | ||
|
192 | expected = [] | |
|
193 | expected.extend([ | |
|
194 | ('[stdout:', '] stdout'), | |
|
195 | 'stdout2', | |
|
196 | ] * len(v)) | |
|
197 | expected.extend([ | |
|
198 | 'IPython.core.display.HTML', | |
|
199 | 'IPython.core.display.Math', | |
|
200 | ] * len(v)) | |
|
201 | expected.extend([ | |
|
202 | ('] Out[', 'IPython.core.display.Math') | |
|
203 | ] * len(v)) | |
|
204 | ||
|
205 | self.assertEquals(len(lines), len(expected), io.stdout) | |
|
206 | for line,expect in zip(lines, expected): | |
|
207 | if isinstance(expect, str): | |
|
208 | expect = [expect] | |
|
209 | for ex in expect: | |
|
210 | self.assertTrue(ex in line, "Expected %r in %r" % (ex, line)) | |
|
211 | ||
|
212 | expected = [ | |
|
213 | ('[stderr:', '] stderr'), | |
|
214 | 'stderr2', | |
|
215 | ] * len(v) | |
|
216 | ||
|
217 | lines = io.stderr.strip().splitlines() | |
|
218 | self.assertEquals(len(lines), len(expected), io.stderr) | |
|
219 | for line,expect in zip(lines, expected): | |
|
220 | if isinstance(expect, str): | |
|
221 | expect = [expect] | |
|
222 | for ex in expect: | |
|
223 | self.assertTrue(ex in line, "Expected %r in %r" % (ex, line)) | |
|
224 | ||
|
225 | ||
|
226 | def test_px_nonblocking(self): | |
|
227 | ip = get_ipython() | |
|
228 | v = self.client[-1:] | |
|
229 | v.activate() | |
|
230 | v.block=False | |
|
231 | ||
|
232 | ip.magic('px a=5') | |
|
233 | self.assertEquals(v['a'], [5]) | |
|
234 | ip.magic('px a=10') | |
|
235 | self.assertEquals(v['a'], [10]) | |
|
236 | with capture_output() as io: | |
|
237 | ar = ip.magic('px print (a)') | |
|
238 | self.assertTrue(isinstance(ar, AsyncResult)) | |
|
239 | self.assertTrue('Async' in io.stdout) | |
|
240 | self.assertFalse('[stdout:' in io.stdout) | |
|
241 | ar = ip.magic('px 1/0') | |
|
242 | self.assertRaisesRemote(ZeroDivisionError, ar.get) | |
|
243 | ||
|
244 | def test_autopx_blocking(self): | |
|
245 | ip = get_ipython() | |
|
246 | v = self.client[-1] | |
|
247 | v.activate() | |
|
248 | v.block=True | |
|
249 | ||
|
250 | with capture_output() as io: | |
|
251 | ip.magic('autopx') | |
|
252 | ip.run_cell('\n'.join(('a=5','b=12345','c=0'))) | |
|
253 | ip.run_cell('b*=2') | |
|
254 | ip.run_cell('print (b)') | |
|
255 | ip.run_cell('b') | |
|
256 | ip.run_cell("b/c") | |
|
257 | ip.magic('autopx') | |
|
258 | ||
|
259 | output = io.stdout.strip() | |
|
260 | ||
|
261 | self.assertTrue(output.startswith('%autopx enabled'), output) | |
|
262 | self.assertTrue(output.endswith('%autopx disabled'), output) | |
|
263 | self.assertTrue('RemoteError: ZeroDivisionError' in output, output) | |
|
264 | self.assertTrue('] Out[' in output, output) | |
|
265 | self.assertTrue(': 24690' in output, output) | |
|
266 | ar = v.get_result(-1) | |
|
267 | self.assertEquals(v['a'], 5) | |
|
268 | self.assertEquals(v['b'], 24690) | |
|
269 | self.assertRaisesRemote(ZeroDivisionError, ar.get) | |
|
270 | ||
|
271 | def test_autopx_nonblocking(self): | |
|
272 | ip = get_ipython() | |
|
273 | v = self.client[-1] | |
|
274 | v.activate() | |
|
275 | v.block=False | |
|
276 | ||
|
277 | with capture_output() as io: | |
|
278 | ip.magic('autopx') | |
|
279 | ip.run_cell('\n'.join(('a=5','b=10','c=0'))) | |
|
280 | ip.run_cell('print (b)') | |
|
281 | ip.run_cell('import time; time.sleep(0.1)') | |
|
282 | ip.run_cell("b/c") | |
|
283 | ip.run_cell('b*=2') | |
|
284 | ip.magic('autopx') | |
|
285 | ||
|
286 | output = io.stdout.strip() | |
|
287 | ||
|
288 | self.assertTrue(output.startswith('%autopx enabled')) | |
|
289 | self.assertTrue(output.endswith('%autopx disabled')) | |
|
290 | self.assertFalse('ZeroDivisionError' in output) | |
|
291 | ar = v.get_result(-2) | |
|
292 | self.assertRaisesRemote(ZeroDivisionError, ar.get) | |
|
293 | # prevent TaskAborted on pulls, due to ZeroDivisionError | |
|
294 | time.sleep(0.5) | |
|
295 | self.assertEquals(v['a'], 5) | |
|
296 | # b*=2 will not fire, due to abort | |
|
297 | self.assertEquals(v['b'], 10) | |
|
298 | ||
|
299 | def test_result(self): | |
|
300 | ip = get_ipython() | |
|
301 | v = self.client[-1] | |
|
302 | v.activate() | |
|
303 | data = dict(a=111,b=222) | |
|
304 | v.push(data, block=True) | |
|
305 | ||
|
306 | ip.magic('px a') | |
|
307 | ip.magic('px b') | |
|
308 | for idx, name in [ | |
|
309 | ('', 'b'), | |
|
310 | ('-1', 'b'), | |
|
311 | ('2', 'b'), | |
|
312 | ('1', 'a'), | |
|
313 | ('-2', 'a'), | |
|
314 | ]: | |
|
315 | with capture_output() as io: | |
|
316 | ip.magic('result ' + idx) | |
|
317 | output = io.stdout.strip() | |
|
318 | msg = "expected %s output to include %s, but got: %s" % \ | |
|
319 | ('%result '+idx, str(data[name]), output) | |
|
320 | self.assertTrue(str(data[name]) in output, msg) | |
|
321 | ||
|
322 | @dec.skipif_not_matplotlib | |
|
323 | def test_px_pylab(self): | |
|
324 | """%pylab works on engines""" | |
|
325 | ip = get_ipython() | |
|
326 | v = self.client[-1] | |
|
327 | v.block = True | |
|
328 | v.activate() | |
|
329 | ||
|
330 | with capture_output() as io: | |
|
331 | ip.magic("px %pylab inline") | |
|
332 | ||
|
333 | self.assertTrue("Welcome to pylab" in io.stdout, io.stdout) | |
|
334 | self.assertTrue("backend_inline" in io.stdout, io.stdout) | |
|
335 | ||
|
336 | with capture_output() as io: | |
|
337 | ip.magic("px plot(rand(100))") | |
|
338 | ||
|
339 | self.assertTrue('] Out[' in io.stdout, io.stdout) | |
|
340 | self.assertTrue('matplotlib.lines' in io.stdout, io.stdout) | |
|
341 | ||
|
342 |
@@ -0,0 +1,228 b'' | |||
|
1 | { | |
|
2 | "metadata": { | |
|
3 | "name": "Parallel Magics" | |
|
4 | }, | |
|
5 | "nbformat": 3, | |
|
6 | "worksheets": [ | |
|
7 | { | |
|
8 | "cells": [ | |
|
9 | { | |
|
10 | "cell_type": "heading", | |
|
11 | "level": 1, | |
|
12 | "source": [ | |
|
13 | "Using Parallel Magics" | |
|
14 | ] | |
|
15 | }, | |
|
16 | { | |
|
17 | "cell_type": "markdown", | |
|
18 | "source": [ | |
|
19 | "IPython has a few magics for working with your engines.", | |
|
20 | "", | |
|
21 | "This assumes you have started an IPython cluster, either with the notebook interface,", | |
|
22 | "or the `ipcluster/controller/engine` commands." | |
|
23 | ] | |
|
24 | }, | |
|
25 | { | |
|
26 | "cell_type": "code", | |
|
27 | "collapsed": false, | |
|
28 | "input": [ | |
|
29 | "from IPython import parallel", | |
|
30 | "rc = parallel.Client()", | |
|
31 | "dv = rc[:]", | |
|
32 | "dv.block = True", | |
|
33 | "dv" | |
|
34 | ], | |
|
35 | "language": "python", | |
|
36 | "outputs": [] | |
|
37 | }, | |
|
38 | { | |
|
39 | "cell_type": "markdown", | |
|
40 | "source": [ | |
|
41 | "The parallel magics come from the `parallelmagics` IPython extension.", | |
|
42 | "The magics are set to work with a particular View object,", | |
|
43 | "so to activate them, you call the `activate()` method on a particular view:" | |
|
44 | ] | |
|
45 | }, | |
|
46 | { | |
|
47 | "cell_type": "code", | |
|
48 | "collapsed": true, | |
|
49 | "input": [ | |
|
50 | "dv.activate()" | |
|
51 | ], | |
|
52 | "language": "python", | |
|
53 | "outputs": [] | |
|
54 | }, | |
|
55 | { | |
|
56 | "cell_type": "markdown", | |
|
57 | "source": [ | |
|
58 | "Now we can execute code remotely with `%px`:" | |
|
59 | ] | |
|
60 | }, | |
|
61 | { | |
|
62 | "cell_type": "code", | |
|
63 | "collapsed": false, | |
|
64 | "input": [ | |
|
65 | "%px a=5" | |
|
66 | ], | |
|
67 | "language": "python", | |
|
68 | "outputs": [] | |
|
69 | }, | |
|
70 | { | |
|
71 | "cell_type": "code", | |
|
72 | "collapsed": false, | |
|
73 | "input": [ | |
|
74 | "%px print a" | |
|
75 | ], | |
|
76 | "language": "python", | |
|
77 | "outputs": [] | |
|
78 | }, | |
|
79 | { | |
|
80 | "cell_type": "code", | |
|
81 | "collapsed": false, | |
|
82 | "input": [ | |
|
83 | "%px a" | |
|
84 | ], | |
|
85 | "language": "python", | |
|
86 | "outputs": [] | |
|
87 | }, | |
|
88 | { | |
|
89 | "cell_type": "markdown", | |
|
90 | "source": [ | |
|
91 | "You don't have to wait for results:" | |
|
92 | ] | |
|
93 | }, | |
|
94 | { | |
|
95 | "cell_type": "code", | |
|
96 | "collapsed": true, | |
|
97 | "input": [ | |
|
98 | "dv.block = False" | |
|
99 | ], | |
|
100 | "language": "python", | |
|
101 | "outputs": [] | |
|
102 | }, | |
|
103 | { | |
|
104 | "cell_type": "code", | |
|
105 | "collapsed": false, | |
|
106 | "input": [ | |
|
107 | "%px import time", | |
|
108 | "%px time.sleep(5)", | |
|
109 | "%px time.time()" | |
|
110 | ], | |
|
111 | "language": "python", | |
|
112 | "outputs": [] | |
|
113 | }, | |
|
114 | { | |
|
115 | "cell_type": "markdown", | |
|
116 | "source": [ | |
|
117 | "But you will notice that this didn't output the result of the last command.", | |
|
118 | "For this, we have `%result`, which displays the output of the latest request:" | |
|
119 | ] | |
|
120 | }, | |
|
121 | { | |
|
122 | "cell_type": "code", | |
|
123 | "collapsed": false, | |
|
124 | "input": [ | |
|
125 | "%result" | |
|
126 | ], | |
|
127 | "language": "python", | |
|
128 | "outputs": [] | |
|
129 | }, | |
|
130 | { | |
|
131 | "cell_type": "markdown", | |
|
132 | "source": [ | |
|
133 | "Remember, an IPython engine is IPython, so you can do magics remotely as well!" | |
|
134 | ] | |
|
135 | }, | |
|
136 | { | |
|
137 | "cell_type": "code", | |
|
138 | "collapsed": false, | |
|
139 | "input": [ | |
|
140 | "dv.block = True", | |
|
141 | "%px %pylab inline" | |
|
142 | ], | |
|
143 | "language": "python", | |
|
144 | "outputs": [] | |
|
145 | }, | |
|
146 | { | |
|
147 | "cell_type": "markdown", | |
|
148 | "source": [ | |
|
149 | "`%%px` can also be used as a cell magic, for submitting whole blocks.", | |
|
150 | "This one acceps `--block` and `--noblock` flags to specify", | |
|
151 | "the blocking behavior, though the default is unchanged.", | |
|
152 | "" | |
|
153 | ] | |
|
154 | }, | |
|
155 | { | |
|
156 | "cell_type": "code", | |
|
157 | "collapsed": true, | |
|
158 | "input": [ | |
|
159 | "dv.scatter('id', dv.targets, flatten=True)", | |
|
160 | "dv['stride'] = len(dv)" | |
|
161 | ], | |
|
162 | "language": "python", | |
|
163 | "outputs": [] | |
|
164 | }, | |
|
165 | { | |
|
166 | "cell_type": "code", | |
|
167 | "collapsed": false, | |
|
168 | "input": [ | |
|
169 | "%%px --noblock", | |
|
170 | "x = linspace(0,pi,1000)", | |
|
171 | "for n in range(id,12, stride):", | |
|
172 | " print n", | |
|
173 | " plt.plot(x,sin(n*x))", | |
|
174 | "plt.title(\"Plot %i\" % id)" | |
|
175 | ], | |
|
176 | "language": "python", | |
|
177 | "outputs": [] | |
|
178 | }, | |
|
179 | { | |
|
180 | "cell_type": "code", | |
|
181 | "collapsed": false, | |
|
182 | "input": [ | |
|
183 | "%result" | |
|
184 | ], | |
|
185 | "language": "python", | |
|
186 | "outputs": [] | |
|
187 | }, | |
|
188 | { | |
|
189 | "cell_type": "markdown", | |
|
190 | "source": [ | |
|
191 | "It also lets you choose some amount of the grouping of the outputs with `--group-outputs`:", | |
|
192 | "", | |
|
193 | "The choices are:", | |
|
194 | "", | |
|
195 | "* `engine` - all of an engine's output is collected together", | |
|
196 | "* `type` - where stdout of each engine is grouped, etc. (the default)", | |
|
197 | "* `order` - same as `type`, but individual displaypub outputs are interleaved.", | |
|
198 | " That is, it will output the first plot from each engine, then the second from each,", | |
|
199 | " etc." | |
|
200 | ] | |
|
201 | }, | |
|
202 | { | |
|
203 | "cell_type": "code", | |
|
204 | "collapsed": false, | |
|
205 | "input": [ | |
|
206 | "%%px --group-outputs=engine", | |
|
207 | "x = linspace(0,pi,1000)", | |
|
208 | "for n in range(id,12, stride):", | |
|
209 | " print n", | |
|
210 | " plt.plot(x,sin(n*x))", | |
|
211 | "plt.title(\"Plot %i\" % id)" | |
|
212 | ], | |
|
213 | "language": "python", | |
|
214 | "outputs": [] | |
|
215 | }, | |
|
216 | { | |
|
217 | "cell_type": "code", | |
|
218 | "collapsed": true, | |
|
219 | "input": [ | |
|
220 | "" | |
|
221 | ], | |
|
222 | "language": "python", | |
|
223 | "outputs": [] | |
|
224 | } | |
|
225 | ] | |
|
226 | } | |
|
227 | ] | |
|
228 | } No newline at end of file |
@@ -542,7 +542,7 b' class Magics(object):' | |||
|
542 | 542 | argv = arg_split(arg_str, posix, strict) |
|
543 | 543 | # Do regular option processing |
|
544 | 544 | try: |
|
545 |
opts,args = getopt(argv,opt_str, |
|
|
545 | opts,args = getopt(argv, opt_str, long_opts) | |
|
546 | 546 | except GetoptError,e: |
|
547 | 547 | raise UsageError('%s ( allowed: "%s" %s)' % (e.msg,opt_str, |
|
548 | 548 | " ".join(long_opts))) |
@@ -69,6 +69,15 b' def test_magic_parse_options():' | |||
|
69 | 69 | expected = path |
|
70 | 70 | nt.assert_equals(opts['f'], expected) |
|
71 | 71 | |
|
72 | def test_magic_parse_long_options(): | |
|
73 | """Magic.parse_options can handle --foo=bar long options""" | |
|
74 | ip = get_ipython() | |
|
75 | m = DummyMagics(ip) | |
|
76 | opts, _ = m.parse_options('--foo --bar=bubble', 'a', 'foo', 'bar=') | |
|
77 | nt.assert_true('foo' in opts) | |
|
78 | nt.assert_true('bar' in opts) | |
|
79 | nt.assert_true(opts['bar'], "bubble") | |
|
80 | ||
|
72 | 81 | |
|
73 | 82 | @dec.skip_without('sqlite3') |
|
74 | 83 | def doctest_hist_f(): |
@@ -11,15 +11,15 b' Usage' | |||
|
11 | 11 | |
|
12 | 12 | ``%autopx`` |
|
13 | 13 | |
|
14 |
|
|
|
14 | {AUTOPX_DOC} | |
|
15 | 15 | |
|
16 | 16 | ``%px`` |
|
17 | 17 | |
|
18 |
|
|
|
18 | {PX_DOC} | |
|
19 | 19 | |
|
20 | 20 | ``%result`` |
|
21 | 21 | |
|
22 |
|
|
|
22 | {RESULT_DOC} | |
|
23 | 23 | |
|
24 | 24 | """ |
|
25 | 25 | |
@@ -37,87 +37,172 b' Usage' | |||
|
37 | 37 | import ast |
|
38 | 38 | import re |
|
39 | 39 | |
|
40 | from IPython.core.magic import Magics, magics_class, line_magic | |
|
40 | from IPython.core.error import UsageError | |
|
41 | from IPython.core.magic import Magics, magics_class, line_magic, cell_magic | |
|
41 | 42 | from IPython.testing.skipdoctest import skip_doctest |
|
42 | 43 | |
|
43 | 44 | #----------------------------------------------------------------------------- |
|
44 | 45 | # Definitions of magic functions for use with IPython |
|
45 | 46 | #----------------------------------------------------------------------------- |
|
46 | 47 | |
|
47 | NO_ACTIVE_VIEW = """ | |
|
48 |
Use activate() on a DirectView object to |
|
|
49 | """ | |
|
48 | ||
|
49 | NO_ACTIVE_VIEW = "Use activate() on a DirectView object to use it with magics." | |
|
50 | 50 | |
|
51 | 51 | |
|
52 | 52 | @magics_class |
|
53 | 53 | class ParallelMagics(Magics): |
|
54 | 54 | """A set of magics useful when controlling a parallel IPython cluster. |
|
55 | 55 | """ |
|
56 | ||
|
57 | def __init__(self, shell): | |
|
58 | super(ParallelMagics, self).__init__(shell) | |
|
59 | # A flag showing if autopx is activated or not | |
|
60 | self.autopx = False | |
|
56 | ||
|
57 | # A flag showing if autopx is activated or not | |
|
58 | _autopx = False | |
|
59 | # the current view used by the magics: | |
|
60 | active_view = None | |
|
61 | 61 | |
|
62 | 62 | @skip_doctest |
|
63 | 63 | @line_magic |
|
64 | 64 | def result(self, parameter_s=''): |
|
65 |
"""Print the result of command i on all engines. |
|
|
65 | """Print the result of command i on all engines. | |
|
66 | 66 | |
|
67 | 67 | To use this a :class:`DirectView` instance must be created |
|
68 | 68 | and then activated by calling its :meth:`activate` method. |
|
69 | ||
|
70 | This lets you recall the results of %px computations after | |
|
71 | asynchronous submission (view.block=False). | |
|
69 | 72 | |
|
70 | 73 | Then you can do the following:: |
|
71 | 74 | |
|
72 |
In [23]: % |
|
|
73 | Out[23]: | |
|
74 | <Results List> | |
|
75 |
|
|
|
76 |
[ |
|
|
77 | ||
|
78 | In [22]: %result 6 | |
|
79 |
|
|
|
80 | <Results List> | |
|
81 | [0] In [6]: a = 10 | |
|
82 | [1] In [6]: a = 10 | |
|
75 | In [23]: %px os.getpid() | |
|
76 | Async parallel execution on engine(s): all | |
|
77 | ||
|
78 | In [24]: %result | |
|
79 | [ 8] Out[10]: 60920 | |
|
80 | [ 9] Out[10]: 60921 | |
|
81 | [10] Out[10]: 60922 | |
|
82 | [11] Out[10]: 60923 | |
|
83 | 83 | """ |
|
84 | ||
|
84 | 85 | if self.active_view is None: |
|
85 |
|
|
|
86 |
|
|
|
87 | ||
|
86 | raise UsageError(NO_ACTIVE_VIEW) | |
|
87 | ||
|
88 | stride = len(self.active_view) | |
|
88 | 89 | try: |
|
89 | 90 | index = int(parameter_s) |
|
90 | 91 | except: |
|
91 |
index = |
|
|
92 | result = self.active_view.get_result(index) | |
|
93 | return result | |
|
92 | index = -1 | |
|
93 | msg_ids = self.active_view.history[stride * index:(stride * (index + 1)) or None] | |
|
94 | ||
|
95 | result = self.active_view.get_result(msg_ids) | |
|
96 | ||
|
97 | result.get() | |
|
98 | result.display_outputs() | |
|
94 | 99 | |
|
95 | 100 | @skip_doctest |
|
96 | 101 | @line_magic |
|
97 | 102 | def px(self, parameter_s=''): |
|
98 | 103 | """Executes the given python command in parallel. |
|
99 | ||
|
104 | ||
|
100 | 105 | To use this a :class:`DirectView` instance must be created |
|
101 | 106 | and then activated by calling its :meth:`activate` method. |
|
102 | 107 | |
|
103 | 108 | Then you can do the following:: |
|
104 | 109 | |
|
105 |
In [24]: %px a = |
|
|
110 | In [24]: %px a = os.getpid() | |
|
106 | 111 | Parallel execution on engine(s): all |
|
107 |
|
|
|
108 | <Results List> | |
|
109 | [0] In [7]: a = 5 | |
|
110 |
[ |
|
|
112 | ||
|
113 | In [25]: %px print a | |
|
114 | [stdout:0] 1234 | |
|
115 | [stdout:1] 1235 | |
|
116 | [stdout:2] 1236 | |
|
117 | [stdout:3] 1237 | |
|
111 | 118 | """ |
|
119 | return self.parallel_execute(parameter_s) | |
|
120 | ||
|
121 | def parallel_execute(self, cell, block=None, groupby='type'): | |
|
122 | """implementation used by %px and %%parallel""" | |
|
112 | 123 | |
|
113 | 124 | if self.active_view is None: |
|
114 |
|
|
|
115 |
|
|
|
116 | print "Parallel execution on engine(s): %s" % self.active_view.targets | |
|
117 | result = self.active_view.execute(parameter_s, block=False) | |
|
118 | if self.active_view.block: | |
|
125 | raise UsageError(NO_ACTIVE_VIEW) | |
|
126 | ||
|
127 | # defaults: | |
|
128 | block = self.active_view.block if block is None else block | |
|
129 | ||
|
130 | base = "Parallel" if block else "Async parallel" | |
|
131 | print base + " execution on engine(s): %s" % self.active_view.targets | |
|
132 | ||
|
133 | result = self.active_view.execute(cell, silent=False, block=False) | |
|
134 | if block: | |
|
119 | 135 | result.get() |
|
120 |
|
|
|
136 | result.display_outputs(groupby) | |
|
137 | else: | |
|
138 | # return AsyncResult only on non-blocking submission | |
|
139 | return result | |
|
140 | ||
|
141 | @skip_doctest | |
|
142 | @cell_magic('px') | |
|
143 | def cell_px(self, line='', cell=None): | |
|
144 | """Executes the given python command in parallel. | |
|
145 | ||
|
146 | Cell magic usage: | |
|
147 | ||
|
148 | %%px [-o] [-e] [--group-options=type|engine|order] [--[no]block] | |
|
149 | ||
|
150 | Options (%%px cell magic only): | |
|
151 | ||
|
152 | -o: collate outputs in oder (same as group-outputs=order) | |
|
153 | ||
|
154 | -e: group outputs by engine (same as group-outputs=engine) | |
|
155 | ||
|
156 | --group-outputs=type [default behavior]: | |
|
157 | each output type (stdout, stderr, displaypub) for all engines | |
|
158 | displayed together. | |
|
159 | ||
|
160 | --group-outputs=order: | |
|
161 | The same as 'type', but individual displaypub outputs (e.g. plots) | |
|
162 | will be interleaved, so it will display all of the first plots, | |
|
163 | then all of the second plots, etc. | |
|
164 | ||
|
165 | --group-outputs=engine: | |
|
166 | All of an engine's output is displayed before moving on to the next. | |
|
167 | ||
|
168 | --[no]block: | |
|
169 | Whether or not to block for the execution to complete | |
|
170 | (and display the results). If unspecified, the active view's | |
|
171 | ||
|
172 | ||
|
173 | To use this a :class:`DirectView` instance must be created | |
|
174 | and then activated by calling its :meth:`activate` method. | |
|
175 | ||
|
176 | Then you can do the following:: | |
|
177 | ||
|
178 | In [24]: %%parallel --noblock a = os.getpid() | |
|
179 | Async parallel execution on engine(s): all | |
|
180 | ||
|
181 | In [25]: %px print a | |
|
182 | [stdout:0] 1234 | |
|
183 | [stdout:1] 1235 | |
|
184 | [stdout:2] 1236 | |
|
185 | [stdout:3] 1237 | |
|
186 | """ | |
|
187 | ||
|
188 | block = None | |
|
189 | groupby = 'type' | |
|
190 | # as a cell magic, we accept args | |
|
191 | opts, _ = self.parse_options(line, 'oe', 'group-outputs=', 'block', 'noblock') | |
|
192 | ||
|
193 | if 'group-outputs' in opts: | |
|
194 | groupby = opts['group-outputs'] | |
|
195 | elif 'o' in opts: | |
|
196 | groupby = 'order' | |
|
197 | elif 'e' in opts: | |
|
198 | groupby = 'engine' | |
|
199 | ||
|
200 | if 'block' in opts: | |
|
201 | block = True | |
|
202 | elif 'noblock' in opts: | |
|
203 | block = False | |
|
204 | ||
|
205 | return self.parallel_execute(cell, block=block, groupby=groupby) | |
|
121 | 206 | |
|
122 | 207 | @skip_doctest |
|
123 | 208 | @line_magic |
@@ -149,7 +234,7 b' class ParallelMagics(Magics):' | |||
|
149 | 234 | In [27]: %autopx |
|
150 | 235 | %autopx disabled |
|
151 | 236 | """ |
|
152 | if self.autopx: | |
|
237 | if self._autopx: | |
|
153 | 238 | self._disable_autopx() |
|
154 | 239 | else: |
|
155 | 240 | self._enable_autopx() |
@@ -159,50 +244,23 b' class ParallelMagics(Magics):' | |||
|
159 | 244 | pxrun_cell. |
|
160 | 245 | """ |
|
161 | 246 | if self.active_view is None: |
|
162 |
|
|
|
163 | return | |
|
247 | raise UsageError(NO_ACTIVE_VIEW) | |
|
164 | 248 | |
|
165 |
# override run_cell |
|
|
249 | # override run_cell | |
|
166 | 250 | self._original_run_cell = self.shell.run_cell |
|
167 | 251 | self.shell.run_cell = self.pxrun_cell |
|
168 | self._original_run_code = self.shell.run_code | |
|
169 | self.shell.run_code = self.pxrun_code | |
|
170 | 252 | |
|
171 | self.autopx = True | |
|
253 | self._autopx = True | |
|
172 | 254 | print "%autopx enabled" |
|
173 | 255 | |
|
174 | 256 | def _disable_autopx(self): |
|
175 | 257 | """Disable %autopx by restoring the original InteractiveShell.run_cell. |
|
176 | 258 | """ |
|
177 | if self.autopx: | |
|
259 | if self._autopx: | |
|
178 | 260 | self.shell.run_cell = self._original_run_cell |
|
179 | self.shell.run_code = self._original_run_code | |
|
180 | self.autopx = False | |
|
261 | self._autopx = False | |
|
181 | 262 | print "%autopx disabled" |
|
182 | 263 | |
|
183 | def _maybe_display_output(self, result): | |
|
184 | """Maybe display the output of a parallel result. | |
|
185 | ||
|
186 | If self.active_view.block is True, wait for the result | |
|
187 | and display the result. Otherwise, this is a noop. | |
|
188 | """ | |
|
189 | if isinstance(result.stdout, basestring): | |
|
190 | # single result | |
|
191 | stdouts = [result.stdout.rstrip()] | |
|
192 | else: | |
|
193 | stdouts = [s.rstrip() for s in result.stdout] | |
|
194 | ||
|
195 | targets = self.active_view.targets | |
|
196 | if isinstance(targets, int): | |
|
197 | targets = [targets] | |
|
198 | elif targets == 'all': | |
|
199 | targets = self.active_view.client.ids | |
|
200 | ||
|
201 | if any(stdouts): | |
|
202 | for eid,stdout in zip(targets, stdouts): | |
|
203 | print '[stdout:%i]'%eid, stdout | |
|
204 | ||
|
205 | ||
|
206 | 264 | def pxrun_cell(self, raw_cell, store_history=False, silent=False): |
|
207 | 265 | """drop-in replacement for InteractiveShell.run_cell. |
|
208 | 266 | |
@@ -263,47 +321,16 b' class ParallelMagics(Magics):' | |||
|
263 | 321 | self.shell.showtraceback() |
|
264 | 322 | return True |
|
265 | 323 | else: |
|
266 |
self. |
|
|
267 | return False | |
|
268 | ||
|
269 | def pxrun_code(self, code_obj): | |
|
270 | """drop-in replacement for InteractiveShell.run_code. | |
|
271 | ||
|
272 | This executes code remotely, instead of in the local namespace. | |
|
273 | ||
|
274 | See InteractiveShell.run_code for details. | |
|
275 | """ | |
|
276 | ipself = self.shell | |
|
277 | # check code object for the autopx magic | |
|
278 | if 'get_ipython' in code_obj.co_names and 'magic' in code_obj.co_names \ | |
|
279 | and any( [ isinstance(c, basestring) and 'autopx' in c | |
|
280 | for c in code_obj.co_consts ]): | |
|
281 | self._disable_autopx() | |
|
282 | return False | |
|
283 | else: | |
|
284 | try: | |
|
285 | result = self.active_view.execute(code_obj, block=False) | |
|
286 | except: | |
|
287 | ipself.showtraceback() | |
|
288 | return True | |
|
289 | else: | |
|
290 | if self.active_view.block: | |
|
291 | try: | |
|
292 | result.get() | |
|
293 | except: | |
|
294 | self.shell.showtraceback() | |
|
295 | return True | |
|
296 | else: | |
|
297 | self._maybe_display_output(result) | |
|
324 | with ipself.builtin_trap: | |
|
325 | result.display_outputs() | |
|
298 | 326 | return False |
|
299 | 327 | |
|
300 | 328 | |
|
301 |
__doc__ = __doc__. |
|
|
302 |
|
|
|
303 | __doc__ = __doc__.replace('@PX_DOC@', | |
|
304 |
|
|
|
305 | __doc__ = __doc__.replace('@RESULT_DOC@', | |
|
306 | " " + ParallelMagics.result.__doc__) | |
|
329 | __doc__ = __doc__.format( | |
|
330 | AUTOPX_DOC = ' '*8 + ParallelMagics.autopx.__doc__, | |
|
331 | PX_DOC = ' '*8 + ParallelMagics.px.__doc__, | |
|
332 | RESULT_DOC = ' '*8 + ParallelMagics.result.__doc__ | |
|
333 | ) | |
|
307 | 334 | |
|
308 | 335 | _loaded = False |
|
309 | 336 |
@@ -21,7 +21,7 b' from datetime import datetime' | |||
|
21 | 21 | |
|
22 | 22 | from zmq import MessageTracker |
|
23 | 23 | |
|
24 | from IPython.core.display import clear_output | |
|
24 | from IPython.core.display import clear_output, display | |
|
25 | 25 | from IPython.external.decorator import decorator |
|
26 | 26 | from IPython.parallel import error |
|
27 | 27 | |
@@ -377,6 +377,140 b' class AsyncResult(object):' | |||
|
377 | 377 | sys.stdout.flush() |
|
378 | 378 | |
|
379 | 379 | print "done" |
|
380 | ||
|
381 | def _republish_displaypub(self, content, eid): | |
|
382 | """republish individual displaypub content dicts""" | |
|
383 | try: | |
|
384 | ip = get_ipython() | |
|
385 | except NameError: | |
|
386 | # displaypub is meaningless outside IPython | |
|
387 | return | |
|
388 | md = content['metadata'] or {} | |
|
389 | md['engine'] = eid | |
|
390 | ip.display_pub.publish(content['source'], content['data'], md) | |
|
391 | ||
|
392 | ||
|
393 | def _display_single_result(self): | |
|
394 | ||
|
395 | print self.stdout | |
|
396 | print >> sys.stderr, self.stderr | |
|
397 | ||
|
398 | try: | |
|
399 | get_ipython() | |
|
400 | except NameError: | |
|
401 | # displaypub is meaningless outside IPython | |
|
402 | return | |
|
403 | ||
|
404 | for output in self.outputs: | |
|
405 | self._republish_displaypub(output, self.engine_id) | |
|
406 | ||
|
407 | if self.pyout is not None: | |
|
408 | display(self.get()) | |
|
409 | ||
|
410 | @check_ready | |
|
411 | def display_outputs(self, groupby="type"): | |
|
412 | """republish the outputs of the computation | |
|
413 | ||
|
414 | Parameters | |
|
415 | ---------- | |
|
416 | ||
|
417 | groupby : str [default: type] | |
|
418 | if 'type': | |
|
419 | Group outputs by type (show all stdout, then all stderr, etc.): | |
|
420 | ||
|
421 | [stdout:1] foo | |
|
422 | [stdout:2] foo | |
|
423 | [stderr:1] bar | |
|
424 | [stderr:2] bar | |
|
425 | if 'engine': | |
|
426 | Display outputs for each engine before moving on to the next: | |
|
427 | ||
|
428 | [stdout:1] foo | |
|
429 | [stderr:1] bar | |
|
430 | [stdout:2] foo | |
|
431 | [stderr:2] bar | |
|
432 | ||
|
433 | if 'order': | |
|
434 | Like 'type', but further collate individual displaypub | |
|
435 | outputs. This is meant for cases of each command producing | |
|
436 | several plots, and you would like to see all of the first | |
|
437 | plots together, then all of the second plots, and so on. | |
|
438 | """ | |
|
439 | # flush iopub, just in case | |
|
440 | self._client._flush_iopub(self._client._iopub_socket) | |
|
441 | if self._single_result: | |
|
442 | self._display_single_result() | |
|
443 | return | |
|
444 | ||
|
445 | stdouts = [s.rstrip() for s in self.stdout] | |
|
446 | stderrs = [s.rstrip() for s in self.stderr] | |
|
447 | pyouts = [p for p in self.pyout] | |
|
448 | output_lists = self.outputs | |
|
449 | results = self.get() | |
|
450 | ||
|
451 | targets = self.engine_id | |
|
452 | ||
|
453 | if groupby == "engine": | |
|
454 | for eid,stdout,stderr,outputs,r,pyout in zip( | |
|
455 | targets, stdouts, stderrs, output_lists, results, pyouts | |
|
456 | ): | |
|
457 | if stdout: | |
|
458 | print '[stdout:%i]' % eid, stdout | |
|
459 | if stderr: | |
|
460 | print >> sys.stderr, '[stderr:%i]' % eid, stderr | |
|
461 | ||
|
462 | try: | |
|
463 | get_ipython() | |
|
464 | except NameError: | |
|
465 | # displaypub is meaningless outside IPython | |
|
466 | return | |
|
467 | ||
|
468 | for output in outputs: | |
|
469 | self._republish_displaypub(output, eid) | |
|
470 | ||
|
471 | if pyout is not None: | |
|
472 | display(r) | |
|
473 | ||
|
474 | elif groupby in ('type', 'order'): | |
|
475 | # republish stdout: | |
|
476 | if any(stdouts): | |
|
477 | for eid,stdout in zip(targets, stdouts): | |
|
478 | print '[stdout:%i]' % eid, stdout | |
|
479 | ||
|
480 | # republish stderr: | |
|
481 | if any(stderrs): | |
|
482 | for eid,stderr in zip(targets, stderrs): | |
|
483 | print >> sys.stderr, '[stderr:%i]' % eid, stderr | |
|
484 | ||
|
485 | try: | |
|
486 | get_ipython() | |
|
487 | except NameError: | |
|
488 | # displaypub is meaningless outside IPython | |
|
489 | return | |
|
490 | ||
|
491 | if groupby == 'order': | |
|
492 | output_dict = dict((eid, outputs) for eid,outputs in zip(targets, output_lists)) | |
|
493 | N = max(len(outputs) for outputs in output_lists) | |
|
494 | for i in range(N): | |
|
495 | for eid in targets: | |
|
496 | outputs = output_dict[eid] | |
|
497 | if len(outputs) >= N: | |
|
498 | self._republish_displaypub(outputs[i], eid) | |
|
499 | else: | |
|
500 | # republish displaypub output | |
|
501 | for eid,outputs in zip(targets, output_lists): | |
|
502 | for output in outputs: | |
|
503 | self._republish_displaypub(output, eid) | |
|
504 | ||
|
505 | # finally, add pyout: | |
|
506 | for eid,r,pyout in zip(targets, results, pyouts): | |
|
507 | if pyout is not None: | |
|
508 | display(r) | |
|
509 | ||
|
510 | else: | |
|
511 | raise ValueError("groupby must be one of 'type', 'engine', 'collate', not %r" % groupby) | |
|
512 | ||
|
513 | ||
|
380 | 514 | |
|
381 | 515 | |
|
382 | 516 | class AsyncMapResult(AsyncResult): |
@@ -33,6 +33,7 b' import zmq' | |||
|
33 | 33 | from IPython.config.configurable import MultipleInstanceError |
|
34 | 34 | from IPython.core.application import BaseIPythonApplication |
|
35 | 35 | |
|
36 | from IPython.utils.coloransi import TermColors | |
|
36 | 37 | from IPython.utils.jsonutil import rekey |
|
37 | 38 | from IPython.utils.localinterfaces import LOCAL_IPS |
|
38 | 39 | from IPython.utils.path import get_ipython_dir |
@@ -90,13 +91,39 b' class ExecuteReply(object):' | |||
|
90 | 91 | return self.metadata[key] |
|
91 | 92 | |
|
92 | 93 | def __repr__(self): |
|
93 | pyout = self.metadata['pyout'] or {} | |
|
94 |
text_out = pyout |
|
|
94 | pyout = self.metadata['pyout'] or {'data':{}} | |
|
95 | text_out = pyout['data'].get('text/plain', '') | |
|
95 | 96 | if len(text_out) > 32: |
|
96 | 97 | text_out = text_out[:29] + '...' |
|
97 | 98 | |
|
98 | 99 | return "<ExecuteReply[%i]: %s>" % (self.execution_count, text_out) |
|
99 | 100 | |
|
101 | def _repr_pretty_(self, p, cycle): | |
|
102 | pyout = self.metadata['pyout'] or {'data':{}} | |
|
103 | text_out = pyout['data'].get('text/plain', '') | |
|
104 | ||
|
105 | if not text_out: | |
|
106 | return | |
|
107 | ||
|
108 | try: | |
|
109 | ip = get_ipython() | |
|
110 | except NameError: | |
|
111 | colors = "NoColor" | |
|
112 | else: | |
|
113 | colors = ip.colors | |
|
114 | ||
|
115 | if colors == "NoColor": | |
|
116 | out = normal = "" | |
|
117 | else: | |
|
118 | out = TermColors.Red | |
|
119 | normal = TermColors.Normal | |
|
120 | ||
|
121 | p.text( | |
|
122 | u'[%i] ' % self.metadata['engine_id'] + | |
|
123 | out + u'Out[%i]: ' % self.execution_count + | |
|
124 | normal + text_out | |
|
125 | ) | |
|
126 | ||
|
100 | 127 | def _repr_html_(self): |
|
101 | 128 | pyout = self.metadata['pyout'] or {'data':{}} |
|
102 | 129 | return pyout['data'].get("text/html") |
@@ -15,6 +15,7 b' Authors:' | |||
|
15 | 15 | import sys |
|
16 | 16 | import tempfile |
|
17 | 17 | import time |
|
18 | from StringIO import StringIO | |
|
18 | 19 | |
|
19 | 20 | from nose import SkipTest |
|
20 | 21 | |
@@ -59,6 +60,28 b' def raiser(eclass):' | |||
|
59 | 60 | """raise an exception""" |
|
60 | 61 | raise eclass() |
|
61 | 62 | |
|
63 | def generate_output(): | |
|
64 | """function for testing output | |
|
65 | ||
|
66 | publishes two outputs of each type, and returns | |
|
67 | a rich displayable object. | |
|
68 | """ | |
|
69 | ||
|
70 | import sys | |
|
71 | from IPython.core.display import display, HTML, Math | |
|
72 | ||
|
73 | print "stdout" | |
|
74 | print >> sys.stderr, "stderr" | |
|
75 | ||
|
76 | display(HTML("<b>HTML</b>")) | |
|
77 | ||
|
78 | print "stdout2" | |
|
79 | print >> sys.stderr, "stderr2" | |
|
80 | ||
|
81 | display(Math(r"\alpha=\beta")) | |
|
82 | ||
|
83 | return Math("42") | |
|
84 | ||
|
62 | 85 | # test decorator for skipping tests when libraries are unavailable |
|
63 | 86 | def skip_without(*names): |
|
64 | 87 | """skip a test if some names are not importable""" |
@@ -73,6 +96,41 b' def skip_without(*names):' | |||
|
73 | 96 | return f(*args, **kwargs) |
|
74 | 97 | return skip_without_names |
|
75 | 98 | |
|
99 | #------------------------------------------------------------------------------- | |
|
100 | # Classes | |
|
101 | #------------------------------------------------------------------------------- | |
|
102 | ||
|
103 | class CapturedIO(object): | |
|
104 | """Simple object for containing captured stdout/err StringIO objects""" | |
|
105 | ||
|
106 | def __init__(self, stdout, stderr): | |
|
107 | self.stdout_io = stdout | |
|
108 | self.stderr_io = stderr | |
|
109 | ||
|
110 | @property | |
|
111 | def stdout(self): | |
|
112 | return self.stdout_io.getvalue() | |
|
113 | ||
|
114 | @property | |
|
115 | def stderr(self): | |
|
116 | return self.stderr_io.getvalue() | |
|
117 | ||
|
118 | ||
|
119 | class capture_output(object): | |
|
120 | """context manager for capturing stdout/err""" | |
|
121 | ||
|
122 | def __enter__(self): | |
|
123 | self.sys_stdout = sys.stdout | |
|
124 | self.sys_stderr = sys.stderr | |
|
125 | stdout = sys.stdout = StringIO() | |
|
126 | stderr = sys.stderr = StringIO() | |
|
127 | return CapturedIO(stdout, stderr) | |
|
128 | ||
|
129 | def __exit__(self, exc_type, exc_value, traceback): | |
|
130 | sys.stdout = self.sys_stdout | |
|
131 | sys.stderr = self.sys_stderr | |
|
132 | ||
|
133 | ||
|
76 | 134 | class ClusterTestCase(BaseZMQTestCase): |
|
77 | 135 | |
|
78 | 136 | def add_engines(self, n=1, block=True): |
@@ -117,6 +175,17 b' class ClusterTestCase(BaseZMQTestCase):' | |||
|
117 | 175 | else: |
|
118 | 176 | self.fail("should have raised a RemoteError") |
|
119 | 177 | |
|
178 | def _wait_for(self, f, timeout=10): | |
|
179 | """wait for a condition""" | |
|
180 | tic = time.time() | |
|
181 | while time.time() <= tic + timeout: | |
|
182 | if f(): | |
|
183 | return | |
|
184 | time.sleep(0.1) | |
|
185 | self.client.spin() | |
|
186 | if not f(): | |
|
187 | print "Warning: Awaited condition never arrived" | |
|
188 | ||
|
120 | 189 | def setUp(self): |
|
121 | 190 | BaseZMQTestCase.setUp(self) |
|
122 | 191 | self.client = self.connect_client() |
@@ -366,118 +366,6 b' class TestView(ClusterTestCase, ParametricTestCase):' | |||
|
366 | 366 | |
|
367 | 367 | self.assertEquals(view.apply_sync(findall, '\w+', 'hello world'), 'hello world'.split()) |
|
368 | 368 | |
|
369 | # parallel magic tests | |
|
370 | ||
|
371 | def test_magic_px_blocking(self): | |
|
372 | ip = get_ipython() | |
|
373 | v = self.client[-1] | |
|
374 | v.activate() | |
|
375 | v.block=True | |
|
376 | ||
|
377 | ip.magic('px a=5') | |
|
378 | self.assertEquals(v['a'], 5) | |
|
379 | ip.magic('px a=10') | |
|
380 | self.assertEquals(v['a'], 10) | |
|
381 | sio = StringIO() | |
|
382 | savestdout = sys.stdout | |
|
383 | sys.stdout = sio | |
|
384 | # just 'print a' worst ~99% of the time, but this ensures that | |
|
385 | # the stdout message has arrived when the result is finished: | |
|
386 | ip.magic('px import sys,time;print (a); sys.stdout.flush();time.sleep(0.2)') | |
|
387 | sys.stdout = savestdout | |
|
388 | buf = sio.getvalue() | |
|
389 | self.assertTrue('[stdout:' in buf, buf) | |
|
390 | self.assertTrue(buf.rstrip().endswith('10')) | |
|
391 | self.assertRaisesRemote(ZeroDivisionError, ip.magic, 'px 1/0') | |
|
392 | ||
|
393 | def test_magic_px_nonblocking(self): | |
|
394 | ip = get_ipython() | |
|
395 | v = self.client[-1] | |
|
396 | v.activate() | |
|
397 | v.block=False | |
|
398 | ||
|
399 | ip.magic('px a=5') | |
|
400 | self.assertEquals(v['a'], 5) | |
|
401 | ip.magic('px a=10') | |
|
402 | self.assertEquals(v['a'], 10) | |
|
403 | sio = StringIO() | |
|
404 | savestdout = sys.stdout | |
|
405 | sys.stdout = sio | |
|
406 | ip.magic('px print a') | |
|
407 | sys.stdout = savestdout | |
|
408 | buf = sio.getvalue() | |
|
409 | self.assertFalse('[stdout:%i]'%v.targets in buf) | |
|
410 | ip.magic('px 1/0') | |
|
411 | ar = v.get_result(-1) | |
|
412 | self.assertRaisesRemote(ZeroDivisionError, ar.get) | |
|
413 | ||
|
414 | def test_magic_autopx_blocking(self): | |
|
415 | ip = get_ipython() | |
|
416 | v = self.client[-1] | |
|
417 | v.activate() | |
|
418 | v.block=True | |
|
419 | ||
|
420 | sio = StringIO() | |
|
421 | savestdout = sys.stdout | |
|
422 | sys.stdout = sio | |
|
423 | ip.magic('autopx') | |
|
424 | ip.run_cell('\n'.join(('a=5','b=10','c=0'))) | |
|
425 | ip.run_cell('b*=2') | |
|
426 | ip.run_cell('print (b)') | |
|
427 | ip.run_cell("b/c") | |
|
428 | ip.magic('autopx') | |
|
429 | sys.stdout = savestdout | |
|
430 | output = sio.getvalue().strip() | |
|
431 | self.assertTrue(output.startswith('%autopx enabled')) | |
|
432 | self.assertTrue(output.endswith('%autopx disabled')) | |
|
433 | self.assertTrue('RemoteError: ZeroDivisionError' in output) | |
|
434 | ar = v.get_result(-1) | |
|
435 | self.assertEquals(v['a'], 5) | |
|
436 | self.assertEquals(v['b'], 20) | |
|
437 | self.assertRaisesRemote(ZeroDivisionError, ar.get) | |
|
438 | ||
|
439 | def test_magic_autopx_nonblocking(self): | |
|
440 | ip = get_ipython() | |
|
441 | v = self.client[-1] | |
|
442 | v.activate() | |
|
443 | v.block=False | |
|
444 | ||
|
445 | sio = StringIO() | |
|
446 | savestdout = sys.stdout | |
|
447 | sys.stdout = sio | |
|
448 | ip.magic('autopx') | |
|
449 | ip.run_cell('\n'.join(('a=5','b=10','c=0'))) | |
|
450 | ip.run_cell('print (b)') | |
|
451 | ip.run_cell('import time; time.sleep(0.1)') | |
|
452 | ip.run_cell("b/c") | |
|
453 | ip.run_cell('b*=2') | |
|
454 | ip.magic('autopx') | |
|
455 | sys.stdout = savestdout | |
|
456 | output = sio.getvalue().strip() | |
|
457 | self.assertTrue(output.startswith('%autopx enabled')) | |
|
458 | self.assertTrue(output.endswith('%autopx disabled')) | |
|
459 | self.assertFalse('ZeroDivisionError' in output) | |
|
460 | ar = v.get_result(-2) | |
|
461 | self.assertRaisesRemote(ZeroDivisionError, ar.get) | |
|
462 | # prevent TaskAborted on pulls, due to ZeroDivisionError | |
|
463 | time.sleep(0.5) | |
|
464 | self.assertEquals(v['a'], 5) | |
|
465 | # b*=2 will not fire, due to abort | |
|
466 | self.assertEquals(v['b'], 10) | |
|
467 | ||
|
468 | def test_magic_result(self): | |
|
469 | ip = get_ipython() | |
|
470 | v = self.client[-1] | |
|
471 | v.activate() | |
|
472 | v['a'] = 111 | |
|
473 | ra = v['a'] | |
|
474 | ||
|
475 | ar = ip.magic('result') | |
|
476 | self.assertEquals(ar.msg_ids, [v.history[-1]]) | |
|
477 | self.assertEquals(ar.get(), 111) | |
|
478 | ar = ip.magic('result -2') | |
|
479 | self.assertEquals(ar.msg_ids, [v.history[-2]]) | |
|
480 | ||
|
481 | 369 | def test_unicode_execute(self): |
|
482 | 370 | """test executing unicode strings""" |
|
483 | 371 | v = self.client[-1] |
@@ -575,16 +463,6 b' class TestView(ClusterTestCase, ParametricTestCase):' | |||
|
575 | 463 | |
|
576 | 464 | |
|
577 | 465 | # begin execute tests |
|
578 | def _wait_for(self, f, timeout=10): | |
|
579 | tic = time.time() | |
|
580 | while time.time() <= tic + timeout: | |
|
581 | if f(): | |
|
582 | return | |
|
583 | time.sleep(0.1) | |
|
584 | self.client.spin() | |
|
585 | if not f(): | |
|
586 | print "Warning: Awaited condition never arrived" | |
|
587 | ||
|
588 | 466 | |
|
589 | 467 | def test_execute_reply(self): |
|
590 | 468 | e0 = self.client[self.client.ids[0]] |
@@ -31,12 +31,15 b' class ZMQDisplayHook(object):' | |||
|
31 | 31 | |
|
32 | 32 | |
|
33 | 33 | def _encode_binary(format_dict): |
|
34 | encoded = format_dict.copy() | |
|
34 | 35 | pngdata = format_dict.get('image/png') |
|
35 | if pngdata is not None: | |
|
36 |
|
|
|
36 | if isinstance(pngdata, bytes): | |
|
37 | encoded['image/png'] = encodestring(pngdata).decode('ascii') | |
|
37 | 38 | jpegdata = format_dict.get('image/jpeg') |
|
38 | if jpegdata is not None: | |
|
39 |
|
|
|
39 | if isinstance(jpegdata, bytes): | |
|
40 | encoded['image/jpeg'] = encodestring(jpegdata).decode('ascii') | |
|
41 | ||
|
42 | return encoded | |
|
40 | 43 | |
|
41 | 44 | |
|
42 | 45 | class ZMQShellDisplayHook(DisplayHook): |
@@ -61,8 +64,7 b' class ZMQShellDisplayHook(DisplayHook):' | |||
|
61 | 64 | self.msg['content']['execution_count'] = self.prompt_count |
|
62 | 65 | |
|
63 | 66 | def write_format_data(self, format_dict): |
|
64 | _encode_binary(format_dict) | |
|
65 | self.msg['content']['data'] = format_dict | |
|
67 | self.msg['content']['data'] = _encode_binary(format_dict) | |
|
66 | 68 | |
|
67 | 69 | def finish_displayhook(self): |
|
68 | 70 | """Finish up all displayhook activities.""" |
@@ -382,6 +382,8 b' class Kernel(Configurable):' | |||
|
382 | 382 | # runlines. We'll need to clean up this logic later. |
|
383 | 383 | if shell._reply_content is not None: |
|
384 | 384 | reply_content.update(shell._reply_content) |
|
385 | e_info = dict(engine_uuid=self.ident, engine_id=self.int_id, method='execute') | |
|
386 | reply_content['engine_info'] = e_info | |
|
385 | 387 | # reset after use |
|
386 | 388 | shell._reply_content = None |
|
387 | 389 |
@@ -77,8 +77,7 b' class ZMQDisplayPublisher(DisplayPublisher):' | |||
|
77 | 77 | self._validate_data(source, data, metadata) |
|
78 | 78 | content = {} |
|
79 | 79 | content['source'] = source |
|
80 | _encode_binary(data) | |
|
81 | content['data'] = data | |
|
80 | content['data'] = _encode_binary(data) | |
|
82 | 81 | content['metadata'] = metadata |
|
83 | 82 | self.session.send( |
|
84 | 83 | self.pub_socket, u'display_data', json_clean(content), |
@@ -120,21 +120,19 b' using our :func:`psum` function:' | |||
|
120 | 120 | |
|
121 | 121 | In [1]: from IPython.parallel import Client |
|
122 | 122 | |
|
123 | In [2]: %load_ext parallel_magic | |
|
124 | ||
|
125 | 123 | In [3]: c = Client(profile='mpi') |
|
126 | 124 | |
|
127 | 125 | In [4]: view = c[:] |
|
128 | 126 | |
|
129 | In [5]: view.activate() | |
|
127 | In [5]: view.activate() # enabe magics | |
|
130 | 128 | |
|
131 | 129 | # run the contents of the file on each engine: |
|
132 | 130 | In [6]: view.run('psum.py') |
|
133 | 131 | |
|
134 | In [6]: px a = np.random.rand(100) | |
|
132 | In [6]: %px a = np.random.rand(100) | |
|
135 | 133 | Parallel execution on engines: [0,1,2,3] |
|
136 | 134 | |
|
137 | In [8]: px s = psum(a) | |
|
135 | In [8]: %px s = psum(a) | |
|
138 | 136 | Parallel execution on engines: [0,1,2,3] |
|
139 | 137 | |
|
140 | 138 | In [9]: view['s'] |
@@ -389,11 +389,11 b' Parallel magic commands' | |||
|
389 | 389 | ----------------------- |
|
390 | 390 | |
|
391 | 391 | We provide a few IPython magic commands (``%px``, ``%autopx`` and ``%result``) |
|
392 | that make it more pleasant to execute Python commands on the engines | |
|
393 |
|
|
|
394 | :meth:`get_result` of the :class:`DirectView`. The ``%px`` magic executes a single | |
|
395 | Python command on the engines specified by the :attr:`targets` attribute of the | |
|
396 | :class:`DirectView` instance: | |
|
392 | that make it a bit more pleasant to execute Python commands on the engines interactively. | |
|
393 | These are simply shortcuts to :meth:`.DirectView.execute` | |
|
394 | and :meth:`.AsyncResult.display_outputs` methods repsectively. | |
|
395 | The ``%px`` magic executes a single Python command on the engines | |
|
396 | specified by the :attr:`targets` attribute of the :class:`DirectView` instance: | |
|
397 | 397 | |
|
398 | 398 | .. sourcecode:: ipython |
|
399 | 399 | |
@@ -413,43 +413,127 b' Python command on the engines specified by the :attr:`targets` attribute of the' | |||
|
413 | 413 | In [27]: %px a = numpy.random.rand(2,2) |
|
414 | 414 | Parallel execution on engines: [0, 1, 2, 3] |
|
415 | 415 | |
|
416 |
In [28]: %px |
|
|
416 | In [28]: %px numpy.linalg.eigvals(a) | |
|
417 | 417 | Parallel execution on engines: [0, 1, 2, 3] |
|
418 | [0] Out[68]: array([ 0.77120707, -0.19448286]) | |
|
419 | [1] Out[68]: array([ 1.10815921, 0.05110369]) | |
|
420 | [2] Out[68]: array([ 0.74625527, -0.37475081]) | |
|
421 | [3] Out[68]: array([ 0.72931905, 0.07159743]) | |
|
422 | ||
|
423 | In [29]: %px print 'hi' | |
|
424 | Parallel execution on engine(s): [0, 1, 2, 3] | |
|
425 | [stdout:0] hi | |
|
426 | [stdout:1] hi | |
|
427 | [stdout:2] hi | |
|
428 | [stdout:3] hi | |
|
429 | ||
|
430 | ||
|
431 | Since engines are IPython as well, you can even run magics remotely: | |
|
432 | ||
|
433 | .. sourcecode:: ipython | |
|
434 | ||
|
435 | In [28]: %px %pylab inline | |
|
436 | Parallel execution on engine(s): [0, 1, 2, 3] | |
|
437 | [stdout:0] | |
|
438 | Welcome to pylab, a matplotlib-based Python environment... | |
|
439 | For more information, type 'help(pylab)'. | |
|
440 | [stdout:1] | |
|
441 | Welcome to pylab, a matplotlib-based Python environment... | |
|
442 | For more information, type 'help(pylab)'. | |
|
443 | [stdout:2] | |
|
444 | Welcome to pylab, a matplotlib-based Python environment... | |
|
445 | For more information, type 'help(pylab)'. | |
|
446 | [stdout:3] | |
|
447 | Welcome to pylab, a matplotlib-based Python environment... | |
|
448 | For more information, type 'help(pylab)'. | |
|
449 | ||
|
450 | And once in pylab mode with the inline backend, | |
|
451 | you can make plots and they will be displayed in your frontend | |
|
452 | if it suports the inline figures (e.g. notebook or qtconsole): | |
|
453 | ||
|
454 | .. sourcecode:: ipython | |
|
455 | ||
|
456 | In [40]: %px plot(rand(100)) | |
|
457 | Parallel execution on engine(s): [0, 1, 2, 3] | |
|
458 | <plot0> | |
|
459 | <plot1> | |
|
460 | <plot2> | |
|
461 | <plot3> | |
|
462 | [0] Out[79]: [<matplotlib.lines.Line2D at 0x10a6286d0>] | |
|
463 | [1] Out[79]: [<matplotlib.lines.Line2D at 0x10b9476d0>] | |
|
464 | [2] Out[79]: [<matplotlib.lines.Line2D at 0x110652750>] | |
|
465 | [3] Out[79]: [<matplotlib.lines.Line2D at 0x10c6566d0>] | |
|
418 | 466 | |
|
419 | In [28]: dv['ev'] | |
|
420 | Out[28]: [ array([ 1.09522024, -0.09645227]), | |
|
421 | ....: array([ 1.21435496, -0.35546712]), | |
|
422 | ....: array([ 0.72180653, 0.07133042]), | |
|
423 | ....: array([ 1.46384341, 1.04353244e-04]) | |
|
424 | ....: ] | |
|
425 | 467 | |
|
426 | The ``%result`` magic gets the most recent result, or takes an argument | |
|
427 | specifying the index of the result to be requested. It is simply a shortcut to the | |
|
428 | :meth:`get_result` method: | |
|
468 | ``%%px`` Cell Magic | |
|
469 | ******************* | |
|
470 | ||
|
471 | `%%px` can also be used as a Cell Magic, which accepts ``--[no]block`` flags, | |
|
472 | and a ``--group-outputs`` argument, which adjust how the outputs of multiple | |
|
473 | engines are presented. | |
|
474 | ||
|
475 | .. seealso:: | |
|
476 | ||
|
477 | :meth:`.AsyncResult.display_outputs` for the grouping options. | |
|
429 | 478 | |
|
430 | 479 | .. sourcecode:: ipython |
|
480 | ||
|
481 | In [50]: %%px --block --group-outputs=engine | |
|
482 | ....: import numpy as np | |
|
483 | ....: A = np.random.random((2,2)) | |
|
484 | ....: ev = numpy.linalg.eigvals(A) | |
|
485 | ....: print ev | |
|
486 | ....: ev.max() | |
|
487 | ....: | |
|
488 | Parallel execution on engine(s): [0, 1, 2, 3] | |
|
489 | [stdout:0] [ 0.60640442 0.95919621] | |
|
490 | [0] Out[73]: 0.9591962130899806 | |
|
491 | [stdout:1] [ 0.38501813 1.29430871] | |
|
492 | [1] Out[73]: 1.2943087091452372 | |
|
493 | [stdout:2] [-0.85925141 0.9387692 ] | |
|
494 | [2] Out[73]: 0.93876920456230284 | |
|
495 | [stdout:3] [ 0.37998269 1.24218246] | |
|
496 | [3] Out[73]: 1.2421824618493817 | |
|
497 | ||
|
498 | ``%result`` Magic | |
|
499 | ***************** | |
|
500 | ||
|
501 | If you are using ``%px`` in non-blocking mode, you won't get output. | |
|
502 | You can use ``%result`` to display the outputs of the latest command, | |
|
503 | just as is done when ``%px`` is blocking: | |
|
504 | ||
|
505 | .. sourcecode:: ipython | |
|
506 | ||
|
507 | In [39]: dv.block = False | |
|
431 | 508 | |
|
432 | In [29]: dv.apply_async(lambda : ev) | |
|
509 | In [40]: %px print 'hi' | |
|
510 | Async parallel execution on engine(s): [0, 1, 2, 3] | |
|
433 | 511 | |
|
434 |
In [ |
|
|
435 | Out[30]: [ [ 1.28167017 0.14197338], | |
|
436 | ....: [-0.14093616 1.27877273], | |
|
437 | ....: [-0.37023573 1.06779409], | |
|
438 | ....: [ 0.83664764 -0.25602658] ] | |
|
512 | In [41]: %result | |
|
513 | [stdout:0] hi | |
|
514 | [stdout:1] hi | |
|
515 | [stdout:2] hi | |
|
516 | [stdout:3] hi | |
|
517 | ||
|
518 | ``%result`` simply calls :meth:`.AsyncResult.display_outputs` on the most recent request. | |
|
519 | You can pass integers as indices if you want a result other than the latest, | |
|
520 | e.g. ``%result -2``, or ``%result 0`` for the first. | |
|
521 | ||
|
522 | ||
|
523 | ``%autopx`` | |
|
524 | *********** | |
|
439 | 525 | |
|
440 | 526 | The ``%autopx`` magic switches to a mode where everything you type is executed |
|
441 | on the engines given by the :attr:`targets` attribute: | |
|
527 | on the engines until you do ``%autopx`` again. | |
|
442 | 528 | |
|
443 | 529 | .. sourcecode:: ipython |
|
444 | 530 | |
|
445 |
In [30]: dv.block= |
|
|
531 | In [30]: dv.block=True | |
|
446 | 532 | |
|
447 | 533 | In [31]: %autopx |
|
448 |
|
|
|
449 | Type %autopx to disable | |
|
534 | %autopx enabled | |
|
450 | 535 | |
|
451 | 536 | In [32]: max_evals = [] |
|
452 | <IPython.parallel.AsyncResult object at 0x17b8a70> | |
|
453 | 537 | |
|
454 | 538 | In [33]: for i in range(100): |
|
455 | 539 | ....: a = numpy.random.rand(10,10) |
@@ -457,22 +541,15 b' on the engines given by the :attr:`targets` attribute:' | |||
|
457 | 541 | ....: evals = numpy.linalg.eigvals(a) |
|
458 | 542 | ....: max_evals.append(evals[0].real) |
|
459 | 543 | ....: |
|
460 | ....: | |
|
461 | <IPython.parallel.AsyncResult object at 0x17af8f0> | |
|
462 | ||
|
463 | In [34]: %autopx | |
|
464 | Auto Parallel Disabled | |
|
465 | 544 | |
|
466 | In [35]: dv.block=True | |
|
467 | ||
|
468 | In [36]: px ans= "Average max eigenvalue is: %f"%(sum(max_evals)/len(max_evals)) | |
|
469 | Parallel execution on engines: [0, 1, 2, 3] | |
|
545 | In [34]: print "Average max eigenvalue is: %f" % (sum(max_evals)/len(max_evals)) | |
|
546 | [stdout:0] Average max eigenvalue is: 10.193101 | |
|
547 | [stdout:1] Average max eigenvalue is: 10.064508 | |
|
548 | [stdout:2] Average max eigenvalue is: 10.055724 | |
|
549 | [stdout:3] Average max eigenvalue is: 10.086876 | |
|
470 | 550 | |
|
471 |
In [3 |
|
|
472 | Out[37]: [ 'Average max eigenvalue is: 10.1387247332', | |
|
473 | ....: 'Average max eigenvalue is: 10.2076902286', | |
|
474 | ....: 'Average max eigenvalue is: 10.1891484655', | |
|
475 | ....: 'Average max eigenvalue is: 10.1158837784',] | |
|
551 | In [35]: %autopx | |
|
552 | Auto Parallel Disabled | |
|
476 | 553 | |
|
477 | 554 | |
|
478 | 555 | Moving Python objects around |
General Comments 0
You need to be logged in to leave comments.
Login now