ipympl icon indicating copy to clipboard operation
ipympl copied to clipboard

display `print` in callbacks in the notebook

Open tacaswell opened this issue 6 years ago • 8 comments

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
n = 0

def printer(event):
    global n
    print(f'in event {event}')
    n += 1
 

fig.canvas.mpl_connect('button_press_event', printer)

when run in IPython prints out to stdout on every mouse click. In jlab + ipympl the prints don't show up anywhere I can find...

From https://ipywidgets.readthedocs.io/en/stable/examples/Output%20Widget.html it looks like this can work via:

output = widgets.Output()
display(fig.canvas, output)

n = 0
def printer(event):
    global n
    with output:
        print(f'in event {event}')
    n += 1
    
fig.canvas.mpl_connect('button_press_event', printer)

but I suspect there is a way for this to work automatically....

tacaswell avatar Jul 04 '19 20:07 tacaswell

This is (unfortunately) the expected behavior in JupyterLab.

JupyterLab does not show outputs from callbacks triggered by a widget change. The use of the output widget is the way to go.

SylvainCorlay avatar Jul 04 '19 20:07 SylvainCorlay

Note: there is ongoing work to add a "global output area" for display events that are not tied to a specific cell, which may be useful for debugging.

SylvainCorlay avatar Jul 04 '19 21:07 SylvainCorlay

A very hacky way to make this work is:

# make sure prints in callbacks make it to the notebook

def subplots(*args, **kwargs):
    from IPython.display import display
    import ipywidgets as widgets
    import weakref
    import functools
    
    fig, ax = plt.subplots(*args, **kwargs)
    fig._output = output = widgets.Output()
    display(output)
    
    orig_mpl_connect = fig.canvas.mpl_connect
    
    @functools.wraps(orig_mpl_connect)
    def mpl_connect(key, cb, **kwargs):
        try:
            r = weakref.WeakMethod(cb)
        except TypeError:
            r = lambda: cb
        def wrapper(*args, **kw):
            cb = r()
               
            with output:
                if cb is None:
                    ...
                else:
                    cb(*args, **kw)
                
        orig_mpl_connect(key, wrapper, **kwargs)
    
    fig.canvas.mpl_connect = mpl_connect
    return fig, ax

which relies on display being called in just the right order and monkey-patches the canvas, but I think something like this could be backed into ipympl?

tacaswell avatar Jul 04 '19 21:07 tacaswell

A slightly more thought out version:

'''
make sure prints in callbacks make it to the notebook

See https://ipywidgets.readthedocs.io/en/stable/examples/Output%20Widget.html

    By default, calling `print` in a ipywidgets callback results in the output
    being lost (because it is not clear _where_ it should go).  You can explictily
    capture that the text to a given output area using at Output widget.

This is a wrapper for `plt.subplots` that makes sure
  a) an ipywidgets.widgets.Output is created with each Figure
  b) the `mpl_connect` on the canvas is monkey-patched such that all
     user callbacks run in a context where the stdout is captured and sent

to that output area.
'''

import matplotlib.pyplot as plt


def _monkey_patch_pyplot():
    import matplotlib.pyplot as plt
    import functools
    from IPython.display import display
    import ipywidgets as widgets
    import weakref

    @functools.wraps(plt.figure)
    def figure(*args, **kwargs):
        fig = figure._figure(*args, **kwargs)
        fig._output = output = widgets.Output()
        display(output)

        orig_mpl_connect = fig.canvas.mpl_connect

        @functools.wraps(orig_mpl_connect)
        def mpl_connect(key, cb, **kwargs):
            # try to use a WeakMethod to make sure we don't keep objects alive
            # to match the behavior of the base mpl_connect
            try:
                r = weakref.WeakMethod(cb)
            except TypeError:
                def r():
                    return cb

            def wrapper(*args, **kw):
                cb = r()

                with output:
                    if cb is not None:
                        cb(*args, **kw)

            orig_mpl_connect(key, wrapper, **kwargs)

        # mokeny patch the canvas
        fig.canvas.mpl_connect = mpl_connect
        return fig

    # stash the orginal
    figure._figure = plt.figure
    # monkey patch pyplot (!?)
    plt.figure = figure
    plt._print_hacked = True


# make sure we only do this once!
if getattr(plt, '_print_hacked', False):
    ...
else:
    _monkey_patch_pyplot()

# clean up after our selves and do not polute the user's namespace
del _monkey_patch_pyplot
del plt

tacaswell avatar Jul 07 '19 04:07 tacaswell

We're running into similar issues and I'm playing with the suggestion above. An interesting case that I run into, that may be of interest: if an Output widgets is used (as a context manager) in a fresh asyncio task (i.e. not during a COM event) both prints and errors are swallowed (i.e. not shown anywhere).

almarklein avatar Aug 18 '21 20:08 almarklein

@almarklein do you have a short example you could post?

ianhi avatar Aug 19 '21 14:08 ianhi

Yes. So this works fine, printing "hello 2" in the cell output.

import asyncio

ran = 0
def print_some():
    global ran
    ran += 1
    print("hello 2")

loop = asyncio.get_event_loop()
loop.call_soon(print_some)

But this does not. It shows "hello 1" in the global log widget. But "hello 2" is never shown.

import asyncio
import ipywidgets

button = ipywidgets.Button(description="click me")

ran = 0
def print_some():
    global ran
    ran += 1
    print("hello 2")

@button.on_click
def on_click(event):
    print("hello 1")
    loop = asyncio.get_event_loop()
    loop.call_soon(print_some)

button

A screenshot of the result: image

almarklein avatar Aug 19 '21 21:08 almarklein

But this does not. It shows "hello 1" in the global log widget. But "hello 2" is never shown.

It would be interesting to see if "hello 2" gets transmitted to the frontend over the websocket connection. If it does, there's probably a change in jupyterlab we could make to get it into the log.

In general, running code outside of the framework of the kernel execute or comm messages (like in this async case) messes with the output capturing and redirection that the ipython kernel does.

jasongrout avatar Aug 20 '21 13:08 jasongrout