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