##// END OF EJS Templates
payload.write_payload: use `single` keyword instead of `update`...
Pablo de Oliveira -
Show More
@@ -1,49 +1,55 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Payload system for IPython.
3 3
4 4 Authors:
5 5
6 6 * Fernando Perez
7 7 * Brian Granger
8 8 """
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Copyright (C) 2008-2011 The IPython Development Team
12 12 #
13 13 # Distributed under the terms of the BSD License. The full license is in
14 14 # the file COPYING, distributed as part of this software.
15 15 #-----------------------------------------------------------------------------
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 from IPython.config.configurable import Configurable
22 22 from IPython.utils.traitlets import List
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Main payload class
26 26 #-----------------------------------------------------------------------------
27 27
28 28 class PayloadManager(Configurable):
29 29
30 30 _payload = List([])
31 31
32 def write_payload(self, data, update=False):
32 def write_payload(self, data, single=True):
33 """Include or update the specified `data` payload in the PayloadManager.
34
35 If a previous payload with the same source than `data` exists
36 and `single` is True, it will be overwritten with the new one.
37 """
38
33 39 if not isinstance(data, dict):
34 40 raise TypeError('Each payload write must be a dict, got: %r' % data)
35 41
36 if update and 'source' in data:
42 if single and 'source' in data:
37 43 source = data['source']
38 44 for i, pl in enumerate(self._payload):
39 45 if 'source' in pl and pl['source'] == source:
40 46 self._payload[i] = data
41 47 return
42 48
43 49 self._payload.append(data)
44 50
45 51 def read_payload(self):
46 52 return self._payload
47 53
48 54 def clear_payload(self):
49 55 self._payload = []
@@ -1,599 +1,599 b''
1 1 """A ZMQ-based subclass of InteractiveShell.
2 2
3 3 This code is meant to ease the refactoring of the base InteractiveShell into
4 4 something with a cleaner architecture for 2-process use, without actually
5 5 breaking InteractiveShell itself. So we're doing something a bit ugly, where
6 6 we subclass and override what we want to fix. Once this is working well, we
7 7 can go back to the base class and refactor the code for a cleaner inheritance
8 8 implementation that doesn't rely on so much monkeypatching.
9 9
10 10 But this lets us maintain a fully working IPython as we develop the new
11 11 machinery. This should thus be thought of as scaffolding.
12 12 """
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16 from __future__ import print_function
17 17
18 18 # Stdlib
19 19 import os
20 20 import sys
21 21 import time
22 22
23 23 # System library imports
24 24 from zmq.eventloop import ioloop
25 25
26 26 # Our own
27 27 from IPython.core.interactiveshell import (
28 28 InteractiveShell, InteractiveShellABC
29 29 )
30 30 from IPython.core import page
31 31 from IPython.core.autocall import ZMQExitAutocall
32 32 from IPython.core.displaypub import DisplayPublisher
33 33 from IPython.core.error import UsageError
34 34 from IPython.core.magics import MacroToEdit, CodeMagics
35 35 from IPython.core.magic import magics_class, line_magic, Magics
36 36 from IPython.core.payloadpage import install_payload_page
37 37 from IPython.display import display, Javascript
38 38 from IPython.kernel.inprocess.socket import SocketABC
39 39 from IPython.kernel import (
40 40 get_connection_file, get_connection_info, connect_qtconsole
41 41 )
42 42 from IPython.testing.skipdoctest import skip_doctest
43 43 from IPython.utils import openpy
44 44 from IPython.utils.jsonutil import json_clean, encode_images
45 45 from IPython.utils.process import arg_split
46 46 from IPython.utils import py3compat
47 47 from IPython.utils.traitlets import Instance, Type, Dict, CBool, CBytes
48 48 from IPython.utils.warn import error
49 49 from IPython.kernel.zmq.displayhook import ZMQShellDisplayHook
50 50 from IPython.kernel.zmq.datapub import ZMQDataPublisher
51 51 from IPython.kernel.zmq.session import extract_header
52 52 from session import Session
53 53
54 54 #-----------------------------------------------------------------------------
55 55 # Functions and classes
56 56 #-----------------------------------------------------------------------------
57 57
58 58 class ZMQDisplayPublisher(DisplayPublisher):
59 59 """A display publisher that publishes data using a ZeroMQ PUB socket."""
60 60
61 61 session = Instance(Session)
62 62 pub_socket = Instance(SocketABC)
63 63 parent_header = Dict({})
64 64 topic = CBytes(b'display_data')
65 65
66 66 def set_parent(self, parent):
67 67 """Set the parent for outbound messages."""
68 68 self.parent_header = extract_header(parent)
69 69
70 70 def _flush_streams(self):
71 71 """flush IO Streams prior to display"""
72 72 sys.stdout.flush()
73 73 sys.stderr.flush()
74 74
75 75 def publish(self, source, data, metadata=None):
76 76 self._flush_streams()
77 77 if metadata is None:
78 78 metadata = {}
79 79 self._validate_data(source, data, metadata)
80 80 content = {}
81 81 content['source'] = source
82 82 content['data'] = encode_images(data)
83 83 content['metadata'] = metadata
84 84 self.session.send(
85 85 self.pub_socket, u'display_data', json_clean(content),
86 86 parent=self.parent_header, ident=self.topic,
87 87 )
88 88
89 89 def clear_output(self, wait=False):
90 90 content = dict(wait=wait)
91 91
92 92 print('\r', file=sys.stdout, end='')
93 93 print('\r', file=sys.stderr, end='')
94 94 self._flush_streams()
95 95
96 96 self.session.send(
97 97 self.pub_socket, u'clear_output', content,
98 98 parent=self.parent_header, ident=self.topic,
99 99 )
100 100
101 101 @magics_class
102 102 class KernelMagics(Magics):
103 103 #------------------------------------------------------------------------
104 104 # Magic overrides
105 105 #------------------------------------------------------------------------
106 106 # Once the base class stops inheriting from magic, this code needs to be
107 107 # moved into a separate machinery as well. For now, at least isolate here
108 108 # the magics which this class needs to implement differently from the base
109 109 # class, or that are unique to it.
110 110
111 111 @line_magic
112 112 def doctest_mode(self, parameter_s=''):
113 113 """Toggle doctest mode on and off.
114 114
115 115 This mode is intended to make IPython behave as much as possible like a
116 116 plain Python shell, from the perspective of how its prompts, exceptions
117 117 and output look. This makes it easy to copy and paste parts of a
118 118 session into doctests. It does so by:
119 119
120 120 - Changing the prompts to the classic ``>>>`` ones.
121 121 - Changing the exception reporting mode to 'Plain'.
122 122 - Disabling pretty-printing of output.
123 123
124 124 Note that IPython also supports the pasting of code snippets that have
125 125 leading '>>>' and '...' prompts in them. This means that you can paste
126 126 doctests from files or docstrings (even if they have leading
127 127 whitespace), and the code will execute correctly. You can then use
128 128 '%history -t' to see the translated history; this will give you the
129 129 input after removal of all the leading prompts and whitespace, which
130 130 can be pasted back into an editor.
131 131
132 132 With these features, you can switch into this mode easily whenever you
133 133 need to do testing and changes to doctests, without having to leave
134 134 your existing IPython session.
135 135 """
136 136
137 137 from IPython.utils.ipstruct import Struct
138 138
139 139 # Shorthands
140 140 shell = self.shell
141 141 disp_formatter = self.shell.display_formatter
142 142 ptformatter = disp_formatter.formatters['text/plain']
143 143 # dstore is a data store kept in the instance metadata bag to track any
144 144 # changes we make, so we can undo them later.
145 145 dstore = shell.meta.setdefault('doctest_mode', Struct())
146 146 save_dstore = dstore.setdefault
147 147
148 148 # save a few values we'll need to recover later
149 149 mode = save_dstore('mode', False)
150 150 save_dstore('rc_pprint', ptformatter.pprint)
151 151 save_dstore('rc_active_types',disp_formatter.active_types)
152 152 save_dstore('xmode', shell.InteractiveTB.mode)
153 153
154 154 if mode == False:
155 155 # turn on
156 156 ptformatter.pprint = False
157 157 disp_formatter.active_types = ['text/plain']
158 158 shell.magic('xmode Plain')
159 159 else:
160 160 # turn off
161 161 ptformatter.pprint = dstore.rc_pprint
162 162 disp_formatter.active_types = dstore.rc_active_types
163 163 shell.magic("xmode " + dstore.xmode)
164 164
165 165 # Store new mode and inform on console
166 166 dstore.mode = bool(1-int(mode))
167 167 mode_label = ['OFF','ON'][dstore.mode]
168 168 print('Doctest mode is:', mode_label)
169 169
170 170 # Send the payload back so that clients can modify their prompt display
171 171 payload = dict(
172 172 source='doctest_mode',
173 173 mode=dstore.mode)
174 174 shell.payload_manager.write_payload(payload)
175 175
176 176
177 177 _find_edit_target = CodeMagics._find_edit_target
178 178
179 179 @skip_doctest
180 180 @line_magic
181 181 def edit(self, parameter_s='', last_call=['','']):
182 182 """Bring up an editor and execute the resulting code.
183 183
184 184 Usage:
185 185 %edit [options] [args]
186 186
187 187 %edit runs an external text editor. You will need to set the command for
188 188 this editor via the ``TerminalInteractiveShell.editor`` option in your
189 189 configuration file before it will work.
190 190
191 191 This command allows you to conveniently edit multi-line code right in
192 192 your IPython session.
193 193
194 194 If called without arguments, %edit opens up an empty editor with a
195 195 temporary file and will execute the contents of this file when you
196 196 close it (don't forget to save it!).
197 197
198 198
199 199 Options:
200 200
201 201 -n <number>: open the editor at a specified line number. By default,
202 202 the IPython editor hook uses the unix syntax 'editor +N filename', but
203 203 you can configure this by providing your own modified hook if your
204 204 favorite editor supports line-number specifications with a different
205 205 syntax.
206 206
207 207 -p: this will call the editor with the same data as the previous time
208 208 it was used, regardless of how long ago (in your current session) it
209 209 was.
210 210
211 211 -r: use 'raw' input. This option only applies to input taken from the
212 212 user's history. By default, the 'processed' history is used, so that
213 213 magics are loaded in their transformed version to valid Python. If
214 214 this option is given, the raw input as typed as the command line is
215 215 used instead. When you exit the editor, it will be executed by
216 216 IPython's own processor.
217 217
218 218 -x: do not execute the edited code immediately upon exit. This is
219 219 mainly useful if you are editing programs which need to be called with
220 220 command line arguments, which you can then do using %run.
221 221
222 222
223 223 Arguments:
224 224
225 225 If arguments are given, the following possibilites exist:
226 226
227 227 - The arguments are numbers or pairs of colon-separated numbers (like
228 228 1 4:8 9). These are interpreted as lines of previous input to be
229 229 loaded into the editor. The syntax is the same of the %macro command.
230 230
231 231 - If the argument doesn't start with a number, it is evaluated as a
232 232 variable and its contents loaded into the editor. You can thus edit
233 233 any string which contains python code (including the result of
234 234 previous edits).
235 235
236 236 - If the argument is the name of an object (other than a string),
237 237 IPython will try to locate the file where it was defined and open the
238 238 editor at the point where it is defined. You can use `%edit function`
239 239 to load an editor exactly at the point where 'function' is defined,
240 240 edit it and have the file be executed automatically.
241 241
242 242 If the object is a macro (see %macro for details), this opens up your
243 243 specified editor with a temporary file containing the macro's data.
244 244 Upon exit, the macro is reloaded with the contents of the file.
245 245
246 246 Note: opening at an exact line is only supported under Unix, and some
247 247 editors (like kedit and gedit up to Gnome 2.8) do not understand the
248 248 '+NUMBER' parameter necessary for this feature. Good editors like
249 249 (X)Emacs, vi, jed, pico and joe all do.
250 250
251 251 - If the argument is not found as a variable, IPython will look for a
252 252 file with that name (adding .py if necessary) and load it into the
253 253 editor. It will execute its contents with execfile() when you exit,
254 254 loading any code in the file into your interactive namespace.
255 255
256 256 After executing your code, %edit will return as output the code you
257 257 typed in the editor (except when it was an existing file). This way
258 258 you can reload the code in further invocations of %edit as a variable,
259 259 via _<NUMBER> or Out[<NUMBER>], where <NUMBER> is the prompt number of
260 260 the output.
261 261
262 262 Note that %edit is also available through the alias %ed.
263 263
264 264 This is an example of creating a simple function inside the editor and
265 265 then modifying it. First, start up the editor:
266 266
267 267 In [1]: ed
268 268 Editing... done. Executing edited code...
269 269 Out[1]: 'def foo():n print "foo() was defined in an editing session"n'
270 270
271 271 We can then call the function foo():
272 272
273 273 In [2]: foo()
274 274 foo() was defined in an editing session
275 275
276 276 Now we edit foo. IPython automatically loads the editor with the
277 277 (temporary) file where foo() was previously defined:
278 278
279 279 In [3]: ed foo
280 280 Editing... done. Executing edited code...
281 281
282 282 And if we call foo() again we get the modified version:
283 283
284 284 In [4]: foo()
285 285 foo() has now been changed!
286 286
287 287 Here is an example of how to edit a code snippet successive
288 288 times. First we call the editor:
289 289
290 290 In [5]: ed
291 291 Editing... done. Executing edited code...
292 292 hello
293 293 Out[5]: "print 'hello'n"
294 294
295 295 Now we call it again with the previous output (stored in _):
296 296
297 297 In [6]: ed _
298 298 Editing... done. Executing edited code...
299 299 hello world
300 300 Out[6]: "print 'hello world'n"
301 301
302 302 Now we call it with the output #8 (stored in _8, also as Out[8]):
303 303
304 304 In [7]: ed _8
305 305 Editing... done. Executing edited code...
306 306 hello again
307 307 Out[7]: "print 'hello again'n"
308 308 """
309 309
310 310 opts,args = self.parse_options(parameter_s,'prn:')
311 311
312 312 try:
313 313 filename, lineno, _ = CodeMagics._find_edit_target(self.shell, args, opts, last_call)
314 314 except MacroToEdit as e:
315 315 # TODO: Implement macro editing over 2 processes.
316 316 print("Macro editing not yet implemented in 2-process model.")
317 317 return
318 318
319 319 # Make sure we send to the client an absolute path, in case the working
320 320 # directory of client and kernel don't match
321 321 filename = os.path.abspath(filename)
322 322
323 323 payload = {
324 324 'source' : 'edit_magic',
325 325 'filename' : filename,
326 326 'line_number' : lineno
327 327 }
328 328 self.shell.payload_manager.write_payload(payload)
329 329
330 330 # A few magics that are adapted to the specifics of using pexpect and a
331 331 # remote terminal
332 332
333 333 @line_magic
334 334 def clear(self, arg_s):
335 335 """Clear the terminal."""
336 336 if os.name == 'posix':
337 337 self.shell.system("clear")
338 338 else:
339 339 self.shell.system("cls")
340 340
341 341 if os.name == 'nt':
342 342 # This is the usual name in windows
343 343 cls = line_magic('cls')(clear)
344 344
345 345 # Terminal pagers won't work over pexpect, but we do have our own pager
346 346
347 347 @line_magic
348 348 def less(self, arg_s):
349 349 """Show a file through the pager.
350 350
351 351 Files ending in .py are syntax-highlighted."""
352 352 if not arg_s:
353 353 raise UsageError('Missing filename.')
354 354
355 355 cont = open(arg_s).read()
356 356 if arg_s.endswith('.py'):
357 357 cont = self.shell.pycolorize(openpy.read_py_file(arg_s, skip_encoding_cookie=False))
358 358 else:
359 359 cont = open(arg_s).read()
360 360 page.page(cont)
361 361
362 362 more = line_magic('more')(less)
363 363
364 364 # Man calls a pager, so we also need to redefine it
365 365 if os.name == 'posix':
366 366 @line_magic
367 367 def man(self, arg_s):
368 368 """Find the man page for the given command and display in pager."""
369 369 page.page(self.shell.getoutput('man %s | col -b' % arg_s,
370 370 split=False))
371 371
372 372 @line_magic
373 373 def connect_info(self, arg_s):
374 374 """Print information for connecting other clients to this kernel
375 375
376 376 It will print the contents of this session's connection file, as well as
377 377 shortcuts for local clients.
378 378
379 379 In the simplest case, when called from the most recently launched kernel,
380 380 secondary clients can be connected, simply with:
381 381
382 382 $> ipython <app> --existing
383 383
384 384 """
385 385
386 386 from IPython.core.application import BaseIPythonApplication as BaseIPApp
387 387
388 388 if BaseIPApp.initialized():
389 389 app = BaseIPApp.instance()
390 390 security_dir = app.profile_dir.security_dir
391 391 profile = app.profile
392 392 else:
393 393 profile = 'default'
394 394 security_dir = ''
395 395
396 396 try:
397 397 connection_file = get_connection_file()
398 398 info = get_connection_info(unpack=False)
399 399 except Exception as e:
400 400 error("Could not get connection info: %r" % e)
401 401 return
402 402
403 403 # add profile flag for non-default profile
404 404 profile_flag = "--profile %s" % profile if profile != 'default' else ""
405 405
406 406 # if it's in the security dir, truncate to basename
407 407 if security_dir == os.path.dirname(connection_file):
408 408 connection_file = os.path.basename(connection_file)
409 409
410 410
411 411 print (info + '\n')
412 412 print ("Paste the above JSON into a file, and connect with:\n"
413 413 " $> ipython <app> --existing <file>\n"
414 414 "or, if you are local, you can connect with just:\n"
415 415 " $> ipython <app> --existing {0} {1}\n"
416 416 "or even just:\n"
417 417 " $> ipython <app> --existing {1}\n"
418 418 "if this is the most recent IPython session you have started.".format(
419 419 connection_file, profile_flag
420 420 )
421 421 )
422 422
423 423 @line_magic
424 424 def qtconsole(self, arg_s):
425 425 """Open a qtconsole connected to this kernel.
426 426
427 427 Useful for connecting a qtconsole to running notebooks, for better
428 428 debugging.
429 429 """
430 430
431 431 # %qtconsole should imply bind_kernel for engines:
432 432 try:
433 433 from IPython.parallel import bind_kernel
434 434 except ImportError:
435 435 # technically possible, because parallel has higher pyzmq min-version
436 436 pass
437 437 else:
438 438 bind_kernel()
439 439
440 440 try:
441 441 p = connect_qtconsole(argv=arg_split(arg_s, os.name=='posix'))
442 442 except Exception as e:
443 443 error("Could not start qtconsole: %r" % e)
444 444 return
445 445
446 446 @line_magic
447 447 def autosave(self, arg_s):
448 448 """Set the autosave interval in the notebook (in seconds).
449 449
450 450 The default value is 120, or two minutes.
451 451 ``%autosave 0`` will disable autosave.
452 452
453 453 This magic only has an effect when called from the notebook interface.
454 454 It has no effect when called in a startup file.
455 455 """
456 456
457 457 try:
458 458 interval = int(arg_s)
459 459 except ValueError:
460 460 raise UsageError("%%autosave requires an integer, got %r" % arg_s)
461 461
462 462 # javascript wants milliseconds
463 463 milliseconds = 1000 * interval
464 464 display(Javascript("IPython.notebook.set_autosave_interval(%i)" % milliseconds),
465 465 include=['application/javascript']
466 466 )
467 467 if interval:
468 468 print("Autosaving every %i seconds" % interval)
469 469 else:
470 470 print("Autosave disabled")
471 471
472 472
473 473 class ZMQInteractiveShell(InteractiveShell):
474 474 """A subclass of InteractiveShell for ZMQ."""
475 475
476 476 displayhook_class = Type(ZMQShellDisplayHook)
477 477 display_pub_class = Type(ZMQDisplayPublisher)
478 478 data_pub_class = Type(ZMQDataPublisher)
479 479
480 480 # Override the traitlet in the parent class, because there's no point using
481 481 # readline for the kernel. Can be removed when the readline code is moved
482 482 # to the terminal frontend.
483 483 colors_force = CBool(True)
484 484 readline_use = CBool(False)
485 485 # autoindent has no meaning in a zmqshell, and attempting to enable it
486 486 # will print a warning in the absence of readline.
487 487 autoindent = CBool(False)
488 488
489 489 exiter = Instance(ZMQExitAutocall)
490 490 def _exiter_default(self):
491 491 return ZMQExitAutocall(self)
492 492
493 493 def _exit_now_changed(self, name, old, new):
494 494 """stop eventloop when exit_now fires"""
495 495 if new:
496 496 loop = ioloop.IOLoop.instance()
497 497 loop.add_timeout(time.time()+0.1, loop.stop)
498 498
499 499 keepkernel_on_exit = None
500 500
501 501 # Over ZeroMQ, GUI control isn't done with PyOS_InputHook as there is no
502 502 # interactive input being read; we provide event loop support in ipkernel
503 503 @staticmethod
504 504 def enable_gui(gui):
505 505 from .eventloops import enable_gui as real_enable_gui
506 506 try:
507 507 real_enable_gui(gui)
508 508 except ValueError as e:
509 509 raise UsageError("%s" % e)
510 510
511 511 def init_environment(self):
512 512 """Configure the user's environment.
513 513
514 514 """
515 515 env = os.environ
516 516 # These two ensure 'ls' produces nice coloring on BSD-derived systems
517 517 env['TERM'] = 'xterm-color'
518 518 env['CLICOLOR'] = '1'
519 519 # Since normal pagers don't work at all (over pexpect we don't have
520 520 # single-key control of the subprocess), try to disable paging in
521 521 # subprocesses as much as possible.
522 522 env['PAGER'] = 'cat'
523 523 env['GIT_PAGER'] = 'cat'
524 524
525 525 # And install the payload version of page.
526 526 install_payload_page()
527 527
528 528 def auto_rewrite_input(self, cmd):
529 529 """Called to show the auto-rewritten input for autocall and friends.
530 530
531 531 FIXME: this payload is currently not correctly processed by the
532 532 frontend.
533 533 """
534 534 new = self.prompt_manager.render('rewrite') + cmd
535 535 payload = dict(
536 536 source='auto_rewrite_input',
537 537 transformed_input=new,
538 538 )
539 539 self.payload_manager.write_payload(payload)
540 540
541 541 def ask_exit(self):
542 542 """Engage the exit actions."""
543 543 self.exit_now = True
544 544 payload = dict(
545 545 source='ask_exit',
546 546 exit=True,
547 547 keepkernel=self.keepkernel_on_exit,
548 548 )
549 549 self.payload_manager.write_payload(payload)
550 550
551 551 def _showtraceback(self, etype, evalue, stb):
552 552
553 553 exc_content = {
554 554 u'traceback' : stb,
555 555 u'ename' : unicode(etype.__name__),
556 556 u'evalue' : py3compat.safe_unicode(evalue),
557 557 }
558 558
559 559 dh = self.displayhook
560 560 # Send exception info over pub socket for other clients than the caller
561 561 # to pick up
562 562 topic = None
563 563 if dh.topic:
564 564 topic = dh.topic.replace(b'pyout', b'pyerr')
565 565
566 566 exc_msg = dh.session.send(dh.pub_socket, u'pyerr', json_clean(exc_content), dh.parent_header, ident=topic)
567 567
568 568 # FIXME - Hack: store exception info in shell object. Right now, the
569 569 # caller is reading this info after the fact, we need to fix this logic
570 570 # to remove this hack. Even uglier, we need to store the error status
571 571 # here, because in the main loop, the logic that sets it is being
572 572 # skipped because runlines swallows the exceptions.
573 573 exc_content[u'status'] = u'error'
574 574 self._reply_content = exc_content
575 575 # /FIXME
576 576
577 577 return exc_content
578 578
579 579 def set_next_input(self, text):
580 580 """Send the specified text to the frontend to be presented at the next
581 581 input cell."""
582 582 payload = dict(
583 583 source='set_next_input',
584 584 text=text
585 585 )
586 self.payload_manager.write_payload(payload, update=True)
586 self.payload_manager.write_payload(payload)
587 587
588 588 #-------------------------------------------------------------------------
589 589 # Things related to magics
590 590 #-------------------------------------------------------------------------
591 591
592 592 def init_magics(self):
593 593 super(ZMQInteractiveShell, self).init_magics()
594 594 self.register_magics(KernelMagics)
595 595 self.magics_manager.register_alias('ed', 'edit')
596 596
597 597
598 598
599 599 InteractiveShellABC.register(ZMQInteractiveShell)
General Comments 0
You need to be logged in to leave comments. Login now