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

Installed qt5 event loop hook. #14006

Open
forallsunday opened this issue Apr 6, 2023 · 21 comments
Open

Installed qt5 event loop hook. #14006

forallsunday opened this issue Apr 6, 2023 · 21 comments

Comments

@forallsunday
Copy link

When I use plt.show() , and run the script,

& D:/Anaconda3/python.exe -m IPython --no-autoindent d:/Documents/C-Project/GeoLocOptim/scripts/estimate.py

The following message showed:

Installed qt5 event loop hook.
Shell is already running a gui event loop for qt5. Call with no arguments to disable the current loop.

What does it mean and how to disable this?

@nodatapoints
Copy link

nodatapoints commented Apr 7, 2023

I think this is due to #13957.
The print statements seem hardcoded, which I don't like.

An option to disable these debug messages would be nice, since I do not need to be reminded of the event loop on every startup.

@francescoboc
Copy link

I agree with @nodatapoints , I would like to suppress the message Installed qt5 event loop hook..

@ccordoba12
Copy link
Member

@shaperilio, how do you think we should address this issue?

@shaperilio
Copy link
Contributor

Is there a global "debug" flag?

The print statements were added precisely because silent failures were happening before; i.e. you run something which is forcing a qt event loop hook without asking, and then you muck around with some tk stuff and nothing works.

Happy to help make this quieter, and maybe work on something that allows multiple event loop hooks to run in sequence, which I believe should be possible as well.

@ccordoba12
Copy link
Member

ccordoba12 commented Apr 28, 2023

I think the problem is that Matplotlib sets the Qt backend by default in some cases, which generates this message in IPython when users try to set that backend again.

So, perhaps the message shouldn't be shown when the backend you're trying to set is the same one that you already have.

@shaperilio
Copy link
Contributor

So, perhaps the message shouldn't be shown when the backend you're trying to set is the same one that you already have.

This was certainly the intent. Let me take a look at what's going on.

@forallsunday can you post relevant package versions?

@nodatapoints
Copy link

@forallsunday can you post relevant package versions?

I run IPython 8.12.0 with Qt5 on Gentoo. In my configuration I set
c.TerminalIPythonApp.matplotlib = 'qt5'
otherwise I use default settings except for some prompt configs.

@shaperilio
Copy link
Contributor

I run IPython 8.12.0 with Qt5 on Gentoo. In my configuration I set c.TerminalIPythonApp.matplotlib = 'qt5' otherwise I use default settings except for some prompt configs.

What is c?

@ccordoba12
Copy link
Member

It's the IPython config object. See here for more details:

https://ipython.readthedocs.io/en/stable/config/intro.html

@shaperilio
Copy link
Contributor

OK, I reproduced this with a config file containing only

c = get_config()  #noqa
c.TerminalIPythonApp.matplotlib = 'qt5'

and a virtualenv containing only matplotlib, pyqt5, and ipython. I'll start working on this.

My intent is to make an effort to ignore multiple requests for the same GUI event loop, but keep all the messaging. If lots of people are against that, maybe we can add a --verbose flag or something similar to what I did for autoreload (which has -p or -l for showing messages via print or log).

@ccordoba12
Copy link
Member

I think ignoring warnings for the same event loop is fine for now.

@shaperilio
Copy link
Contributor

OK, there's a bit of a mess here with Qt versions.

When IPython starts, the profile configuration is read. After the banner is printed, TerminalIPythonApp.init_gui_pylab() executes.

The user, in this case, has set matplotlib to qt5 in their config. So init_gui_pylab calls InteractiveShellApp.enable_matplotlib('qt5').

This, via a call to matplotlib.get_backend(), will eventually call matplotlib.pyplot.switch_backend(xxx) where xxx is the first in the list ["macosx", "qtagg", "gtk4agg", "gtk3agg", "tkagg", "wxagg"] to succeed.

Note that this call comes from trying to retrieve the value rcParams['backend']; effectively asking for a backend will set it.

So, for both Windows and Linux, this would be qtagg.

This causes matplotlib to import the latest Qt in the system.

At the very end of the call to matplotlib.pyplot.switch_backend('qtagg'), pyplot.install_repl_displayhook() is called, and this effectively calls %gui yyy, where yyy comes from the mappings below.

pyplot.install_repl_displayhook() goes into IPython.core.pylabtools to look for which GUI event hook goes with which backend. Currently, there is this mapping from GUI->backend:

backends = {
    "tk": "TkAgg",
    "gtk": "GTKAgg",
    "gtk3": "GTK3Agg",
    "gtk4": "GTK4Agg",
    "wx": "WXAgg",
    "qt4": "Qt4Agg",
    "qt5": "Qt5Agg",
    "qt6": "QtAgg",
    "qt": "Qt5Agg",
    "osx": "MacOSX",
    "nbagg": "nbAgg",
    "webagg": "WebAgg",
    "notebook": "nbAgg",
    "agg": "agg",
    "svg": "svg",
    "pdf": "pdf",
    "ps": "ps",
    "inline": "module://matplotlib_inline.backend_inline",
    "ipympl": "module://ipympl.backend_nbagg",
    "widget": "module://ipympl.backend_nbagg",
}

I see a discrepancy here: If qt6 maps to QtAgg, why does qt map to Qt5Agg?

The reverse mapping from backend->GUI has modifications:

# There needs to be a hysteresis here as the new QtAgg Matplotlib backend
# supports either Qt5 or Qt6 and the IPython qt event loop support Qt4, Qt5,
# and Qt6.
backend2gui["QtAgg"] = "qt"
backend2gui["Qt4Agg"] = "qt"
backend2gui["Qt5Agg"] = "qt"

I don't follow these. If there are specifically versioned backends, and the Qt imports on both matplotlib and IPython are specifically versioned, and we assume qt means latest, these shouldn't be here. (I can certainly remove any reference to Qt4 and older since support for it was removed in #13864.)

Note that this all happens before actually processing whatever the user set TerminalIPythonApp.matplotlib to in their config file. This definition of backend2gui will just return qt, so before even reading the user's config file, we will install an event loop hook for the latest installed version of Qt. For example, in a system with both pyside6 and pyqt5 installed, this will import pyside6 into the session and install a hook for it.

This entire sequence is being triggered by importing matplotlib_inline.backend_inline. In short, InteractiveShell.enable_matplotlib is first calling %gui qt and then immediately trying to call %gui yyy, where yyy is what the user asked for in their config. If the two Qt versions don't match, this will fail.

(This is more of a note to self; let me think about this more and see how we can proceed.)

@shaperilio
Copy link
Contributor

@tacaswell do you have any thoughts on the backend<->GUI mapping?

@tacaswell
Copy link
Contributor

There is a winding road to get here and I am not sure I have all of the details correct, but my rough understanding is:

  • IPython provides nicer names (without the agg) to select the backend
  • these names date back probably 15+ years and the current versions of Qt
  • in Matplotlib we had a pattern of doing a new backend for each GUI toolkit version (backend_qt4agg.py, backend_qt5agg.py, backend_gtk3agg.py, backend_gtk4agg.py) etc
  • when qt6 came out instead of making a backend_qt6agg.py we managed to push enough compatibility shims into backends/qt_compat.py that we could support all 4 Qt bindings (pyside / pyqt X Qt5, Qt6) and made one new backend called backend_qtagg.py. Support for PyQt6/PySide6. matplotlib/matplotlib#19255 . This is mpl3.5
  • we kept backend_qt5agg.py around as a very thin shim (and a knob to force qt_compat.py to pick one of the Qt5 bindings) because there is no good reason to break any users who are importing directly
  • It looks like I was the last person to touch the forward mapping of 'qt' in 127b45b when I changed 'qt' to map to qt5agg!
  • There is a bug in the table at https://matplotlib.org/stable/users/explain/backends.html#the-builtin-backends which claims %matplotlib qt selects qtagg

I think the course of action here should be:

  • version gate the meaning of qt to be qtagg

I'll open this PR.

This is closely related to #14038

@shaperilio
Copy link
Contributor

Thanks @tacaswell; I'll need some time to process your comment.

In the meantime, I've started a draft pull request. The messaging is improved (that's the easy part) but I left a comment there because importing matplotlib_inline.backend_inline triggers a GUI event loop hook that I don't think is needed, but I need more guidance on that.

@tacaswell
Copy link
Contributor

but I left a comment there because importing matplotlib_inline.backend_inline triggers a GUI event loop hook that I don't think is needed, but I need more guidance on that.

I'm 75% sure that is something that needs to be resolved in the inline backend.

@tacaswell
Copy link
Contributor

The other thing to check is that the default backend in Matplotlib in now "auto", however if you ask Matplotlib what the current backend is or create a figure we will sort out what the best currently available backend is. If you have any of the Qt bindings installed, we will pick that (as interactive figures are better than dead figures).

https://matplotlib.org/stable/users/explain/backends.html#selecting-a-backend

Without a backend explicitly set, Matplotlib automatically detects a usable backend based on what is available on your system and on whether a GUI event loop is already running. The first usable backend in the following list is selected: MacOSX, QtAgg, GTK4Agg, Gtk3Agg, TkAgg, WxAgg, Agg. The last, Agg, is a non-interactive backend that can only write to files. It is used on Linux, if Matplotlib cannot connect to either an X display or a Wayland display.

I do not see matplotlib_inline doing anything I expect to force the backend, but I may have missed it or am forgetting another condition that force us to resolve the backend.

@shaperilio
Copy link
Contributor

shaperilio commented May 3, 2023

The other thing to check is that the default backend in Matplotlib in now "auto"

This is exactly what matplotlib_inline is triggering, by calling _enable_matplotlib_integration() in backend_inline.py. That calls matplotlib.get_backend(), which will trigger __getitem__ in RcParams and execute this:

https://github.com/matplotlib/matplotlib/blob/5189609b9d2c0ea0b78ceba9d4003358db160b81/lib/matplotlib/__init__.py#L750-L754

and that last line will go through the list mentioned in the docs.

So yes, it seems that _enable_matplotlib_integration() should probably call matplotlib._get_backend_or_none().

@shaperilio
Copy link
Contributor

OK, #14067 implements what I think we should do.

I think it needs to wait to see what we hear from matplotlib_inline.

@shaperilio
Copy link
Contributor

I created a PR for this in matplotlib_inline: ipython/matplotlib-inline#26

Now _enable_matplotlib_integration() won't trigger the auto-selection of a backend.

@shaperilio
Copy link
Contributor

I just wrote up #14089; maybe we can take the choice away from users who don't really care? Especially with this Qt version stuff, I wonder if we support it just because the code is there and not because most people actually need it.

Carreau added a commit to Carreau/ipython that referenced this issue Sep 19, 2023
Carreau added a commit to Carreau/ipython that referenced this issue Sep 19, 2023
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

6 participants