Skip to content
This repository

Pylab fix #1052

Merged
merged 9 commits into from over 2 years ago

2 participants

Fernando Perez Min RK
Fernando Perez
Owner

When merging #648, we inadvertently broke pretty badly the pylab support. I added a test to catch the problem and started fixing things, but quickly saw we had a pretty messy setup in our pylab support machinery, just the product of the fast development we had during the creation of the qt console and zeromq support. The only way to fix this cleanly was to refactor things a little bit, but I think it's in decent shape now.

The main problem is that we had a ton of code duplication, with several methods implemented twice in the terminal and zmq shells, but with actually minimal differences. But the duplication meant that fixing a but required hunting down a lot of stuff around.

Now all duplicate code is gone, and the only real difference is how gui event loops are integrated, which is reduced to a single static method that each relevant class grabs from its specific machinery.

Merging this is, however, very urgent because right now master is fully broken. I'm going to see if @minrk is available on IRC and can have a quick look. If not, I'll merge it immediately, but having the pull request helps to easily see what I did in isolation, and we can do a post-merge reivew. I did verify that it fixes the problem we had, added new tests, and checked that both the qt console and the notebook work fine.

added some commits
Fernando Perez Fix critical bug with pylab support inadvertently introduced in #648.
code used it as a dict.  Updated that code to handle a dict correctly,
and added tests to catch this issue in the future (also increases test
coverage of pylab code).
cd84e0e
Fernando Perez Refactor gui/pylab integration to eliminate code duplication.
Also, fix a few tests that the previous commit broke.
cfd87e9
Fernando Perez Move zmq event loop support into a separate file.
This avoids a circular import problem, and also organizes more cleanly
the code that is event-loop specific.
0e21764
Fernando Perez Minor cleanups after a check with pyflakes of the refactored code. 5c4e9a0
Fernando Perez Add missing file. 37afbb3
IPython/core/pylabtools.py
((53 lines not shown))
  261
+    shell : InteractiveShell instance
  262
+      If None, this function returns immediately.
  263
+
  264
+    user_ns : dict
  265
+      A namespace where all configured variables will be placed.  If not given,
  266
+      the `user_ns` attribute of the shell object is used.
  267
+    """
  268
+    if shell is None:
  269
+        return
  270
+
  271
+    user_ns = shell.user_ns if user_ns is None else user_ns
  272
+    
  273
+    # If using our svg payload backend, register the post-execution
  274
+    # function that will pick up the results for display.  This can only be
  275
+    # done with access to the real shell object.
  276
+    from IPython.zmq.pylab.backend_inline import InlineBackend
1
Min RK Owner
minrk added a note

This will make ipython --pylab fail in the absence of zmq.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
IPython/core/pylabtools.py
((45 lines not shown))
290  
-            exec s in shell.user_ns_hidden
  254
+
  255
+
  256
+def configure_shell(shell, backend, user_ns=None):
  257
+    """Configure an IPython shell object for matplotlib use.
  258
+
  259
+    Parameters
  260
+    ----------
  261
+    shell : InteractiveShell instance
  262
+      If None, this function returns immediately.
  263
+
  264
+    user_ns : dict
  265
+      A namespace where all configured variables will be placed.  If not given,
  266
+      the `user_ns` attribute of the shell object is used.
  267
+    """
  268
+    if shell is None:
1
Min RK Owner
minrk added a note

I would just not call this function if shell is None - no need for it to conditionally do nothing if it was given inappropriate args.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Fernando Perez
Owner

Merging now after detailed review with @minrk on IRC. If anyone else spots any issues let me know, and I'm happy to revisit. But at least @minrk did have a careful look, and he spotted a number of problems (all fixed now).

Fernando Perez fperez merged commit f15123a into from
Fernando Perez fperez closed this
Matthias Bussonnier Carreau referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Fernando Perez fperez referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Fernando Perez fperez referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 9 unique commits by 1 author.

Nov 26, 2011
Fernando Perez Fix critical bug with pylab support inadvertently introduced in #648.
code used it as a dict.  Updated that code to handle a dict correctly,
and added tests to catch this issue in the future (also increases test
coverage of pylab code).
cd84e0e
Fernando Perez Refactor gui/pylab integration to eliminate code duplication.
Also, fix a few tests that the previous commit broke.
cfd87e9
Fernando Perez Move zmq event loop support into a separate file.
This avoids a circular import problem, and also organizes more cleanly
the code that is event-loop specific.
0e21764
Fernando Perez Minor cleanups after a check with pyflakes of the refactored code. 5c4e9a0
Fernando Perez Add missing file. 37afbb3
Nov 27, 2011
Fernando Perez Only import zeromq pieces in networked shells. cf5afdd
Fernando Perez Fix inline backend logic and avoid tests if mpl not available. a2fcd13
Fernando Perez Avoid calling inline config if no shell - per @minrk feedback on #1052 36e96e8
Fernando Perez fix docstring 3f11378
This page is out of date. Refresh to see the latest.
41  IPython/core/interactiveshell.py
@@ -62,6 +62,7 @@
62 62
 from IPython.core.plugin import PluginManager
63 63
 from IPython.core.prefilter import PrefilterManager, ESC_MAGIC
64 64
 from IPython.core.profiledir import ProfileDir
  65
+from IPython.core.pylabtools import pylab_activate
65 66
 from IPython.external.Itpl import ItplNS
66 67
 from IPython.utils import PyColorize
67 68
 from IPython.utils import io
@@ -2531,8 +2532,46 @@ def run_code(self, code_obj):
2531 2532
     # Things related to GUI support and pylab
2532 2533
     #-------------------------------------------------------------------------
2533 2534
 
  2535
+    def enable_gui(self, gui=None):
  2536
+        raise NotImplementedError('Implement enable_gui in a subclass')
  2537
+
2534 2538
     def enable_pylab(self, gui=None, import_all=True):
2535  
-        raise NotImplementedError('Implement enable_pylab in a subclass')
  2539
+        """Activate pylab support at runtime.
  2540
+
  2541
+        This turns on support for matplotlib, preloads into the interactive
  2542
+        namespace all of numpy and pylab, and configures IPython to correctly
  2543
+        interact with the GUI event loop.  The GUI backend to be used can be
  2544
+        optionally selected with the optional :param:`gui` argument.
  2545
