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