##// END OF EJS Templates
reformat rest of ipython
Matthias Bussonnier -
Show More
@@ -1,399 +1,397 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 importlib.abc
11 import importlib.abc
12 import sys
12 import sys
13 import types
13 import types
14 from functools import partial, lru_cache
14 from functools import partial, lru_cache
15 import operator
15 import operator
16
16
17 # ### Available APIs.
17 # ### Available APIs.
18 # Qt6
18 # Qt6
19 QT_API_PYQT6 = "pyqt6"
19 QT_API_PYQT6 = "pyqt6"
20 QT_API_PYSIDE6 = "pyside6"
20 QT_API_PYSIDE6 = "pyside6"
21
21
22 # Qt5
22 # Qt5
23 QT_API_PYQT5 = 'pyqt5'
23 QT_API_PYQT5 = 'pyqt5'
24 QT_API_PYSIDE2 = 'pyside2'
24 QT_API_PYSIDE2 = 'pyside2'
25
25
26 # Qt4
26 # Qt4
27 QT_API_PYQT = "pyqt" # Force version 2
27 QT_API_PYQT = "pyqt" # Force version 2
28 QT_API_PYQTv1 = "pyqtv1" # Force version 2
28 QT_API_PYQTv1 = "pyqtv1" # Force version 2
29 QT_API_PYSIDE = "pyside"
29 QT_API_PYSIDE = "pyside"
30
30
31 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
31 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
32
32
33 api_to_module = {
33 api_to_module = {
34 # Qt6
34 # Qt6
35 QT_API_PYQT6: "PyQt6",
35 QT_API_PYQT6: "PyQt6",
36 QT_API_PYSIDE6: "PySide6",
36 QT_API_PYSIDE6: "PySide6",
37 # Qt5
37 # Qt5
38 QT_API_PYQT5: "PyQt5",
38 QT_API_PYQT5: "PyQt5",
39 QT_API_PYSIDE2: "PySide2",
39 QT_API_PYSIDE2: "PySide2",
40 # Qt4
40 # Qt4
41 QT_API_PYSIDE: "PySide",
41 QT_API_PYSIDE: "PySide",
42 QT_API_PYQT: "PyQt4",
42 QT_API_PYQT: "PyQt4",
43 QT_API_PYQTv1: "PyQt4",
43 QT_API_PYQTv1: "PyQt4",
44 # default
44 # default
45 QT_API_PYQT_DEFAULT: "PyQt6",
45 QT_API_PYQT_DEFAULT: "PyQt6",
46 }
46 }
47
47
48
48
49 class ImportDenier(importlib.abc.MetaPathFinder):
49 class ImportDenier(importlib.abc.MetaPathFinder):
50 """Import Hook that will guard against bad Qt imports
50 """Import Hook that will guard against bad Qt imports
51 once IPython commits to a specific binding
51 once IPython commits to a specific binding
52 """
52 """
53
53
54 def __init__(self):
54 def __init__(self):
55 self.__forbidden = set()
55 self.__forbidden = set()
56
56
57 def forbid(self, module_name):
57 def forbid(self, module_name):
58 sys.modules.pop(module_name, None)
58 sys.modules.pop(module_name, None)
59 self.__forbidden.add(module_name)
59 self.__forbidden.add(module_name)
60
60
61 def find_spec(self, fullname, path, target=None):
61 def find_spec(self, fullname, path, target=None):
62 if path:
62 if path:
63 return
63 return
64 if fullname in self.__forbidden:
64 if fullname in self.__forbidden:
65 raise ImportError(
65 raise ImportError(
66 """
66 """
67 Importing %s disabled by IPython, which has
67 Importing %s disabled by IPython, which has
68 already imported an Incompatible QT Binding: %s
68 already imported an Incompatible QT Binding: %s
69 """ % (fullname, loaded_api()))
69 """ % (fullname, loaded_api()))
70
70
71
71
72 ID = ImportDenier()
72 ID = ImportDenier()
73 sys.meta_path.insert(0, ID)
73 sys.meta_path.insert(0, ID)
74
74
75
75
76 def commit_api(api):
76 def commit_api(api):
77 """Commit to a particular API, and trigger ImportErrors on subsequent
77 """Commit to a particular API, and trigger ImportErrors on subsequent
78 dangerous imports"""
78 dangerous imports"""
79 modules = set(api_to_module.values())
79 modules = set(api_to_module.values())
80
80
81 modules.remove(api_to_module[api])
81 modules.remove(api_to_module[api])
82 for mod in modules:
82 for mod in modules:
83 ID.forbid(mod)
83 ID.forbid(mod)
84
84
85
85
86 def loaded_api():
86 def loaded_api():
87 """Return which API is loaded, if any
87 """Return which API is loaded, if any
88
88
89 If this returns anything besides None,
89 If this returns anything besides None,
90 importing any other Qt binding is unsafe.
90 importing any other Qt binding is unsafe.
91
91
92 Returns
92 Returns
93 -------
93 -------
94 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
94 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
95 """
95 """
96 if sys.modules.get("PyQt6.QtCore"):
96 if sys.modules.get("PyQt6.QtCore"):
97 return QT_API_PYQT6
97 return QT_API_PYQT6
98 elif sys.modules.get("PySide6.QtCore"):
98 elif sys.modules.get("PySide6.QtCore"):
99 return QT_API_PYSIDE6
99 return QT_API_PYSIDE6
100 elif sys.modules.get("PyQt5.QtCore"):
100 elif sys.modules.get("PyQt5.QtCore"):
101 return QT_API_PYQT5
101 return QT_API_PYQT5
102 elif sys.modules.get("PySide2.QtCore"):
102 elif sys.modules.get("PySide2.QtCore"):
103 return QT_API_PYSIDE2
103 return QT_API_PYSIDE2
104 elif sys.modules.get("PyQt4.QtCore"):
104 elif sys.modules.get("PyQt4.QtCore"):
105 if qtapi_version() == 2:
105 if qtapi_version() == 2:
106 return QT_API_PYQT
106 return QT_API_PYQT
107 else:
107 else:
108 return QT_API_PYQTv1
108 return QT_API_PYQTv1
109 elif sys.modules.get("PySide.QtCore"):
109 elif sys.modules.get("PySide.QtCore"):
110 return QT_API_PYSIDE
110 return QT_API_PYSIDE
111
111
112 return None
112 return None
113
113
114
114
115 def has_binding(api):
115 def has_binding(api):
116 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
116 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
117
117
118 Parameters
118 Parameters
119 ----------
119 ----------
120 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
120 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
121 Which module to check for
121 Which module to check for
122
122
123 Returns
123 Returns
124 -------
124 -------
125 True if the relevant module appears to be importable
125 True if the relevant module appears to be importable
126 """
126 """
127 module_name = api_to_module[api]
127 module_name = api_to_module[api]
128 from importlib.util import find_spec
128 from importlib.util import find_spec
129
129
130 required = ['QtCore', 'QtGui', 'QtSvg']
130 required = ['QtCore', 'QtGui', 'QtSvg']
131 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
131 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
132 # QT5 requires QtWidgets too
132 # QT5 requires QtWidgets too
133 required.append('QtWidgets')
133 required.append('QtWidgets')
134
134
135 for submod in required:
135 for submod in required:
136 try:
136 try:
137 spec = find_spec('%s.%s' % (module_name, submod))
137 spec = find_spec('%s.%s' % (module_name, submod))
138 except ImportError:
138 except ImportError:
139 # Package (e.g. PyQt5) not found
139 # Package (e.g. PyQt5) not found
140 return False
140 return False
141 else:
141 else:
142 if spec is None:
142 if spec is None:
143 # Submodule (e.g. PyQt5.QtCore) not found
143 # Submodule (e.g. PyQt5.QtCore) not found
144 return False
144 return False
145
145
146 if api == QT_API_PYSIDE:
146 if api == QT_API_PYSIDE:
147 # We can also safely check PySide version
147 # We can also safely check PySide version
148 import PySide
148 import PySide
149
149
150 return PySide.__version_info__ >= (1, 0, 3)
150 return PySide.__version_info__ >= (1, 0, 3)
151
151
152 return True
152 return True
153
153
154
154
155 def qtapi_version():
155 def qtapi_version():
156 """Return which QString API has been set, if any
156 """Return which QString API has been set, if any
157
157
158 Returns
158 Returns
159 -------
159 -------
160 The QString API version (1 or 2), or None if not set
160 The QString API version (1 or 2), or None if not set
161 """
161 """
162 try:
162 try:
163 import sip
163 import sip
164 except ImportError:
164 except ImportError:
165 # as of PyQt5 5.11, sip is no longer available as a top-level
165 # as of PyQt5 5.11, sip is no longer available as a top-level
166 # module and needs to be imported from the PyQt5 namespace
166 # module and needs to be imported from the PyQt5 namespace
167 try:
167 try:
168 from PyQt5 import sip
168 from PyQt5 import sip
169 except ImportError:
169 except ImportError:
170 return
170 return
171 try:
171 try:
172 return sip.getapi('QString')
172 return sip.getapi('QString')
173 except ValueError:
173 except ValueError:
174 return
174 return
175
175
176
176
177 def can_import(api):
177 def can_import(api):
178 """Safely query whether an API is importable, without importing it"""
178 """Safely query whether an API is importable, without importing it"""
179 if not has_binding(api):
179 if not has_binding(api):
180 return False
180 return False
181
181
182 current = loaded_api()
182 current = loaded_api()
183 if api == QT_API_PYQT_DEFAULT:
183 if api == QT_API_PYQT_DEFAULT:
184 return current in [QT_API_PYQT6, None]
184 return current in [QT_API_PYQT6, None]
185 else:
185 else:
186 return current in [api, None]
186 return current in [api, None]
187
187
188
188
189 def import_pyqt4(version=2):
189 def import_pyqt4(version=2):
190 """
190 """
191 Import PyQt4
191 Import PyQt4
192
192
193 Parameters
193 Parameters
194 ----------
194 ----------
195 version : 1, 2, or None
195 version : 1, 2, or None
196 Which QString/QVariant API to use. Set to None to use the system
196 Which QString/QVariant API to use. Set to None to use the system
197 default
197 default
198
199 ImportErrors raised within this function are non-recoverable
198 ImportErrors raised within this function are non-recoverable
200 """
199 """
201 # The new-style string API (version=2) automatically
200 # The new-style string API (version=2) automatically
202 # converts QStrings to Unicode Python strings. Also, automatically unpacks
201 # converts QStrings to Unicode Python strings. Also, automatically unpacks
203 # QVariants to their underlying objects.
202 # QVariants to their underlying objects.
204 import sip
203 import sip
205
204
206 if version is not None:
205 if version is not None:
207 sip.setapi('QString', version)
206 sip.setapi('QString', version)
208 sip.setapi('QVariant', version)
207 sip.setapi('QVariant', version)
209
208
210 from PyQt4 import QtGui, QtCore, QtSvg
209 from PyQt4 import QtGui, QtCore, QtSvg
211
210
212 if QtCore.PYQT_VERSION < 0x040700:
211 if QtCore.PYQT_VERSION < 0x040700:
213 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
212 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
214 QtCore.PYQT_VERSION_STR)
213 QtCore.PYQT_VERSION_STR)
215
214
216 # Alias PyQt-specific functions for PySide compatibility.
215 # Alias PyQt-specific functions for PySide compatibility.
217 QtCore.Signal = QtCore.pyqtSignal
216 QtCore.Signal = QtCore.pyqtSignal
218 QtCore.Slot = QtCore.pyqtSlot
217 QtCore.Slot = QtCore.pyqtSlot
219
218
220 # query for the API version (in case version == None)
219 # query for the API version (in case version == None)
221 version = sip.getapi('QString')
220 version = sip.getapi('QString')
222 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
221 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
223 return QtCore, QtGui, QtSvg, api
222 return QtCore, QtGui, QtSvg, api
224
223
225
224
226 def import_pyqt5():
225 def import_pyqt5():
227 """
226 """
228 Import PyQt5
227 Import PyQt5
229
228
230 ImportErrors raised within this function are non-recoverable
229 ImportErrors raised within this function are non-recoverable
231 """
230 """
232
231
233 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
232 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
234
233
235 # Alias PyQt-specific functions for PySide compatibility.
234 # Alias PyQt-specific functions for PySide compatibility.
236 QtCore.Signal = QtCore.pyqtSignal
235 QtCore.Signal = QtCore.pyqtSignal
237 QtCore.Slot = QtCore.pyqtSlot
236 QtCore.Slot = QtCore.pyqtSlot
238
237
239 # Join QtGui and QtWidgets for Qt4 compatibility.
238 # Join QtGui and QtWidgets for Qt4 compatibility.
240 QtGuiCompat = types.ModuleType('QtGuiCompat')
239 QtGuiCompat = types.ModuleType('QtGuiCompat')
241 QtGuiCompat.__dict__.update(QtGui.__dict__)
240 QtGuiCompat.__dict__.update(QtGui.__dict__)
242 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
241 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
243
242
244 api = QT_API_PYQT5
243 api = QT_API_PYQT5
245 return QtCore, QtGuiCompat, QtSvg, api
244 return QtCore, QtGuiCompat, QtSvg, api
246
245
247
246
248 def import_pyqt6():
247 def import_pyqt6():
249 """
248 """
250 Import PyQt6
249 Import PyQt6
251
250
252 ImportErrors raised within this function are non-recoverable
251 ImportErrors raised within this function are non-recoverable
253 """
252 """
254
253
255 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
254 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
256
255
257 # Alias PyQt-specific functions for PySide compatibility.
256 # Alias PyQt-specific functions for PySide compatibility.
258 QtCore.Signal = QtCore.pyqtSignal
257 QtCore.Signal = QtCore.pyqtSignal
259 QtCore.Slot = QtCore.pyqtSlot
258 QtCore.Slot = QtCore.pyqtSlot
260
259
261 # Join QtGui and QtWidgets for Qt4 compatibility.
260 # Join QtGui and QtWidgets for Qt4 compatibility.
262 QtGuiCompat = types.ModuleType("QtGuiCompat")
261 QtGuiCompat = types.ModuleType("QtGuiCompat")
263 QtGuiCompat.__dict__.update(QtGui.__dict__)
262 QtGuiCompat.__dict__.update(QtGui.__dict__)
264 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
263 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
265
264
266 api = QT_API_PYQT6
265 api = QT_API_PYQT6
267 return QtCore, QtGuiCompat, QtSvg, api
266 return QtCore, QtGuiCompat, QtSvg, api
268
267
269
268
270 def import_pyside():
269 def import_pyside():
271 """
270 """
272 Import PySide
271 Import PySide
273
272
274 ImportErrors raised within this function are non-recoverable
273 ImportErrors raised within this function are non-recoverable
275 """
274 """
276 from PySide import QtGui, QtCore, QtSvg
275 from PySide import QtGui, QtCore, QtSvg
277 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
276 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
278
277
279 def import_pyside2():
278 def import_pyside2():
280 """
279 """
281 Import PySide2
280 Import PySide2
282
281
283 ImportErrors raised within this function are non-recoverable
282 ImportErrors raised within this function are non-recoverable
284 """
283 """
285 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
284 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
286
285
287 # Join QtGui and QtWidgets for Qt4 compatibility.
286 # Join QtGui and QtWidgets for Qt4 compatibility.
288 QtGuiCompat = types.ModuleType('QtGuiCompat')
287 QtGuiCompat = types.ModuleType('QtGuiCompat')
289 QtGuiCompat.__dict__.update(QtGui.__dict__)
288 QtGuiCompat.__dict__.update(QtGui.__dict__)
290 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
289 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
291 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
290 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
292
291
293 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
292 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
294
293
295
294
296 def import_pyside6():
295 def import_pyside6():
297 """
296 """
298 Import PySide6
297 Import PySide6
299
298
300 ImportErrors raised within this function are non-recoverable
299 ImportErrors raised within this function are non-recoverable
301 """
300 """
302 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
301 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
303
302
304 # Join QtGui and QtWidgets for Qt4 compatibility.
303 # Join QtGui and QtWidgets for Qt4 compatibility.
305 QtGuiCompat = types.ModuleType("QtGuiCompat")
304 QtGuiCompat = types.ModuleType("QtGuiCompat")
306 QtGuiCompat.__dict__.update(QtGui.__dict__)
305 QtGuiCompat.__dict__.update(QtGui.__dict__)
307 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
306 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
308 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
307 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
309
308
310 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
309 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
311
310
312
311
313 def load_qt(api_options):
312 def load_qt(api_options):
314 """
313 """
315 Attempt to import Qt, given a preference list
314 Attempt to import Qt, given a preference list
316 of permissible bindings
315 of permissible bindings
317
316
318 It is safe to call this function multiple times.
317 It is safe to call this function multiple times.
319
318
320 Parameters
319 Parameters
321 ----------
320 ----------
322 api_options: List of strings
321 api_options : List of strings
323 The order of APIs to try. Valid items are 'pyside', 'pyside2',
322 The order of APIs to try. Valid items are 'pyside', 'pyside2',
324 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
323 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
325
324
326 Returns
325 Returns
327 -------
326 -------
328
329 A tuple of QtCore, QtGui, QtSvg, QT_API
327 A tuple of QtCore, QtGui, QtSvg, QT_API
330 The first three are the Qt modules. The last is the
328 The first three are the Qt modules. The last is the
331 string indicating which module was loaded.
329 string indicating which module was loaded.
332
330
333 Raises
331 Raises
334 ------
332 ------
335 ImportError, if it isn't possible to import any requested
333 ImportError, if it isn't possible to import any requested
336 bindings (either because they aren't installed, or because
334 bindings (either because they aren't installed, or because
337 an incompatible library has already been installed)
335 an incompatible library has already been installed)
338 """
336 """
339 loaders = {
337 loaders = {
340 # Qt6
338 # Qt6
341 QT_API_PYQT6: import_pyqt6,
339 QT_API_PYQT6: import_pyqt6,
342 QT_API_PYSIDE6: import_pyside6,
340 QT_API_PYSIDE6: import_pyside6,
343 # Qt5
341 # Qt5
344 QT_API_PYQT5: import_pyqt5,
342 QT_API_PYQT5: import_pyqt5,
345 QT_API_PYSIDE2: import_pyside2,
343 QT_API_PYSIDE2: import_pyside2,
346 # Qt4
344 # Qt4
347 QT_API_PYSIDE: import_pyside,
345 QT_API_PYSIDE: import_pyside,
348 QT_API_PYQT: import_pyqt4,
346 QT_API_PYQT: import_pyqt4,
349 QT_API_PYQTv1: partial(import_pyqt4, version=1),
347 QT_API_PYQTv1: partial(import_pyqt4, version=1),
350 # default
348 # default
351 QT_API_PYQT_DEFAULT: import_pyqt6,
349 QT_API_PYQT_DEFAULT: import_pyqt6,
352 }
350 }
353
351
354 for api in api_options:
352 for api in api_options:
355
353
356 if api not in loaders:
354 if api not in loaders:
357 raise RuntimeError(
355 raise RuntimeError(
358 "Invalid Qt API %r, valid values are: %s" %
356 "Invalid Qt API %r, valid values are: %s" %
359 (api, ", ".join(["%r" % k for k in loaders.keys()])))
357 (api, ", ".join(["%r" % k for k in loaders.keys()])))
360
358
361 if not can_import(api):
359 if not can_import(api):
362 continue
360 continue
363
361
364 #cannot safely recover from an ImportError during this
362 #cannot safely recover from an ImportError during this
365 result = loaders[api]()
363 result = loaders[api]()
366 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
364 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
367 commit_api(api)
365 commit_api(api)
368 return result
366 return result
369 else:
367 else:
370 raise ImportError("""
368 raise ImportError("""
371 Could not load requested Qt binding. Please ensure that
369 Could not load requested Qt binding. Please ensure that
372 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
370 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
373 and only one is imported per session.
371 and only one is imported per session.
374
372
375 Currently-imported Qt library: %r
373 Currently-imported Qt library: %r
376 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
374 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
377 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
375 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
378 PySide >= 1.0.3 installed: %s
376 PySide >= 1.0.3 installed: %s
379 PySide2 installed: %s
377 PySide2 installed: %s
380 Tried to load: %r
378 Tried to load: %r
381 """ % (loaded_api(),
379 """ % (loaded_api(),
382 has_binding(QT_API_PYQT),
380 has_binding(QT_API_PYQT),
383 has_binding(QT_API_PYQT5),
381 has_binding(QT_API_PYQT5),
384 has_binding(QT_API_PYSIDE),
382 has_binding(QT_API_PYSIDE),
385 has_binding(QT_API_PYSIDE2),
383 has_binding(QT_API_PYSIDE2),
386 api_options))
384 api_options))
387
385
388
386
389 def enum_factory(QT_API, QtCore):
387 def enum_factory(QT_API, QtCore):
390 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
388 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
391
389
392 @lru_cache(None)
390 @lru_cache(None)
393 def _enum(name):
391 def _enum(name):
394 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
392 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
395 return operator.attrgetter(
393 return operator.attrgetter(
396 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
394 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
397 )(sys.modules[QtCore.__package__])
395 )(sys.modules[QtCore.__package__])
398
396
399 return _enum
397 return _enum
1 NO CONTENT: modified file
NO CONTENT: modified file
1 NO CONTENT: modified file
NO CONTENT: modified file
1 NO CONTENT: modified file
NO CONTENT: modified file
@@ -1,247 +1,246 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Tools for handling LaTeX."""
2 """Tools for handling LaTeX."""
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, open
7 from io import BytesIO, open
8 import os
8 import os
9 import tempfile
9 import tempfile
10 import shutil
10 import shutil
11 import subprocess
11 import subprocess
12 from base64 import encodebytes
12 from base64 import encodebytes
13 import textwrap
13 import textwrap
14
14
15 from pathlib import Path, PurePath
15 from pathlib import Path, PurePath
16
16
17 from IPython.utils.process import find_cmd, FindCmdError
17 from IPython.utils.process import find_cmd, FindCmdError
18 from traitlets.config import get_config
18 from traitlets.config import get_config
19 from traitlets.config.configurable import SingletonConfigurable
19 from traitlets.config.configurable import SingletonConfigurable
20 from traitlets import List, Bool, Unicode
20 from traitlets import List, Bool, Unicode
21 from IPython.utils.py3compat import cast_unicode
21 from IPython.utils.py3compat import cast_unicode
22
22
23
23
24 class LaTeXTool(SingletonConfigurable):
24 class LaTeXTool(SingletonConfigurable):
25 """An object to store configuration of the LaTeX tool."""
25 """An object to store configuration of the LaTeX tool."""
26 def _config_default(self):
26 def _config_default(self):
27 return get_config()
27 return get_config()
28
28
29 backends = List(
29 backends = List(
30 Unicode(), ["matplotlib", "dvipng"],
30 Unicode(), ["matplotlib", "dvipng"],
31 help="Preferred backend to draw LaTeX math equations. "
31 help="Preferred backend to draw LaTeX math equations. "
32 "Backends in the list are checked one by one and the first "
32 "Backends in the list are checked one by one and the first "
33 "usable one is used. Note that `matplotlib` backend "
33 "usable one is used. Note that `matplotlib` backend "
34 "is usable only for inline style equations. To draw "
34 "is usable only for inline style equations. To draw "
35 "display style equations, `dvipng` backend must be specified. ",
35 "display style equations, `dvipng` backend must be specified. ",
36 # It is a List instead of Enum, to make configuration more
36 # It is a List instead of Enum, to make configuration more
37 # flexible. For example, to use matplotlib mainly but dvipng
37 # flexible. For example, to use matplotlib mainly but dvipng
38 # for display style, the default ["matplotlib", "dvipng"] can
38 # for display style, the default ["matplotlib", "dvipng"] can
39 # be used. To NOT use dvipng so that other repr such as
39 # be used. To NOT use dvipng so that other repr such as
40 # unicode pretty printing is used, you can use ["matplotlib"].
40 # unicode pretty printing is used, you can use ["matplotlib"].
41 ).tag(config=True)
41 ).tag(config=True)
42
42
43 use_breqn = Bool(
43 use_breqn = Bool(
44 True,
44 True,
45 help="Use breqn.sty to automatically break long equations. "
45 help="Use breqn.sty to automatically break long equations. "
46 "This configuration takes effect only for dvipng backend.",
46 "This configuration takes effect only for dvipng backend.",
47 ).tag(config=True)
47 ).tag(config=True)
48
48
49 packages = List(
49 packages = List(
50 ['amsmath', 'amsthm', 'amssymb', 'bm'],
50 ['amsmath', 'amsthm', 'amssymb', 'bm'],
51 help="A list of packages to use for dvipng backend. "
51 help="A list of packages to use for dvipng backend. "
52 "'breqn' will be automatically appended when use_breqn=True.",
52 "'breqn' will be automatically appended when use_breqn=True.",
53 ).tag(config=True)
53 ).tag(config=True)
54
54
55 preamble = Unicode(
55 preamble = Unicode(
56 help="Additional preamble to use when generating LaTeX source "
56 help="Additional preamble to use when generating LaTeX source "
57 "for dvipng backend.",
57 "for dvipng backend.",
58 ).tag(config=True)
58 ).tag(config=True)
59
59
60
60
61 def latex_to_png(s, encode=False, backend=None, wrap=False, color='Black',
61 def latex_to_png(s, encode=False, backend=None, wrap=False, color='Black',
62 scale=1.0):
62 scale=1.0):
63 """Render a LaTeX string to PNG.
63 """Render a LaTeX string to PNG.
64
64
65 Parameters
65 Parameters
66 ----------
66 ----------
67 s : str
67 s : str
68 The raw string containing valid inline LaTeX.
68 The raw string containing valid inline LaTeX.
69 encode : bool, optional
69 encode : bool, optional
70 Should the PNG data base64 encoded to make it JSON'able.
70 Should the PNG data base64 encoded to make it JSON'able.
71 backend : {matplotlib, dvipng}
71 backend : {matplotlib, dvipng}
72 Backend for producing PNG data.
72 Backend for producing PNG data.
73 wrap : bool
73 wrap : bool
74 If true, Automatically wrap `s` as a LaTeX equation.
74 If true, Automatically wrap `s` as a LaTeX equation.
75 color : string
75 color : string
76 Foreground color name among dvipsnames, e.g. 'Maroon' or on hex RGB
76 Foreground color name among dvipsnames, e.g. 'Maroon' or on hex RGB
77 format, e.g. '#AA20FA'.
77 format, e.g. '#AA20FA'.
78 scale : float
78 scale : float
79 Scale factor for the resulting PNG.
79 Scale factor for the resulting PNG.
80
81 None is returned when the backend cannot be used.
80 None is returned when the backend cannot be used.
82
81
83 """
82 """
84 s = cast_unicode(s)
83 s = cast_unicode(s)
85 allowed_backends = LaTeXTool.instance().backends
84 allowed_backends = LaTeXTool.instance().backends
86 if backend is None:
85 if backend is None:
87 backend = allowed_backends[0]
86 backend = allowed_backends[0]
88 if backend not in allowed_backends:
87 if backend not in allowed_backends:
89 return None
88 return None
90 if backend == 'matplotlib':
89 if backend == 'matplotlib':
91 f = latex_to_png_mpl
90 f = latex_to_png_mpl
92 elif backend == 'dvipng':
91 elif backend == 'dvipng':
93 f = latex_to_png_dvipng
92 f = latex_to_png_dvipng
94 if color.startswith('#'):
93 if color.startswith('#'):
95 # Convert hex RGB color to LaTeX RGB color.
94 # Convert hex RGB color to LaTeX RGB color.
96 if len(color) == 7:
95 if len(color) == 7:
97 try:
96 try:
98 color = "RGB {}".format(" ".join([str(int(x, 16)) for x in
97 color = "RGB {}".format(" ".join([str(int(x, 16)) for x in
99 textwrap.wrap(color[1:], 2)]))
98 textwrap.wrap(color[1:], 2)]))
100 except ValueError as e:
99 except ValueError as e:
101 raise ValueError('Invalid color specification {}.'.format(color)) from e
100 raise ValueError('Invalid color specification {}.'.format(color)) from e
102 else:
101 else:
103 raise ValueError('Invalid color specification {}.'.format(color))
102 raise ValueError('Invalid color specification {}.'.format(color))
104 else:
103 else:
105 raise ValueError('No such backend {0}'.format(backend))
104 raise ValueError('No such backend {0}'.format(backend))
106 bin_data = f(s, wrap, color, scale)
105 bin_data = f(s, wrap, color, scale)
107 if encode and bin_data:
106 if encode and bin_data:
108 bin_data = encodebytes(bin_data)
107 bin_data = encodebytes(bin_data)
109 return bin_data
108 return bin_data
110
109
111
110
112 def latex_to_png_mpl(s, wrap, color='Black', scale=1.0):
111 def latex_to_png_mpl(s, wrap, color='Black', scale=1.0):
113 try:
112 try:
114 from matplotlib import figure, font_manager, mathtext
113 from matplotlib import figure, font_manager, mathtext
115 from matplotlib.backends import backend_agg
114 from matplotlib.backends import backend_agg
116 from pyparsing import ParseFatalException
115 from pyparsing import ParseFatalException
117 except ImportError:
116 except ImportError:
118 return None
117 return None
119
118
120 # mpl mathtext doesn't support display math, force inline
119 # mpl mathtext doesn't support display math, force inline
121 s = s.replace('$$', '$')
120 s = s.replace('$$', '$')
122 if wrap:
121 if wrap:
123 s = u'${0}$'.format(s)
122 s = u'${0}$'.format(s)
124
123
125 try:
124 try:
126 prop = font_manager.FontProperties(size=12)
125 prop = font_manager.FontProperties(size=12)
127 dpi = 120 * scale
126 dpi = 120 * scale
128 buffer = BytesIO()
127 buffer = BytesIO()
129
128
130 # Adapted from mathtext.math_to_image
129 # Adapted from mathtext.math_to_image
131 parser = mathtext.MathTextParser("path")
130 parser = mathtext.MathTextParser("path")
132 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
131 width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop)
133 fig = figure.Figure(figsize=(width / 72, height / 72))
132 fig = figure.Figure(figsize=(width / 72, height / 72))
134 fig.text(0, depth / height, s, fontproperties=prop, color=color)
133 fig.text(0, depth / height, s, fontproperties=prop, color=color)
135 backend_agg.FigureCanvasAgg(fig)
134 backend_agg.FigureCanvasAgg(fig)
136 fig.savefig(buffer, dpi=dpi, format="png", transparent=True)
135 fig.savefig(buffer, dpi=dpi, format="png", transparent=True)
137 return buffer.getvalue()
136 return buffer.getvalue()
138 except (ValueError, RuntimeError, ParseFatalException):
137 except (ValueError, RuntimeError, ParseFatalException):
139 return None
138 return None
140
139
141
140
142 def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
141 def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
143 try:
142 try:
144 find_cmd('latex')
143 find_cmd('latex')
145 find_cmd('dvipng')
144 find_cmd('dvipng')
146 except FindCmdError:
145 except FindCmdError:
147 return None
146 return None
148 try:
147 try:
149 workdir = Path(tempfile.mkdtemp())
148 workdir = Path(tempfile.mkdtemp())
150 tmpfile = workdir.joinpath("tmp.tex")
149 tmpfile = workdir.joinpath("tmp.tex")
151 dvifile = workdir.joinpath("tmp.dvi")
150 dvifile = workdir.joinpath("tmp.dvi")
152 outfile = workdir.joinpath("tmp.png")
151 outfile = workdir.joinpath("tmp.png")
153
152
154 with tmpfile.open("w", encoding="utf8") as f:
153 with tmpfile.open("w", encoding="utf8") as f:
155 f.writelines(genelatex(s, wrap))
154 f.writelines(genelatex(s, wrap))
156
155
157 with open(os.devnull, 'wb') as devnull:
156 with open(os.devnull, 'wb') as devnull:
158 subprocess.check_call(
157 subprocess.check_call(
159 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
158 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
160 cwd=workdir, stdout=devnull, stderr=devnull)
159 cwd=workdir, stdout=devnull, stderr=devnull)
161
160
162 resolution = round(150*scale)
161 resolution = round(150*scale)
163 subprocess.check_call(
162 subprocess.check_call(
164 [
163 [
165 "dvipng",
164 "dvipng",
166 "-T",
165 "-T",
167 "tight",
166 "tight",
168 "-D",
167 "-D",
169 str(resolution),
168 str(resolution),
170 "-z",
169 "-z",
171 "9",
170 "9",
172 "-bg",
171 "-bg",
173 "Transparent",
172 "Transparent",
174 "-o",
173 "-o",
175 outfile,
174 outfile,
176 dvifile,
175 dvifile,
177 "-fg",
176 "-fg",
178 color,
177 color,
179 ],
178 ],
180 cwd=workdir,
179 cwd=workdir,
181 stdout=devnull,
180 stdout=devnull,
182 stderr=devnull,
181 stderr=devnull,
183 )
182 )
184
183
185 with outfile.open("rb") as f:
184 with outfile.open("rb") as f:
186 return f.read()
185 return f.read()
187 except subprocess.CalledProcessError:
186 except subprocess.CalledProcessError:
188 return None
187 return None
189 finally:
188 finally:
190 shutil.rmtree(workdir)
189 shutil.rmtree(workdir)
191
190
192
191
193 def kpsewhich(filename):
192 def kpsewhich(filename):
194 """Invoke kpsewhich command with an argument `filename`."""
193 """Invoke kpsewhich command with an argument `filename`."""
195 try:
194 try:
196 find_cmd("kpsewhich")
195 find_cmd("kpsewhich")
197 proc = subprocess.Popen(
196 proc = subprocess.Popen(
198 ["kpsewhich", filename],
197 ["kpsewhich", filename],
199 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
198 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
200 (stdout, stderr) = proc.communicate()
199 (stdout, stderr) = proc.communicate()
201 return stdout.strip().decode('utf8', 'replace')
200 return stdout.strip().decode('utf8', 'replace')
202 except FindCmdError:
201 except FindCmdError:
203 pass
202 pass
204
203
205
204
206 def genelatex(body, wrap):
205 def genelatex(body, wrap):
207 """Generate LaTeX document for dvipng backend."""
206 """Generate LaTeX document for dvipng backend."""
208 lt = LaTeXTool.instance()
207 lt = LaTeXTool.instance()
209 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
208 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
210 yield r'\documentclass{article}'
209 yield r'\documentclass{article}'
211 packages = lt.packages
210 packages = lt.packages
212 if breqn:
211 if breqn:
213 packages = packages + ['breqn']
212 packages = packages + ['breqn']
214 for pack in packages:
213 for pack in packages:
215 yield r'\usepackage{{{0}}}'.format(pack)
214 yield r'\usepackage{{{0}}}'.format(pack)
216 yield r'\pagestyle{empty}'
215 yield r'\pagestyle{empty}'
217 if lt.preamble:
216 if lt.preamble:
218 yield lt.preamble
217 yield lt.preamble
219 yield r'\begin{document}'
218 yield r'\begin{document}'
220 if breqn:
219 if breqn:
221 yield r'\begin{dmath*}'
220 yield r'\begin{dmath*}'
222 yield body
221 yield body
223 yield r'\end{dmath*}'
222 yield r'\end{dmath*}'
224 elif wrap:
223 elif wrap:
225 yield u'$${0}$$'.format(body)
224 yield u'$${0}$$'.format(body)
226 else:
225 else:
227 yield body
226 yield body
228 yield u'\\end{document}'
227 yield u'\\end{document}'
229
228
230
229
231 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
230 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
232
231
233 def latex_to_html(s, alt='image'):
232 def latex_to_html(s, alt='image'):
234 """Render LaTeX to HTML with embedded PNG data using data URIs.
233 """Render LaTeX to HTML with embedded PNG data using data URIs.
235
234
236 Parameters
235 Parameters
237 ----------
236 ----------
238 s : str
237 s : str
239 The raw string containing valid inline LateX.
238 The raw string containing valid inline LateX.
240 alt : str
239 alt : str
241 The alt text to use for the HTML.
240 The alt text to use for the HTML.
242 """
241 """
243 base64_data = latex_to_png(s, encode=True).decode('ascii')
242 base64_data = latex_to_png(s, encode=True).decode('ascii')
244 if base64_data:
243 if base64_data:
245 return _data_uri_template_png % (base64_data, alt)
244 return _data_uri_template_png % (base64_data, alt)
246
245
247
246
General Comments 0
You need to be logged in to leave comments. Login now