+
  2546
+        Parameters
  2547
+        ----------
  2548
+        gui : optional, string
  2549
+
  2550
+          If given, dictates the choice of matplotlib GUI backend to use
  2551
+          (should be one of IPython's supported backends, 'qt', 'osx', 'tk',
  2552
+          'gtk', 'wx' or 'inline'), otherwise we use the default chosen by
  2553
+          matplotlib (as dictated by the matplotlib build-time options plus the
  2554
+          user's matplotlibrc configuration file).  Note that not all backends
  2555
+          make sense in all contexts, for example a terminal ipython can't
  2556
+          display figures inline.
  2557
+        """
  2558
+
  2559
+        # We want to prevent the loading of pylab to pollute the user's
  2560
+        # namespace as shown by the %who* magics, so we execute the activation
  2561
+        # code in an empty namespace, and we update *both* user_ns and
  2562
+        # user_ns_hidden with this information.
  2563
+        ns = {}
  2564
+        try:
  2565
+            gui = pylab_activate(ns, gui, import_all, self)
  2566
+        except KeyError:
  2567
+            error("Backend %r not supported" % gui)
  2568
+            return
  2569
+        self.user_ns.update(ns)
  2570
+        self.user_ns_hidden.update(ns)
  2571
+        # Now we must activate the gui pylab wants to use, and fix %run to take
  2572
+        # plot updates into account
  2573
+        self.enable_gui(gui)
  2574
+        self.magic_run = self._pylab_magic_run
2536 2575
 
2537 2576
     #-------------------------------------------------------------------------
2538 2577
     # Utilities
24  IPython/core/magic.py
@@ -50,7 +50,7 @@
50 50
 from IPython.core.macro import Macro
51 51
 from IPython.core import magic_arguments, page
52 52
 from IPython.core.prefilter import ESC_MAGIC
53  
-from IPython.lib.pylabtools import mpl_runner
  53
+from IPython.core.pylabtools import mpl_runner
54 54
 from IPython.testing.skipdoctest import skip_doctest
55 55
 from IPython.utils import py3compat
56 56
 from IPython.utils.io import file_read, nlprint
@@ -3305,24 +3305,30 @@ def magic_gui(self, parameter_s=''):
3305 3305
 
3306 3306
         This magic replaces IPython's threaded shells that were activated
3307 3307
         using the (pylab/wthread/etc.) command line flags.  GUI toolkits
3308  
-        can now be enabled, disabled and changed at runtime and keyboard
  3308
+        can now be enabled at runtime and keyboard
3309 3309
         interrupts should work without any problems.  The following toolkits
3310  
-        are supported:  wxPython, PyQt4, PyGTK, and Tk::
  3310
+        are supported:  wxPython, PyQt4, PyGTK, Tk and Cocoa (OSX)::
3311 3311
 
3312 3312
             %gui wx      # enable wxPython event loop integration
3313 3313
             %gui qt4|qt  # enable PyQt4 event loop integration
3314 3314
             %gui gtk     # enable PyGTK event loop integration
3315 3315
             %gui tk      # enable Tk event loop integration
  3316
+            %gui OSX     # enable Cocoa event loop integration
  3317
+                         # (requires %matplotlib 1.1) 
3316 3318
             %gui         # disable all event loop integration
3317 3319
 
3318 3320
         WARNING:  after any of these has been called you can simply create
3319 3321
         an application object, but DO NOT start the event loop yourself, as
3320 3322
         we have already handled that.
3321 3323
         """
3322  
-        from IPython.lib.inputhook import enable_gui
3323 3324
         opts, arg = self.parse_options(parameter_s, '')
3324 3325
         if arg=='': arg = None
3325  
-        return enable_gui(arg)
  3326
+        try:
  3327
+            return self.enable_gui(arg)
  3328
+        except Exception as e:
  3329
+            # print simple error message, rather than traceback if we can't
  3330
+            # hook up the GUI
  3331
+            error(str(e))
3326 3332
 
3327 3333
     def magic_load_ext(self, module_str):
3328 3334
         """Load an IPython extension by its module name."""
@@ -3416,9 +3422,9 @@ def magic_pylab(self, s):
3416 3422
         Parameters
3417 3423
         ----------
3418 3424
         guiname : optional
3419  
-          One of the valid arguments to the %gui magic ('qt', 'wx', 'gtk', 'osx' or
3420  
-          'tk').  If given, the corresponding Matplotlib backend is used,
3421  
-          otherwise matplotlib's default (which you can override in your
  3425
+          One of the valid arguments to the %gui magic ('qt', 'wx', 'gtk',
  3426
+          'osx' or 'tk').  If given, the corresponding Matplotlib backend is
  3427
+          used, otherwise matplotlib's default (which you can override in your
3422 3428
           matplotlib config file) is used.
3423 3429
 
3424 3430
         Examples
@@ -3449,7 +3455,7 @@ def magic_pylab(self, s):
3449 3455
         else:
3450 3456
             import_all_status = True
3451 3457
 
3452  
-        self.shell.enable_pylab(s,import_all=import_all_status)
  3458
+        self.shell.enable_pylab(s, import_all=import_all_status)
3453 3459
 
3454 3460
     def magic_tb(self, s):
