Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

display print in callbacks in the notebook #116

Open
tacaswell opened this issue Jul 4, 2019 · 8 comments
Open

display print in callbacks in the notebook #116

tacaswell opened this issue Jul 4, 2019 · 8 comments

Comments

@tacaswell
Copy link
Member

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....

@SylvainCorlay
Copy link
Member

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
Copy link
Member

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.

@tacaswell
Copy link
Member Author

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
Copy link
Member Author

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

@almarklein
Copy link

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).

@ianhi
Copy link
Collaborator

ianhi commented Aug 19, 2021

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

@almarklein
Copy link

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

@jasongrout
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants