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