3455 3461
         """Print the last traceback with the currently active exception mode.
99  IPython/lib/pylabtools.py → IPython/core/pylabtools.py
@@ -232,7 +232,8 @@ def activate_matplotlib(backend):
232 232
     # For this, we wrap it into a decorator which adds a 'called' flag.
233 233
     pylab.draw_if_interactive = flag_calls(pylab.draw_if_interactive)
234 234
 
235  
-def import_pylab(user_ns, backend, import_all=True, shell=None):
  235
+
  236
+def import_pylab(user_ns, import_all=True):
236 237
     """Import the standard pylab symbols into user_ns."""
237 238
 
238 239
     # Import numpy as np/pyplot as plt are conventions we're trying to
@@ -246,48 +247,62 @@ def import_pylab(user_ns, backend, import_all=True, shell=None):
246 247
           )
247 248
     exec s in user_ns
248 249
 
249  
-    if shell is not None:
250  
-        exec s in shell.user_ns_hidden
251  
-        # If using our svg payload backend, register the post-execution
252  
-        # function that will pick up the results for display.  This can only be
253  
-        # done with access to the real shell object.
254  
-        #
255  
-        from IPython.zmq.pylab.backend_inline import InlineBackend
256  
-
257  
-        cfg = InlineBackend.instance(config=shell.config)
258  
-        cfg.shell = shell
259  
-        if cfg not in shell.configurables:
260  
-            shell.configurables.append(cfg)
261  
-
262  
-        if backend == backends['inline']:
263  
-            from IPython.zmq.pylab.backend_inline import flush_figures
264  
-            from matplotlib import pyplot
265  
-            shell.register_post_execute(flush_figures)
266  
-            # load inline_rc
267  
-            pyplot.rcParams.update(cfg.rc)
268  
-            
269  
-            # Add 'figsize' to pyplot and to the user's namespace
270  
-            user_ns['figsize'] = pyplot.figsize = figsize
271  
-            shell.user_ns_hidden['figsize'] = figsize
272  
-        
273  
-        # Setup the default figure format
274  
-        fmt = cfg.figure_format
275  
-        select_figure_format(shell, fmt)
276  
-
277  
-        # The old pastefig function has been replaced by display
278  
-        from IPython.core.display import display
279  
-        # Add display and display_png to the user's namespace
280  
-        user_ns['display'] = display
281  
-        shell.user_ns_hidden['display'] = display
282  
-        user_ns['getfigs'] = getfigs
283  
-        shell.user_ns_hidden['getfigs'] = getfigs
284  
-
285 250
     if import_all:
286 251
         s = ("from matplotlib.pylab import *\n"
287 252
              "from numpy import *\n")
288 253
         exec s in user_ns
289  
-        if shell is not None:
290  
-            exec s in shell.user_ns_hidden
  254
+
  255
+
  256
+def configure_inline_support(shell, backend, user_ns=None):
  257
+    """Configure an IPython shell object for matplotlib use.
  258
+
  259
+    Parameters
  260
+    ----------
  261
+    shell : InteractiveShell instance
  262
+
  263
+    backend : matplotlib backend
  264
+
  265
+    user_ns : dict
  266
+      A namespace where all configured variables will be placed.  If not given,
  267
+      the `user_ns` attribute of the shell object is used.
  268
+    """
  269
+    # If using our svg payload backend, register the post-execution
  270
+    # function that will pick up the results for display.  This can only be
  271
+    # done with access to the real shell object.
  272
+
  273
+    # Note: if we can't load the inline backend, then there's no point
  274
+    # continuing (such as in terminal-only shells in environments without
  275
+    # zeromq available).
  276
+    try:
  277
+        from IPython.zmq.pylab.backend_inline import InlineBackend
  278
+    except ImportError:
  279
+        return
  280
+
  281
+    user_ns = shell.user_ns if user_ns is None else user_ns
  282
+    
  283
+    cfg = InlineBackend.instance(config=shell.config)
  284
+    cfg.shell = shell
  285
+    if cfg not in shell.configurables:
  286
+        shell.configurables.append(cfg)
  287
+
  288
+    if backend == backends['inline']:
  289
+        from IPython.zmq.pylab.backend_inline import flush_figures
  290
+        from matplotlib import pyplot
  291
+        shell.register_post_execute(flush_figures)
  292
+        # load inline_rc
  293
+        pyplot.rcParams.update(cfg.rc)
  294
+        # Add 'figsize' to pyplot and to the user's namespace
  295
+        user_ns['figsize'] = pyplot.figsize = figsize
  296
+
  297
+    # Setup the default figure format
  298
+    fmt = cfg.figure_format
  299
+    select_figure_format(shell, fmt)
  300
+
  301
+    # The old pastefig function has been replaced by display
  302
+    from IPython.core.display import display
  303
+    # Add display and getfigs to the user's namespace
  304
+    user_ns['display'] = display
  305
+    user_ns['getfigs'] = getfigs
291 306
 
292 307
 
293 308
 def pylab_activate(user_ns, gui=None, import_all=True, shell=None):
@@ -313,8 +328,10 @@ def pylab_activate(user_ns, gui=None, import_all=True, shell=None):
313 328
     """
314 329
     gui, backend = find_gui_and_backend(gui)
315 330
     activate_matplotlib(backend)
316  
-    import_pylab(user_ns, backend, import_all, shell)
317  
-
  331
+    import_pylab(user_ns, import_all)
  332
+    if shell is not None:
  333
+        configure_inline_support(shell, backend, user_ns)
  334
+        
318 335
     print """
319 336
 Welcome to pylab, a matplotlib-based Python environment [backend: %s].
320 337
 For more information, type 'help(pylab)'.""" % backend
9  IPython/lib/tests/test_pylabtools.py → IPython/core/tests/test_pylabtools.py
@@ -20,6 +20,7 @@
20 20
 import nose.tools as nt
21 21
 
22 22
 from matplotlib import pyplot as plt
  23
+import numpy as np
23 24
 
24 25
 # Our own imports
25 26
 from IPython.testing import decorators as dec
@@ -52,3 +53,11 @@ def test_figure_to_svg():
52 53
     plt.draw()
53 54
     svg = pt.print_figure(fig, 'svg')[:100].lower()
54 55
     yield nt.assert_true('doctype svg' in svg)
  56
+
  57
+
  58
+def test_import_pylab():
  59
+    ip = get_ipython()
  60
+    ns = {}
  61
+    pt.import_pylab(ns, import_all=False)
  62
+    nt.assert_true('plt' in ns)
  63
+    nt.assert_equal(ns['np'], np)
53  IPython/frontend/terminal/interactiveshell.py
@@ -29,8 +29,7 @@
29 29
 from IPython.core.error import TryNext
30 30
 from IPython.core.usage import interactive_usage, default_banner
31 31
 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
32  
-from IPython.lib.inputhook import enable_gui
33  
-from IPython.lib.pylabtools import pylab_activate
  32
+from IPython.core.pylabtools import pylab_activate
34 33
 from IPython.testing.skipdoctest import skip_doctest
35 34
 from IPython.utils import py3compat
36 35
 from IPython.utils.terminal import toggle_set_term_title, set_term_title
@@ -171,10 +170,13 @@ class TerminalInteractiveShell(InteractiveShell):
171 170
         help="Enable auto setting the terminal title."
172 171
     )
173 172
 
