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

[Bug]: crash due to backend issue in ipython session started explicitly with InteractiveShell #23770

Open
zpincus opened this issue Aug 29, 2022 · 11 comments · May be fixed by #25870
Open

[Bug]: crash due to backend issue in ipython session started explicitly with InteractiveShell #23770

zpincus opened this issue Aug 29, 2022 · 11 comments · May be fixed by #25870
Labels
Difficulty: Medium https://matplotlib.org/devdocs/devel/contribute.html#good-first-issues Good first issue Open a pull request against these issues if there are no active ones!

Comments

@zpincus
Copy link
Contributor

zpincus commented Aug 29, 2022

Bug summary

If an IPython session is started via IPython.core.interactiveshell.InteractiveShell.instance(), trying to create a matplotlib figure, or even query the backend, produces a crash (stack trace below) on OS X, and on Linux if there is an active X11 session.

Without an X session on Linux or if ipython is started via the command-line ipython command, everything works as expected.

Code for reproduction

import IPython.core.interactiveshell
IPython.core.interactiveshell.InteractiveShell.instance()

import matplotlib.pyplot
matplotlib.pyplot.figure()
# or the below crashes too
import matplotlib
matplotlib.get_backend()

Actual outcome

In : matplotlib.pyplot.figure()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/zpincus/miniconda/envs/mplcrash/lib/python3.10/site-packages/matplotlib/pyplot.py", line 806, in figure
    manager = new_figure_manager(
  File "/Users/zpincus/miniconda/envs/mplcrash/lib/python3.10/site-packages/matplotlib/pyplot.py", line 324, in new_figure_manager
    _warn_if_gui_out_of_main_thread()
  File "/Users/zpincus/miniconda/envs/mplcrash/lib/python3.10/site-packages/matplotlib/pyplot.py", line 314, in _warn_if_gui_out_of_main_thread
    if (_get_required_interactive_framework(_get_backend_mod())
  File "/Users/zpincus/miniconda/envs/mplcrash/lib/python3.10/site-packages/matplotlib/pyplot.py", line 217, in _get_backend_mod
    switch_backend(dict.__getitem__(rcParams, "backend"))
  File "/Users/zpincus/miniconda/envs/mplcrash/lib/python3.10/site-packages/matplotlib/pyplot.py", line 262, in switch_backend
    switch_backend(candidate)
  File "/Users/zpincus/miniconda/envs/mplcrash/lib/python3.10/site-packages/matplotlib/pyplot.py", line 310, in switch_backend
    install_repl_displayhook()
  File "/Users/zpincus/miniconda/envs/mplcrash/lib/python3.10/site-packages/matplotlib/pyplot.py", line 150, in install_repl_displayhook
    ip.enable_gui(ipython_gui_name)
  File "/Users/zpincus/miniconda/envs/mplcrash/lib/python3.10/site-packages/IPython/core/interactiveshell.py", line 3455, in enable_gui
    raise NotImplementedError('Implement enable_gui in a subclass')
NotImplementedError: Implement enable_gui in a subclass

Expected outcome

In : matplotlib.pyplot.figure()
<Figure size 640x480 with 0 Axes>

Additional information

The below is sufficient to reproduce the crash from a clean environment.

conda create -n "mplcrash" -c conda-forge python=3.10 matplotlib ipython
conda activate mplcrash
python -c 'import IPython.core.interactiveshell as ipsh; ipsh.InteractiveShell.instance(); import matplotlib.pyplot as plt; plt.figure()'

It's a weird corner case to be creating an ipython session this way, and perhaps the problem is on my end, but I wasn't expecting a crash like this regardless...

Operating system

macOS, Linux, maybe windows too?

Matplotlib Version

3.5.3

Matplotlib Backend

this crashes too with the same error when invoked after starting InteractiveShell.instance()

Python version

3.10.6 (but also occurs with various 3.9 versions)

Jupyter version

No response

Installation

conda

@tacaswell tacaswell added this to the v3.7.0 milestone Aug 29, 2022
@tacaswell tacaswell added Good first issue Open a pull request against these issues if there are no active ones! Difficulty: Medium https://matplotlib.org/devdocs/devel/contribute.html#good-first-issues labels Aug 29, 2022
@tacaswell
Copy link
Member

The source of the problem is that we by default try to pick the "best" backend and use that. If you have an active X session and a GUI framework installed we will select that. In order for the GUI window to be responsive someone needs to take care of running the GUI event loop while waiting for the human to type into the terminal. What enable_gui is doing is telling IPython to set up its inputhooks [1]. However because you are launching IPython from the base class to hook to install the hooks is not implemented, and hence the failure.

The immediate work around is to either force the backend to be non-interactive [2] via

MPLBACKEND=agg python -c 'import IPython.core.interactiveshell as ipsh; ipsh.InteractiveShell.instance(); import matplotlib.pyplot as plt; plt.figure()'

(svg and pdf would also work, pick the one that matches the type you expect to save most often). If you want to have GUI integration then you need to launch one of the sub-classes of InteractiveShell that has the GUI hooks implemented.

Medium term, we should catch errors in the call to enable_gui and continue down the fallback chain.

I am going to label this as good first issue and medium difficulty. There is no API design involved here (we have a reproduction case that should not fail), but the backend selection code / fallback logic in pyplot is a bit complicated (it is a complicated thing and while we are working on simplifying it we are currently in the awkward state where both the old and new are around). The exact steps here are:

  • adjust pyplot so in this case we will (eventually) fallback to a non-interactive backend (probably convert the NotImplementedError into an ImportError and undo a bunch of work we just did)
  • write a test that will reliably exercise this path (probably will require a subprocess? We have some tools to make that easy)

The relevant code:

def switch_backend(newbackend):
"""
Close all open figures and set the Matplotlib backend.
The argument is case-insensitive. Switching to an interactive backend is
possible only if no event loop for another interactive backend has started.
Switching to and from non-interactive backends is always possible.
Parameters
----------
newbackend : str
The name of the backend to use.
"""
global _backend_mod
# make sure the init is pulled up so we can assign to it later
import matplotlib.backends
close("all")
if newbackend is rcsetup._auto_backend_sentinel:
current_framework = cbook._get_running_interactive_framework()
mapping = {'qt': 'qtagg',
'gtk3': 'gtk3agg',
'gtk4': 'gtk4agg',
'wx': 'wxagg',
'tk': 'tkagg',
'macosx': 'macosx',
'headless': 'agg'}
best_guess = mapping.get(current_framework, None)
if best_guess is not None:
candidates = [best_guess]
else:
candidates = []
candidates += [
"macosx", "qtagg", "gtk4agg", "gtk3agg", "tkagg", "wxagg"]
# Don't try to fallback on the cairo-based backends as they each have
# an additional dependency (pycairo) over the agg-based backend, and
# are of worse quality.
for candidate in candidates:
try:
switch_backend(candidate)
except ImportError:
continue
else:
rcParamsOrig['backend'] = candidate
return
else:
# Switching to Agg should always succeed; if it doesn't, let the
# exception propagate out.
switch_backend("agg")
rcParamsOrig["backend"] = "agg"
return
backend_mod = importlib.import_module(
cbook._backend_module_name(newbackend))
canvas_class = backend_mod.FigureCanvas
required_framework = _get_required_interactive_framework(backend_mod)
if required_framework is not None:
current_framework = cbook._get_running_interactive_framework()
if (current_framework and required_framework
and current_framework != required_framework):
raise ImportError(
"Cannot load backend {!r} which requires the {!r} interactive "
"framework, as {!r} is currently running".format(
newbackend, required_framework, current_framework))
# Load the new_figure_manager() and show() functions from the backend.
# Classically, backends can directly export these functions. This should
# keep working for backcompat.
new_figure_manager = getattr(backend_mod, "new_figure_manager", None)
# show = getattr(backend_mod, "show", None)
# In that classical approach, backends are implemented as modules, but
# "inherit" default method implementations from backend_bases._Backend.
# This is achieved by creating a "class" that inherits from
# backend_bases._Backend and whose body is filled with the module globals.
class backend_mod(matplotlib.backend_bases._Backend):
locals().update(vars(backend_mod))
# However, the newer approach for defining new_figure_manager (and, in
# the future, show) is to derive them from canvas methods. In that case,
# also update backend_mod accordingly; also, per-backend customization of
# draw_if_interactive is disabled.
if new_figure_manager is None:
def new_figure_manager_given_figure(num, figure):
return canvas_class.new_manager(figure, num)
def new_figure_manager(num, *args, FigureClass=Figure, **kwargs):
fig = FigureClass(*args, **kwargs)
return new_figure_manager_given_figure(num, fig)
def draw_if_interactive():
if matplotlib.is_interactive():
manager = _pylab_helpers.Gcf.get_active()
if manager:
manager.canvas.draw_idle()
backend_mod.new_figure_manager_given_figure = \
new_figure_manager_given_figure
backend_mod.new_figure_manager = new_figure_manager
backend_mod.draw_if_interactive = draw_if_interactive
_log.debug("Loaded backend %s version %s.",
newbackend, backend_mod.backend_version)
rcParams['backend'] = rcParamsDefault['backend'] = newbackend
_backend_mod = backend_mod
for func_name in ["new_figure_manager", "draw_if_interactive", "show"]:
globals()[func_name].__signature__ = inspect.signature(
getattr(backend_mod, func_name))
# Need to keep a global reference to the backend for compatibility reasons.
# See https://github.com/matplotlib/matplotlib/issues/6092
matplotlib.backends.backend = newbackend
# make sure the repl display hook is installed in case we become
# interactive
install_repl_displayhook()

[1] : more details than anyone wants about input hooks: https://matplotlib.org/stable/users/explain/interactive_guide.html
[2]: https://matplotlib.org/stable/users/explain/backends.html

@zpincus
Copy link
Contributor Author

zpincus commented Aug 29, 2022

Thanks! This all makes sense.

For posterity / future googling: I discovered this issue because bokeh.io.output_notebook() actually (and unexpectedly to me at least) winds up invoking IPython.core.interactiveshell.InteractiveShell.instance() if no ipython is running already. So, for example, a stray output_notebook() in a script will summon this above exception.

@nithinivi
Copy link

I can try to take this issue! if you guys would guide me

@tacaswell
Copy link
Member

@nithinivi Do you have specific questions about #23770 (comment) ?

@nithinivi
Copy link

@tacaswell I'm working from a windows laptop, and I was able to reproduce the error, Is there something is should specifically consider while developing windows? so sorry for the late reply ! :)

@tacaswell
Copy link
Member

No, I do not think that this issue is platform specific.

@ksunden ksunden modified the milestones: v3.7.0, v3.7.1 Feb 14, 2023
@QuLogic QuLogic modified the milestones: v3.7.1, v3.7.2 Mar 4, 2023
@turnipseason
Copy link
Contributor

Hi! It seems that this issue is still around (tried and got it on windows, at least).
Is it free to work on? I'd like to give it a try.

@turnipseason
Copy link
Contributor

turnipseason commented May 7, 2023

Hi, @tacaswell! I had a couple questions regarding this issue.

So far I changed the code so that it looks like this in the "switch_backend()" method:

  # make sure the repl display hook is installed in case we become
  # interactive
    try:
        install_repl_displayhook()
    except ImportError as err:
        _log.error(str(err))
        raise ImportError

and like this in the "install_repl_displayhook()" method:

 try:
        ipython_gui_name = backend2gui.get(get_backend())
        if ipython_gui_name:
            ip.enable_gui(ipython_gui_name)
 except NotImplementedError as err:
        raise ImportError('Fallback to a different backend.') from err

I tried running the original prompt in the terminal -- nothing happened, but there were no error messages except for the logger output.
I altered it like so:

python -c "import IPython.core.interactiveshell as ipsh; ipsh.InteractiveShell.instance(); import matplotlib.pyplot as plt; import numpy as np; f = plt.figure(); ny = int(f.get_figheight() * f.dpi); nx = int(f.get_figwidth() * f.dpi); data = np.random.random((ny,nx)); f.figimage(data); plt.show()"

...and got this as an output:
image

It seems to be working with my changes (but unable to load the icons, even though the menus themselves are functional). However -- the backend this defaulted to is GTK4Agg. In your comment you said that we should be defaulting to a non-interactive backend. Plus the code mentions that it's not ideal since it's cairo-based:

  # Don't try to fallback on the cairo-based backends as they each have
  # an additional dependency (pycairo) over the agg-based backend, and
  # are of worse quality.

but after qtagg "fails", the next two backends are cairo-based.

candidates += [
            "macosx", "qtagg", "gtk4agg", "gtk3agg", "tkagg", "wxagg"]

The issue with icons only appeared for gtk4 and gtk3.

I also got an additional failed test after making these changes:

 FAILED lib/matplotlib/tests/test_pickle.py::test_pickle_load_from_subprocess[png] - UserWarning: This figure was saved with matplotlib version 3.8.0.dev952+g3133213d8b.d19700101 and is unlikely to function correctly.

This is my first issue so it's very likely that I misunderstood something, but I still wanted to ask.

  1. Is this what you had in mind for these changes?
  2. Should I change it so that gtk4agg and gtk3agg go after tkagg and wxagg?
  3. Is the failed test something to worry about?

Thanks in advance :)

@story645
Copy link
Member

Hi @turnipseason, as there's no active open PR, you're welcome to open one. For 3, you may want to do a build clean and then rebuild matplotlib.

@tacaswell
Copy link
Member

but after qtagg "fails", the next two backends are cairo-based.

we also have *cairo variant backends which are what depend on pycairo. Please do not re-arrange the backend order.

If you commit your changes it will work (we write the version Matpltolib used to generate the pickle into the file and then check if it matches the current version on the way out, the reported version of Matpltolib depends on the current state of git).

@turnipseason
Copy link
Contributor

we also have *cairo variant backends which are what depend on pycairo. Please do not re-arrange the backend order.

If you commit your changes it will work (we write the version Matpltolib used to generate the pickle into the file and then check if it matches the current version on the way out, the reported version of Matpltolib depends on the current state of git).

Thanks for replying and the clarification! I'll check everything once again and try submitting a PR in the morning then :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Difficulty: Medium https://matplotlib.org/devdocs/devel/contribute.html#good-first-issues Good first issue Open a pull request against these issues if there are no active ones!
Projects
None yet
7 participants