##// END OF EJS Templates
ENH: support for `PySide6` in `%gui` (#13864)...
Matthias Bussonnier -
r28120:0374cf80 merge
parent child Browse files
Show More
@@ -0,0 +1,50 b''
1 import os
2 import importlib
3
4 import pytest
5
6 from IPython.terminal.pt_inputhooks import set_qt_api, get_inputhook_name_and_func
7
8
9 guis_avail = []
10
11
12 def _get_qt_vers():
13 """If any version of Qt is available, this will populate `guis_avail` with 'qt' and 'qtx'. Due
14 to the import mechanism, we can't import multiple versions of Qt in one session."""
15 for gui in ["qt", "qt6", "qt5"]:
16 print(f"Trying {gui}")
17 try:
18 set_qt_api(gui)
19 importlib.import_module("IPython.terminal.pt_inputhooks.qt")
20 guis_avail.append(gui)
21 if "QT_API" in os.environ.keys():
22 del os.environ["QT_API"]
23 except ImportError:
24 pass # that version of Qt isn't available.
25 except RuntimeError:
26 pass # the version of IPython doesn't know what to do with this Qt version.
27
28
29 _get_qt_vers()
30
31
32 @pytest.mark.skipif(
33 len(guis_avail) == 0, reason="No viable version of PyQt or PySide installed."
34 )
35 def test_inputhook_qt():
36 gui = guis_avail[0]
37
38 # Choose a qt version and get the input hook function. This will import Qt...
39 get_inputhook_name_and_func(gui)
40
41 # ...and now we're stuck with this version of Qt for good; can't switch.
42 for not_gui in ["qt6", "qt5"]:
43 if not_gui not in guis_avail:
44 break
45
46 with pytest.raises(ImportError):
47 get_inputhook_name_and_func(not_gui)
48
49 # A gui of 'qt' means "best available", or in this case, the last one that was used.
50 get_inputhook_name_and_func("qt")
@@ -493,8 +493,10 b' Currently the magic system has the following functions:""",'
493 are supported: wxPython, PyQt4, PyGTK, Tk and Cocoa (OSX)::
493 are supported: wxPython, PyQt4, PyGTK, Tk and Cocoa (OSX)::
494
494
495 %gui wx # enable wxPython event loop integration
495 %gui wx # enable wxPython event loop integration
496 %gui qt4|qt # enable PyQt4 event loop integration
496 %gui qt # enable PyQt/PySide event loop integration
497 %gui qt5 # enable PyQt5 event loop integration
497 # with the latest version available.
498 %gui qt6 # enable PyQt6/PySide6 event loop integration
499 %gui qt5 # enable PyQt5/PySide2 event loop integration
498 %gui gtk # enable PyGTK event loop integration
500 %gui gtk # enable PyGTK event loop integration
499 %gui gtk3 # enable Gtk3 event loop integration
501 %gui gtk3 # enable Gtk3 event loop integration
500 %gui gtk4 # enable Gtk4 event loop integration
502 %gui gtk4 # enable Gtk4 event loop integration
@@ -23,6 +23,9 b' if QT_API not set:'
23 else:
23 else:
24 use what QT_API says
24 use what QT_API says
25
25
26 Note that %gui's implementation will always set a `QT_API`, see
27 `IPython.terminal.pt_inputhooks.get_inputhook_name_and_func`
28
26 """
29 """
27 # NOTE: This is no longer an external, third-party module, and should be
30 # NOTE: This is no longer an external, third-party module, and should be
28 # considered part of IPython. For compatibility however, it is being kept in
31 # considered part of IPython. For compatibility however, it is being kept in
@@ -42,7 +45,6 b' from IPython.external.qt_loaders import ('
42 QT_API_PYQT5,
45 QT_API_PYQT5,
43 QT_API_PYSIDE2,
46 QT_API_PYSIDE2,
44 # QT4
47 # QT4
45 QT_API_PYQTv1,
46 QT_API_PYQT,
48 QT_API_PYQT,
47 QT_API_PYSIDE,
49 QT_API_PYSIDE,
48 # default
50 # default
@@ -56,10 +58,6 b' _qt_apis = ('
56 # QT5
58 # QT5
57 QT_API_PYQT5,
59 QT_API_PYQT5,
58 QT_API_PYSIDE2,
60 QT_API_PYSIDE2,
59 # QT4
60 QT_API_PYQTv1,
61 QT_API_PYQT,
62 QT_API_PYSIDE,
63 # default
61 # default
64 QT_API_PYQT_DEFAULT,
62 QT_API_PYQT_DEFAULT,
65 )
63 )
@@ -98,7 +96,7 b' def get_options():'
98 if loaded is not None:
96 if loaded is not None:
99 return [loaded]
97 return [loaded]
100
98
101 mpl = sys.modules.get('matplotlib', None)
99 mpl = sys.modules.get("matplotlib", None)
102
100
103 if mpl is not None and tuple(mpl.__version__.split(".")) < ("1", "0", "2"):
101 if mpl is not None and tuple(mpl.__version__.split(".")) < ("1", "0", "2"):
104 # 1.0.1 only supports PyQt4 v1
102 # 1.0.1 only supports PyQt4 v1
@@ -113,8 +111,6 b' def get_options():'
113 QT_API_PYSIDE6,
111 QT_API_PYSIDE6,
114 QT_API_PYQT5,
112 QT_API_PYQT5,
115 QT_API_PYSIDE2,
113 QT_API_PYSIDE2,
116 QT_API_PYQT,
117 QT_API_PYSIDE,
118 ]
114 ]
119 elif qt_api not in _qt_apis:
115 elif qt_api not in _qt_apis:
120 raise RuntimeError("Invalid Qt API %r, valid values are: %r" %
116 raise RuntimeError("Invalid Qt API %r, valid values are: %r" %
@@ -24,6 +24,7 b" QT_API_PYQT5 = 'pyqt5'"
24 QT_API_PYSIDE2 = 'pyside2'
24 QT_API_PYSIDE2 = 'pyside2'
25
25
26 # Qt4
26 # Qt4
27 # NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side.
27 QT_API_PYQT = "pyqt" # Force version 2
28 QT_API_PYQT = "pyqt" # Force version 2
28 QT_API_PYQTv1 = "pyqtv1" # Force version 2
29 QT_API_PYQTv1 = "pyqtv1" # Force version 2
29 QT_API_PYSIDE = "pyside"
30 QT_API_PYSIDE = "pyside"
@@ -367,23 +368,28 b' def load_qt(api_options):'
367 commit_api(api)
368 commit_api(api)
368 return result
369 return result
369 else:
370 else:
370 raise ImportError("""
371 raise ImportError(
372 """
371 Could not load requested Qt binding. Please ensure that
373 Could not load requested Qt binding. Please ensure that
372 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
374 PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or
373 and only one is imported per session.
375 PySide6 is available, and only one is imported per session.
374
376
375 Currently-imported Qt library: %r
377 Currently-imported Qt library: %r
376 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
377 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
378 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
378 PySide >= 1.0.3 installed: %s
379 PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
379 PySide2 installed: %s
380 PySide2 installed: %s
381 PySide6 installed: %s
380 Tried to load: %r
382 Tried to load: %r
381 """ % (loaded_api(),
383 """
382 has_binding(QT_API_PYQT),
384 % (
383 has_binding(QT_API_PYQT5),
385 loaded_api(),
384 has_binding(QT_API_PYSIDE),
386 has_binding(QT_API_PYQT5),
385 has_binding(QT_API_PYSIDE2),
387 has_binding(QT_API_PYQT6),
386 api_options))
388 has_binding(QT_API_PYSIDE2),
389 has_binding(QT_API_PYSIDE6),
390 api_options,
391 )
392 )
387
393
388
394
389 def enum_factory(QT_API, QtCore):
395 def enum_factory(QT_API, QtCore):
@@ -106,21 +106,21 b' def start_event_loop_wx(app=None):'
106 app._in_event_loop = True
106 app._in_event_loop = True
107
107
108 #-----------------------------------------------------------------------------
108 #-----------------------------------------------------------------------------
109 # qt4
109 # Qt
110 #-----------------------------------------------------------------------------
110 #-----------------------------------------------------------------------------
111
111
112 def get_app_qt4(*args, **kwargs):
112 def get_app_qt4(*args, **kwargs):
113 """Create a new qt4 app or return an existing one."""
113 """Create a new Qt app or return an existing one."""
114 from IPython.external.qt_for_kernel import QtGui
114 from IPython.external.qt_for_kernel import QtGui
115 app = QtGui.QApplication.instance()
115 app = QtGui.QApplication.instance()
116 if app is None:
116 if app is None:
117 if not args:
117 if not args:
118 args = ([''],)
118 args = ([""],)
119 app = QtGui.QApplication(*args, **kwargs)
119 app = QtGui.QApplication(*args, **kwargs)
120 return app
120 return app
121
121
122 def is_event_loop_running_qt4(app=None):
122 def is_event_loop_running_qt4(app=None):
123 """Is the qt4 event loop running."""
123 """Is the qt event loop running."""
124 # New way: check attribute on shell instance
124 # New way: check attribute on shell instance
125 ip = get_ipython()
125 ip = get_ipython()
126 if ip is not None:
126 if ip is not None:
@@ -128,17 +128,17 b' def is_event_loop_running_qt4(app=None):'
128
128
129 # Old way: check attribute on QApplication singleton
129 # Old way: check attribute on QApplication singleton
130 if app is None:
130 if app is None:
131 app = get_app_qt4([''])
131 app = get_app_qt4([""])
132 if hasattr(app, '_in_event_loop'):
132 if hasattr(app, '_in_event_loop'):
133 return app._in_event_loop
133 return app._in_event_loop
134 else:
134 else:
135 # Does qt4 provide a other way to detect this?
135 # Does qt provide a other way to detect this?
136 return False
136 return False
137
137
138 def start_event_loop_qt4(app=None):
138 def start_event_loop_qt4(app=None):
139 """Start the qt4 event loop in a consistent manner."""
139 """Start the qt event loop in a consistent manner."""
140 if app is None:
140 if app is None:
141 app = get_app_qt4([''])
141 app = get_app_qt4([""])
142 if not is_event_loop_running_qt4(app):
142 if not is_event_loop_running_qt4(app):
143 app._in_event_loop = True
143 app._in_event_loop = True
144 app.exec_()
144 app.exec_()
@@ -911,7 +911,12 b' class TerminalInteractiveShell(InteractiveShell):'
911
911
912 active_eventloop = None
912 active_eventloop = None
913 def enable_gui(self, gui=None):
913 def enable_gui(self, gui=None):
914 if self._inputhook is not None and gui is not None:
915 warn(
916 f"Shell was already running a gui event loop for {self.active_eventloop}; switching to {gui}."
917 )
914 if gui and (gui not in {"inline", "webagg"}):
918 if gui and (gui not in {"inline", "webagg"}):
919 # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
915 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
920 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
916 else:
921 else:
917 self.active_eventloop = self._inputhook = None
922 self.active_eventloop = self._inputhook = None
@@ -8,7 +8,6 b' aliases = {'
8
8
9 backends = [
9 backends = [
10 "qt",
10 "qt",
11 "qt4",
12 "qt5",
11 "qt5",
13 "qt6",
12 "qt6",
14 "gtk",
13 "gtk",
@@ -40,6 +39,80 b' class UnknownBackend(KeyError):'
40 ', '.join(backends + sorted(registered)))
39 ', '.join(backends + sorted(registered)))
41
40
42
41
42 def set_qt_api(gui):
43 """Sets the `QT_API` environment variable if it isn't already set."""
44
45 qt_api = os.environ.get("QT_API", None)
46
47 from IPython.external.qt_loaders import (
48 QT_API_PYQT,
49 QT_API_PYQT5,
50 QT_API_PYQT6,
51 QT_API_PYSIDE,
52 QT_API_PYSIDE2,
53 QT_API_PYSIDE6,
54 QT_API_PYQTv1,
55 loaded_api,
56 )
57
58 loaded = loaded_api()
59
60 qt_env2gui = {
61 QT_API_PYSIDE: "qt4",
62 QT_API_PYQTv1: "qt4",
63 QT_API_PYQT: "qt4",
64 QT_API_PYSIDE2: "qt5",
65 QT_API_PYQT5: "qt5",
66 QT_API_PYSIDE6: "qt6",
67 QT_API_PYQT6: "qt6",
68 }
69 if loaded is not None and gui != "qt":
70 if qt_env2gui[loaded] != gui:
71 print(
72 f"Cannot switch Qt versions for this session; must use {qt_env2gui[loaded]}."
73 )
74 return
75
76 if qt_api is not None and gui != "qt":
77 if qt_env2gui[qt_api] != gui:
78 print(
79 f'Request for "{gui}" will be ignored because `QT_API` '
80 f'environment variable is set to "{qt_api}"'
81 )
82 else:
83 if gui == "qt5":
84 try:
85 import PyQt5 # noqa
86
87 os.environ["QT_API"] = "pyqt5"
88 except ImportError:
89 try:
90 import PySide2 # noqa
91
92 os.environ["QT_API"] = "pyside2"
93 except ImportError:
94 os.environ["QT_API"] = "pyqt5"
95 elif gui == "qt6":
96 try:
97 import PyQt6 # noqa
98
99 os.environ["QT_API"] = "pyqt6"
100 except ImportError:
101 try:
102 import PySide6 # noqa
103
104 os.environ["QT_API"] = "pyside6"
105 except ImportError:
106 os.environ["QT_API"] = "pyqt6"
107 elif gui == "qt":
108 # Don't set QT_API; let IPython logic choose the version.
109 if "QT_API" in os.environ.keys():
110 del os.environ["QT_API"]
111 else:
112 print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".')
113 return
114
115
43 def get_inputhook_name_and_func(gui):
116 def get_inputhook_name_and_func(gui):
44 if gui in registered:
117 if gui in registered:
45 return gui, registered[gui]
118 return gui, registered[gui]
@@ -51,12 +124,9 b' def get_inputhook_name_and_func(gui):'
51 return get_inputhook_name_and_func(aliases[gui])
124 return get_inputhook_name_and_func(aliases[gui])
52
125
53 gui_mod = gui
126 gui_mod = gui
54 if gui == "qt5":
127 if gui.startswith("qt"):
55 os.environ["QT_API"] = "pyqt5"
128 set_qt_api(gui)
56 gui_mod = "qt"
57 elif gui == "qt6":
58 os.environ["QT_API"] = "pyqt6"
59 gui_mod = "qt"
129 gui_mod = "qt"
60
130
61 mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui_mod)
131 mod = importlib.import_module("IPython.terminal.pt_inputhooks." + gui_mod)
62 return gui, mod.inputhook
132 return gui, mod.inputhook
@@ -63,6 +63,7 b' def inputhook(context):'
63 timer = QtCore.QTimer()
63 timer = QtCore.QTimer()
64 timer.timeout.connect(event_loop.quit)
64 timer.timeout.connect(event_loop.quit)
65 while not context.input_is_ready():
65 while not context.input_is_ready():
66 # NOTE: run the event loop, and after 50 ms, call `quit` to exit it.
66 timer.start(50) # 50 ms
67 timer.start(50) # 50 ms
67 _exec(event_loop)
68 _exec(event_loop)
68 timer.stop()
69 timer.stop()
@@ -7,7 +7,7 b' loop, so you can use both a GUI and an interactive prompt together. IPython'
7 supports a number of common GUI toolkits, but from IPython 3.0, it is possible
7 supports a number of common GUI toolkits, but from IPython 3.0, it is possible
8 to integrate other event loops without modifying IPython itself.
8 to integrate other event loops without modifying IPython itself.
9
9
10 Supported event loops include ``qt4``, ``qt5``, ``gtk2``, ``gtk3``, ``gtk4``,
10 Supported event loops include ``qt5``, ``qt6``, ``gtk2``, ``gtk3``, ``gtk4``,
11 ``wx``, ``osx`` and ``tk``. Make sure the event loop you specify matches the
11 ``wx``, ``osx`` and ``tk``. Make sure the event loop you specify matches the
12 GUI toolkit used by your own code.
12 GUI toolkit used by your own code.
13
13
@@ -44,7 +44,7 b' the command-line by passing the full class name and a corresponding value; type'
44 <...snip...>
44 <...snip...>
45 --matplotlib=<CaselessStrEnum> (InteractiveShellApp.matplotlib)
45 --matplotlib=<CaselessStrEnum> (InteractiveShellApp.matplotlib)
46 Default: None
46 Default: None
47 Choices: ['auto', 'gtk', 'gtk3', 'gtk4', 'inline', 'nbagg', 'notebook', 'osx', 'qt', 'qt4', 'qt5', 'tk', 'wx']
47 Choices: ['auto', 'gtk', 'gtk3', 'gtk4', 'inline', 'nbagg', 'notebook', 'osx', 'qt', 'qt5', 'qt6', 'tk', 'wx']
48 Configure matplotlib for interactive use with the default matplotlib
48 Configure matplotlib for interactive use with the default matplotlib
49 backend.
49 backend.
50 <...snip...>
50 <...snip...>
@@ -892,7 +892,7 b' GUI event loop support'
892 ======================
892 ======================
893
893
894 IPython has excellent support for working interactively with Graphical User
894 IPython has excellent support for working interactively with Graphical User
895 Interface (GUI) toolkits, such as wxPython, PyQt4/PySide, PyGTK and Tk. This is
895 Interface (GUI) toolkits, such as wxPython, PyQt/PySide, PyGTK and Tk. This is
896 implemented by running the toolkit's event loop while IPython is waiting for
896 implemented by running the toolkit's event loop while IPython is waiting for
897 input.
897 input.
898
898
@@ -902,7 +902,7 b' For users, enabling GUI event loop integration is simple. You simple use the'
902 %gui [GUINAME]
902 %gui [GUINAME]
903
903
904 With no arguments, ``%gui`` removes all GUI support. Valid ``GUINAME``
904 With no arguments, ``%gui`` removes all GUI support. Valid ``GUINAME``
905 arguments include ``wx``, ``qt``, ``qt5``, ``gtk``, ``gtk3`` ``gtk4``, and
905 arguments include ``wx``, ``qt``, ``qt5``, ``qt6``, ``gtk``, ``gtk3`` ``gtk4``, and
906 ``tk``.
906 ``tk``.
907
907
908 Thus, to use wxPython interactively and create a running :class:`wx.App`
908 Thus, to use wxPython interactively and create a running :class:`wx.App`
@@ -936,16 +936,9 b' PyQt and PySide'
936 .. attempt at explanation of the complete mess that is Qt support
936 .. attempt at explanation of the complete mess that is Qt support
937
937
938 When you use ``--gui=qt`` or ``--matplotlib=qt``, IPython can work with either
938 When you use ``--gui=qt`` or ``--matplotlib=qt``, IPython can work with either
939 PyQt4 or PySide. There are three options for configuration here, because
939 PyQt or PySide. ``qt`` implies "use the latest version available", and it favors
940 PyQt4 has two APIs for QString and QVariant: v1, which is the default on
940 PyQt over PySide. To request a specific version, use ``qt5`` or ``qt6``. Note that
941 Python 2, and the more natural v2, which is the only API supported by PySide.
941 Qt4 is not supported with the ``--gui`` switch (and has not been for some time now).
942 v2 is also the default for PyQt4 on Python 3. IPython's code for the QtConsole
943 uses v2, but you can still use any interface in your code, since the
944 Qt frontend is in a different process.
945
946 The default will be to import PyQt4 without configuration of the APIs, thus
947 matching what most applications would expect. It will fall back to PySide if
948 PyQt4 is unavailable.
949
942
950 If specified, IPython will respect the environment variable ``QT_API`` used
943 If specified, IPython will respect the environment variable ``QT_API`` used
951 by ETS. ETS 4.0 also works with both PyQt4 and PySide, but it requires
944 by ETS. ETS 4.0 also works with both PyQt4 and PySide, but it requires
General Comments 0
You need to be logged in to leave comments. Login now