174  
-    def __init__(self, config=None, ipython_dir=None, profile_dir=None, user_ns=None,
175  
-                 user_module=None, custom_exceptions=((),None),
176  
-                 usage=None, banner1=None, banner2=None,
177  
-                 display_banner=None):
  173
+    # In the terminal, GUI control is done via PyOS_InputHook
  174
+    from IPython.lib.inputhook import enable_gui
  175
+    enable_gui = staticmethod(enable_gui)
  176
+    
  177
+    def __init__(self, config=None, ipython_dir=None, profile_dir=None,
  178
+                 user_ns=None, user_module=None, custom_exceptions=((),None),
  179
+                 usage=None, banner1=None, banner2=None, display_banner=None):
178 180
 
179 181
         super(TerminalInteractiveShell, self).__init__(
180 182
             config=config, profile_dir=profile_dir, user_ns=user_ns,
@@ -517,45 +519,6 @@ def int0(x):
517 519
         return True
518 520
 
519 521
     #-------------------------------------------------------------------------
520  
-    # Things related to GUI support and pylab
521  
-    #-------------------------------------------------------------------------
522  
-
523  
-    def enable_pylab(self, gui=None, import_all=True):
524  
-        """Activate pylab support at runtime.
525  
-
526  
-        This turns on support for matplotlib, preloads into the interactive
527  
-        namespace all of numpy and pylab, and configures IPython to correcdtly
528  
-        interact with the GUI event loop.  The GUI backend to be used can be
529  
-        optionally selected with the optional :param:`gui` argument.
530  
-
531  
-        Parameters
532  
-        ----------
533  
-        gui : optional, string
534  
-
535  
-          If given, dictates the choice of matplotlib GUI backend to use
536  
-          (should be one of IPython's supported backends, 'tk', 'qt', 'wx' or
537  
-          'gtk'), otherwise we use the default chosen by matplotlib (as
538  
-          dictated by the matplotlib build-time options plus the user's
539  
-          matplotlibrc configuration file).
540  
-        """
541  
-        # We want to prevent the loading of pylab to pollute the user's
542  
-        # namespace as shown by the %who* magics, so we execute the activation
543  
-        # code in an empty namespace, and we update *both* user_ns and
544  
-        # user_ns_hidden with this information.
545  
-        ns = {}
546  
-        try:
547  
-            gui = pylab_activate(ns, gui, import_all)
548  
-        except KeyError:
549  
-            error("Backend %r not supported" % gui)
550  
-            return
551  
-        self.user_ns.update(ns)
552  
-        self.user_ns_hidden.update(ns)
553  
-        # Now we must activate the gui pylab wants to use, and fix %run to take
554  
-        # plot updates into account
555  
-        enable_gui(gui)
556  
-        self.magic_run = self._pylab_magic_run
557  
-
558  
-    #-------------------------------------------------------------------------
559 522
     # Things related to exiting
560 523
     #-------------------------------------------------------------------------
561 524
 
4  IPython/testing/iptest.py
@@ -238,8 +238,8 @@ def make_exclude():
238 238
         exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
239 239
 
240 240
     if not have['matplotlib']:
241  
-        exclusions.extend([ipjoin('lib', 'pylabtools'),
242  
-                           ipjoin('lib', 'tests', 'test_pylabtools')])
  241
+        exclusions.extend([ipjoin('core', 'pylabtools'),
  242
+                           ipjoin('core', 'tests', 'test_pylabtools')])
243 243
 
244 244
     if not have['tornado']:
245 245
         exclusions.append(ipjoin('frontend', 'html'))
206  IPython/zmq/eventloops.py
... ...
@@ -0,0 +1,206 @@
  1
+# encoding: utf-8
  2
+"""Event loop integration for the ZeroMQ-based kernels.
  3
+"""
  4
+
  5
+#-----------------------------------------------------------------------------
  6
+#  Copyright (C) 2011  The IPython Development Team
  7
+
  8
+#  Distributed under the terms of the BSD License.  The full license is in
  9
+#  the file COPYING, distributed as part of this software.
  10
+#-----------------------------------------------------------------------------
  11
+
  12
+
  13
+#-----------------------------------------------------------------------------
  14
+# Imports
  15
+#-----------------------------------------------------------------------------
  16
+
  17
+import sys
  18
+
  19
+# System library imports.
  20
+import zmq
  21
+
  22
+# Local imports.
  23
+from IPython.utils import io
  24
+
  25
+#------------------------------------------------------------------------------
  26
+# Eventloops for integrating the Kernel into different GUIs
  27
+#------------------------------------------------------------------------------
  28
+
  29
+def loop_qt4(kernel):
  30
+    """Start a kernel with PyQt4 event loop integration."""
  31
+
  32
+    from IPython.external.qt_for_kernel import QtCore
  33
+    from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4
  34
+
  35
+    kernel.app = get_app_qt4([" "])
  36
+    kernel.app.setQuitOnLastWindowClosed(False)
  37
+    kernel.timer = QtCore.QTimer()
  38
+    kernel.timer.timeout.connect(kernel.do_one_iteration)
  39
+    # Units for the timer are in milliseconds
  40
+    kernel.timer.start(1000*kernel._poll_interval)
  41
+    start_event_loop_qt4(kernel.app)
  42
+
  43
+
  44
+def loop_wx(kernel):
  45
+    """Start a kernel with wx event loop support."""
  46
+
  47
+    import wx
  48
+    from IPython.lib.guisupport import start_event_loop_wx
  49
+
  50
+    doi = kernel.do_one_iteration
  51
+     # Wx uses milliseconds
  52
+    poll_interval = int(1000*kernel._poll_interval)
  53
+
  54
+    # We have to put the wx.Timer in a wx.Frame for it to fire properly.
  55
+    # We make the Frame hidden when we create it in the main app below.
  56
+    class TimerFrame(wx.Frame):
  57
+        def __init__(self, func):
  58
+            wx.Frame.__init__(self, None, -1)
  59
+            self.timer = wx.Timer(self)
  60
+            # Units for the timer are in milliseconds
  61
+            self.timer.Start(poll_interval)
  62
+            self.Bind(wx.EVT_TIMER, self.on_timer)
  63
+            self.func = func
  64
+
  65
+        def on_timer(self, event):
  66
+            self.func()
  67
+
  68
+    # We need a custom wx.App to create our Frame subclass that has the
  69
+    # wx.Timer to drive the ZMQ event loop.
  70
+    class IPWxApp(wx.App):
  71
+        def OnInit(self):
  72
+            self.frame = TimerFrame(doi)
  73
+            self.frame.Show(False)
  74
+            return True
  75
+
  76
+    # The redirect=False here makes sure that wx doesn't replace
  77
+    # sys.stdout/stderr with its own classes.
  78
+    kernel.app = IPWxApp(redirect=False)
  79
+    start_event_loop_wx(kernel.app)
  80
+
  81
+
  82
+def loop_tk(kernel):
  83
+    """Start a kernel with the Tk event loop."""
  84
+
  85
+    import Tkinter
  86
+    doi = kernel.do_one_iteration
  87
+    # Tk uses milliseconds
  88
+    poll_interval = int(1000*kernel._poll_interval)
  89
+    # For Tkinter, we create a Tk object and call its withdraw method.
  90
+    class Timer(object):
  91
+        def __init__(self, func):
  92
+            self.app = Tkinter.Tk()
  93
+            self.app.withdraw()
  94
+            self.func = func
  95
+
  96
+        def on_timer(self):
  97
+            self.func()
  98
+            self.app.after(poll_interval, self.on_timer)
  99
+
  100
+        def start(self):
  101
+            self.on_timer()  # Call it once to get things going.
  102
+            self.app.mainloop()
  103
+
  104
+    kernel.timer = Timer(doi)
  105
+    kernel.timer.start()
  106
+
  107
+
  108
+def loop_gtk(kernel):
  109
+    """Start the kernel, coordinating with the GTK event loop"""
  110
+    from .gui.gtkembed import GTKEmbed
  111
+
  112
+    gtk_kernel = GTKEmbed(kernel)
  113
+    gtk_kernel.start()
  114
+
  115
+
  116
+def loop_cocoa(kernel):
  117
+    """Start the kernel, coordinating with the Cocoa CFRunLoop event loop
  118
+    via the matplotlib MacOSX backend.
  119
+    """
  120
+    import matplotlib
  121
+    if matplotlib.__version__ < '1.1.0':
  122
+        kernel.log.warn(
  123
+        "MacOSX backend in matplotlib %s doesn't have a Timer, "
  124
+        "falling back on Tk for CFRunLoop integration.  Note that "
  125
+        "even this won't work if Tk is linked against X11 instead of "
  126
+        "Cocoa (e.g. EPD).  To use the MacOSX backend in the kernel, "
  127
+        "you must use matplotlib >= 1.1.0, or a native libtk."
  128
+        )
  129
+        return loop_tk(kernel)
  130
+    
  131
+    from matplotlib.backends.backend_macosx import TimerMac, show
  132
+
  133
+    # scale interval for sec->ms
  134
+    poll_interval = int(1000*kernel._poll_interval)
  135
+
  136
+    real_excepthook = sys.excepthook
  137
+    def handle_int(etype, value, tb):
  138
+        """don't let KeyboardInterrupts look like crashes"""
  139
+        if etype is KeyboardInterrupt:
  140
+            io.raw_print("KeyboardInterrupt caught in CFRunLoop")
  141
+        else:
  142
+            real_excepthook(etype, value, tb)
  143
+
  144
+    # add doi() as a Timer to the CFRunLoop
  145
+    def doi():
  146
+        # restore excepthook during IPython code
  147
+        sys.excepthook = real_excepthook
  148
+        kernel.do_one_iteration()
  149
+        # and back:
  150
+        sys.excepthook = handle_int
  151
+
  152
+    t = TimerMac(poll_interval)
  153
+    t.add_callback(doi)
  154
+    t.start()
  155
+
  156
+    # but still need a Poller for when there are no active windows,
  157
+    # during which time mainloop() returns immediately
  158
+    poller = zmq.Poller()
  159
+    poller.register(kernel.shell_socket, zmq.POLLIN)
  160
+
  161
+    while True:
  162
+        try:
  163
+            # double nested try/except, to properly catch KeyboardInterrupt
  164
+            # due to pyzmq Issue #130
  165
+            try:
  166
+                # don't let interrupts during mainloop invoke crash_handler:
  167
+                sys.excepthook = handle_int
  168
+                show.mainloop()
  169
+                sys.excepthook = real_excepthook
  170
+                # use poller if mainloop returned (no windows)
  171
+                # scale by extra factor of 10, since it's a real poll
  172
+                poller.poll(10*poll_interval)
  173
+                kernel.do_one_iteration()
  174
+            except:
  175
+                raise
  176
+        except KeyboardInterrupt:
  177
+            # Ctrl-C shouldn't crash the kernel
  178
+            io.raw_print("KeyboardInterrupt caught in kernel")
  179
+        finally:
  180
+            # ensure excepthook is restored
  181
+            sys.excepthook = real_excepthook
  182
+
  183
+# mapping of keys to loop functions
  184
+loop_map = {
  185
+    'qt' : loop_qt4,
  186
+    'qt4': loop_qt4,
  187
+    'inline': None,
  188
+    'osx': loop_cocoa,
  189
+    'wx' : loop_wx,
  190
+    'tk' : loop_tk,
  191
+    'gtk': loop_gtk,
  192
+    None : None,
  193
+}
  194
+
  195
+
  196
+def enable_gui(gui, kernel=None):
  197
+    """Enable integration with a given GUI"""
  198
+    if kernel is None:
  199
+        from .ipkernel import IPKernelApp
  200
+        kernel = IPKernelApp.instance().kernel
  201
+    if gui not in loop_map:
  202
+        raise ValueError("GUI %r not supported" % gui)
  203
+    loop = loop_map[gui]
  204
+    if kernel.eventloop is not None and kernel.eventloop is not loop:
  205
+        raise RuntimeError("Cannot activate multiple GUI eventloops")
  206
+    kernel.eventloop = loop
231  IPython/zmq/ipkernel.py
@@ -39,12 +39,11 @@
39 39
 from IPython.utils.jsonutil import json_clean
40 40
 from IPython.lib import pylabtools
41 41
 from IPython.utils.traitlets import (
42  
-    Any, List, Instance, Float, Dict, Bool, Unicode, CaselessStrEnum
  42
+    Any, Instance, Float, Dict, CaselessStrEnum
43 43
 )
44 44
 
45 45
 from entry_point import base_launch_kernel
46 46
 from kernelapp import KernelApp, kernel_flags, kernel_aliases
47  
-from iostream import OutStream
48 47
 from session import Session, Message
49 48
 from zmqshell import ZMQInteractiveShell
50 49
 
@@ -212,16 +211,16 @@ def record_ports(self, ports):
212 211
     def _publish_pyin(self, code, parent):
213 212
         """Publish the code request on the pyin stream."""
214 213
 
215  
-        pyin_msg = self.session.send(self.iopub_socket, u'pyin',{u'code':code}, parent=parent)
  214
+        self.session.send(self.iopub_socket, u'pyin', {u'code':code},
  215
+                          parent=parent)
216 216
 
217 217
     def execute_request(self, ident, parent):
218 218
 
219  
-        status_msg = self.session.send(self.iopub_socket,
220  
-            u'status',
221  
-            {u'execution_state':u'busy'},
222  
-            parent=parent
223  
-        )
224  
-
  219
+        self.session.send(self.iopub_socket,
  220
+                          u'status',
  221
+                          {u'execution_state':u'busy'},
  222
+                          parent=parent )
  223
+        
225 224
         try:
226 225
             content = parent[u'content']
227 226
             code = content[u'code']
@@ -331,11 +330,10 @@ def execute_request(self, ident, parent):
331 330
         if reply_msg['content']['status'] == u'error':
332 331
             self._abort_queue()
333 332
 
334  
-        status_msg = self.session.send(self.iopub_socket,
335  
-            u'status',
336  
-            {u'execution_state':u'idle'},
337  
-            parent=parent
338  
-        )
  333
+        self.session.send(self.iopub_socket,
  334
+                          u'status',
  335
+                          {u'execution_state':u'idle'},
  336
+                          parent=parent )
339 337
 
340 338
     def complete_request(self, ident, parent):
341 339
         txt, matches = self._complete(parent)
@@ -375,7 +373,8 @@ def history_request(self, ident, parent):
375 373
 
376 374
         elif hist_access_type == 'search':
377 375
             pattern = parent['content']['pattern']
378  
-            hist = self.shell.history_manager.search(pattern, raw=raw, output=output)
  376
+            hist = self.shell.history_manager.search(pattern, raw=raw,
  377
+                                                     output=output) 
379 378
 
380 379
         else:
381 380
             hist = []
@@ -396,7 +395,8 @@ def connect_request(self, ident, parent):
396 395
 
397 396
     def shutdown_request(self, ident, parent):
398 397
         self.shell.exit_now = True
399  
-        self._shutdown_message = self.session.msg(u'shutdown_reply', parent['content'], parent)
  398
+        self._shutdown_message = self.session.msg(u'shutdown_reply',
  399
+                                                  parent['content'], parent)
400 400
         sys.exit(0)
401 401
 
402 402
     #---------------------------------------------------------------------------
@@ -427,8 +427,10 @@ def _abort_queue(self):
427 427
             time.sleep(0.1)
428 428
 
429 429
     def _no_raw_input(self):
430  
-        """Raise StdinNotImplentedError if active frontend doesn't support stdin."""
431  
-        raise StdinNotImplementedError("raw_input was called, but this frontend does not support stdin.")
  430
+        """Raise StdinNotImplentedError if active frontend doesn't support
  431
+        stdin."""
  432
+        raise StdinNotImplementedError("raw_input was called, but this "
  433
+                                       "frontend does not support stdin.") 
432 434
         
433 435
     def _raw_input(self, prompt, ident, parent):
434 436
         # Flush output before making the request.
@@ -437,7 +439,8 @@ def _raw_input(self, prompt, ident, parent):
437 439
 
438 440
         # Send the input request.
439 441
         content = json_clean(dict(prompt=prompt))
440  
-        msg = self.session.send(self.stdin_socket, u'input_request', content, parent, ident=ident)
  442
+        self.session.send(self.stdin_socket, u'input_request', content, parent,
  443
+                          ident=ident)
441 444
 
442 445
         # Await a response.
443 446
         while True:
@@ -510,189 +513,6 @@ def _at_shutdown(self):
510 513
             # before Python truly shuts down.
511 514
             time.sleep(0.01)
512 515
 
513  
-
514  
-#------------------------------------------------------------------------------
515  
-# Eventloops for integrating the Kernel into different GUIs
516  
-#------------------------------------------------------------------------------
517  
-
518  
-
519  
-def loop_qt4(kernel):
520  
-    """Start a kernel with PyQt4 event loop integration."""
521  
-
522  
-    from IPython.external.qt_for_kernel import QtCore
523  
-    from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4
524  
-
525  
-    kernel.app = get_app_qt4([" "])
526  
-    kernel.app.setQuitOnLastWindowClosed(False)
527  
-    kernel.timer = QtCore.QTimer()
528  
-    kernel.timer.timeout.connect(kernel.do_one_iteration)
529  
-    # Units for the timer are in milliseconds
530  
-    kernel.timer.start(1000*kernel._poll_interval)
531  
-    start_event_loop_qt4(kernel.app)
532  
-
533  
-
534  
-def loop_wx(kernel):
535  
-    """Start a kernel with wx event loop support."""
536  
-
537  
-    import wx
538  
-    from IPython.lib.guisupport import start_event_loop_wx
539  
-
540  
-    doi = kernel.do_one_iteration
541  
-     # Wx uses milliseconds
542  
-    poll_interval = int(1000*kernel._poll_interval)
543  
-
544  
-    # We have to put the wx.Timer in a wx.Frame for it to fire properly.
545  
-    # We make the Frame hidden when we create it in the main app below.
546  
-    class TimerFrame(wx.Frame):
547  
-        def __init__(self, func):
548  
-            wx.Frame.__init__(self, None, -1)
549  
-            self.timer = wx.Timer(self)
550  
-            # Units for the timer are in milliseconds
551  
-            self.timer.Start(poll_interval)
552  
-            self.Bind(wx.EVT_TIMER, self.on_timer)
553  
-            self.func = func
554  
-
555  
-        def on_timer(self, event):
556  
-            self.func()
557  
-
558  
-    # We need a custom wx.App to create our Frame subclass that has the
559  
-    # wx.Timer to drive the ZMQ event loop.
560  
-    class IPWxApp(wx.App):
561  
-        def OnInit(self):
562  
-            self.frame = TimerFrame(doi)
563  
-            self.frame.Show(False)
564  
-            return True
565  
-
566  
-    # The redirect=False here makes sure that wx doesn't replace
567  
-    # sys.stdout/stderr with its own classes.
568  
-    kernel.app = IPWxApp(redirect=False)
569  
-    start_event_loop_wx(kernel.app)
570  
-
571  
-
572  
-def loop_tk(kernel):
573  
-    """Start a kernel with the Tk event loop."""
574  
-
575  
-    import Tkinter
576  
-    doi = kernel.do_one_iteration
577  
-    # Tk uses milliseconds
578  
-    poll_interval = int(1000*kernel._poll_interval)
579  
-    # For Tkinter, we create a Tk object and call its withdraw method.
580  
-    class Timer(object):
581  
-        def __init__(self, func):
582  
-            self.app = Tkinter.Tk()
583  
-            self.app.withdraw()
584  
-            self.func = func
585  
-
586  
-        def on_timer(self):
587  
-            self.func()
588  
-            self.app.after(poll_interval, self.on_timer)
589  
-
590  
-        def start(self):
591  
-            self.on_timer()  # Call it once to get things going.
592  
-            self.app.mainloop()
593  
-
594  
-    kernel.timer = Timer(doi)
595  
-    kernel.timer.start()
596  
-
597  
-
598  
-def loop_gtk(kernel):
599  
-    """Start the kernel, coordinating with the GTK event loop"""
600  
-    from .gui.gtkembed import GTKEmbed
601  
-
602  
-    gtk_kernel = GTKEmbed(kernel)
603  
-    gtk_kernel.start()
604  
-
605  
-
606  
-def loop_cocoa(kernel):
607  
-    """Start the kernel, coordinating with the Cocoa CFRunLoop event loop
608  
-    via the matplotlib MacOSX backend.
609  
-    """
610  
-    import matplotlib
611  
-    if matplotlib.__version__ < '1.1.0':
612  
-        kernel.log.warn(
613  
-        "MacOSX backend in matplotlib %s doesn't have a Timer, "
614  
-        "falling back on Tk for CFRunLoop integration.  Note that "
615  
-        "even this won't work if Tk is linked against X11 instead of "
616  
-        "Cocoa (e.g. EPD).  To use the MacOSX backend in the kernel, "
617  
-        "you must use matplotlib >= 1.1.0, or a native libtk."
618  
-        )
619  
-        return loop_tk(kernel)
620  
-    
621  
-    from matplotlib.backends.backend_macosx import TimerMac, show
622  
-
623  
-    # scale interval for sec->ms
624  
-    poll_interval = int(1000*kernel._poll_interval)
625  
-
626  
-    real_excepthook = sys.excepthook
627  
-    def handle_int(etype, value, tb):
628  
-        """don't let KeyboardInterrupts look like crashes"""
629  
-        if etype is KeyboardInterrupt:
630  
-            io.raw_print("KeyboardInterrupt caught in CFRunLoop")
631  
-        else:
632  
-            real_excepthook(etype, value, tb)
633  
-
634  
-    # add doi() as a Timer to the CFRunLoop
635  
-    def doi():
636  
-        # restore excepthook during IPython code
637  
-        sys.excepthook = real_excepthook
638  
-        kernel.do_one_iteration()
639  
-        # and back:
640  
-        sys.excepthook = handle_int
641  
-
642  
-    t = TimerMac(poll_interval)
643  
-    t.add_callback(doi)
644  
-    t.start()
645  
-
646  
-    # but still need a Poller for when there are no active windows,
647  
-    # during which time mainloop() returns immediately
648  
-    poller = zmq.Poller()
649  
-    poller.register(kernel.shell_socket, zmq.POLLIN)
650  
-
651  
-    while True:
652  
-        try:
653  
-            # double nested try/except, to properly catch KeyboardInterrupt
654  
-            # due to pyzmq Issue #130
655  
-            try:
656  
-                # don't let interrupts during mainloop invoke crash_handler:
657  
-                sys.excepthook = handle_int
658  
-                show.mainloop()
659  
-                sys.excepthook = real_excepthook
660  
-                # use poller if mainloop returned (no windows)
661  
-                # scale by extra factor of 10, since it's a real poll
662  
-                poller.poll(10*poll_interval)
663  
-                kernel.do_one_iteration()
664  
-            except:
665  
-                raise
666  
-        except KeyboardInterrupt:
667  
-            # Ctrl-C shouldn't crash the kernel
668  
-            io.raw_print("KeyboardInterrupt caught in kernel")
669  
-        finally:
670  
-            # ensure excepthook is restored
671  
-            sys.excepthook = real_excepthook
672  
-
673  
-# mapping of keys to loop functions
674  
-loop_map = {
675  
-    'qt' : loop_qt4,
676  
-    'qt4': loop_qt4,
677  
-    'inline': None,
678  
-    'osx': loop_cocoa,
679  
-    'wx' : loop_wx,
680  
-    'tk' : loop_tk,
681  
-    'gtk': loop_gtk,
682  
-}
683  
-
684  
-def enable_gui(gui, kernel=None):
685  
-    """Enable integration with a give GUI"""
686  
-    if kernel is None:
687  
-        kernel = IPKernelApp.instance().kernel
688  
-    if gui not in loop_map:
689  
-        raise ValueError("GUI %r not supported" % gui)
690  
-    loop = loop_map[gui]
691  
-    if kernel.eventloop is not None and kernel.eventloop is not loop:
692  
-        raise RuntimeError("Cannot activate multiple GUI eventloops")
693  
-    kernel.eventloop = loop
694  
-
695  
-
696 516
 #-----------------------------------------------------------------------------
697 517
 # Aliases and Flags for the IPKernelApp
698 518
 #-----------------------------------------------------------------------------
@@ -767,7 +587,8 @@ def init_kernel(self):
767 587
                 # replace pyerr-sending traceback with stdout
768 588
                 _showtraceback = shell._showtraceback
769 589
                 def print_tb(etype, evalue, stb):
770  
-                    print ("Error initializing pylab, pylab mode will not be active", file=io.stderr)
  590
+                    print ("Error initializing pylab, pylab mode will not "
  591
+                           "be active", file=io.stderr)
771 592
                     print (shell.InteractiveTB.stb2text(stb), file=io.stdout)
772 593
                 shell._showtraceback = print_tb
773 594
                 
@@ -790,8 +611,8 @@ def init_shell(self):
790 611
 def launch_kernel(*args, **kwargs):
791 612
     """Launches a localhost IPython kernel, binding to the specified ports.
792 613
 
793  
-    This function simply calls entry_point.base_launch_kernel with the right first
794  
-    command to start an ipkernel.  See base_launch_kernel for arguments.
  614
+    This function simply calls entry_point.base_launch_kernel with the right
  615
+    first command to start an ipkernel.  See base_launch_kernel for arguments.
795 616
 
796 617
     Returns
797 618
     -------
2  IPython/zmq/pylab/backend_inline.py
@@ -16,7 +16,7 @@
16 16
 # Local imports.
17 17
 from IPython.config.configurable import SingletonConfigurable
18 18
 from IPython.core.displaypub import publish_display_data
19  
-from IPython.lib.pylabtools import print_figure, select_figure_format
  19
+from IPython.core.pylabtools import print_figure, select_figure_format
20 20
 from IPython.utils.traitlets import Dict, Instance, CaselessStrEnum, CBool
21 21
 from IPython.utils.warn import warn
22 22
 
80  IPython/zmq/zmqshell.py
@@ -109,6 +109,11 @@ def _exiter_default(self):
109 109
 
110 110
     keepkernel_on_exit = None
111 111
 
  112
+    # Over ZeroMQ, GUI control isn't done with PyOS_InputHook as there is no
  113
+    # interactive input being read; we provide event loop support in ipkernel
  114
+    from .eventloops import enable_gui
  115
+    enable_gui = staticmethod(enable_gui)
  116
+
112 117
     def init_environment(self):
113 118
         """Configure the user's environment.
114 119
 
@@ -390,79 +395,6 @@ def magic_edit(self,parameter_s='',last_call=['','']):
390 395
         }
