##// END OF EJS Templates
Backport PR #13085: ENH: add support for Qt6 input hooks
Matthias Bussonnier -
Show More
@@ -1,388 +1,389 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Pylab (matplotlib) support utilities."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from io import BytesIO
8 8 import warnings
9 9
10 10 from IPython.core.display import _pngxy
11 11 from IPython.utils.decorators import flag_calls
12 12
13 13 # If user specifies a GUI, that dictates the backend, otherwise we read the
14 14 # user's mpl default from the mpl rc structure
15 15 backends = {
16 16 "tk": "TkAgg",
17 17 "gtk": "GTKAgg",
18 18 "gtk3": "GTK3Agg",
19 19 "wx": "WXAgg",
20 20 "qt4": "Qt4Agg",
21 21 "qt5": "Qt5Agg",
22 "qt6": "QtAgg",
22 23 "qt": "Qt5Agg",
23 24 "osx": "MacOSX",
24 25 "nbagg": "nbAgg",
25 26 "notebook": "nbAgg",
26 27 "agg": "agg",
27 28 "svg": "svg",
28 29 "pdf": "pdf",
29 30 "ps": "ps",
30 31 "inline": "module://matplotlib_inline.backend_inline",
31 32 "ipympl": "module://ipympl.backend_nbagg",
32 33 "widget": "module://ipympl.backend_nbagg",
33 34 }
34 35
35 36 # We also need a reverse backends2guis mapping that will properly choose which
36 37 # GUI support to activate based on the desired matplotlib backend. For the
37 38 # most part it's just a reverse of the above dict, but we also need to add a
38 39 # few others that map to the same GUI manually:
39 40 backend2gui = dict(zip(backends.values(), backends.keys()))
40 41 # Our tests expect backend2gui to just return 'qt'
41 42 backend2gui['Qt4Agg'] = 'qt'
42 43 # In the reverse mapping, there are a few extra valid matplotlib backends that
43 44 # map to the same GUI support
44 45 backend2gui['GTK'] = backend2gui['GTKCairo'] = 'gtk'
45 46 backend2gui['GTK3Cairo'] = 'gtk3'
46 47 backend2gui['WX'] = 'wx'
47 48 backend2gui['CocoaAgg'] = 'osx'
48 49 # And some backends that don't need GUI integration
49 50 del backend2gui["nbAgg"]
50 51 del backend2gui["agg"]
51 52 del backend2gui["svg"]
52 53 del backend2gui["pdf"]
53 54 del backend2gui["ps"]
54 55 del backend2gui["module://matplotlib_inline.backend_inline"]
55 56
56 57 #-----------------------------------------------------------------------------
57 58 # Matplotlib utilities
58 59 #-----------------------------------------------------------------------------
59 60
60 61
61 62 def getfigs(*fig_nums):
62 63 """Get a list of matplotlib figures by figure numbers.
63 64
64 65 If no arguments are given, all available figures are returned. If the
65 66 argument list contains references to invalid figures, a warning is printed
66 67 but the function continues pasting further figures.
67 68
68 69 Parameters
69 70 ----------
70 71 figs : tuple
71 72 A tuple of ints giving the figure numbers of the figures to return.
72 73 """
73 74 from matplotlib._pylab_helpers import Gcf
74 75 if not fig_nums:
75 76 fig_managers = Gcf.get_all_fig_managers()
76 77 return [fm.canvas.figure for fm in fig_managers]
77 78 else:
78 79 figs = []
79 80 for num in fig_nums:
80 81 f = Gcf.figs.get(num)
81 82 if f is None:
82 83 print('Warning: figure %s not available.' % num)
83 84 else:
84 85 figs.append(f.canvas.figure)
85 86 return figs
86 87
87 88
88 89 def figsize(sizex, sizey):
89 90 """Set the default figure size to be [sizex, sizey].
90 91
91 92 This is just an easy to remember, convenience wrapper that sets::
92 93
93 94 matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
94 95 """
95 96 import matplotlib
96 97 matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
97 98
98 99
99 100 def print_figure(fig, fmt='png', bbox_inches='tight', **kwargs):
100 101 """Print a figure to an image, and return the resulting file data
101 102
102 103 Returned data will be bytes unless ``fmt='svg'``,
103 104 in which case it will be unicode.
104 105
105 106 Any keyword args are passed to fig.canvas.print_figure,
106 107 such as ``quality`` or ``bbox_inches``.
107 108 """
108 109 # When there's an empty figure, we shouldn't return anything, otherwise we
109 110 # get big blank areas in the qt console.
110 111 if not fig.axes and not fig.lines:
111 112 return
112 113
113 114 dpi = fig.dpi
114 115 if fmt == 'retina':
115 116 dpi = dpi * 2
116 117 fmt = 'png'
117 118
118 119 # build keyword args
119 120 kw = {
120 121 "format":fmt,
121 122 "facecolor":fig.get_facecolor(),
122 123 "edgecolor":fig.get_edgecolor(),
123 124 "dpi":dpi,
124 125 "bbox_inches":bbox_inches,
125 126 }
126 127 # **kwargs get higher priority
127 128 kw.update(kwargs)
128 129
129 130 bytes_io = BytesIO()
130 131 if fig.canvas is None:
131 132 from matplotlib.backend_bases import FigureCanvasBase
132 133 FigureCanvasBase(fig)
133 134
134 135 fig.canvas.print_figure(bytes_io, **kw)
135 136 data = bytes_io.getvalue()
136 137 if fmt == 'svg':
137 138 data = data.decode('utf-8')
138 139 return data
139 140
140 141 def retina_figure(fig, **kwargs):
141 142 """format a figure as a pixel-doubled (retina) PNG"""
142 143 pngdata = print_figure(fig, fmt='retina', **kwargs)
143 144 # Make sure that retina_figure acts just like print_figure and returns
144 145 # None when the figure is empty.
145 146 if pngdata is None:
146 147 return
147 148 w, h = _pngxy(pngdata)
148 149 metadata = {"width": w//2, "height":h//2}
149 150 return pngdata, metadata
150 151
151 152 # We need a little factory function here to create the closure where
152 153 # safe_execfile can live.
153 154 def mpl_runner(safe_execfile):
154 155 """Factory to return a matplotlib-enabled runner for %run.
155 156
156 157 Parameters
157 158 ----------
158 159 safe_execfile : function
159 160 This must be a function with the same interface as the
160 161 :meth:`safe_execfile` method of IPython.
161 162
162 163 Returns
163 164 -------
164 165 A function suitable for use as the ``runner`` argument of the %run magic
165 166 function.
166 167 """
167 168
168 169 def mpl_execfile(fname,*where,**kw):
169 170 """matplotlib-aware wrapper around safe_execfile.
170 171
171 172 Its interface is identical to that of the :func:`execfile` builtin.
172 173
173 174 This is ultimately a call to execfile(), but wrapped in safeties to
174 175 properly handle interactive rendering."""
175 176
176 177 import matplotlib
177 178 import matplotlib.pyplot as plt
178 179
179 180 #print '*** Matplotlib runner ***' # dbg
180 181 # turn off rendering until end of script
181 182 is_interactive = matplotlib.rcParams['interactive']
182 183 matplotlib.interactive(False)
183 184 safe_execfile(fname,*where,**kw)
184 185 matplotlib.interactive(is_interactive)
185 186 # make rendering call now, if the user tried to do it
186 187 if plt.draw_if_interactive.called:
187 188 plt.draw()
188 189 plt.draw_if_interactive.called = False
189 190
190 191 # re-draw everything that is stale
191 192 try:
192 193 da = plt.draw_all
193 194 except AttributeError:
194 195 pass
195 196 else:
196 197 da()
197 198
198 199 return mpl_execfile
199 200
200 201
201 202 def _reshow_nbagg_figure(fig):
202 203 """reshow an nbagg figure"""
203 204 try:
204 205 reshow = fig.canvas.manager.reshow
205 206 except AttributeError:
206 207 raise NotImplementedError()
207 208 else:
208 209 reshow()
209 210
210 211
211 212 def select_figure_formats(shell, formats, **kwargs):
212 213 """Select figure formats for the inline backend.
213 214
214 215 Parameters
215 216 ==========
216 217 shell : InteractiveShell
217 218 The main IPython instance.
218 219 formats : str or set
219 220 One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
220 221 **kwargs : any
221 222 Extra keyword arguments to be passed to fig.canvas.print_figure.
222 223 """
223 224 import matplotlib
224 225 from matplotlib.figure import Figure
225 226
226 227 svg_formatter = shell.display_formatter.formatters['image/svg+xml']
227 228 png_formatter = shell.display_formatter.formatters['image/png']
228 229 jpg_formatter = shell.display_formatter.formatters['image/jpeg']
229 230 pdf_formatter = shell.display_formatter.formatters['application/pdf']
230 231
231 232 if isinstance(formats, str):
232 233 formats = {formats}
233 234 # cast in case of list / tuple
234 235 formats = set(formats)
235 236
236 237 [ f.pop(Figure, None) for f in shell.display_formatter.formatters.values() ]
237 238 mplbackend = matplotlib.get_backend().lower()
238 239 if mplbackend == 'nbagg' or mplbackend == 'module://ipympl.backend_nbagg':
239 240 formatter = shell.display_formatter.ipython_display_formatter
240 241 formatter.for_type(Figure, _reshow_nbagg_figure)
241 242
242 243 supported = {'png', 'png2x', 'retina', 'jpg', 'jpeg', 'svg', 'pdf'}
243 244 bad = formats.difference(supported)
244 245 if bad:
245 246 bs = "%s" % ','.join([repr(f) for f in bad])
246 247 gs = "%s" % ','.join([repr(f) for f in supported])
247 248 raise ValueError("supported formats are: %s not %s" % (gs, bs))
248 249
249 250 if 'png' in formats:
250 251 png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png', **kwargs))
251 252 if 'retina' in formats or 'png2x' in formats:
252 253 png_formatter.for_type(Figure, lambda fig: retina_figure(fig, **kwargs))
253 254 if 'jpg' in formats or 'jpeg' in formats:
254 255 jpg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'jpg', **kwargs))
255 256 if 'svg' in formats:
256 257 svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg', **kwargs))
257 258 if 'pdf' in formats:
258 259 pdf_formatter.for_type(Figure, lambda fig: print_figure(fig, 'pdf', **kwargs))
259 260
260 261 #-----------------------------------------------------------------------------
261 262 # Code for initializing matplotlib and importing pylab
262 263 #-----------------------------------------------------------------------------
263 264
264 265
265 266 def find_gui_and_backend(gui=None, gui_select=None):
266 267 """Given a gui string return the gui and mpl backend.
267 268
268 269 Parameters
269 270 ----------
270 271 gui : str
271 272 Can be one of ('tk','gtk','wx','qt','qt4','inline','agg').
272 273 gui_select : str
273 274 Can be one of ('tk','gtk','wx','qt','qt4','inline').
274 275 This is any gui already selected by the shell.
275 276
276 277 Returns
277 278 -------
278 279 A tuple of (gui, backend) where backend is one of ('TkAgg','GTKAgg',
279 280 'WXAgg','Qt4Agg','module://matplotlib_inline.backend_inline','agg').
280 281 """
281 282
282 283 import matplotlib
283 284
284 285 if gui and gui != 'auto':
285 286 # select backend based on requested gui
286 287 backend = backends[gui]
287 288 if gui == 'agg':
288 289 gui = None
289 290 else:
290 291 # We need to read the backend from the original data structure, *not*
291 292 # from mpl.rcParams, since a prior invocation of %matplotlib may have
292 293 # overwritten that.
293 294 # WARNING: this assumes matplotlib 1.1 or newer!!
294 295 backend = matplotlib.rcParamsOrig['backend']
295 296 # In this case, we need to find what the appropriate gui selection call
296 297 # should be for IPython, so we can activate inputhook accordingly
297 298 gui = backend2gui.get(backend, None)
298 299
299 300 # If we have already had a gui active, we need it and inline are the
300 301 # ones allowed.
301 302 if gui_select and gui != gui_select:
302 303 gui = gui_select
303 304 backend = backends[gui]
304 305
305 306 return gui, backend
306 307
307 308
308 309 def activate_matplotlib(backend):
309 310 """Activate the given backend and set interactive to True."""
310 311
311 312 import matplotlib
312 313 matplotlib.interactive(True)
313 314
314 315 # Matplotlib had a bug where even switch_backend could not force
315 316 # the rcParam to update. This needs to be set *before* the module
316 317 # magic of switch_backend().
317 318 matplotlib.rcParams['backend'] = backend
318 319
319 320 # Due to circular imports, pyplot may be only partially initialised
320 321 # when this function runs.
321 322 # So avoid needing matplotlib attribute-lookup to access pyplot.
322 323 from matplotlib import pyplot as plt
323 324
324 325 plt.switch_backend(backend)
325 326
326 327 plt.show._needmain = False
327 328 # We need to detect at runtime whether show() is called by the user.
328 329 # For this, we wrap it into a decorator which adds a 'called' flag.
329 330 plt.draw_if_interactive = flag_calls(plt.draw_if_interactive)
330 331
331 332
332 333 def import_pylab(user_ns, import_all=True):
333 334 """Populate the namespace with pylab-related values.
334 335
335 336 Imports matplotlib, pylab, numpy, and everything from pylab and numpy.
336 337
337 338 Also imports a few names from IPython (figsize, display, getfigs)
338 339
339 340 """
340 341
341 342 # Import numpy as np/pyplot as plt are conventions we're trying to
342 343 # somewhat standardize on. Making them available to users by default
343 344 # will greatly help this.
344 345 s = ("import numpy\n"
345 346 "import matplotlib\n"
346 347 "from matplotlib import pylab, mlab, pyplot\n"
347 348 "np = numpy\n"
348 349 "plt = pyplot\n"
349 350 )
350 351 exec(s, user_ns)
351 352
352 353 if import_all:
353 354 s = ("from matplotlib.pylab import *\n"
354 355 "from numpy import *\n")
355 356 exec(s, user_ns)
356 357
357 358 # IPython symbols to add
358 359 user_ns['figsize'] = figsize
359 360 from IPython.core.display import display
360 361 # Add display and getfigs to the user's namespace
361 362 user_ns['display'] = display
362 363 user_ns['getfigs'] = getfigs
363 364
364 365
365 366 def configure_inline_support(shell, backend):
366 367 """
367 368 .. deprecated: 7.23
368 369
369 370 use `matplotlib_inline.backend_inline.configure_inline_support()`
370 371
371 372 Configure an IPython shell object for matplotlib use.
372 373
373 374 Parameters
374 375 ----------
375 376 shell : InteractiveShell instance
376 377
377 378 backend : matplotlib backend
378 379 """
379 380 warnings.warn(
380 381 "`configure_inline_support` is deprecated since IPython 7.23, directly "
381 382 "use `matplotlib_inline.backend_inline.configure_inline_support()`",
382 383 DeprecationWarning,
383 384 stacklevel=2,
384 385 )
385 386
386 387 from matplotlib_inline.backend_inline import configure_inline_support as configure_inline_support_orig
387 388
388 389 configure_inline_support_orig(shell, backend)
@@ -1,95 +1,129 b''
1 1 """ Import Qt in a manner suitable for an IPython kernel.
2 2
3 3 This is the import used for the `gui=qt` or `matplotlib=qt` initialization.
4 4
5 5 Import Priority:
6 6
7 7 if Qt has been imported anywhere else:
8 8 use that
9 9
10 10 if matplotlib has been imported and doesn't support v2 (<= 1.0.1):
11 11 use PyQt4 @v1
12 12
13 13 Next, ask QT_API env variable
14 14
15 15 if QT_API not set:
16 16 ask matplotlib what it's using. If Qt4Agg or Qt5Agg, then use the
17 17 version matplotlib is configured with
18 18
19 19 else: (matplotlib said nothing)
20 20 # this is the default path - nobody told us anything
21 21 try in this order:
22 22 PyQt default version, PySide, PyQt5
23 23 else:
24 24 use what QT_API says
25 25
26 26 """
27 27 # NOTE: This is no longer an external, third-party module, and should be
28 28 # considered part of IPython. For compatibility however, it is being kept in
29 29 # IPython/external.
30 30
31 31 import os
32 32 import sys
33 33
34 34 from IPython.utils.version import check_version
35 from IPython.external.qt_loaders import (load_qt, loaded_api, QT_API_PYSIDE,
36 QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT5,
37 QT_API_PYQTv1, QT_API_PYQT_DEFAULT)
35 from IPython.external.qt_loaders import (
36 load_qt,
37 loaded_api,
38 enum_factory,
39 # QT6
40 QT_API_PYQT6,
41 QT_API_PYSIDE6,
42 # QT5
43 QT_API_PYQT5,
44 QT_API_PYSIDE2,
45 # QT4
46 QT_API_PYQTv1,
47 QT_API_PYQT,
48 QT_API_PYSIDE,
49 # default
50 QT_API_PYQT_DEFAULT,
51 )
52
53 _qt_apis = (
54 # QT6
55 QT_API_PYQT6,
56 QT_API_PYSIDE6,
57 # QT5
58 QT_API_PYQT5,
59 QT_API_PYSIDE2,
60 # QT4
61 QT_API_PYQTv1,
62 QT_API_PYQT,
63 QT_API_PYSIDE,
64 # default
65 QT_API_PYQT_DEFAULT,
66 )
38 67
39 _qt_apis = (QT_API_PYSIDE, QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT5, QT_API_PYQTv1,
40 QT_API_PYQT_DEFAULT)
41 68
42 #Constraints placed on an imported matplotlib
43 69 def matplotlib_options(mpl):
70 """Constraints placed on an imported matplotlib."""
44 71 if mpl is None:
45 72 return
46 73 backend = mpl.rcParams.get('backend', None)
47 74 if backend == 'Qt4Agg':
48 75 mpqt = mpl.rcParams.get('backend.qt4', None)
49 76 if mpqt is None:
50 77 return None
51 78 if mpqt.lower() == 'pyside':
52 79 return [QT_API_PYSIDE]
53 80 elif mpqt.lower() == 'pyqt4':
54 81 return [QT_API_PYQT_DEFAULT]
55 82 elif mpqt.lower() == 'pyqt4v2':
56 83 return [QT_API_PYQT]
57 84 raise ImportError("unhandled value for backend.qt4 from matplotlib: %r" %
58 85 mpqt)
59 86 elif backend == 'Qt5Agg':
60 87 mpqt = mpl.rcParams.get('backend.qt5', None)
61 88 if mpqt is None:
62 89 return None
63 90 if mpqt.lower() == 'pyqt5':
64 91 return [QT_API_PYQT5]
65 92 raise ImportError("unhandled value for backend.qt5 from matplotlib: %r" %
66 93 mpqt)
67 94
68 95 def get_options():
69 """Return a list of acceptable QT APIs, in decreasing order of
70 preference
71 """
96 """Return a list of acceptable QT APIs, in decreasing order of preference."""
72 97 #already imported Qt somewhere. Use that
73 98 loaded = loaded_api()
74 99 if loaded is not None:
75 100 return [loaded]
76 101
77 102 mpl = sys.modules.get('matplotlib', None)
78 103
79 104 if mpl is not None and not check_version(mpl.__version__, '1.0.2'):
80 105 #1.0.1 only supports PyQt4 v1
81 106 return [QT_API_PYQT_DEFAULT]
82 107
83 108 qt_api = os.environ.get('QT_API', None)
84 109 if qt_api is None:
85 110 #no ETS variable. Ask mpl, then use default fallback path
86 return matplotlib_options(mpl) or [QT_API_PYQT_DEFAULT, QT_API_PYSIDE,
87 QT_API_PYQT5, QT_API_PYSIDE2]
111 return matplotlib_options(mpl) or [
112 QT_API_PYQT_DEFAULT,
113 QT_API_PYQT6,
114 QT_API_PYSIDE6,
115 QT_API_PYQT5,
116 QT_API_PYSIDE2,
117 QT_API_PYQT,
118 QT_API_PYSIDE,
119 ]
88 120 elif qt_api not in _qt_apis:
89 121 raise RuntimeError("Invalid Qt API %r, valid values are: %r" %
90 122 (qt_api, ', '.join(_qt_apis)))
91 123 else:
92 124 return [qt_api]
93 125
126
94 127 api_opts = get_options()
95 128 QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts)
129 enum_helper = enum_factory(QT_API, QtCore)
@@ -1,334 +1,401 b''
1 1 """
2 2 This module contains factory functions that attempt
3 3 to return Qt submodules from the various python Qt bindings.
4 4
5 5 It also protects against double-importing Qt with different
6 6 bindings, which is unstable and likely to crash
7 7
8 8 This is used primarily by qt and qt_for_kernel, and shouldn't
9 9 be accessed directly from the outside
10 10 """
11 11 import sys
12 12 import types
13 from functools import partial
14 from importlib import import_module
13 from functools import partial, lru_cache
14 import operator
15 15
16 16 from IPython.utils.version import check_version
17 17
18 # Available APIs.
19 QT_API_PYQT = 'pyqt' # Force version 2
18 # ### Available APIs.
19 # Qt6
20 QT_API_PYQT6 = "pyqt6"
21 QT_API_PYSIDE6 = "pyside6"
22
23 # Qt5
20 24 QT_API_PYQT5 = 'pyqt5'
21 QT_API_PYQTv1 = 'pyqtv1' # Force version 2
22 QT_API_PYQT_DEFAULT = 'pyqtdefault' # use system default for version 1 vs. 2
23 QT_API_PYSIDE = 'pyside'
24 25 QT_API_PYSIDE2 = 'pyside2'
25 26
26 api_to_module = {QT_API_PYSIDE2: 'PySide2',
27 QT_API_PYSIDE: 'PySide',
28 QT_API_PYQT: 'PyQt4',
29 QT_API_PYQTv1: 'PyQt4',
30 QT_API_PYQT5: 'PyQt5',
31 QT_API_PYQT_DEFAULT: 'PyQt4',
32 }
27 # Qt4
28 QT_API_PYQT = "pyqt" # Force version 2
29 QT_API_PYQTv1 = "pyqtv1" # Force version 2
30 QT_API_PYSIDE = "pyside"
31
32 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
33
34 api_to_module = {
35 # Qt6
36 QT_API_PYQT6: "PyQt6",
37 QT_API_PYSIDE6: "PySide6",
38 # Qt5
39 QT_API_PYQT5: "PyQt5",
40 QT_API_PYSIDE2: "PySide2",
41 # Qt4
42 QT_API_PYSIDE: "PySide",
43 QT_API_PYQT: "PyQt4",
44 QT_API_PYQTv1: "PyQt4",
45 # default
46 QT_API_PYQT_DEFAULT: "PyQt6",
47 }
33 48
34 49
35 50 class ImportDenier(object):
36 51 """Import Hook that will guard against bad Qt imports
37 52 once IPython commits to a specific binding
38 53 """
39 54
40 55 def __init__(self):
41 56 self.__forbidden = set()
42 57
43 58 def forbid(self, module_name):
44 59 sys.modules.pop(module_name, None)
45 60 self.__forbidden.add(module_name)
46 61
47 62 def find_module(self, fullname, path=None):
48 63 if path:
49 64 return
50 65 if fullname in self.__forbidden:
51 66 return self
52 67
53 68 def load_module(self, fullname):
54 69 raise ImportError("""
55 70 Importing %s disabled by IPython, which has
56 71 already imported an Incompatible QT Binding: %s
57 72 """ % (fullname, loaded_api()))
58 73
74
59 75 ID = ImportDenier()
60 76 sys.meta_path.insert(0, ID)
61 77
62 78
63 79 def commit_api(api):
64 80 """Commit to a particular API, and trigger ImportErrors on subsequent
65 81 dangerous imports"""
82 modules = set(api_to_module.values())
66 83
67 if api == QT_API_PYSIDE2:
68 ID.forbid('PySide')
69 ID.forbid('PyQt4')
70 ID.forbid('PyQt5')
71 elif api == QT_API_PYSIDE:
72 ID.forbid('PySide2')
73 ID.forbid('PyQt4')
74 ID.forbid('PyQt5')
75 elif api == QT_API_PYQT5:
76 ID.forbid('PySide2')
77 ID.forbid('PySide')
78 ID.forbid('PyQt4')
79 else: # There are three other possibilities, all representing PyQt4
80 ID.forbid('PyQt5')
81 ID.forbid('PySide2')
82 ID.forbid('PySide')
84 modules.remove(api_to_module[api])
85 for mod in modules:
86 ID.forbid(mod)
83 87
84 88
85 89 def loaded_api():
86 90 """Return which API is loaded, if any
87 91
88 92 If this returns anything besides None,
89 93 importing any other Qt binding is unsafe.
90 94
91 95 Returns
92 96 -------
93 None, 'pyside2', 'pyside', 'pyqt', 'pyqt5', or 'pyqtv1'
97 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
94 98 """
95 if 'PyQt4.QtCore' in sys.modules:
99 if sys.modules.get("PyQt6.QtCore"):
100 return QT_API_PYQT6
101 elif sys.modules.get("PySide6.QtCore"):
102 return QT_API_PYSIDE6
103 elif sys.modules.get("PyQt5.QtCore"):
104 return QT_API_PYQT5
105 elif sys.modules.get("PySide2.QtCore"):
106 return QT_API_PYSIDE2
107 elif sys.modules.get("PyQt4.QtCore"):
96 108 if qtapi_version() == 2:
97 109 return QT_API_PYQT
98 110 else:
99 111 return QT_API_PYQTv1
100 elif 'PySide.QtCore' in sys.modules:
112 elif sys.modules.get("PySide.QtCore"):
101 113 return QT_API_PYSIDE
102 elif 'PySide2.QtCore' in sys.modules:
103 return QT_API_PYSIDE2
104 elif 'PyQt5.QtCore' in sys.modules:
105 return QT_API_PYQT5
114
106 115 return None
107 116
108 117
109 118 def has_binding(api):
110 119 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
111 120
112 121 Parameters
113 122 ----------
114 123 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
115 124 Which module to check for
116 125
117 126 Returns
118 127 -------
119 128 True if the relevant module appears to be importable
120 129 """
121 130 module_name = api_to_module[api]
122 131 from importlib.util import find_spec
123 132
124 133 required = ['QtCore', 'QtGui', 'QtSvg']
125 if api in (QT_API_PYQT5, QT_API_PYSIDE2):
134 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
126 135 # QT5 requires QtWidgets too
127 136 required.append('QtWidgets')
128 137
129 138 for submod in required:
130 139 try:
131 140 spec = find_spec('%s.%s' % (module_name, submod))
132 141 except ImportError:
133 142 # Package (e.g. PyQt5) not found
134 143 return False
135 144 else:
136 145 if spec is None:
137 146 # Submodule (e.g. PyQt5.QtCore) not found
138 147 return False
139 148
140 149 if api == QT_API_PYSIDE:
141 150 # We can also safely check PySide version
142 151 import PySide
143 152 return check_version(PySide.__version__, '1.0.3')
144 153
145 154 return True
146 155
147 156
148 157 def qtapi_version():
149 158 """Return which QString API has been set, if any
150 159
151 160 Returns
152 161 -------
153 162 The QString API version (1 or 2), or None if not set
154 163 """
155 164 try:
156 165 import sip
157 166 except ImportError:
158 167 # as of PyQt5 5.11, sip is no longer available as a top-level
159 168 # module and needs to be imported from the PyQt5 namespace
160 169 try:
161 170 from PyQt5 import sip
162 171 except ImportError:
163 172 return
164 173 try:
165 174 return sip.getapi('QString')
166 175 except ValueError:
167 176 return
168 177
169 178
170 179 def can_import(api):
171 180 """Safely query whether an API is importable, without importing it"""
172 181 if not has_binding(api):
173 182 return False
174 183
175 184 current = loaded_api()
176 185 if api == QT_API_PYQT_DEFAULT:
177 return current in [QT_API_PYQT, QT_API_PYQTv1, None]
186 return current in [QT_API_PYQT6, None]
178 187 else:
179 188 return current in [api, None]
180 189
181 190
182 191 def import_pyqt4(version=2):
183 192 """
184 193 Import PyQt4
185 194
186 195 Parameters
187 196 ----------
188 197 version : 1, 2, or None
189 198 Which QString/QVariant API to use. Set to None to use the system
190 199 default
191 200
192 201 ImportErrors rasied within this function are non-recoverable
193 202 """
194 203 # The new-style string API (version=2) automatically
195 204 # converts QStrings to Unicode Python strings. Also, automatically unpacks
196 205 # QVariants to their underlying objects.
197 206 import sip
198 207
199 208 if version is not None:
200 209 sip.setapi('QString', version)
201 210 sip.setapi('QVariant', version)
202 211
203 212 from PyQt4 import QtGui, QtCore, QtSvg
204 213
205 214 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
206 215 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
207 216 QtCore.PYQT_VERSION_STR)
208 217
209 218 # Alias PyQt-specific functions for PySide compatibility.
210 219 QtCore.Signal = QtCore.pyqtSignal
211 220 QtCore.Slot = QtCore.pyqtSlot
212 221
213 222 # query for the API version (in case version == None)
214 223 version = sip.getapi('QString')
215 224 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
216 225 return QtCore, QtGui, QtSvg, api
217 226
218 227
219 228 def import_pyqt5():
220 229 """
221 230 Import PyQt5
222 231
223 232 ImportErrors rasied within this function are non-recoverable
224 233 """
225 234
226 235 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
227
236
228 237 # Alias PyQt-specific functions for PySide compatibility.
229 238 QtCore.Signal = QtCore.pyqtSignal
230 239 QtCore.Slot = QtCore.pyqtSlot
231 240
232 241 # Join QtGui and QtWidgets for Qt4 compatibility.
233 242 QtGuiCompat = types.ModuleType('QtGuiCompat')
234 243 QtGuiCompat.__dict__.update(QtGui.__dict__)
235 244 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
236 245
237 246 api = QT_API_PYQT5
238 247 return QtCore, QtGuiCompat, QtSvg, api
239 248
240 249
250 def import_pyqt6():
251 """
252 Import PyQt6
253
254 ImportErrors rasied within this function are non-recoverable
255 """
256
257 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
258
259 # Alias PyQt-specific functions for PySide compatibility.
260 QtCore.Signal = QtCore.pyqtSignal
261 QtCore.Slot = QtCore.pyqtSlot
262
263 # Join QtGui and QtWidgets for Qt4 compatibility.
264 QtGuiCompat = types.ModuleType("QtGuiCompat")
265 QtGuiCompat.__dict__.update(QtGui.__dict__)
266 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
267
268 api = QT_API_PYQT6
269 return QtCore, QtGuiCompat, QtSvg, api
270
271
241 272 def import_pyside():
242 273 """
243 274 Import PySide
244 275
245 276 ImportErrors raised within this function are non-recoverable
246 277 """
247 278 from PySide import QtGui, QtCore, QtSvg
248 279 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
249 280
250 281 def import_pyside2():
251 282 """
252 283 Import PySide2
253 284
254 285 ImportErrors raised within this function are non-recoverable
255 286 """
256 287 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
257 288
258 289 # Join QtGui and QtWidgets for Qt4 compatibility.
259 290 QtGuiCompat = types.ModuleType('QtGuiCompat')
260 291 QtGuiCompat.__dict__.update(QtGui.__dict__)
261 292 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
262 293 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
263 294
264 295 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
265 296
266 297
298 def import_pyside6():
299 """
300 Import PySide6
301
302 ImportErrors raised within this function are non-recoverable
303 """
304 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
305
306 # Join QtGui and QtWidgets for Qt4 compatibility.
307 QtGuiCompat = types.ModuleType("QtGuiCompat")
308 QtGuiCompat.__dict__.update(QtGui.__dict__)
309 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
310 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
311
312 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
313
314
267 315 def load_qt(api_options):
268 316 """
269 317 Attempt to import Qt, given a preference list
270 318 of permissible bindings
271 319
272 320 It is safe to call this function multiple times.
273 321
274 322 Parameters
275 323 ----------
276 324 api_options: List of strings
277 325 The order of APIs to try. Valid items are 'pyside', 'pyside2',
278 326 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
279 327
280 328 Returns
281 329 -------
282 330
283 331 A tuple of QtCore, QtGui, QtSvg, QT_API
284 332 The first three are the Qt modules. The last is the
285 333 string indicating which module was loaded.
286 334
287 335 Raises
288 336 ------
289 337 ImportError, if it isn't possible to import any requested
290 338 bindings (either because they aren't installed, or because
291 339 an incompatible library has already been installed)
292 340 """
293 341 loaders = {
294 QT_API_PYSIDE2: import_pyside2,
295 QT_API_PYSIDE: import_pyside,
296 QT_API_PYQT: import_pyqt4,
297 QT_API_PYQT5: import_pyqt5,
298 QT_API_PYQTv1: partial(import_pyqt4, version=1),
299 QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None)
300 }
342 # Qt6
343 QT_API_PYQT6: import_pyqt6,
344 QT_API_PYSIDE6: import_pyside6,
345 # Qt5
346 QT_API_PYQT5: import_pyqt5,
347 QT_API_PYSIDE2: import_pyside2,
348 # Qt4
349 QT_API_PYSIDE: import_pyside,
350 QT_API_PYQT: import_pyqt4,
351 QT_API_PYQTv1: partial(import_pyqt4, version=1),
352 # default
353 QT_API_PYQT_DEFAULT: import_pyqt6,
354 }
301 355
302 356 for api in api_options:
303 357
304 358 if api not in loaders:
305 359 raise RuntimeError(
306 360 "Invalid Qt API %r, valid values are: %s" %
307 361 (api, ", ".join(["%r" % k for k in loaders.keys()])))
308 362
309 363 if not can_import(api):
310 364 continue
311 365
312 366 #cannot safely recover from an ImportError during this
313 367 result = loaders[api]()
314 368 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
315 369 commit_api(api)
316 370 return result
317 371 else:
318 372 raise ImportError("""
319 373 Could not load requested Qt binding. Please ensure that
320 374 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
321 375 and only one is imported per session.
322 376
323 377 Currently-imported Qt library: %r
324 378 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
325 379 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
326 380 PySide >= 1.0.3 installed: %s
327 381 PySide2 installed: %s
328 382 Tried to load: %r
329 383 """ % (loaded_api(),
330 384 has_binding(QT_API_PYQT),
331 385 has_binding(QT_API_PYQT5),
332 386 has_binding(QT_API_PYSIDE),
333 387 has_binding(QT_API_PYSIDE2),
334 388 api_options))
389
390
391 def enum_factory(QT_API, QtCore):
392 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
393
394 @lru_cache(None)
395 def _enum(name):
396 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
397 return operator.attrgetter(
398 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
399 )(sys.modules[QtCore.__package__])
400
401 return _enum
@@ -1,50 +1,61 b''
1 1 import importlib
2 2 import os
3 3
4 4 aliases = {
5 5 'qt4': 'qt',
6 6 'gtk2': 'gtk',
7 7 }
8 8
9 9 backends = [
10 'qt', 'qt4', 'qt5',
11 'gtk', 'gtk2', 'gtk3',
12 'tk',
13 'wx',
14 'pyglet', 'glut',
15 'osx',
16 'asyncio'
10 "qt",
11 "qt4",
12 "qt5",
13 "qt6",
14 "gtk",
15 "gtk2",
16 "gtk3",
17 "tk",
18 "wx",
19 "pyglet",
20 "glut",
21 "osx",
22 "asyncio",
17 23 ]
18 24
19 25 registered = {}
20 26
21 27 def register(name, inputhook):
22 28 """Register the function *inputhook* as an event loop integration."""
23 29 registered[name] = inputhook
24 30
31
25 32 class UnknownBackend(KeyError):
26 33 def __init__(self, name):
27 34 self.name = name
28 35
29 36 def __str__(self):
30 37 return ("No event loop integration for {!r}. "
31 38 "Supported event loops are: {}").format(self.name,
32 39 ', '.join(backends + sorted(registered)))
33 40
41
34 42 def get_inputhook_name_and_func(gui):
35 43 if gui in registered:
36 44 return gui, registered[gui]
37 45
38 46 if gui not in backends:
39 47 raise UnknownBackend(gui)
40 48
41 49 if gui in aliases:
42 50 return get_inputhook_name_and_func(aliases[gui])
43 51
44 52 gui_mod = gui
45 if gui == 'qt5':
46 os.environ['QT_API'] = 'pyqt5'
47 gui_mod = 'qt'
53 if gui == "qt5":
54 os.environ["QT_API"] = "pyqt5"
55 gui_mod = "qt"
56 elif gui == "qt6":
57 os.environ["QT_API"] = "pyqt6"
58 gui_mod = "qt"
48 59
49 60 mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui_mod)
50 61 return gui, mod.inputhook
@@ -1,68 +1,83 b''
1 1 import sys
2 2 import os
3 from IPython.external.qt_for_kernel import QtCore, QtGui
3 from IPython.external.qt_for_kernel import QtCore, QtGui, enum_helper
4 4 from IPython import get_ipython
5 5
6 6 # If we create a QApplication, keep a reference to it so that it doesn't get
7 7 # garbage collected.
8 8 _appref = None
9 9 _already_warned = False
10 10
11 11
12 def _exec(obj):
13 # exec on PyQt6, exec_ elsewhere.
14 obj.exec() if hasattr(obj, "exec") else obj.exec_()
15
16
12 17 def _reclaim_excepthook():
13 18 shell = get_ipython()
14 19 if shell is not None:
15 20 sys.excepthook = shell.excepthook
16 21
17 22
18 23 def inputhook(context):
19 24 global _appref
20 25 app = QtCore.QCoreApplication.instance()
21 26 if not app:
22 27 if sys.platform == 'linux':
23 28 if not os.environ.get('DISPLAY') \
24 29 and not os.environ.get('WAYLAND_DISPLAY'):
25 30 import warnings
26 31 global _already_warned
27 32 if not _already_warned:
28 33 _already_warned = True
29 34 warnings.warn(
30 35 'The DISPLAY or WAYLAND_DISPLAY environment variable is '
31 36 'not set or empty and Qt5 requires this environment '
32 37 'variable. Deactivate Qt5 code.'
33 38 )
34 39 return
35 QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
40 try:
41 QtCore.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
42 except AttributeError: # Only for Qt>=5.6, <6.
43 pass
44 try:
45 QtCore.QApplication.setHighDpiScaleFactorRoundingPolicy(
46 QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
47 )
48 except AttributeError: # Only for Qt>=5.14.
49 pass
36 50 _appref = app = QtGui.QApplication([" "])
37 51
38 52 # "reclaim" IPython sys.excepthook after event loop starts
39 53 # without this, it defaults back to BaseIPythonApplication.excepthook
40 54 # and exceptions in the Qt event loop are rendered without traceback
41 55 # formatting and look like "bug in IPython".
42 56 QtCore.QTimer.singleShot(0, _reclaim_excepthook)
43 57
44 58 event_loop = QtCore.QEventLoop(app)
45 59
46 60 if sys.platform == 'win32':
47 61 # The QSocketNotifier method doesn't appear to work on Windows.
48 62 # Use polling instead.
49 63 timer = QtCore.QTimer()
50 64 timer.timeout.connect(event_loop.quit)
51 65 while not context.input_is_ready():
52 66 timer.start(50) # 50 ms
53 67 event_loop.exec_()
54 68 timer.stop()
55 69 else:
56 70 # On POSIX platforms, we can use a file descriptor to quit the event
57 71 # loop when there is input ready to read.
58 notifier = QtCore.QSocketNotifier(context.fileno(),
59 QtCore.QSocketNotifier.Read)
72 notifier = QtCore.QSocketNotifier(
73 context.fileno(), enum_helper("QtCore.QSocketNotifier.Type").Read
74 )
60 75 try:
61 76 # connect the callback we care about before we turn it on
62 77 notifier.activated.connect(lambda: event_loop.exit())
63 78 notifier.setEnabled(True)
64 79 # only start the event loop we are not already flipped
65 80 if not context.input_is_ready():
66 event_loop.exec_()
81 _exec(event_loop)
67 82 finally:
68 83 notifier.setEnabled(False)
General Comments 0
You need to be logged in to leave comments. Login now