##// 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,491 +1,491 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Manage background (threaded) jobs conveniently from an interactive shell.
2 """Manage background (threaded) jobs conveniently from an interactive shell.
3
3
4 This module provides a BackgroundJobManager class. This is the main class
4 This module provides a BackgroundJobManager class. This is the main class
5 meant for public usage, it implements an object which can create and manage
5 meant for public usage, it implements an object which can create and manage
6 new background jobs.
6 new background jobs.
7
7
8 It also provides the actual job classes managed by these BackgroundJobManager
8 It also provides the actual job classes managed by these BackgroundJobManager
9 objects, see their docstrings below.
9 objects, see their docstrings below.
10
10
11
11
12 This system was inspired by discussions with B. Granger and the
12 This system was inspired by discussions with B. Granger and the
13 BackgroundCommand class described in the book Python Scripting for
13 BackgroundCommand class described in the book Python Scripting for
14 Computational Science, by H. P. Langtangen:
14 Computational Science, by H. P. Langtangen:
15
15
16 http://folk.uio.no/hpl/scripting
16 http://folk.uio.no/hpl/scripting
17
17
18 (although ultimately no code from this text was used, as IPython's system is a
18 (although ultimately no code from this text was used, as IPython's system is a
19 separate implementation).
19 separate implementation).
20
20
21 An example notebook is provided in our documentation illustrating interactive
21 An example notebook is provided in our documentation illustrating interactive
22 use of the system.
22 use of the system.
23 """
23 """
24
24
25 #*****************************************************************************
25 #*****************************************************************************
26 # Copyright (C) 2005-2006 Fernando Perez <fperez@colorado.edu>
26 # Copyright (C) 2005-2006 Fernando Perez <fperez@colorado.edu>
27 #
27 #
28 # Distributed under the terms of the BSD License. The full license is in
28 # Distributed under the terms of the BSD License. The full license is in
29 # the file COPYING, distributed as part of this software.
29 # the file COPYING, distributed as part of this software.
30 #*****************************************************************************
30 #*****************************************************************************
31
31
32 # Code begins
32 # Code begins
33 import sys
33 import sys
34 import threading
34 import threading
35
35
36 from IPython import get_ipython
36 from IPython import get_ipython
37 from IPython.core.ultratb import AutoFormattedTB
37 from IPython.core.ultratb import AutoFormattedTB
38 from logging import error, debug
38 from logging import error, debug
39
39
40
40
41 class BackgroundJobManager(object):
41 class BackgroundJobManager(object):
42 """Class to manage a pool of backgrounded threaded jobs.
42 """Class to manage a pool of backgrounded threaded jobs.
43
43
44 Below, we assume that 'jobs' is a BackgroundJobManager instance.
44 Below, we assume that 'jobs' is a BackgroundJobManager instance.
45
45
46 Usage summary (see the method docstrings for details):
46 Usage summary (see the method docstrings for details):
47
47
48 jobs.new(...) -> start a new job
48 jobs.new(...) -> start a new job
49
49
50 jobs() or jobs.status() -> print status summary of all jobs
50 jobs() or jobs.status() -> print status summary of all jobs
51
51
52 jobs[N] -> returns job number N.
52 jobs[N] -> returns job number N.
53
53
54 foo = jobs[N].result -> assign to variable foo the result of job N
54 foo = jobs[N].result -> assign to variable foo the result of job N
55
55
56 jobs[N].traceback() -> print the traceback of dead job N
56 jobs[N].traceback() -> print the traceback of dead job N
57
57
58 jobs.remove(N) -> remove (finished) job N
58 jobs.remove(N) -> remove (finished) job N
59
59
60 jobs.flush() -> remove all finished jobs
60 jobs.flush() -> remove all finished jobs
61
61
62 As a convenience feature, BackgroundJobManager instances provide the
62 As a convenience feature, BackgroundJobManager instances provide the
63 utility result and traceback methods which retrieve the corresponding
63 utility result and traceback methods which retrieve the corresponding
64 information from the jobs list:
64 information from the jobs list:
65
65
66 jobs.result(N) <--> jobs[N].result
66 jobs.result(N) <--> jobs[N].result
67 jobs.traceback(N) <--> jobs[N].traceback()
67 jobs.traceback(N) <--> jobs[N].traceback()
68
68
69 While this appears minor, it allows you to use tab completion
69 While this appears minor, it allows you to use tab completion
70 interactively on the job manager instance.
70 interactively on the job manager instance.
71 """
71 """
72
72
73 def __init__(self):
73 def __init__(self):
74 # Lists for job management, accessed via a property to ensure they're
74 # Lists for job management, accessed via a property to ensure they're
75 # up to date.x
75 # up to date.x
76 self._running = []
76 self._running = []
77 self._completed = []
77 self._completed = []
78 self._dead = []
78 self._dead = []
79 # A dict of all jobs, so users can easily access any of them
79 # A dict of all jobs, so users can easily access any of them
80 self.all = {}
80 self.all = {}
81 # For reporting
81 # For reporting
82 self._comp_report = []
82 self._comp_report = []
83 self._dead_report = []
83 self._dead_report = []
84 # Store status codes locally for fast lookups
84 # Store status codes locally for fast lookups
85 self._s_created = BackgroundJobBase.stat_created_c
85 self._s_created = BackgroundJobBase.stat_created_c
86 self._s_running = BackgroundJobBase.stat_running_c
86 self._s_running = BackgroundJobBase.stat_running_c
87 self._s_completed = BackgroundJobBase.stat_completed_c
87 self._s_completed = BackgroundJobBase.stat_completed_c
88 self._s_dead = BackgroundJobBase.stat_dead_c
88 self._s_dead = BackgroundJobBase.stat_dead_c
89 self._current_job_id = 0
89 self._current_job_id = 0
90
90
91 @property
91 @property
92 def running(self):
92 def running(self):
93 self._update_status()
93 self._update_status()
94 return self._running
94 return self._running
95
95
96 @property
96 @property
97 def dead(self):
97 def dead(self):
98 self._update_status()
98 self._update_status()
99 return self._dead
99 return self._dead
100
100
101 @property
101 @property
102 def completed(self):
102 def completed(self):
103 self._update_status()
103 self._update_status()
104 return self._completed
104 return self._completed
105
105
106 def new(self, func_or_exp, *args, **kwargs):
106 def new(self, func_or_exp, *args, **kwargs):
107 """Add a new background job and start it in a separate thread.
107 """Add a new background job and start it in a separate thread.
108
108
109 There are two types of jobs which can be created:
109 There are two types of jobs which can be created:
110
110
111 1. Jobs based on expressions which can be passed to an eval() call.
111 1. Jobs based on expressions which can be passed to an eval() call.
112 The expression must be given as a string. For example:
112 The expression must be given as a string. For example:
113
113
114 job_manager.new('myfunc(x,y,z=1)'[,glob[,loc]])
114 job_manager.new('myfunc(x,y,z=1)'[,glob[,loc]])
115
115
116 The given expression is passed to eval(), along with the optional
116 The given expression is passed to eval(), along with the optional
117 global/local dicts provided. If no dicts are given, they are
117 global/local dicts provided. If no dicts are given, they are
118 extracted automatically from the caller's frame.
118 extracted automatically from the caller's frame.
119
119
120 A Python statement is NOT a valid eval() expression. Basically, you
120 A Python statement is NOT a valid eval() expression. Basically, you
121 can only use as an eval() argument something which can go on the right
121 can only use as an eval() argument something which can go on the right
122 of an '=' sign and be assigned to a variable.
122 of an '=' sign and be assigned to a variable.
123
123
124 For example,"print 'hello'" is not valid, but '2+3' is.
124 For example,"print 'hello'" is not valid, but '2+3' is.
125
125
126 2. Jobs given a function object, optionally passing additional
126 2. Jobs given a function object, optionally passing additional
127 positional arguments:
127 positional arguments:
128
128
129 job_manager.new(myfunc, x, y)
129 job_manager.new(myfunc, x, y)
130
130
131 The function is called with the given arguments.
131 The function is called with the given arguments.
132
132
133 If you need to pass keyword arguments to your function, you must
133 If you need to pass keyword arguments to your function, you must
134 supply them as a dict named kw:
134 supply them as a dict named kw:
135
135
136 job_manager.new(myfunc, x, y, kw=dict(z=1))
136 job_manager.new(myfunc, x, y, kw=dict(z=1))
137
137
138 The reason for this asymmetry is that the new() method needs to
138 The reason for this asymmetry is that the new() method needs to
139 maintain access to its own keywords, and this prevents name collisions
139 maintain access to its own keywords, and this prevents name collisions
140 between arguments to new() and arguments to your own functions.
140 between arguments to new() and arguments to your own functions.
141
141
142 In both cases, the result is stored in the job.result field of the
142 In both cases, the result is stored in the job.result field of the
143 background job object.
143 background job object.
144
144
145 You can set `daemon` attribute of the thread by giving the keyword
145 You can set `daemon` attribute of the thread by giving the keyword
146 argument `daemon`.
146 argument `daemon`.
147
147
148 Notes and caveats:
148 Notes and caveats:
149
149
150 1. All threads running share the same standard output. Thus, if your
150 1. All threads running share the same standard output. Thus, if your
151 background jobs generate output, it will come out on top of whatever
151 background jobs generate output, it will come out on top of whatever
152 you are currently writing. For this reason, background jobs are best
152 you are currently writing. For this reason, background jobs are best
153 used with silent functions which simply return their output.
153 used with silent functions which simply return their output.
154
154
155 2. Threads also all work within the same global namespace, and this
155 2. Threads also all work within the same global namespace, and this
156 system does not lock interactive variables. So if you send job to the
156 system does not lock interactive variables. So if you send job to the
157 background which operates on a mutable object for a long time, and
157 background which operates on a mutable object for a long time, and
158 start modifying that same mutable object interactively (or in another
158 start modifying that same mutable object interactively (or in another
159 backgrounded job), all sorts of bizarre behaviour will occur.
159 backgrounded job), all sorts of bizarre behaviour will occur.
160
160
161 3. If a background job is spending a lot of time inside a C extension
161 3. If a background job is spending a lot of time inside a C extension
162 module which does not release the Python Global Interpreter Lock
162 module which does not release the Python Global Interpreter Lock
163 (GIL), this will block the IPython prompt. This is simply because the
163 (GIL), this will block the IPython prompt. This is simply because the
164 Python interpreter can only switch between threads at Python
164 Python interpreter can only switch between threads at Python
165 bytecodes. While the execution is inside C code, the interpreter must
165 bytecodes. While the execution is inside C code, the interpreter must
166 simply wait unless the extension module releases the GIL.
166 simply wait unless the extension module releases the GIL.
167
167
168 4. There is no way, due to limitations in the Python threads library,
168 4. There is no way, due to limitations in the Python threads library,
169 to kill a thread once it has started."""
169 to kill a thread once it has started."""
170
170
171 if callable(func_or_exp):
171 if callable(func_or_exp):
172 kw = kwargs.get('kw',{})
172 kw = kwargs.get('kw',{})
173 job = BackgroundJobFunc(func_or_exp,*args,**kw)
173 job = BackgroundJobFunc(func_or_exp,*args,**kw)
174 elif isinstance(func_or_exp, str):
174 elif isinstance(func_or_exp, str):
175 if not args:
175 if not args:
176 frame = sys._getframe(1)
176 frame = sys._getframe(1)
177 glob, loc = frame.f_globals, frame.f_locals
177 glob, loc = frame.f_globals, frame.f_locals
178 elif len(args)==1:
178 elif len(args)==1:
179 glob = loc = args[0]
179 glob = loc = args[0]
180 elif len(args)==2:
180 elif len(args)==2:
181 glob,loc = args
181 glob,loc = args
182 else:
182 else:
183 raise ValueError(
183 raise ValueError(
184 'Expression jobs take at most 2 args (globals,locals)')
184 'Expression jobs take at most 2 args (globals,locals)')
185 job = BackgroundJobExpr(func_or_exp, glob, loc)
185 job = BackgroundJobExpr(func_or_exp, glob, loc)
186 else:
186 else:
187 raise TypeError('invalid args for new job')
187 raise TypeError('invalid args for new job')
188
188
189 if kwargs.get('daemon', False):
189 if kwargs.get('daemon', False):
190 job.daemon = True
190 job.daemon = True
191 job.num = self._current_job_id
191 job.num = self._current_job_id
192 self._current_job_id += 1
192 self._current_job_id += 1
193 self.running.append(job)
193 self.running.append(job)
194 self.all[job.num] = job
194 self.all[job.num] = job
195 debug('Starting job # %s in a separate thread.' % job.num)
195 debug('Starting job # %s in a separate thread.' % job.num)
196 job.start()
196 job.start()
197 return job
197 return job
198
198
199 def __getitem__(self, job_key):
199 def __getitem__(self, job_key):
200 num = job_key if isinstance(job_key, int) else job_key.num
200 num = job_key if isinstance(job_key, int) else job_key.num
201 return self.all[num]
201 return self.all[num]
202
202
203 def __call__(self):
203 def __call__(self):
204 """An alias to self.status(),
204 """An alias to self.status(),
205
205
206 This allows you to simply call a job manager instance much like the
206 This allows you to simply call a job manager instance much like the
207 Unix `jobs` shell command."""
207 Unix `jobs` shell command."""
208
208
209 return self.status()
209 return self.status()
210
210
211 def _update_status(self):
211 def _update_status(self):
212 """Update the status of the job lists.
212 """Update the status of the job lists.
213
213
214 This method moves finished jobs to one of two lists:
214 This method moves finished jobs to one of two lists:
215 - self.completed: jobs which completed successfully
215 - self.completed: jobs which completed successfully
216 - self.dead: jobs which finished but died.
216 - self.dead: jobs which finished but died.
217
217
218 It also copies those jobs to corresponding _report lists. These lists
218 It also copies those jobs to corresponding _report lists. These lists
219 are used to report jobs completed/dead since the last update, and are
219 are used to report jobs completed/dead since the last update, and are
220 then cleared by the reporting function after each call."""
220 then cleared by the reporting function after each call."""
221
221
222 # Status codes
222 # Status codes
223 srun, scomp, sdead = self._s_running, self._s_completed, self._s_dead
223 srun, scomp, sdead = self._s_running, self._s_completed, self._s_dead
224 # State lists, use the actual lists b/c the public names are properties
224 # State lists, use the actual lists b/c the public names are properties
225 # that call this very function on access
225 # that call this very function on access
226 running, completed, dead = self._running, self._completed, self._dead
226 running, completed, dead = self._running, self._completed, self._dead
227
227
228 # Now, update all state lists
228 # Now, update all state lists
229 for num, job in enumerate(running):
229 for num, job in enumerate(running):
230 stat = job.stat_code
230 stat = job.stat_code
231 if stat == srun:
231 if stat == srun:
232 continue
232 continue
233 elif stat == scomp:
233 elif stat == scomp:
234 completed.append(job)
234 completed.append(job)
235 self._comp_report.append(job)
235 self._comp_report.append(job)
236 running[num] = False
236 running[num] = False
237 elif stat == sdead:
237 elif stat == sdead:
238 dead.append(job)
238 dead.append(job)
239 self._dead_report.append(job)
239 self._dead_report.append(job)
240 running[num] = False
240 running[num] = False
241 # Remove dead/completed jobs from running list
241 # Remove dead/completed jobs from running list
242 running[:] = filter(None, running)
242 running[:] = filter(None, running)
243
243
244 def _group_report(self,group,name):
244 def _group_report(self,group,name):
245 """Report summary for a given job group.
245 """Report summary for a given job group.
246
246
247 Return True if the group had any elements."""
247 Return True if the group had any elements."""
248
248
249 if group:
249 if group:
250 print('%s jobs:' % name)
250 print('%s jobs:' % name)
251 for job in group:
251 for job in group:
252 print('%s : %s' % (job.num,job))
252 print('%s : %s' % (job.num,job))
253 print()
253 print()
254 return True
254 return True
255
255
256 def _group_flush(self,group,name):
256 def _group_flush(self,group,name):
257 """Flush a given job group
257 """Flush a given job group
258
258
259 Return True if the group had any elements."""
259 Return True if the group had any elements."""
260
260
261 njobs = len(group)
261 njobs = len(group)
262 if njobs:
262 if njobs:
263 plural = {1:''}.setdefault(njobs,'s')
263 plural = {1:''}.setdefault(njobs,'s')
264 print('Flushing %s %s job%s.' % (njobs,name,plural))
264 print('Flushing %s %s job%s.' % (njobs,name,plural))
265 group[:] = []
265 group[:] = []
266 return True
266 return True
267
267
268 def _status_new(self):
268 def _status_new(self):
269 """Print the status of newly finished jobs.
269 """Print the status of newly finished jobs.
270
270
271 Return True if any new jobs are reported.
271 Return True if any new jobs are reported.
272
272
273 This call resets its own state every time, so it only reports jobs
273 This call resets its own state every time, so it only reports jobs
274 which have finished since the last time it was called."""
274 which have finished since the last time it was called."""
275
275
276 self._update_status()
276 self._update_status()
277 new_comp = self._group_report(self._comp_report, 'Completed')
277 new_comp = self._group_report(self._comp_report, 'Completed')
278 new_dead = self._group_report(self._dead_report,
278 new_dead = self._group_report(self._dead_report,
279 'Dead, call jobs.traceback() for details')
279 'Dead, call jobs.traceback() for details')
280 self._comp_report[:] = []
280 self._comp_report[:] = []
281 self._dead_report[:] = []
281 self._dead_report[:] = []
282 return new_comp or new_dead
282 return new_comp or new_dead
283
283
284 def status(self,verbose=0):
284 def status(self,verbose=0):
285 """Print a status of all jobs currently being managed."""
285 """Print a status of all jobs currently being managed."""
286
286
287 self._update_status()
287 self._update_status()
288 self._group_report(self.running,'Running')
288 self._group_report(self.running,'Running')
289 self._group_report(self.completed,'Completed')
289 self._group_report(self.completed,'Completed')
290 self._group_report(self.dead,'Dead')
290 self._group_report(self.dead,'Dead')
291 # Also flush the report queues
291 # Also flush the report queues
292 self._comp_report[:] = []
292 self._comp_report[:] = []
293 self._dead_report[:] = []
293 self._dead_report[:] = []
294
294
295 def remove(self,num):
295 def remove(self,num):
296 """Remove a finished (completed or dead) job."""
296 """Remove a finished (completed or dead) job."""
297
297
298 try:
298 try:
299 job = self.all[num]
299 job = self.all[num]
300 except KeyError:
300 except KeyError:
301 error('Job #%s not found' % num)
301 error('Job #%s not found' % num)
302 else:
302 else:
303 stat_code = job.stat_code
303 stat_code = job.stat_code
304 if stat_code == self._s_running:
304 if stat_code == self._s_running:
305 error('Job #%s is still running, it can not be removed.' % num)
305 error('Job #%s is still running, it can not be removed.' % num)
306 return
306 return
307 elif stat_code == self._s_completed:
307 elif stat_code == self._s_completed:
308 self.completed.remove(job)
308 self.completed.remove(job)
309 elif stat_code == self._s_dead:
309 elif stat_code == self._s_dead:
310 self.dead.remove(job)
310 self.dead.remove(job)
311
311
312 def flush(self):
312 def flush(self):
313 """Flush all finished jobs (completed and dead) from lists.
313 """Flush all finished jobs (completed and dead) from lists.
314
314
315 Running jobs are never flushed.
315 Running jobs are never flushed.
316
316
317 It first calls _status_new(), to update info. If any jobs have
317 It first calls _status_new(), to update info. If any jobs have
318 completed since the last _status_new() call, the flush operation
318 completed since the last _status_new() call, the flush operation
319 aborts."""
319 aborts."""
320
320
321 # Remove the finished jobs from the master dict
321 # Remove the finished jobs from the master dict
322 alljobs = self.all
322 alljobs = self.all
323 for job in self.completed+self.dead:
323 for job in self.completed+self.dead:
324 del(alljobs[job.num])
324 del(alljobs[job.num])
325
325
326 # Now flush these lists completely
326 # Now flush these lists completely
327 fl_comp = self._group_flush(self.completed, 'Completed')
327 fl_comp = self._group_flush(self.completed, 'Completed')
328 fl_dead = self._group_flush(self.dead, 'Dead')
328 fl_dead = self._group_flush(self.dead, 'Dead')
329 if not (fl_comp or fl_dead):
329 if not (fl_comp or fl_dead):
330 print('No jobs to flush.')
330 print('No jobs to flush.')
331
331
332 def result(self,num):
332 def result(self,num):
333 """result(N) -> return the result of job N."""
333 """result(N) -> return the result of job N."""
334 try:
334 try:
335 return self.all[num].result
335 return self.all[num].result
336 except KeyError:
336 except KeyError:
337 error('Job #%s not found' % num)
337 error('Job #%s not found' % num)
338
338
339 def _traceback(self, job):
339 def _traceback(self, job):
340 num = job if isinstance(job, int) else job.num
340 num = job if isinstance(job, int) else job.num
341 try:
341 try:
342 self.all[num].traceback()
342 self.all[num].traceback()
343 except KeyError:
343 except KeyError:
344 error('Job #%s not found' % num)
344 error('Job #%s not found' % num)
345
345
346 def traceback(self, job=None):
346 def traceback(self, job=None):
347 if job is None:
347 if job is None:
348 self._update_status()
348 self._update_status()
349 for deadjob in self.dead:
349 for deadjob in self.dead:
350 print("Traceback for: %r" % deadjob)
350 print("Traceback for: %r" % deadjob)
351 self._traceback(deadjob)
351 self._traceback(deadjob)
352 print()
352 print()
353 else:
353 else:
354 self._traceback(job)
354 self._traceback(job)
355
355
356
356
357 class BackgroundJobBase(threading.Thread):
357 class BackgroundJobBase(threading.Thread):
358 """Base class to build BackgroundJob classes.
358 """Base class to build BackgroundJob classes.
359
359
360 The derived classes must implement:
360 The derived classes must implement:
361
361
362 - Their own __init__, since the one here raises NotImplementedError. The
362 - Their own __init__, since the one here raises NotImplementedError. The
363 derived constructor must call self._init() at the end, to provide common
363 derived constructor must call self._init() at the end, to provide common
364 initialization.
364 initialization.
365
365
366 - A strform attribute used in calls to __str__.
366 - A strform attribute used in calls to __str__.
367
367
368 - A call() method, which will make the actual execution call and must
368 - A call() method, which will make the actual execution call and must
369 return a value to be held in the 'result' field of the job object.
369 return a value to be held in the 'result' field of the job object.
370 """
370 """
371
371
372 # Class constants for status, in string and as numerical codes (when
372 # Class constants for status, in string and as numerical codes (when
373 # updating jobs lists, we don't want to do string comparisons). This will
373 # updating jobs lists, we don't want to do string comparisons). This will
374 # be done at every user prompt, so it has to be as fast as possible
374 # be done at every user prompt, so it has to be as fast as possible
375 stat_created = 'Created'; stat_created_c = 0
375 stat_created = 'Created'; stat_created_c = 0
376 stat_running = 'Running'; stat_running_c = 1
376 stat_running = 'Running'; stat_running_c = 1
377 stat_completed = 'Completed'; stat_completed_c = 2
377 stat_completed = 'Completed'; stat_completed_c = 2
378 stat_dead = 'Dead (Exception), call jobs.traceback() for details'
378 stat_dead = 'Dead (Exception), call jobs.traceback() for details'
379 stat_dead_c = -1
379 stat_dead_c = -1
380
380
381 def __init__(self):
381 def __init__(self):
382 """Must be implemented in subclasses.
382 """Must be implemented in subclasses.
383
383
384 Subclasses must call :meth:`_init` for standard initialisation.
384 Subclasses must call :meth:`_init` for standard initialisation.
385 """
385 """
386 raise NotImplementedError("This class can not be instantiated directly.")
386 raise NotImplementedError("This class can not be instantiated directly.")
387
387
388 def _init(self):
388 def _init(self):
389 """Common initialization for all BackgroundJob objects"""
389 """Common initialization for all BackgroundJob objects"""
390
390
391 for attr in ['call','strform']:
391 for attr in ['call','strform']:
392 assert hasattr(self,attr), "Missing attribute <%s>" % attr
392 assert hasattr(self,attr), "Missing attribute <%s>" % attr
393
393
394 # The num tag can be set by an external job manager
394 # The num tag can be set by an external job manager
395 self.num = None
395 self.num = None
396
396
397 self.status = BackgroundJobBase.stat_created
397 self.status = BackgroundJobBase.stat_created
398 self.stat_code = BackgroundJobBase.stat_created_c
398 self.stat_code = BackgroundJobBase.stat_created_c
399 self.finished = False
399 self.finished = False
400 self.result = '<BackgroundJob has not completed>'
400 self.result = '<BackgroundJob has not completed>'
401
401
402 # reuse the ipython traceback handler if we can get to it, otherwise
402 # reuse the ipython traceback handler if we can get to it, otherwise
403 # make a new one
403 # make a new one
404 try:
404 try:
405 make_tb = get_ipython().InteractiveTB.text
405 make_tb = get_ipython().InteractiveTB.text
406 except:
406 except:
407 make_tb = AutoFormattedTB(mode = 'Context',
407 make_tb = AutoFormattedTB(mode = 'Context',
408 color_scheme='NoColor',
408 color_scheme='NoColor',
409 tb_offset = 1).text
409 tb_offset = 1).text
410 # Note that the actual API for text() requires the three args to be
410 # Note that the actual API for text() requires the three args to be
411 # passed in, so we wrap it in a simple lambda.
411 # passed in, so we wrap it in a simple lambda.
412 self._make_tb = lambda : make_tb(None, None, None)
412 self._make_tb = lambda : make_tb(None, None, None)
413
413
414 # Hold a formatted traceback if one is generated.
414 # Hold a formatted traceback if one is generated.
415 self._tb = None
415 self._tb = None
416
416
417 threading.Thread.__init__(self)
417 threading.Thread.__init__(self)
418
418
419 def __str__(self):
419 def __str__(self):
420 return self.strform
420 return self.strform
421
421
422 def __repr__(self):
422 def __repr__(self):
423 return '<BackgroundJob #%d: %s>' % (self.num, self.strform)
423 return '<BackgroundJob #%d: %s>' % (self.num, self.strform)
424
424
425 def traceback(self):
425 def traceback(self):
426 print(self._tb)
426 print(self._tb)
427
427
428 def run(self):
428 def run(self):
429 try:
429 try:
430 self.status = BackgroundJobBase.stat_running
430 self.status = BackgroundJobBase.stat_running
431 self.stat_code = BackgroundJobBase.stat_running_c
431 self.stat_code = BackgroundJobBase.stat_running_c
432 self.result = self.call()
432 self.result = self.call()
433 except:
433 except:
434 self.status = BackgroundJobBase.stat_dead
434 self.status = BackgroundJobBase.stat_dead
435 self.stat_code = BackgroundJobBase.stat_dead_c
435 self.stat_code = BackgroundJobBase.stat_dead_c
436 self.finished = None
436 self.finished = None
437 self.result = ('<BackgroundJob died, call jobs.traceback() for details>')
437 self.result = ('<BackgroundJob died, call jobs.traceback() for details>')
438 self._tb = self._make_tb()
438 self._tb = self._make_tb()
439 else:
439 else:
440 self.status = BackgroundJobBase.stat_completed
440 self.status = BackgroundJobBase.stat_completed
441 self.stat_code = BackgroundJobBase.stat_completed_c
441 self.stat_code = BackgroundJobBase.stat_completed_c
442 self.finished = True
442 self.finished = True
443
443
444
444
445 class BackgroundJobExpr(BackgroundJobBase):
445 class BackgroundJobExpr(BackgroundJobBase):
446 """Evaluate an expression as a background job (uses a separate thread)."""
446 """Evaluate an expression as a background job (uses a separate thread)."""
447
447
448 def __init__(self, expression, glob=None, loc=None):
448 def __init__(self, expression, glob=None, loc=None):
449 """Create a new job from a string which can be fed to eval().
449 """Create a new job from a string which can be fed to eval().
450
450
451 global/locals dicts can be provided, which will be passed to the eval
451 global/locals dicts can be provided, which will be passed to the eval
452 call."""
452 call."""
453
453
454 # fail immediately if the given expression can't be compiled
454 # fail immediately if the given expression can't be compiled
455 self.code = compile(expression,'<BackgroundJob compilation>','eval')
455 self.code = compile(expression,'<BackgroundJob compilation>','eval')
456
456
457 glob = {} if glob is None else glob
457 glob = {} if glob is None else glob
458 loc = {} if loc is None else loc
458 loc = {} if loc is None else loc
459 self.expression = self.strform = expression
459 self.expression = self.strform = expression
460 self.glob = glob
460 self.glob = glob
461 self.loc = loc
461 self.loc = loc
462 self._init()
462 self._init()
463
463
464 def call(self):
464 def call(self):
465 return eval(self.code,self.glob,self.loc)
465 return eval(self.code,self.glob,self.loc)
466
466
467
467
468 class BackgroundJobFunc(BackgroundJobBase):
468 class BackgroundJobFunc(BackgroundJobBase):
469 """Run a function call as a background job (uses a separate thread)."""
469 """Run a function call as a background job (uses a separate thread)."""
470
470
471 def __init__(self, func, *args, **kwargs):
471 def __init__(self, func, *args, **kwargs):
472 """Create a new job from a callable object.
472 """Create a new job from a callable object.
473
473
474 Any positional arguments and keyword args given to this constructor
474 Any positional arguments and keyword args given to this constructor
475 after the initial callable are passed directly to it."""
475 after the initial callable are passed directly to it."""
476
476
477 if not callable(func):
477 if not callable(func):
478 raise TypeError(
478 raise TypeError(
479 'first argument to BackgroundJobFunc must be callable')
479 'first argument to BackgroundJobFunc must be callable')
480
480
481 self.func = func
481 self.func = func
482 self.args = args
482 self.args = args
483 self.kwargs = kwargs
483 self.kwargs = kwargs
484 # The string form will only include the function passed, because
484 # The string form will only include the function passed, because
485 # generating string representations of the arguments is a potentially
485 # generating string representations of the arguments is a potentially
486 # _very_ expensive operation (e.g. with large arrays).
486 # _very_ expensive operation (e.g. with large arrays).
487 self.strform = str(func)
487 self.strform = str(func)
488 self._init()
488 self._init()
489
489
490 def call(self):
490 def call(self):
491 return self.func(*self.args, **self.kwargs)
491 return self.func(*self.args, **self.kwargs)
@@ -1,672 +1,672 b''
1 """Module for interactive demos using IPython.
1 """Module for interactive demos using IPython.
2
2
3 This module implements a few classes for running Python scripts interactively
3 This module implements a few classes for running Python scripts interactively
4 in IPython for demonstrations. With very simple markup (a few tags in
4 in IPython for demonstrations. With very simple markup (a few tags in
5 comments), you can control points where the script stops executing and returns
5 comments), you can control points where the script stops executing and returns
6 control to IPython.
6 control to IPython.
7
7
8
8
9 Provided classes
9 Provided classes
10 ----------------
10 ----------------
11
11
12 The classes are (see their docstrings for further details):
12 The classes are (see their docstrings for further details):
13
13
14 - Demo: pure python demos
14 - Demo: pure python demos
15
15
16 - IPythonDemo: demos with input to be processed by IPython as if it had been
16 - IPythonDemo: demos with input to be processed by IPython as if it had been
17 typed interactively (so magics work, as well as any other special syntax you
17 typed interactively (so magics work, as well as any other special syntax you
18 may have added via input prefilters).
18 may have added via input prefilters).
19
19
20 - LineDemo: single-line version of the Demo class. These demos are executed
20 - LineDemo: single-line version of the Demo class. These demos are executed
21 one line at a time, and require no markup.
21 one line at a time, and require no markup.
22
22
23 - IPythonLineDemo: IPython version of the LineDemo class (the demo is
23 - IPythonLineDemo: IPython version of the LineDemo class (the demo is
24 executed a line at a time, but processed via IPython).
24 executed a line at a time, but processed via IPython).
25
25
26 - ClearMixin: mixin to make Demo classes with less visual clutter. It
26 - ClearMixin: mixin to make Demo classes with less visual clutter. It
27 declares an empty marquee and a pre_cmd that clears the screen before each
27 declares an empty marquee and a pre_cmd that clears the screen before each
28 block (see Subclassing below).
28 block (see Subclassing below).
29
29
30 - ClearDemo, ClearIPDemo: mixin-enabled versions of the Demo and IPythonDemo
30 - ClearDemo, ClearIPDemo: mixin-enabled versions of the Demo and IPythonDemo
31 classes.
31 classes.
32
32
33 Inheritance diagram:
33 Inheritance diagram:
34
34
35 .. inheritance-diagram:: IPython.lib.demo
35 .. inheritance-diagram:: IPython.lib.demo
36 :parts: 3
36 :parts: 3
37
37
38 Subclassing
38 Subclassing
39 -----------
39 -----------
40
40
41 The classes here all include a few methods meant to make customization by
41 The classes here all include a few methods meant to make customization by
42 subclassing more convenient. Their docstrings below have some more details:
42 subclassing more convenient. Their docstrings below have some more details:
43
43
44 - highlight(): format every block and optionally highlight comments and
44 - highlight(): format every block and optionally highlight comments and
45 docstring content.
45 docstring content.
46
46
47 - marquee(): generates a marquee to provide visible on-screen markers at each
47 - marquee(): generates a marquee to provide visible on-screen markers at each
48 block start and end.
48 block start and end.
49
49
50 - pre_cmd(): run right before the execution of each block.
50 - pre_cmd(): run right before the execution of each block.
51
51
52 - post_cmd(): run right after the execution of each block. If the block
52 - post_cmd(): run right after the execution of each block. If the block
53 raises an exception, this is NOT called.
53 raises an exception, this is NOT called.
54
54
55
55
56 Operation
56 Operation
57 ---------
57 ---------
58
58
59 The file is run in its own empty namespace (though you can pass it a string of
59 The file is run in its own empty namespace (though you can pass it a string of
60 arguments as if in a command line environment, and it will see those as
60 arguments as if in a command line environment, and it will see those as
61 sys.argv). But at each stop, the global IPython namespace is updated with the
61 sys.argv). But at each stop, the global IPython namespace is updated with the
62 current internal demo namespace, so you can work interactively with the data
62 current internal demo namespace, so you can work interactively with the data
63 accumulated so far.
63 accumulated so far.
64
64
65 By default, each block of code is printed (with syntax highlighting) before
65 By default, each block of code is printed (with syntax highlighting) before
66 executing it and you have to confirm execution. This is intended to show the
66 executing it and you have to confirm execution. This is intended to show the
67 code to an audience first so you can discuss it, and only proceed with
67 code to an audience first so you can discuss it, and only proceed with
68 execution once you agree. There are a few tags which allow you to modify this
68 execution once you agree. There are a few tags which allow you to modify this
69 behavior.
69 behavior.
70
70
71 The supported tags are:
71 The supported tags are:
72
72
73 # <demo> stop
73 # <demo> stop
74
74
75 Defines block boundaries, the points where IPython stops execution of the
75 Defines block boundaries, the points where IPython stops execution of the
76 file and returns to the interactive prompt.
76 file and returns to the interactive prompt.
77
77
78 You can optionally mark the stop tag with extra dashes before and after the
78 You can optionally mark the stop tag with extra dashes before and after the
79 word 'stop', to help visually distinguish the blocks in a text editor:
79 word 'stop', to help visually distinguish the blocks in a text editor:
80
80
81 # <demo> --- stop ---
81 # <demo> --- stop ---
82
82
83
83
84 # <demo> silent
84 # <demo> silent
85
85
86 Make a block execute silently (and hence automatically). Typically used in
86 Make a block execute silently (and hence automatically). Typically used in
87 cases where you have some boilerplate or initialization code which you need
87 cases where you have some boilerplate or initialization code which you need
88 executed but do not want to be seen in the demo.
88 executed but do not want to be seen in the demo.
89
89
90 # <demo> auto
90 # <demo> auto
91
91
92 Make a block execute automatically, but still being printed. Useful for
92 Make a block execute automatically, but still being printed. Useful for
93 simple code which does not warrant discussion, since it avoids the extra
93 simple code which does not warrant discussion, since it avoids the extra
94 manual confirmation.
94 manual confirmation.
95
95
96 # <demo> auto_all
96 # <demo> auto_all
97
97
98 This tag can _only_ be in the first block, and if given it overrides the
98 This tag can _only_ be in the first block, and if given it overrides the
99 individual auto tags to make the whole demo fully automatic (no block asks
99 individual auto tags to make the whole demo fully automatic (no block asks
100 for confirmation). It can also be given at creation time (or the attribute
100 for confirmation). It can also be given at creation time (or the attribute
101 set later) to override what's in the file.
101 set later) to override what's in the file.
102
102
103 While _any_ python file can be run as a Demo instance, if there are no stop
103 While _any_ python file can be run as a Demo instance, if there are no stop
104 tags the whole file will run in a single block (no different that calling
104 tags the whole file will run in a single block (no different that calling
105 first %pycat and then %run). The minimal markup to make this useful is to
105 first %pycat and then %run). The minimal markup to make this useful is to
106 place a set of stop tags; the other tags are only there to let you fine-tune
106 place a set of stop tags; the other tags are only there to let you fine-tune
107 the execution.
107 the execution.
108
108
109 This is probably best explained with the simple example file below. You can
109 This is probably best explained with the simple example file below. You can
110 copy this into a file named ex_demo.py, and try running it via::
110 copy this into a file named ex_demo.py, and try running it via::
111
111
112 from IPython.lib.demo import Demo
112 from IPython.lib.demo import Demo
113 d = Demo('ex_demo.py')
113 d = Demo('ex_demo.py')
114 d()
114 d()
115
115
116 Each time you call the demo object, it runs the next block. The demo object
116 Each time you call the demo object, it runs the next block. The demo object
117 has a few useful methods for navigation, like again(), edit(), jump(), seek()
117 has a few useful methods for navigation, like again(), edit(), jump(), seek()
118 and back(). It can be reset for a new run via reset() or reloaded from disk
118 and back(). It can be reset for a new run via reset() or reloaded from disk
119 (in case you've edited the source) via reload(). See their docstrings below.
119 (in case you've edited the source) via reload(). See their docstrings below.
120
120
121 Note: To make this simpler to explore, a file called "demo-exercizer.py" has
121 Note: To make this simpler to explore, a file called "demo-exercizer.py" has
122 been added to the "docs/examples/core" directory. Just cd to this directory in
122 been added to the "docs/examples/core" directory. Just cd to this directory in
123 an IPython session, and type::
123 an IPython session, and type::
124
124
125 %run demo-exercizer.py
125 %run demo-exercizer.py
126
126
127 and then follow the directions.
127 and then follow the directions.
128
128
129 Example
129 Example
130 -------
130 -------
131
131
132 The following is a very simple example of a valid demo file.
132 The following is a very simple example of a valid demo file.
133
133
134 ::
134 ::
135
135
136 #################### EXAMPLE DEMO <ex_demo.py> ###############################
136 #################### EXAMPLE DEMO <ex_demo.py> ###############################
137 '''A simple interactive demo to illustrate the use of IPython's Demo class.'''
137 '''A simple interactive demo to illustrate the use of IPython's Demo class.'''
138
138
139 print 'Hello, welcome to an interactive IPython demo.'
139 print 'Hello, welcome to an interactive IPython demo.'
140
140
141 # The mark below defines a block boundary, which is a point where IPython will
141 # The mark below defines a block boundary, which is a point where IPython will
142 # stop execution and return to the interactive prompt. The dashes are actually
142 # stop execution and return to the interactive prompt. The dashes are actually
143 # optional and used only as a visual aid to clearly separate blocks while
143 # optional and used only as a visual aid to clearly separate blocks while
144 # editing the demo code.
144 # editing the demo code.
145 # <demo> stop
145 # <demo> stop
146
146
147 x = 1
147 x = 1
148 y = 2
148 y = 2
149
149
150 # <demo> stop
150 # <demo> stop
151
151
152 # the mark below makes this block as silent
152 # the mark below makes this block as silent
153 # <demo> silent
153 # <demo> silent
154
154
155 print 'This is a silent block, which gets executed but not printed.'
155 print 'This is a silent block, which gets executed but not printed.'
156
156
157 # <demo> stop
157 # <demo> stop
158 # <demo> auto
158 # <demo> auto
159 print 'This is an automatic block.'
159 print 'This is an automatic block.'
160 print 'It is executed without asking for confirmation, but printed.'
160 print 'It is executed without asking for confirmation, but printed.'
161 z = x+y
161 z = x+y
162
162
163 print 'z=',x
163 print 'z=',x
164
164
165 # <demo> stop
165 # <demo> stop
166 # This is just another normal block.
166 # This is just another normal block.
167 print 'z is now:', z
167 print 'z is now:', z
168
168
169 print 'bye!'
169 print 'bye!'
170 ################### END EXAMPLE DEMO <ex_demo.py> ############################
170 ################### END EXAMPLE DEMO <ex_demo.py> ############################
171 """
171 """
172
172
173
173
174 #*****************************************************************************
174 #*****************************************************************************
175 # Copyright (C) 2005-2006 Fernando Perez. <Fernando.Perez@colorado.edu>
175 # Copyright (C) 2005-2006 Fernando Perez. <Fernando.Perez@colorado.edu>
176 #
176 #
177 # Distributed under the terms of the BSD License. The full license is in
177 # Distributed under the terms of the BSD License. The full license is in
178 # the file COPYING, distributed as part of this software.
178 # the file COPYING, distributed as part of this software.
179 #
179 #
180 #*****************************************************************************
180 #*****************************************************************************
181
181
182 import os
182 import os
183 import re
183 import re
184 import shlex
184 import shlex
185 import sys
185 import sys
186 import pygments
186 import pygments
187 from pathlib import Path
187 from pathlib import Path
188
188
189 from IPython.utils.text import marquee
189 from IPython.utils.text import marquee
190 from IPython.utils import openpy
190 from IPython.utils import openpy
191 from IPython.utils import py3compat
191 from IPython.utils import py3compat
192 __all__ = ['Demo','IPythonDemo','LineDemo','IPythonLineDemo','DemoError']
192 __all__ = ['Demo','IPythonDemo','LineDemo','IPythonLineDemo','DemoError']
193
193
194 class DemoError(Exception): pass
194 class DemoError(Exception): pass
195
195
196 def re_mark(mark):
196 def re_mark(mark):
197 return re.compile(r'^\s*#\s+<demo>\s+%s\s*$' % mark,re.MULTILINE)
197 return re.compile(r'^\s*#\s+<demo>\s+%s\s*$' % mark,re.MULTILINE)
198
198
199 class Demo(object):
199 class Demo(object):
200
200
201 re_stop = re_mark(r'-*\s?stop\s?-*')
201 re_stop = re_mark(r'-*\s?stop\s?-*')
202 re_silent = re_mark('silent')
202 re_silent = re_mark('silent')
203 re_auto = re_mark('auto')
203 re_auto = re_mark('auto')
204 re_auto_all = re_mark('auto_all')
204 re_auto_all = re_mark('auto_all')
205
205
206 def __init__(self,src,title='',arg_str='',auto_all=None, format_rst=False,
206 def __init__(self,src,title='',arg_str='',auto_all=None, format_rst=False,
207 formatter='terminal', style='default'):
207 formatter='terminal', style='default'):
208 """Make a new demo object. To run the demo, simply call the object.
208 """Make a new demo object. To run the demo, simply call the object.
209
209
210 See the module docstring for full details and an example (you can use
210 See the module docstring for full details and an example (you can use
211 IPython.Demo? in IPython to see it).
211 IPython.Demo? in IPython to see it).
212
212
213 Inputs:
213 Inputs:
214
214
215 - src is either a file, or file-like object, or a
215 - src is either a file, or file-like object, or a
216 string that can be resolved to a filename.
216 string that can be resolved to a filename.
217
217
218 Optional inputs:
218 Optional inputs:
219
219
220 - title: a string to use as the demo name. Of most use when the demo
220 - title: a string to use as the demo name. Of most use when the demo
221 you are making comes from an object that has no filename, or if you
221 you are making comes from an object that has no filename, or if you
222 want an alternate denotation distinct from the filename.
222 want an alternate denotation distinct from the filename.
223
223
224 - arg_str(''): a string of arguments, internally converted to a list
224 - arg_str(''): a string of arguments, internally converted to a list
225 just like sys.argv, so the demo script can see a similar
225 just like sys.argv, so the demo script can see a similar
226 environment.
226 environment.
227
227
228 - auto_all(None): global flag to run all blocks automatically without
228 - auto_all(None): global flag to run all blocks automatically without
229 confirmation. This attribute overrides the block-level tags and
229 confirmation. This attribute overrides the block-level tags and
230 applies to the whole demo. It is an attribute of the object, and
230 applies to the whole demo. It is an attribute of the object, and
231 can be changed at runtime simply by reassigning it to a boolean
231 can be changed at runtime simply by reassigning it to a boolean
232 value.
232 value.
233
233
234 - format_rst(False): a bool to enable comments and doc strings
234 - format_rst(False): a bool to enable comments and doc strings
235 formatting with pygments rst lexer
235 formatting with pygments rst lexer
236
236
237 - formatter('terminal'): a string of pygments formatter name to be
237 - formatter('terminal'): a string of pygments formatter name to be
238 used. Useful values for terminals: terminal, terminal256,
238 used. Useful values for terminals: terminal, terminal256,
239 terminal16m
239 terminal16m
240
240
241 - style('default'): a string of pygments style name to be used.
241 - style('default'): a string of pygments style name to be used.
242 """
242 """
243 if hasattr(src, "read"):
243 if hasattr(src, "read"):
244 # It seems to be a file or a file-like object
244 # It seems to be a file or a file-like object
245 self.fname = "from a file-like object"
245 self.fname = "from a file-like object"
246 if title == '':
246 if title == '':
247 self.title = "from a file-like object"
247 self.title = "from a file-like object"
248 else:
248 else:
249 self.title = title
249 self.title = title
250 else:
250 else:
251 # Assume it's a string or something that can be converted to one
251 # Assume it's a string or something that can be converted to one
252 self.fname = src
252 self.fname = src
253 if title == '':
253 if title == '':
254 (filepath, filename) = os.path.split(src)
254 (filepath, filename) = os.path.split(src)
255 self.title = filename
255 self.title = filename
256 else:
256 else:
257 self.title = title
257 self.title = title
258 self.sys_argv = [src] + shlex.split(arg_str)
258 self.sys_argv = [src] + shlex.split(arg_str)
259 self.auto_all = auto_all
259 self.auto_all = auto_all
260 self.src = src
260 self.src = src
261
261
262 try:
262 try:
263 ip = get_ipython() # this is in builtins whenever IPython is running
263 ip = get_ipython() # this is in builtins whenever IPython is running
264 self.inside_ipython = True
264 self.inside_ipython = True
265 except NameError:
265 except NameError:
266 self.inside_ipython = False
266 self.inside_ipython = False
267
267
268 if self.inside_ipython:
268 if self.inside_ipython:
269 # get a few things from ipython. While it's a bit ugly design-wise,
269 # get a few things from ipython. While it's a bit ugly design-wise,
270 # it ensures that things like color scheme and the like are always in
270 # it ensures that things like color scheme and the like are always in
271 # sync with the ipython mode being used. This class is only meant to
271 # sync with the ipython mode being used. This class is only meant to
272 # be used inside ipython anyways, so it's OK.
272 # be used inside ipython anyways, so it's OK.
273 self.ip_ns = ip.user_ns
273 self.ip_ns = ip.user_ns
274 self.ip_colorize = ip.pycolorize
274 self.ip_colorize = ip.pycolorize
275 self.ip_showtb = ip.showtraceback
275 self.ip_showtb = ip.showtraceback
276 self.ip_run_cell = ip.run_cell
276 self.ip_run_cell = ip.run_cell
277 self.shell = ip
277 self.shell = ip
278
278
279 self.formatter = pygments.formatters.get_formatter_by_name(formatter,
279 self.formatter = pygments.formatters.get_formatter_by_name(formatter,
280 style=style)
280 style=style)
281 self.python_lexer = pygments.lexers.get_lexer_by_name("py3")
281 self.python_lexer = pygments.lexers.get_lexer_by_name("py3")
282 self.format_rst = format_rst
282 self.format_rst = format_rst
283 if format_rst:
283 if format_rst:
284 self.rst_lexer = pygments.lexers.get_lexer_by_name("rst")
284 self.rst_lexer = pygments.lexers.get_lexer_by_name("rst")
285
285
286 # load user data and initialize data structures
286 # load user data and initialize data structures
287 self.reload()
287 self.reload()
288
288
289 def fload(self):
289 def fload(self):
290 """Load file object."""
290 """Load file object."""
291 # read data and parse into blocks
291 # read data and parse into blocks
292 if hasattr(self, 'fobj') and self.fobj is not None:
292 if hasattr(self, 'fobj') and self.fobj is not None:
293 self.fobj.close()
293 self.fobj.close()
294 if hasattr(self.src, "read"):
294 if hasattr(self.src, "read"):
295 # It seems to be a file or a file-like object
295 # It seems to be a file or a file-like object
296 self.fobj = self.src
296 self.fobj = self.src
297 else:
297 else:
298 # Assume it's a string or something that can be converted to one
298 # Assume it's a string or something that can be converted to one
299 self.fobj = openpy.open(self.fname)
299 self.fobj = openpy.open(self.fname)
300
300
301 def reload(self):
301 def reload(self):
302 """Reload source from disk and initialize state."""
302 """Reload source from disk and initialize state."""
303 self.fload()
303 self.fload()
304
304
305 self.src = "".join(openpy.strip_encoding_cookie(self.fobj))
305 self.src = "".join(openpy.strip_encoding_cookie(self.fobj))
306 src_b = [b.strip() for b in self.re_stop.split(self.src) if b]
306 src_b = [b.strip() for b in self.re_stop.split(self.src) if b]
307 self._silent = [bool(self.re_silent.findall(b)) for b in src_b]
307 self._silent = [bool(self.re_silent.findall(b)) for b in src_b]
308 self._auto = [bool(self.re_auto.findall(b)) for b in src_b]
308 self._auto = [bool(self.re_auto.findall(b)) for b in src_b]
309
309
310 # if auto_all is not given (def. None), we read it from the file
310 # if auto_all is not given (def. None), we read it from the file
311 if self.auto_all is None:
311 if self.auto_all is None:
312 self.auto_all = bool(self.re_auto_all.findall(src_b[0]))
312 self.auto_all = bool(self.re_auto_all.findall(src_b[0]))
313 else:
313 else:
314 self.auto_all = bool(self.auto_all)
314 self.auto_all = bool(self.auto_all)
315
315
316 # Clean the sources from all markup so it doesn't get displayed when
316 # Clean the sources from all markup so it doesn't get displayed when
317 # running the demo
317 # running the demo
318 src_blocks = []
318 src_blocks = []
319 auto_strip = lambda s: self.re_auto.sub('',s)
319 auto_strip = lambda s: self.re_auto.sub('',s)
320 for i,b in enumerate(src_b):
320 for i,b in enumerate(src_b):
321 if self._auto[i]:
321 if self._auto[i]:
322 src_blocks.append(auto_strip(b))
322 src_blocks.append(auto_strip(b))
323 else:
323 else:
324 src_blocks.append(b)
324 src_blocks.append(b)
325 # remove the auto_all marker
325 # remove the auto_all marker
326 src_blocks[0] = self.re_auto_all.sub('',src_blocks[0])
326 src_blocks[0] = self.re_auto_all.sub('',src_blocks[0])
327
327
328 self.nblocks = len(src_blocks)
328 self.nblocks = len(src_blocks)
329 self.src_blocks = src_blocks
329 self.src_blocks = src_blocks
330
330
331 # also build syntax-highlighted source
331 # also build syntax-highlighted source
332 self.src_blocks_colored = list(map(self.highlight,self.src_blocks))
332 self.src_blocks_colored = list(map(self.highlight,self.src_blocks))
333
333
334 # ensure clean namespace and seek offset
334 # ensure clean namespace and seek offset
335 self.reset()
335 self.reset()
336
336
337 def reset(self):
337 def reset(self):
338 """Reset the namespace and seek pointer to restart the demo"""
338 """Reset the namespace and seek pointer to restart the demo"""
339 self.user_ns = {}
339 self.user_ns = {}
340 self.finished = False
340 self.finished = False
341 self.block_index = 0
341 self.block_index = 0
342
342
343 def _validate_index(self,index):
343 def _validate_index(self,index):
344 if index<0 or index>=self.nblocks:
344 if index<0 or index>=self.nblocks:
345 raise ValueError('invalid block index %s' % index)
345 raise ValueError('invalid block index %s' % index)
346
346
347 def _get_index(self,index):
347 def _get_index(self,index):
348 """Get the current block index, validating and checking status.
348 """Get the current block index, validating and checking status.
349
349
350 Returns None if the demo is finished"""
350 Returns None if the demo is finished"""
351
351
352 if index is None:
352 if index is None:
353 if self.finished:
353 if self.finished:
354 print('Demo finished. Use <demo_name>.reset() if you want to rerun it.')
354 print('Demo finished. Use <demo_name>.reset() if you want to rerun it.')
355 return None
355 return None
356 index = self.block_index
356 index = self.block_index
357 else:
357 else:
358 self._validate_index(index)
358 self._validate_index(index)
359 return index
359 return index
360
360
361 def seek(self,index):
361 def seek(self,index):
362 """Move the current seek pointer to the given block.
362 """Move the current seek pointer to the given block.
363
363
364 You can use negative indices to seek from the end, with identical
364 You can use negative indices to seek from the end, with identical
365 semantics to those of Python lists."""
365 semantics to those of Python lists."""
366 if index<0:
366 if index<0:
367 index = self.nblocks + index
367 index = self.nblocks + index
368 self._validate_index(index)
368 self._validate_index(index)
369 self.block_index = index
369 self.block_index = index
370 self.finished = False
370 self.finished = False
371
371
372 def back(self,num=1):
372 def back(self,num=1):
373 """Move the seek pointer back num blocks (default is 1)."""
373 """Move the seek pointer back num blocks (default is 1)."""
374 self.seek(self.block_index-num)
374 self.seek(self.block_index-num)
375
375
376 def jump(self,num=1):
376 def jump(self,num=1):
377 """Jump a given number of blocks relative to the current one.
377 """Jump a given number of blocks relative to the current one.
378
378
379 The offset can be positive or negative, defaults to 1."""
379 The offset can be positive or negative, defaults to 1."""
380 self.seek(self.block_index+num)
380 self.seek(self.block_index+num)
381
381
382 def again(self):
382 def again(self):
383 """Move the seek pointer back one block and re-execute."""
383 """Move the seek pointer back one block and re-execute."""
384 self.back(1)
384 self.back(1)
385 self()
385 self()
386
386
387 def edit(self,index=None):
387 def edit(self,index=None):
388 """Edit a block.
388 """Edit a block.
389
389
390 If no number is given, use the last block executed.
390 If no number is given, use the last block executed.
391
391
392 This edits the in-memory copy of the demo, it does NOT modify the
392 This edits the in-memory copy of the demo, it does NOT modify the
393 original source file. If you want to do that, simply open the file in
393 original source file. If you want to do that, simply open the file in
394 an editor and use reload() when you make changes to the file. This
394 an editor and use reload() when you make changes to the file. This
395 method is meant to let you change a block during a demonstration for
395 method is meant to let you change a block during a demonstration for
396 explanatory purposes, without damaging your original script."""
396 explanatory purposes, without damaging your original script."""
397
397
398 index = self._get_index(index)
398 index = self._get_index(index)
399 if index is None:
399 if index is None:
400 return
400 return
401 # decrease the index by one (unless we're at the very beginning), so
401 # decrease the index by one (unless we're at the very beginning), so
402 # that the default demo.edit() call opens up the sblock we've last run
402 # that the default demo.edit() call opens up the sblock we've last run
403 if index>0:
403 if index>0:
404 index -= 1
404 index -= 1
405
405
406 filename = self.shell.mktempfile(self.src_blocks[index])
406 filename = self.shell.mktempfile(self.src_blocks[index])
407 self.shell.hooks.editor(filename, 1)
407 self.shell.hooks.editor(filename, 1)
408 with open(Path(filename), "r") as f:
408 with open(Path(filename), "r") as f:
409 new_block = f.read()
409 new_block = f.read()
410 # update the source and colored block
410 # update the source and colored block
411 self.src_blocks[index] = new_block
411 self.src_blocks[index] = new_block
412 self.src_blocks_colored[index] = self.highlight(new_block)
412 self.src_blocks_colored[index] = self.highlight(new_block)
413 self.block_index = index
413 self.block_index = index
414 # call to run with the newly edited index
414 # call to run with the newly edited index
415 self()
415 self()
416
416
417 def show(self,index=None):
417 def show(self,index=None):
418 """Show a single block on screen"""
418 """Show a single block on screen"""
419
419
420 index = self._get_index(index)
420 index = self._get_index(index)
421 if index is None:
421 if index is None:
422 return
422 return
423
423
424 print(self.marquee('<%s> block # %s (%s remaining)' %
424 print(self.marquee('<%s> block # %s (%s remaining)' %
425 (self.title,index,self.nblocks-index-1)))
425 (self.title,index,self.nblocks-index-1)))
426 print(self.src_blocks_colored[index])
426 print(self.src_blocks_colored[index])
427 sys.stdout.flush()
427 sys.stdout.flush()
428
428
429 def show_all(self):
429 def show_all(self):
430 """Show entire demo on screen, block by block"""
430 """Show entire demo on screen, block by block"""
431
431
432 fname = self.title
432 fname = self.title
433 title = self.title
433 title = self.title
434 nblocks = self.nblocks
434 nblocks = self.nblocks
435 silent = self._silent
435 silent = self._silent
436 marquee = self.marquee
436 marquee = self.marquee
437 for index,block in enumerate(self.src_blocks_colored):
437 for index,block in enumerate(self.src_blocks_colored):
438 if silent[index]:
438 if silent[index]:
439 print(marquee('<%s> SILENT block # %s (%s remaining)' %
439 print(marquee('<%s> SILENT block # %s (%s remaining)' %
440 (title,index,nblocks-index-1)))
440 (title,index,nblocks-index-1)))
441 else:
441 else:
442 print(marquee('<%s> block # %s (%s remaining)' %
442 print(marquee('<%s> block # %s (%s remaining)' %
443 (title,index,nblocks-index-1)))
443 (title,index,nblocks-index-1)))
444 print(block, end=' ')
444 print(block, end=' ')
445 sys.stdout.flush()
445 sys.stdout.flush()
446
446
447 def run_cell(self,source):
447 def run_cell(self,source):
448 """Execute a string with one or more lines of code"""
448 """Execute a string with one or more lines of code"""
449
449
450 exec(source, self.user_ns)
450 exec(source, self.user_ns)
451
451
452 def __call__(self,index=None):
452 def __call__(self,index=None):
453 """run a block of the demo.
453 """run a block of the demo.
454
454
455 If index is given, it should be an integer >=1 and <= nblocks. This
455 If index is given, it should be an integer >=1 and <= nblocks. This
456 means that the calling convention is one off from typical Python
456 means that the calling convention is one off from typical Python
457 lists. The reason for the inconsistency is that the demo always
457 lists. The reason for the inconsistency is that the demo always
458 prints 'Block n/N, and N is the total, so it would be very odd to use
458 prints 'Block n/N, and N is the total, so it would be very odd to use
459 zero-indexing here."""
459 zero-indexing here."""
460
460
461 index = self._get_index(index)
461 index = self._get_index(index)
462 if index is None:
462 if index is None:
463 return
463 return
464 try:
464 try:
465 marquee = self.marquee
465 marquee = self.marquee
466 next_block = self.src_blocks[index]
466 next_block = self.src_blocks[index]
467 self.block_index += 1
467 self.block_index += 1
468 if self._silent[index]:
468 if self._silent[index]:
469 print(marquee('Executing silent block # %s (%s remaining)' %
469 print(marquee('Executing silent block # %s (%s remaining)' %
470 (index,self.nblocks-index-1)))
470 (index,self.nblocks-index-1)))
471 else:
471 else:
472 self.pre_cmd()
472 self.pre_cmd()
473 self.show(index)
473 self.show(index)
474 if self.auto_all or self._auto[index]:
474 if self.auto_all or self._auto[index]:
475 print(marquee('output:'))
475 print(marquee('output:'))
476 else:
476 else:
477 print(marquee('Press <q> to quit, <Enter> to execute...'), end=' ')
477 print(marquee('Press <q> to quit, <Enter> to execute...'), end=' ')
478 ans = py3compat.input().strip()
478 ans = py3compat.input().strip()
479 if ans:
479 if ans:
480 print(marquee('Block NOT executed'))
480 print(marquee('Block NOT executed'))
481 return
481 return
482 try:
482 try:
483 save_argv = sys.argv
483 save_argv = sys.argv
484 sys.argv = self.sys_argv
484 sys.argv = self.sys_argv
485 self.run_cell(next_block)
485 self.run_cell(next_block)
486 self.post_cmd()
486 self.post_cmd()
487 finally:
487 finally:
488 sys.argv = save_argv
488 sys.argv = save_argv
489
489
490 except:
490 except:
491 if self.inside_ipython:
491 if self.inside_ipython:
492 self.ip_showtb(filename=self.fname)
492 self.ip_showtb(filename=self.fname)
493 else:
493 else:
494 if self.inside_ipython:
494 if self.inside_ipython:
495 self.ip_ns.update(self.user_ns)
495 self.ip_ns.update(self.user_ns)
496
496
497 if self.block_index == self.nblocks:
497 if self.block_index == self.nblocks:
498 mq1 = self.marquee('END OF DEMO')
498 mq1 = self.marquee('END OF DEMO')
499 if mq1:
499 if mq1:
500 # avoid spurious print if empty marquees are used
500 # avoid spurious print if empty marquees are used
501 print()
501 print()
502 print(mq1)
502 print(mq1)
503 print(self.marquee('Use <demo_name>.reset() if you want to rerun it.'))
503 print(self.marquee('Use <demo_name>.reset() if you want to rerun it.'))
504 self.finished = True
504 self.finished = True
505
505
506 # These methods are meant to be overridden by subclasses who may wish to
506 # These methods are meant to be overridden by subclasses who may wish to
507 # customize the behavior of of their demos.
507 # customize the behavior of of their demos.
508 def marquee(self,txt='',width=78,mark='*'):
508 def marquee(self,txt='',width=78,mark='*'):
509 """Return the input string centered in a 'marquee'."""
509 """Return the input string centered in a 'marquee'."""
510 return marquee(txt,width,mark)
510 return marquee(txt,width,mark)
511
511
512 def pre_cmd(self):
512 def pre_cmd(self):
513 """Method called before executing each block."""
513 """Method called before executing each block."""
514 pass
514 pass
515
515
516 def post_cmd(self):
516 def post_cmd(self):
517 """Method called after executing each block."""
517 """Method called after executing each block."""
518 pass
518 pass
519
519
520 def highlight(self, block):
520 def highlight(self, block):
521 """Method called on each block to highlight it content"""
521 """Method called on each block to highlight it content"""
522 tokens = pygments.lex(block, self.python_lexer)
522 tokens = pygments.lex(block, self.python_lexer)
523 if self.format_rst:
523 if self.format_rst:
524 from pygments.token import Token
524 from pygments.token import Token
525 toks = []
525 toks = []
526 for token in tokens:
526 for token in tokens:
527 if token[0] == Token.String.Doc and len(token[1]) > 6:
527 if token[0] == Token.String.Doc and len(token[1]) > 6:
528 toks += pygments.lex(token[1][:3], self.python_lexer)
528 toks += pygments.lex(token[1][:3], self.python_lexer)
529 # parse doc string content by rst lexer
529 # parse doc string content by rst lexer
530 toks += pygments.lex(token[1][3:-3], self.rst_lexer)
530 toks += pygments.lex(token[1][3:-3], self.rst_lexer)
531 toks += pygments.lex(token[1][-3:], self.python_lexer)
531 toks += pygments.lex(token[1][-3:], self.python_lexer)
532 elif token[0] == Token.Comment.Single:
532 elif token[0] == Token.Comment.Single:
533 toks.append((Token.Comment.Single, token[1][0]))
533 toks.append((Token.Comment.Single, token[1][0]))
534 # parse comment content by rst lexer
534 # parse comment content by rst lexer
535 # remove the extra newline added by rst lexer
535 # remove the extra newline added by rst lexer
536 toks += list(pygments.lex(token[1][1:], self.rst_lexer))[:-1]
536 toks += list(pygments.lex(token[1][1:], self.rst_lexer))[:-1]
537 else:
537 else:
538 toks.append(token)
538 toks.append(token)
539 tokens = toks
539 tokens = toks
540 return pygments.format(tokens, self.formatter)
540 return pygments.format(tokens, self.formatter)
541
541
542
542
543 class IPythonDemo(Demo):
543 class IPythonDemo(Demo):
544 """Class for interactive demos with IPython's input processing applied.
544 """Class for interactive demos with IPython's input processing applied.
545
545
546 This subclasses Demo, but instead of executing each block by the Python
546 This subclasses Demo, but instead of executing each block by the Python
547 interpreter (via exec), it actually calls IPython on it, so that any input
547 interpreter (via exec), it actually calls IPython on it, so that any input
548 filters which may be in place are applied to the input block.
548 filters which may be in place are applied to the input block.
549
549
550 If you have an interactive environment which exposes special input
550 If you have an interactive environment which exposes special input
551 processing, you can use this class instead to write demo scripts which
551 processing, you can use this class instead to write demo scripts which
552 operate exactly as if you had typed them interactively. The default Demo
552 operate exactly as if you had typed them interactively. The default Demo
553 class requires the input to be valid, pure Python code.
553 class requires the input to be valid, pure Python code.
554 """
554 """
555
555
556 def run_cell(self,source):
556 def run_cell(self,source):
557 """Execute a string with one or more lines of code"""
557 """Execute a string with one or more lines of code"""
558
558
559 self.shell.run_cell(source)
559 self.shell.run_cell(source)
560
560
561 class LineDemo(Demo):
561 class LineDemo(Demo):
562 """Demo where each line is executed as a separate block.
562 """Demo where each line is executed as a separate block.
563
563
564 The input script should be valid Python code.
564 The input script should be valid Python code.
565
565
566 This class doesn't require any markup at all, and it's meant for simple
566 This class doesn't require any markup at all, and it's meant for simple
567 scripts (with no nesting or any kind of indentation) which consist of
567 scripts (with no nesting or any kind of indentation) which consist of
568 multiple lines of input to be executed, one at a time, as if they had been
568 multiple lines of input to be executed, one at a time, as if they had been
569 typed in the interactive prompt.
569 typed in the interactive prompt.
570
570
571 Note: the input can not have *any* indentation, which means that only
571 Note: the input can not have *any* indentation, which means that only
572 single-lines of input are accepted, not even function definitions are
572 single-lines of input are accepted, not even function definitions are
573 valid."""
573 valid."""
574
574
575 def reload(self):
575 def reload(self):
576 """Reload source from disk and initialize state."""
576 """Reload source from disk and initialize state."""
577 # read data and parse into blocks
577 # read data and parse into blocks
578 self.fload()
578 self.fload()
579 lines = self.fobj.readlines()
579 lines = self.fobj.readlines()
580 src_b = [l for l in lines if l.strip()]
580 src_b = [l for l in lines if l.strip()]
581 nblocks = len(src_b)
581 nblocks = len(src_b)
582 self.src = ''.join(lines)
582 self.src = ''.join(lines)
583 self._silent = [False]*nblocks
583 self._silent = [False]*nblocks
584 self._auto = [True]*nblocks
584 self._auto = [True]*nblocks
585 self.auto_all = True
585 self.auto_all = True
586 self.nblocks = nblocks
586 self.nblocks = nblocks
587 self.src_blocks = src_b
587 self.src_blocks = src_b
588
588
589 # also build syntax-highlighted source
589 # also build syntax-highlighted source
590 self.src_blocks_colored = list(map(self.highlight,self.src_blocks))
590 self.src_blocks_colored = list(map(self.highlight,self.src_blocks))
591
591
592 # ensure clean namespace and seek offset
592 # ensure clean namespace and seek offset
593 self.reset()
593 self.reset()
594
594
595
595
596 class IPythonLineDemo(IPythonDemo,LineDemo):
596 class IPythonLineDemo(IPythonDemo,LineDemo):
597 """Variant of the LineDemo class whose input is processed by IPython."""
597 """Variant of the LineDemo class whose input is processed by IPython."""
598 pass
598 pass
599
599
600
600
601 class ClearMixin(object):
601 class ClearMixin(object):
602 """Use this mixin to make Demo classes with less visual clutter.
602 """Use this mixin to make Demo classes with less visual clutter.
603
603
604 Demos using this mixin will clear the screen before every block and use
604 Demos using this mixin will clear the screen before every block and use
605 blank marquees.
605 blank marquees.
606
606
607 Note that in order for the methods defined here to actually override those
607 Note that in order for the methods defined here to actually override those
608 of the classes it's mixed with, it must go /first/ in the inheritance
608 of the classes it's mixed with, it must go /first/ in the inheritance
609 tree. For example:
609 tree. For example:
610
610
611 class ClearIPDemo(ClearMixin,IPythonDemo): pass
611 class ClearIPDemo(ClearMixin,IPythonDemo): pass
612
612
613 will provide an IPythonDemo class with the mixin's features.
613 will provide an IPythonDemo class with the mixin's features.
614 """
614 """
615
615
616 def marquee(self,txt='',width=78,mark='*'):
616 def marquee(self,txt='',width=78,mark='*'):
617 """Blank marquee that returns '' no matter what the input."""
617 """Blank marquee that returns '' no matter what the input."""
618 return ''
618 return ''
619
619
620 def pre_cmd(self):
620 def pre_cmd(self):
621 """Method called before executing each block.
621 """Method called before executing each block.
622
622
623 This one simply clears the screen."""
623 This one simply clears the screen."""
624 from IPython.utils.terminal import _term_clear
624 from IPython.utils.terminal import _term_clear
625 _term_clear()
625 _term_clear()
626
626
627 class ClearDemo(ClearMixin,Demo):
627 class ClearDemo(ClearMixin,Demo):
628 pass
628 pass
629
629
630
630
631 class ClearIPDemo(ClearMixin,IPythonDemo):
631 class ClearIPDemo(ClearMixin,IPythonDemo):
632 pass
632 pass
633
633
634
634
635 def slide(file_path, noclear=False, format_rst=True, formatter="terminal",
635 def slide(file_path, noclear=False, format_rst=True, formatter="terminal",
636 style="native", auto_all=False, delimiter='...'):
636 style="native", auto_all=False, delimiter='...'):
637 if noclear:
637 if noclear:
638 demo_class = Demo
638 demo_class = Demo
639 else:
639 else:
640 demo_class = ClearDemo
640 demo_class = ClearDemo
641 demo = demo_class(file_path, format_rst=format_rst, formatter=formatter,
641 demo = demo_class(file_path, format_rst=format_rst, formatter=formatter,
642 style=style, auto_all=auto_all)
642 style=style, auto_all=auto_all)
643 while not demo.finished:
643 while not demo.finished:
644 demo()
644 demo()
645 try:
645 try:
646 py3compat.input('\n' + delimiter)
646 py3compat.input('\n' + delimiter)
647 except KeyboardInterrupt:
647 except KeyboardInterrupt:
648 exit(1)
648 exit(1)
649
649
650 if __name__ == '__main__':
650 if __name__ == '__main__':
651 import argparse
651 import argparse
652 parser = argparse.ArgumentParser(description='Run python demos')
652 parser = argparse.ArgumentParser(description='Run python demos')
653 parser.add_argument('--noclear', '-C', action='store_true',
653 parser.add_argument('--noclear', '-C', action='store_true',
654 help='Do not clear terminal on each slide')
654 help='Do not clear terminal on each slide')
655 parser.add_argument('--rst', '-r', action='store_true',
655 parser.add_argument('--rst', '-r', action='store_true',
656 help='Highlight comments and dostrings as rst')
656 help='Highlight comments and dostrings as rst')
657 parser.add_argument('--formatter', '-f', default='terminal',
657 parser.add_argument('--formatter', '-f', default='terminal',
658 help='pygments formatter name could be: terminal, '
658 help='pygments formatter name could be: terminal, '
659 'terminal256, terminal16m')
659 'terminal256, terminal16m')
660 parser.add_argument('--style', '-s', default='default',
660 parser.add_argument('--style', '-s', default='default',
661 help='pygments style name')
661 help='pygments style name')
662 parser.add_argument('--auto', '-a', action='store_true',
662 parser.add_argument('--auto', '-a', action='store_true',
663 help='Run all blocks automatically without'
663 help='Run all blocks automatically without'
664 'confirmation')
664 'confirmation')
665 parser.add_argument('--delimiter', '-d', default='...',
665 parser.add_argument('--delimiter', '-d', default='...',
666 help='slides delimiter added after each slide run')
666 help='slides delimiter added after each slide run')
667 parser.add_argument('file', nargs=1,
667 parser.add_argument('file', nargs=1,
668 help='python demo file')
668 help='python demo file')
669 args = parser.parse_args()
669 args = parser.parse_args()
670 slide(args.file[0], noclear=args.noclear, format_rst=args.rst,
670 slide(args.file[0], noclear=args.noclear, format_rst=args.rst,
671 formatter=args.formatter, style=args.style, auto_all=args.auto,
671 formatter=args.formatter, style=args.style, auto_all=args.auto,
672 delimiter=args.delimiter)
672 delimiter=args.delimiter)
@@ -1,677 +1,677 b''
1 """Various display related classes.
1 """Various display related classes.
2
2
3 Authors : MinRK, gregcaporaso, dannystaple
3 Authors : MinRK, gregcaporaso, dannystaple
4 """
4 """
5 from html import escape as html_escape
5 from html import escape as html_escape
6 from os.path import exists, isfile, splitext, abspath, join, isdir
6 from os.path import exists, isfile, splitext, abspath, join, isdir
7 from os import walk, sep, fsdecode
7 from os import walk, sep, fsdecode
8
8
9 from IPython.core.display import DisplayObject, TextDisplayObject
9 from IPython.core.display import DisplayObject, TextDisplayObject
10
10
11 from typing import Tuple, Iterable
11 from typing import Tuple, Iterable
12
12
13 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
13 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
14 'FileLink', 'FileLinks', 'Code']
14 'FileLink', 'FileLinks', 'Code']
15
15
16
16
17 class Audio(DisplayObject):
17 class Audio(DisplayObject):
18 """Create an audio object.
18 """Create an audio object.
19
19
20 When this object is returned by an input cell or passed to the
20 When this object is returned by an input cell or passed to the
21 display function, it will result in Audio controls being displayed
21 display function, it will result in Audio controls being displayed
22 in the frontend (only works in the notebook).
22 in the frontend (only works in the notebook).
23
23
24 Parameters
24 Parameters
25 ----------
25 ----------
26 data : numpy array, list, unicode, str or bytes
26 data : numpy array, list, unicode, str or bytes
27 Can be one of
27 Can be one of
28
28
29 * Numpy 1d array containing the desired waveform (mono)
29 * Numpy 1d array containing the desired waveform (mono)
30 * Numpy 2d array containing waveforms for each channel.
30 * Numpy 2d array containing waveforms for each channel.
31 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
31 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
32 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
32 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
33 * List of float or integer representing the waveform (mono)
33 * List of float or integer representing the waveform (mono)
34 * String containing the filename
34 * String containing the filename
35 * Bytestring containing raw PCM data or
35 * Bytestring containing raw PCM data or
36 * URL pointing to a file on the web.
36 * URL pointing to a file on the web.
37
37
38 If the array option is used, the waveform will be normalized.
38 If the array option is used, the waveform will be normalized.
39
39
40 If a filename or url is used, the format support will be browser
40 If a filename or url is used, the format support will be browser
41 dependent.
41 dependent.
42 url : unicode
42 url : unicode
43 A URL to download the data from.
43 A URL to download the data from.
44 filename : unicode
44 filename : unicode
45 Path to a local file to load the data from.
45 Path to a local file to load the data from.
46 embed : boolean
46 embed : boolean
47 Should the audio data be embedded using a data URI (True) or should
47 Should the audio data be embedded using a data URI (True) or should
48 the original source be referenced. Set this to True if you want the
48 the original source be referenced. Set this to True if you want the
49 audio to playable later with no internet connection in the notebook.
49 audio to playable later with no internet connection in the notebook.
50
50
51 Default is `True`, unless the keyword argument `url` is set, then
51 Default is `True`, unless the keyword argument `url` is set, then
52 default value is `False`.
52 default value is `False`.
53 rate : integer
53 rate : integer
54 The sampling rate of the raw data.
54 The sampling rate of the raw data.
55 Only required when data parameter is being used as an array
55 Only required when data parameter is being used as an array
56 autoplay : bool
56 autoplay : bool
57 Set to True if the audio should immediately start playing.
57 Set to True if the audio should immediately start playing.
58 Default is `False`.
58 Default is `False`.
59 normalize : bool
59 normalize : bool
60 Whether audio should be normalized (rescaled) to the maximum possible
60 Whether audio should be normalized (rescaled) to the maximum possible
61 range. Default is `True`. When set to `False`, `data` must be between
61 range. Default is `True`. When set to `False`, `data` must be between
62 -1 and 1 (inclusive), otherwise an error is raised.
62 -1 and 1 (inclusive), otherwise an error is raised.
63 Applies only when `data` is a list or array of samples; other types of
63 Applies only when `data` is a list or array of samples; other types of
64 audio are never normalized.
64 audio are never normalized.
65
65
66 Examples
66 Examples
67 --------
67 --------
68
68
69 >>> import pytest
69 >>> import pytest
70 >>> np = pytest.importorskip("numpy")
70 >>> np = pytest.importorskip("numpy")
71
71
72 Generate a sound
72 Generate a sound
73
73
74 >>> import numpy as np
74 >>> import numpy as np
75 >>> framerate = 44100
75 >>> framerate = 44100
76 >>> t = np.linspace(0,5,framerate*5)
76 >>> t = np.linspace(0,5,framerate*5)
77 >>> data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t)
77 >>> data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t)
78 >>> Audio(data, rate=framerate)
78 >>> Audio(data, rate=framerate)
79 <IPython.lib.display.Audio object>
79 <IPython.lib.display.Audio object>
80
80
81 Can also do stereo or more channels
81 Can also do stereo or more channels
82
82
83 >>> dataleft = np.sin(2*np.pi*220*t)
83 >>> dataleft = np.sin(2*np.pi*220*t)
84 >>> dataright = np.sin(2*np.pi*224*t)
84 >>> dataright = np.sin(2*np.pi*224*t)
85 >>> Audio([dataleft, dataright], rate=framerate)
85 >>> Audio([dataleft, dataright], rate=framerate)
86 <IPython.lib.display.Audio object>
86 <IPython.lib.display.Audio object>
87
87
88 From URL:
88 From URL:
89
89
90 >>> Audio("http://www.nch.com.au/acm/8k16bitpcm.wav") # doctest: +SKIP
90 >>> Audio("http://www.nch.com.au/acm/8k16bitpcm.wav") # doctest: +SKIP
91 >>> Audio(url="http://www.w3schools.com/html/horse.ogg") # doctest: +SKIP
91 >>> Audio(url="http://www.w3schools.com/html/horse.ogg") # doctest: +SKIP
92
92
93 From a File:
93 From a File:
94
94
95 >>> Audio('/path/to/sound.wav') # doctest: +SKIP
95 >>> Audio('/path/to/sound.wav') # doctest: +SKIP
96 >>> Audio(filename='/path/to/sound.ogg') # doctest: +SKIP
96 >>> Audio(filename='/path/to/sound.ogg') # doctest: +SKIP
97
97
98 From Bytes:
98 From Bytes:
99
99
100 >>> Audio(b'RAW_WAV_DATA..') # doctest: +SKIP
100 >>> Audio(b'RAW_WAV_DATA..') # doctest: +SKIP
101 >>> Audio(data=b'RAW_WAV_DATA..') # doctest: +SKIP
101 >>> Audio(data=b'RAW_WAV_DATA..') # doctest: +SKIP
102
102
103 See Also
103 See Also
104 --------
104 --------
105 ipywidgets.Audio
105 ipywidgets.Audio
106
106
107 AUdio widget with more more flexibility and options.
107 AUdio widget with more more flexibility and options.
108
108
109 """
109 """
110 _read_flags = 'rb'
110 _read_flags = 'rb'
111
111
112 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False, normalize=True, *,
112 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False, normalize=True, *,
113 element_id=None):
113 element_id=None):
114 if filename is None and url is None and data is None:
114 if filename is None and url is None and data is None:
115 raise ValueError("No audio data found. Expecting filename, url, or data.")
115 raise ValueError("No audio data found. Expecting filename, url, or data.")
116 if embed is False and url is None:
116 if embed is False and url is None:
117 raise ValueError("No url found. Expecting url when embed=False")
117 raise ValueError("No url found. Expecting url when embed=False")
118
118
119 if url is not None and embed is not True:
119 if url is not None and embed is not True:
120 self.embed = False
120 self.embed = False
121 else:
121 else:
122 self.embed = True
122 self.embed = True
123 self.autoplay = autoplay
123 self.autoplay = autoplay
124 self.element_id = element_id
124 self.element_id = element_id
125 super(Audio, self).__init__(data=data, url=url, filename=filename)
125 super(Audio, self).__init__(data=data, url=url, filename=filename)
126
126
127 if self.data is not None and not isinstance(self.data, bytes):
127 if self.data is not None and not isinstance(self.data, bytes):
128 if rate is None:
128 if rate is None:
129 raise ValueError("rate must be specified when data is a numpy array or list of audio samples.")
129 raise ValueError("rate must be specified when data is a numpy array or list of audio samples.")
130 self.data = Audio._make_wav(data, rate, normalize)
130 self.data = Audio._make_wav(data, rate, normalize)
131
131
132 def reload(self):
132 def reload(self):
133 """Reload the raw data from file or URL."""
133 """Reload the raw data from file or URL."""
134 import mimetypes
134 import mimetypes
135 if self.embed:
135 if self.embed:
136 super(Audio, self).reload()
136 super(Audio, self).reload()
137
137
138 if self.filename is not None:
138 if self.filename is not None:
139 self.mimetype = mimetypes.guess_type(self.filename)[0]
139 self.mimetype = mimetypes.guess_type(self.filename)[0]
140 elif self.url is not None:
140 elif self.url is not None:
141 self.mimetype = mimetypes.guess_type(self.url)[0]
141 self.mimetype = mimetypes.guess_type(self.url)[0]
142 else:
142 else:
143 self.mimetype = "audio/wav"
143 self.mimetype = "audio/wav"
144
144
145 @staticmethod
145 @staticmethod
146 def _make_wav(data, rate, normalize):
146 def _make_wav(data, rate, normalize):
147 """ Transform a numpy array to a PCM bytestring """
147 """ Transform a numpy array to a PCM bytestring """
148 from io import BytesIO
148 from io import BytesIO
149 import wave
149 import wave
150
150
151 try:
151 try:
152 scaled, nchan = Audio._validate_and_normalize_with_numpy(data, normalize)
152 scaled, nchan = Audio._validate_and_normalize_with_numpy(data, normalize)
153 except ImportError:
153 except ImportError:
154 scaled, nchan = Audio._validate_and_normalize_without_numpy(data, normalize)
154 scaled, nchan = Audio._validate_and_normalize_without_numpy(data, normalize)
155
155
156 fp = BytesIO()
156 fp = BytesIO()
157 waveobj = wave.open(fp,mode='wb')
157 waveobj = wave.open(fp,mode='wb')
158 waveobj.setnchannels(nchan)
158 waveobj.setnchannels(nchan)
159 waveobj.setframerate(rate)
159 waveobj.setframerate(rate)
160 waveobj.setsampwidth(2)
160 waveobj.setsampwidth(2)
161 waveobj.setcomptype('NONE','NONE')
161 waveobj.setcomptype('NONE','NONE')
162 waveobj.writeframes(scaled)
162 waveobj.writeframes(scaled)
163 val = fp.getvalue()
163 val = fp.getvalue()
164 waveobj.close()
164 waveobj.close()
165
165
166 return val
166 return val
167
167
168 @staticmethod
168 @staticmethod
169 def _validate_and_normalize_with_numpy(data, normalize) -> Tuple[bytes, int]:
169 def _validate_and_normalize_with_numpy(data, normalize) -> Tuple[bytes, int]:
170 import numpy as np
170 import numpy as np
171
171
172 data = np.array(data, dtype=float)
172 data = np.array(data, dtype=float)
173 if len(data.shape) == 1:
173 if len(data.shape) == 1:
174 nchan = 1
174 nchan = 1
175 elif len(data.shape) == 2:
175 elif len(data.shape) == 2:
176 # In wave files,channels are interleaved. E.g.,
176 # In wave files,channels are interleaved. E.g.,
177 # "L1R1L2R2..." for stereo. See
177 # "L1R1L2R2..." for stereo. See
178 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
178 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
179 # for channel ordering
179 # for channel ordering
180 nchan = data.shape[0]
180 nchan = data.shape[0]
181 data = data.T.ravel()
181 data = data.T.ravel()
182 else:
182 else:
183 raise ValueError('Array audio input must be a 1D or 2D array')
183 raise ValueError('Array audio input must be a 1D or 2D array')
184
184
185 max_abs_value = np.max(np.abs(data))
185 max_abs_value = np.max(np.abs(data))
186 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
186 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
187 scaled = data / normalization_factor * 32767
187 scaled = data / normalization_factor * 32767
188 return scaled.astype("<h").tobytes(), nchan
188 return scaled.astype("<h").tobytes(), nchan
189
189
190 @staticmethod
190 @staticmethod
191 def _validate_and_normalize_without_numpy(data, normalize):
191 def _validate_and_normalize_without_numpy(data, normalize):
192 import array
192 import array
193 import sys
193 import sys
194
194
195 data = array.array('f', data)
195 data = array.array('f', data)
196
196
197 try:
197 try:
198 max_abs_value = float(max([abs(x) for x in data]))
198 max_abs_value = float(max([abs(x) for x in data]))
199 except TypeError as e:
199 except TypeError as e:
200 raise TypeError('Only lists of mono audio are '
200 raise TypeError('Only lists of mono audio are '
201 'supported if numpy is not installed') from e
201 'supported if numpy is not installed') from e
202
202
203 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
203 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
204 scaled = array.array('h', [int(x / normalization_factor * 32767) for x in data])
204 scaled = array.array('h', [int(x / normalization_factor * 32767) for x in data])
205 if sys.byteorder == 'big':
205 if sys.byteorder == 'big':
206 scaled.byteswap()
206 scaled.byteswap()
207 nchan = 1
207 nchan = 1
208 return scaled.tobytes(), nchan
208 return scaled.tobytes(), nchan
209
209
210 @staticmethod
210 @staticmethod
211 def _get_normalization_factor(max_abs_value, normalize):
211 def _get_normalization_factor(max_abs_value, normalize):
212 if not normalize and max_abs_value > 1:
212 if not normalize and max_abs_value > 1:
213 raise ValueError('Audio data must be between -1 and 1 when normalize=False.')
213 raise ValueError('Audio data must be between -1 and 1 when normalize=False.')
214 return max_abs_value if normalize else 1
214 return max_abs_value if normalize else 1
215
215
216 def _data_and_metadata(self):
216 def _data_and_metadata(self):
217 """shortcut for returning metadata with url information, if defined"""
217 """shortcut for returning metadata with url information, if defined"""
218 md = {}
218 md = {}
219 if self.url:
219 if self.url:
220 md['url'] = self.url
220 md['url'] = self.url
221 if md:
221 if md:
222 return self.data, md
222 return self.data, md
223 else:
223 else:
224 return self.data
224 return self.data
225
225
226 def _repr_html_(self):
226 def _repr_html_(self):
227 src = """
227 src = """
228 <audio {element_id} controls="controls" {autoplay}>
228 <audio {element_id} controls="controls" {autoplay}>
229 <source src="{src}" type="{type}" />
229 <source src="{src}" type="{type}" />
230 Your browser does not support the audio element.
230 Your browser does not support the audio element.
231 </audio>
231 </audio>
232 """
232 """
233 return src.format(src=self.src_attr(), type=self.mimetype, autoplay=self.autoplay_attr(),
233 return src.format(src=self.src_attr(), type=self.mimetype, autoplay=self.autoplay_attr(),
234 element_id=self.element_id_attr())
234 element_id=self.element_id_attr())
235
235
236 def src_attr(self):
236 def src_attr(self):
237 import base64
237 import base64
238 if self.embed and (self.data is not None):
238 if self.embed and (self.data is not None):
239 data = base64=base64.b64encode(self.data).decode('ascii')
239 data = base64=base64.b64encode(self.data).decode('ascii')
240 return """data:{type};base64,{base64}""".format(type=self.mimetype,
240 return """data:{type};base64,{base64}""".format(type=self.mimetype,
241 base64=data)
241 base64=data)
242 elif self.url is not None:
242 elif self.url is not None:
243 return self.url
243 return self.url
244 else:
244 else:
245 return ""
245 return ""
246
246
247 def autoplay_attr(self):
247 def autoplay_attr(self):
248 if(self.autoplay):
248 if(self.autoplay):
249 return 'autoplay="autoplay"'
249 return 'autoplay="autoplay"'
250 else:
250 else:
251 return ''
251 return ''
252
252
253 def element_id_attr(self):
253 def element_id_attr(self):
254 if (self.element_id):
254 if (self.element_id):
255 return 'id="{element_id}"'.format(element_id=self.element_id)
255 return 'id="{element_id}"'.format(element_id=self.element_id)
256 else:
256 else:
257 return ''
257 return ''
258
258
259 class IFrame(object):
259 class IFrame(object):
260 """
260 """
261 Generic class to embed an iframe in an IPython notebook
261 Generic class to embed an iframe in an IPython notebook
262 """
262 """
263
263
264 iframe = """
264 iframe = """
265 <iframe
265 <iframe
266 width="{width}"
266 width="{width}"
267 height="{height}"
267 height="{height}"
268 src="{src}{params}"
268 src="{src}{params}"
269 frameborder="0"
269 frameborder="0"
270 allowfullscreen
270 allowfullscreen
271 {extras}
271 {extras}
272 ></iframe>
272 ></iframe>
273 """
273 """
274
274
275 def __init__(self, src, width, height, extras: Iterable[str] = None, **kwargs):
275 def __init__(self, src, width, height, extras: Iterable[str] = None, **kwargs):
276 if extras is None:
276 if extras is None:
277 extras = []
277 extras = []
278
278
279 self.src = src
279 self.src = src
280 self.width = width
280 self.width = width
281 self.height = height
281 self.height = height
282 self.extras = extras
282 self.extras = extras
283 self.params = kwargs
283 self.params = kwargs
284
284
285 def _repr_html_(self):
285 def _repr_html_(self):
286 """return the embed iframe"""
286 """return the embed iframe"""
287 if self.params:
287 if self.params:
288 from urllib.parse import urlencode
288 from urllib.parse import urlencode
289 params = "?" + urlencode(self.params)
289 params = "?" + urlencode(self.params)
290 else:
290 else:
291 params = ""
291 params = ""
292 return self.iframe.format(
292 return self.iframe.format(
293 src=self.src,
293 src=self.src,
294 width=self.width,
294 width=self.width,
295 height=self.height,
295 height=self.height,
296 params=params,
296 params=params,
297 extras=" ".join(self.extras),
297 extras=" ".join(self.extras),
298 )
298 )
299
299
300
300
301 class YouTubeVideo(IFrame):
301 class YouTubeVideo(IFrame):
302 """Class for embedding a YouTube Video in an IPython session, based on its video id.
302 """Class for embedding a YouTube Video in an IPython session, based on its video id.
303
303
304 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
304 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
305 do::
305 do::
306
306
307 vid = YouTubeVideo("foo")
307 vid = YouTubeVideo("foo")
308 display(vid)
308 display(vid)
309
309
310 To start from 30 seconds::
310 To start from 30 seconds::
311
311
312 vid = YouTubeVideo("abc", start=30)
312 vid = YouTubeVideo("abc", start=30)
313 display(vid)
313 display(vid)
314
314
315 To calculate seconds from time as hours, minutes, seconds use
315 To calculate seconds from time as hours, minutes, seconds use
316 :class:`datetime.timedelta`::
316 :class:`datetime.timedelta`::
317
317
318 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
318 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
319
319
320 Other parameters can be provided as documented at
320 Other parameters can be provided as documented at
321 https://developers.google.com/youtube/player_parameters#Parameters
321 https://developers.google.com/youtube/player_parameters#Parameters
322
322
323 When converting the notebook using nbconvert, a jpeg representation of the video
323 When converting the notebook using nbconvert, a jpeg representation of the video
324 will be inserted in the document.
324 will be inserted in the document.
325 """
325 """
326
326
327 def __init__(self, id, width=400, height=300, allow_autoplay=False, **kwargs):
327 def __init__(self, id, width=400, height=300, allow_autoplay=False, **kwargs):
328 self.id=id
328 self.id=id
329 src = "https://www.youtube.com/embed/{0}".format(id)
329 src = "https://www.youtube.com/embed/{0}".format(id)
330 if allow_autoplay:
330 if allow_autoplay:
331 extras = list(kwargs.get("extras", [])) + ['allow="autoplay"']
331 extras = list(kwargs.get("extras", [])) + ['allow="autoplay"']
332 kwargs.update(autoplay=1, extras=extras)
332 kwargs.update(autoplay=1, extras=extras)
333 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
333 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
334
334
335 def _repr_jpeg_(self):
335 def _repr_jpeg_(self):
336 # Deferred import
336 # Deferred import
337 from urllib.request import urlopen
337 from urllib.request import urlopen
338
338
339 try:
339 try:
340 return urlopen("https://img.youtube.com/vi/{id}/hqdefault.jpg".format(id=self.id)).read()
340 return urlopen("https://img.youtube.com/vi/{id}/hqdefault.jpg".format(id=self.id)).read()
341 except IOError:
341 except IOError:
342 return None
342 return None
343
343
344 class VimeoVideo(IFrame):
344 class VimeoVideo(IFrame):
345 """
345 """
346 Class for embedding a Vimeo video in an IPython session, based on its video id.
346 Class for embedding a Vimeo video in an IPython session, based on its video id.
347 """
347 """
348
348
349 def __init__(self, id, width=400, height=300, **kwargs):
349 def __init__(self, id, width=400, height=300, **kwargs):
350 src="https://player.vimeo.com/video/{0}".format(id)
350 src="https://player.vimeo.com/video/{0}".format(id)
351 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
351 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
352
352
353 class ScribdDocument(IFrame):
353 class ScribdDocument(IFrame):
354 """
354 """
355 Class for embedding a Scribd document in an IPython session
355 Class for embedding a Scribd document in an IPython session
356
356
357 Use the start_page params to specify a starting point in the document
357 Use the start_page params to specify a starting point in the document
358 Use the view_mode params to specify display type one off scroll | slideshow | book
358 Use the view_mode params to specify display type one off scroll | slideshow | book
359
359
360 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
360 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
361
361
362 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
362 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
363 """
363 """
364
364
365 def __init__(self, id, width=400, height=300, **kwargs):
365 def __init__(self, id, width=400, height=300, **kwargs):
366 src="https://www.scribd.com/embeds/{0}/content".format(id)
366 src="https://www.scribd.com/embeds/{0}/content".format(id)
367 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
367 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
368
368
369 class FileLink(object):
369 class FileLink(object):
370 """Class for embedding a local file link in an IPython session, based on path
370 """Class for embedding a local file link in an IPython session, based on path
371
371
372 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
372 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
373
373
374 you would do::
374 you would do::
375
375
376 local_file = FileLink("my/data.txt")
376 local_file = FileLink("my/data.txt")
377 display(local_file)
377 display(local_file)
378
378
379 or in the HTML notebook, just::
379 or in the HTML notebook, just::
380
380
381 FileLink("my/data.txt")
381 FileLink("my/data.txt")
382 """
382 """
383
383
384 html_link_str = "<a href='%s' target='_blank'>%s</a>"
384 html_link_str = "<a href='%s' target='_blank'>%s</a>"
385
385
386 def __init__(self,
386 def __init__(self,
387 path,
387 path,
388 url_prefix='',
388 url_prefix='',
389 result_html_prefix='',
389 result_html_prefix='',
390 result_html_suffix='<br>'):
390 result_html_suffix='<br>'):
391 """
391 """
392 Parameters
392 Parameters
393 ----------
393 ----------
394 path : str
394 path : str
395 path to the file or directory that should be formatted
395 path to the file or directory that should be formatted
396 url_prefix : str
396 url_prefix : str
397 prefix to be prepended to all files to form a working link [default:
397 prefix to be prepended to all files to form a working link [default:
398 '']
398 '']
399 result_html_prefix : str
399 result_html_prefix : str
400 text to append to beginning to link [default: '']
400 text to append to beginning to link [default: '']
401 result_html_suffix : str
401 result_html_suffix : str
402 text to append at the end of link [default: '<br>']
402 text to append at the end of link [default: '<br>']
403 """
403 """
404 if isdir(path):
404 if isdir(path):
405 raise ValueError("Cannot display a directory using FileLink. "
405 raise ValueError("Cannot display a directory using FileLink. "
406 "Use FileLinks to display '%s'." % path)
406 "Use FileLinks to display '%s'." % path)
407 self.path = fsdecode(path)
407 self.path = fsdecode(path)
408 self.url_prefix = url_prefix
408 self.url_prefix = url_prefix
409 self.result_html_prefix = result_html_prefix
409 self.result_html_prefix = result_html_prefix
410 self.result_html_suffix = result_html_suffix
410 self.result_html_suffix = result_html_suffix
411
411
412 def _format_path(self):
412 def _format_path(self):
413 fp = ''.join([self.url_prefix, html_escape(self.path)])
413 fp = ''.join([self.url_prefix, html_escape(self.path)])
414 return ''.join([self.result_html_prefix,
414 return ''.join([self.result_html_prefix,
415 self.html_link_str % \
415 self.html_link_str % \
416 (fp, html_escape(self.path, quote=False)),
416 (fp, html_escape(self.path, quote=False)),
417 self.result_html_suffix])
417 self.result_html_suffix])
418
418
419 def _repr_html_(self):
419 def _repr_html_(self):
420 """return html link to file
420 """return html link to file
421 """
421 """
422 if not exists(self.path):
422 if not exists(self.path):
423 return ("Path (<tt>%s</tt>) doesn't exist. "
423 return ("Path (<tt>%s</tt>) doesn't exist. "
424 "It may still be in the process of "
424 "It may still be in the process of "
425 "being generated, or you may have the "
425 "being generated, or you may have the "
426 "incorrect path." % self.path)
426 "incorrect path." % self.path)
427
427
428 return self._format_path()
428 return self._format_path()
429
429
430 def __repr__(self):
430 def __repr__(self):
431 """return absolute path to file
431 """return absolute path to file
432 """
432 """
433 return abspath(self.path)
433 return abspath(self.path)
434
434
435 class FileLinks(FileLink):
435 class FileLinks(FileLink):
436 """Class for embedding local file links in an IPython session, based on path
436 """Class for embedding local file links in an IPython session, based on path
437
437
438 e.g. to embed links to files that were generated in the IPython notebook
438 e.g. to embed links to files that were generated in the IPython notebook
439 under ``my/data``, you would do::
439 under ``my/data``, you would do::
440
440
441 local_files = FileLinks("my/data")
441 local_files = FileLinks("my/data")
442 display(local_files)
442 display(local_files)
443
443
444 or in the HTML notebook, just::
444 or in the HTML notebook, just::
445
445
446 FileLinks("my/data")
446 FileLinks("my/data")
447 """
447 """
448 def __init__(self,
448 def __init__(self,
449 path,
449 path,
450 url_prefix='',
450 url_prefix='',
451 included_suffixes=None,
451 included_suffixes=None,
452 result_html_prefix='',
452 result_html_prefix='',
453 result_html_suffix='<br>',
453 result_html_suffix='<br>',
454 notebook_display_formatter=None,
454 notebook_display_formatter=None,
455 terminal_display_formatter=None,
455 terminal_display_formatter=None,
456 recursive=True):
456 recursive=True):
457 """
457 """
458 See :class:`FileLink` for the ``path``, ``url_prefix``,
458 See :class:`FileLink` for the ``path``, ``url_prefix``,
459 ``result_html_prefix`` and ``result_html_suffix`` parameters.
459 ``result_html_prefix`` and ``result_html_suffix`` parameters.
460
460
461 included_suffixes : list
461 included_suffixes : list
462 Filename suffixes to include when formatting output [default: include
462 Filename suffixes to include when formatting output [default: include
463 all files]
463 all files]
464
464
465 notebook_display_formatter : function
465 notebook_display_formatter : function
466 Used to format links for display in the notebook. See discussion of
466 Used to format links for display in the notebook. See discussion of
467 formatter functions below.
467 formatter functions below.
468
468
469 terminal_display_formatter : function
469 terminal_display_formatter : function
470 Used to format links for display in the terminal. See discussion of
470 Used to format links for display in the terminal. See discussion of
471 formatter functions below.
471 formatter functions below.
472
472
473 Formatter functions must be of the form::
473 Formatter functions must be of the form::
474
474
475 f(dirname, fnames, included_suffixes)
475 f(dirname, fnames, included_suffixes)
476
476
477 dirname : str
477 dirname : str
478 The name of a directory
478 The name of a directory
479 fnames : list
479 fnames : list
480 The files in that directory
480 The files in that directory
481 included_suffixes : list
481 included_suffixes : list
482 The file suffixes that should be included in the output (passing None
482 The file suffixes that should be included in the output (passing None
483 meansto include all suffixes in the output in the built-in formatters)
483 meansto include all suffixes in the output in the built-in formatters)
484 recursive : boolean
484 recursive : boolean
485 Whether to recurse into subdirectories. Default is True.
485 Whether to recurse into subdirectories. Default is True.
486
486
487 The function should return a list of lines that will be printed in the
487 The function should return a list of lines that will be printed in the
488 notebook (if passing notebook_display_formatter) or the terminal (if
488 notebook (if passing notebook_display_formatter) or the terminal (if
489 passing terminal_display_formatter). This function is iterated over for
489 passing terminal_display_formatter). This function is iterated over for
490 each directory in self.path. Default formatters are in place, can be
490 each directory in self.path. Default formatters are in place, can be
491 passed here to support alternative formatting.
491 passed here to support alternative formatting.
492
492
493 """
493 """
494 if isfile(path):
494 if isfile(path):
495 raise ValueError("Cannot display a file using FileLinks. "
495 raise ValueError("Cannot display a file using FileLinks. "
496 "Use FileLink to display '%s'." % path)
496 "Use FileLink to display '%s'." % path)
497 self.included_suffixes = included_suffixes
497 self.included_suffixes = included_suffixes
498 # remove trailing slashes for more consistent output formatting
498 # remove trailing slashes for more consistent output formatting
499 path = path.rstrip('/')
499 path = path.rstrip('/')
500
500
501 self.path = path
501 self.path = path
502 self.url_prefix = url_prefix
502 self.url_prefix = url_prefix
503 self.result_html_prefix = result_html_prefix
503 self.result_html_prefix = result_html_prefix
504 self.result_html_suffix = result_html_suffix
504 self.result_html_suffix = result_html_suffix
505
505
506 self.notebook_display_formatter = \
506 self.notebook_display_formatter = \
507 notebook_display_formatter or self._get_notebook_display_formatter()
507 notebook_display_formatter or self._get_notebook_display_formatter()
508 self.terminal_display_formatter = \
508 self.terminal_display_formatter = \
509 terminal_display_formatter or self._get_terminal_display_formatter()
509 terminal_display_formatter or self._get_terminal_display_formatter()
510
510
511 self.recursive = recursive
511 self.recursive = recursive
512
512
513 def _get_display_formatter(self,
513 def _get_display_formatter(self,
514 dirname_output_format,
514 dirname_output_format,
515 fname_output_format,
515 fname_output_format,
516 fp_format,
516 fp_format,
517 fp_cleaner=None):
517 fp_cleaner=None):
518 """ generate built-in formatter function
518 """ generate built-in formatter function
519
519
520 this is used to define both the notebook and terminal built-in
520 this is used to define both the notebook and terminal built-in
521 formatters as they only differ by some wrapper text for each entry
521 formatters as they only differ by some wrapper text for each entry
522
522
523 dirname_output_format: string to use for formatting directory
523 dirname_output_format: string to use for formatting directory
524 names, dirname will be substituted for a single "%s" which
524 names, dirname will be substituted for a single "%s" which
525 must appear in this string
525 must appear in this string
526 fname_output_format: string to use for formatting file names,
526 fname_output_format: string to use for formatting file names,
527 if a single "%s" appears in the string, fname will be substituted
527 if a single "%s" appears in the string, fname will be substituted
528 if two "%s" appear in the string, the path to fname will be
528 if two "%s" appear in the string, the path to fname will be
529 substituted for the first and fname will be substituted for the
529 substituted for the first and fname will be substituted for the
530 second
530 second
531 fp_format: string to use for formatting filepaths, must contain
531 fp_format: string to use for formatting filepaths, must contain
532 exactly two "%s" and the dirname will be substituted for the first
532 exactly two "%s" and the dirname will be substituted for the first
533 and fname will be substituted for the second
533 and fname will be substituted for the second
534 """
534 """
535 def f(dirname, fnames, included_suffixes=None):
535 def f(dirname, fnames, included_suffixes=None):
536 result = []
536 result = []
537 # begin by figuring out which filenames, if any,
537 # begin by figuring out which filenames, if any,
538 # are going to be displayed
538 # are going to be displayed
539 display_fnames = []
539 display_fnames = []
540 for fname in fnames:
540 for fname in fnames:
541 if (isfile(join(dirname,fname)) and
541 if (isfile(join(dirname,fname)) and
542 (included_suffixes is None or
542 (included_suffixes is None or
543 splitext(fname)[1] in included_suffixes)):
543 splitext(fname)[1] in included_suffixes)):
544 display_fnames.append(fname)
544 display_fnames.append(fname)
545
545
546 if len(display_fnames) == 0:
546 if len(display_fnames) == 0:
547 # if there are no filenames to display, don't print anything
547 # if there are no filenames to display, don't print anything
548 # (not even the directory name)
548 # (not even the directory name)
549 pass
549 pass
550 else:
550 else:
551 # otherwise print the formatted directory name followed by
551 # otherwise print the formatted directory name followed by
552 # the formatted filenames
552 # the formatted filenames
553 dirname_output_line = dirname_output_format % dirname
553 dirname_output_line = dirname_output_format % dirname
554 result.append(dirname_output_line)
554 result.append(dirname_output_line)
555 for fname in display_fnames:
555 for fname in display_fnames:
556 fp = fp_format % (dirname,fname)
556 fp = fp_format % (dirname,fname)
557 if fp_cleaner is not None:
557 if fp_cleaner is not None:
558 fp = fp_cleaner(fp)
558 fp = fp_cleaner(fp)
559 try:
559 try:
560 # output can include both a filepath and a filename...
560 # output can include both a filepath and a filename...
561 fname_output_line = fname_output_format % (fp, fname)
561 fname_output_line = fname_output_format % (fp, fname)
562 except TypeError:
562 except TypeError:
563 # ... or just a single filepath
563 # ... or just a single filepath
564 fname_output_line = fname_output_format % fname
564 fname_output_line = fname_output_format % fname
565 result.append(fname_output_line)
565 result.append(fname_output_line)
566 return result
566 return result
567 return f
567 return f
568
568
569 def _get_notebook_display_formatter(self,
569 def _get_notebook_display_formatter(self,
570 spacer="&nbsp;&nbsp;"):
570 spacer="&nbsp;&nbsp;"):
571 """ generate function to use for notebook formatting
571 """ generate function to use for notebook formatting
572 """
572 """
573 dirname_output_format = \
573 dirname_output_format = \
574 self.result_html_prefix + "%s/" + self.result_html_suffix
574 self.result_html_prefix + "%s/" + self.result_html_suffix
575 fname_output_format = \
575 fname_output_format = \
576 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
576 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
577 fp_format = self.url_prefix + '%s/%s'
577 fp_format = self.url_prefix + '%s/%s'
578 if sep == "\\":
578 if sep == "\\":
579 # Working on a platform where the path separator is "\", so
579 # Working on a platform where the path separator is "\", so
580 # must convert these to "/" for generating a URI
580 # must convert these to "/" for generating a URI
581 def fp_cleaner(fp):
581 def fp_cleaner(fp):
582 # Replace all occurrences of backslash ("\") with a forward
582 # Replace all occurrences of backslash ("\") with a forward
583 # slash ("/") - this is necessary on windows when a path is
583 # slash ("/") - this is necessary on windows when a path is
584 # provided as input, but we must link to a URI
584 # provided as input, but we must link to a URI
585 return fp.replace('\\','/')
585 return fp.replace('\\','/')
586 else:
586 else:
587 fp_cleaner = None
587 fp_cleaner = None
588
588
589 return self._get_display_formatter(dirname_output_format,
589 return self._get_display_formatter(dirname_output_format,
590 fname_output_format,
590 fname_output_format,
591 fp_format,
591 fp_format,
592 fp_cleaner)
592 fp_cleaner)
593
593
594 def _get_terminal_display_formatter(self,
594 def _get_terminal_display_formatter(self,
595 spacer=" "):
595 spacer=" "):
596 """ generate function to use for terminal formatting
596 """ generate function to use for terminal formatting
597 """
597 """
598 dirname_output_format = "%s/"
598 dirname_output_format = "%s/"
599 fname_output_format = spacer + "%s"
599 fname_output_format = spacer + "%s"
600 fp_format = '%s/%s'
600 fp_format = '%s/%s'
601
601
602 return self._get_display_formatter(dirname_output_format,
602 return self._get_display_formatter(dirname_output_format,
603 fname_output_format,
603 fname_output_format,
604 fp_format)
604 fp_format)
605
605
606 def _format_path(self):
606 def _format_path(self):
607 result_lines = []
607 result_lines = []
608 if self.recursive:
608 if self.recursive:
609 walked_dir = list(walk(self.path))
609 walked_dir = list(walk(self.path))
610 else:
610 else:
611 walked_dir = [next(walk(self.path))]
611 walked_dir = [next(walk(self.path))]
612 walked_dir.sort()
612 walked_dir.sort()
613 for dirname, subdirs, fnames in walked_dir:
613 for dirname, subdirs, fnames in walked_dir:
614 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
614 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
615 return '\n'.join(result_lines)
615 return '\n'.join(result_lines)
616
616
617 def __repr__(self):
617 def __repr__(self):
618 """return newline-separated absolute paths
618 """return newline-separated absolute paths
619 """
619 """
620 result_lines = []
620 result_lines = []
621 if self.recursive:
621 if self.recursive:
622 walked_dir = list(walk(self.path))
622 walked_dir = list(walk(self.path))
623 else:
623 else:
624 walked_dir = [next(walk(self.path))]
624 walked_dir = [next(walk(self.path))]
625 walked_dir.sort()
625 walked_dir.sort()
626 for dirname, subdirs, fnames in walked_dir:
626 for dirname, subdirs, fnames in walked_dir:
627 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
627 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
628 return '\n'.join(result_lines)
628 return '\n'.join(result_lines)
629
629
630
630
631 class Code(TextDisplayObject):
631 class Code(TextDisplayObject):
632 """Display syntax-highlighted source code.
632 """Display syntax-highlighted source code.
633
633
634 This uses Pygments to highlight the code for HTML and Latex output.
634 This uses Pygments to highlight the code for HTML and Latex output.
635
635
636 Parameters
636 Parameters
637 ----------
637 ----------
638 data : str
638 data : str
639 The code as a string
639 The code as a string
640 url : str
640 url : str
641 A URL to fetch the code from
641 A URL to fetch the code from
642 filename : str
642 filename : str
643 A local filename to load the code from
643 A local filename to load the code from
644 language : str
644 language : str
645 The short name of a Pygments lexer to use for highlighting.
645 The short name of a Pygments lexer to use for highlighting.
646 If not specified, it will guess the lexer based on the filename
646 If not specified, it will guess the lexer based on the filename
647 or the code. Available lexers: http://pygments.org/docs/lexers/
647 or the code. Available lexers: http://pygments.org/docs/lexers/
648 """
648 """
649 def __init__(self, data=None, url=None, filename=None, language=None):
649 def __init__(self, data=None, url=None, filename=None, language=None):
650 self.language = language
650 self.language = language
651 super().__init__(data=data, url=url, filename=filename)
651 super().__init__(data=data, url=url, filename=filename)
652
652
653 def _get_lexer(self):
653 def _get_lexer(self):
654 if self.language:
654 if self.language:
655 from pygments.lexers import get_lexer_by_name
655 from pygments.lexers import get_lexer_by_name
656 return get_lexer_by_name(self.language)
656 return get_lexer_by_name(self.language)
657 elif self.filename:
657 elif self.filename:
658 from pygments.lexers import get_lexer_for_filename
658 from pygments.lexers import get_lexer_for_filename
659 return get_lexer_for_filename(self.filename)
659 return get_lexer_for_filename(self.filename)
660 else:
660 else:
661 from pygments.lexers import guess_lexer
661 from pygments.lexers import guess_lexer
662 return guess_lexer(self.data)
662 return guess_lexer(self.data)
663
663
664 def __repr__(self):
664 def __repr__(self):
665 return self.data
665 return self.data
666
666
667 def _repr_html_(self):
667 def _repr_html_(self):
668 from pygments import highlight
668 from pygments import highlight
669 from pygments.formatters import HtmlFormatter
669 from pygments.formatters import HtmlFormatter
670 fmt = HtmlFormatter()
670 fmt = HtmlFormatter()
671 style = '<style>{}</style>'.format(fmt.get_style_defs('.output_html'))
671 style = '<style>{}</style>'.format(fmt.get_style_defs('.output_html'))
672 return style + highlight(self.data, self._get_lexer(), fmt)
672 return style + highlight(self.data, self._get_lexer(), fmt)
673
673
674 def _repr_latex_(self):
674 def _repr_latex_(self):
675 from pygments import highlight
675 from pygments import highlight
676 from pygments.formatters import LatexFormatter
676 from pygments.formatters import LatexFormatter
677 return highlight(self.data, self._get_lexer(), LatexFormatter())
677 return highlight(self.data, self._get_lexer(), LatexFormatter())
@@ -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