391 396
         self.payload_manager.write_payload(payload)
392 397
 
393  
-    def magic_gui(self, parameter_s=''):
394  
-        """Enable or disable IPython GUI event loop integration.
395  
-
396  
-        %gui [GUINAME]
397  
-
398  
-        This magic replaces IPython's threaded shells that were activated
399  
-        using the (pylab/wthread/etc.) command line flags.  GUI toolkits
400  
-        can now be enabled at runtime and keyboard
401  
-        interrupts should work without any problems.  The following toolkits
402  
-        are supported:  wxPython, PyQt4, PyGTK, Cocoa, and Tk::
403  
-
404  
-            %gui wx      # enable wxPython event loop integration
405  
-            %gui qt4|qt  # enable PyQt4 event loop integration
406  
-            %gui gtk     # enable PyGTK event loop integration
407  
-            %gui OSX     # enable Cocoa event loop integration (requires matplotlib 1.1)
408  
-            %gui tk      # enable Tk event loop integration
409  
-
410  
-        WARNING:  after any of these has been called you can simply create
411  
-        an application object, but DO NOT start the event loop yourself, as
412  
-        we have already handled that.
413  
-        """
414  
-        from IPython.zmq.ipkernel import enable_gui
415  
-        opts, arg = self.parse_options(parameter_s, '')
416  
-        if arg=='': arg = None
417  
-        try:
418  
-            enable_gui(arg)
419  
-        except Exception as e:
420  
-            # print simple error message, rather than traceback if we can't
421  
-            # hook up the GUI
422  
-            error(str(e))
423  
-
424  
-    def enable_pylab(self, gui=None, import_all=True):
425  
-        """Activate pylab support at runtime.
426  
-
427  
-        This turns on support for matplotlib, preloads into the interactive
428  
-        namespace all of numpy and pylab, and configures IPython to correcdtly
429  
-        interact with the GUI event loop.  The GUI backend to be used can be
430  
-        optionally selected with the optional :param:`gui` argument.
431  
-
432  
-        Parameters
433  
-        ----------
434  
-        gui : optional, string [default: inline]
435  
-
436  
-          If given, dictates the choice of matplotlib GUI backend to use
437  
-          (should be one of IPython's supported backends, 'inline', 'qt', 'osx',
438  
-          'tk', or 'gtk'), otherwise we use the default chosen by matplotlib
439  
-          (as dictated by the matplotlib build-time options plus the user's
440  
-          matplotlibrc configuration file).
441  
-        """
442  
-        from IPython.zmq.ipkernel import enable_gui
443  
-        # We want to prevent the loading of pylab to pollute the user's
444  
-        # namespace as shown by the %who* magics, so we execute the activation
445  
-        # code in an empty namespace, and we update *both* user_ns and
446  
-        # user_ns_hidden with this information.
447  
-        ns = {}
448  
-        try:
449  
-            gui = pylabtools.pylab_activate(ns, gui, import_all, self)
450  
-        except KeyError:
451  
-            error("Backend %r not supported" % gui)
452  
-            return
453  
-        self.user_ns.update(ns)
454  
-        self.user_ns_hidden.update(ns)
455  
-        # Now we must activate the gui pylab wants to use, and fix %run to take
456  
-        # plot updates into account
457  
-        try:
458  
-            enable_gui(gui)
459  
-        except Exception as e:
460  
-            # print simple error message, rather than traceback if we can't
461  
-            # hook up the GUI
462  
-            error(str(e))
463  
-        self.magic_run = self._pylab_magic_run
464  
-
465  
-
466 398
     # A few magics that are adapted to the specifics of using pexpect and a
467 399
     # remote terminal
468 400
 
@@ -567,7 +499,6 @@ def magic_qtconsole(self, arg_s):
567 499
         except Exception as e:
568 500
             error("Could not start qtconsole: %r" % e)
569 501
             return
570  
-        
571 502
 
572 503
     def set_next_input(self, text):
573 504
         """Send the specified text to the frontend to be presented at the next
@@ -578,4 +509,5 @@ def set_next_input(self, text):
578 509
         )
579 510
         self.payload_manager.write_payload(payload)
580 511
 
  512
+
581 513
 InteractiveShellABC.register(ZMQInteractiveShell)
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.