##// END OF EJS Templates
Fix exception causes all over the codebase
Ram Rachum -
Show More
@@ -1,1206 +1,1206 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Top-level display functions for displaying object in different formats."""
2 """Top-level display functions for displaying object in different formats."""
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
7
8 from binascii import b2a_base64, hexlify
8 from binascii import b2a_base64, hexlify
9 import json
9 import json
10 import mimetypes
10 import mimetypes
11 import os
11 import os
12 import struct
12 import struct
13 import warnings
13 import warnings
14 from copy import deepcopy
14 from copy import deepcopy
15 from os.path import splitext
15 from os.path import splitext
16 from pathlib import Path, PurePath
16 from pathlib import Path, PurePath
17
17
18 from IPython.utils.py3compat import cast_unicode
18 from IPython.utils.py3compat import cast_unicode
19 from IPython.testing.skipdoctest import skip_doctest
19 from IPython.testing.skipdoctest import skip_doctest
20 from . import display_functions
20 from . import display_functions
21
21
22
22
23 __all__ = ['display_pretty', 'display_html', 'display_markdown',
23 __all__ = ['display_pretty', 'display_html', 'display_markdown',
24 'display_svg', 'display_png', 'display_jpeg', 'display_latex', 'display_json',
24 'display_svg', 'display_png', 'display_jpeg', 'display_latex', 'display_json',
25 'display_javascript', 'display_pdf', 'DisplayObject', 'TextDisplayObject',
25 'display_javascript', 'display_pdf', 'DisplayObject', 'TextDisplayObject',
26 'Pretty', 'HTML', 'Markdown', 'Math', 'Latex', 'SVG', 'ProgressBar', 'JSON',
26 'Pretty', 'HTML', 'Markdown', 'Math', 'Latex', 'SVG', 'ProgressBar', 'JSON',
27 'GeoJSON', 'Javascript', 'Image', 'set_matplotlib_formats',
27 'GeoJSON', 'Javascript', 'Image', 'set_matplotlib_formats',
28 'set_matplotlib_close',
28 'set_matplotlib_close',
29 'Video']
29 'Video']
30
30
31 _deprecated_names = ["display", "clear_output", "publish_display_data", "update_display", "DisplayHandle"]
31 _deprecated_names = ["display", "clear_output", "publish_display_data", "update_display", "DisplayHandle"]
32
32
33 __all__ = __all__ + _deprecated_names
33 __all__ = __all__ + _deprecated_names
34
34
35
35
36 # ----- warn to import from IPython.display -----
36 # ----- warn to import from IPython.display -----
37
37
38 from warnings import warn
38 from warnings import warn
39
39
40
40
41 def __getattr__(name):
41 def __getattr__(name):
42 if name in _deprecated_names:
42 if name in _deprecated_names:
43 warn(f"Importing {name} from IPython.core.display is deprecated since IPython 7.14, please import from IPython display", DeprecationWarning, stacklevel=2)
43 warn(f"Importing {name} from IPython.core.display is deprecated since IPython 7.14, please import from IPython display", DeprecationWarning, stacklevel=2)
44 return getattr(display_functions, name)
44 return getattr(display_functions, name)
45
45
46 if name in globals().keys():
46 if name in globals().keys():
47 return globals()[name]
47 return globals()[name]
48 else:
48 else:
49 raise AttributeError(f"module {__name__} has no attribute {name}")
49 raise AttributeError(f"module {__name__} has no attribute {name}")
50
50
51
51
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53 # utility functions
53 # utility functions
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55
55
56 def _safe_exists(path):
56 def _safe_exists(path):
57 """Check path, but don't let exceptions raise"""
57 """Check path, but don't let exceptions raise"""
58 try:
58 try:
59 return os.path.exists(path)
59 return os.path.exists(path)
60 except Exception:
60 except Exception:
61 return False
61 return False
62
62
63
63
64 def _display_mimetype(mimetype, objs, raw=False, metadata=None):
64 def _display_mimetype(mimetype, objs, raw=False, metadata=None):
65 """internal implementation of all display_foo methods
65 """internal implementation of all display_foo methods
66
66
67 Parameters
67 Parameters
68 ----------
68 ----------
69 mimetype : str
69 mimetype : str
70 The mimetype to be published (e.g. 'image/png')
70 The mimetype to be published (e.g. 'image/png')
71 *objs : object
71 *objs : object
72 The Python objects to display, or if raw=True raw text data to
72 The Python objects to display, or if raw=True raw text data to
73 display.
73 display.
74 raw : bool
74 raw : bool
75 Are the data objects raw data or Python objects that need to be
75 Are the data objects raw data or Python objects that need to be
76 formatted before display? [default: False]
76 formatted before display? [default: False]
77 metadata : dict (optional)
77 metadata : dict (optional)
78 Metadata to be associated with the specific mimetype output.
78 Metadata to be associated with the specific mimetype output.
79 """
79 """
80 if metadata:
80 if metadata:
81 metadata = {mimetype: metadata}
81 metadata = {mimetype: metadata}
82 if raw:
82 if raw:
83 # turn list of pngdata into list of { 'image/png': pngdata }
83 # turn list of pngdata into list of { 'image/png': pngdata }
84 objs = [ {mimetype: obj} for obj in objs ]
84 objs = [ {mimetype: obj} for obj in objs ]
85 display(*objs, raw=raw, metadata=metadata, include=[mimetype])
85 display(*objs, raw=raw, metadata=metadata, include=[mimetype])
86
86
87 #-----------------------------------------------------------------------------
87 #-----------------------------------------------------------------------------
88 # Main functions
88 # Main functions
89 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
90
90
91
91
92 def display_pretty(*objs, **kwargs):
92 def display_pretty(*objs, **kwargs):
93 """Display the pretty (default) representation of an object.
93 """Display the pretty (default) representation of an object.
94
94
95 Parameters
95 Parameters
96 ----------
96 ----------
97 *objs : object
97 *objs : object
98 The Python objects to display, or if raw=True raw text data to
98 The Python objects to display, or if raw=True raw text data to
99 display.
99 display.
100 raw : bool
100 raw : bool
101 Are the data objects raw data or Python objects that need to be
101 Are the data objects raw data or Python objects that need to be
102 formatted before display? [default: False]
102 formatted before display? [default: False]
103 metadata : dict (optional)
103 metadata : dict (optional)
104 Metadata to be associated with the specific mimetype output.
104 Metadata to be associated with the specific mimetype output.
105 """
105 """
106 _display_mimetype('text/plain', objs, **kwargs)
106 _display_mimetype('text/plain', objs, **kwargs)
107
107
108
108
109 def display_html(*objs, **kwargs):
109 def display_html(*objs, **kwargs):
110 """Display the HTML representation of an object.
110 """Display the HTML representation of an object.
111
111
112 Note: If raw=False and the object does not have a HTML
112 Note: If raw=False and the object does not have a HTML
113 representation, no HTML will be shown.
113 representation, no HTML will be shown.
114
114
115 Parameters
115 Parameters
116 ----------
116 ----------
117 *objs : object
117 *objs : object
118 The Python objects to display, or if raw=True raw HTML data to
118 The Python objects to display, or if raw=True raw HTML data to
119 display.
119 display.
120 raw : bool
120 raw : bool
121 Are the data objects raw data or Python objects that need to be
121 Are the data objects raw data or Python objects that need to be
122 formatted before display? [default: False]
122 formatted before display? [default: False]
123 metadata : dict (optional)
123 metadata : dict (optional)
124 Metadata to be associated with the specific mimetype output.
124 Metadata to be associated with the specific mimetype output.
125 """
125 """
126 _display_mimetype('text/html', objs, **kwargs)
126 _display_mimetype('text/html', objs, **kwargs)
127
127
128
128
129 def display_markdown(*objs, **kwargs):
129 def display_markdown(*objs, **kwargs):
130 """Displays the Markdown representation of an object.
130 """Displays the Markdown representation of an object.
131
131
132 Parameters
132 Parameters
133 ----------
133 ----------
134 *objs : object
134 *objs : object
135 The Python objects to display, or if raw=True raw markdown data to
135 The Python objects to display, or if raw=True raw markdown data to
136 display.
136 display.
137 raw : bool
137 raw : bool
138 Are the data objects raw data or Python objects that need to be
138 Are the data objects raw data or Python objects that need to be
139 formatted before display? [default: False]
139 formatted before display? [default: False]
140 metadata : dict (optional)
140 metadata : dict (optional)
141 Metadata to be associated with the specific mimetype output.
141 Metadata to be associated with the specific mimetype output.
142 """
142 """
143
143
144 _display_mimetype('text/markdown', objs, **kwargs)
144 _display_mimetype('text/markdown', objs, **kwargs)
145
145
146
146
147 def display_svg(*objs, **kwargs):
147 def display_svg(*objs, **kwargs):
148 """Display the SVG representation of an object.
148 """Display the SVG representation of an object.
149
149
150 Parameters
150 Parameters
151 ----------
151 ----------
152 *objs : object
152 *objs : object
153 The Python objects to display, or if raw=True raw svg data to
153 The Python objects to display, or if raw=True raw svg data to
154 display.
154 display.
155 raw : bool
155 raw : bool
156 Are the data objects raw data or Python objects that need to be
156 Are the data objects raw data or Python objects that need to be
157 formatted before display? [default: False]
157 formatted before display? [default: False]
158 metadata : dict (optional)
158 metadata : dict (optional)
159 Metadata to be associated with the specific mimetype output.
159 Metadata to be associated with the specific mimetype output.
160 """
160 """
161 _display_mimetype('image/svg+xml', objs, **kwargs)
161 _display_mimetype('image/svg+xml', objs, **kwargs)
162
162
163
163
164 def display_png(*objs, **kwargs):
164 def display_png(*objs, **kwargs):
165 """Display the PNG representation of an object.
165 """Display the PNG representation of an object.
166
166
167 Parameters
167 Parameters
168 ----------
168 ----------
169 *objs : object
169 *objs : object
170 The Python objects to display, or if raw=True raw png data to
170 The Python objects to display, or if raw=True raw png data to
171 display.
171 display.
172 raw : bool
172 raw : bool
173 Are the data objects raw data or Python objects that need to be
173 Are the data objects raw data or Python objects that need to be
174 formatted before display? [default: False]
174 formatted before display? [default: False]
175 metadata : dict (optional)
175 metadata : dict (optional)
176 Metadata to be associated with the specific mimetype output.
176 Metadata to be associated with the specific mimetype output.
177 """
177 """
178 _display_mimetype('image/png', objs, **kwargs)
178 _display_mimetype('image/png', objs, **kwargs)
179
179
180
180
181 def display_jpeg(*objs, **kwargs):
181 def display_jpeg(*objs, **kwargs):
182 """Display the JPEG representation of an object.
182 """Display the JPEG representation of an object.
183
183
184 Parameters
184 Parameters
185 ----------
185 ----------
186 *objs : object
186 *objs : object
187 The Python objects to display, or if raw=True raw JPEG data to
187 The Python objects to display, or if raw=True raw JPEG data to
188 display.
188 display.
189 raw : bool
189 raw : bool
190 Are the data objects raw data or Python objects that need to be
190 Are the data objects raw data or Python objects that need to be
191 formatted before display? [default: False]
191 formatted before display? [default: False]
192 metadata : dict (optional)
192 metadata : dict (optional)
193 Metadata to be associated with the specific mimetype output.
193 Metadata to be associated with the specific mimetype output.
194 """
194 """
195 _display_mimetype('image/jpeg', objs, **kwargs)
195 _display_mimetype('image/jpeg', objs, **kwargs)
196
196
197
197
198 def display_latex(*objs, **kwargs):
198 def display_latex(*objs, **kwargs):
199 """Display the LaTeX representation of an object.
199 """Display the LaTeX representation of an object.
200
200
201 Parameters
201 Parameters
202 ----------
202 ----------
203 *objs : object
203 *objs : object
204 The Python objects to display, or if raw=True raw latex data to
204 The Python objects to display, or if raw=True raw latex data to
205 display.
205 display.
206 raw : bool
206 raw : bool
207 Are the data objects raw data or Python objects that need to be
207 Are the data objects raw data or Python objects that need to be
208 formatted before display? [default: False]
208 formatted before display? [default: False]
209 metadata : dict (optional)
209 metadata : dict (optional)
210 Metadata to be associated with the specific mimetype output.
210 Metadata to be associated with the specific mimetype output.
211 """
211 """
212 _display_mimetype('text/latex', objs, **kwargs)
212 _display_mimetype('text/latex', objs, **kwargs)
213
213
214
214
215 def display_json(*objs, **kwargs):
215 def display_json(*objs, **kwargs):
216 """Display the JSON representation of an object.
216 """Display the JSON representation of an object.
217
217
218 Note that not many frontends support displaying JSON.
218 Note that not many frontends support displaying JSON.
219
219
220 Parameters
220 Parameters
221 ----------
221 ----------
222 *objs : object
222 *objs : object
223 The Python objects to display, or if raw=True raw json data to
223 The Python objects to display, or if raw=True raw json data to
224 display.
224 display.
225 raw : bool
225 raw : bool
226 Are the data objects raw data or Python objects that need to be
226 Are the data objects raw data or Python objects that need to be
227 formatted before display? [default: False]
227 formatted before display? [default: False]
228 metadata : dict (optional)
228 metadata : dict (optional)
229 Metadata to be associated with the specific mimetype output.
229 Metadata to be associated with the specific mimetype output.
230 """
230 """
231 _display_mimetype('application/json', objs, **kwargs)
231 _display_mimetype('application/json', objs, **kwargs)
232
232
233
233
234 def display_javascript(*objs, **kwargs):
234 def display_javascript(*objs, **kwargs):
235 """Display the Javascript representation of an object.
235 """Display the Javascript representation of an object.
236
236
237 Parameters
237 Parameters
238 ----------
238 ----------
239 *objs : object
239 *objs : object
240 The Python objects to display, or if raw=True raw javascript data to
240 The Python objects to display, or if raw=True raw javascript data to
241 display.
241 display.
242 raw : bool
242 raw : bool
243 Are the data objects raw data or Python objects that need to be
243 Are the data objects raw data or Python objects that need to be
244 formatted before display? [default: False]
244 formatted before display? [default: False]
245 metadata : dict (optional)
245 metadata : dict (optional)
246 Metadata to be associated with the specific mimetype output.
246 Metadata to be associated with the specific mimetype output.
247 """
247 """
248 _display_mimetype('application/javascript', objs, **kwargs)
248 _display_mimetype('application/javascript', objs, **kwargs)
249
249
250
250
251 def display_pdf(*objs, **kwargs):
251 def display_pdf(*objs, **kwargs):
252 """Display the PDF representation of an object.
252 """Display the PDF representation of an object.
253
253
254 Parameters
254 Parameters
255 ----------
255 ----------
256 *objs : object
256 *objs : object
257 The Python objects to display, or if raw=True raw javascript data to
257 The Python objects to display, or if raw=True raw javascript data to
258 display.
258 display.
259 raw : bool
259 raw : bool
260 Are the data objects raw data or Python objects that need to be
260 Are the data objects raw data or Python objects that need to be
261 formatted before display? [default: False]
261 formatted before display? [default: False]
262 metadata : dict (optional)
262 metadata : dict (optional)
263 Metadata to be associated with the specific mimetype output.
263 Metadata to be associated with the specific mimetype output.
264 """
264 """
265 _display_mimetype('application/pdf', objs, **kwargs)
265 _display_mimetype('application/pdf', objs, **kwargs)
266
266
267
267
268 #-----------------------------------------------------------------------------
268 #-----------------------------------------------------------------------------
269 # Smart classes
269 # Smart classes
270 #-----------------------------------------------------------------------------
270 #-----------------------------------------------------------------------------
271
271
272
272
273 class DisplayObject(object):
273 class DisplayObject(object):
274 """An object that wraps data to be displayed."""
274 """An object that wraps data to be displayed."""
275
275
276 _read_flags = 'r'
276 _read_flags = 'r'
277 _show_mem_addr = False
277 _show_mem_addr = False
278 metadata = None
278 metadata = None
279
279
280 def __init__(self, data=None, url=None, filename=None, metadata=None):
280 def __init__(self, data=None, url=None, filename=None, metadata=None):
281 """Create a display object given raw data.
281 """Create a display object given raw data.
282
282
283 When this object is returned by an expression or passed to the
283 When this object is returned by an expression or passed to the
284 display function, it will result in the data being displayed
284 display function, it will result in the data being displayed
285 in the frontend. The MIME type of the data should match the
285 in the frontend. The MIME type of the data should match the
286 subclasses used, so the Png subclass should be used for 'image/png'
286 subclasses used, so the Png subclass should be used for 'image/png'
287 data. If the data is a URL, the data will first be downloaded
287 data. If the data is a URL, the data will first be downloaded
288 and then displayed. If
288 and then displayed. If
289
289
290 Parameters
290 Parameters
291 ----------
291 ----------
292 data : unicode, str or bytes
292 data : unicode, str or bytes
293 The raw data or a URL or file to load the data from
293 The raw data or a URL or file to load the data from
294 url : unicode
294 url : unicode
295 A URL to download the data from.
295 A URL to download the data from.
296 filename : unicode
296 filename : unicode
297 Path to a local file to load the data from.
297 Path to a local file to load the data from.
298 metadata : dict
298 metadata : dict
299 Dict of metadata associated to be the object when displayed
299 Dict of metadata associated to be the object when displayed
300 """
300 """
301 if isinstance(data, (Path, PurePath)):
301 if isinstance(data, (Path, PurePath)):
302 data = str(data)
302 data = str(data)
303
303
304 if data is not None and isinstance(data, str):
304 if data is not None and isinstance(data, str):
305 if data.startswith('http') and url is None:
305 if data.startswith('http') and url is None:
306 url = data
306 url = data
307 filename = None
307 filename = None
308 data = None
308 data = None
309 elif _safe_exists(data) and filename is None:
309 elif _safe_exists(data) and filename is None:
310 url = None
310 url = None
311 filename = data
311 filename = data
312 data = None
312 data = None
313
313
314 self.url = url
314 self.url = url
315 self.filename = filename
315 self.filename = filename
316 # because of @data.setter methods in
316 # because of @data.setter methods in
317 # subclasses ensure url and filename are set
317 # subclasses ensure url and filename are set
318 # before assigning to self.data
318 # before assigning to self.data
319 self.data = data
319 self.data = data
320
320
321 if metadata is not None:
321 if metadata is not None:
322 self.metadata = metadata
322 self.metadata = metadata
323 elif self.metadata is None:
323 elif self.metadata is None:
324 self.metadata = {}
324 self.metadata = {}
325
325
326 self.reload()
326 self.reload()
327 self._check_data()
327 self._check_data()
328
328
329 def __repr__(self):
329 def __repr__(self):
330 if not self._show_mem_addr:
330 if not self._show_mem_addr:
331 cls = self.__class__
331 cls = self.__class__
332 r = "<%s.%s object>" % (cls.__module__, cls.__name__)
332 r = "<%s.%s object>" % (cls.__module__, cls.__name__)
333 else:
333 else:
334 r = super(DisplayObject, self).__repr__()
334 r = super(DisplayObject, self).__repr__()
335 return r
335 return r
336
336
337 def _check_data(self):
337 def _check_data(self):
338 """Override in subclasses if there's something to check."""
338 """Override in subclasses if there's something to check."""
339 pass
339 pass
340
340
341 def _data_and_metadata(self):
341 def _data_and_metadata(self):
342 """shortcut for returning metadata with shape information, if defined"""
342 """shortcut for returning metadata with shape information, if defined"""
343 if self.metadata:
343 if self.metadata:
344 return self.data, deepcopy(self.metadata)
344 return self.data, deepcopy(self.metadata)
345 else:
345 else:
346 return self.data
346 return self.data
347
347
348 def reload(self):
348 def reload(self):
349 """Reload the raw data from file or URL."""
349 """Reload the raw data from file or URL."""
350 if self.filename is not None:
350 if self.filename is not None:
351 with open(self.filename, self._read_flags) as f:
351 with open(self.filename, self._read_flags) as f:
352 self.data = f.read()
352 self.data = f.read()
353 elif self.url is not None:
353 elif self.url is not None:
354 # Deferred import
354 # Deferred import
355 from urllib.request import urlopen
355 from urllib.request import urlopen
356 response = urlopen(self.url)
356 response = urlopen(self.url)
357 data = response.read()
357 data = response.read()
358 # extract encoding from header, if there is one:
358 # extract encoding from header, if there is one:
359 encoding = None
359 encoding = None
360 if 'content-type' in response.headers:
360 if 'content-type' in response.headers:
361 for sub in response.headers['content-type'].split(';'):
361 for sub in response.headers['content-type'].split(';'):
362 sub = sub.strip()
362 sub = sub.strip()
363 if sub.startswith('charset'):
363 if sub.startswith('charset'):
364 encoding = sub.split('=')[-1].strip()
364 encoding = sub.split('=')[-1].strip()
365 break
365 break
366 if 'content-encoding' in response.headers:
366 if 'content-encoding' in response.headers:
367 # TODO: do deflate?
367 # TODO: do deflate?
368 if 'gzip' in response.headers['content-encoding']:
368 if 'gzip' in response.headers['content-encoding']:
369 import gzip
369 import gzip
370 from io import BytesIO
370 from io import BytesIO
371 with gzip.open(BytesIO(data), 'rt', encoding=encoding) as fp:
371 with gzip.open(BytesIO(data), 'rt', encoding=encoding) as fp:
372 encoding = None
372 encoding = None
373 data = fp.read()
373 data = fp.read()
374
374
375 # decode data, if an encoding was specified
375 # decode data, if an encoding was specified
376 # We only touch self.data once since
376 # We only touch self.data once since
377 # subclasses such as SVG have @data.setter methods
377 # subclasses such as SVG have @data.setter methods
378 # that transform self.data into ... well svg.
378 # that transform self.data into ... well svg.
379 if encoding:
379 if encoding:
380 self.data = data.decode(encoding, 'replace')
380 self.data = data.decode(encoding, 'replace')
381 else:
381 else:
382 self.data = data
382 self.data = data
383
383
384
384
385 class TextDisplayObject(DisplayObject):
385 class TextDisplayObject(DisplayObject):
386 """Validate that display data is text"""
386 """Validate that display data is text"""
387 def _check_data(self):
387 def _check_data(self):
388 if self.data is not None and not isinstance(self.data, str):
388 if self.data is not None and not isinstance(self.data, str):
389 raise TypeError("%s expects text, not %r" % (self.__class__.__name__, self.data))
389 raise TypeError("%s expects text, not %r" % (self.__class__.__name__, self.data))
390
390
391 class Pretty(TextDisplayObject):
391 class Pretty(TextDisplayObject):
392
392
393 def _repr_pretty_(self, pp, cycle):
393 def _repr_pretty_(self, pp, cycle):
394 return pp.text(self.data)
394 return pp.text(self.data)
395
395
396
396
397 class HTML(TextDisplayObject):
397 class HTML(TextDisplayObject):
398
398
399 def __init__(self, data=None, url=None, filename=None, metadata=None):
399 def __init__(self, data=None, url=None, filename=None, metadata=None):
400 def warn():
400 def warn():
401 if not data:
401 if not data:
402 return False
402 return False
403
403
404 #
404 #
405 # Avoid calling lower() on the entire data, because it could be a
405 # Avoid calling lower() on the entire data, because it could be a
406 # long string and we're only interested in its beginning and end.
406 # long string and we're only interested in its beginning and end.
407 #
407 #
408 prefix = data[:10].lower()
408 prefix = data[:10].lower()
409 suffix = data[-10:].lower()
409 suffix = data[-10:].lower()
410 return prefix.startswith("<iframe ") and suffix.endswith("</iframe>")
410 return prefix.startswith("<iframe ") and suffix.endswith("</iframe>")
411
411
412 if warn():
412 if warn():
413 warnings.warn("Consider using IPython.display.IFrame instead")
413 warnings.warn("Consider using IPython.display.IFrame instead")
414 super(HTML, self).__init__(data=data, url=url, filename=filename, metadata=metadata)
414 super(HTML, self).__init__(data=data, url=url, filename=filename, metadata=metadata)
415
415
416 def _repr_html_(self):
416 def _repr_html_(self):
417 return self._data_and_metadata()
417 return self._data_and_metadata()
418
418
419 def __html__(self):
419 def __html__(self):
420 """
420 """
421 This method exists to inform other HTML-using modules (e.g. Markupsafe,
421 This method exists to inform other HTML-using modules (e.g. Markupsafe,
422 htmltag, etc) that this object is HTML and does not need things like
422 htmltag, etc) that this object is HTML and does not need things like
423 special characters (<>&) escaped.
423 special characters (<>&) escaped.
424 """
424 """
425 return self._repr_html_()
425 return self._repr_html_()
426
426
427
427
428 class Markdown(TextDisplayObject):
428 class Markdown(TextDisplayObject):
429
429
430 def _repr_markdown_(self):
430 def _repr_markdown_(self):
431 return self._data_and_metadata()
431 return self._data_and_metadata()
432
432
433
433
434 class Math(TextDisplayObject):
434 class Math(TextDisplayObject):
435
435
436 def _repr_latex_(self):
436 def _repr_latex_(self):
437 s = r"$\displaystyle %s$" % self.data.strip('$')
437 s = r"$\displaystyle %s$" % self.data.strip('$')
438 if self.metadata:
438 if self.metadata:
439 return s, deepcopy(self.metadata)
439 return s, deepcopy(self.metadata)
440 else:
440 else:
441 return s
441 return s
442
442
443
443
444 class Latex(TextDisplayObject):
444 class Latex(TextDisplayObject):
445
445
446 def _repr_latex_(self):
446 def _repr_latex_(self):
447 return self._data_and_metadata()
447 return self._data_and_metadata()
448
448
449
449
450 class SVG(DisplayObject):
450 class SVG(DisplayObject):
451 """Embed an SVG into the display.
451 """Embed an SVG into the display.
452
452
453 Note if you just want to view a svg image via a URL use `:class:Image` with
453 Note if you just want to view a svg image via a URL use `:class:Image` with
454 a url=URL keyword argument.
454 a url=URL keyword argument.
455 """
455 """
456
456
457 _read_flags = 'rb'
457 _read_flags = 'rb'
458 # wrap data in a property, which extracts the <svg> tag, discarding
458 # wrap data in a property, which extracts the <svg> tag, discarding
459 # document headers
459 # document headers
460 _data = None
460 _data = None
461
461
462 @property
462 @property
463 def data(self):
463 def data(self):
464 return self._data
464 return self._data
465
465
466 @data.setter
466 @data.setter
467 def data(self, svg):
467 def data(self, svg):
468 if svg is None:
468 if svg is None:
469 self._data = None
469 self._data = None
470 return
470 return
471 # parse into dom object
471 # parse into dom object
472 from xml.dom import minidom
472 from xml.dom import minidom
473 x = minidom.parseString(svg)
473 x = minidom.parseString(svg)
474 # get svg tag (should be 1)
474 # get svg tag (should be 1)
475 found_svg = x.getElementsByTagName('svg')
475 found_svg = x.getElementsByTagName('svg')
476 if found_svg:
476 if found_svg:
477 svg = found_svg[0].toxml()
477 svg = found_svg[0].toxml()
478 else:
478 else:
479 # fallback on the input, trust the user
479 # fallback on the input, trust the user
480 # but this is probably an error.
480 # but this is probably an error.
481 pass
481 pass
482 svg = cast_unicode(svg)
482 svg = cast_unicode(svg)
483 self._data = svg
483 self._data = svg
484
484
485 def _repr_svg_(self):
485 def _repr_svg_(self):
486 return self._data_and_metadata()
486 return self._data_and_metadata()
487
487
488 class ProgressBar(DisplayObject):
488 class ProgressBar(DisplayObject):
489 """Progressbar supports displaying a progressbar like element
489 """Progressbar supports displaying a progressbar like element
490 """
490 """
491 def __init__(self, total):
491 def __init__(self, total):
492 """Creates a new progressbar
492 """Creates a new progressbar
493
493
494 Parameters
494 Parameters
495 ----------
495 ----------
496 total : int
496 total : int
497 maximum size of the progressbar
497 maximum size of the progressbar
498 """
498 """
499 self.total = total
499 self.total = total
500 self._progress = 0
500 self._progress = 0
501 self.html_width = '60ex'
501 self.html_width = '60ex'
502 self.text_width = 60
502 self.text_width = 60
503 self._display_id = hexlify(os.urandom(8)).decode('ascii')
503 self._display_id = hexlify(os.urandom(8)).decode('ascii')
504
504
505 def __repr__(self):
505 def __repr__(self):
506 fraction = self.progress / self.total
506 fraction = self.progress / self.total
507 filled = '=' * int(fraction * self.text_width)
507 filled = '=' * int(fraction * self.text_width)
508 rest = ' ' * (self.text_width - len(filled))
508 rest = ' ' * (self.text_width - len(filled))
509 return '[{}{}] {}/{}'.format(
509 return '[{}{}] {}/{}'.format(
510 filled, rest,
510 filled, rest,
511 self.progress, self.total,
511 self.progress, self.total,
512 )
512 )
513
513
514 def _repr_html_(self):
514 def _repr_html_(self):
515 return "<progress style='width:{}' max='{}' value='{}'></progress>".format(
515 return "<progress style='width:{}' max='{}' value='{}'></progress>".format(
516 self.html_width, self.total, self.progress)
516 self.html_width, self.total, self.progress)
517
517
518 def display(self):
518 def display(self):
519 display(self, display_id=self._display_id)
519 display(self, display_id=self._display_id)
520
520
521 def update(self):
521 def update(self):
522 display(self, display_id=self._display_id, update=True)
522 display(self, display_id=self._display_id, update=True)
523
523
524 @property
524 @property
525 def progress(self):
525 def progress(self):
526 return self._progress
526 return self._progress
527
527
528 @progress.setter
528 @progress.setter
529 def progress(self, value):
529 def progress(self, value):
530 self._progress = value
530 self._progress = value
531 self.update()
531 self.update()
532
532
533 def __iter__(self):
533 def __iter__(self):
534 self.display()
534 self.display()
535 self._progress = -1 # First iteration is 0
535 self._progress = -1 # First iteration is 0
536 return self
536 return self
537
537
538 def __next__(self):
538 def __next__(self):
539 """Returns current value and increments display by one."""
539 """Returns current value and increments display by one."""
540 self.progress += 1
540 self.progress += 1
541 if self.progress < self.total:
541 if self.progress < self.total:
542 return self.progress
542 return self.progress
543 else:
543 else:
544 raise StopIteration()
544 raise StopIteration()
545
545
546 class JSON(DisplayObject):
546 class JSON(DisplayObject):
547 """JSON expects a JSON-able dict or list
547 """JSON expects a JSON-able dict or list
548
548
549 not an already-serialized JSON string.
549 not an already-serialized JSON string.
550
550
551 Scalar types (None, number, string) are not allowed, only dict or list containers.
551 Scalar types (None, number, string) are not allowed, only dict or list containers.
552 """
552 """
553 # wrap data in a property, which warns about passing already-serialized JSON
553 # wrap data in a property, which warns about passing already-serialized JSON
554 _data = None
554 _data = None
555 def __init__(self, data=None, url=None, filename=None, expanded=False, metadata=None, root='root', **kwargs):
555 def __init__(self, data=None, url=None, filename=None, expanded=False, metadata=None, root='root', **kwargs):
556 """Create a JSON display object given raw data.
556 """Create a JSON display object given raw data.
557
557
558 Parameters
558 Parameters
559 ----------
559 ----------
560 data : dict or list
560 data : dict or list
561 JSON data to display. Not an already-serialized JSON string.
561 JSON data to display. Not an already-serialized JSON string.
562 Scalar types (None, number, string) are not allowed, only dict
562 Scalar types (None, number, string) are not allowed, only dict
563 or list containers.
563 or list containers.
564 url : unicode
564 url : unicode
565 A URL to download the data from.
565 A URL to download the data from.
566 filename : unicode
566 filename : unicode
567 Path to a local file to load the data from.
567 Path to a local file to load the data from.
568 expanded : boolean
568 expanded : boolean
569 Metadata to control whether a JSON display component is expanded.
569 Metadata to control whether a JSON display component is expanded.
570 metadata: dict
570 metadata: dict
571 Specify extra metadata to attach to the json display object.
571 Specify extra metadata to attach to the json display object.
572 root : str
572 root : str
573 The name of the root element of the JSON tree
573 The name of the root element of the JSON tree
574 """
574 """
575 self.metadata = {
575 self.metadata = {
576 'expanded': expanded,
576 'expanded': expanded,
577 'root': root,
577 'root': root,
578 }
578 }
579 if metadata:
579 if metadata:
580 self.metadata.update(metadata)
580 self.metadata.update(metadata)
581 if kwargs:
581 if kwargs:
582 self.metadata.update(kwargs)
582 self.metadata.update(kwargs)
583 super(JSON, self).__init__(data=data, url=url, filename=filename)
583 super(JSON, self).__init__(data=data, url=url, filename=filename)
584
584
585 def _check_data(self):
585 def _check_data(self):
586 if self.data is not None and not isinstance(self.data, (dict, list)):
586 if self.data is not None and not isinstance(self.data, (dict, list)):
587 raise TypeError("%s expects JSONable dict or list, not %r" % (self.__class__.__name__, self.data))
587 raise TypeError("%s expects JSONable dict or list, not %r" % (self.__class__.__name__, self.data))
588
588
589 @property
589 @property
590 def data(self):
590 def data(self):
591 return self._data
591 return self._data
592
592
593 @data.setter
593 @data.setter
594 def data(self, data):
594 def data(self, data):
595 if isinstance(data, (Path, PurePath)):
595 if isinstance(data, (Path, PurePath)):
596 data = str(data)
596 data = str(data)
597
597
598 if isinstance(data, str):
598 if isinstance(data, str):
599 if self.filename is None and self.url is None:
599 if self.filename is None and self.url is None:
600 warnings.warn("JSON expects JSONable dict or list, not JSON strings")
600 warnings.warn("JSON expects JSONable dict or list, not JSON strings")
601 data = json.loads(data)
601 data = json.loads(data)
602 self._data = data
602 self._data = data
603
603
604 def _data_and_metadata(self):
604 def _data_and_metadata(self):
605 return self.data, self.metadata
605 return self.data, self.metadata
606
606
607 def _repr_json_(self):
607 def _repr_json_(self):
608 return self._data_and_metadata()
608 return self._data_and_metadata()
609
609
610 _css_t = """var link = document.createElement("link");
610 _css_t = """var link = document.createElement("link");
611 link.ref = "stylesheet";
611 link.ref = "stylesheet";
612 link.type = "text/css";
612 link.type = "text/css";
613 link.href = "%s";
613 link.href = "%s";
614 document.head.appendChild(link);
614 document.head.appendChild(link);
615 """
615 """
616
616
617 _lib_t1 = """new Promise(function(resolve, reject) {
617 _lib_t1 = """new Promise(function(resolve, reject) {
618 var script = document.createElement("script");
618 var script = document.createElement("script");
619 script.onload = resolve;
619 script.onload = resolve;
620 script.onerror = reject;
620 script.onerror = reject;
621 script.src = "%s";
621 script.src = "%s";
622 document.head.appendChild(script);
622 document.head.appendChild(script);
623 }).then(() => {
623 }).then(() => {
624 """
624 """
625
625
626 _lib_t2 = """
626 _lib_t2 = """
627 });"""
627 });"""
628
628
629 class GeoJSON(JSON):
629 class GeoJSON(JSON):
630 """GeoJSON expects JSON-able dict
630 """GeoJSON expects JSON-able dict
631
631
632 not an already-serialized JSON string.
632 not an already-serialized JSON string.
633
633
634 Scalar types (None, number, string) are not allowed, only dict containers.
634 Scalar types (None, number, string) are not allowed, only dict containers.
635 """
635 """
636
636
637 def __init__(self, *args, **kwargs):
637 def __init__(self, *args, **kwargs):
638 """Create a GeoJSON display object given raw data.
638 """Create a GeoJSON display object given raw data.
639
639
640 Parameters
640 Parameters
641 ----------
641 ----------
642 data : dict or list
642 data : dict or list
643 VegaLite data. Not an already-serialized JSON string.
643 VegaLite data. Not an already-serialized JSON string.
644 Scalar types (None, number, string) are not allowed, only dict
644 Scalar types (None, number, string) are not allowed, only dict
645 or list containers.
645 or list containers.
646 url_template : string
646 url_template : string
647 Leaflet TileLayer URL template: http://leafletjs.com/reference.html#url-template
647 Leaflet TileLayer URL template: http://leafletjs.com/reference.html#url-template
648 layer_options : dict
648 layer_options : dict
649 Leaflet TileLayer options: http://leafletjs.com/reference.html#tilelayer-options
649 Leaflet TileLayer options: http://leafletjs.com/reference.html#tilelayer-options
650 url : unicode
650 url : unicode
651 A URL to download the data from.
651 A URL to download the data from.
652 filename : unicode
652 filename : unicode
653 Path to a local file to load the data from.
653 Path to a local file to load the data from.
654 metadata: dict
654 metadata: dict
655 Specify extra metadata to attach to the json display object.
655 Specify extra metadata to attach to the json display object.
656
656
657 Examples
657 Examples
658 --------
658 --------
659
659
660 The following will display an interactive map of Mars with a point of
660 The following will display an interactive map of Mars with a point of
661 interest on frontend that do support GeoJSON display.
661 interest on frontend that do support GeoJSON display.
662
662
663 >>> from IPython.display import GeoJSON
663 >>> from IPython.display import GeoJSON
664
664
665 >>> GeoJSON(data={
665 >>> GeoJSON(data={
666 ... "type": "Feature",
666 ... "type": "Feature",
667 ... "geometry": {
667 ... "geometry": {
668 ... "type": "Point",
668 ... "type": "Point",
669 ... "coordinates": [-81.327, 296.038]
669 ... "coordinates": [-81.327, 296.038]
670 ... }
670 ... }
671 ... },
671 ... },
672 ... url_template="http://s3-eu-west-1.amazonaws.com/whereonmars.cartodb.net/{basemap_id}/{z}/{x}/{y}.png",
672 ... url_template="http://s3-eu-west-1.amazonaws.com/whereonmars.cartodb.net/{basemap_id}/{z}/{x}/{y}.png",
673 ... layer_options={
673 ... layer_options={
674 ... "basemap_id": "celestia_mars-shaded-16k_global",
674 ... "basemap_id": "celestia_mars-shaded-16k_global",
675 ... "attribution" : "Celestia/praesepe",
675 ... "attribution" : "Celestia/praesepe",
676 ... "minZoom" : 0,
676 ... "minZoom" : 0,
677 ... "maxZoom" : 18,
677 ... "maxZoom" : 18,
678 ... })
678 ... })
679 <IPython.core.display.GeoJSON object>
679 <IPython.core.display.GeoJSON object>
680
680
681 In the terminal IPython, you will only see the text representation of
681 In the terminal IPython, you will only see the text representation of
682 the GeoJSON object.
682 the GeoJSON object.
683
683
684 """
684 """
685
685
686 super(GeoJSON, self).__init__(*args, **kwargs)
686 super(GeoJSON, self).__init__(*args, **kwargs)
687
687
688
688
689 def _ipython_display_(self):
689 def _ipython_display_(self):
690 bundle = {
690 bundle = {
691 'application/geo+json': self.data,
691 'application/geo+json': self.data,
692 'text/plain': '<IPython.display.GeoJSON object>'
692 'text/plain': '<IPython.display.GeoJSON object>'
693 }
693 }
694 metadata = {
694 metadata = {
695 'application/geo+json': self.metadata
695 'application/geo+json': self.metadata
696 }
696 }
697 display(bundle, metadata=metadata, raw=True)
697 display(bundle, metadata=metadata, raw=True)
698
698
699 class Javascript(TextDisplayObject):
699 class Javascript(TextDisplayObject):
700
700
701 def __init__(self, data=None, url=None, filename=None, lib=None, css=None):
701 def __init__(self, data=None, url=None, filename=None, lib=None, css=None):
702 """Create a Javascript display object given raw data.
702 """Create a Javascript display object given raw data.
703
703
704 When this object is returned by an expression or passed to the
704 When this object is returned by an expression or passed to the
705 display function, it will result in the data being displayed
705 display function, it will result in the data being displayed
706 in the frontend. If the data is a URL, the data will first be
706 in the frontend. If the data is a URL, the data will first be
707 downloaded and then displayed.
707 downloaded and then displayed.
708
708
709 In the Notebook, the containing element will be available as `element`,
709 In the Notebook, the containing element will be available as `element`,
710 and jQuery will be available. Content appended to `element` will be
710 and jQuery will be available. Content appended to `element` will be
711 visible in the output area.
711 visible in the output area.
712
712
713 Parameters
713 Parameters
714 ----------
714 ----------
715 data : unicode, str or bytes
715 data : unicode, str or bytes
716 The Javascript source code or a URL to download it from.
716 The Javascript source code or a URL to download it from.
717 url : unicode
717 url : unicode
718 A URL to download the data from.
718 A URL to download the data from.
719 filename : unicode
719 filename : unicode
720 Path to a local file to load the data from.
720 Path to a local file to load the data from.
721 lib : list or str
721 lib : list or str
722 A sequence of Javascript library URLs to load asynchronously before
722 A sequence of Javascript library URLs to load asynchronously before
723 running the source code. The full URLs of the libraries should
723 running the source code. The full URLs of the libraries should
724 be given. A single Javascript library URL can also be given as a
724 be given. A single Javascript library URL can also be given as a
725 string.
725 string.
726 css: : list or str
726 css: : list or str
727 A sequence of css files to load before running the source code.
727 A sequence of css files to load before running the source code.
728 The full URLs of the css files should be given. A single css URL
728 The full URLs of the css files should be given. A single css URL
729 can also be given as a string.
729 can also be given as a string.
730 """
730 """
731 if isinstance(lib, str):
731 if isinstance(lib, str):
732 lib = [lib]
732 lib = [lib]
733 elif lib is None:
733 elif lib is None:
734 lib = []
734 lib = []
735 if isinstance(css, str):
735 if isinstance(css, str):
736 css = [css]
736 css = [css]
737 elif css is None:
737 elif css is None:
738 css = []
738 css = []
739 if not isinstance(lib, (list,tuple)):
739 if not isinstance(lib, (list,tuple)):
740 raise TypeError('expected sequence, got: %r' % lib)
740 raise TypeError('expected sequence, got: %r' % lib)
741 if not isinstance(css, (list,tuple)):
741 if not isinstance(css, (list,tuple)):
742 raise TypeError('expected sequence, got: %r' % css)
742 raise TypeError('expected sequence, got: %r' % css)
743 self.lib = lib
743 self.lib = lib
744 self.css = css
744 self.css = css
745 super(Javascript, self).__init__(data=data, url=url, filename=filename)
745 super(Javascript, self).__init__(data=data, url=url, filename=filename)
746
746
747 def _repr_javascript_(self):
747 def _repr_javascript_(self):
748 r = ''
748 r = ''
749 for c in self.css:
749 for c in self.css:
750 r += _css_t % c
750 r += _css_t % c
751 for l in self.lib:
751 for l in self.lib:
752 r += _lib_t1 % l
752 r += _lib_t1 % l
753 r += self.data
753 r += self.data
754 r += _lib_t2*len(self.lib)
754 r += _lib_t2*len(self.lib)
755 return r
755 return r
756
756
757 # constants for identifying png/jpeg data
757 # constants for identifying png/jpeg data
758 _PNG = b'\x89PNG\r\n\x1a\n'
758 _PNG = b'\x89PNG\r\n\x1a\n'
759 _JPEG = b'\xff\xd8'
759 _JPEG = b'\xff\xd8'
760
760
761 def _pngxy(data):
761 def _pngxy(data):
762 """read the (width, height) from a PNG header"""
762 """read the (width, height) from a PNG header"""
763 ihdr = data.index(b'IHDR')
763 ihdr = data.index(b'IHDR')
764 # next 8 bytes are width/height
764 # next 8 bytes are width/height
765 return struct.unpack('>ii', data[ihdr+4:ihdr+12])
765 return struct.unpack('>ii', data[ihdr+4:ihdr+12])
766
766
767 def _jpegxy(data):
767 def _jpegxy(data):
768 """read the (width, height) from a JPEG header"""
768 """read the (width, height) from a JPEG header"""
769 # adapted from http://www.64lines.com/jpeg-width-height
769 # adapted from http://www.64lines.com/jpeg-width-height
770
770
771 idx = 4
771 idx = 4
772 while True:
772 while True:
773 block_size = struct.unpack('>H', data[idx:idx+2])[0]
773 block_size = struct.unpack('>H', data[idx:idx+2])[0]
774 idx = idx + block_size
774 idx = idx + block_size
775 if data[idx:idx+2] == b'\xFF\xC0':
775 if data[idx:idx+2] == b'\xFF\xC0':
776 # found Start of Frame
776 # found Start of Frame
777 iSOF = idx
777 iSOF = idx
778 break
778 break
779 else:
779 else:
780 # read another block
780 # read another block
781 idx += 2
781 idx += 2
782
782
783 h, w = struct.unpack('>HH', data[iSOF+5:iSOF+9])
783 h, w = struct.unpack('>HH', data[iSOF+5:iSOF+9])
784 return w, h
784 return w, h
785
785
786 def _gifxy(data):
786 def _gifxy(data):
787 """read the (width, height) from a GIF header"""
787 """read the (width, height) from a GIF header"""
788 return struct.unpack('<HH', data[6:10])
788 return struct.unpack('<HH', data[6:10])
789
789
790
790
791 class Image(DisplayObject):
791 class Image(DisplayObject):
792
792
793 _read_flags = 'rb'
793 _read_flags = 'rb'
794 _FMT_JPEG = u'jpeg'
794 _FMT_JPEG = u'jpeg'
795 _FMT_PNG = u'png'
795 _FMT_PNG = u'png'
796 _FMT_GIF = u'gif'
796 _FMT_GIF = u'gif'
797 _ACCEPTABLE_EMBEDDINGS = [_FMT_JPEG, _FMT_PNG, _FMT_GIF]
797 _ACCEPTABLE_EMBEDDINGS = [_FMT_JPEG, _FMT_PNG, _FMT_GIF]
798 _MIMETYPES = {
798 _MIMETYPES = {
799 _FMT_PNG: 'image/png',
799 _FMT_PNG: 'image/png',
800 _FMT_JPEG: 'image/jpeg',
800 _FMT_JPEG: 'image/jpeg',
801 _FMT_GIF: 'image/gif',
801 _FMT_GIF: 'image/gif',
802 }
802 }
803
803
804 def __init__(self, data=None, url=None, filename=None, format=None,
804 def __init__(self, data=None, url=None, filename=None, format=None,
805 embed=None, width=None, height=None, retina=False,
805 embed=None, width=None, height=None, retina=False,
806 unconfined=False, metadata=None):
806 unconfined=False, metadata=None):
807 """Create a PNG/JPEG/GIF image object given raw data.
807 """Create a PNG/JPEG/GIF image object given raw data.
808
808
809 When this object is returned by an input cell or passed to the
809 When this object is returned by an input cell or passed to the
810 display function, it will result in the image being displayed
810 display function, it will result in the image being displayed
811 in the frontend.
811 in the frontend.
812
812
813 Parameters
813 Parameters
814 ----------
814 ----------
815 data : unicode, str or bytes
815 data : unicode, str or bytes
816 The raw image data or a URL or filename to load the data from.
816 The raw image data or a URL or filename to load the data from.
817 This always results in embedded image data.
817 This always results in embedded image data.
818 url : unicode
818 url : unicode
819 A URL to download the data from. If you specify `url=`,
819 A URL to download the data from. If you specify `url=`,
820 the image data will not be embedded unless you also specify `embed=True`.
820 the image data will not be embedded unless you also specify `embed=True`.
821 filename : unicode
821 filename : unicode
822 Path to a local file to load the data from.
822 Path to a local file to load the data from.
823 Images from a file are always embedded.
823 Images from a file are always embedded.
824 format : unicode
824 format : unicode
825 The format of the image data (png/jpeg/jpg/gif). If a filename or URL is given
825 The format of the image data (png/jpeg/jpg/gif). If a filename or URL is given
826 for format will be inferred from the filename extension.
826 for format will be inferred from the filename extension.
827 embed : bool
827 embed : bool
828 Should the image data be embedded using a data URI (True) or be
828 Should the image data be embedded using a data URI (True) or be
829 loaded using an <img> tag. Set this to True if you want the image
829 loaded using an <img> tag. Set this to True if you want the image
830 to be viewable later with no internet connection in the notebook.
830 to be viewable later with no internet connection in the notebook.
831
831
832 Default is `True`, unless the keyword argument `url` is set, then
832 Default is `True`, unless the keyword argument `url` is set, then
833 default value is `False`.
833 default value is `False`.
834
834
835 Note that QtConsole is not able to display images if `embed` is set to `False`
835 Note that QtConsole is not able to display images if `embed` is set to `False`
836 width : int
836 width : int
837 Width in pixels to which to constrain the image in html
837 Width in pixels to which to constrain the image in html
838 height : int
838 height : int
839 Height in pixels to which to constrain the image in html
839 Height in pixels to which to constrain the image in html
840 retina : bool
840 retina : bool
841 Automatically set the width and height to half of the measured
841 Automatically set the width and height to half of the measured
842 width and height.
842 width and height.
843 This only works for embedded images because it reads the width/height
843 This only works for embedded images because it reads the width/height
844 from image data.
844 from image data.
845 For non-embedded images, you can just set the desired display width
845 For non-embedded images, you can just set the desired display width
846 and height directly.
846 and height directly.
847 unconfined: bool
847 unconfined: bool
848 Set unconfined=True to disable max-width confinement of the image.
848 Set unconfined=True to disable max-width confinement of the image.
849 metadata: dict
849 metadata: dict
850 Specify extra metadata to attach to the image.
850 Specify extra metadata to attach to the image.
851
851
852 Examples
852 Examples
853 --------
853 --------
854 # embedded image data, works in qtconsole and notebook
854 # embedded image data, works in qtconsole and notebook
855 # when passed positionally, the first arg can be any of raw image data,
855 # when passed positionally, the first arg can be any of raw image data,
856 # a URL, or a filename from which to load image data.
856 # a URL, or a filename from which to load image data.
857 # The result is always embedding image data for inline images.
857 # The result is always embedding image data for inline images.
858 Image('http://www.google.fr/images/srpr/logo3w.png')
858 Image('http://www.google.fr/images/srpr/logo3w.png')
859 Image('/path/to/image.jpg')
859 Image('/path/to/image.jpg')
860 Image(b'RAW_PNG_DATA...')
860 Image(b'RAW_PNG_DATA...')
861
861
862 # Specifying Image(url=...) does not embed the image data,
862 # Specifying Image(url=...) does not embed the image data,
863 # it only generates `<img>` tag with a link to the source.
863 # it only generates `<img>` tag with a link to the source.
864 # This will not work in the qtconsole or offline.
864 # This will not work in the qtconsole or offline.
865 Image(url='http://www.google.fr/images/srpr/logo3w.png')
865 Image(url='http://www.google.fr/images/srpr/logo3w.png')
866
866
867 """
867 """
868 if isinstance(data, (Path, PurePath)):
868 if isinstance(data, (Path, PurePath)):
869 data = str(data)
869 data = str(data)
870
870
871 if filename is not None:
871 if filename is not None:
872 ext = self._find_ext(filename)
872 ext = self._find_ext(filename)
873 elif url is not None:
873 elif url is not None:
874 ext = self._find_ext(url)
874 ext = self._find_ext(url)
875 elif data is None:
875 elif data is None:
876 raise ValueError("No image data found. Expecting filename, url, or data.")
876 raise ValueError("No image data found. Expecting filename, url, or data.")
877 elif isinstance(data, str) and (
877 elif isinstance(data, str) and (
878 data.startswith('http') or _safe_exists(data)
878 data.startswith('http') or _safe_exists(data)
879 ):
879 ):
880 ext = self._find_ext(data)
880 ext = self._find_ext(data)
881 else:
881 else:
882 ext = None
882 ext = None
883
883
884 if format is None:
884 if format is None:
885 if ext is not None:
885 if ext is not None:
886 if ext == u'jpg' or ext == u'jpeg':
886 if ext == u'jpg' or ext == u'jpeg':
887 format = self._FMT_JPEG
887 format = self._FMT_JPEG
888 elif ext == u'png':
888 elif ext == u'png':
889 format = self._FMT_PNG
889 format = self._FMT_PNG
890 elif ext == u'gif':
890 elif ext == u'gif':
891 format = self._FMT_GIF
891 format = self._FMT_GIF
892 else:
892 else:
893 format = ext.lower()
893 format = ext.lower()
894 elif isinstance(data, bytes):
894 elif isinstance(data, bytes):
895 # infer image type from image data header,
895 # infer image type from image data header,
896 # only if format has not been specified.
896 # only if format has not been specified.
897 if data[:2] == _JPEG:
897 if data[:2] == _JPEG:
898 format = self._FMT_JPEG
898 format = self._FMT_JPEG
899
899
900 # failed to detect format, default png
900 # failed to detect format, default png
901 if format is None:
901 if format is None:
902 format = self._FMT_PNG
902 format = self._FMT_PNG
903
903
904 if format.lower() == 'jpg':
904 if format.lower() == 'jpg':
905 # jpg->jpeg
905 # jpg->jpeg
906 format = self._FMT_JPEG
906 format = self._FMT_JPEG
907
907
908 self.format = format.lower()
908 self.format = format.lower()
909 self.embed = embed if embed is not None else (url is None)
909 self.embed = embed if embed is not None else (url is None)
910
910
911 if self.embed and self.format not in self._ACCEPTABLE_EMBEDDINGS:
911 if self.embed and self.format not in self._ACCEPTABLE_EMBEDDINGS:
912 raise ValueError("Cannot embed the '%s' image format" % (self.format))
912 raise ValueError("Cannot embed the '%s' image format" % (self.format))
913 if self.embed:
913 if self.embed:
914 self._mimetype = self._MIMETYPES.get(self.format)
914 self._mimetype = self._MIMETYPES.get(self.format)
915
915
916 self.width = width
916 self.width = width
917 self.height = height
917 self.height = height
918 self.retina = retina
918 self.retina = retina
919 self.unconfined = unconfined
919 self.unconfined = unconfined
920 super(Image, self).__init__(data=data, url=url, filename=filename,
920 super(Image, self).__init__(data=data, url=url, filename=filename,
921 metadata=metadata)
921 metadata=metadata)
922
922
923 if self.width is None and self.metadata.get('width', {}):
923 if self.width is None and self.metadata.get('width', {}):
924 self.width = metadata['width']
924 self.width = metadata['width']
925
925
926 if self.height is None and self.metadata.get('height', {}):
926 if self.height is None and self.metadata.get('height', {}):
927 self.height = metadata['height']
927 self.height = metadata['height']
928
928
929 if retina:
929 if retina:
930 self._retina_shape()
930 self._retina_shape()
931
931
932
932
933 def _retina_shape(self):
933 def _retina_shape(self):
934 """load pixel-doubled width and height from image data"""
934 """load pixel-doubled width and height from image data"""
935 if not self.embed:
935 if not self.embed:
936 return
936 return
937 if self.format == self._FMT_PNG:
937 if self.format == self._FMT_PNG:
938 w, h = _pngxy(self.data)
938 w, h = _pngxy(self.data)
939 elif self.format == self._FMT_JPEG:
939 elif self.format == self._FMT_JPEG:
940 w, h = _jpegxy(self.data)
940 w, h = _jpegxy(self.data)
941 elif self.format == self._FMT_GIF:
941 elif self.format == self._FMT_GIF:
942 w, h = _gifxy(self.data)
942 w, h = _gifxy(self.data)
943 else:
943 else:
944 # retina only supports png
944 # retina only supports png
945 return
945 return
946 self.width = w // 2
946 self.width = w // 2
947 self.height = h // 2
947 self.height = h // 2
948
948
949 def reload(self):
949 def reload(self):
950 """Reload the raw data from file or URL."""
950 """Reload the raw data from file or URL."""
951 if self.embed:
951 if self.embed:
952 super(Image,self).reload()
952 super(Image,self).reload()
953 if self.retina:
953 if self.retina:
954 self._retina_shape()
954 self._retina_shape()
955
955
956 def _repr_html_(self):
956 def _repr_html_(self):
957 if not self.embed:
957 if not self.embed:
958 width = height = klass = ''
958 width = height = klass = ''
959 if self.width:
959 if self.width:
960 width = ' width="%d"' % self.width
960 width = ' width="%d"' % self.width
961 if self.height:
961 if self.height:
962 height = ' height="%d"' % self.height
962 height = ' height="%d"' % self.height
963 if self.unconfined:
963 if self.unconfined:
964 klass = ' class="unconfined"'
964 klass = ' class="unconfined"'
965 return u'<img src="{url}"{width}{height}{klass}/>'.format(
965 return u'<img src="{url}"{width}{height}{klass}/>'.format(
966 url=self.url,
966 url=self.url,
967 width=width,
967 width=width,
968 height=height,
968 height=height,
969 klass=klass,
969 klass=klass,
970 )
970 )
971
971
972 def _repr_mimebundle_(self, include=None, exclude=None):
972 def _repr_mimebundle_(self, include=None, exclude=None):
973 """Return the image as a mimebundle
973 """Return the image as a mimebundle
974
974
975 Any new mimetype support should be implemented here.
975 Any new mimetype support should be implemented here.
976 """
976 """
977 if self.embed:
977 if self.embed:
978 mimetype = self._mimetype
978 mimetype = self._mimetype
979 data, metadata = self._data_and_metadata(always_both=True)
979 data, metadata = self._data_and_metadata(always_both=True)
980 if metadata:
980 if metadata:
981 metadata = {mimetype: metadata}
981 metadata = {mimetype: metadata}
982 return {mimetype: data}, metadata
982 return {mimetype: data}, metadata
983 else:
983 else:
984 return {'text/html': self._repr_html_()}
984 return {'text/html': self._repr_html_()}
985
985
986 def _data_and_metadata(self, always_both=False):
986 def _data_and_metadata(self, always_both=False):
987 """shortcut for returning metadata with shape information, if defined"""
987 """shortcut for returning metadata with shape information, if defined"""
988 try:
988 try:
989 b64_data = b2a_base64(self.data).decode('ascii')
989 b64_data = b2a_base64(self.data).decode('ascii')
990 except TypeError:
990 except TypeError as e:
991 raise FileNotFoundError(
991 raise FileNotFoundError(
992 "No such file or directory: '%s'" % (self.data))
992 "No such file or directory: '%s'" % (self.data)) from e
993 md = {}
993 md = {}
994 if self.metadata:
994 if self.metadata:
995 md.update(self.metadata)
995 md.update(self.metadata)
996 if self.width:
996 if self.width:
997 md['width'] = self.width
997 md['width'] = self.width
998 if self.height:
998 if self.height:
999 md['height'] = self.height
999 md['height'] = self.height
1000 if self.unconfined:
1000 if self.unconfined:
1001 md['unconfined'] = self.unconfined
1001 md['unconfined'] = self.unconfined
1002 if md or always_both:
1002 if md or always_both:
1003 return b64_data, md
1003 return b64_data, md
1004 else:
1004 else:
1005 return b64_data
1005 return b64_data
1006
1006
1007 def _repr_png_(self):
1007 def _repr_png_(self):
1008 if self.embed and self.format == self._FMT_PNG:
1008 if self.embed and self.format == self._FMT_PNG:
1009 return self._data_and_metadata()
1009 return self._data_and_metadata()
1010
1010
1011 def _repr_jpeg_(self):
1011 def _repr_jpeg_(self):
1012 if self.embed and self.format == self._FMT_JPEG:
1012 if self.embed and self.format == self._FMT_JPEG:
1013 return self._data_and_metadata()
1013 return self._data_and_metadata()
1014
1014
1015 def _find_ext(self, s):
1015 def _find_ext(self, s):
1016 base, ext = splitext(s)
1016 base, ext = splitext(s)
1017
1017
1018 if not ext:
1018 if not ext:
1019 return base
1019 return base
1020
1020
1021 # `splitext` includes leading period, so we skip it
1021 # `splitext` includes leading period, so we skip it
1022 return ext[1:].lower()
1022 return ext[1:].lower()
1023
1023
1024
1024
1025 class Video(DisplayObject):
1025 class Video(DisplayObject):
1026
1026
1027 def __init__(self, data=None, url=None, filename=None, embed=False,
1027 def __init__(self, data=None, url=None, filename=None, embed=False,
1028 mimetype=None, width=None, height=None, html_attributes="controls"):
1028 mimetype=None, width=None, height=None, html_attributes="controls"):
1029 """Create a video object given raw data or an URL.
1029 """Create a video object given raw data or an URL.
1030
1030
1031 When this object is returned by an input cell or passed to the
1031 When this object is returned by an input cell or passed to the
1032 display function, it will result in the video being displayed
1032 display function, it will result in the video being displayed
1033 in the frontend.
1033 in the frontend.
1034
1034
1035 Parameters
1035 Parameters
1036 ----------
1036 ----------
1037 data : unicode, str or bytes
1037 data : unicode, str or bytes
1038 The raw video data or a URL or filename to load the data from.
1038 The raw video data or a URL or filename to load the data from.
1039 Raw data will require passing `embed=True`.
1039 Raw data will require passing `embed=True`.
1040 url : unicode
1040 url : unicode
1041 A URL for the video. If you specify `url=`,
1041 A URL for the video. If you specify `url=`,
1042 the image data will not be embedded.
1042 the image data will not be embedded.
1043 filename : unicode
1043 filename : unicode
1044 Path to a local file containing the video.
1044 Path to a local file containing the video.
1045 Will be interpreted as a local URL unless `embed=True`.
1045 Will be interpreted as a local URL unless `embed=True`.
1046 embed : bool
1046 embed : bool
1047 Should the video be embedded using a data URI (True) or be
1047 Should the video be embedded using a data URI (True) or be
1048 loaded using a <video> tag (False).
1048 loaded using a <video> tag (False).
1049
1049
1050 Since videos are large, embedding them should be avoided, if possible.
1050 Since videos are large, embedding them should be avoided, if possible.
1051 You must confirm embedding as your intention by passing `embed=True`.
1051 You must confirm embedding as your intention by passing `embed=True`.
1052
1052
1053 Local files can be displayed with URLs without embedding the content, via::
1053 Local files can be displayed with URLs without embedding the content, via::
1054
1054
1055 Video('./video.mp4')
1055 Video('./video.mp4')
1056
1056
1057 mimetype: unicode
1057 mimetype: unicode
1058 Specify the mimetype for embedded videos.
1058 Specify the mimetype for embedded videos.
1059 Default will be guessed from file extension, if available.
1059 Default will be guessed from file extension, if available.
1060 width : int
1060 width : int
1061 Width in pixels to which to constrain the video in HTML.
1061 Width in pixels to which to constrain the video in HTML.
1062 If not supplied, defaults to the width of the video.
1062 If not supplied, defaults to the width of the video.
1063 height : int
1063 height : int
1064 Height in pixels to which to constrain the video in html.
1064 Height in pixels to which to constrain the video in html.
1065 If not supplied, defaults to the height of the video.
1065 If not supplied, defaults to the height of the video.
1066 html_attributes : str
1066 html_attributes : str
1067 Attributes for the HTML `<video>` block.
1067 Attributes for the HTML `<video>` block.
1068 Default: `"controls"` to get video controls.
1068 Default: `"controls"` to get video controls.
1069 Other examples: `"controls muted"` for muted video with controls,
1069 Other examples: `"controls muted"` for muted video with controls,
1070 `"loop autoplay"` for looping autoplaying video without controls.
1070 `"loop autoplay"` for looping autoplaying video without controls.
1071
1071
1072 Examples
1072 Examples
1073 --------
1073 --------
1074
1074
1075 ::
1075 ::
1076
1076
1077 Video('https://archive.org/download/Sita_Sings_the_Blues/Sita_Sings_the_Blues_small.mp4')
1077 Video('https://archive.org/download/Sita_Sings_the_Blues/Sita_Sings_the_Blues_small.mp4')
1078 Video('path/to/video.mp4')
1078 Video('path/to/video.mp4')
1079 Video('path/to/video.mp4', embed=True)
1079 Video('path/to/video.mp4', embed=True)
1080 Video('path/to/video.mp4', embed=True, html_attributes="controls muted autoplay")
1080 Video('path/to/video.mp4', embed=True, html_attributes="controls muted autoplay")
1081 Video(b'raw-videodata', embed=True)
1081 Video(b'raw-videodata', embed=True)
1082 """
1082 """
1083 if isinstance(data, (Path, PurePath)):
1083 if isinstance(data, (Path, PurePath)):
1084 data = str(data)
1084 data = str(data)
1085
1085
1086 if url is None and isinstance(data, str) and data.startswith(('http:', 'https:')):
1086 if url is None and isinstance(data, str) and data.startswith(('http:', 'https:')):
1087 url = data
1087 url = data
1088 data = None
1088 data = None
1089 elif os.path.exists(data):
1089 elif os.path.exists(data):
1090 filename = data
1090 filename = data
1091 data = None
1091 data = None
1092
1092
1093 if data and not embed:
1093 if data and not embed:
1094 msg = ''.join([
1094 msg = ''.join([
1095 "To embed videos, you must pass embed=True ",
1095 "To embed videos, you must pass embed=True ",
1096 "(this may make your notebook files huge)\n",
1096 "(this may make your notebook files huge)\n",
1097 "Consider passing Video(url='...')",
1097 "Consider passing Video(url='...')",
1098 ])
1098 ])
1099 raise ValueError(msg)
1099 raise ValueError(msg)
1100
1100
1101 self.mimetype = mimetype
1101 self.mimetype = mimetype
1102 self.embed = embed
1102 self.embed = embed
1103 self.width = width
1103 self.width = width
1104 self.height = height
1104 self.height = height
1105 self.html_attributes = html_attributes
1105 self.html_attributes = html_attributes
1106 super(Video, self).__init__(data=data, url=url, filename=filename)
1106 super(Video, self).__init__(data=data, url=url, filename=filename)
1107
1107
1108 def _repr_html_(self):
1108 def _repr_html_(self):
1109 width = height = ''
1109 width = height = ''
1110 if self.width:
1110 if self.width:
1111 width = ' width="%d"' % self.width
1111 width = ' width="%d"' % self.width
1112 if self.height:
1112 if self.height:
1113 height = ' height="%d"' % self.height
1113 height = ' height="%d"' % self.height
1114
1114
1115 # External URLs and potentially local files are not embedded into the
1115 # External URLs and potentially local files are not embedded into the
1116 # notebook output.
1116 # notebook output.
1117 if not self.embed:
1117 if not self.embed:
1118 url = self.url if self.url is not None else self.filename
1118 url = self.url if self.url is not None else self.filename
1119 output = """<video src="{0}" {1} {2} {3}>
1119 output = """<video src="{0}" {1} {2} {3}>
1120 Your browser does not support the <code>video</code> element.
1120 Your browser does not support the <code>video</code> element.
1121 </video>""".format(url, self.html_attributes, width, height)
1121 </video>""".format(url, self.html_attributes, width, height)
1122 return output
1122 return output
1123
1123
1124 # Embedded videos are base64-encoded.
1124 # Embedded videos are base64-encoded.
1125 mimetype = self.mimetype
1125 mimetype = self.mimetype
1126 if self.filename is not None:
1126 if self.filename is not None:
1127 if not mimetype:
1127 if not mimetype:
1128 mimetype, _ = mimetypes.guess_type(self.filename)
1128 mimetype, _ = mimetypes.guess_type(self.filename)
1129
1129
1130 with open(self.filename, 'rb') as f:
1130 with open(self.filename, 'rb') as f:
1131 video = f.read()
1131 video = f.read()
1132 else:
1132 else:
1133 video = self.data
1133 video = self.data
1134 if isinstance(video, str):
1134 if isinstance(video, str):
1135 # unicode input is already b64-encoded
1135 # unicode input is already b64-encoded
1136 b64_video = video
1136 b64_video = video
1137 else:
1137 else:
1138 b64_video = b2a_base64(video).decode('ascii').rstrip()
1138 b64_video = b2a_base64(video).decode('ascii').rstrip()
1139
1139
1140 output = """<video {0} {1} {2}>
1140 output = """<video {0} {1} {2}>
1141 <source src="data:{3};base64,{4}" type="{3}">
1141 <source src="data:{3};base64,{4}" type="{3}">
1142 Your browser does not support the video tag.
1142 Your browser does not support the video tag.
1143 </video>""".format(self.html_attributes, width, height, mimetype, b64_video)
1143 </video>""".format(self.html_attributes, width, height, mimetype, b64_video)
1144 return output
1144 return output
1145
1145
1146 def reload(self):
1146 def reload(self):
1147 # TODO
1147 # TODO
1148 pass
1148 pass
1149
1149
1150
1150
1151 @skip_doctest
1151 @skip_doctest
1152 def set_matplotlib_formats(*formats, **kwargs):
1152 def set_matplotlib_formats(*formats, **kwargs):
1153 """Select figure formats for the inline backend. Optionally pass quality for JPEG.
1153 """Select figure formats for the inline backend. Optionally pass quality for JPEG.
1154
1154
1155 For example, this enables PNG and JPEG output with a JPEG quality of 90%::
1155 For example, this enables PNG and JPEG output with a JPEG quality of 90%::
1156
1156
1157 In [1]: set_matplotlib_formats('png', 'jpeg', quality=90)
1157 In [1]: set_matplotlib_formats('png', 'jpeg', quality=90)
1158
1158
1159 To set this in your config files use the following::
1159 To set this in your config files use the following::
1160
1160
1161 c.InlineBackend.figure_formats = {'png', 'jpeg'}
1161 c.InlineBackend.figure_formats = {'png', 'jpeg'}
1162 c.InlineBackend.print_figure_kwargs.update({'quality' : 90})
1162 c.InlineBackend.print_figure_kwargs.update({'quality' : 90})
1163
1163
1164 Parameters
1164 Parameters
1165 ----------
1165 ----------
1166 *formats : strs
1166 *formats : strs
1167 One or more figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
1167 One or more figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
1168 **kwargs :
1168 **kwargs :
1169 Keyword args will be relayed to ``figure.canvas.print_figure``.
1169 Keyword args will be relayed to ``figure.canvas.print_figure``.
1170 """
1170 """
1171 from IPython.core.interactiveshell import InteractiveShell
1171 from IPython.core.interactiveshell import InteractiveShell
1172 from IPython.core.pylabtools import select_figure_formats
1172 from IPython.core.pylabtools import select_figure_formats
1173 # build kwargs, starting with InlineBackend config
1173 # build kwargs, starting with InlineBackend config
1174 kw = {}
1174 kw = {}
1175 from ipykernel.pylab.config import InlineBackend
1175 from ipykernel.pylab.config import InlineBackend
1176 cfg = InlineBackend.instance()
1176 cfg = InlineBackend.instance()
1177 kw.update(cfg.print_figure_kwargs)
1177 kw.update(cfg.print_figure_kwargs)
1178 kw.update(**kwargs)
1178 kw.update(**kwargs)
1179 shell = InteractiveShell.instance()
1179 shell = InteractiveShell.instance()
1180 select_figure_formats(shell, formats, **kw)
1180 select_figure_formats(shell, formats, **kw)
1181
1181
1182 @skip_doctest
1182 @skip_doctest
1183 def set_matplotlib_close(close=True):
1183 def set_matplotlib_close(close=True):
1184 """Set whether the inline backend closes all figures automatically or not.
1184 """Set whether the inline backend closes all figures automatically or not.
1185
1185
1186 By default, the inline backend used in the IPython Notebook will close all
1186 By default, the inline backend used in the IPython Notebook will close all
1187 matplotlib figures automatically after each cell is run. This means that
1187 matplotlib figures automatically after each cell is run. This means that
1188 plots in different cells won't interfere. Sometimes, you may want to make
1188 plots in different cells won't interfere. Sometimes, you may want to make
1189 a plot in one cell and then refine it in later cells. This can be accomplished
1189 a plot in one cell and then refine it in later cells. This can be accomplished
1190 by::
1190 by::
1191
1191
1192 In [1]: set_matplotlib_close(False)
1192 In [1]: set_matplotlib_close(False)
1193
1193
1194 To set this in your config files use the following::
1194 To set this in your config files use the following::
1195
1195
1196 c.InlineBackend.close_figures = False
1196 c.InlineBackend.close_figures = False
1197
1197
1198 Parameters
1198 Parameters
1199 ----------
1199 ----------
1200 close : bool
1200 close : bool
1201 Should all matplotlib figures be automatically closed after each cell is
1201 Should all matplotlib figures be automatically closed after each cell is
1202 run?
1202 run?
1203 """
1203 """
1204 from ipykernel.pylab.config import InlineBackend
1204 from ipykernel.pylab.config import InlineBackend
1205 cfg = InlineBackend.instance()
1205 cfg = InlineBackend.instance()
1206 cfg.close_figures = close
1206 cfg.close_figures = close
@@ -1,343 +1,343 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Paging capabilities for IPython.core
3 Paging capabilities for IPython.core
4
4
5 Notes
5 Notes
6 -----
6 -----
7
7
8 For now this uses IPython hooks, so it can't be in IPython.utils. If we can get
8 For now this uses IPython hooks, so it can't be in IPython.utils. If we can get
9 rid of that dependency, we could move it there.
9 rid of that dependency, we could move it there.
10 -----
10 -----
11 """
11 """
12
12
13 # Copyright (c) IPython Development Team.
13 # Copyright (c) IPython Development Team.
14 # Distributed under the terms of the Modified BSD License.
14 # Distributed under the terms of the Modified BSD License.
15
15
16
16
17 import os
17 import os
18 import io
18 import io
19 import re
19 import re
20 import sys
20 import sys
21 import tempfile
21 import tempfile
22 import subprocess
22 import subprocess
23
23
24 from io import UnsupportedOperation
24 from io import UnsupportedOperation
25
25
26 from IPython import get_ipython
26 from IPython import get_ipython
27 from IPython.display import display
27 from IPython.display import display
28 from IPython.core.error import TryNext
28 from IPython.core.error import TryNext
29 from IPython.utils.data import chop
29 from IPython.utils.data import chop
30 from IPython.utils.process import system
30 from IPython.utils.process import system
31 from IPython.utils.terminal import get_terminal_size
31 from IPython.utils.terminal import get_terminal_size
32 from IPython.utils import py3compat
32 from IPython.utils import py3compat
33
33
34
34
35 def display_page(strng, start=0, screen_lines=25):
35 def display_page(strng, start=0, screen_lines=25):
36 """Just display, no paging. screen_lines is ignored."""
36 """Just display, no paging. screen_lines is ignored."""
37 if isinstance(strng, dict):
37 if isinstance(strng, dict):
38 data = strng
38 data = strng
39 else:
39 else:
40 if start:
40 if start:
41 strng = u'\n'.join(strng.splitlines()[start:])
41 strng = u'\n'.join(strng.splitlines()[start:])
42 data = { 'text/plain': strng }
42 data = { 'text/plain': strng }
43 display(data, raw=True)
43 display(data, raw=True)
44
44
45
45
46 def as_hook(page_func):
46 def as_hook(page_func):
47 """Wrap a pager func to strip the `self` arg
47 """Wrap a pager func to strip the `self` arg
48
48
49 so it can be called as a hook.
49 so it can be called as a hook.
50 """
50 """
51 return lambda self, *args, **kwargs: page_func(*args, **kwargs)
51 return lambda self, *args, **kwargs: page_func(*args, **kwargs)
52
52
53
53
54 esc_re = re.compile(r"(\x1b[^m]+m)")
54 esc_re = re.compile(r"(\x1b[^m]+m)")
55
55
56 def page_dumb(strng, start=0, screen_lines=25):
56 def page_dumb(strng, start=0, screen_lines=25):
57 """Very dumb 'pager' in Python, for when nothing else works.
57 """Very dumb 'pager' in Python, for when nothing else works.
58
58
59 Only moves forward, same interface as page(), except for pager_cmd and
59 Only moves forward, same interface as page(), except for pager_cmd and
60 mode.
60 mode.
61 """
61 """
62 if isinstance(strng, dict):
62 if isinstance(strng, dict):
63 strng = strng.get('text/plain', '')
63 strng = strng.get('text/plain', '')
64 out_ln = strng.splitlines()[start:]
64 out_ln = strng.splitlines()[start:]
65 screens = chop(out_ln,screen_lines-1)
65 screens = chop(out_ln,screen_lines-1)
66 if len(screens) == 1:
66 if len(screens) == 1:
67 print(os.linesep.join(screens[0]))
67 print(os.linesep.join(screens[0]))
68 else:
68 else:
69 last_escape = ""
69 last_escape = ""
70 for scr in screens[0:-1]:
70 for scr in screens[0:-1]:
71 hunk = os.linesep.join(scr)
71 hunk = os.linesep.join(scr)
72 print(last_escape + hunk)
72 print(last_escape + hunk)
73 if not page_more():
73 if not page_more():
74 return
74 return
75 esc_list = esc_re.findall(hunk)
75 esc_list = esc_re.findall(hunk)
76 if len(esc_list) > 0:
76 if len(esc_list) > 0:
77 last_escape = esc_list[-1]
77 last_escape = esc_list[-1]
78 print(last_escape + os.linesep.join(screens[-1]))
78 print(last_escape + os.linesep.join(screens[-1]))
79
79
80 def _detect_screen_size(screen_lines_def):
80 def _detect_screen_size(screen_lines_def):
81 """Attempt to work out the number of lines on the screen.
81 """Attempt to work out the number of lines on the screen.
82
82
83 This is called by page(). It can raise an error (e.g. when run in the
83 This is called by page(). It can raise an error (e.g. when run in the
84 test suite), so it's separated out so it can easily be called in a try block.
84 test suite), so it's separated out so it can easily be called in a try block.
85 """
85 """
86 TERM = os.environ.get('TERM',None)
86 TERM = os.environ.get('TERM',None)
87 if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'):
87 if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'):
88 # curses causes problems on many terminals other than xterm, and
88 # curses causes problems on many terminals other than xterm, and
89 # some termios calls lock up on Sun OS5.
89 # some termios calls lock up on Sun OS5.
90 return screen_lines_def
90 return screen_lines_def
91
91
92 try:
92 try:
93 import termios
93 import termios
94 import curses
94 import curses
95 except ImportError:
95 except ImportError:
96 return screen_lines_def
96 return screen_lines_def
97
97
98 # There is a bug in curses, where *sometimes* it fails to properly
98 # There is a bug in curses, where *sometimes* it fails to properly
99 # initialize, and then after the endwin() call is made, the
99 # initialize, and then after the endwin() call is made, the
100 # terminal is left in an unusable state. Rather than trying to
100 # terminal is left in an unusable state. Rather than trying to
101 # check every time for this (by requesting and comparing termios
101 # check every time for this (by requesting and comparing termios
102 # flags each time), we just save the initial terminal state and
102 # flags each time), we just save the initial terminal state and
103 # unconditionally reset it every time. It's cheaper than making
103 # unconditionally reset it every time. It's cheaper than making
104 # the checks.
104 # the checks.
105 try:
105 try:
106 term_flags = termios.tcgetattr(sys.stdout)
106 term_flags = termios.tcgetattr(sys.stdout)
107 except termios.error as err:
107 except termios.error as err:
108 # can fail on Linux 2.6, pager_page will catch the TypeError
108 # can fail on Linux 2.6, pager_page will catch the TypeError
109 raise TypeError('termios error: {0}'.format(err))
109 raise TypeError('termios error: {0}'.format(err)) from err
110
110
111 try:
111 try:
112 scr = curses.initscr()
112 scr = curses.initscr()
113 except AttributeError:
113 except AttributeError:
114 # Curses on Solaris may not be complete, so we can't use it there
114 # Curses on Solaris may not be complete, so we can't use it there
115 return screen_lines_def
115 return screen_lines_def
116
116
117 screen_lines_real,screen_cols = scr.getmaxyx()
117 screen_lines_real,screen_cols = scr.getmaxyx()
118 curses.endwin()
118 curses.endwin()
119
119
120 # Restore terminal state in case endwin() didn't.
120 # Restore terminal state in case endwin() didn't.
121 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags)
121 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags)
122 # Now we have what we needed: the screen size in rows/columns
122 # Now we have what we needed: the screen size in rows/columns
123 return screen_lines_real
123 return screen_lines_real
124 #print '***Screen size:',screen_lines_real,'lines x',\
124 #print '***Screen size:',screen_lines_real,'lines x',\
125 #screen_cols,'columns.' # dbg
125 #screen_cols,'columns.' # dbg
126
126
127 def pager_page(strng, start=0, screen_lines=0, pager_cmd=None):
127 def pager_page(strng, start=0, screen_lines=0, pager_cmd=None):
128 """Display a string, piping through a pager after a certain length.
128 """Display a string, piping through a pager after a certain length.
129
129
130 strng can be a mime-bundle dict, supplying multiple representations,
130 strng can be a mime-bundle dict, supplying multiple representations,
131 keyed by mime-type.
131 keyed by mime-type.
132
132
133 The screen_lines parameter specifies the number of *usable* lines of your
133 The screen_lines parameter specifies the number of *usable* lines of your
134 terminal screen (total lines minus lines you need to reserve to show other
134 terminal screen (total lines minus lines you need to reserve to show other
135 information).
135 information).
136
136
137 If you set screen_lines to a number <=0, page() will try to auto-determine
137 If you set screen_lines to a number <=0, page() will try to auto-determine
138 your screen size and will only use up to (screen_size+screen_lines) for
138 your screen size and will only use up to (screen_size+screen_lines) for
139 printing, paging after that. That is, if you want auto-detection but need
139 printing, paging after that. That is, if you want auto-detection but need
140 to reserve the bottom 3 lines of the screen, use screen_lines = -3, and for
140 to reserve the bottom 3 lines of the screen, use screen_lines = -3, and for
141 auto-detection without any lines reserved simply use screen_lines = 0.
141 auto-detection without any lines reserved simply use screen_lines = 0.
142
142
143 If a string won't fit in the allowed lines, it is sent through the
143 If a string won't fit in the allowed lines, it is sent through the
144 specified pager command. If none given, look for PAGER in the environment,
144 specified pager command. If none given, look for PAGER in the environment,
145 and ultimately default to less.
145 and ultimately default to less.
146
146
147 If no system pager works, the string is sent through a 'dumb pager'
147 If no system pager works, the string is sent through a 'dumb pager'
148 written in python, very simplistic.
148 written in python, very simplistic.
149 """
149 """
150
150
151 # for compatibility with mime-bundle form:
151 # for compatibility with mime-bundle form:
152 if isinstance(strng, dict):
152 if isinstance(strng, dict):
153 strng = strng['text/plain']
153 strng = strng['text/plain']
154
154
155 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs
155 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs
156 TERM = os.environ.get('TERM','dumb')
156 TERM = os.environ.get('TERM','dumb')
157 if TERM in ['dumb','emacs'] and os.name != 'nt':
157 if TERM in ['dumb','emacs'] and os.name != 'nt':
158 print(strng)
158 print(strng)
159 return
159 return
160 # chop off the topmost part of the string we don't want to see
160 # chop off the topmost part of the string we don't want to see
161 str_lines = strng.splitlines()[start:]
161 str_lines = strng.splitlines()[start:]
162 str_toprint = os.linesep.join(str_lines)
162 str_toprint = os.linesep.join(str_lines)
163 num_newlines = len(str_lines)
163 num_newlines = len(str_lines)
164 len_str = len(str_toprint)
164 len_str = len(str_toprint)
165
165
166 # Dumb heuristics to guesstimate number of on-screen lines the string
166 # Dumb heuristics to guesstimate number of on-screen lines the string
167 # takes. Very basic, but good enough for docstrings in reasonable
167 # takes. Very basic, but good enough for docstrings in reasonable
168 # terminals. If someone later feels like refining it, it's not hard.
168 # terminals. If someone later feels like refining it, it's not hard.
169 numlines = max(num_newlines,int(len_str/80)+1)
169 numlines = max(num_newlines,int(len_str/80)+1)
170
170
171 screen_lines_def = get_terminal_size()[1]
171 screen_lines_def = get_terminal_size()[1]
172
172
173 # auto-determine screen size
173 # auto-determine screen size
174 if screen_lines <= 0:
174 if screen_lines <= 0:
175 try:
175 try:
176 screen_lines += _detect_screen_size(screen_lines_def)
176 screen_lines += _detect_screen_size(screen_lines_def)
177 except (TypeError, UnsupportedOperation):
177 except (TypeError, UnsupportedOperation):
178 print(str_toprint)
178 print(str_toprint)
179 return
179 return
180
180
181 #print 'numlines',numlines,'screenlines',screen_lines # dbg
181 #print 'numlines',numlines,'screenlines',screen_lines # dbg
182 if numlines <= screen_lines :
182 if numlines <= screen_lines :
183 #print '*** normal print' # dbg
183 #print '*** normal print' # dbg
184 print(str_toprint)
184 print(str_toprint)
185 else:
185 else:
186 # Try to open pager and default to internal one if that fails.
186 # Try to open pager and default to internal one if that fails.
187 # All failure modes are tagged as 'retval=1', to match the return
187 # All failure modes are tagged as 'retval=1', to match the return
188 # value of a failed system command. If any intermediate attempt
188 # value of a failed system command. If any intermediate attempt
189 # sets retval to 1, at the end we resort to our own page_dumb() pager.
189 # sets retval to 1, at the end we resort to our own page_dumb() pager.
190 pager_cmd = get_pager_cmd(pager_cmd)
190 pager_cmd = get_pager_cmd(pager_cmd)
191 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
191 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
192 if os.name == 'nt':
192 if os.name == 'nt':
193 if pager_cmd.startswith('type'):
193 if pager_cmd.startswith('type'):
194 # The default WinXP 'type' command is failing on complex strings.
194 # The default WinXP 'type' command is failing on complex strings.
195 retval = 1
195 retval = 1
196 else:
196 else:
197 fd, tmpname = tempfile.mkstemp('.txt')
197 fd, tmpname = tempfile.mkstemp('.txt')
198 try:
198 try:
199 os.close(fd)
199 os.close(fd)
200 with open(tmpname, 'wt') as tmpfile:
200 with open(tmpname, 'wt') as tmpfile:
201 tmpfile.write(strng)
201 tmpfile.write(strng)
202 cmd = "%s < %s" % (pager_cmd, tmpname)
202 cmd = "%s < %s" % (pager_cmd, tmpname)
203 # tmpfile needs to be closed for windows
203 # tmpfile needs to be closed for windows
204 if os.system(cmd):
204 if os.system(cmd):
205 retval = 1
205 retval = 1
206 else:
206 else:
207 retval = None
207 retval = None
208 finally:
208 finally:
209 os.remove(tmpname)
209 os.remove(tmpname)
210 else:
210 else:
211 try:
211 try:
212 retval = None
212 retval = None
213 # Emulate os.popen, but redirect stderr
213 # Emulate os.popen, but redirect stderr
214 proc = subprocess.Popen(pager_cmd,
214 proc = subprocess.Popen(pager_cmd,
215 shell=True,
215 shell=True,
216 stdin=subprocess.PIPE,
216 stdin=subprocess.PIPE,
217 stderr=subprocess.DEVNULL
217 stderr=subprocess.DEVNULL
218 )
218 )
219 pager = os._wrap_close(io.TextIOWrapper(proc.stdin), proc)
219 pager = os._wrap_close(io.TextIOWrapper(proc.stdin), proc)
220 try:
220 try:
221 pager_encoding = pager.encoding or sys.stdout.encoding
221 pager_encoding = pager.encoding or sys.stdout.encoding
222 pager.write(strng)
222 pager.write(strng)
223 finally:
223 finally:
224 retval = pager.close()
224 retval = pager.close()
225 except IOError as msg: # broken pipe when user quits
225 except IOError as msg: # broken pipe when user quits
226 if msg.args == (32, 'Broken pipe'):
226 if msg.args == (32, 'Broken pipe'):
227 retval = None
227 retval = None
228 else:
228 else:
229 retval = 1
229 retval = 1
230 except OSError:
230 except OSError:
231 # Other strange problems, sometimes seen in Win2k/cygwin
231 # Other strange problems, sometimes seen in Win2k/cygwin
232 retval = 1
232 retval = 1
233 if retval is not None:
233 if retval is not None:
234 page_dumb(strng,screen_lines=screen_lines)
234 page_dumb(strng,screen_lines=screen_lines)
235
235
236
236
237 def page(data, start=0, screen_lines=0, pager_cmd=None):
237 def page(data, start=0, screen_lines=0, pager_cmd=None):
238 """Display content in a pager, piping through a pager after a certain length.
238 """Display content in a pager, piping through a pager after a certain length.
239
239
240 data can be a mime-bundle dict, supplying multiple representations,
240 data can be a mime-bundle dict, supplying multiple representations,
241 keyed by mime-type, or text.
241 keyed by mime-type, or text.
242
242
243 Pager is dispatched via the `show_in_pager` IPython hook.
243 Pager is dispatched via the `show_in_pager` IPython hook.
244 If no hook is registered, `pager_page` will be used.
244 If no hook is registered, `pager_page` will be used.
245 """
245 """
246 # Some routines may auto-compute start offsets incorrectly and pass a
246 # Some routines may auto-compute start offsets incorrectly and pass a
247 # negative value. Offset to 0 for robustness.
247 # negative value. Offset to 0 for robustness.
248 start = max(0, start)
248 start = max(0, start)
249
249
250 # first, try the hook
250 # first, try the hook
251 ip = get_ipython()
251 ip = get_ipython()
252 if ip:
252 if ip:
253 try:
253 try:
254 ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines)
254 ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines)
255 return
255 return
256 except TryNext:
256 except TryNext:
257 pass
257 pass
258
258
259 # fallback on default pager
259 # fallback on default pager
260 return pager_page(data, start, screen_lines, pager_cmd)
260 return pager_page(data, start, screen_lines, pager_cmd)
261
261
262
262
263 def page_file(fname, start=0, pager_cmd=None):
263 def page_file(fname, start=0, pager_cmd=None):
264 """Page a file, using an optional pager command and starting line.
264 """Page a file, using an optional pager command and starting line.
265 """
265 """
266
266
267 pager_cmd = get_pager_cmd(pager_cmd)
267 pager_cmd = get_pager_cmd(pager_cmd)
268 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
268 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
269
269
270 try:
270 try:
271 if os.environ['TERM'] in ['emacs','dumb']:
271 if os.environ['TERM'] in ['emacs','dumb']:
272 raise EnvironmentError
272 raise EnvironmentError
273 system(pager_cmd + ' ' + fname)
273 system(pager_cmd + ' ' + fname)
274 except:
274 except:
275 try:
275 try:
276 if start > 0:
276 if start > 0:
277 start -= 1
277 start -= 1
278 page(open(fname).read(),start)
278 page(open(fname).read(),start)
279 except:
279 except:
280 print('Unable to show file',repr(fname))
280 print('Unable to show file',repr(fname))
281
281
282
282
283 def get_pager_cmd(pager_cmd=None):
283 def get_pager_cmd(pager_cmd=None):
284 """Return a pager command.
284 """Return a pager command.
285
285
286 Makes some attempts at finding an OS-correct one.
286 Makes some attempts at finding an OS-correct one.
287 """
287 """
288 if os.name == 'posix':
288 if os.name == 'posix':
289 default_pager_cmd = 'less -R' # -R for color control sequences
289 default_pager_cmd = 'less -R' # -R for color control sequences
290 elif os.name in ['nt','dos']:
290 elif os.name in ['nt','dos']:
291 default_pager_cmd = 'type'
291 default_pager_cmd = 'type'
292
292
293 if pager_cmd is None:
293 if pager_cmd is None:
294 try:
294 try:
295 pager_cmd = os.environ['PAGER']
295 pager_cmd = os.environ['PAGER']
296 except:
296 except:
297 pager_cmd = default_pager_cmd
297 pager_cmd = default_pager_cmd
298
298
299 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower():
299 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower():
300 pager_cmd += ' -R'
300 pager_cmd += ' -R'
301
301
302 return pager_cmd
302 return pager_cmd
303
303
304
304
305 def get_pager_start(pager, start):
305 def get_pager_start(pager, start):
306 """Return the string for paging files with an offset.
306 """Return the string for paging files with an offset.
307
307
308 This is the '+N' argument which less and more (under Unix) accept.
308 This is the '+N' argument which less and more (under Unix) accept.
309 """
309 """
310
310
311 if pager in ['less','more']:
311 if pager in ['less','more']:
312 if start:
312 if start:
313 start_string = '+' + str(start)
313 start_string = '+' + str(start)
314 else:
314 else:
315 start_string = ''
315 start_string = ''
316 else:
316 else:
317 start_string = ''
317 start_string = ''
318 return start_string
318 return start_string
319
319
320
320
321 # (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch()
321 # (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch()
322 if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs':
322 if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs':
323 import msvcrt
323 import msvcrt
324 def page_more():
324 def page_more():
325 """ Smart pausing between pages
325 """ Smart pausing between pages
326
326
327 @return: True if need print more lines, False if quit
327 @return: True if need print more lines, False if quit
328 """
328 """
329 sys.stdout.write('---Return to continue, q to quit--- ')
329 sys.stdout.write('---Return to continue, q to quit--- ')
330 ans = msvcrt.getwch()
330 ans = msvcrt.getwch()
331 if ans in ("q", "Q"):
331 if ans in ("q", "Q"):
332 result = False
332 result = False
333 else:
333 else:
334 result = True
334 result = True
335 sys.stdout.write("\b"*37 + " "*37 + "\b"*37)
335 sys.stdout.write("\b"*37 + " "*37 + "\b"*37)
336 return result
336 return result
337 else:
337 else:
338 def page_more():
338 def page_more():
339 ans = py3compat.input('---Return to continue, q to quit--- ')
339 ans = py3compat.input('---Return to continue, q to quit--- ')
340 if ans.lower().startswith('q'):
340 if ans.lower().startswith('q'):
341 return False
341 return False
342 else:
342 else:
343 return True
343 return True
@@ -1,419 +1,419 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Pylab (matplotlib) support utilities."""
2 """Pylab (matplotlib) support utilities."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from io import BytesIO
7 from io import BytesIO
8
8
9 from IPython.core.display import _pngxy
9 from IPython.core.display import _pngxy
10 from IPython.utils.decorators import flag_calls
10 from IPython.utils.decorators import flag_calls
11
11
12 # If user specifies a GUI, that dictates the backend, otherwise we read the
12 # If user specifies a GUI, that dictates the backend, otherwise we read the
13 # user's mpl default from the mpl rc structure
13 # user's mpl default from the mpl rc structure
14 backends = {'tk': 'TkAgg',
14 backends = {'tk': 'TkAgg',
15 'gtk': 'GTKAgg',
15 'gtk': 'GTKAgg',
16 'gtk3': 'GTK3Agg',
16 'gtk3': 'GTK3Agg',
17 'wx': 'WXAgg',
17 'wx': 'WXAgg',
18 'qt4': 'Qt4Agg',
18 'qt4': 'Qt4Agg',
19 'qt5': 'Qt5Agg',
19 'qt5': 'Qt5Agg',
20 'qt': 'Qt5Agg',
20 'qt': 'Qt5Agg',
21 'osx': 'MacOSX',
21 'osx': 'MacOSX',
22 'nbagg': 'nbAgg',
22 'nbagg': 'nbAgg',
23 'notebook': 'nbAgg',
23 'notebook': 'nbAgg',
24 'agg': 'agg',
24 'agg': 'agg',
25 'svg': 'svg',
25 'svg': 'svg',
26 'pdf': 'pdf',
26 'pdf': 'pdf',
27 'ps': 'ps',
27 'ps': 'ps',
28 'inline': 'module://ipykernel.pylab.backend_inline',
28 'inline': 'module://ipykernel.pylab.backend_inline',
29 'ipympl': 'module://ipympl.backend_nbagg',
29 'ipympl': 'module://ipympl.backend_nbagg',
30 'widget': 'module://ipympl.backend_nbagg',
30 'widget': 'module://ipympl.backend_nbagg',
31 }
31 }
32
32
33 # We also need a reverse backends2guis mapping that will properly choose which
33 # We also need a reverse backends2guis mapping that will properly choose which
34 # GUI support to activate based on the desired matplotlib backend. For the
34 # GUI support to activate based on the desired matplotlib backend. For the
35 # most part it's just a reverse of the above dict, but we also need to add a
35 # most part it's just a reverse of the above dict, but we also need to add a
36 # few others that map to the same GUI manually:
36 # few others that map to the same GUI manually:
37 backend2gui = dict(zip(backends.values(), backends.keys()))
37 backend2gui = dict(zip(backends.values(), backends.keys()))
38 # Our tests expect backend2gui to just return 'qt'
38 # Our tests expect backend2gui to just return 'qt'
39 backend2gui['Qt4Agg'] = 'qt'
39 backend2gui['Qt4Agg'] = 'qt'
40 # In the reverse mapping, there are a few extra valid matplotlib backends that
40 # In the reverse mapping, there are a few extra valid matplotlib backends that
41 # map to the same GUI support
41 # map to the same GUI support
42 backend2gui['GTK'] = backend2gui['GTKCairo'] = 'gtk'
42 backend2gui['GTK'] = backend2gui['GTKCairo'] = 'gtk'
43 backend2gui['GTK3Cairo'] = 'gtk3'
43 backend2gui['GTK3Cairo'] = 'gtk3'
44 backend2gui['WX'] = 'wx'
44 backend2gui['WX'] = 'wx'
45 backend2gui['CocoaAgg'] = 'osx'
45 backend2gui['CocoaAgg'] = 'osx'
46 # And some backends that don't need GUI integration
46 # And some backends that don't need GUI integration
47 del backend2gui['nbAgg']
47 del backend2gui['nbAgg']
48 del backend2gui['agg']
48 del backend2gui['agg']
49 del backend2gui['svg']
49 del backend2gui['svg']
50 del backend2gui['pdf']
50 del backend2gui['pdf']
51 del backend2gui['ps']
51 del backend2gui['ps']
52 del backend2gui['module://ipykernel.pylab.backend_inline']
52 del backend2gui['module://ipykernel.pylab.backend_inline']
53
53
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55 # Matplotlib utilities
55 # Matplotlib utilities
56 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
57
57
58
58
59 def getfigs(*fig_nums):
59 def getfigs(*fig_nums):
60 """Get a list of matplotlib figures by figure numbers.
60 """Get a list of matplotlib figures by figure numbers.
61
61
62 If no arguments are given, all available figures are returned. If the
62 If no arguments are given, all available figures are returned. If the
63 argument list contains references to invalid figures, a warning is printed
63 argument list contains references to invalid figures, a warning is printed
64 but the function continues pasting further figures.
64 but the function continues pasting further figures.
65
65
66 Parameters
66 Parameters
67 ----------
67 ----------
68 figs : tuple
68 figs : tuple
69 A tuple of ints giving the figure numbers of the figures to return.
69 A tuple of ints giving the figure numbers of the figures to return.
70 """
70 """
71 from matplotlib._pylab_helpers import Gcf
71 from matplotlib._pylab_helpers import Gcf
72 if not fig_nums:
72 if not fig_nums:
73 fig_managers = Gcf.get_all_fig_managers()
73 fig_managers = Gcf.get_all_fig_managers()
74 return [fm.canvas.figure for fm in fig_managers]
74 return [fm.canvas.figure for fm in fig_managers]
75 else:
75 else:
76 figs = []
76 figs = []
77 for num in fig_nums:
77 for num in fig_nums:
78 f = Gcf.figs.get(num)
78 f = Gcf.figs.get(num)
79 if f is None:
79 if f is None:
80 print('Warning: figure %s not available.' % num)
80 print('Warning: figure %s not available.' % num)
81 else:
81 else:
82 figs.append(f.canvas.figure)
82 figs.append(f.canvas.figure)
83 return figs
83 return figs
84
84
85
85
86 def figsize(sizex, sizey):
86 def figsize(sizex, sizey):
87 """Set the default figure size to be [sizex, sizey].
87 """Set the default figure size to be [sizex, sizey].
88
88
89 This is just an easy to remember, convenience wrapper that sets::
89 This is just an easy to remember, convenience wrapper that sets::
90
90
91 matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
91 matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
92 """
92 """
93 import matplotlib
93 import matplotlib
94 matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
94 matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
95
95
96
96
97 def print_figure(fig, fmt='png', bbox_inches='tight', **kwargs):
97 def print_figure(fig, fmt='png', bbox_inches='tight', **kwargs):
98 """Print a figure to an image, and return the resulting file data
98 """Print a figure to an image, and return the resulting file data
99
99
100 Returned data will be bytes unless ``fmt='svg'``,
100 Returned data will be bytes unless ``fmt='svg'``,
101 in which case it will be unicode.
101 in which case it will be unicode.
102
102
103 Any keyword args are passed to fig.canvas.print_figure,
103 Any keyword args are passed to fig.canvas.print_figure,
104 such as ``quality`` or ``bbox_inches``.
104 such as ``quality`` or ``bbox_inches``.
105 """
105 """
106 # When there's an empty figure, we shouldn't return anything, otherwise we
106 # When there's an empty figure, we shouldn't return anything, otherwise we
107 # get big blank areas in the qt console.
107 # get big blank areas in the qt console.
108 if not fig.axes and not fig.lines:
108 if not fig.axes and not fig.lines:
109 return
109 return
110
110
111 dpi = fig.dpi
111 dpi = fig.dpi
112 if fmt == 'retina':
112 if fmt == 'retina':
113 dpi = dpi * 2
113 dpi = dpi * 2
114 fmt = 'png'
114 fmt = 'png'
115
115
116 # build keyword args
116 # build keyword args
117 kw = {
117 kw = {
118 "format":fmt,
118 "format":fmt,
119 "facecolor":fig.get_facecolor(),
119 "facecolor":fig.get_facecolor(),
120 "edgecolor":fig.get_edgecolor(),
120 "edgecolor":fig.get_edgecolor(),
121 "dpi":dpi,
121 "dpi":dpi,
122 "bbox_inches":bbox_inches,
122 "bbox_inches":bbox_inches,
123 }
123 }
124 # **kwargs get higher priority
124 # **kwargs get higher priority
125 kw.update(kwargs)
125 kw.update(kwargs)
126
126
127 bytes_io = BytesIO()
127 bytes_io = BytesIO()
128 if fig.canvas is None:
128 if fig.canvas is None:
129 from matplotlib.backend_bases import FigureCanvasBase
129 from matplotlib.backend_bases import FigureCanvasBase
130 FigureCanvasBase(fig)
130 FigureCanvasBase(fig)
131
131
132 fig.canvas.print_figure(bytes_io, **kw)
132 fig.canvas.print_figure(bytes_io, **kw)
133 data = bytes_io.getvalue()
133 data = bytes_io.getvalue()
134 if fmt == 'svg':
134 if fmt == 'svg':
135 data = data.decode('utf-8')
135 data = data.decode('utf-8')
136 return data
136 return data
137
137
138 def retina_figure(fig, **kwargs):
138 def retina_figure(fig, **kwargs):
139 """format a figure as a pixel-doubled (retina) PNG"""
139 """format a figure as a pixel-doubled (retina) PNG"""
140 pngdata = print_figure(fig, fmt='retina', **kwargs)
140 pngdata = print_figure(fig, fmt='retina', **kwargs)
141 # Make sure that retina_figure acts just like print_figure and returns
141 # Make sure that retina_figure acts just like print_figure and returns
142 # None when the figure is empty.
142 # None when the figure is empty.
143 if pngdata is None:
143 if pngdata is None:
144 return
144 return
145 w, h = _pngxy(pngdata)
145 w, h = _pngxy(pngdata)
146 metadata = {"width": w//2, "height":h//2}
146 metadata = {"width": w//2, "height":h//2}
147 return pngdata, metadata
147 return pngdata, metadata
148
148
149 # We need a little factory function here to create the closure where
149 # We need a little factory function here to create the closure where
150 # safe_execfile can live.
150 # safe_execfile can live.
151 def mpl_runner(safe_execfile):
151 def mpl_runner(safe_execfile):
152 """Factory to return a matplotlib-enabled runner for %run.
152 """Factory to return a matplotlib-enabled runner for %run.
153
153
154 Parameters
154 Parameters
155 ----------
155 ----------
156 safe_execfile : function
156 safe_execfile : function
157 This must be a function with the same interface as the
157 This must be a function with the same interface as the
158 :meth:`safe_execfile` method of IPython.
158 :meth:`safe_execfile` method of IPython.
159
159
160 Returns
160 Returns
161 -------
161 -------
162 A function suitable for use as the ``runner`` argument of the %run magic
162 A function suitable for use as the ``runner`` argument of the %run magic
163 function.
163 function.
164 """
164 """
165
165
166 def mpl_execfile(fname,*where,**kw):
166 def mpl_execfile(fname,*where,**kw):
167 """matplotlib-aware wrapper around safe_execfile.
167 """matplotlib-aware wrapper around safe_execfile.
168
168
169 Its interface is identical to that of the :func:`execfile` builtin.
169 Its interface is identical to that of the :func:`execfile` builtin.
170
170
171 This is ultimately a call to execfile(), but wrapped in safeties to
171 This is ultimately a call to execfile(), but wrapped in safeties to
172 properly handle interactive rendering."""
172 properly handle interactive rendering."""
173
173
174 import matplotlib
174 import matplotlib
175 import matplotlib.pyplot as plt
175 import matplotlib.pyplot as plt
176
176
177 #print '*** Matplotlib runner ***' # dbg
177 #print '*** Matplotlib runner ***' # dbg
178 # turn off rendering until end of script
178 # turn off rendering until end of script
179 is_interactive = matplotlib.rcParams['interactive']
179 is_interactive = matplotlib.rcParams['interactive']
180 matplotlib.interactive(False)
180 matplotlib.interactive(False)
181 safe_execfile(fname,*where,**kw)
181 safe_execfile(fname,*where,**kw)
182 matplotlib.interactive(is_interactive)
182 matplotlib.interactive(is_interactive)
183 # make rendering call now, if the user tried to do it
183 # make rendering call now, if the user tried to do it
184 if plt.draw_if_interactive.called:
184 if plt.draw_if_interactive.called:
185 plt.draw()
185 plt.draw()
186 plt.draw_if_interactive.called = False
186 plt.draw_if_interactive.called = False
187
187
188 # re-draw everything that is stale
188 # re-draw everything that is stale
189 try:
189 try:
190 da = plt.draw_all
190 da = plt.draw_all
191 except AttributeError:
191 except AttributeError:
192 pass
192 pass
193 else:
193 else:
194 da()
194 da()
195
195
196 return mpl_execfile
196 return mpl_execfile
197
197
198
198
199 def _reshow_nbagg_figure(fig):
199 def _reshow_nbagg_figure(fig):
200 """reshow an nbagg figure"""
200 """reshow an nbagg figure"""
201 try:
201 try:
202 reshow = fig.canvas.manager.reshow
202 reshow = fig.canvas.manager.reshow
203 except AttributeError:
203 except AttributeError as e:
204 raise NotImplementedError()
204 raise NotImplementedError() from e
205 else:
205 else:
206 reshow()
206 reshow()
207
207
208
208
209 def select_figure_formats(shell, formats, **kwargs):
209 def select_figure_formats(shell, formats, **kwargs):
210 """Select figure formats for the inline backend.
210 """Select figure formats for the inline backend.
211
211
212 Parameters
212 Parameters
213 ==========
213 ==========
214 shell : InteractiveShell
214 shell : InteractiveShell
215 The main IPython instance.
215 The main IPython instance.
216 formats : str or set
216 formats : str or set
217 One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
217 One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
218 **kwargs : any
218 **kwargs : any
219 Extra keyword arguments to be passed to fig.canvas.print_figure.
219 Extra keyword arguments to be passed to fig.canvas.print_figure.
220 """
220 """
221 import matplotlib
221 import matplotlib
222 from matplotlib.figure import Figure
222 from matplotlib.figure import Figure
223
223
224 svg_formatter = shell.display_formatter.formatters['image/svg+xml']
224 svg_formatter = shell.display_formatter.formatters['image/svg+xml']
225 png_formatter = shell.display_formatter.formatters['image/png']
225 png_formatter = shell.display_formatter.formatters['image/png']
226 jpg_formatter = shell.display_formatter.formatters['image/jpeg']
226 jpg_formatter = shell.display_formatter.formatters['image/jpeg']
227 pdf_formatter = shell.display_formatter.formatters['application/pdf']
227 pdf_formatter = shell.display_formatter.formatters['application/pdf']
228
228
229 if isinstance(formats, str):
229 if isinstance(formats, str):
230 formats = {formats}
230 formats = {formats}
231 # cast in case of list / tuple
231 # cast in case of list / tuple
232 formats = set(formats)
232 formats = set(formats)
233
233
234 [ f.pop(Figure, None) for f in shell.display_formatter.formatters.values() ]
234 [ f.pop(Figure, None) for f in shell.display_formatter.formatters.values() ]
235 mplbackend = matplotlib.get_backend().lower()
235 mplbackend = matplotlib.get_backend().lower()
236 if mplbackend == 'nbagg' or mplbackend == 'module://ipympl.backend_nbagg':
236 if mplbackend == 'nbagg' or mplbackend == 'module://ipympl.backend_nbagg':
237 formatter = shell.display_formatter.ipython_display_formatter
237 formatter = shell.display_formatter.ipython_display_formatter
238 formatter.for_type(Figure, _reshow_nbagg_figure)
238 formatter.for_type(Figure, _reshow_nbagg_figure)
239
239
240 supported = {'png', 'png2x', 'retina', 'jpg', 'jpeg', 'svg', 'pdf'}
240 supported = {'png', 'png2x', 'retina', 'jpg', 'jpeg', 'svg', 'pdf'}
241 bad = formats.difference(supported)
241 bad = formats.difference(supported)
242 if bad:
242 if bad:
243 bs = "%s" % ','.join([repr(f) for f in bad])
243 bs = "%s" % ','.join([repr(f) for f in bad])
244 gs = "%s" % ','.join([repr(f) for f in supported])
244 gs = "%s" % ','.join([repr(f) for f in supported])
245 raise ValueError("supported formats are: %s not %s" % (gs, bs))
245 raise ValueError("supported formats are: %s not %s" % (gs, bs))
246
246
247 if 'png' in formats:
247 if 'png' in formats:
248 png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png', **kwargs))
248 png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png', **kwargs))
249 if 'retina' in formats or 'png2x' in formats:
249 if 'retina' in formats or 'png2x' in formats:
250 png_formatter.for_type(Figure, lambda fig: retina_figure(fig, **kwargs))
250 png_formatter.for_type(Figure, lambda fig: retina_figure(fig, **kwargs))
251 if 'jpg' in formats or 'jpeg' in formats:
251 if 'jpg' in formats or 'jpeg' in formats:
252 jpg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'jpg', **kwargs))
252 jpg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'jpg', **kwargs))
253 if 'svg' in formats:
253 if 'svg' in formats:
254 svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg', **kwargs))
254 svg_formatter.for_type(Figure, lambda fig: print_figure(fig, 'svg', **kwargs))
255 if 'pdf' in formats:
255 if 'pdf' in formats:
256 pdf_formatter.for_type(Figure, lambda fig: print_figure(fig, 'pdf', **kwargs))
256 pdf_formatter.for_type(Figure, lambda fig: print_figure(fig, 'pdf', **kwargs))
257
257
258 #-----------------------------------------------------------------------------
258 #-----------------------------------------------------------------------------
259 # Code for initializing matplotlib and importing pylab
259 # Code for initializing matplotlib and importing pylab
260 #-----------------------------------------------------------------------------
260 #-----------------------------------------------------------------------------
261
261
262
262
263 def find_gui_and_backend(gui=None, gui_select=None):
263 def find_gui_and_backend(gui=None, gui_select=None):
264 """Given a gui string return the gui and mpl backend.
264 """Given a gui string return the gui and mpl backend.
265
265
266 Parameters
266 Parameters
267 ----------
267 ----------
268 gui : str
268 gui : str
269 Can be one of ('tk','gtk','wx','qt','qt4','inline','agg').
269 Can be one of ('tk','gtk','wx','qt','qt4','inline','agg').
270 gui_select : str
270 gui_select : str
271 Can be one of ('tk','gtk','wx','qt','qt4','inline').
271 Can be one of ('tk','gtk','wx','qt','qt4','inline').
272 This is any gui already selected by the shell.
272 This is any gui already selected by the shell.
273
273
274 Returns
274 Returns
275 -------
275 -------
276 A tuple of (gui, backend) where backend is one of ('TkAgg','GTKAgg',
276 A tuple of (gui, backend) where backend is one of ('TkAgg','GTKAgg',
277 'WXAgg','Qt4Agg','module://ipykernel.pylab.backend_inline','agg').
277 'WXAgg','Qt4Agg','module://ipykernel.pylab.backend_inline','agg').
278 """
278 """
279
279
280 import matplotlib
280 import matplotlib
281
281
282 if gui and gui != 'auto':
282 if gui and gui != 'auto':
283 # select backend based on requested gui
283 # select backend based on requested gui
284 backend = backends[gui]
284 backend = backends[gui]
285 if gui == 'agg':
285 if gui == 'agg':
286 gui = None
286 gui = None
287 else:
287 else:
288 # We need to read the backend from the original data structure, *not*
288 # We need to read the backend from the original data structure, *not*
289 # from mpl.rcParams, since a prior invocation of %matplotlib may have
289 # from mpl.rcParams, since a prior invocation of %matplotlib may have
290 # overwritten that.
290 # overwritten that.
291 # WARNING: this assumes matplotlib 1.1 or newer!!
291 # WARNING: this assumes matplotlib 1.1 or newer!!
292 backend = matplotlib.rcParamsOrig['backend']
292 backend = matplotlib.rcParamsOrig['backend']
293 # In this case, we need to find what the appropriate gui selection call
293 # In this case, we need to find what the appropriate gui selection call
294 # should be for IPython, so we can activate inputhook accordingly
294 # should be for IPython, so we can activate inputhook accordingly
295 gui = backend2gui.get(backend, None)
295 gui = backend2gui.get(backend, None)
296
296
297 # If we have already had a gui active, we need it and inline are the
297 # If we have already had a gui active, we need it and inline are the
298 # ones allowed.
298 # ones allowed.
299 if gui_select and gui != gui_select:
299 if gui_select and gui != gui_select:
300 gui = gui_select
300 gui = gui_select
301 backend = backends[gui]
301 backend = backends[gui]
302
302
303 return gui, backend
303 return gui, backend
304
304
305
305
306 def activate_matplotlib(backend):
306 def activate_matplotlib(backend):
307 """Activate the given backend and set interactive to True."""
307 """Activate the given backend and set interactive to True."""
308
308
309 import matplotlib
309 import matplotlib
310 matplotlib.interactive(True)
310 matplotlib.interactive(True)
311
311
312 # Matplotlib had a bug where even switch_backend could not force
312 # Matplotlib had a bug where even switch_backend could not force
313 # the rcParam to update. This needs to be set *before* the module
313 # the rcParam to update. This needs to be set *before* the module
314 # magic of switch_backend().
314 # magic of switch_backend().
315 matplotlib.rcParams['backend'] = backend
315 matplotlib.rcParams['backend'] = backend
316
316
317 # Due to circular imports, pyplot may be only partially initialised
317 # Due to circular imports, pyplot may be only partially initialised
318 # when this function runs.
318 # when this function runs.
319 # So avoid needing matplotlib attribute-lookup to access pyplot.
319 # So avoid needing matplotlib attribute-lookup to access pyplot.
320 from matplotlib import pyplot as plt
320 from matplotlib import pyplot as plt
321
321
322 plt.switch_backend(backend)
322 plt.switch_backend(backend)
323
323
324 plt.show._needmain = False
324 plt.show._needmain = False
325 # We need to detect at runtime whether show() is called by the user.
325 # We need to detect at runtime whether show() is called by the user.
326 # For this, we wrap it into a decorator which adds a 'called' flag.
326 # For this, we wrap it into a decorator which adds a 'called' flag.
327 plt.draw_if_interactive = flag_calls(plt.draw_if_interactive)
327 plt.draw_if_interactive = flag_calls(plt.draw_if_interactive)
328
328
329
329
330 def import_pylab(user_ns, import_all=True):
330 def import_pylab(user_ns, import_all=True):
331 """Populate the namespace with pylab-related values.
331 """Populate the namespace with pylab-related values.
332
332
333 Imports matplotlib, pylab, numpy, and everything from pylab and numpy.
333 Imports matplotlib, pylab, numpy, and everything from pylab and numpy.
334
334
335 Also imports a few names from IPython (figsize, display, getfigs)
335 Also imports a few names from IPython (figsize, display, getfigs)
336
336
337 """
337 """
338
338
339 # Import numpy as np/pyplot as plt are conventions we're trying to
339 # Import numpy as np/pyplot as plt are conventions we're trying to
340 # somewhat standardize on. Making them available to users by default
340 # somewhat standardize on. Making them available to users by default
341 # will greatly help this.
341 # will greatly help this.
342 s = ("import numpy\n"
342 s = ("import numpy\n"
343 "import matplotlib\n"
343 "import matplotlib\n"
344 "from matplotlib import pylab, mlab, pyplot\n"
344 "from matplotlib import pylab, mlab, pyplot\n"
345 "np = numpy\n"
345 "np = numpy\n"
346 "plt = pyplot\n"
346 "plt = pyplot\n"
347 )
347 )
348 exec(s, user_ns)
348 exec(s, user_ns)
349
349
350 if import_all:
350 if import_all:
351 s = ("from matplotlib.pylab import *\n"
351 s = ("from matplotlib.pylab import *\n"
352 "from numpy import *\n")
352 "from numpy import *\n")
353 exec(s, user_ns)
353 exec(s, user_ns)
354
354
355 # IPython symbols to add
355 # IPython symbols to add
356 user_ns['figsize'] = figsize
356 user_ns['figsize'] = figsize
357 from IPython.display import display
357 from IPython.display import display
358 # Add display and getfigs to the user's namespace
358 # Add display and getfigs to the user's namespace
359 user_ns['display'] = display
359 user_ns['display'] = display
360 user_ns['getfigs'] = getfigs
360 user_ns['getfigs'] = getfigs
361
361
362
362
363 def configure_inline_support(shell, backend):
363 def configure_inline_support(shell, backend):
364 """Configure an IPython shell object for matplotlib use.
364 """Configure an IPython shell object for matplotlib use.
365
365
366 Parameters
366 Parameters
367 ----------
367 ----------
368 shell : InteractiveShell instance
368 shell : InteractiveShell instance
369
369
370 backend : matplotlib backend
370 backend : matplotlib backend
371 """
371 """
372 # If using our svg payload backend, register the post-execution
372 # If using our svg payload backend, register the post-execution
373 # function that will pick up the results for display. This can only be
373 # function that will pick up the results for display. This can only be
374 # done with access to the real shell object.
374 # done with access to the real shell object.
375
375
376 # Note: if we can't load the inline backend, then there's no point
376 # Note: if we can't load the inline backend, then there's no point
377 # continuing (such as in terminal-only shells in environments without
377 # continuing (such as in terminal-only shells in environments without
378 # zeromq available).
378 # zeromq available).
379 try:
379 try:
380 from ipykernel.pylab.backend_inline import InlineBackend
380 from ipykernel.pylab.backend_inline import InlineBackend
381 except ImportError:
381 except ImportError:
382 return
382 return
383 import matplotlib
383 import matplotlib
384
384
385 cfg = InlineBackend.instance(parent=shell)
385 cfg = InlineBackend.instance(parent=shell)
386 cfg.shell = shell
386 cfg.shell = shell
387 if cfg not in shell.configurables:
387 if cfg not in shell.configurables:
388 shell.configurables.append(cfg)
388 shell.configurables.append(cfg)
389
389
390 if backend == backends['inline']:
390 if backend == backends['inline']:
391 from ipykernel.pylab.backend_inline import flush_figures
391 from ipykernel.pylab.backend_inline import flush_figures
392 shell.events.register('post_execute', flush_figures)
392 shell.events.register('post_execute', flush_figures)
393
393
394 # Save rcParams that will be overwrittern
394 # Save rcParams that will be overwrittern
395 shell._saved_rcParams = {}
395 shell._saved_rcParams = {}
396 for k in cfg.rc:
396 for k in cfg.rc:
397 shell._saved_rcParams[k] = matplotlib.rcParams[k]
397 shell._saved_rcParams[k] = matplotlib.rcParams[k]
398 # load inline_rc
398 # load inline_rc
399 matplotlib.rcParams.update(cfg.rc)
399 matplotlib.rcParams.update(cfg.rc)
400 new_backend_name = "inline"
400 new_backend_name = "inline"
401 else:
401 else:
402 from ipykernel.pylab.backend_inline import flush_figures
402 from ipykernel.pylab.backend_inline import flush_figures
403 try:
403 try:
404 shell.events.unregister('post_execute', flush_figures)
404 shell.events.unregister('post_execute', flush_figures)
405 except ValueError:
405 except ValueError:
406 pass
406 pass
407 if hasattr(shell, '_saved_rcParams'):
407 if hasattr(shell, '_saved_rcParams'):
408 matplotlib.rcParams.update(shell._saved_rcParams)
408 matplotlib.rcParams.update(shell._saved_rcParams)
409 del shell._saved_rcParams
409 del shell._saved_rcParams
410 new_backend_name = "other"
410 new_backend_name = "other"
411
411
412 # only enable the formats once -> don't change the enabled formats (which the user may
412 # only enable the formats once -> don't change the enabled formats (which the user may
413 # has changed) when getting another "%matplotlib inline" call.
413 # has changed) when getting another "%matplotlib inline" call.
414 # See https://github.com/ipython/ipykernel/issues/29
414 # See https://github.com/ipython/ipykernel/issues/29
415 cur_backend = getattr(configure_inline_support, "current_backend", "unset")
415 cur_backend = getattr(configure_inline_support, "current_backend", "unset")
416 if new_backend_name != cur_backend:
416 if new_backend_name != cur_backend:
417 # Setup the default figure format
417 # Setup the default figure format
418 select_figure_formats(shell, cfg.figure_formats, **cfg.print_figure_kwargs)
418 select_figure_formats(shell, cfg.figure_formats, **cfg.print_figure_kwargs)
419 configure_inline_support.current_backend = new_backend_name
419 configure_inline_support.current_backend = new_backend_name
@@ -1,256 +1,256 b''
1 """Tests for pylab tools module.
1 """Tests for pylab tools module.
2 """
2 """
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
7
8 from io import UnsupportedOperation, BytesIO
8 from io import UnsupportedOperation, BytesIO
9
9
10 import matplotlib
10 import matplotlib
11 matplotlib.use('Agg')
11 matplotlib.use('Agg')
12 from matplotlib.figure import Figure
12 from matplotlib.figure import Figure
13
13
14 from nose import SkipTest
14 from nose import SkipTest
15 import nose.tools as nt
15 import nose.tools as nt
16
16
17 from matplotlib import pyplot as plt
17 from matplotlib import pyplot as plt
18 import numpy as np
18 import numpy as np
19
19
20 from IPython.core.getipython import get_ipython
20 from IPython.core.getipython import get_ipython
21 from IPython.core.interactiveshell import InteractiveShell
21 from IPython.core.interactiveshell import InteractiveShell
22 from IPython.core.display import _PNG, _JPEG
22 from IPython.core.display import _PNG, _JPEG
23 from .. import pylabtools as pt
23 from .. import pylabtools as pt
24
24
25 from IPython.testing import decorators as dec
25 from IPython.testing import decorators as dec
26
26
27
27
28 def test_figure_to_svg():
28 def test_figure_to_svg():
29 # simple empty-figure test
29 # simple empty-figure test
30 fig = plt.figure()
30 fig = plt.figure()
31 nt.assert_equal(pt.print_figure(fig, 'svg'), None)
31 nt.assert_equal(pt.print_figure(fig, 'svg'), None)
32
32
33 plt.close('all')
33 plt.close('all')
34
34
35 # simple check for at least svg-looking output
35 # simple check for at least svg-looking output
36 fig = plt.figure()
36 fig = plt.figure()
37 ax = fig.add_subplot(1,1,1)
37 ax = fig.add_subplot(1,1,1)
38 ax.plot([1,2,3])
38 ax.plot([1,2,3])
39 plt.draw()
39 plt.draw()
40 svg = pt.print_figure(fig, 'svg')[:100].lower()
40 svg = pt.print_figure(fig, 'svg')[:100].lower()
41 nt.assert_in(u'doctype svg', svg)
41 nt.assert_in(u'doctype svg', svg)
42
42
43 def _check_pil_jpeg_bytes():
43 def _check_pil_jpeg_bytes():
44 """Skip if PIL can't write JPEGs to BytesIO objects"""
44 """Skip if PIL can't write JPEGs to BytesIO objects"""
45 # PIL's JPEG plugin can't write to BytesIO objects
45 # PIL's JPEG plugin can't write to BytesIO objects
46 # Pillow fixes this
46 # Pillow fixes this
47 from PIL import Image
47 from PIL import Image
48 buf = BytesIO()
48 buf = BytesIO()
49 img = Image.new("RGB", (4,4))
49 img = Image.new("RGB", (4,4))
50 try:
50 try:
51 img.save(buf, 'jpeg')
51 img.save(buf, 'jpeg')
52 except Exception as e:
52 except Exception as e:
53 ename = e.__class__.__name__
53 ename = e.__class__.__name__
54 raise SkipTest("PIL can't write JPEG to BytesIO: %s: %s" % (ename, e))
54 raise SkipTest("PIL can't write JPEG to BytesIO: %s: %s" % (ename, e)) from e
55
55
56 @dec.skip_without("PIL.Image")
56 @dec.skip_without("PIL.Image")
57 def test_figure_to_jpeg():
57 def test_figure_to_jpeg():
58 _check_pil_jpeg_bytes()
58 _check_pil_jpeg_bytes()
59 # simple check for at least jpeg-looking output
59 # simple check for at least jpeg-looking output
60 fig = plt.figure()
60 fig = plt.figure()
61 ax = fig.add_subplot(1,1,1)
61 ax = fig.add_subplot(1,1,1)
62 ax.plot([1,2,3])
62 ax.plot([1,2,3])
63 plt.draw()
63 plt.draw()
64 jpeg = pt.print_figure(fig, 'jpeg', pil_kwargs={'optimize': 50})[:100].lower()
64 jpeg = pt.print_figure(fig, 'jpeg', pil_kwargs={'optimize': 50})[:100].lower()
65 assert jpeg.startswith(_JPEG)
65 assert jpeg.startswith(_JPEG)
66
66
67 def test_retina_figure():
67 def test_retina_figure():
68 # simple empty-figure test
68 # simple empty-figure test
69 fig = plt.figure()
69 fig = plt.figure()
70 nt.assert_equal(pt.retina_figure(fig), None)
70 nt.assert_equal(pt.retina_figure(fig), None)
71 plt.close('all')
71 plt.close('all')
72
72
73 fig = plt.figure()
73 fig = plt.figure()
74 ax = fig.add_subplot(1,1,1)
74 ax = fig.add_subplot(1,1,1)
75 ax.plot([1,2,3])
75 ax.plot([1,2,3])
76 plt.draw()
76 plt.draw()
77 png, md = pt.retina_figure(fig)
77 png, md = pt.retina_figure(fig)
78 assert png.startswith(_PNG)
78 assert png.startswith(_PNG)
79 nt.assert_in('width', md)
79 nt.assert_in('width', md)
80 nt.assert_in('height', md)
80 nt.assert_in('height', md)
81
81
82 _fmt_mime_map = {
82 _fmt_mime_map = {
83 'png': 'image/png',
83 'png': 'image/png',
84 'jpeg': 'image/jpeg',
84 'jpeg': 'image/jpeg',
85 'pdf': 'application/pdf',
85 'pdf': 'application/pdf',
86 'retina': 'image/png',
86 'retina': 'image/png',
87 'svg': 'image/svg+xml',
87 'svg': 'image/svg+xml',
88 }
88 }
89
89
90 def test_select_figure_formats_str():
90 def test_select_figure_formats_str():
91 ip = get_ipython()
91 ip = get_ipython()
92 for fmt, active_mime in _fmt_mime_map.items():
92 for fmt, active_mime in _fmt_mime_map.items():
93 pt.select_figure_formats(ip, fmt)
93 pt.select_figure_formats(ip, fmt)
94 for mime, f in ip.display_formatter.formatters.items():
94 for mime, f in ip.display_formatter.formatters.items():
95 if mime == active_mime:
95 if mime == active_mime:
96 nt.assert_in(Figure, f)
96 nt.assert_in(Figure, f)
97 else:
97 else:
98 nt.assert_not_in(Figure, f)
98 nt.assert_not_in(Figure, f)
99
99
100 def test_select_figure_formats_kwargs():
100 def test_select_figure_formats_kwargs():
101 ip = get_ipython()
101 ip = get_ipython()
102 kwargs = dict(quality=10, bbox_inches='tight')
102 kwargs = dict(quality=10, bbox_inches='tight')
103 pt.select_figure_formats(ip, 'png', **kwargs)
103 pt.select_figure_formats(ip, 'png', **kwargs)
104 formatter = ip.display_formatter.formatters['image/png']
104 formatter = ip.display_formatter.formatters['image/png']
105 f = formatter.lookup_by_type(Figure)
105 f = formatter.lookup_by_type(Figure)
106 cell = f.__closure__[0].cell_contents
106 cell = f.__closure__[0].cell_contents
107 nt.assert_equal(cell, kwargs)
107 nt.assert_equal(cell, kwargs)
108
108
109 # check that the formatter doesn't raise
109 # check that the formatter doesn't raise
110 fig = plt.figure()
110 fig = plt.figure()
111 ax = fig.add_subplot(1,1,1)
111 ax = fig.add_subplot(1,1,1)
112 ax.plot([1,2,3])
112 ax.plot([1,2,3])
113 plt.draw()
113 plt.draw()
114 formatter.enabled = True
114 formatter.enabled = True
115 png = formatter(fig)
115 png = formatter(fig)
116 assert png.startswith(_PNG)
116 assert png.startswith(_PNG)
117
117
118 def test_select_figure_formats_set():
118 def test_select_figure_formats_set():
119 ip = get_ipython()
119 ip = get_ipython()
120 for fmts in [
120 for fmts in [
121 {'png', 'svg'},
121 {'png', 'svg'},
122 ['png'],
122 ['png'],
123 ('jpeg', 'pdf', 'retina'),
123 ('jpeg', 'pdf', 'retina'),
124 {'svg'},
124 {'svg'},
125 ]:
125 ]:
126 active_mimes = {_fmt_mime_map[fmt] for fmt in fmts}
126 active_mimes = {_fmt_mime_map[fmt] for fmt in fmts}
127 pt.select_figure_formats(ip, fmts)
127 pt.select_figure_formats(ip, fmts)
128 for mime, f in ip.display_formatter.formatters.items():
128 for mime, f in ip.display_formatter.formatters.items():
129 if mime in active_mimes:
129 if mime in active_mimes:
130 nt.assert_in(Figure, f)
130 nt.assert_in(Figure, f)
131 else:
131 else:
132 nt.assert_not_in(Figure, f)
132 nt.assert_not_in(Figure, f)
133
133
134 def test_select_figure_formats_bad():
134 def test_select_figure_formats_bad():
135 ip = get_ipython()
135 ip = get_ipython()
136 with nt.assert_raises(ValueError):
136 with nt.assert_raises(ValueError):
137 pt.select_figure_formats(ip, 'foo')
137 pt.select_figure_formats(ip, 'foo')
138 with nt.assert_raises(ValueError):
138 with nt.assert_raises(ValueError):
139 pt.select_figure_formats(ip, {'png', 'foo'})
139 pt.select_figure_formats(ip, {'png', 'foo'})
140 with nt.assert_raises(ValueError):
140 with nt.assert_raises(ValueError):
141 pt.select_figure_formats(ip, ['retina', 'pdf', 'bar', 'bad'])
141 pt.select_figure_formats(ip, ['retina', 'pdf', 'bar', 'bad'])
142
142
143 def test_import_pylab():
143 def test_import_pylab():
144 ns = {}
144 ns = {}
145 pt.import_pylab(ns, import_all=False)
145 pt.import_pylab(ns, import_all=False)
146 nt.assert_true('plt' in ns)
146 nt.assert_true('plt' in ns)
147 nt.assert_equal(ns['np'], np)
147 nt.assert_equal(ns['np'], np)
148
148
149 class TestPylabSwitch(object):
149 class TestPylabSwitch(object):
150 class Shell(InteractiveShell):
150 class Shell(InteractiveShell):
151 def enable_gui(self, gui):
151 def enable_gui(self, gui):
152 pass
152 pass
153
153
154 def setup(self):
154 def setup(self):
155 import matplotlib
155 import matplotlib
156 def act_mpl(backend):
156 def act_mpl(backend):
157 matplotlib.rcParams['backend'] = backend
157 matplotlib.rcParams['backend'] = backend
158
158
159 # Save rcParams since they get modified
159 # Save rcParams since they get modified
160 self._saved_rcParams = matplotlib.rcParams
160 self._saved_rcParams = matplotlib.rcParams
161 self._saved_rcParamsOrig = matplotlib.rcParamsOrig
161 self._saved_rcParamsOrig = matplotlib.rcParamsOrig
162 matplotlib.rcParams = dict(backend='Qt4Agg')
162 matplotlib.rcParams = dict(backend='Qt4Agg')
163 matplotlib.rcParamsOrig = dict(backend='Qt4Agg')
163 matplotlib.rcParamsOrig = dict(backend='Qt4Agg')
164
164
165 # Mock out functions
165 # Mock out functions
166 self._save_am = pt.activate_matplotlib
166 self._save_am = pt.activate_matplotlib
167 pt.activate_matplotlib = act_mpl
167 pt.activate_matplotlib = act_mpl
168 self._save_ip = pt.import_pylab
168 self._save_ip = pt.import_pylab
169 pt.import_pylab = lambda *a,**kw:None
169 pt.import_pylab = lambda *a,**kw:None
170 self._save_cis = pt.configure_inline_support
170 self._save_cis = pt.configure_inline_support
171 pt.configure_inline_support = lambda *a,**kw:None
171 pt.configure_inline_support = lambda *a,**kw:None
172
172
173 def teardown(self):
173 def teardown(self):
174 pt.activate_matplotlib = self._save_am
174 pt.activate_matplotlib = self._save_am
175 pt.import_pylab = self._save_ip
175 pt.import_pylab = self._save_ip
176 pt.configure_inline_support = self._save_cis
176 pt.configure_inline_support = self._save_cis
177 import matplotlib
177 import matplotlib
178 matplotlib.rcParams = self._saved_rcParams
178 matplotlib.rcParams = self._saved_rcParams
179 matplotlib.rcParamsOrig = self._saved_rcParamsOrig
179 matplotlib.rcParamsOrig = self._saved_rcParamsOrig
180
180
181 def test_qt(self):
181 def test_qt(self):
182 s = self.Shell()
182 s = self.Shell()
183 gui, backend = s.enable_matplotlib(None)
183 gui, backend = s.enable_matplotlib(None)
184 nt.assert_equal(gui, 'qt')
184 nt.assert_equal(gui, 'qt')
185 nt.assert_equal(s.pylab_gui_select, 'qt')
185 nt.assert_equal(s.pylab_gui_select, 'qt')
186
186
187 gui, backend = s.enable_matplotlib('inline')
187 gui, backend = s.enable_matplotlib('inline')
188 nt.assert_equal(gui, 'inline')
188 nt.assert_equal(gui, 'inline')
189 nt.assert_equal(s.pylab_gui_select, 'qt')
189 nt.assert_equal(s.pylab_gui_select, 'qt')
190
190
191 gui, backend = s.enable_matplotlib('qt')
191 gui, backend = s.enable_matplotlib('qt')
192 nt.assert_equal(gui, 'qt')
192 nt.assert_equal(gui, 'qt')
193 nt.assert_equal(s.pylab_gui_select, 'qt')
193 nt.assert_equal(s.pylab_gui_select, 'qt')
194
194
195 gui, backend = s.enable_matplotlib('inline')
195 gui, backend = s.enable_matplotlib('inline')
196 nt.assert_equal(gui, 'inline')
196 nt.assert_equal(gui, 'inline')
197 nt.assert_equal(s.pylab_gui_select, 'qt')
197 nt.assert_equal(s.pylab_gui_select, 'qt')
198
198
199 gui, backend = s.enable_matplotlib()
199 gui, backend = s.enable_matplotlib()
200 nt.assert_equal(gui, 'qt')
200 nt.assert_equal(gui, 'qt')
201 nt.assert_equal(s.pylab_gui_select, 'qt')
201 nt.assert_equal(s.pylab_gui_select, 'qt')
202
202
203 def test_inline(self):
203 def test_inline(self):
204 s = self.Shell()
204 s = self.Shell()
205 gui, backend = s.enable_matplotlib('inline')
205 gui, backend = s.enable_matplotlib('inline')
206 nt.assert_equal(gui, 'inline')
206 nt.assert_equal(gui, 'inline')
207 nt.assert_equal(s.pylab_gui_select, None)
207 nt.assert_equal(s.pylab_gui_select, None)
208
208
209 gui, backend = s.enable_matplotlib('inline')
209 gui, backend = s.enable_matplotlib('inline')
210 nt.assert_equal(gui, 'inline')
210 nt.assert_equal(gui, 'inline')
211 nt.assert_equal(s.pylab_gui_select, None)
211 nt.assert_equal(s.pylab_gui_select, None)
212
212
213 gui, backend = s.enable_matplotlib('qt')
213 gui, backend = s.enable_matplotlib('qt')
214 nt.assert_equal(gui, 'qt')
214 nt.assert_equal(gui, 'qt')
215 nt.assert_equal(s.pylab_gui_select, 'qt')
215 nt.assert_equal(s.pylab_gui_select, 'qt')
216
216
217 def test_inline_twice(self):
217 def test_inline_twice(self):
218 "Using '%matplotlib inline' twice should not reset formatters"
218 "Using '%matplotlib inline' twice should not reset formatters"
219
219
220 ip = self.Shell()
220 ip = self.Shell()
221 gui, backend = ip.enable_matplotlib('inline')
221 gui, backend = ip.enable_matplotlib('inline')
222 nt.assert_equal(gui, 'inline')
222 nt.assert_equal(gui, 'inline')
223
223
224 fmts = {'png'}
224 fmts = {'png'}
225 active_mimes = {_fmt_mime_map[fmt] for fmt in fmts}
225 active_mimes = {_fmt_mime_map[fmt] for fmt in fmts}
226 pt.select_figure_formats(ip, fmts)
226 pt.select_figure_formats(ip, fmts)
227
227
228 gui, backend = ip.enable_matplotlib('inline')
228 gui, backend = ip.enable_matplotlib('inline')
229 nt.assert_equal(gui, 'inline')
229 nt.assert_equal(gui, 'inline')
230
230
231 for mime, f in ip.display_formatter.formatters.items():
231 for mime, f in ip.display_formatter.formatters.items():
232 if mime in active_mimes:
232 if mime in active_mimes:
233 nt.assert_in(Figure, f)
233 nt.assert_in(Figure, f)
234 else:
234 else:
235 nt.assert_not_in(Figure, f)
235 nt.assert_not_in(Figure, f)
236
236
237 def test_qt_gtk(self):
237 def test_qt_gtk(self):
238 s = self.Shell()
238 s = self.Shell()
239 gui, backend = s.enable_matplotlib('qt')
239 gui, backend = s.enable_matplotlib('qt')
240 nt.assert_equal(gui, 'qt')
240 nt.assert_equal(gui, 'qt')
241 nt.assert_equal(s.pylab_gui_select, 'qt')
241 nt.assert_equal(s.pylab_gui_select, 'qt')
242
242
243 gui, backend = s.enable_matplotlib('gtk')
243 gui, backend = s.enable_matplotlib('gtk')
244 nt.assert_equal(gui, 'qt')
244 nt.assert_equal(gui, 'qt')
245 nt.assert_equal(s.pylab_gui_select, 'qt')
245 nt.assert_equal(s.pylab_gui_select, 'qt')
246
246
247
247
248 def test_no_gui_backends():
248 def test_no_gui_backends():
249 for k in ['agg', 'svg', 'pdf', 'ps']:
249 for k in ['agg', 'svg', 'pdf', 'ps']:
250 assert k not in pt.backend2gui
250 assert k not in pt.backend2gui
251
251
252
252
253 def test_figure_no_canvas():
253 def test_figure_no_canvas():
254 fig = Figure()
254 fig = Figure()
255 fig.canvas = None
255 fig.canvas = None
256 pt.print_figure(fig)
256 pt.print_figure(fig)
@@ -1,601 +1,601 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """Tests for code execution (%run and related), which is particularly tricky.
2 """Tests for code execution (%run and related), which is particularly tricky.
3
3
4 Because of how %run manages namespaces, and the fact that we are trying here to
4 Because of how %run manages namespaces, and the fact that we are trying here to
5 verify subtle object deletion and reference counting issues, the %run tests
5 verify subtle object deletion and reference counting issues, the %run tests
6 will be kept in this separate file. This makes it easier to aggregate in one
6 will be kept in this separate file. This makes it easier to aggregate in one
7 place the tricks needed to handle it; most other magics are much easier to test
7 place the tricks needed to handle it; most other magics are much easier to test
8 and we do so in a common test_magic file.
8 and we do so in a common test_magic file.
9
9
10 Note that any test using `run -i` should make sure to do a `reset` afterwards,
10 Note that any test using `run -i` should make sure to do a `reset` afterwards,
11 as otherwise it may influence later tests.
11 as otherwise it may influence later tests.
12 """
12 """
13
13
14 # Copyright (c) IPython Development Team.
14 # Copyright (c) IPython Development Team.
15 # Distributed under the terms of the Modified BSD License.
15 # Distributed under the terms of the Modified BSD License.
16
16
17
17
18
18
19 import functools
19 import functools
20 import os
20 import os
21 from os.path import join as pjoin
21 from os.path import join as pjoin
22 import random
22 import random
23 import string
23 import string
24 import sys
24 import sys
25 import textwrap
25 import textwrap
26 import unittest
26 import unittest
27 from unittest.mock import patch
27 from unittest.mock import patch
28
28
29 import nose.tools as nt
29 import nose.tools as nt
30 from nose import SkipTest
30 from nose import SkipTest
31
31
32 from IPython.testing import decorators as dec
32 from IPython.testing import decorators as dec
33 from IPython.testing import tools as tt
33 from IPython.testing import tools as tt
34 from IPython.utils.io import capture_output
34 from IPython.utils.io import capture_output
35 from IPython.utils.tempdir import TemporaryDirectory
35 from IPython.utils.tempdir import TemporaryDirectory
36 from IPython.core import debugger
36 from IPython.core import debugger
37
37
38 def doctest_refbug():
38 def doctest_refbug():
39 """Very nasty problem with references held by multiple runs of a script.
39 """Very nasty problem with references held by multiple runs of a script.
40 See: https://github.com/ipython/ipython/issues/141
40 See: https://github.com/ipython/ipython/issues/141
41
41
42 In [1]: _ip.clear_main_mod_cache()
42 In [1]: _ip.clear_main_mod_cache()
43 # random
43 # random
44
44
45 In [2]: %run refbug
45 In [2]: %run refbug
46
46
47 In [3]: call_f()
47 In [3]: call_f()
48 lowercased: hello
48 lowercased: hello
49
49
50 In [4]: %run refbug
50 In [4]: %run refbug
51
51
52 In [5]: call_f()
52 In [5]: call_f()
53 lowercased: hello
53 lowercased: hello
54 lowercased: hello
54 lowercased: hello
55 """
55 """
56
56
57
57
58 def doctest_run_builtins():
58 def doctest_run_builtins():
59 r"""Check that %run doesn't damage __builtins__.
59 r"""Check that %run doesn't damage __builtins__.
60
60
61 In [1]: import tempfile
61 In [1]: import tempfile
62
62
63 In [2]: bid1 = id(__builtins__)
63 In [2]: bid1 = id(__builtins__)
64
64
65 In [3]: fname = tempfile.mkstemp('.py')[1]
65 In [3]: fname = tempfile.mkstemp('.py')[1]
66
66
67 In [3]: f = open(fname,'w')
67 In [3]: f = open(fname,'w')
68
68
69 In [4]: dummy= f.write('pass\n')
69 In [4]: dummy= f.write('pass\n')
70
70
71 In [5]: f.flush()
71 In [5]: f.flush()
72
72
73 In [6]: t1 = type(__builtins__)
73 In [6]: t1 = type(__builtins__)
74
74
75 In [7]: %run $fname
75 In [7]: %run $fname
76
76
77 In [7]: f.close()
77 In [7]: f.close()
78
78
79 In [8]: bid2 = id(__builtins__)
79 In [8]: bid2 = id(__builtins__)
80
80
81 In [9]: t2 = type(__builtins__)
81 In [9]: t2 = type(__builtins__)
82
82
83 In [10]: t1 == t2
83 In [10]: t1 == t2
84 Out[10]: True
84 Out[10]: True
85
85
86 In [10]: bid1 == bid2
86 In [10]: bid1 == bid2
87 Out[10]: True
87 Out[10]: True
88
88
89 In [12]: try:
89 In [12]: try:
90 ....: os.unlink(fname)
90 ....: os.unlink(fname)
91 ....: except:
91 ....: except:
92 ....: pass
92 ....: pass
93 ....:
93 ....:
94 """
94 """
95
95
96
96
97 def doctest_run_option_parser():
97 def doctest_run_option_parser():
98 r"""Test option parser in %run.
98 r"""Test option parser in %run.
99
99
100 In [1]: %run print_argv.py
100 In [1]: %run print_argv.py
101 []
101 []
102
102
103 In [2]: %run print_argv.py print*.py
103 In [2]: %run print_argv.py print*.py
104 ['print_argv.py']
104 ['print_argv.py']
105
105
106 In [3]: %run -G print_argv.py print*.py
106 In [3]: %run -G print_argv.py print*.py
107 ['print*.py']
107 ['print*.py']
108
108
109 """
109 """
110
110
111
111
112 @dec.skip_win32
112 @dec.skip_win32
113 def doctest_run_option_parser_for_posix():
113 def doctest_run_option_parser_for_posix():
114 r"""Test option parser in %run (Linux/OSX specific).
114 r"""Test option parser in %run (Linux/OSX specific).
115
115
116 You need double quote to escape glob in POSIX systems:
116 You need double quote to escape glob in POSIX systems:
117
117
118 In [1]: %run print_argv.py print\\*.py
118 In [1]: %run print_argv.py print\\*.py
119 ['print*.py']
119 ['print*.py']
120
120
121 You can't use quote to escape glob in POSIX systems:
121 You can't use quote to escape glob in POSIX systems:
122
122
123 In [2]: %run print_argv.py 'print*.py'
123 In [2]: %run print_argv.py 'print*.py'
124 ['print_argv.py']
124 ['print_argv.py']
125
125
126 """
126 """
127
127
128
128
129 @dec.skip_if_not_win32
129 @dec.skip_if_not_win32
130 def doctest_run_option_parser_for_windows():
130 def doctest_run_option_parser_for_windows():
131 r"""Test option parser in %run (Windows specific).
131 r"""Test option parser in %run (Windows specific).
132
132
133 In Windows, you can't escape ``*` `by backslash:
133 In Windows, you can't escape ``*` `by backslash:
134
134
135 In [1]: %run print_argv.py print\\*.py
135 In [1]: %run print_argv.py print\\*.py
136 ['print\\*.py']
136 ['print\\*.py']
137
137
138 You can use quote to escape glob:
138 You can use quote to escape glob:
139
139
140 In [2]: %run print_argv.py 'print*.py'
140 In [2]: %run print_argv.py 'print*.py'
141 ['print*.py']
141 ['print*.py']
142
142
143 """
143 """
144
144
145
145
146 def doctest_reset_del():
146 def doctest_reset_del():
147 """Test that resetting doesn't cause errors in __del__ methods.
147 """Test that resetting doesn't cause errors in __del__ methods.
148
148
149 In [2]: class A(object):
149 In [2]: class A(object):
150 ...: def __del__(self):
150 ...: def __del__(self):
151 ...: print(str("Hi"))
151 ...: print(str("Hi"))
152 ...:
152 ...:
153
153
154 In [3]: a = A()
154 In [3]: a = A()
155
155
156 In [4]: get_ipython().reset()
156 In [4]: get_ipython().reset()
157 Hi
157 Hi
158
158
159 In [5]: 1+1
159 In [5]: 1+1
160 Out[5]: 2
160 Out[5]: 2
161 """
161 """
162
162
163 # For some tests, it will be handy to organize them in a class with a common
163 # For some tests, it will be handy to organize them in a class with a common
164 # setup that makes a temp file
164 # setup that makes a temp file
165
165
166 class TestMagicRunPass(tt.TempFileMixin):
166 class TestMagicRunPass(tt.TempFileMixin):
167
167
168 def setUp(self):
168 def setUp(self):
169 content = "a = [1,2,3]\nb = 1"
169 content = "a = [1,2,3]\nb = 1"
170 self.mktmp(content)
170 self.mktmp(content)
171
171
172 def run_tmpfile(self):
172 def run_tmpfile(self):
173 _ip = get_ipython()
173 _ip = get_ipython()
174 # This fails on Windows if self.tmpfile.name has spaces or "~" in it.
174 # This fails on Windows if self.tmpfile.name has spaces or "~" in it.
175 # See below and ticket https://bugs.launchpad.net/bugs/366353
175 # See below and ticket https://bugs.launchpad.net/bugs/366353
176 _ip.magic('run %s' % self.fname)
176 _ip.magic('run %s' % self.fname)
177
177
178 def run_tmpfile_p(self):
178 def run_tmpfile_p(self):
179 _ip = get_ipython()
179 _ip = get_ipython()
180 # This fails on Windows if self.tmpfile.name has spaces or "~" in it.
180 # This fails on Windows if self.tmpfile.name has spaces or "~" in it.
181 # See below and ticket https://bugs.launchpad.net/bugs/366353
181 # See below and ticket https://bugs.launchpad.net/bugs/366353
182 _ip.magic('run -p %s' % self.fname)
182 _ip.magic('run -p %s' % self.fname)
183
183
184 def test_builtins_id(self):
184 def test_builtins_id(self):
185 """Check that %run doesn't damage __builtins__ """
185 """Check that %run doesn't damage __builtins__ """
186 _ip = get_ipython()
186 _ip = get_ipython()
187 # Test that the id of __builtins__ is not modified by %run
187 # Test that the id of __builtins__ is not modified by %run
188 bid1 = id(_ip.user_ns['__builtins__'])
188 bid1 = id(_ip.user_ns['__builtins__'])
189 self.run_tmpfile()
189 self.run_tmpfile()
190 bid2 = id(_ip.user_ns['__builtins__'])
190 bid2 = id(_ip.user_ns['__builtins__'])
191 nt.assert_equal(bid1, bid2)
191 nt.assert_equal(bid1, bid2)
192
192
193 def test_builtins_type(self):
193 def test_builtins_type(self):
194 """Check that the type of __builtins__ doesn't change with %run.
194 """Check that the type of __builtins__ doesn't change with %run.
195
195
196 However, the above could pass if __builtins__ was already modified to
196 However, the above could pass if __builtins__ was already modified to
197 be a dict (it should be a module) by a previous use of %run. So we
197 be a dict (it should be a module) by a previous use of %run. So we
198 also check explicitly that it really is a module:
198 also check explicitly that it really is a module:
199 """
199 """
200 _ip = get_ipython()
200 _ip = get_ipython()
201 self.run_tmpfile()
201 self.run_tmpfile()
202 nt.assert_equal(type(_ip.user_ns['__builtins__']),type(sys))
202 nt.assert_equal(type(_ip.user_ns['__builtins__']),type(sys))
203
203
204 def test_run_profile( self ):
204 def test_run_profile( self ):
205 """Test that the option -p, which invokes the profiler, do not
205 """Test that the option -p, which invokes the profiler, do not
206 crash by invoking execfile"""
206 crash by invoking execfile"""
207 self.run_tmpfile_p()
207 self.run_tmpfile_p()
208
208
209 def test_run_debug_twice(self):
209 def test_run_debug_twice(self):
210 # https://github.com/ipython/ipython/issues/10028
210 # https://github.com/ipython/ipython/issues/10028
211 _ip = get_ipython()
211 _ip = get_ipython()
212 with tt.fake_input(['c']):
212 with tt.fake_input(['c']):
213 _ip.magic('run -d %s' % self.fname)
213 _ip.magic('run -d %s' % self.fname)
214 with tt.fake_input(['c']):
214 with tt.fake_input(['c']):
215 _ip.magic('run -d %s' % self.fname)
215 _ip.magic('run -d %s' % self.fname)
216
216
217 def test_run_debug_twice_with_breakpoint(self):
217 def test_run_debug_twice_with_breakpoint(self):
218 """Make a valid python temp file."""
218 """Make a valid python temp file."""
219 _ip = get_ipython()
219 _ip = get_ipython()
220 with tt.fake_input(['b 2', 'c', 'c']):
220 with tt.fake_input(['b 2', 'c', 'c']):
221 _ip.magic('run -d %s' % self.fname)
221 _ip.magic('run -d %s' % self.fname)
222
222
223 with tt.fake_input(['c']):
223 with tt.fake_input(['c']):
224 with tt.AssertNotPrints('KeyError'):
224 with tt.AssertNotPrints('KeyError'):
225 _ip.magic('run -d %s' % self.fname)
225 _ip.magic('run -d %s' % self.fname)
226
226
227
227
228 class TestMagicRunSimple(tt.TempFileMixin):
228 class TestMagicRunSimple(tt.TempFileMixin):
229
229
230 def test_simpledef(self):
230 def test_simpledef(self):
231 """Test that simple class definitions work."""
231 """Test that simple class definitions work."""
232 src = ("class foo: pass\n"
232 src = ("class foo: pass\n"
233 "def f(): return foo()")
233 "def f(): return foo()")
234 self.mktmp(src)
234 self.mktmp(src)
235 _ip.magic('run %s' % self.fname)
235 _ip.magic('run %s' % self.fname)
236 _ip.run_cell('t = isinstance(f(), foo)')
236 _ip.run_cell('t = isinstance(f(), foo)')
237 nt.assert_true(_ip.user_ns['t'])
237 nt.assert_true(_ip.user_ns['t'])
238
238
239 def test_obj_del(self):
239 def test_obj_del(self):
240 """Test that object's __del__ methods are called on exit."""
240 """Test that object's __del__ methods are called on exit."""
241 if sys.platform == 'win32':
241 if sys.platform == 'win32':
242 try:
242 try:
243 import win32api
243 import win32api
244 except ImportError:
244 except ImportError as e:
245 raise SkipTest("Test requires pywin32")
245 raise SkipTest("Test requires pywin32") from e
246 src = ("class A(object):\n"
246 src = ("class A(object):\n"
247 " def __del__(self):\n"
247 " def __del__(self):\n"
248 " print('object A deleted')\n"
248 " print('object A deleted')\n"
249 "a = A()\n")
249 "a = A()\n")
250 self.mktmp(src)
250 self.mktmp(src)
251 err = None
251 err = None
252 tt.ipexec_validate(self.fname, 'object A deleted', err)
252 tt.ipexec_validate(self.fname, 'object A deleted', err)
253
253
254 def test_aggressive_namespace_cleanup(self):
254 def test_aggressive_namespace_cleanup(self):
255 """Test that namespace cleanup is not too aggressive GH-238
255 """Test that namespace cleanup is not too aggressive GH-238
256
256
257 Returning from another run magic deletes the namespace"""
257 Returning from another run magic deletes the namespace"""
258 # see ticket https://github.com/ipython/ipython/issues/238
258 # see ticket https://github.com/ipython/ipython/issues/238
259
259
260 with tt.TempFileMixin() as empty:
260 with tt.TempFileMixin() as empty:
261 empty.mktmp('')
261 empty.mktmp('')
262 # On Windows, the filename will have \users in it, so we need to use the
262 # On Windows, the filename will have \users in it, so we need to use the
263 # repr so that the \u becomes \\u.
263 # repr so that the \u becomes \\u.
264 src = ("ip = get_ipython()\n"
264 src = ("ip = get_ipython()\n"
265 "for i in range(5):\n"
265 "for i in range(5):\n"
266 " try:\n"
266 " try:\n"
267 " ip.magic(%r)\n"
267 " ip.magic(%r)\n"
268 " except NameError as e:\n"
268 " except NameError as e:\n"
269 " print(i)\n"
269 " print(i)\n"
270 " break\n" % ('run ' + empty.fname))
270 " break\n" % ('run ' + empty.fname))
271 self.mktmp(src)
271 self.mktmp(src)
272 _ip.magic('run %s' % self.fname)
272 _ip.magic('run %s' % self.fname)
273 _ip.run_cell('ip == get_ipython()')
273 _ip.run_cell('ip == get_ipython()')
274 nt.assert_equal(_ip.user_ns['i'], 4)
274 nt.assert_equal(_ip.user_ns['i'], 4)
275
275
276 def test_run_second(self):
276 def test_run_second(self):
277 """Test that running a second file doesn't clobber the first, gh-3547
277 """Test that running a second file doesn't clobber the first, gh-3547
278 """
278 """
279 self.mktmp("avar = 1\n"
279 self.mktmp("avar = 1\n"
280 "def afunc():\n"
280 "def afunc():\n"
281 " return avar\n")
281 " return avar\n")
282
282
283 with tt.TempFileMixin() as empty:
283 with tt.TempFileMixin() as empty:
284 empty.mktmp("")
284 empty.mktmp("")
285
285
286 _ip.magic('run %s' % self.fname)
286 _ip.magic('run %s' % self.fname)
287 _ip.magic('run %s' % empty.fname)
287 _ip.magic('run %s' % empty.fname)
288 nt.assert_equal(_ip.user_ns['afunc'](), 1)
288 nt.assert_equal(_ip.user_ns['afunc'](), 1)
289
289
290 @dec.skip_win32
290 @dec.skip_win32
291 def test_tclass(self):
291 def test_tclass(self):
292 mydir = os.path.dirname(__file__)
292 mydir = os.path.dirname(__file__)
293 tc = os.path.join(mydir, 'tclass')
293 tc = os.path.join(mydir, 'tclass')
294 src = ("%%run '%s' C-first\n"
294 src = ("%%run '%s' C-first\n"
295 "%%run '%s' C-second\n"
295 "%%run '%s' C-second\n"
296 "%%run '%s' C-third\n") % (tc, tc, tc)
296 "%%run '%s' C-third\n") % (tc, tc, tc)
297 self.mktmp(src, '.ipy')
297 self.mktmp(src, '.ipy')
298 out = """\
298 out = """\
299 ARGV 1-: ['C-first']
299 ARGV 1-: ['C-first']
300 ARGV 1-: ['C-second']
300 ARGV 1-: ['C-second']
301 tclass.py: deleting object: C-first
301 tclass.py: deleting object: C-first
302 ARGV 1-: ['C-third']
302 ARGV 1-: ['C-third']
303 tclass.py: deleting object: C-second
303 tclass.py: deleting object: C-second
304 tclass.py: deleting object: C-third
304 tclass.py: deleting object: C-third
305 """
305 """
306 err = None
306 err = None
307 tt.ipexec_validate(self.fname, out, err)
307 tt.ipexec_validate(self.fname, out, err)
308
308
309 def test_run_i_after_reset(self):
309 def test_run_i_after_reset(self):
310 """Check that %run -i still works after %reset (gh-693)"""
310 """Check that %run -i still works after %reset (gh-693)"""
311 src = "yy = zz\n"
311 src = "yy = zz\n"
312 self.mktmp(src)
312 self.mktmp(src)
313 _ip.run_cell("zz = 23")
313 _ip.run_cell("zz = 23")
314 try:
314 try:
315 _ip.magic('run -i %s' % self.fname)
315 _ip.magic('run -i %s' % self.fname)
316 nt.assert_equal(_ip.user_ns['yy'], 23)
316 nt.assert_equal(_ip.user_ns['yy'], 23)
317 finally:
317 finally:
318 _ip.magic('reset -f')
318 _ip.magic('reset -f')
319
319
320 _ip.run_cell("zz = 23")
320 _ip.run_cell("zz = 23")
321 try:
321 try:
322 _ip.magic('run -i %s' % self.fname)
322 _ip.magic('run -i %s' % self.fname)
323 nt.assert_equal(_ip.user_ns['yy'], 23)
323 nt.assert_equal(_ip.user_ns['yy'], 23)
324 finally:
324 finally:
325 _ip.magic('reset -f')
325 _ip.magic('reset -f')
326
326
327 def test_unicode(self):
327 def test_unicode(self):
328 """Check that files in odd encodings are accepted."""
328 """Check that files in odd encodings are accepted."""
329 mydir = os.path.dirname(__file__)
329 mydir = os.path.dirname(__file__)
330 na = os.path.join(mydir, 'nonascii.py')
330 na = os.path.join(mydir, 'nonascii.py')
331 _ip.magic('run "%s"' % na)
331 _ip.magic('run "%s"' % na)
332 nt.assert_equal(_ip.user_ns['u'], u'ΠŽΡ‚β„–Π€')
332 nt.assert_equal(_ip.user_ns['u'], u'ΠŽΡ‚β„–Π€')
333
333
334 def test_run_py_file_attribute(self):
334 def test_run_py_file_attribute(self):
335 """Test handling of `__file__` attribute in `%run <file>.py`."""
335 """Test handling of `__file__` attribute in `%run <file>.py`."""
336 src = "t = __file__\n"
336 src = "t = __file__\n"
337 self.mktmp(src)
337 self.mktmp(src)
338 _missing = object()
338 _missing = object()
339 file1 = _ip.user_ns.get('__file__', _missing)
339 file1 = _ip.user_ns.get('__file__', _missing)
340 _ip.magic('run %s' % self.fname)
340 _ip.magic('run %s' % self.fname)
341 file2 = _ip.user_ns.get('__file__', _missing)
341 file2 = _ip.user_ns.get('__file__', _missing)
342
342
343 # Check that __file__ was equal to the filename in the script's
343 # Check that __file__ was equal to the filename in the script's
344 # namespace.
344 # namespace.
345 nt.assert_equal(_ip.user_ns['t'], self.fname)
345 nt.assert_equal(_ip.user_ns['t'], self.fname)
346
346
347 # Check that __file__ was not leaked back into user_ns.
347 # Check that __file__ was not leaked back into user_ns.
348 nt.assert_equal(file1, file2)
348 nt.assert_equal(file1, file2)
349
349
350 def test_run_ipy_file_attribute(self):
350 def test_run_ipy_file_attribute(self):
351 """Test handling of `__file__` attribute in `%run <file.ipy>`."""
351 """Test handling of `__file__` attribute in `%run <file.ipy>`."""
352 src = "t = __file__\n"
352 src = "t = __file__\n"
353 self.mktmp(src, ext='.ipy')
353 self.mktmp(src, ext='.ipy')
354 _missing = object()
354 _missing = object()
355 file1 = _ip.user_ns.get('__file__', _missing)
355 file1 = _ip.user_ns.get('__file__', _missing)
356 _ip.magic('run %s' % self.fname)
356 _ip.magic('run %s' % self.fname)
357 file2 = _ip.user_ns.get('__file__', _missing)
357 file2 = _ip.user_ns.get('__file__', _missing)
358
358
359 # Check that __file__ was equal to the filename in the script's
359 # Check that __file__ was equal to the filename in the script's
360 # namespace.
360 # namespace.
361 nt.assert_equal(_ip.user_ns['t'], self.fname)
361 nt.assert_equal(_ip.user_ns['t'], self.fname)
362
362
363 # Check that __file__ was not leaked back into user_ns.
363 # Check that __file__ was not leaked back into user_ns.
364 nt.assert_equal(file1, file2)
364 nt.assert_equal(file1, file2)
365
365
366 def test_run_formatting(self):
366 def test_run_formatting(self):
367 """ Test that %run -t -N<N> does not raise a TypeError for N > 1."""
367 """ Test that %run -t -N<N> does not raise a TypeError for N > 1."""
368 src = "pass"
368 src = "pass"
369 self.mktmp(src)
369 self.mktmp(src)
370 _ip.magic('run -t -N 1 %s' % self.fname)
370 _ip.magic('run -t -N 1 %s' % self.fname)
371 _ip.magic('run -t -N 10 %s' % self.fname)
371 _ip.magic('run -t -N 10 %s' % self.fname)
372
372
373 def test_ignore_sys_exit(self):
373 def test_ignore_sys_exit(self):
374 """Test the -e option to ignore sys.exit()"""
374 """Test the -e option to ignore sys.exit()"""
375 src = "import sys; sys.exit(1)"
375 src = "import sys; sys.exit(1)"
376 self.mktmp(src)
376 self.mktmp(src)
377 with tt.AssertPrints('SystemExit'):
377 with tt.AssertPrints('SystemExit'):
378 _ip.magic('run %s' % self.fname)
378 _ip.magic('run %s' % self.fname)
379
379
380 with tt.AssertNotPrints('SystemExit'):
380 with tt.AssertNotPrints('SystemExit'):
381 _ip.magic('run -e %s' % self.fname)
381 _ip.magic('run -e %s' % self.fname)
382
382
383 def test_run_nb(self):
383 def test_run_nb(self):
384 """Test %run notebook.ipynb"""
384 """Test %run notebook.ipynb"""
385 from nbformat import v4, writes
385 from nbformat import v4, writes
386 nb = v4.new_notebook(
386 nb = v4.new_notebook(
387 cells=[
387 cells=[
388 v4.new_markdown_cell("The Ultimate Question of Everything"),
388 v4.new_markdown_cell("The Ultimate Question of Everything"),
389 v4.new_code_cell("answer=42")
389 v4.new_code_cell("answer=42")
390 ]
390 ]
391 )
391 )
392 src = writes(nb, version=4)
392 src = writes(nb, version=4)
393 self.mktmp(src, ext='.ipynb')
393 self.mktmp(src, ext='.ipynb')
394
394
395 _ip.magic("run %s" % self.fname)
395 _ip.magic("run %s" % self.fname)
396
396
397 nt.assert_equal(_ip.user_ns['answer'], 42)
397 nt.assert_equal(_ip.user_ns['answer'], 42)
398
398
399 def test_run_nb_error(self):
399 def test_run_nb_error(self):
400 """Test %run notebook.ipynb error"""
400 """Test %run notebook.ipynb error"""
401 from nbformat import v4, writes
401 from nbformat import v4, writes
402 # %run when a file name isn't provided
402 # %run when a file name isn't provided
403 nt.assert_raises(Exception, _ip.magic, "run")
403 nt.assert_raises(Exception, _ip.magic, "run")
404
404
405 # %run when a file doesn't exist
405 # %run when a file doesn't exist
406 nt.assert_raises(Exception, _ip.magic, "run foobar.ipynb")
406 nt.assert_raises(Exception, _ip.magic, "run foobar.ipynb")
407
407
408 # %run on a notebook with an error
408 # %run on a notebook with an error
409 nb = v4.new_notebook(
409 nb = v4.new_notebook(
410 cells=[
410 cells=[
411 v4.new_code_cell("0/0")
411 v4.new_code_cell("0/0")
412 ]
412 ]
413 )
413 )
414 src = writes(nb, version=4)
414 src = writes(nb, version=4)
415 self.mktmp(src, ext='.ipynb')
415 self.mktmp(src, ext='.ipynb')
416 nt.assert_raises(Exception, _ip.magic, "run %s" % self.fname)
416 nt.assert_raises(Exception, _ip.magic, "run %s" % self.fname)
417
417
418 def test_file_options(self):
418 def test_file_options(self):
419 src = ('import sys\n'
419 src = ('import sys\n'
420 'a = " ".join(sys.argv[1:])\n')
420 'a = " ".join(sys.argv[1:])\n')
421 self.mktmp(src)
421 self.mktmp(src)
422 test_opts = '-x 3 --verbose'
422 test_opts = '-x 3 --verbose'
423 _ip.run_line_magic("run", '{0} {1}'.format(self.fname, test_opts))
423 _ip.run_line_magic("run", '{0} {1}'.format(self.fname, test_opts))
424 nt.assert_equal(_ip.user_ns['a'], test_opts)
424 nt.assert_equal(_ip.user_ns['a'], test_opts)
425
425
426
426
427 class TestMagicRunWithPackage(unittest.TestCase):
427 class TestMagicRunWithPackage(unittest.TestCase):
428
428
429 def writefile(self, name, content):
429 def writefile(self, name, content):
430 path = os.path.join(self.tempdir.name, name)
430 path = os.path.join(self.tempdir.name, name)
431 d = os.path.dirname(path)
431 d = os.path.dirname(path)
432 if not os.path.isdir(d):
432 if not os.path.isdir(d):
433 os.makedirs(d)
433 os.makedirs(d)
434 with open(path, 'w') as f:
434 with open(path, 'w') as f:
435 f.write(textwrap.dedent(content))
435 f.write(textwrap.dedent(content))
436
436
437 def setUp(self):
437 def setUp(self):
438 self.package = package = 'tmp{0}'.format(''.join([random.choice(string.ascii_letters) for i in range(10)]))
438 self.package = package = 'tmp{0}'.format(''.join([random.choice(string.ascii_letters) for i in range(10)]))
439 """Temporary (probably) valid python package name."""
439 """Temporary (probably) valid python package name."""
440
440
441 self.value = int(random.random() * 10000)
441 self.value = int(random.random() * 10000)
442
442
443 self.tempdir = TemporaryDirectory()
443 self.tempdir = TemporaryDirectory()
444 self.__orig_cwd = os.getcwd()
444 self.__orig_cwd = os.getcwd()
445 sys.path.insert(0, self.tempdir.name)
445 sys.path.insert(0, self.tempdir.name)
446
446
447 self.writefile(os.path.join(package, '__init__.py'), '')
447 self.writefile(os.path.join(package, '__init__.py'), '')
448 self.writefile(os.path.join(package, 'sub.py'), """
448 self.writefile(os.path.join(package, 'sub.py'), """
449 x = {0!r}
449 x = {0!r}
450 """.format(self.value))
450 """.format(self.value))
451 self.writefile(os.path.join(package, 'relative.py'), """
451 self.writefile(os.path.join(package, 'relative.py'), """
452 from .sub import x
452 from .sub import x
453 """)
453 """)
454 self.writefile(os.path.join(package, 'absolute.py'), """
454 self.writefile(os.path.join(package, 'absolute.py'), """
455 from {0}.sub import x
455 from {0}.sub import x
456 """.format(package))
456 """.format(package))
457 self.writefile(os.path.join(package, 'args.py'), """
457 self.writefile(os.path.join(package, 'args.py'), """
458 import sys
458 import sys
459 a = " ".join(sys.argv[1:])
459 a = " ".join(sys.argv[1:])
460 """.format(package))
460 """.format(package))
461
461
462 def tearDown(self):
462 def tearDown(self):
463 os.chdir(self.__orig_cwd)
463 os.chdir(self.__orig_cwd)
464 sys.path[:] = [p for p in sys.path if p != self.tempdir.name]
464 sys.path[:] = [p for p in sys.path if p != self.tempdir.name]
465 self.tempdir.cleanup()
465 self.tempdir.cleanup()
466
466
467 def check_run_submodule(self, submodule, opts=''):
467 def check_run_submodule(self, submodule, opts=''):
468 _ip.user_ns.pop('x', None)
468 _ip.user_ns.pop('x', None)
469 _ip.magic('run {2} -m {0}.{1}'.format(self.package, submodule, opts))
469 _ip.magic('run {2} -m {0}.{1}'.format(self.package, submodule, opts))
470 self.assertEqual(_ip.user_ns['x'], self.value,
470 self.assertEqual(_ip.user_ns['x'], self.value,
471 'Variable `x` is not loaded from module `{0}`.'
471 'Variable `x` is not loaded from module `{0}`.'
472 .format(submodule))
472 .format(submodule))
473
473
474 def test_run_submodule_with_absolute_import(self):
474 def test_run_submodule_with_absolute_import(self):
475 self.check_run_submodule('absolute')
475 self.check_run_submodule('absolute')
476
476
477 def test_run_submodule_with_relative_import(self):
477 def test_run_submodule_with_relative_import(self):
478 """Run submodule that has a relative import statement (#2727)."""
478 """Run submodule that has a relative import statement (#2727)."""
479 self.check_run_submodule('relative')
479 self.check_run_submodule('relative')
480
480
481 def test_prun_submodule_with_absolute_import(self):
481 def test_prun_submodule_with_absolute_import(self):
482 self.check_run_submodule('absolute', '-p')
482 self.check_run_submodule('absolute', '-p')
483
483
484 def test_prun_submodule_with_relative_import(self):
484 def test_prun_submodule_with_relative_import(self):
485 self.check_run_submodule('relative', '-p')
485 self.check_run_submodule('relative', '-p')
486
486
487 def with_fake_debugger(func):
487 def with_fake_debugger(func):
488 @functools.wraps(func)
488 @functools.wraps(func)
489 def wrapper(*args, **kwds):
489 def wrapper(*args, **kwds):
490 with patch.object(debugger.Pdb, 'run', staticmethod(eval)):
490 with patch.object(debugger.Pdb, 'run', staticmethod(eval)):
491 return func(*args, **kwds)
491 return func(*args, **kwds)
492 return wrapper
492 return wrapper
493
493
494 @with_fake_debugger
494 @with_fake_debugger
495 def test_debug_run_submodule_with_absolute_import(self):
495 def test_debug_run_submodule_with_absolute_import(self):
496 self.check_run_submodule('absolute', '-d')
496 self.check_run_submodule('absolute', '-d')
497
497
498 @with_fake_debugger
498 @with_fake_debugger
499 def test_debug_run_submodule_with_relative_import(self):
499 def test_debug_run_submodule_with_relative_import(self):
500 self.check_run_submodule('relative', '-d')
500 self.check_run_submodule('relative', '-d')
501
501
502 def test_module_options(self):
502 def test_module_options(self):
503 _ip.user_ns.pop('a', None)
503 _ip.user_ns.pop('a', None)
504 test_opts = '-x abc -m test'
504 test_opts = '-x abc -m test'
505 _ip.run_line_magic('run', '-m {0}.args {1}'.format(self.package, test_opts))
505 _ip.run_line_magic('run', '-m {0}.args {1}'.format(self.package, test_opts))
506 nt.assert_equal(_ip.user_ns['a'], test_opts)
506 nt.assert_equal(_ip.user_ns['a'], test_opts)
507
507
508 def test_module_options_with_separator(self):
508 def test_module_options_with_separator(self):
509 _ip.user_ns.pop('a', None)
509 _ip.user_ns.pop('a', None)
510 test_opts = '-x abc -m test'
510 test_opts = '-x abc -m test'
511 _ip.run_line_magic('run', '-m {0}.args -- {1}'.format(self.package, test_opts))
511 _ip.run_line_magic('run', '-m {0}.args -- {1}'.format(self.package, test_opts))
512 nt.assert_equal(_ip.user_ns['a'], test_opts)
512 nt.assert_equal(_ip.user_ns['a'], test_opts)
513
513
514 def test_run__name__():
514 def test_run__name__():
515 with TemporaryDirectory() as td:
515 with TemporaryDirectory() as td:
516 path = pjoin(td, 'foo.py')
516 path = pjoin(td, 'foo.py')
517 with open(path, 'w') as f:
517 with open(path, 'w') as f:
518 f.write("q = __name__")
518 f.write("q = __name__")
519
519
520 _ip.user_ns.pop('q', None)
520 _ip.user_ns.pop('q', None)
521 _ip.magic('run {}'.format(path))
521 _ip.magic('run {}'.format(path))
522 nt.assert_equal(_ip.user_ns.pop('q'), '__main__')
522 nt.assert_equal(_ip.user_ns.pop('q'), '__main__')
523
523
524 _ip.magic('run -n {}'.format(path))
524 _ip.magic('run -n {}'.format(path))
525 nt.assert_equal(_ip.user_ns.pop('q'), 'foo')
525 nt.assert_equal(_ip.user_ns.pop('q'), 'foo')
526
526
527 try:
527 try:
528 _ip.magic('run -i -n {}'.format(path))
528 _ip.magic('run -i -n {}'.format(path))
529 nt.assert_equal(_ip.user_ns.pop('q'), 'foo')
529 nt.assert_equal(_ip.user_ns.pop('q'), 'foo')
530 finally:
530 finally:
531 _ip.magic('reset -f')
531 _ip.magic('reset -f')
532
532
533
533
534 def test_run_tb():
534 def test_run_tb():
535 """Test traceback offset in %run"""
535 """Test traceback offset in %run"""
536 with TemporaryDirectory() as td:
536 with TemporaryDirectory() as td:
537 path = pjoin(td, 'foo.py')
537 path = pjoin(td, 'foo.py')
538 with open(path, 'w') as f:
538 with open(path, 'w') as f:
539 f.write('\n'.join([
539 f.write('\n'.join([
540 "def foo():",
540 "def foo():",
541 " return bar()",
541 " return bar()",
542 "def bar():",
542 "def bar():",
543 " raise RuntimeError('hello!')",
543 " raise RuntimeError('hello!')",
544 "foo()",
544 "foo()",
545 ]))
545 ]))
546 with capture_output() as io:
546 with capture_output() as io:
547 _ip.magic('run {}'.format(path))
547 _ip.magic('run {}'.format(path))
548 out = io.stdout
548 out = io.stdout
549 nt.assert_not_in("execfile", out)
549 nt.assert_not_in("execfile", out)
550 nt.assert_in("RuntimeError", out)
550 nt.assert_in("RuntimeError", out)
551 nt.assert_equal(out.count("---->"), 3)
551 nt.assert_equal(out.count("---->"), 3)
552 del ip.user_ns['bar']
552 del ip.user_ns['bar']
553 del ip.user_ns['foo']
553 del ip.user_ns['foo']
554
554
555
555
556 def test_multiprocessing_run():
556 def test_multiprocessing_run():
557 """Set we can run mutiprocesgin without messing up up main namespace
557 """Set we can run mutiprocesgin without messing up up main namespace
558
558
559 Note that import `nose.tools as nt` mdify the value s
559 Note that import `nose.tools as nt` mdify the value s
560 sys.module['__mp_main__'] so wee need to temporarily set it to None to test
560 sys.module['__mp_main__'] so wee need to temporarily set it to None to test
561 the issue.
561 the issue.
562 """
562 """
563 with TemporaryDirectory() as td:
563 with TemporaryDirectory() as td:
564 mpm = sys.modules.get('__mp_main__')
564 mpm = sys.modules.get('__mp_main__')
565 assert mpm is not None
565 assert mpm is not None
566 sys.modules['__mp_main__'] = None
566 sys.modules['__mp_main__'] = None
567 try:
567 try:
568 path = pjoin(td, 'test.py')
568 path = pjoin(td, 'test.py')
569 with open(path, 'w') as f:
569 with open(path, 'w') as f:
570 f.write("import multiprocessing\nprint('hoy')")
570 f.write("import multiprocessing\nprint('hoy')")
571 with capture_output() as io:
571 with capture_output() as io:
572 _ip.run_line_magic('run', path)
572 _ip.run_line_magic('run', path)
573 _ip.run_cell("i_m_undefined")
573 _ip.run_cell("i_m_undefined")
574 out = io.stdout
574 out = io.stdout
575 nt.assert_in("hoy", out)
575 nt.assert_in("hoy", out)
576 nt.assert_not_in("AttributeError", out)
576 nt.assert_not_in("AttributeError", out)
577 nt.assert_in("NameError", out)
577 nt.assert_in("NameError", out)
578 nt.assert_equal(out.count("---->"), 1)
578 nt.assert_equal(out.count("---->"), 1)
579 except:
579 except:
580 raise
580 raise
581 finally:
581 finally:
582 sys.modules['__mp_main__'] = mpm
582 sys.modules['__mp_main__'] = mpm
583
583
584 @dec.knownfailureif(sys.platform == 'win32', "writes to io.stdout aren't captured on Windows")
584 @dec.knownfailureif(sys.platform == 'win32', "writes to io.stdout aren't captured on Windows")
585 def test_script_tb():
585 def test_script_tb():
586 """Test traceback offset in `ipython script.py`"""
586 """Test traceback offset in `ipython script.py`"""
587 with TemporaryDirectory() as td:
587 with TemporaryDirectory() as td:
588 path = pjoin(td, 'foo.py')
588 path = pjoin(td, 'foo.py')
589 with open(path, 'w') as f:
589 with open(path, 'w') as f:
590 f.write('\n'.join([
590 f.write('\n'.join([
591 "def foo():",
591 "def foo():",
592 " return bar()",
592 " return bar()",
593 "def bar():",
593 "def bar():",
594 " raise RuntimeError('hello!')",
594 " raise RuntimeError('hello!')",
595 "foo()",
595 "foo()",
596 ]))
596 ]))
597 out, err = tt.ipexec(path)
597 out, err = tt.ipexec(path)
598 nt.assert_not_in("execfile", out)
598 nt.assert_not_in("execfile", out)
599 nt.assert_in("RuntimeError", out)
599 nt.assert_in("RuntimeError", out)
600 nt.assert_equal(out.count("---->"), 3)
600 nt.assert_equal(out.count("---->"), 3)
601
601
@@ -1,233 +1,233 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 %store magic for lightweight persistence.
3 %store magic for lightweight persistence.
4
4
5 Stores variables, aliases and macros in IPython's database.
5 Stores variables, aliases and macros in IPython's database.
6
6
7 To automatically restore stored variables at startup, add this to your
7 To automatically restore stored variables at startup, add this to your
8 :file:`ipython_config.py` file::
8 :file:`ipython_config.py` file::
9
9
10 c.StoreMagics.autorestore = True
10 c.StoreMagics.autorestore = True
11 """
11 """
12
12
13 # Copyright (c) IPython Development Team.
13 # Copyright (c) IPython Development Team.
14 # Distributed under the terms of the Modified BSD License.
14 # Distributed under the terms of the Modified BSD License.
15
15
16 import inspect, os, sys, textwrap
16 import inspect, os, sys, textwrap
17
17
18 from IPython.core.error import UsageError
18 from IPython.core.error import UsageError
19 from IPython.core.magic import Magics, magics_class, line_magic
19 from IPython.core.magic import Magics, magics_class, line_magic
20 from traitlets import Bool
20 from traitlets import Bool
21
21
22
22
23 def restore_aliases(ip, alias=None):
23 def restore_aliases(ip, alias=None):
24 staliases = ip.db.get('stored_aliases', {})
24 staliases = ip.db.get('stored_aliases', {})
25 if alias is None:
25 if alias is None:
26 for k,v in staliases.items():
26 for k,v in staliases.items():
27 #print "restore alias",k,v # dbg
27 #print "restore alias",k,v # dbg
28 #self.alias_table[k] = v
28 #self.alias_table[k] = v
29 ip.alias_manager.define_alias(k,v)
29 ip.alias_manager.define_alias(k,v)
30 else:
30 else:
31 ip.alias_manager.define_alias(alias, staliases[alias])
31 ip.alias_manager.define_alias(alias, staliases[alias])
32
32
33
33
34 def refresh_variables(ip):
34 def refresh_variables(ip):
35 db = ip.db
35 db = ip.db
36 for key in db.keys('autorestore/*'):
36 for key in db.keys('autorestore/*'):
37 # strip autorestore
37 # strip autorestore
38 justkey = os.path.basename(key)
38 justkey = os.path.basename(key)
39 try:
39 try:
40 obj = db[key]
40 obj = db[key]
41 except KeyError:
41 except KeyError:
42 print("Unable to restore variable '%s', ignoring (use %%store -d to forget!)" % justkey)
42 print("Unable to restore variable '%s', ignoring (use %%store -d to forget!)" % justkey)
43 print("The error was:", sys.exc_info()[0])
43 print("The error was:", sys.exc_info()[0])
44 else:
44 else:
45 #print "restored",justkey,"=",obj #dbg
45 #print "restored",justkey,"=",obj #dbg
46 ip.user_ns[justkey] = obj
46 ip.user_ns[justkey] = obj
47
47
48
48
49 def restore_dhist(ip):
49 def restore_dhist(ip):
50 ip.user_ns['_dh'] = ip.db.get('dhist',[])
50 ip.user_ns['_dh'] = ip.db.get('dhist',[])
51
51
52
52
53 def restore_data(ip):
53 def restore_data(ip):
54 refresh_variables(ip)
54 refresh_variables(ip)
55 restore_aliases(ip)
55 restore_aliases(ip)
56 restore_dhist(ip)
56 restore_dhist(ip)
57
57
58
58
59 @magics_class
59 @magics_class
60 class StoreMagics(Magics):
60 class StoreMagics(Magics):
61 """Lightweight persistence for python variables.
61 """Lightweight persistence for python variables.
62
62
63 Provides the %store magic."""
63 Provides the %store magic."""
64
64
65 autorestore = Bool(False, help=
65 autorestore = Bool(False, help=
66 """If True, any %store-d variables will be automatically restored
66 """If True, any %store-d variables will be automatically restored
67 when IPython starts.
67 when IPython starts.
68 """
68 """
69 ).tag(config=True)
69 ).tag(config=True)
70
70
71 def __init__(self, shell):
71 def __init__(self, shell):
72 super(StoreMagics, self).__init__(shell=shell)
72 super(StoreMagics, self).__init__(shell=shell)
73 self.shell.configurables.append(self)
73 self.shell.configurables.append(self)
74 if self.autorestore:
74 if self.autorestore:
75 restore_data(self.shell)
75 restore_data(self.shell)
76
76
77 @line_magic
77 @line_magic
78 def store(self, parameter_s=''):
78 def store(self, parameter_s=''):
79 """Lightweight persistence for python variables.
79 """Lightweight persistence for python variables.
80
80
81 Example::
81 Example::
82
82
83 In [1]: l = ['hello',10,'world']
83 In [1]: l = ['hello',10,'world']
84 In [2]: %store l
84 In [2]: %store l
85 In [3]: exit
85 In [3]: exit
86
86
87 (IPython session is closed and started again...)
87 (IPython session is closed and started again...)
88
88
89 ville@badger:~$ ipython
89 ville@badger:~$ ipython
90 In [1]: l
90 In [1]: l
91 NameError: name 'l' is not defined
91 NameError: name 'l' is not defined
92 In [2]: %store -r
92 In [2]: %store -r
93 In [3]: l
93 In [3]: l
94 Out[3]: ['hello', 10, 'world']
94 Out[3]: ['hello', 10, 'world']
95
95
96 Usage:
96 Usage:
97
97
98 * ``%store`` - Show list of all variables and their current
98 * ``%store`` - Show list of all variables and their current
99 values
99 values
100 * ``%store spam bar`` - Store the *current* value of the variables spam
100 * ``%store spam bar`` - Store the *current* value of the variables spam
101 and bar to disk
101 and bar to disk
102 * ``%store -d spam`` - Remove the variable and its value from storage
102 * ``%store -d spam`` - Remove the variable and its value from storage
103 * ``%store -z`` - Remove all variables from storage
103 * ``%store -z`` - Remove all variables from storage
104 * ``%store -r`` - Refresh all variables, aliases and directory history
104 * ``%store -r`` - Refresh all variables, aliases and directory history
105 from store (overwrite current vals)
105 from store (overwrite current vals)
106 * ``%store -r spam bar`` - Refresh specified variables and aliases from store
106 * ``%store -r spam bar`` - Refresh specified variables and aliases from store
107 (delete current val)
107 (delete current val)
108 * ``%store foo >a.txt`` - Store value of foo to new file a.txt
108 * ``%store foo >a.txt`` - Store value of foo to new file a.txt
109 * ``%store foo >>a.txt`` - Append value of foo to file a.txt
109 * ``%store foo >>a.txt`` - Append value of foo to file a.txt
110
110
111 It should be noted that if you change the value of a variable, you
111 It should be noted that if you change the value of a variable, you
112 need to %store it again if you want to persist the new value.
112 need to %store it again if you want to persist the new value.
113
113
114 Note also that the variables will need to be pickleable; most basic
114 Note also that the variables will need to be pickleable; most basic
115 python types can be safely %store'd.
115 python types can be safely %store'd.
116
116
117 Also aliases can be %store'd across sessions.
117 Also aliases can be %store'd across sessions.
118 To remove an alias from the storage, use the %unalias magic.
118 To remove an alias from the storage, use the %unalias magic.
119 """
119 """
120
120
121 opts,argsl = self.parse_options(parameter_s,'drz',mode='string')
121 opts,argsl = self.parse_options(parameter_s,'drz',mode='string')
122 args = argsl.split()
122 args = argsl.split()
123 ip = self.shell
123 ip = self.shell
124 db = ip.db
124 db = ip.db
125 # delete
125 # delete
126 if 'd' in opts:
126 if 'd' in opts:
127 try:
127 try:
128 todel = args[0]
128 todel = args[0]
129 except IndexError:
129 except IndexError as e:
130 raise UsageError('You must provide the variable to forget')
130 raise UsageError('You must provide the variable to forget') from e
131 else:
131 else:
132 try:
132 try:
133 del db['autorestore/' + todel]
133 del db['autorestore/' + todel]
134 except:
134 except BaseException as e:
135 raise UsageError("Can't delete variable '%s'" % todel)
135 raise UsageError("Can't delete variable '%s'" % todel) from e
136 # reset
136 # reset
137 elif 'z' in opts:
137 elif 'z' in opts:
138 for k in db.keys('autorestore/*'):
138 for k in db.keys('autorestore/*'):
139 del db[k]
139 del db[k]
140
140
141 elif 'r' in opts:
141 elif 'r' in opts:
142 if args:
142 if args:
143 for arg in args:
143 for arg in args:
144 try:
144 try:
145 obj = db['autorestore/' + arg]
145 obj = db['autorestore/' + arg]
146 except KeyError:
146 except KeyError:
147 try:
147 try:
148 restore_aliases(ip, alias=arg)
148 restore_aliases(ip, alias=arg)
149 except KeyError:
149 except KeyError:
150 print("no stored variable or alias %s" % arg)
150 print("no stored variable or alias %s" % arg)
151 else:
151 else:
152 ip.user_ns[arg] = obj
152 ip.user_ns[arg] = obj
153 else:
153 else:
154 restore_data(ip)
154 restore_data(ip)
155
155
156 # run without arguments -> list variables & values
156 # run without arguments -> list variables & values
157 elif not args:
157 elif not args:
158 vars = db.keys('autorestore/*')
158 vars = db.keys('autorestore/*')
159 vars.sort()
159 vars.sort()
160 if vars:
160 if vars:
161 size = max(map(len, vars))
161 size = max(map(len, vars))
162 else:
162 else:
163 size = 0
163 size = 0
164
164
165 print('Stored variables and their in-db values:')
165 print('Stored variables and their in-db values:')
166 fmt = '%-'+str(size)+'s -> %s'
166 fmt = '%-'+str(size)+'s -> %s'
167 get = db.get
167 get = db.get
168 for var in vars:
168 for var in vars:
169 justkey = os.path.basename(var)
169 justkey = os.path.basename(var)
170 # print 30 first characters from every var
170 # print 30 first characters from every var
171 print(fmt % (justkey, repr(get(var, '<unavailable>'))[:50]))
171 print(fmt % (justkey, repr(get(var, '<unavailable>'))[:50]))
172
172
173 # default action - store the variable
173 # default action - store the variable
174 else:
174 else:
175 # %store foo >file.txt or >>file.txt
175 # %store foo >file.txt or >>file.txt
176 if len(args) > 1 and args[1].startswith('>'):
176 if len(args) > 1 and args[1].startswith('>'):
177 fnam = os.path.expanduser(args[1].lstrip('>').lstrip())
177 fnam = os.path.expanduser(args[1].lstrip('>').lstrip())
178 if args[1].startswith('>>'):
178 if args[1].startswith('>>'):
179 fil = open(fnam, 'a')
179 fil = open(fnam, 'a')
180 else:
180 else:
181 fil = open(fnam, 'w')
181 fil = open(fnam, 'w')
182 with fil:
182 with fil:
183 obj = ip.ev(args[0])
183 obj = ip.ev(args[0])
184 print("Writing '%s' (%s) to file '%s'." % (args[0],
184 print("Writing '%s' (%s) to file '%s'." % (args[0],
185 obj.__class__.__name__, fnam))
185 obj.__class__.__name__, fnam))
186
186
187 if not isinstance (obj, str):
187 if not isinstance (obj, str):
188 from pprint import pprint
188 from pprint import pprint
189 pprint(obj, fil)
189 pprint(obj, fil)
190 else:
190 else:
191 fil.write(obj)
191 fil.write(obj)
192 if not obj.endswith('\n'):
192 if not obj.endswith('\n'):
193 fil.write('\n')
193 fil.write('\n')
194
194
195 return
195 return
196
196
197 # %store foo
197 # %store foo
198 for arg in args:
198 for arg in args:
199 try:
199 try:
200 obj = ip.user_ns[arg]
200 obj = ip.user_ns[arg]
201 except KeyError:
201 except KeyError:
202 # it might be an alias
202 # it might be an alias
203 name = arg
203 name = arg
204 try:
204 try:
205 cmd = ip.alias_manager.retrieve_alias(name)
205 cmd = ip.alias_manager.retrieve_alias(name)
206 except ValueError:
206 except ValueError as e:
207 raise UsageError("Unknown variable '%s'" % name)
207 raise UsageError("Unknown variable '%s'" % name) from e
208
208
209 staliases = db.get('stored_aliases',{})
209 staliases = db.get('stored_aliases',{})
210 staliases[name] = cmd
210 staliases[name] = cmd
211 db['stored_aliases'] = staliases
211 db['stored_aliases'] = staliases
212 print("Alias stored: %s (%s)" % (name, cmd))
212 print("Alias stored: %s (%s)" % (name, cmd))
213 return
213 return
214
214
215 else:
215 else:
216 modname = getattr(inspect.getmodule(obj), '__name__', '')
216 modname = getattr(inspect.getmodule(obj), '__name__', '')
217 if modname == '__main__':
217 if modname == '__main__':
218 print(textwrap.dedent("""\
218 print(textwrap.dedent("""\
219 Warning:%s is %s
219 Warning:%s is %s
220 Proper storage of interactively declared classes (or instances
220 Proper storage of interactively declared classes (or instances
221 of those classes) is not possible! Only instances
221 of those classes) is not possible! Only instances
222 of classes in real modules on file system can be %%store'd.
222 of classes in real modules on file system can be %%store'd.
223 """ % (arg, obj) ))
223 """ % (arg, obj) ))
224 return
224 return
225 #pickled = pickle.dumps(obj)
225 #pickled = pickle.dumps(obj)
226 db[ 'autorestore/' + arg ] = obj
226 db[ 'autorestore/' + arg ] = obj
227 print("Stored '%s' (%s)" % (arg, obj.__class__.__name__))
227 print("Stored '%s' (%s)" % (arg, obj.__class__.__name__))
228
228
229
229
230 def load_ipython_extension(ip):
230 def load_ipython_extension(ip):
231 """Load the extension in IPython."""
231 """Load the extension in IPython."""
232 ip.register_magics(StoreMagics)
232 ip.register_magics(StoreMagics)
233
233
@@ -1,69 +1,69 b''
1 """ Utilities for accessing the platform's clipboard.
1 """ Utilities for accessing the platform's clipboard.
2 """
2 """
3
3
4 import subprocess
4 import subprocess
5
5
6 from IPython.core.error import TryNext
6 from IPython.core.error import TryNext
7 import IPython.utils.py3compat as py3compat
7 import IPython.utils.py3compat as py3compat
8
8
9 class ClipboardEmpty(ValueError):
9 class ClipboardEmpty(ValueError):
10 pass
10 pass
11
11
12 def win32_clipboard_get():
12 def win32_clipboard_get():
13 """ Get the current clipboard's text on Windows.
13 """ Get the current clipboard's text on Windows.
14
14
15 Requires Mark Hammond's pywin32 extensions.
15 Requires Mark Hammond's pywin32 extensions.
16 """
16 """
17 try:
17 try:
18 import win32clipboard
18 import win32clipboard
19 except ImportError:
19 except ImportError as e:
20 raise TryNext("Getting text from the clipboard requires the pywin32 "
20 raise TryNext("Getting text from the clipboard requires the pywin32 "
21 "extensions: http://sourceforge.net/projects/pywin32/")
21 "extensions: http://sourceforge.net/projects/pywin32/") from e
22 win32clipboard.OpenClipboard()
22 win32clipboard.OpenClipboard()
23 try:
23 try:
24 text = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
24 text = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
25 except (TypeError, win32clipboard.error):
25 except (TypeError, win32clipboard.error):
26 try:
26 try:
27 text = win32clipboard.GetClipboardData(win32clipboard.CF_TEXT)
27 text = win32clipboard.GetClipboardData(win32clipboard.CF_TEXT)
28 text = py3compat.cast_unicode(text, py3compat.DEFAULT_ENCODING)
28 text = py3compat.cast_unicode(text, py3compat.DEFAULT_ENCODING)
29 except (TypeError, win32clipboard.error):
29 except (TypeError, win32clipboard.error) as e:
30 raise ClipboardEmpty
30 raise ClipboardEmpty from e
31 finally:
31 finally:
32 win32clipboard.CloseClipboard()
32 win32clipboard.CloseClipboard()
33 return text
33 return text
34
34
35 def osx_clipboard_get() -> str:
35 def osx_clipboard_get() -> str:
36 """ Get the clipboard's text on OS X.
36 """ Get the clipboard's text on OS X.
37 """
37 """
38 p = subprocess.Popen(['pbpaste', '-Prefer', 'ascii'],
38 p = subprocess.Popen(['pbpaste', '-Prefer', 'ascii'],
39 stdout=subprocess.PIPE)
39 stdout=subprocess.PIPE)
40 bytes_, stderr = p.communicate()
40 bytes_, stderr = p.communicate()
41 # Text comes in with old Mac \r line endings. Change them to \n.
41 # Text comes in with old Mac \r line endings. Change them to \n.
42 bytes_ = bytes_.replace(b'\r', b'\n')
42 bytes_ = bytes_.replace(b'\r', b'\n')
43 text = py3compat.decode(bytes_)
43 text = py3compat.decode(bytes_)
44 return text
44 return text
45
45
46 def tkinter_clipboard_get():
46 def tkinter_clipboard_get():
47 """ Get the clipboard's text using Tkinter.
47 """ Get the clipboard's text using Tkinter.
48
48
49 This is the default on systems that are not Windows or OS X. It may
49 This is the default on systems that are not Windows or OS X. It may
50 interfere with other UI toolkits and should be replaced with an
50 interfere with other UI toolkits and should be replaced with an
51 implementation that uses that toolkit.
51 implementation that uses that toolkit.
52 """
52 """
53 try:
53 try:
54 from tkinter import Tk, TclError
54 from tkinter import Tk, TclError
55 except ImportError:
55 except ImportError as e:
56 raise TryNext("Getting text from the clipboard on this platform requires tkinter.")
56 raise TryNext("Getting text from the clipboard on this platform requires tkinter.") from e
57
57
58 root = Tk()
58 root = Tk()
59 root.withdraw()
59 root.withdraw()
60 try:
60 try:
61 text = root.clipboard_get()
61 text = root.clipboard_get()
62 except TclError:
62 except TclError as e:
63 raise ClipboardEmpty
63 raise ClipboardEmpty from e
64 finally:
64 finally:
65 root.destroy()
65 root.destroy()
66 text = py3compat.cast_unicode(text, py3compat.DEFAULT_ENCODING)
66 text = py3compat.cast_unicode(text, py3compat.DEFAULT_ENCODING)
67 return text
67 return text
68
68
69
69
@@ -1,341 +1,341 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 Provides a reload() function that acts recursively.
3 Provides a reload() function that acts recursively.
4
4
5 Python's normal :func:`python:reload` function only reloads the module that it's
5 Python's normal :func:`python:reload` function only reloads the module that it's
6 passed. The :func:`reload` function in this module also reloads everything
6 passed. The :func:`reload` function in this module also reloads everything
7 imported from that module, which is useful when you're changing files deep
7 imported from that module, which is useful when you're changing files deep
8 inside a package.
8 inside a package.
9
9
10 To use this as your default reload function, type this::
10 To use this as your default reload function, type this::
11
11
12 import builtins
12 import builtins
13 from IPython.lib import deepreload
13 from IPython.lib import deepreload
14 builtins.reload = deepreload.reload
14 builtins.reload = deepreload.reload
15
15
16 A reference to the original :func:`python:reload` is stored in this module as
16 A reference to the original :func:`python:reload` is stored in this module as
17 :data:`original_reload`, so you can restore it later.
17 :data:`original_reload`, so you can restore it later.
18
18
19 This code is almost entirely based on knee.py, which is a Python
19 This code is almost entirely based on knee.py, which is a Python
20 re-implementation of hierarchical module import.
20 re-implementation of hierarchical module import.
21 """
21 """
22 #*****************************************************************************
22 #*****************************************************************************
23 # Copyright (C) 2001 Nathaniel Gray <n8gray@caltech.edu>
23 # Copyright (C) 2001 Nathaniel Gray <n8gray@caltech.edu>
24 #
24 #
25 # Distributed under the terms of the BSD License. The full license is in
25 # Distributed under the terms of the BSD License. The full license is in
26 # the file COPYING, distributed as part of this software.
26 # the file COPYING, distributed as part of this software.
27 #*****************************************************************************
27 #*****************************************************************************
28
28
29 import builtins as builtin_mod
29 import builtins as builtin_mod
30 from contextlib import contextmanager
30 from contextlib import contextmanager
31 import imp
31 import imp
32 import sys
32 import sys
33
33
34 from types import ModuleType
34 from types import ModuleType
35 from warnings import warn
35 from warnings import warn
36 import types
36 import types
37
37
38 original_import = builtin_mod.__import__
38 original_import = builtin_mod.__import__
39
39
40 @contextmanager
40 @contextmanager
41 def replace_import_hook(new_import):
41 def replace_import_hook(new_import):
42 saved_import = builtin_mod.__import__
42 saved_import = builtin_mod.__import__
43 builtin_mod.__import__ = new_import
43 builtin_mod.__import__ = new_import
44 try:
44 try:
45 yield
45 yield
46 finally:
46 finally:
47 builtin_mod.__import__ = saved_import
47 builtin_mod.__import__ = saved_import
48
48
49 def get_parent(globals, level):
49 def get_parent(globals, level):
50 """
50 """
51 parent, name = get_parent(globals, level)
51 parent, name = get_parent(globals, level)
52
52
53 Return the package that an import is being performed in. If globals comes
53 Return the package that an import is being performed in. If globals comes
54 from the module foo.bar.bat (not itself a package), this returns the
54 from the module foo.bar.bat (not itself a package), this returns the
55 sys.modules entry for foo.bar. If globals is from a package's __init__.py,
55 sys.modules entry for foo.bar. If globals is from a package's __init__.py,
56 the package's entry in sys.modules is returned.
56 the package's entry in sys.modules is returned.
57
57
58 If globals doesn't come from a package or a module in a package, or a
58 If globals doesn't come from a package or a module in a package, or a
59 corresponding entry is not found in sys.modules, None is returned.
59 corresponding entry is not found in sys.modules, None is returned.
60 """
60 """
61 orig_level = level
61 orig_level = level
62
62
63 if not level or not isinstance(globals, dict):
63 if not level or not isinstance(globals, dict):
64 return None, ''
64 return None, ''
65
65
66 pkgname = globals.get('__package__', None)
66 pkgname = globals.get('__package__', None)
67
67
68 if pkgname is not None:
68 if pkgname is not None:
69 # __package__ is set, so use it
69 # __package__ is set, so use it
70 if not hasattr(pkgname, 'rindex'):
70 if not hasattr(pkgname, 'rindex'):
71 raise ValueError('__package__ set to non-string')
71 raise ValueError('__package__ set to non-string')
72 if len(pkgname) == 0:
72 if len(pkgname) == 0:
73 if level > 0:
73 if level > 0:
74 raise ValueError('Attempted relative import in non-package')
74 raise ValueError('Attempted relative import in non-package')
75 return None, ''
75 return None, ''
76 name = pkgname
76 name = pkgname
77 else:
77 else:
78 # __package__ not set, so figure it out and set it
78 # __package__ not set, so figure it out and set it
79 if '__name__' not in globals:
79 if '__name__' not in globals:
80 return None, ''
80 return None, ''
81 modname = globals['__name__']
81 modname = globals['__name__']
82
82
83 if '__path__' in globals:
83 if '__path__' in globals:
84 # __path__ is set, so modname is already the package name
84 # __path__ is set, so modname is already the package name
85 globals['__package__'] = name = modname
85 globals['__package__'] = name = modname
86 else:
86 else:
87 # Normal module, so work out the package name if any
87 # Normal module, so work out the package name if any
88 lastdot = modname.rfind('.')
88 lastdot = modname.rfind('.')
89 if lastdot < 0 < level:
89 if lastdot < 0 < level:
90 raise ValueError("Attempted relative import in non-package")
90 raise ValueError("Attempted relative import in non-package")
91 if lastdot < 0:
91 if lastdot < 0:
92 globals['__package__'] = None
92 globals['__package__'] = None
93 return None, ''
93 return None, ''
94 globals['__package__'] = name = modname[:lastdot]
94 globals['__package__'] = name = modname[:lastdot]
95
95
96 dot = len(name)
96 dot = len(name)
97 for x in range(level, 1, -1):
97 for x in range(level, 1, -1):
98 try:
98 try:
99 dot = name.rindex('.', 0, dot)
99 dot = name.rindex('.', 0, dot)
100 except ValueError:
100 except ValueError as e:
101 raise ValueError("attempted relative import beyond top-level "
101 raise ValueError("attempted relative import beyond top-level "
102 "package")
102 "package") from e
103 name = name[:dot]
103 name = name[:dot]
104
104
105 try:
105 try:
106 parent = sys.modules[name]
106 parent = sys.modules[name]
107 except:
107 except BaseException as e:
108 if orig_level < 1:
108 if orig_level < 1:
109 warn("Parent module '%.200s' not found while handling absolute "
109 warn("Parent module '%.200s' not found while handling absolute "
110 "import" % name)
110 "import" % name)
111 parent = None
111 parent = None
112 else:
112 else:
113 raise SystemError("Parent module '%.200s' not loaded, cannot "
113 raise SystemError("Parent module '%.200s' not loaded, cannot "
114 "perform relative import" % name)
114 "perform relative import" % name) from e
115
115
116 # We expect, but can't guarantee, if parent != None, that:
116 # We expect, but can't guarantee, if parent != None, that:
117 # - parent.__name__ == name
117 # - parent.__name__ == name
118 # - parent.__dict__ is globals
118 # - parent.__dict__ is globals
119 # If this is violated... Who cares?
119 # If this is violated... Who cares?
120 return parent, name
120 return parent, name
121
121
122 def load_next(mod, altmod, name, buf):
122 def load_next(mod, altmod, name, buf):
123 """
123 """
124 mod, name, buf = load_next(mod, altmod, name, buf)
124 mod, name, buf = load_next(mod, altmod, name, buf)
125
125
126 altmod is either None or same as mod
126 altmod is either None or same as mod
127 """
127 """
128
128
129 if len(name) == 0:
129 if len(name) == 0:
130 # completely empty module name should only happen in
130 # completely empty module name should only happen in
131 # 'from . import' (or '__import__("")')
131 # 'from . import' (or '__import__("")')
132 return mod, None, buf
132 return mod, None, buf
133
133
134 dot = name.find('.')
134 dot = name.find('.')
135 if dot == 0:
135 if dot == 0:
136 raise ValueError('Empty module name')
136 raise ValueError('Empty module name')
137
137
138 if dot < 0:
138 if dot < 0:
139 subname = name
139 subname = name
140 next = None
140 next = None
141 else:
141 else:
142 subname = name[:dot]
142 subname = name[:dot]
143 next = name[dot+1:]
143 next = name[dot+1:]
144
144
145 if buf != '':
145 if buf != '':
146 buf += '.'
146 buf += '.'
147 buf += subname
147 buf += subname
148
148
149 result = import_submodule(mod, subname, buf)
149 result = import_submodule(mod, subname, buf)
150 if result is None and mod != altmod:
150 if result is None and mod != altmod:
151 result = import_submodule(altmod, subname, subname)
151 result = import_submodule(altmod, subname, subname)
152 if result is not None:
152 if result is not None:
153 buf = subname
153 buf = subname
154
154
155 if result is None:
155 if result is None:
156 raise ImportError("No module named %.200s" % name)
156 raise ImportError("No module named %.200s" % name)
157
157
158 return result, next, buf
158 return result, next, buf
159
159
160
160
161 # Need to keep track of what we've already reloaded to prevent cyclic evil
161 # Need to keep track of what we've already reloaded to prevent cyclic evil
162 found_now = {}
162 found_now = {}
163
163
164 def import_submodule(mod, subname, fullname):
164 def import_submodule(mod, subname, fullname):
165 """m = import_submodule(mod, subname, fullname)"""
165 """m = import_submodule(mod, subname, fullname)"""
166 # Require:
166 # Require:
167 # if mod == None: subname == fullname
167 # if mod == None: subname == fullname
168 # else: mod.__name__ + "." + subname == fullname
168 # else: mod.__name__ + "." + subname == fullname
169
169
170 global found_now
170 global found_now
171 if fullname in found_now and fullname in sys.modules:
171 if fullname in found_now and fullname in sys.modules:
172 m = sys.modules[fullname]
172 m = sys.modules[fullname]
173 else:
173 else:
174 print('Reloading', fullname)
174 print('Reloading', fullname)
175 found_now[fullname] = 1
175 found_now[fullname] = 1
176 oldm = sys.modules.get(fullname, None)
176 oldm = sys.modules.get(fullname, None)
177
177
178 if mod is None:
178 if mod is None:
179 path = None
179 path = None
180 elif hasattr(mod, '__path__'):
180 elif hasattr(mod, '__path__'):
181 path = mod.__path__
181 path = mod.__path__
182 else:
182 else:
183 return None
183 return None
184
184
185 try:
185 try:
186 # This appears to be necessary on Python 3, because imp.find_module()
186 # This appears to be necessary on Python 3, because imp.find_module()
187 # tries to import standard libraries (like io) itself, and we don't
187 # tries to import standard libraries (like io) itself, and we don't
188 # want them to be processed by our deep_import_hook.
188 # want them to be processed by our deep_import_hook.
189 with replace_import_hook(original_import):
189 with replace_import_hook(original_import):
190 fp, filename, stuff = imp.find_module(subname, path)
190 fp, filename, stuff = imp.find_module(subname, path)
191 except ImportError:
191 except ImportError:
192 return None
192 return None
193
193
194 try:
194 try:
195 m = imp.load_module(fullname, fp, filename, stuff)
195 m = imp.load_module(fullname, fp, filename, stuff)
196 except:
196 except:
197 # load_module probably removed name from modules because of
197 # load_module probably removed name from modules because of
198 # the error. Put back the original module object.
198 # the error. Put back the original module object.
199 if oldm:
199 if oldm:
200 sys.modules[fullname] = oldm
200 sys.modules[fullname] = oldm
201 raise
201 raise
202 finally:
202 finally:
203 if fp: fp.close()
203 if fp: fp.close()
204
204
205 add_submodule(mod, m, fullname, subname)
205 add_submodule(mod, m, fullname, subname)
206
206
207 return m
207 return m
208
208
209 def add_submodule(mod, submod, fullname, subname):
209 def add_submodule(mod, submod, fullname, subname):
210 """mod.{subname} = submod"""
210 """mod.{subname} = submod"""
211 if mod is None:
211 if mod is None:
212 return #Nothing to do here.
212 return #Nothing to do here.
213
213
214 if submod is None:
214 if submod is None:
215 submod = sys.modules[fullname]
215 submod = sys.modules[fullname]
216
216
217 setattr(mod, subname, submod)
217 setattr(mod, subname, submod)
218
218
219 return
219 return
220
220
221 def ensure_fromlist(mod, fromlist, buf, recursive):
221 def ensure_fromlist(mod, fromlist, buf, recursive):
222 """Handle 'from module import a, b, c' imports."""
222 """Handle 'from module import a, b, c' imports."""
223 if not hasattr(mod, '__path__'):
223 if not hasattr(mod, '__path__'):
224 return
224 return
225 for item in fromlist:
225 for item in fromlist:
226 if not hasattr(item, 'rindex'):
226 if not hasattr(item, 'rindex'):
227 raise TypeError("Item in ``from list'' not a string")
227 raise TypeError("Item in ``from list'' not a string")
228 if item == '*':
228 if item == '*':
229 if recursive:
229 if recursive:
230 continue # avoid endless recursion
230 continue # avoid endless recursion
231 try:
231 try:
232 all = mod.__all__
232 all = mod.__all__
233 except AttributeError:
233 except AttributeError:
234 pass
234 pass
235 else:
235 else:
236 ret = ensure_fromlist(mod, all, buf, 1)
236 ret = ensure_fromlist(mod, all, buf, 1)
237 if not ret:
237 if not ret:
238 return 0
238 return 0
239 elif not hasattr(mod, item):
239 elif not hasattr(mod, item):
240 import_submodule(mod, item, buf + '.' + item)
240 import_submodule(mod, item, buf + '.' + item)
241
241
242 def deep_import_hook(name, globals=None, locals=None, fromlist=None, level=-1):
242 def deep_import_hook(name, globals=None, locals=None, fromlist=None, level=-1):
243 """Replacement for __import__()"""
243 """Replacement for __import__()"""
244 parent, buf = get_parent(globals, level)
244 parent, buf = get_parent(globals, level)
245
245
246 head, name, buf = load_next(parent, None if level < 0 else parent, name, buf)
246 head, name, buf = load_next(parent, None if level < 0 else parent, name, buf)
247
247
248 tail = head
248 tail = head
249 while name:
249 while name:
250 tail, name, buf = load_next(tail, tail, name, buf)
250 tail, name, buf = load_next(tail, tail, name, buf)
251
251
252 # If tail is None, both get_parent and load_next found
252 # If tail is None, both get_parent and load_next found
253 # an empty module name: someone called __import__("") or
253 # an empty module name: someone called __import__("") or
254 # doctored faulty bytecode
254 # doctored faulty bytecode
255 if tail is None:
255 if tail is None:
256 raise ValueError('Empty module name')
256 raise ValueError('Empty module name')
257
257
258 if not fromlist:
258 if not fromlist:
259 return head
259 return head
260
260
261 ensure_fromlist(tail, fromlist, buf, 0)
261 ensure_fromlist(tail, fromlist, buf, 0)
262 return tail
262 return tail
263
263
264 modules_reloading = {}
264 modules_reloading = {}
265
265
266 def deep_reload_hook(m):
266 def deep_reload_hook(m):
267 """Replacement for reload()."""
267 """Replacement for reload()."""
268 # Hardcode this one as it would raise a NotImplementedError from the
268 # Hardcode this one as it would raise a NotImplementedError from the
269 # bowels of Python and screw up the import machinery after.
269 # bowels of Python and screw up the import machinery after.
270 # unlike other imports the `exclude` list already in place is not enough.
270 # unlike other imports the `exclude` list already in place is not enough.
271
271
272 if m is types:
272 if m is types:
273 return m
273 return m
274 if not isinstance(m, ModuleType):
274 if not isinstance(m, ModuleType):
275 raise TypeError("reload() argument must be module")
275 raise TypeError("reload() argument must be module")
276
276
277 name = m.__name__
277 name = m.__name__
278
278
279 if name not in sys.modules:
279 if name not in sys.modules:
280 raise ImportError("reload(): module %.200s not in sys.modules" % name)
280 raise ImportError("reload(): module %.200s not in sys.modules" % name)
281
281
282 global modules_reloading
282 global modules_reloading
283 try:
283 try:
284 return modules_reloading[name]
284 return modules_reloading[name]
285 except:
285 except:
286 modules_reloading[name] = m
286 modules_reloading[name] = m
287
287
288 dot = name.rfind('.')
288 dot = name.rfind('.')
289 if dot < 0:
289 if dot < 0:
290 subname = name
290 subname = name
291 path = None
291 path = None
292 else:
292 else:
293 try:
293 try:
294 parent = sys.modules[name[:dot]]
294 parent = sys.modules[name[:dot]]
295 except KeyError:
295 except KeyError as e:
296 modules_reloading.clear()
296 modules_reloading.clear()
297 raise ImportError("reload(): parent %.200s not in sys.modules" % name[:dot])
297 raise ImportError("reload(): parent %.200s not in sys.modules" % name[:dot]) from e
298 subname = name[dot+1:]
298 subname = name[dot+1:]
299 path = getattr(parent, "__path__", None)
299 path = getattr(parent, "__path__", None)
300
300
301 try:
301 try:
302 # This appears to be necessary on Python 3, because imp.find_module()
302 # This appears to be necessary on Python 3, because imp.find_module()
303 # tries to import standard libraries (like io) itself, and we don't
303 # tries to import standard libraries (like io) itself, and we don't
304 # want them to be processed by our deep_import_hook.
304 # want them to be processed by our deep_import_hook.
305 with replace_import_hook(original_import):
305 with replace_import_hook(original_import):
306 fp, filename, stuff = imp.find_module(subname, path)
306 fp, filename, stuff = imp.find_module(subname, path)
307 finally:
307 finally:
308 modules_reloading.clear()
308 modules_reloading.clear()
309
309
310 try:
310 try:
311 newm = imp.load_module(name, fp, filename, stuff)
311 newm = imp.load_module(name, fp, filename, stuff)
312 except:
312 except:
313 # load_module probably removed name from modules because of
313 # load_module probably removed name from modules because of
314 # the error. Put back the original module object.
314 # the error. Put back the original module object.
315 sys.modules[name] = m
315 sys.modules[name] = m
316 raise
316 raise
317 finally:
317 finally:
318 if fp: fp.close()
318 if fp: fp.close()
319
319
320 modules_reloading.clear()
320 modules_reloading.clear()
321 return newm
321 return newm
322
322
323 # Save the original hooks
323 # Save the original hooks
324 original_reload = imp.reload
324 original_reload = imp.reload
325
325
326 # Replacement for reload()
326 # Replacement for reload()
327 def reload(module, exclude=('sys', 'os.path', 'builtins', '__main__',
327 def reload(module, exclude=('sys', 'os.path', 'builtins', '__main__',
328 'numpy', 'numpy._globals')):
328 'numpy', 'numpy._globals')):
329 """Recursively reload all modules used in the given module. Optionally
329 """Recursively reload all modules used in the given module. Optionally
330 takes a list of modules to exclude from reloading. The default exclude
330 takes a list of modules to exclude from reloading. The default exclude
331 list contains sys, __main__, and __builtin__, to prevent, e.g., resetting
331 list contains sys, __main__, and __builtin__, to prevent, e.g., resetting
332 display, exception, and io hooks.
332 display, exception, and io hooks.
333 """
333 """
334 global found_now
334 global found_now
335 for i in exclude:
335 for i in exclude:
336 found_now[i] = 1
336 found_now[i] = 1
337 try:
337 try:
338 with replace_import_hook(deep_import_hook):
338 with replace_import_hook(deep_import_hook):
339 return deep_reload_hook(module)
339 return deep_reload_hook(module)
340 finally:
340 finally:
341 found_now = {}
341 found_now = {}
@@ -1,651 +1,651 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 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
11 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
12 'FileLink', 'FileLinks', 'Code']
12 'FileLink', 'FileLinks', 'Code']
13
13
14
14
15 class Audio(DisplayObject):
15 class Audio(DisplayObject):
16 """Create an audio object.
16 """Create an audio object.
17
17
18 When this object is returned by an input cell or passed to the
18 When this object is returned by an input cell or passed to the
19 display function, it will result in Audio controls being displayed
19 display function, it will result in Audio controls being displayed
20 in the frontend (only works in the notebook).
20 in the frontend (only works in the notebook).
21
21
22 Parameters
22 Parameters
23 ----------
23 ----------
24 data : numpy array, list, unicode, str or bytes
24 data : numpy array, list, unicode, str or bytes
25 Can be one of
25 Can be one of
26
26
27 * Numpy 1d array containing the desired waveform (mono)
27 * Numpy 1d array containing the desired waveform (mono)
28 * Numpy 2d array containing waveforms for each channel.
28 * Numpy 2d array containing waveforms for each channel.
29 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
29 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
30 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
30 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
31 * List of float or integer representing the waveform (mono)
31 * List of float or integer representing the waveform (mono)
32 * String containing the filename
32 * String containing the filename
33 * Bytestring containing raw PCM data or
33 * Bytestring containing raw PCM data or
34 * URL pointing to a file on the web.
34 * URL pointing to a file on the web.
35
35
36 If the array option is used, the waveform will be normalized.
36 If the array option is used, the waveform will be normalized.
37
37
38 If a filename or url is used, the format support will be browser
38 If a filename or url is used, the format support will be browser
39 dependent.
39 dependent.
40 url : unicode
40 url : unicode
41 A URL to download the data from.
41 A URL to download the data from.
42 filename : unicode
42 filename : unicode
43 Path to a local file to load the data from.
43 Path to a local file to load the data from.
44 embed : boolean
44 embed : boolean
45 Should the audio data be embedded using a data URI (True) or should
45 Should the audio data be embedded using a data URI (True) or should
46 the original source be referenced. Set this to True if you want the
46 the original source be referenced. Set this to True if you want the
47 audio to playable later with no internet connection in the notebook.
47 audio to playable later with no internet connection in the notebook.
48
48
49 Default is `True`, unless the keyword argument `url` is set, then
49 Default is `True`, unless the keyword argument `url` is set, then
50 default value is `False`.
50 default value is `False`.
51 rate : integer
51 rate : integer
52 The sampling rate of the raw data.
52 The sampling rate of the raw data.
53 Only required when data parameter is being used as an array
53 Only required when data parameter is being used as an array
54 autoplay : bool
54 autoplay : bool
55 Set to True if the audio should immediately start playing.
55 Set to True if the audio should immediately start playing.
56 Default is `False`.
56 Default is `False`.
57 normalize : bool
57 normalize : bool
58 Whether audio should be normalized (rescaled) to the maximum possible
58 Whether audio should be normalized (rescaled) to the maximum possible
59 range. Default is `True`. When set to `False`, `data` must be between
59 range. Default is `True`. When set to `False`, `data` must be between
60 -1 and 1 (inclusive), otherwise an error is raised.
60 -1 and 1 (inclusive), otherwise an error is raised.
61 Applies only when `data` is a list or array of samples; other types of
61 Applies only when `data` is a list or array of samples; other types of
62 audio are never normalized.
62 audio are never normalized.
63
63
64 Examples
64 Examples
65 --------
65 --------
66 ::
66 ::
67
67
68 # Generate a sound
68 # Generate a sound
69 import numpy as np
69 import numpy as np
70 framerate = 44100
70 framerate = 44100
71 t = np.linspace(0,5,framerate*5)
71 t = np.linspace(0,5,framerate*5)
72 data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t)
72 data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t)
73 Audio(data,rate=framerate)
73 Audio(data,rate=framerate)
74
74
75 # Can also do stereo or more channels
75 # Can also do stereo or more channels
76 dataleft = np.sin(2*np.pi*220*t)
76 dataleft = np.sin(2*np.pi*220*t)
77 dataright = np.sin(2*np.pi*224*t)
77 dataright = np.sin(2*np.pi*224*t)
78 Audio([dataleft, dataright],rate=framerate)
78 Audio([dataleft, dataright],rate=framerate)
79
79
80 Audio("http://www.nch.com.au/acm/8k16bitpcm.wav") # From URL
80 Audio("http://www.nch.com.au/acm/8k16bitpcm.wav") # From URL
81 Audio(url="http://www.w3schools.com/html/horse.ogg")
81 Audio(url="http://www.w3schools.com/html/horse.ogg")
82
82
83 Audio('/path/to/sound.wav') # From file
83 Audio('/path/to/sound.wav') # From file
84 Audio(filename='/path/to/sound.ogg')
84 Audio(filename='/path/to/sound.ogg')
85
85
86 Audio(b'RAW_WAV_DATA..) # From bytes
86 Audio(b'RAW_WAV_DATA..) # From bytes
87 Audio(data=b'RAW_WAV_DATA..)
87 Audio(data=b'RAW_WAV_DATA..)
88
88
89 See Also
89 See Also
90 --------
90 --------
91
91
92 See also the ``Audio`` widgets form the ``ipywidget`` package for more flexibility and options.
92 See also the ``Audio`` widgets form the ``ipywidget`` package for more flexibility and options.
93
93
94 """
94 """
95 _read_flags = 'rb'
95 _read_flags = 'rb'
96
96
97 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False, normalize=True, *,
97 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False, normalize=True, *,
98 element_id=None):
98 element_id=None):
99 if filename is None and url is None and data is None:
99 if filename is None and url is None and data is None:
100 raise ValueError("No audio data found. Expecting filename, url, or data.")
100 raise ValueError("No audio data found. Expecting filename, url, or data.")
101 if embed is False and url is None:
101 if embed is False and url is None:
102 raise ValueError("No url found. Expecting url when embed=False")
102 raise ValueError("No url found. Expecting url when embed=False")
103
103
104 if url is not None and embed is not True:
104 if url is not None and embed is not True:
105 self.embed = False
105 self.embed = False
106 else:
106 else:
107 self.embed = True
107 self.embed = True
108 self.autoplay = autoplay
108 self.autoplay = autoplay
109 self.element_id = element_id
109 self.element_id = element_id
110 super(Audio, self).__init__(data=data, url=url, filename=filename)
110 super(Audio, self).__init__(data=data, url=url, filename=filename)
111
111
112 if self.data is not None and not isinstance(self.data, bytes):
112 if self.data is not None and not isinstance(self.data, bytes):
113 if rate is None:
113 if rate is None:
114 raise ValueError("rate must be specified when data is a numpy array or list of audio samples.")
114 raise ValueError("rate must be specified when data is a numpy array or list of audio samples.")
115 self.data = Audio._make_wav(data, rate, normalize)
115 self.data = Audio._make_wav(data, rate, normalize)
116
116
117 def reload(self):
117 def reload(self):
118 """Reload the raw data from file or URL."""
118 """Reload the raw data from file or URL."""
119 import mimetypes
119 import mimetypes
120 if self.embed:
120 if self.embed:
121 super(Audio, self).reload()
121 super(Audio, self).reload()
122
122
123 if self.filename is not None:
123 if self.filename is not None:
124 self.mimetype = mimetypes.guess_type(self.filename)[0]
124 self.mimetype = mimetypes.guess_type(self.filename)[0]
125 elif self.url is not None:
125 elif self.url is not None:
126 self.mimetype = mimetypes.guess_type(self.url)[0]
126 self.mimetype = mimetypes.guess_type(self.url)[0]
127 else:
127 else:
128 self.mimetype = "audio/wav"
128 self.mimetype = "audio/wav"
129
129
130 @staticmethod
130 @staticmethod
131 def _make_wav(data, rate, normalize):
131 def _make_wav(data, rate, normalize):
132 """ Transform a numpy array to a PCM bytestring """
132 """ Transform a numpy array to a PCM bytestring """
133 from io import BytesIO
133 from io import BytesIO
134 import wave
134 import wave
135
135
136 try:
136 try:
137 scaled, nchan = Audio._validate_and_normalize_with_numpy(data, normalize)
137 scaled, nchan = Audio._validate_and_normalize_with_numpy(data, normalize)
138 except ImportError:
138 except ImportError:
139 scaled, nchan = Audio._validate_and_normalize_without_numpy(data, normalize)
139 scaled, nchan = Audio._validate_and_normalize_without_numpy(data, normalize)
140
140
141 fp = BytesIO()
141 fp = BytesIO()
142 waveobj = wave.open(fp,mode='wb')
142 waveobj = wave.open(fp,mode='wb')
143 waveobj.setnchannels(nchan)
143 waveobj.setnchannels(nchan)
144 waveobj.setframerate(rate)
144 waveobj.setframerate(rate)
145 waveobj.setsampwidth(2)
145 waveobj.setsampwidth(2)
146 waveobj.setcomptype('NONE','NONE')
146 waveobj.setcomptype('NONE','NONE')
147 waveobj.writeframes(scaled)
147 waveobj.writeframes(scaled)
148 val = fp.getvalue()
148 val = fp.getvalue()
149 waveobj.close()
149 waveobj.close()
150
150
151 return val
151 return val
152
152
153 @staticmethod
153 @staticmethod
154 def _validate_and_normalize_with_numpy(data, normalize):
154 def _validate_and_normalize_with_numpy(data, normalize):
155 import numpy as np
155 import numpy as np
156
156
157 data = np.array(data, dtype=float)
157 data = np.array(data, dtype=float)
158 if len(data.shape) == 1:
158 if len(data.shape) == 1:
159 nchan = 1
159 nchan = 1
160 elif len(data.shape) == 2:
160 elif len(data.shape) == 2:
161 # In wave files,channels are interleaved. E.g.,
161 # In wave files,channels are interleaved. E.g.,
162 # "L1R1L2R2..." for stereo. See
162 # "L1R1L2R2..." for stereo. See
163 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
163 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
164 # for channel ordering
164 # for channel ordering
165 nchan = data.shape[0]
165 nchan = data.shape[0]
166 data = data.T.ravel()
166 data = data.T.ravel()
167 else:
167 else:
168 raise ValueError('Array audio input must be a 1D or 2D array')
168 raise ValueError('Array audio input must be a 1D or 2D array')
169
169
170 max_abs_value = np.max(np.abs(data))
170 max_abs_value = np.max(np.abs(data))
171 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
171 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
172 scaled = data / normalization_factor * 32767
172 scaled = data / normalization_factor * 32767
173 return scaled.astype('<h').tostring(), nchan
173 return scaled.astype('<h').tostring(), nchan
174
174
175
175
176 @staticmethod
176 @staticmethod
177 def _validate_and_normalize_without_numpy(data, normalize):
177 def _validate_and_normalize_without_numpy(data, normalize):
178 import array
178 import array
179 import sys
179 import sys
180
180
181 data = array.array('f', data)
181 data = array.array('f', data)
182
182
183 try:
183 try:
184 max_abs_value = float(max([abs(x) for x in data]))
184 max_abs_value = float(max([abs(x) for x in data]))
185 except TypeError:
185 except TypeError as e:
186 raise TypeError('Only lists of mono audio are '
186 raise TypeError('Only lists of mono audio are '
187 'supported if numpy is not installed')
187 'supported if numpy is not installed') from e
188
188
189 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
189 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
190 scaled = array.array('h', [int(x / normalization_factor * 32767) for x in data])
190 scaled = array.array('h', [int(x / normalization_factor * 32767) for x in data])
191 if sys.byteorder == 'big':
191 if sys.byteorder == 'big':
192 scaled.byteswap()
192 scaled.byteswap()
193 nchan = 1
193 nchan = 1
194 return scaled.tobytes(), nchan
194 return scaled.tobytes(), nchan
195
195
196 @staticmethod
196 @staticmethod
197 def _get_normalization_factor(max_abs_value, normalize):
197 def _get_normalization_factor(max_abs_value, normalize):
198 if not normalize and max_abs_value > 1:
198 if not normalize and max_abs_value > 1:
199 raise ValueError('Audio data must be between -1 and 1 when normalize=False.')
199 raise ValueError('Audio data must be between -1 and 1 when normalize=False.')
200 return max_abs_value if normalize else 1
200 return max_abs_value if normalize else 1
201
201
202 def _data_and_metadata(self):
202 def _data_and_metadata(self):
203 """shortcut for returning metadata with url information, if defined"""
203 """shortcut for returning metadata with url information, if defined"""
204 md = {}
204 md = {}
205 if self.url:
205 if self.url:
206 md['url'] = self.url
206 md['url'] = self.url
207 if md:
207 if md:
208 return self.data, md
208 return self.data, md
209 else:
209 else:
210 return self.data
210 return self.data
211
211
212 def _repr_html_(self):
212 def _repr_html_(self):
213 src = """
213 src = """
214 <audio {element_id} controls="controls" {autoplay}>
214 <audio {element_id} controls="controls" {autoplay}>
215 <source src="{src}" type="{type}" />
215 <source src="{src}" type="{type}" />
216 Your browser does not support the audio element.
216 Your browser does not support the audio element.
217 </audio>
217 </audio>
218 """
218 """
219 return src.format(src=self.src_attr(), type=self.mimetype, autoplay=self.autoplay_attr(),
219 return src.format(src=self.src_attr(), type=self.mimetype, autoplay=self.autoplay_attr(),
220 element_id=self.element_id_attr())
220 element_id=self.element_id_attr())
221
221
222 def src_attr(self):
222 def src_attr(self):
223 import base64
223 import base64
224 if self.embed and (self.data is not None):
224 if self.embed and (self.data is not None):
225 data = base64=base64.b64encode(self.data).decode('ascii')
225 data = base64=base64.b64encode(self.data).decode('ascii')
226 return """data:{type};base64,{base64}""".format(type=self.mimetype,
226 return """data:{type};base64,{base64}""".format(type=self.mimetype,
227 base64=data)
227 base64=data)
228 elif self.url is not None:
228 elif self.url is not None:
229 return self.url
229 return self.url
230 else:
230 else:
231 return ""
231 return ""
232
232
233 def autoplay_attr(self):
233 def autoplay_attr(self):
234 if(self.autoplay):
234 if(self.autoplay):
235 return 'autoplay="autoplay"'
235 return 'autoplay="autoplay"'
236 else:
236 else:
237 return ''
237 return ''
238
238
239 def element_id_attr(self):
239 def element_id_attr(self):
240 if (self.element_id):
240 if (self.element_id):
241 return 'id="{element_id}"'.format(element_id=self.element_id)
241 return 'id="{element_id}"'.format(element_id=self.element_id)
242 else:
242 else:
243 return ''
243 return ''
244
244
245 class IFrame(object):
245 class IFrame(object):
246 """
246 """
247 Generic class to embed an iframe in an IPython notebook
247 Generic class to embed an iframe in an IPython notebook
248 """
248 """
249
249
250 iframe = """
250 iframe = """
251 <iframe
251 <iframe
252 width="{width}"
252 width="{width}"
253 height="{height}"
253 height="{height}"
254 src="{src}{params}"
254 src="{src}{params}"
255 frameborder="0"
255 frameborder="0"
256 allowfullscreen
256 allowfullscreen
257 ></iframe>
257 ></iframe>
258 """
258 """
259
259
260 def __init__(self, src, width, height, **kwargs):
260 def __init__(self, src, width, height, **kwargs):
261 self.src = src
261 self.src = src
262 self.width = width
262 self.width = width
263 self.height = height
263 self.height = height
264 self.params = kwargs
264 self.params = kwargs
265
265
266 def _repr_html_(self):
266 def _repr_html_(self):
267 """return the embed iframe"""
267 """return the embed iframe"""
268 if self.params:
268 if self.params:
269 from urllib.parse import urlencode
269 from urllib.parse import urlencode
270 params = "?" + urlencode(self.params)
270 params = "?" + urlencode(self.params)
271 else:
271 else:
272 params = ""
272 params = ""
273 return self.iframe.format(src=self.src,
273 return self.iframe.format(src=self.src,
274 width=self.width,
274 width=self.width,
275 height=self.height,
275 height=self.height,
276 params=params)
276 params=params)
277
277
278 class YouTubeVideo(IFrame):
278 class YouTubeVideo(IFrame):
279 """Class for embedding a YouTube Video in an IPython session, based on its video id.
279 """Class for embedding a YouTube Video in an IPython session, based on its video id.
280
280
281 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
281 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
282 do::
282 do::
283
283
284 vid = YouTubeVideo("foo")
284 vid = YouTubeVideo("foo")
285 display(vid)
285 display(vid)
286
286
287 To start from 30 seconds::
287 To start from 30 seconds::
288
288
289 vid = YouTubeVideo("abc", start=30)
289 vid = YouTubeVideo("abc", start=30)
290 display(vid)
290 display(vid)
291
291
292 To calculate seconds from time as hours, minutes, seconds use
292 To calculate seconds from time as hours, minutes, seconds use
293 :class:`datetime.timedelta`::
293 :class:`datetime.timedelta`::
294
294
295 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
295 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
296
296
297 Other parameters can be provided as documented at
297 Other parameters can be provided as documented at
298 https://developers.google.com/youtube/player_parameters#Parameters
298 https://developers.google.com/youtube/player_parameters#Parameters
299
299
300 When converting the notebook using nbconvert, a jpeg representation of the video
300 When converting the notebook using nbconvert, a jpeg representation of the video
301 will be inserted in the document.
301 will be inserted in the document.
302 """
302 """
303
303
304 def __init__(self, id, width=400, height=300, **kwargs):
304 def __init__(self, id, width=400, height=300, **kwargs):
305 self.id=id
305 self.id=id
306 src = "https://www.youtube.com/embed/{0}".format(id)
306 src = "https://www.youtube.com/embed/{0}".format(id)
307 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
307 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
308
308
309 def _repr_jpeg_(self):
309 def _repr_jpeg_(self):
310 # Deferred import
310 # Deferred import
311 from urllib.request import urlopen
311 from urllib.request import urlopen
312
312
313 try:
313 try:
314 return urlopen("https://img.youtube.com/vi/{id}/hqdefault.jpg".format(id=self.id)).read()
314 return urlopen("https://img.youtube.com/vi/{id}/hqdefault.jpg".format(id=self.id)).read()
315 except IOError:
315 except IOError:
316 return None
316 return None
317
317
318 class VimeoVideo(IFrame):
318 class VimeoVideo(IFrame):
319 """
319 """
320 Class for embedding a Vimeo video in an IPython session, based on its video id.
320 Class for embedding a Vimeo video in an IPython session, based on its video id.
321 """
321 """
322
322
323 def __init__(self, id, width=400, height=300, **kwargs):
323 def __init__(self, id, width=400, height=300, **kwargs):
324 src="https://player.vimeo.com/video/{0}".format(id)
324 src="https://player.vimeo.com/video/{0}".format(id)
325 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
325 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
326
326
327 class ScribdDocument(IFrame):
327 class ScribdDocument(IFrame):
328 """
328 """
329 Class for embedding a Scribd document in an IPython session
329 Class for embedding a Scribd document in an IPython session
330
330
331 Use the start_page params to specify a starting point in the document
331 Use the start_page params to specify a starting point in the document
332 Use the view_mode params to specify display type one off scroll | slideshow | book
332 Use the view_mode params to specify display type one off scroll | slideshow | book
333
333
334 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
334 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
335
335
336 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
336 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
337 """
337 """
338
338
339 def __init__(self, id, width=400, height=300, **kwargs):
339 def __init__(self, id, width=400, height=300, **kwargs):
340 src="https://www.scribd.com/embeds/{0}/content".format(id)
340 src="https://www.scribd.com/embeds/{0}/content".format(id)
341 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
341 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
342
342
343 class FileLink(object):
343 class FileLink(object):
344 """Class for embedding a local file link in an IPython session, based on path
344 """Class for embedding a local file link in an IPython session, based on path
345
345
346 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
346 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
347
347
348 you would do::
348 you would do::
349
349
350 local_file = FileLink("my/data.txt")
350 local_file = FileLink("my/data.txt")
351 display(local_file)
351 display(local_file)
352
352
353 or in the HTML notebook, just::
353 or in the HTML notebook, just::
354
354
355 FileLink("my/data.txt")
355 FileLink("my/data.txt")
356 """
356 """
357
357
358 html_link_str = "<a href='%s' target='_blank'>%s</a>"
358 html_link_str = "<a href='%s' target='_blank'>%s</a>"
359
359
360 def __init__(self,
360 def __init__(self,
361 path,
361 path,
362 url_prefix='',
362 url_prefix='',
363 result_html_prefix='',
363 result_html_prefix='',
364 result_html_suffix='<br>'):
364 result_html_suffix='<br>'):
365 """
365 """
366 Parameters
366 Parameters
367 ----------
367 ----------
368 path : str
368 path : str
369 path to the file or directory that should be formatted
369 path to the file or directory that should be formatted
370 url_prefix : str
370 url_prefix : str
371 prefix to be prepended to all files to form a working link [default:
371 prefix to be prepended to all files to form a working link [default:
372 '']
372 '']
373 result_html_prefix : str
373 result_html_prefix : str
374 text to append to beginning to link [default: '']
374 text to append to beginning to link [default: '']
375 result_html_suffix : str
375 result_html_suffix : str
376 text to append at the end of link [default: '<br>']
376 text to append at the end of link [default: '<br>']
377 """
377 """
378 if isdir(path):
378 if isdir(path):
379 raise ValueError("Cannot display a directory using FileLink. "
379 raise ValueError("Cannot display a directory using FileLink. "
380 "Use FileLinks to display '%s'." % path)
380 "Use FileLinks to display '%s'." % path)
381 self.path = fsdecode(path)
381 self.path = fsdecode(path)
382 self.url_prefix = url_prefix
382 self.url_prefix = url_prefix
383 self.result_html_prefix = result_html_prefix
383 self.result_html_prefix = result_html_prefix
384 self.result_html_suffix = result_html_suffix
384 self.result_html_suffix = result_html_suffix
385
385
386 def _format_path(self):
386 def _format_path(self):
387 fp = ''.join([self.url_prefix, html_escape(self.path)])
387 fp = ''.join([self.url_prefix, html_escape(self.path)])
388 return ''.join([self.result_html_prefix,
388 return ''.join([self.result_html_prefix,
389 self.html_link_str % \
389 self.html_link_str % \
390 (fp, html_escape(self.path, quote=False)),
390 (fp, html_escape(self.path, quote=False)),
391 self.result_html_suffix])
391 self.result_html_suffix])
392
392
393 def _repr_html_(self):
393 def _repr_html_(self):
394 """return html link to file
394 """return html link to file
395 """
395 """
396 if not exists(self.path):
396 if not exists(self.path):
397 return ("Path (<tt>%s</tt>) doesn't exist. "
397 return ("Path (<tt>%s</tt>) doesn't exist. "
398 "It may still be in the process of "
398 "It may still be in the process of "
399 "being generated, or you may have the "
399 "being generated, or you may have the "
400 "incorrect path." % self.path)
400 "incorrect path." % self.path)
401
401
402 return self._format_path()
402 return self._format_path()
403
403
404 def __repr__(self):
404 def __repr__(self):
405 """return absolute path to file
405 """return absolute path to file
406 """
406 """
407 return abspath(self.path)
407 return abspath(self.path)
408
408
409 class FileLinks(FileLink):
409 class FileLinks(FileLink):
410 """Class for embedding local file links in an IPython session, based on path
410 """Class for embedding local file links in an IPython session, based on path
411
411
412 e.g. to embed links to files that were generated in the IPython notebook
412 e.g. to embed links to files that were generated in the IPython notebook
413 under ``my/data``, you would do::
413 under ``my/data``, you would do::
414
414
415 local_files = FileLinks("my/data")
415 local_files = FileLinks("my/data")
416 display(local_files)
416 display(local_files)
417
417
418 or in the HTML notebook, just::
418 or in the HTML notebook, just::
419
419
420 FileLinks("my/data")
420 FileLinks("my/data")
421 """
421 """
422 def __init__(self,
422 def __init__(self,
423 path,
423 path,
424 url_prefix='',
424 url_prefix='',
425 included_suffixes=None,
425 included_suffixes=None,
426 result_html_prefix='',
426 result_html_prefix='',
427 result_html_suffix='<br>',
427 result_html_suffix='<br>',
428 notebook_display_formatter=None,
428 notebook_display_formatter=None,
429 terminal_display_formatter=None,
429 terminal_display_formatter=None,
430 recursive=True):
430 recursive=True):
431 """
431 """
432 See :class:`FileLink` for the ``path``, ``url_prefix``,
432 See :class:`FileLink` for the ``path``, ``url_prefix``,
433 ``result_html_prefix`` and ``result_html_suffix`` parameters.
433 ``result_html_prefix`` and ``result_html_suffix`` parameters.
434
434
435 included_suffixes : list
435 included_suffixes : list
436 Filename suffixes to include when formatting output [default: include
436 Filename suffixes to include when formatting output [default: include
437 all files]
437 all files]
438
438
439 notebook_display_formatter : function
439 notebook_display_formatter : function
440 Used to format links for display in the notebook. See discussion of
440 Used to format links for display in the notebook. See discussion of
441 formatter functions below.
441 formatter functions below.
442
442
443 terminal_display_formatter : function
443 terminal_display_formatter : function
444 Used to format links for display in the terminal. See discussion of
444 Used to format links for display in the terminal. See discussion of
445 formatter functions below.
445 formatter functions below.
446
446
447 Formatter functions must be of the form::
447 Formatter functions must be of the form::
448
448
449 f(dirname, fnames, included_suffixes)
449 f(dirname, fnames, included_suffixes)
450
450
451 dirname : str
451 dirname : str
452 The name of a directory
452 The name of a directory
453 fnames : list
453 fnames : list
454 The files in that directory
454 The files in that directory
455 included_suffixes : list
455 included_suffixes : list
456 The file suffixes that should be included in the output (passing None
456 The file suffixes that should be included in the output (passing None
457 meansto include all suffixes in the output in the built-in formatters)
457 meansto include all suffixes in the output in the built-in formatters)
458 recursive : boolean
458 recursive : boolean
459 Whether to recurse into subdirectories. Default is True.
459 Whether to recurse into subdirectories. Default is True.
460
460
461 The function should return a list of lines that will be printed in the
461 The function should return a list of lines that will be printed in the
462 notebook (if passing notebook_display_formatter) or the terminal (if
462 notebook (if passing notebook_display_formatter) or the terminal (if
463 passing terminal_display_formatter). This function is iterated over for
463 passing terminal_display_formatter). This function is iterated over for
464 each directory in self.path. Default formatters are in place, can be
464 each directory in self.path. Default formatters are in place, can be
465 passed here to support alternative formatting.
465 passed here to support alternative formatting.
466
466
467 """
467 """
468 if isfile(path):
468 if isfile(path):
469 raise ValueError("Cannot display a file using FileLinks. "
469 raise ValueError("Cannot display a file using FileLinks. "
470 "Use FileLink to display '%s'." % path)
470 "Use FileLink to display '%s'." % path)
471 self.included_suffixes = included_suffixes
471 self.included_suffixes = included_suffixes
472 # remove trailing slashes for more consistent output formatting
472 # remove trailing slashes for more consistent output formatting
473 path = path.rstrip('/')
473 path = path.rstrip('/')
474
474
475 self.path = path
475 self.path = path
476 self.url_prefix = url_prefix
476 self.url_prefix = url_prefix
477 self.result_html_prefix = result_html_prefix
477 self.result_html_prefix = result_html_prefix
478 self.result_html_suffix = result_html_suffix
478 self.result_html_suffix = result_html_suffix
479
479
480 self.notebook_display_formatter = \
480 self.notebook_display_formatter = \
481 notebook_display_formatter or self._get_notebook_display_formatter()
481 notebook_display_formatter or self._get_notebook_display_formatter()
482 self.terminal_display_formatter = \
482 self.terminal_display_formatter = \
483 terminal_display_formatter or self._get_terminal_display_formatter()
483 terminal_display_formatter or self._get_terminal_display_formatter()
484
484
485 self.recursive = recursive
485 self.recursive = recursive
486
486
487 def _get_display_formatter(self,
487 def _get_display_formatter(self,
488 dirname_output_format,
488 dirname_output_format,
489 fname_output_format,
489 fname_output_format,
490 fp_format,
490 fp_format,
491 fp_cleaner=None):
491 fp_cleaner=None):
492 """ generate built-in formatter function
492 """ generate built-in formatter function
493
493
494 this is used to define both the notebook and terminal built-in
494 this is used to define both the notebook and terminal built-in
495 formatters as they only differ by some wrapper text for each entry
495 formatters as they only differ by some wrapper text for each entry
496
496
497 dirname_output_format: string to use for formatting directory
497 dirname_output_format: string to use for formatting directory
498 names, dirname will be substituted for a single "%s" which
498 names, dirname will be substituted for a single "%s" which
499 must appear in this string
499 must appear in this string
500 fname_output_format: string to use for formatting file names,
500 fname_output_format: string to use for formatting file names,
501 if a single "%s" appears in the string, fname will be substituted
501 if a single "%s" appears in the string, fname will be substituted
502 if two "%s" appear in the string, the path to fname will be
502 if two "%s" appear in the string, the path to fname will be
503 substituted for the first and fname will be substituted for the
503 substituted for the first and fname will be substituted for the
504 second
504 second
505 fp_format: string to use for formatting filepaths, must contain
505 fp_format: string to use for formatting filepaths, must contain
506 exactly two "%s" and the dirname will be substituted for the first
506 exactly two "%s" and the dirname will be substituted for the first
507 and fname will be substituted for the second
507 and fname will be substituted for the second
508 """
508 """
509 def f(dirname, fnames, included_suffixes=None):
509 def f(dirname, fnames, included_suffixes=None):
510 result = []
510 result = []
511 # begin by figuring out which filenames, if any,
511 # begin by figuring out which filenames, if any,
512 # are going to be displayed
512 # are going to be displayed
513 display_fnames = []
513 display_fnames = []
514 for fname in fnames:
514 for fname in fnames:
515 if (isfile(join(dirname,fname)) and
515 if (isfile(join(dirname,fname)) and
516 (included_suffixes is None or
516 (included_suffixes is None or
517 splitext(fname)[1] in included_suffixes)):
517 splitext(fname)[1] in included_suffixes)):
518 display_fnames.append(fname)
518 display_fnames.append(fname)
519
519
520 if len(display_fnames) == 0:
520 if len(display_fnames) == 0:
521 # if there are no filenames to display, don't print anything
521 # if there are no filenames to display, don't print anything
522 # (not even the directory name)
522 # (not even the directory name)
523 pass
523 pass
524 else:
524 else:
525 # otherwise print the formatted directory name followed by
525 # otherwise print the formatted directory name followed by
526 # the formatted filenames
526 # the formatted filenames
527 dirname_output_line = dirname_output_format % dirname
527 dirname_output_line = dirname_output_format % dirname
528 result.append(dirname_output_line)
528 result.append(dirname_output_line)
529 for fname in display_fnames:
529 for fname in display_fnames:
530 fp = fp_format % (dirname,fname)
530 fp = fp_format % (dirname,fname)
531 if fp_cleaner is not None:
531 if fp_cleaner is not None:
532 fp = fp_cleaner(fp)
532 fp = fp_cleaner(fp)
533 try:
533 try:
534 # output can include both a filepath and a filename...
534 # output can include both a filepath and a filename...
535 fname_output_line = fname_output_format % (fp, fname)
535 fname_output_line = fname_output_format % (fp, fname)
536 except TypeError:
536 except TypeError:
537 # ... or just a single filepath
537 # ... or just a single filepath
538 fname_output_line = fname_output_format % fname
538 fname_output_line = fname_output_format % fname
539 result.append(fname_output_line)
539 result.append(fname_output_line)
540 return result
540 return result
541 return f
541 return f
542
542
543 def _get_notebook_display_formatter(self,
543 def _get_notebook_display_formatter(self,
544 spacer="&nbsp;&nbsp;"):
544 spacer="&nbsp;&nbsp;"):
545 """ generate function to use for notebook formatting
545 """ generate function to use for notebook formatting
546 """
546 """
547 dirname_output_format = \
547 dirname_output_format = \
548 self.result_html_prefix + "%s/" + self.result_html_suffix
548 self.result_html_prefix + "%s/" + self.result_html_suffix
549 fname_output_format = \
549 fname_output_format = \
550 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
550 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
551 fp_format = self.url_prefix + '%s/%s'
551 fp_format = self.url_prefix + '%s/%s'
552 if sep == "\\":
552 if sep == "\\":
553 # Working on a platform where the path separator is "\", so
553 # Working on a platform where the path separator is "\", so
554 # must convert these to "/" for generating a URI
554 # must convert these to "/" for generating a URI
555 def fp_cleaner(fp):
555 def fp_cleaner(fp):
556 # Replace all occurrences of backslash ("\") with a forward
556 # Replace all occurrences of backslash ("\") with a forward
557 # slash ("/") - this is necessary on windows when a path is
557 # slash ("/") - this is necessary on windows when a path is
558 # provided as input, but we must link to a URI
558 # provided as input, but we must link to a URI
559 return fp.replace('\\','/')
559 return fp.replace('\\','/')
560 else:
560 else:
561 fp_cleaner = None
561 fp_cleaner = None
562
562
563 return self._get_display_formatter(dirname_output_format,
563 return self._get_display_formatter(dirname_output_format,
564 fname_output_format,
564 fname_output_format,
565 fp_format,
565 fp_format,
566 fp_cleaner)
566 fp_cleaner)
567
567
568 def _get_terminal_display_formatter(self,
568 def _get_terminal_display_formatter(self,
569 spacer=" "):
569 spacer=" "):
570 """ generate function to use for terminal formatting
570 """ generate function to use for terminal formatting
571 """
571 """
572 dirname_output_format = "%s/"
572 dirname_output_format = "%s/"
573 fname_output_format = spacer + "%s"
573 fname_output_format = spacer + "%s"
574 fp_format = '%s/%s'
574 fp_format = '%s/%s'
575
575
576 return self._get_display_formatter(dirname_output_format,
576 return self._get_display_formatter(dirname_output_format,
577 fname_output_format,
577 fname_output_format,
578 fp_format)
578 fp_format)
579
579
580 def _format_path(self):
580 def _format_path(self):
581 result_lines = []
581 result_lines = []
582 if self.recursive:
582 if self.recursive:
583 walked_dir = list(walk(self.path))
583 walked_dir = list(walk(self.path))
584 else:
584 else:
585 walked_dir = [next(walk(self.path))]
585 walked_dir = [next(walk(self.path))]
586 walked_dir.sort()
586 walked_dir.sort()
587 for dirname, subdirs, fnames in walked_dir:
587 for dirname, subdirs, fnames in walked_dir:
588 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
588 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
589 return '\n'.join(result_lines)
589 return '\n'.join(result_lines)
590
590
591 def __repr__(self):
591 def __repr__(self):
592 """return newline-separated absolute paths
592 """return newline-separated absolute paths
593 """
593 """
594 result_lines = []
594 result_lines = []
595 if self.recursive:
595 if self.recursive:
596 walked_dir = list(walk(self.path))
596 walked_dir = list(walk(self.path))
597 else:
597 else:
598 walked_dir = [next(walk(self.path))]
598 walked_dir = [next(walk(self.path))]
599 walked_dir.sort()
599 walked_dir.sort()
600 for dirname, subdirs, fnames in walked_dir:
600 for dirname, subdirs, fnames in walked_dir:
601 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
601 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
602 return '\n'.join(result_lines)
602 return '\n'.join(result_lines)
603
603
604
604
605 class Code(TextDisplayObject):
605 class Code(TextDisplayObject):
606 """Display syntax-highlighted source code.
606 """Display syntax-highlighted source code.
607
607
608 This uses Pygments to highlight the code for HTML and Latex output.
608 This uses Pygments to highlight the code for HTML and Latex output.
609
609
610 Parameters
610 Parameters
611 ----------
611 ----------
612 data : str
612 data : str
613 The code as a string
613 The code as a string
614 url : str
614 url : str
615 A URL to fetch the code from
615 A URL to fetch the code from
616 filename : str
616 filename : str
617 A local filename to load the code from
617 A local filename to load the code from
618 language : str
618 language : str
619 The short name of a Pygments lexer to use for highlighting.
619 The short name of a Pygments lexer to use for highlighting.
620 If not specified, it will guess the lexer based on the filename
620 If not specified, it will guess the lexer based on the filename
621 or the code. Available lexers: http://pygments.org/docs/lexers/
621 or the code. Available lexers: http://pygments.org/docs/lexers/
622 """
622 """
623 def __init__(self, data=None, url=None, filename=None, language=None):
623 def __init__(self, data=None, url=None, filename=None, language=None):
624 self.language = language
624 self.language = language
625 super().__init__(data=data, url=url, filename=filename)
625 super().__init__(data=data, url=url, filename=filename)
626
626
627 def _get_lexer(self):
627 def _get_lexer(self):
628 if self.language:
628 if self.language:
629 from pygments.lexers import get_lexer_by_name
629 from pygments.lexers import get_lexer_by_name
630 return get_lexer_by_name(self.language)
630 return get_lexer_by_name(self.language)
631 elif self.filename:
631 elif self.filename:
632 from pygments.lexers import get_lexer_for_filename
632 from pygments.lexers import get_lexer_for_filename
633 return get_lexer_for_filename(self.filename)
633 return get_lexer_for_filename(self.filename)
634 else:
634 else:
635 from pygments.lexers import guess_lexer
635 from pygments.lexers import guess_lexer
636 return guess_lexer(self.data)
636 return guess_lexer(self.data)
637
637
638 def __repr__(self):
638 def __repr__(self):
639 return self.data
639 return self.data
640
640
641 def _repr_html_(self):
641 def _repr_html_(self):
642 from pygments import highlight
642 from pygments import highlight
643 from pygments.formatters import HtmlFormatter
643 from pygments.formatters import HtmlFormatter
644 fmt = HtmlFormatter()
644 fmt = HtmlFormatter()
645 style = '<style>{}</style>'.format(fmt.get_style_defs('.output_html'))
645 style = '<style>{}</style>'.format(fmt.get_style_defs('.output_html'))
646 return style + highlight(self.data, self._get_lexer(), fmt)
646 return style + highlight(self.data, self._get_lexer(), fmt)
647
647
648 def _repr_latex_(self):
648 def _repr_latex_(self):
649 from pygments import highlight
649 from pygments import highlight
650 from pygments.formatters import LatexFormatter
650 from pygments.formatters import LatexFormatter
651 return highlight(self.data, self._get_lexer(), LatexFormatter())
651 return highlight(self.data, self._get_lexer(), LatexFormatter())
@@ -1,663 +1,663 b''
1 # coding: utf-8
1 # coding: utf-8
2 """
2 """
3 Deprecated since IPython 5.0
3 Deprecated since IPython 5.0
4
4
5 Inputhook management for GUI event loop integration.
5 Inputhook management for GUI event loop integration.
6 """
6 """
7
7
8 # Copyright (c) IPython Development Team.
8 # Copyright (c) IPython Development Team.
9 # Distributed under the terms of the Modified BSD License.
9 # Distributed under the terms of the Modified BSD License.
10
10
11 try:
11 try:
12 import ctypes
12 import ctypes
13 except ImportError:
13 except ImportError:
14 ctypes = None
14 ctypes = None
15 except SystemError: # IronPython issue, 2/8/2014
15 except SystemError: # IronPython issue, 2/8/2014
16 ctypes = None
16 ctypes = None
17 import os
17 import os
18 import platform
18 import platform
19 import sys
19 import sys
20 from distutils.version import LooseVersion as V
20 from distutils.version import LooseVersion as V
21
21
22 from warnings import warn
22 from warnings import warn
23
23
24
24
25 warn("`IPython.lib.inputhook` is deprecated since IPython 5.0 and will be removed in future versions.",
25 warn("`IPython.lib.inputhook` is deprecated since IPython 5.0 and will be removed in future versions.",
26 DeprecationWarning, stacklevel=2)
26 DeprecationWarning, stacklevel=2)
27
27
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Constants
30 # Constants
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33 # Constants for identifying the GUI toolkits.
33 # Constants for identifying the GUI toolkits.
34 GUI_WX = 'wx'
34 GUI_WX = 'wx'
35 GUI_QT = 'qt'
35 GUI_QT = 'qt'
36 GUI_QT4 = 'qt4'
36 GUI_QT4 = 'qt4'
37 GUI_GTK = 'gtk'
37 GUI_GTK = 'gtk'
38 GUI_TK = 'tk'
38 GUI_TK = 'tk'
39 GUI_OSX = 'osx'
39 GUI_OSX = 'osx'
40 GUI_GLUT = 'glut'
40 GUI_GLUT = 'glut'
41 GUI_PYGLET = 'pyglet'
41 GUI_PYGLET = 'pyglet'
42 GUI_GTK3 = 'gtk3'
42 GUI_GTK3 = 'gtk3'
43 GUI_NONE = 'none' # i.e. disable
43 GUI_NONE = 'none' # i.e. disable
44
44
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46 # Utilities
46 # Utilities
47 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
48
48
49 def _stdin_ready_posix():
49 def _stdin_ready_posix():
50 """Return True if there's something to read on stdin (posix version)."""
50 """Return True if there's something to read on stdin (posix version)."""
51 infds, outfds, erfds = select.select([sys.stdin],[],[],0)
51 infds, outfds, erfds = select.select([sys.stdin],[],[],0)
52 return bool(infds)
52 return bool(infds)
53
53
54 def _stdin_ready_nt():
54 def _stdin_ready_nt():
55 """Return True if there's something to read on stdin (nt version)."""
55 """Return True if there's something to read on stdin (nt version)."""
56 return msvcrt.kbhit()
56 return msvcrt.kbhit()
57
57
58 def _stdin_ready_other():
58 def _stdin_ready_other():
59 """Return True, assuming there's something to read on stdin."""
59 """Return True, assuming there's something to read on stdin."""
60 return True
60 return True
61
61
62 def _use_appnope():
62 def _use_appnope():
63 """Should we use appnope for dealing with OS X app nap?
63 """Should we use appnope for dealing with OS X app nap?
64
64
65 Checks if we are on OS X 10.9 or greater.
65 Checks if we are on OS X 10.9 or greater.
66 """
66 """
67 return sys.platform == 'darwin' and V(platform.mac_ver()[0]) >= V('10.9')
67 return sys.platform == 'darwin' and V(platform.mac_ver()[0]) >= V('10.9')
68
68
69 def _ignore_CTRL_C_posix():
69 def _ignore_CTRL_C_posix():
70 """Ignore CTRL+C (SIGINT)."""
70 """Ignore CTRL+C (SIGINT)."""
71 signal.signal(signal.SIGINT, signal.SIG_IGN)
71 signal.signal(signal.SIGINT, signal.SIG_IGN)
72
72
73 def _allow_CTRL_C_posix():
73 def _allow_CTRL_C_posix():
74 """Take CTRL+C into account (SIGINT)."""
74 """Take CTRL+C into account (SIGINT)."""
75 signal.signal(signal.SIGINT, signal.default_int_handler)
75 signal.signal(signal.SIGINT, signal.default_int_handler)
76
76
77 def _ignore_CTRL_C_other():
77 def _ignore_CTRL_C_other():
78 """Ignore CTRL+C (not implemented)."""
78 """Ignore CTRL+C (not implemented)."""
79 pass
79 pass
80
80
81 def _allow_CTRL_C_other():
81 def _allow_CTRL_C_other():
82 """Take CTRL+C into account (not implemented)."""
82 """Take CTRL+C into account (not implemented)."""
83 pass
83 pass
84
84
85 if os.name == 'posix':
85 if os.name == 'posix':
86 import select
86 import select
87 import signal
87 import signal
88 stdin_ready = _stdin_ready_posix
88 stdin_ready = _stdin_ready_posix
89 ignore_CTRL_C = _ignore_CTRL_C_posix
89 ignore_CTRL_C = _ignore_CTRL_C_posix
90 allow_CTRL_C = _allow_CTRL_C_posix
90 allow_CTRL_C = _allow_CTRL_C_posix
91 elif os.name == 'nt':
91 elif os.name == 'nt':
92 import msvcrt
92 import msvcrt
93 stdin_ready = _stdin_ready_nt
93 stdin_ready = _stdin_ready_nt
94 ignore_CTRL_C = _ignore_CTRL_C_other
94 ignore_CTRL_C = _ignore_CTRL_C_other
95 allow_CTRL_C = _allow_CTRL_C_other
95 allow_CTRL_C = _allow_CTRL_C_other
96 else:
96 else:
97 stdin_ready = _stdin_ready_other
97 stdin_ready = _stdin_ready_other
98 ignore_CTRL_C = _ignore_CTRL_C_other
98 ignore_CTRL_C = _ignore_CTRL_C_other
99 allow_CTRL_C = _allow_CTRL_C_other
99 allow_CTRL_C = _allow_CTRL_C_other
100
100
101
101
102 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
103 # Main InputHookManager class
103 # Main InputHookManager class
104 #-----------------------------------------------------------------------------
104 #-----------------------------------------------------------------------------
105
105
106
106
107 class InputHookManager(object):
107 class InputHookManager(object):
108 """DEPRECATED since IPython 5.0
108 """DEPRECATED since IPython 5.0
109
109
110 Manage PyOS_InputHook for different GUI toolkits.
110 Manage PyOS_InputHook for different GUI toolkits.
111
111
112 This class installs various hooks under ``PyOSInputHook`` to handle
112 This class installs various hooks under ``PyOSInputHook`` to handle
113 GUI event loop integration.
113 GUI event loop integration.
114 """
114 """
115
115
116 def __init__(self):
116 def __init__(self):
117 if ctypes is None:
117 if ctypes is None:
118 warn("IPython GUI event loop requires ctypes, %gui will not be available")
118 warn("IPython GUI event loop requires ctypes, %gui will not be available")
119 else:
119 else:
120 self.PYFUNC = ctypes.PYFUNCTYPE(ctypes.c_int)
120 self.PYFUNC = ctypes.PYFUNCTYPE(ctypes.c_int)
121 self.guihooks = {}
121 self.guihooks = {}
122 self.aliases = {}
122 self.aliases = {}
123 self.apps = {}
123 self.apps = {}
124 self._reset()
124 self._reset()
125
125
126 def _reset(self):
126 def _reset(self):
127 self._callback_pyfunctype = None
127 self._callback_pyfunctype = None
128 self._callback = None
128 self._callback = None
129 self._installed = False
129 self._installed = False
130 self._current_gui = None
130 self._current_gui = None
131
131
132 def get_pyos_inputhook(self):
132 def get_pyos_inputhook(self):
133 """DEPRECATED since IPython 5.0
133 """DEPRECATED since IPython 5.0
134
134
135 Return the current PyOS_InputHook as a ctypes.c_void_p."""
135 Return the current PyOS_InputHook as a ctypes.c_void_p."""
136 warn("`get_pyos_inputhook` is deprecated since IPython 5.0 and will be removed in future versions.",
136 warn("`get_pyos_inputhook` is deprecated since IPython 5.0 and will be removed in future versions.",
137 DeprecationWarning, stacklevel=2)
137 DeprecationWarning, stacklevel=2)
138 return ctypes.c_void_p.in_dll(ctypes.pythonapi,"PyOS_InputHook")
138 return ctypes.c_void_p.in_dll(ctypes.pythonapi,"PyOS_InputHook")
139
139
140 def get_pyos_inputhook_as_func(self):
140 def get_pyos_inputhook_as_func(self):
141 """DEPRECATED since IPython 5.0
141 """DEPRECATED since IPython 5.0
142
142
143 Return the current PyOS_InputHook as a ctypes.PYFUNCYPE."""
143 Return the current PyOS_InputHook as a ctypes.PYFUNCYPE."""
144 warn("`get_pyos_inputhook_as_func` is deprecated since IPython 5.0 and will be removed in future versions.",
144 warn("`get_pyos_inputhook_as_func` is deprecated since IPython 5.0 and will be removed in future versions.",
145 DeprecationWarning, stacklevel=2)
145 DeprecationWarning, stacklevel=2)
146 return self.PYFUNC.in_dll(ctypes.pythonapi,"PyOS_InputHook")
146 return self.PYFUNC.in_dll(ctypes.pythonapi,"PyOS_InputHook")
147
147
148 def set_inputhook(self, callback):
148 def set_inputhook(self, callback):
149 """DEPRECATED since IPython 5.0
149 """DEPRECATED since IPython 5.0
150
150
151 Set PyOS_InputHook to callback and return the previous one."""
151 Set PyOS_InputHook to callback and return the previous one."""
152 # On platforms with 'readline' support, it's all too likely to
152 # On platforms with 'readline' support, it's all too likely to
153 # have a KeyboardInterrupt signal delivered *even before* an
153 # have a KeyboardInterrupt signal delivered *even before* an
154 # initial ``try:`` clause in the callback can be executed, so
154 # initial ``try:`` clause in the callback can be executed, so
155 # we need to disable CTRL+C in this situation.
155 # we need to disable CTRL+C in this situation.
156 ignore_CTRL_C()
156 ignore_CTRL_C()
157 self._callback = callback
157 self._callback = callback
158 self._callback_pyfunctype = self.PYFUNC(callback)
158 self._callback_pyfunctype = self.PYFUNC(callback)
159 pyos_inputhook_ptr = self.get_pyos_inputhook()
159 pyos_inputhook_ptr = self.get_pyos_inputhook()
160 original = self.get_pyos_inputhook_as_func()
160 original = self.get_pyos_inputhook_as_func()
161 pyos_inputhook_ptr.value = \
161 pyos_inputhook_ptr.value = \
162 ctypes.cast(self._callback_pyfunctype, ctypes.c_void_p).value
162 ctypes.cast(self._callback_pyfunctype, ctypes.c_void_p).value
163 self._installed = True
163 self._installed = True
164 return original
164 return original
165
165
166 def clear_inputhook(self, app=None):
166 def clear_inputhook(self, app=None):
167 """DEPRECATED since IPython 5.0
167 """DEPRECATED since IPython 5.0
168
168
169 Set PyOS_InputHook to NULL and return the previous one.
169 Set PyOS_InputHook to NULL and return the previous one.
170
170
171 Parameters
171 Parameters
172 ----------
172 ----------
173 app : optional, ignored
173 app : optional, ignored
174 This parameter is allowed only so that clear_inputhook() can be
174 This parameter is allowed only so that clear_inputhook() can be
175 called with a similar interface as all the ``enable_*`` methods. But
175 called with a similar interface as all the ``enable_*`` methods. But
176 the actual value of the parameter is ignored. This uniform interface
176 the actual value of the parameter is ignored. This uniform interface
177 makes it easier to have user-level entry points in the main IPython
177 makes it easier to have user-level entry points in the main IPython
178 app like :meth:`enable_gui`."""
178 app like :meth:`enable_gui`."""
179 warn("`clear_inputhook` is deprecated since IPython 5.0 and will be removed in future versions.",
179 warn("`clear_inputhook` is deprecated since IPython 5.0 and will be removed in future versions.",
180 DeprecationWarning, stacklevel=2)
180 DeprecationWarning, stacklevel=2)
181 pyos_inputhook_ptr = self.get_pyos_inputhook()
181 pyos_inputhook_ptr = self.get_pyos_inputhook()
182 original = self.get_pyos_inputhook_as_func()
182 original = self.get_pyos_inputhook_as_func()
183 pyos_inputhook_ptr.value = ctypes.c_void_p(None).value
183 pyos_inputhook_ptr.value = ctypes.c_void_p(None).value
184 allow_CTRL_C()
184 allow_CTRL_C()
185 self._reset()
185 self._reset()
186 return original
186 return original
187
187
188 def clear_app_refs(self, gui=None):
188 def clear_app_refs(self, gui=None):
189 """DEPRECATED since IPython 5.0
189 """DEPRECATED since IPython 5.0
190
190
191 Clear IPython's internal reference to an application instance.
191 Clear IPython's internal reference to an application instance.
192
192
193 Whenever we create an app for a user on qt4 or wx, we hold a
193 Whenever we create an app for a user on qt4 or wx, we hold a
194 reference to the app. This is needed because in some cases bad things
194 reference to the app. This is needed because in some cases bad things
195 can happen if a user doesn't hold a reference themselves. This
195 can happen if a user doesn't hold a reference themselves. This
196 method is provided to clear the references we are holding.
196 method is provided to clear the references we are holding.
197
197
198 Parameters
198 Parameters
199 ----------
199 ----------
200 gui : None or str
200 gui : None or str
201 If None, clear all app references. If ('wx', 'qt4') clear
201 If None, clear all app references. If ('wx', 'qt4') clear
202 the app for that toolkit. References are not held for gtk or tk
202 the app for that toolkit. References are not held for gtk or tk
203 as those toolkits don't have the notion of an app.
203 as those toolkits don't have the notion of an app.
204 """
204 """
205 warn("`clear_app_refs` is deprecated since IPython 5.0 and will be removed in future versions.",
205 warn("`clear_app_refs` is deprecated since IPython 5.0 and will be removed in future versions.",
206 DeprecationWarning, stacklevel=2)
206 DeprecationWarning, stacklevel=2)
207 if gui is None:
207 if gui is None:
208 self.apps = {}
208 self.apps = {}
209 elif gui in self.apps:
209 elif gui in self.apps:
210 del self.apps[gui]
210 del self.apps[gui]
211
211
212 def register(self, toolkitname, *aliases):
212 def register(self, toolkitname, *aliases):
213 """DEPRECATED since IPython 5.0
213 """DEPRECATED since IPython 5.0
214
214
215 Register a class to provide the event loop for a given GUI.
215 Register a class to provide the event loop for a given GUI.
216
216
217 This is intended to be used as a class decorator. It should be passed
217 This is intended to be used as a class decorator. It should be passed
218 the names with which to register this GUI integration. The classes
218 the names with which to register this GUI integration. The classes
219 themselves should subclass :class:`InputHookBase`.
219 themselves should subclass :class:`InputHookBase`.
220
220
221 ::
221 ::
222
222
223 @inputhook_manager.register('qt')
223 @inputhook_manager.register('qt')
224 class QtInputHook(InputHookBase):
224 class QtInputHook(InputHookBase):
225 def enable(self, app=None):
225 def enable(self, app=None):
226 ...
226 ...
227 """
227 """
228 warn("`register` is deprecated since IPython 5.0 and will be removed in future versions.",
228 warn("`register` is deprecated since IPython 5.0 and will be removed in future versions.",
229 DeprecationWarning, stacklevel=2)
229 DeprecationWarning, stacklevel=2)
230 def decorator(cls):
230 def decorator(cls):
231 if ctypes is not None:
231 if ctypes is not None:
232 inst = cls(self)
232 inst = cls(self)
233 self.guihooks[toolkitname] = inst
233 self.guihooks[toolkitname] = inst
234 for a in aliases:
234 for a in aliases:
235 self.aliases[a] = toolkitname
235 self.aliases[a] = toolkitname
236 return cls
236 return cls
237 return decorator
237 return decorator
238
238
239 def current_gui(self):
239 def current_gui(self):
240 """DEPRECATED since IPython 5.0
240 """DEPRECATED since IPython 5.0
241
241
242 Return a string indicating the currently active GUI or None."""
242 Return a string indicating the currently active GUI or None."""
243 warn("`current_gui` is deprecated since IPython 5.0 and will be removed in future versions.",
243 warn("`current_gui` is deprecated since IPython 5.0 and will be removed in future versions.",
244 DeprecationWarning, stacklevel=2)
244 DeprecationWarning, stacklevel=2)
245 return self._current_gui
245 return self._current_gui
246
246
247 def enable_gui(self, gui=None, app=None):
247 def enable_gui(self, gui=None, app=None):
248 """DEPRECATED since IPython 5.0
248 """DEPRECATED since IPython 5.0
249
249
250 Switch amongst GUI input hooks by name.
250 Switch amongst GUI input hooks by name.
251
251
252 This is a higher level method than :meth:`set_inputhook` - it uses the
252 This is a higher level method than :meth:`set_inputhook` - it uses the
253 GUI name to look up a registered object which enables the input hook
253 GUI name to look up a registered object which enables the input hook
254 for that GUI.
254 for that GUI.
255
255
256 Parameters
256 Parameters
257 ----------
257 ----------
258 gui : optional, string or None
258 gui : optional, string or None
259 If None (or 'none'), clears input hook, otherwise it must be one
259 If None (or 'none'), clears input hook, otherwise it must be one
260 of the recognized GUI names (see ``GUI_*`` constants in module).
260 of the recognized GUI names (see ``GUI_*`` constants in module).
261
261
262 app : optional, existing application object.
262 app : optional, existing application object.
263 For toolkits that have the concept of a global app, you can supply an
263 For toolkits that have the concept of a global app, you can supply an
264 existing one. If not given, the toolkit will be probed for one, and if
264 existing one. If not given, the toolkit will be probed for one, and if
265 none is found, a new one will be created. Note that GTK does not have
265 none is found, a new one will be created. Note that GTK does not have
266 this concept, and passing an app if ``gui=="GTK"`` will raise an error.
266 this concept, and passing an app if ``gui=="GTK"`` will raise an error.
267
267
268 Returns
268 Returns
269 -------
269 -------
270 The output of the underlying gui switch routine, typically the actual
270 The output of the underlying gui switch routine, typically the actual
271 PyOS_InputHook wrapper object or the GUI toolkit app created, if there was
271 PyOS_InputHook wrapper object or the GUI toolkit app created, if there was
272 one.
272 one.
273 """
273 """
274 warn("`enable_gui` is deprecated since IPython 5.0 and will be removed in future versions.",
274 warn("`enable_gui` is deprecated since IPython 5.0 and will be removed in future versions.",
275 DeprecationWarning, stacklevel=2)
275 DeprecationWarning, stacklevel=2)
276 if gui in (None, GUI_NONE):
276 if gui in (None, GUI_NONE):
277 return self.disable_gui()
277 return self.disable_gui()
278
278
279 if gui in self.aliases:
279 if gui in self.aliases:
280 return self.enable_gui(self.aliases[gui], app)
280 return self.enable_gui(self.aliases[gui], app)
281
281
282 try:
282 try:
283 gui_hook = self.guihooks[gui]
283 gui_hook = self.guihooks[gui]
284 except KeyError:
284 except KeyError as e:
285 e = "Invalid GUI request {!r}, valid ones are: {}"
285 e = "Invalid GUI request {!r}, valid ones are: {}"
286 raise ValueError(e.format(gui, ', '.join(self.guihooks)))
286 raise ValueError(e.format(gui, ', '.join(self.guihooks))) from e
287 self._current_gui = gui
287 self._current_gui = gui
288
288
289 app = gui_hook.enable(app)
289 app = gui_hook.enable(app)
290 if app is not None:
290 if app is not None:
291 app._in_event_loop = True
291 app._in_event_loop = True
292 self.apps[gui] = app
292 self.apps[gui] = app
293 return app
293 return app
294
294
295 def disable_gui(self):
295 def disable_gui(self):
296 """DEPRECATED since IPython 5.0
296 """DEPRECATED since IPython 5.0
297
297
298 Disable GUI event loop integration.
298 Disable GUI event loop integration.
299
299
300 If an application was registered, this sets its ``_in_event_loop``
300 If an application was registered, this sets its ``_in_event_loop``
301 attribute to False. It then calls :meth:`clear_inputhook`.
301 attribute to False. It then calls :meth:`clear_inputhook`.
302 """
302 """
303 warn("`disable_gui` is deprecated since IPython 5.0 and will be removed in future versions.",
303 warn("`disable_gui` is deprecated since IPython 5.0 and will be removed in future versions.",
304 DeprecationWarning, stacklevel=2)
304 DeprecationWarning, stacklevel=2)
305 gui = self._current_gui
305 gui = self._current_gui
306 if gui in self.apps:
306 if gui in self.apps:
307 self.apps[gui]._in_event_loop = False
307 self.apps[gui]._in_event_loop = False
308 return self.clear_inputhook()
308 return self.clear_inputhook()
309
309
310 class InputHookBase(object):
310 class InputHookBase(object):
311 """DEPRECATED since IPython 5.0
311 """DEPRECATED since IPython 5.0
312
312
313 Base class for input hooks for specific toolkits.
313 Base class for input hooks for specific toolkits.
314
314
315 Subclasses should define an :meth:`enable` method with one argument, ``app``,
315 Subclasses should define an :meth:`enable` method with one argument, ``app``,
316 which will either be an instance of the toolkit's application class, or None.
316 which will either be an instance of the toolkit's application class, or None.
317 They may also define a :meth:`disable` method with no arguments.
317 They may also define a :meth:`disable` method with no arguments.
318 """
318 """
319 def __init__(self, manager):
319 def __init__(self, manager):
320 self.manager = manager
320 self.manager = manager
321
321
322 def disable(self):
322 def disable(self):
323 pass
323 pass
324
324
325 inputhook_manager = InputHookManager()
325 inputhook_manager = InputHookManager()
326
326
327 @inputhook_manager.register('osx')
327 @inputhook_manager.register('osx')
328 class NullInputHook(InputHookBase):
328 class NullInputHook(InputHookBase):
329 """DEPRECATED since IPython 5.0
329 """DEPRECATED since IPython 5.0
330
330
331 A null inputhook that doesn't need to do anything"""
331 A null inputhook that doesn't need to do anything"""
332 def enable(self, app=None):
332 def enable(self, app=None):
333 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
333 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
334 DeprecationWarning, stacklevel=2)
334 DeprecationWarning, stacklevel=2)
335
335
336 @inputhook_manager.register('wx')
336 @inputhook_manager.register('wx')
337 class WxInputHook(InputHookBase):
337 class WxInputHook(InputHookBase):
338 def enable(self, app=None):
338 def enable(self, app=None):
339 """DEPRECATED since IPython 5.0
339 """DEPRECATED since IPython 5.0
340
340
341 Enable event loop integration with wxPython.
341 Enable event loop integration with wxPython.
342
342
343 Parameters
343 Parameters
344 ----------
344 ----------
345 app : WX Application, optional.
345 app : WX Application, optional.
346 Running application to use. If not given, we probe WX for an
346 Running application to use. If not given, we probe WX for an
347 existing application object, and create a new one if none is found.
347 existing application object, and create a new one if none is found.
348
348
349 Notes
349 Notes
350 -----
350 -----
351 This methods sets the ``PyOS_InputHook`` for wxPython, which allows
351 This methods sets the ``PyOS_InputHook`` for wxPython, which allows
352 the wxPython to integrate with terminal based applications like
352 the wxPython to integrate with terminal based applications like
353 IPython.
353 IPython.
354
354
355 If ``app`` is not given we probe for an existing one, and return it if
355 If ``app`` is not given we probe for an existing one, and return it if
356 found. If no existing app is found, we create an :class:`wx.App` as
356 found. If no existing app is found, we create an :class:`wx.App` as
357 follows::
357 follows::
358
358
359 import wx
359 import wx
360 app = wx.App(redirect=False, clearSigInt=False)
360 app = wx.App(redirect=False, clearSigInt=False)
361 """
361 """
362 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
362 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
363 DeprecationWarning, stacklevel=2)
363 DeprecationWarning, stacklevel=2)
364 import wx
364 import wx
365
365
366 wx_version = V(wx.__version__).version
366 wx_version = V(wx.__version__).version
367
367
368 if wx_version < [2, 8]:
368 if wx_version < [2, 8]:
369 raise ValueError("requires wxPython >= 2.8, but you have %s" % wx.__version__)
369 raise ValueError("requires wxPython >= 2.8, but you have %s" % wx.__version__)
370
370
371 from IPython.lib.inputhookwx import inputhook_wx
371 from IPython.lib.inputhookwx import inputhook_wx
372 self.manager.set_inputhook(inputhook_wx)
372 self.manager.set_inputhook(inputhook_wx)
373 if _use_appnope():
373 if _use_appnope():
374 from appnope import nope
374 from appnope import nope
375 nope()
375 nope()
376
376
377 import wx
377 import wx
378 if app is None:
378 if app is None:
379 app = wx.GetApp()
379 app = wx.GetApp()
380 if app is None:
380 if app is None:
381 app = wx.App(redirect=False, clearSigInt=False)
381 app = wx.App(redirect=False, clearSigInt=False)
382
382
383 return app
383 return app
384
384
385 def disable(self):
385 def disable(self):
386 """DEPRECATED since IPython 5.0
386 """DEPRECATED since IPython 5.0
387
387
388 Disable event loop integration with wxPython.
388 Disable event loop integration with wxPython.
389
389
390 This restores appnapp on OS X
390 This restores appnapp on OS X
391 """
391 """
392 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
392 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
393 DeprecationWarning, stacklevel=2)
393 DeprecationWarning, stacklevel=2)
394 if _use_appnope():
394 if _use_appnope():
395 from appnope import nap
395 from appnope import nap
396 nap()
396 nap()
397
397
398 @inputhook_manager.register('qt', 'qt4')
398 @inputhook_manager.register('qt', 'qt4')
399 class Qt4InputHook(InputHookBase):
399 class Qt4InputHook(InputHookBase):
400 def enable(self, app=None):
400 def enable(self, app=None):
401 """DEPRECATED since IPython 5.0
401 """DEPRECATED since IPython 5.0
402
402
403 Enable event loop integration with PyQt4.
403 Enable event loop integration with PyQt4.
404
404
405 Parameters
405 Parameters
406 ----------
406 ----------
407 app : Qt Application, optional.
407 app : Qt Application, optional.
408 Running application to use. If not given, we probe Qt for an
408 Running application to use. If not given, we probe Qt for an
409 existing application object, and create a new one if none is found.
409 existing application object, and create a new one if none is found.
410
410
411 Notes
411 Notes
412 -----
412 -----
413 This methods sets the PyOS_InputHook for PyQt4, which allows
413 This methods sets the PyOS_InputHook for PyQt4, which allows
414 the PyQt4 to integrate with terminal based applications like
414 the PyQt4 to integrate with terminal based applications like
415 IPython.
415 IPython.
416
416
417 If ``app`` is not given we probe for an existing one, and return it if
417 If ``app`` is not given we probe for an existing one, and return it if
418 found. If no existing app is found, we create an :class:`QApplication`
418 found. If no existing app is found, we create an :class:`QApplication`
419 as follows::
419 as follows::
420
420
421 from PyQt4 import QtCore
421 from PyQt4 import QtCore
422 app = QtGui.QApplication(sys.argv)
422 app = QtGui.QApplication(sys.argv)
423 """
423 """
424 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
424 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
425 DeprecationWarning, stacklevel=2)
425 DeprecationWarning, stacklevel=2)
426 from IPython.lib.inputhookqt4 import create_inputhook_qt4
426 from IPython.lib.inputhookqt4 import create_inputhook_qt4
427 app, inputhook_qt4 = create_inputhook_qt4(self.manager, app)
427 app, inputhook_qt4 = create_inputhook_qt4(self.manager, app)
428 self.manager.set_inputhook(inputhook_qt4)
428 self.manager.set_inputhook(inputhook_qt4)
429 if _use_appnope():
429 if _use_appnope():
430 from appnope import nope
430 from appnope import nope
431 nope()
431 nope()
432
432
433 return app
433 return app
434
434
435 def disable_qt4(self):
435 def disable_qt4(self):
436 """DEPRECATED since IPython 5.0
436 """DEPRECATED since IPython 5.0
437
437
438 Disable event loop integration with PyQt4.
438 Disable event loop integration with PyQt4.
439
439
440 This restores appnapp on OS X
440 This restores appnapp on OS X
441 """
441 """
442 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
442 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
443 DeprecationWarning, stacklevel=2)
443 DeprecationWarning, stacklevel=2)
444 if _use_appnope():
444 if _use_appnope():
445 from appnope import nap
445 from appnope import nap
446 nap()
446 nap()
447
447
448
448
449 @inputhook_manager.register('qt5')
449 @inputhook_manager.register('qt5')
450 class Qt5InputHook(Qt4InputHook):
450 class Qt5InputHook(Qt4InputHook):
451 def enable(self, app=None):
451 def enable(self, app=None):
452 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
452 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
453 DeprecationWarning, stacklevel=2)
453 DeprecationWarning, stacklevel=2)
454 os.environ['QT_API'] = 'pyqt5'
454 os.environ['QT_API'] = 'pyqt5'
455 return Qt4InputHook.enable(self, app)
455 return Qt4InputHook.enable(self, app)
456
456
457
457
458 @inputhook_manager.register('gtk')
458 @inputhook_manager.register('gtk')
459 class GtkInputHook(InputHookBase):
459 class GtkInputHook(InputHookBase):
460 def enable(self, app=None):
460 def enable(self, app=None):
461 """DEPRECATED since IPython 5.0
461 """DEPRECATED since IPython 5.0
462
462
463 Enable event loop integration with PyGTK.
463 Enable event loop integration with PyGTK.
464
464
465 Parameters
465 Parameters
466 ----------
466 ----------
467 app : ignored
467 app : ignored
468 Ignored, it's only a placeholder to keep the call signature of all
468 Ignored, it's only a placeholder to keep the call signature of all
469 gui activation methods consistent, which simplifies the logic of
469 gui activation methods consistent, which simplifies the logic of
470 supporting magics.
470 supporting magics.
471
471
472 Notes
472 Notes
473 -----
473 -----
474 This methods sets the PyOS_InputHook for PyGTK, which allows
474 This methods sets the PyOS_InputHook for PyGTK, which allows
475 the PyGTK to integrate with terminal based applications like
475 the PyGTK to integrate with terminal based applications like
476 IPython.
476 IPython.
477 """
477 """
478 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
478 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
479 DeprecationWarning, stacklevel=2)
479 DeprecationWarning, stacklevel=2)
480 import gtk
480 import gtk
481 try:
481 try:
482 gtk.set_interactive(True)
482 gtk.set_interactive(True)
483 except AttributeError:
483 except AttributeError:
484 # For older versions of gtk, use our own ctypes version
484 # For older versions of gtk, use our own ctypes version
485 from IPython.lib.inputhookgtk import inputhook_gtk
485 from IPython.lib.inputhookgtk import inputhook_gtk
486 self.manager.set_inputhook(inputhook_gtk)
486 self.manager.set_inputhook(inputhook_gtk)
487
487
488
488
489 @inputhook_manager.register('tk')
489 @inputhook_manager.register('tk')
490 class TkInputHook(InputHookBase):
490 class TkInputHook(InputHookBase):
491 def enable(self, app=None):
491 def enable(self, app=None):
492 """DEPRECATED since IPython 5.0
492 """DEPRECATED since IPython 5.0
493
493
494 Enable event loop integration with Tk.
494 Enable event loop integration with Tk.
495
495
496 Parameters
496 Parameters
497 ----------
497 ----------
498 app : toplevel :class:`Tkinter.Tk` widget, optional.
498 app : toplevel :class:`Tkinter.Tk` widget, optional.
499 Running toplevel widget to use. If not given, we probe Tk for an
499 Running toplevel widget to use. If not given, we probe Tk for an
500 existing one, and create a new one if none is found.
500 existing one, and create a new one if none is found.
501
501
502 Notes
502 Notes
503 -----
503 -----
504 If you have already created a :class:`Tkinter.Tk` object, the only
504 If you have already created a :class:`Tkinter.Tk` object, the only
505 thing done by this method is to register with the
505 thing done by this method is to register with the
506 :class:`InputHookManager`, since creating that object automatically
506 :class:`InputHookManager`, since creating that object automatically
507 sets ``PyOS_InputHook``.
507 sets ``PyOS_InputHook``.
508 """
508 """
509 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
509 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
510 DeprecationWarning, stacklevel=2)
510 DeprecationWarning, stacklevel=2)
511 if app is None:
511 if app is None:
512 from tkinter import Tk
512 from tkinter import Tk
513 app = Tk()
513 app = Tk()
514 app.withdraw()
514 app.withdraw()
515 self.manager.apps[GUI_TK] = app
515 self.manager.apps[GUI_TK] = app
516 return app
516 return app
517
517
518
518
519 @inputhook_manager.register('glut')
519 @inputhook_manager.register('glut')
520 class GlutInputHook(InputHookBase):
520 class GlutInputHook(InputHookBase):
521 def enable(self, app=None):
521 def enable(self, app=None):
522 """DEPRECATED since IPython 5.0
522 """DEPRECATED since IPython 5.0
523
523
524 Enable event loop integration with GLUT.
524 Enable event loop integration with GLUT.
525
525
526 Parameters
526 Parameters
527 ----------
527 ----------
528
528
529 app : ignored
529 app : ignored
530 Ignored, it's only a placeholder to keep the call signature of all
530 Ignored, it's only a placeholder to keep the call signature of all
531 gui activation methods consistent, which simplifies the logic of
531 gui activation methods consistent, which simplifies the logic of
532 supporting magics.
532 supporting magics.
533
533
534 Notes
534 Notes
535 -----
535 -----
536
536
537 This methods sets the PyOS_InputHook for GLUT, which allows the GLUT to
537 This methods sets the PyOS_InputHook for GLUT, which allows the GLUT to
538 integrate with terminal based applications like IPython. Due to GLUT
538 integrate with terminal based applications like IPython. Due to GLUT
539 limitations, it is currently not possible to start the event loop
539 limitations, it is currently not possible to start the event loop
540 without first creating a window. You should thus not create another
540 without first creating a window. You should thus not create another
541 window but use instead the created one. See 'gui-glut.py' in the
541 window but use instead the created one. See 'gui-glut.py' in the
542 docs/examples/lib directory.
542 docs/examples/lib directory.
543
543
544 The default screen mode is set to:
544 The default screen mode is set to:
545 glut.GLUT_DOUBLE | glut.GLUT_RGBA | glut.GLUT_DEPTH
545 glut.GLUT_DOUBLE | glut.GLUT_RGBA | glut.GLUT_DEPTH
546 """
546 """
547 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
547 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
548 DeprecationWarning, stacklevel=2)
548 DeprecationWarning, stacklevel=2)
549
549
550 import OpenGL.GLUT as glut
550 import OpenGL.GLUT as glut
551 from IPython.lib.inputhookglut import glut_display_mode, \
551 from IPython.lib.inputhookglut import glut_display_mode, \
552 glut_close, glut_display, \
552 glut_close, glut_display, \
553 glut_idle, inputhook_glut
553 glut_idle, inputhook_glut
554
554
555 if GUI_GLUT not in self.manager.apps:
555 if GUI_GLUT not in self.manager.apps:
556 glut.glutInit( sys.argv )
556 glut.glutInit( sys.argv )
557 glut.glutInitDisplayMode( glut_display_mode )
557 glut.glutInitDisplayMode( glut_display_mode )
558 # This is specific to freeglut
558 # This is specific to freeglut
559 if bool(glut.glutSetOption):
559 if bool(glut.glutSetOption):
560 glut.glutSetOption( glut.GLUT_ACTION_ON_WINDOW_CLOSE,
560 glut.glutSetOption( glut.GLUT_ACTION_ON_WINDOW_CLOSE,
561 glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS )
561 glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS )
562 glut.glutCreateWindow( sys.argv[0] )
562 glut.glutCreateWindow( sys.argv[0] )
563 glut.glutReshapeWindow( 1, 1 )
563 glut.glutReshapeWindow( 1, 1 )
564 glut.glutHideWindow( )
564 glut.glutHideWindow( )
565 glut.glutWMCloseFunc( glut_close )
565 glut.glutWMCloseFunc( glut_close )
566 glut.glutDisplayFunc( glut_display )
566 glut.glutDisplayFunc( glut_display )
567 glut.glutIdleFunc( glut_idle )
567 glut.glutIdleFunc( glut_idle )
568 else:
568 else:
569 glut.glutWMCloseFunc( glut_close )
569 glut.glutWMCloseFunc( glut_close )
570 glut.glutDisplayFunc( glut_display )
570 glut.glutDisplayFunc( glut_display )
571 glut.glutIdleFunc( glut_idle)
571 glut.glutIdleFunc( glut_idle)
572 self.manager.set_inputhook( inputhook_glut )
572 self.manager.set_inputhook( inputhook_glut )
573
573
574
574
575 def disable(self):
575 def disable(self):
576 """DEPRECATED since IPython 5.0
576 """DEPRECATED since IPython 5.0
577
577
578 Disable event loop integration with glut.
578 Disable event loop integration with glut.
579
579
580 This sets PyOS_InputHook to NULL and set the display function to a
580 This sets PyOS_InputHook to NULL and set the display function to a
581 dummy one and set the timer to a dummy timer that will be triggered
581 dummy one and set the timer to a dummy timer that will be triggered
582 very far in the future.
582 very far in the future.
583 """
583 """
584 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
584 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
585 DeprecationWarning, stacklevel=2)
585 DeprecationWarning, stacklevel=2)
586 import OpenGL.GLUT as glut
586 import OpenGL.GLUT as glut
587 from glut_support import glutMainLoopEvent
587 from glut_support import glutMainLoopEvent
588
588
589 glut.glutHideWindow() # This is an event to be processed below
589 glut.glutHideWindow() # This is an event to be processed below
590 glutMainLoopEvent()
590 glutMainLoopEvent()
591 super(GlutInputHook, self).disable()
591 super(GlutInputHook, self).disable()
592
592
593 @inputhook_manager.register('pyglet')
593 @inputhook_manager.register('pyglet')
594 class PygletInputHook(InputHookBase):
594 class PygletInputHook(InputHookBase):
595 def enable(self, app=None):
595 def enable(self, app=None):
596 """DEPRECATED since IPython 5.0
596 """DEPRECATED since IPython 5.0
597
597
598 Enable event loop integration with pyglet.
598 Enable event loop integration with pyglet.
599
599
600 Parameters
600 Parameters
601 ----------
601 ----------
602 app : ignored
602 app : ignored
603 Ignored, it's only a placeholder to keep the call signature of all
603 Ignored, it's only a placeholder to keep the call signature of all
604 gui activation methods consistent, which simplifies the logic of
604 gui activation methods consistent, which simplifies the logic of
605 supporting magics.
605 supporting magics.
606
606
607 Notes
607 Notes
608 -----
608 -----
609 This methods sets the ``PyOS_InputHook`` for pyglet, which allows
609 This methods sets the ``PyOS_InputHook`` for pyglet, which allows
610 pyglet to integrate with terminal based applications like
610 pyglet to integrate with terminal based applications like
611 IPython.
611 IPython.
612
612
613 """
613 """
614 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
614 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
615 DeprecationWarning, stacklevel=2)
615 DeprecationWarning, stacklevel=2)
616 from IPython.lib.inputhookpyglet import inputhook_pyglet
616 from IPython.lib.inputhookpyglet import inputhook_pyglet
617 self.manager.set_inputhook(inputhook_pyglet)
617 self.manager.set_inputhook(inputhook_pyglet)
618 return app
618 return app
619
619
620
620
621 @inputhook_manager.register('gtk3')
621 @inputhook_manager.register('gtk3')
622 class Gtk3InputHook(InputHookBase):
622 class Gtk3InputHook(InputHookBase):
623 def enable(self, app=None):
623 def enable(self, app=None):
624 """DEPRECATED since IPython 5.0
624 """DEPRECATED since IPython 5.0
625
625
626 Enable event loop integration with Gtk3 (gir bindings).
626 Enable event loop integration with Gtk3 (gir bindings).
627
627
628 Parameters
628 Parameters
629 ----------
629 ----------
630 app : ignored
630 app : ignored
631 Ignored, it's only a placeholder to keep the call signature of all
631 Ignored, it's only a placeholder to keep the call signature of all
632 gui activation methods consistent, which simplifies the logic of
632 gui activation methods consistent, which simplifies the logic of
633 supporting magics.
633 supporting magics.
634
634
635 Notes
635 Notes
636 -----
636 -----
637 This methods sets the PyOS_InputHook for Gtk3, which allows
637 This methods sets the PyOS_InputHook for Gtk3, which allows
638 the Gtk3 to integrate with terminal based applications like
638 the Gtk3 to integrate with terminal based applications like
639 IPython.
639 IPython.
640 """
640 """
641 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
641 warn("This function is deprecated since IPython 5.0 and will be removed in future versions.",
642 DeprecationWarning, stacklevel=2)
642 DeprecationWarning, stacklevel=2)
643 from IPython.lib.inputhookgtk3 import inputhook_gtk3
643 from IPython.lib.inputhookgtk3 import inputhook_gtk3
644 self.manager.set_inputhook(inputhook_gtk3)
644 self.manager.set_inputhook(inputhook_gtk3)
645
645
646
646
647 clear_inputhook = inputhook_manager.clear_inputhook
647 clear_inputhook = inputhook_manager.clear_inputhook
648 set_inputhook = inputhook_manager.set_inputhook
648 set_inputhook = inputhook_manager.set_inputhook
649 current_gui = inputhook_manager.current_gui
649 current_gui = inputhook_manager.current_gui
650 clear_app_refs = inputhook_manager.clear_app_refs
650 clear_app_refs = inputhook_manager.clear_app_refs
651 enable_gui = inputhook_manager.enable_gui
651 enable_gui = inputhook_manager.enable_gui
652 disable_gui = inputhook_manager.disable_gui
652 disable_gui = inputhook_manager.disable_gui
653 register = inputhook_manager.register
653 register = inputhook_manager.register
654 guis = inputhook_manager.guihooks
654 guis = inputhook_manager.guihooks
655
655
656
656
657 def _deprecated_disable():
657 def _deprecated_disable():
658 warn("This function is deprecated since IPython 4.0 use disable_gui() instead",
658 warn("This function is deprecated since IPython 4.0 use disable_gui() instead",
659 DeprecationWarning, stacklevel=2)
659 DeprecationWarning, stacklevel=2)
660 inputhook_manager.disable_gui()
660 inputhook_manager.disable_gui()
661
661
662 disable_wx = disable_qt4 = disable_gtk = disable_gtk3 = disable_glut = \
662 disable_wx = disable_qt4 = disable_gtk = disable_gtk3 = disable_glut = \
663 disable_pyglet = disable_osx = _deprecated_disable
663 disable_pyglet = disable_osx = _deprecated_disable
@@ -1,172 +1,172 b''
1 # coding: utf-8
1 # coding: utf-8
2 """
2 """
3 GLUT Inputhook support functions
3 GLUT Inputhook support functions
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2008-2011 The IPython Development Team
7 # Copyright (C) 2008-2011 The IPython Development Team
8 #
8 #
9 # Distributed under the terms of the BSD License. The full license is in
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 # GLUT is quite an old library and it is difficult to ensure proper
13 # GLUT is quite an old library and it is difficult to ensure proper
14 # integration within IPython since original GLUT does not allow to handle
14 # integration within IPython since original GLUT does not allow to handle
15 # events one by one. Instead, it requires for the mainloop to be entered
15 # events one by one. Instead, it requires for the mainloop to be entered
16 # and never returned (there is not even a function to exit he
16 # and never returned (there is not even a function to exit he
17 # mainloop). Fortunately, there are alternatives such as freeglut
17 # mainloop). Fortunately, there are alternatives such as freeglut
18 # (available for linux and windows) and the OSX implementation gives
18 # (available for linux and windows) and the OSX implementation gives
19 # access to a glutCheckLoop() function that blocks itself until a new
19 # access to a glutCheckLoop() function that blocks itself until a new
20 # event is received. This means we have to setup the idle callback to
20 # event is received. This means we have to setup the idle callback to
21 # ensure we got at least one event that will unblock the function.
21 # ensure we got at least one event that will unblock the function.
22 #
22 #
23 # Furthermore, it is not possible to install these handlers without a window
23 # Furthermore, it is not possible to install these handlers without a window
24 # being first created. We choose to make this window invisible. This means that
24 # being first created. We choose to make this window invisible. This means that
25 # display mode options are set at this level and user won't be able to change
25 # display mode options are set at this level and user won't be able to change
26 # them later without modifying the code. This should probably be made available
26 # them later without modifying the code. This should probably be made available
27 # via IPython options system.
27 # via IPython options system.
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Imports
30 # Imports
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32 import os
32 import os
33 import sys
33 import sys
34 import time
34 import time
35 import signal
35 import signal
36 import OpenGL.GLUT as glut
36 import OpenGL.GLUT as glut
37 import OpenGL.platform as platform
37 import OpenGL.platform as platform
38 from timeit import default_timer as clock
38 from timeit import default_timer as clock
39
39
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41 # Constants
41 # Constants
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43
43
44 # Frame per second : 60
44 # Frame per second : 60
45 # Should probably be an IPython option
45 # Should probably be an IPython option
46 glut_fps = 60
46 glut_fps = 60
47
47
48
48
49 # Display mode : double buffeed + rgba + depth
49 # Display mode : double buffeed + rgba + depth
50 # Should probably be an IPython option
50 # Should probably be an IPython option
51 glut_display_mode = (glut.GLUT_DOUBLE |
51 glut_display_mode = (glut.GLUT_DOUBLE |
52 glut.GLUT_RGBA |
52 glut.GLUT_RGBA |
53 glut.GLUT_DEPTH)
53 glut.GLUT_DEPTH)
54
54
55 glutMainLoopEvent = None
55 glutMainLoopEvent = None
56 if sys.platform == 'darwin':
56 if sys.platform == 'darwin':
57 try:
57 try:
58 glutCheckLoop = platform.createBaseFunction(
58 glutCheckLoop = platform.createBaseFunction(
59 'glutCheckLoop', dll=platform.GLUT, resultType=None,
59 'glutCheckLoop', dll=platform.GLUT, resultType=None,
60 argTypes=[],
60 argTypes=[],
61 doc='glutCheckLoop( ) -> None',
61 doc='glutCheckLoop( ) -> None',
62 argNames=(),
62 argNames=(),
63 )
63 )
64 except AttributeError:
64 except AttributeError as e:
65 raise RuntimeError(
65 raise RuntimeError(
66 '''Your glut implementation does not allow interactive sessions. '''
66 '''Your glut implementation does not allow interactive sessions. '''
67 '''Consider installing freeglut.''')
67 '''Consider installing freeglut.''') from e
68 glutMainLoopEvent = glutCheckLoop
68 glutMainLoopEvent = glutCheckLoop
69 elif glut.HAVE_FREEGLUT:
69 elif glut.HAVE_FREEGLUT:
70 glutMainLoopEvent = glut.glutMainLoopEvent
70 glutMainLoopEvent = glut.glutMainLoopEvent
71 else:
71 else:
72 raise RuntimeError(
72 raise RuntimeError(
73 '''Your glut implementation does not allow interactive sessions. '''
73 '''Your glut implementation does not allow interactive sessions. '''
74 '''Consider installing freeglut.''')
74 '''Consider installing freeglut.''')
75
75
76
76
77 #-----------------------------------------------------------------------------
77 #-----------------------------------------------------------------------------
78 # Platform-dependent imports and functions
78 # Platform-dependent imports and functions
79 #-----------------------------------------------------------------------------
79 #-----------------------------------------------------------------------------
80
80
81 if os.name == 'posix':
81 if os.name == 'posix':
82 import select
82 import select
83
83
84 def stdin_ready():
84 def stdin_ready():
85 infds, outfds, erfds = select.select([sys.stdin],[],[],0)
85 infds, outfds, erfds = select.select([sys.stdin],[],[],0)
86 if infds:
86 if infds:
87 return True
87 return True
88 else:
88 else:
89 return False
89 return False
90
90
91 elif sys.platform == 'win32':
91 elif sys.platform == 'win32':
92 import msvcrt
92 import msvcrt
93
93
94 def stdin_ready():
94 def stdin_ready():
95 return msvcrt.kbhit()
95 return msvcrt.kbhit()
96
96
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 # Callback functions
98 # Callback functions
99 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
100
100
101 def glut_display():
101 def glut_display():
102 # Dummy display function
102 # Dummy display function
103 pass
103 pass
104
104
105 def glut_idle():
105 def glut_idle():
106 # Dummy idle function
106 # Dummy idle function
107 pass
107 pass
108
108
109 def glut_close():
109 def glut_close():
110 # Close function only hides the current window
110 # Close function only hides the current window
111 glut.glutHideWindow()
111 glut.glutHideWindow()
112 glutMainLoopEvent()
112 glutMainLoopEvent()
113
113
114 def glut_int_handler(signum, frame):
114 def glut_int_handler(signum, frame):
115 # Catch sigint and print the default message
115 # Catch sigint and print the default message
116 signal.signal(signal.SIGINT, signal.default_int_handler)
116 signal.signal(signal.SIGINT, signal.default_int_handler)
117 print('\nKeyboardInterrupt')
117 print('\nKeyboardInterrupt')
118 # Need to reprint the prompt at this stage
118 # Need to reprint the prompt at this stage
119
119
120
120
121
121
122 #-----------------------------------------------------------------------------
122 #-----------------------------------------------------------------------------
123 # Code
123 # Code
124 #-----------------------------------------------------------------------------
124 #-----------------------------------------------------------------------------
125 def inputhook_glut():
125 def inputhook_glut():
126 """Run the pyglet event loop by processing pending events only.
126 """Run the pyglet event loop by processing pending events only.
127
127
128 This keeps processing pending events until stdin is ready. After
128 This keeps processing pending events until stdin is ready. After
129 processing all pending events, a call to time.sleep is inserted. This is
129 processing all pending events, a call to time.sleep is inserted. This is
130 needed, otherwise, CPU usage is at 100%. This sleep time should be tuned
130 needed, otherwise, CPU usage is at 100%. This sleep time should be tuned
131 though for best performance.
131 though for best performance.
132 """
132 """
133 # We need to protect against a user pressing Control-C when IPython is
133 # We need to protect against a user pressing Control-C when IPython is
134 # idle and this is running. We trap KeyboardInterrupt and pass.
134 # idle and this is running. We trap KeyboardInterrupt and pass.
135
135
136 signal.signal(signal.SIGINT, glut_int_handler)
136 signal.signal(signal.SIGINT, glut_int_handler)
137
137
138 try:
138 try:
139 t = clock()
139 t = clock()
140
140
141 # Make sure the default window is set after a window has been closed
141 # Make sure the default window is set after a window has been closed
142 if glut.glutGetWindow() == 0:
142 if glut.glutGetWindow() == 0:
143 glut.glutSetWindow( 1 )
143 glut.glutSetWindow( 1 )
144 glutMainLoopEvent()
144 glutMainLoopEvent()
145 return 0
145 return 0
146
146
147 while not stdin_ready():
147 while not stdin_ready():
148 glutMainLoopEvent()
148 glutMainLoopEvent()
149 # We need to sleep at this point to keep the idle CPU load
149 # We need to sleep at this point to keep the idle CPU load
150 # low. However, if sleep to long, GUI response is poor. As
150 # low. However, if sleep to long, GUI response is poor. As
151 # a compromise, we watch how often GUI events are being processed
151 # a compromise, we watch how often GUI events are being processed
152 # and switch between a short and long sleep time. Here are some
152 # and switch between a short and long sleep time. Here are some
153 # stats useful in helping to tune this.
153 # stats useful in helping to tune this.
154 # time CPU load
154 # time CPU load
155 # 0.001 13%
155 # 0.001 13%
156 # 0.005 3%
156 # 0.005 3%
157 # 0.01 1.5%
157 # 0.01 1.5%
158 # 0.05 0.5%
158 # 0.05 0.5%
159 used_time = clock() - t
159 used_time = clock() - t
160 if used_time > 10.0:
160 if used_time > 10.0:
161 # print 'Sleep for 1 s' # dbg
161 # print 'Sleep for 1 s' # dbg
162 time.sleep(1.0)
162 time.sleep(1.0)
163 elif used_time > 0.1:
163 elif used_time > 0.1:
164 # Few GUI events coming in, so we can sleep longer
164 # Few GUI events coming in, so we can sleep longer
165 # print 'Sleep for 0.05 s' # dbg
165 # print 'Sleep for 0.05 s' # dbg
166 time.sleep(0.05)
166 time.sleep(0.05)
167 else:
167 else:
168 # Many GUI events coming in, so sleep only very little
168 # Many GUI events coming in, so sleep only very little
169 time.sleep(0.001)
169 time.sleep(0.001)
170 except KeyboardInterrupt:
170 except KeyboardInterrupt:
171 pass
171 pass
172 return 0
172 return 0
@@ -1,220 +1,220 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 IPython.utils.process import find_cmd, FindCmdError
15 from IPython.utils.process import find_cmd, FindCmdError
16 from traitlets.config import get_config
16 from traitlets.config import get_config
17 from traitlets.config.configurable import SingletonConfigurable
17 from traitlets.config.configurable import SingletonConfigurable
18 from traitlets import List, Bool, Unicode
18 from traitlets import List, Bool, Unicode
19 from IPython.utils.py3compat import cast_unicode
19 from IPython.utils.py3compat import cast_unicode
20
20
21
21
22 class LaTeXTool(SingletonConfigurable):
22 class LaTeXTool(SingletonConfigurable):
23 """An object to store configuration of the LaTeX tool."""
23 """An object to store configuration of the LaTeX tool."""
24 def _config_default(self):
24 def _config_default(self):
25 return get_config()
25 return get_config()
26
26
27 backends = List(
27 backends = List(
28 Unicode(), ["matplotlib", "dvipng"],
28 Unicode(), ["matplotlib", "dvipng"],
29 help="Preferred backend to draw LaTeX math equations. "
29 help="Preferred backend to draw LaTeX math equations. "
30 "Backends in the list are checked one by one and the first "
30 "Backends in the list are checked one by one and the first "
31 "usable one is used. Note that `matplotlib` backend "
31 "usable one is used. Note that `matplotlib` backend "
32 "is usable only for inline style equations. To draw "
32 "is usable only for inline style equations. To draw "
33 "display style equations, `dvipng` backend must be specified. ",
33 "display style equations, `dvipng` backend must be specified. ",
34 # It is a List instead of Enum, to make configuration more
34 # It is a List instead of Enum, to make configuration more
35 # flexible. For example, to use matplotlib mainly but dvipng
35 # flexible. For example, to use matplotlib mainly but dvipng
36 # for display style, the default ["matplotlib", "dvipng"] can
36 # for display style, the default ["matplotlib", "dvipng"] can
37 # be used. To NOT use dvipng so that other repr such as
37 # be used. To NOT use dvipng so that other repr such as
38 # unicode pretty printing is used, you can use ["matplotlib"].
38 # unicode pretty printing is used, you can use ["matplotlib"].
39 ).tag(config=True)
39 ).tag(config=True)
40
40
41 use_breqn = Bool(
41 use_breqn = Bool(
42 True,
42 True,
43 help="Use breqn.sty to automatically break long equations. "
43 help="Use breqn.sty to automatically break long equations. "
44 "This configuration takes effect only for dvipng backend.",
44 "This configuration takes effect only for dvipng backend.",
45 ).tag(config=True)
45 ).tag(config=True)
46
46
47 packages = List(
47 packages = List(
48 ['amsmath', 'amsthm', 'amssymb', 'bm'],
48 ['amsmath', 'amsthm', 'amssymb', 'bm'],
49 help="A list of packages to use for dvipng backend. "
49 help="A list of packages to use for dvipng backend. "
50 "'breqn' will be automatically appended when use_breqn=True.",
50 "'breqn' will be automatically appended when use_breqn=True.",
51 ).tag(config=True)
51 ).tag(config=True)
52
52
53 preamble = Unicode(
53 preamble = Unicode(
54 help="Additional preamble to use when generating LaTeX source "
54 help="Additional preamble to use when generating LaTeX source "
55 "for dvipng backend.",
55 "for dvipng backend.",
56 ).tag(config=True)
56 ).tag(config=True)
57
57
58
58
59 def latex_to_png(s, encode=False, backend=None, wrap=False, color='Black',
59 def latex_to_png(s, encode=False, backend=None, wrap=False, color='Black',
60 scale=1.0):
60 scale=1.0):
61 """Render a LaTeX string to PNG.
61 """Render a LaTeX string to PNG.
62
62
63 Parameters
63 Parameters
64 ----------
64 ----------
65 s : str
65 s : str
66 The raw string containing valid inline LaTeX.
66 The raw string containing valid inline LaTeX.
67 encode : bool, optional
67 encode : bool, optional
68 Should the PNG data base64 encoded to make it JSON'able.
68 Should the PNG data base64 encoded to make it JSON'able.
69 backend : {matplotlib, dvipng}
69 backend : {matplotlib, dvipng}
70 Backend for producing PNG data.
70 Backend for producing PNG data.
71 wrap : bool
71 wrap : bool
72 If true, Automatically wrap `s` as a LaTeX equation.
72 If true, Automatically wrap `s` as a LaTeX equation.
73 color : string
73 color : string
74 Foreground color name among dvipsnames, e.g. 'Maroon' or on hex RGB
74 Foreground color name among dvipsnames, e.g. 'Maroon' or on hex RGB
75 format, e.g. '#AA20FA'.
75 format, e.g. '#AA20FA'.
76 scale : float
76 scale : float
77 Scale factor for the resulting PNG.
77 Scale factor for the resulting PNG.
78
78
79 None is returned when the backend cannot be used.
79 None is returned when the backend cannot be used.
80
80
81 """
81 """
82 s = cast_unicode(s)
82 s = cast_unicode(s)
83 allowed_backends = LaTeXTool.instance().backends
83 allowed_backends = LaTeXTool.instance().backends
84 if backend is None:
84 if backend is None:
85 backend = allowed_backends[0]
85 backend = allowed_backends[0]
86 if backend not in allowed_backends:
86 if backend not in allowed_backends:
87 return None
87 return None
88 if backend == 'matplotlib':
88 if backend == 'matplotlib':
89 f = latex_to_png_mpl
89 f = latex_to_png_mpl
90 elif backend == 'dvipng':
90 elif backend == 'dvipng':
91 f = latex_to_png_dvipng
91 f = latex_to_png_dvipng
92 if color.startswith('#'):
92 if color.startswith('#'):
93 # Convert hex RGB color to LaTeX RGB color.
93 # Convert hex RGB color to LaTeX RGB color.
94 if len(color) == 7:
94 if len(color) == 7:
95 try:
95 try:
96 color = "RGB {}".format(" ".join([str(int(x, 16)) for x in
96 color = "RGB {}".format(" ".join([str(int(x, 16)) for x in
97 textwrap.wrap(color[1:], 2)]))
97 textwrap.wrap(color[1:], 2)]))
98 except ValueError:
98 except ValueError as e:
99 raise ValueError('Invalid color specification {}.'.format(color))
99 raise ValueError('Invalid color specification {}.'.format(color)) from e
100 else:
100 else:
101 raise ValueError('Invalid color specification {}.'.format(color))
101 raise ValueError('Invalid color specification {}.'.format(color))
102 else:
102 else:
103 raise ValueError('No such backend {0}'.format(backend))
103 raise ValueError('No such backend {0}'.format(backend))
104 bin_data = f(s, wrap, color, scale)
104 bin_data = f(s, wrap, color, scale)
105 if encode and bin_data:
105 if encode and bin_data:
106 bin_data = encodebytes(bin_data)
106 bin_data = encodebytes(bin_data)
107 return bin_data
107 return bin_data
108
108
109
109
110 def latex_to_png_mpl(s, wrap, color='Black', scale=1.0):
110 def latex_to_png_mpl(s, wrap, color='Black', scale=1.0):
111 try:
111 try:
112 from matplotlib import mathtext
112 from matplotlib import mathtext
113 from pyparsing import ParseFatalException
113 from pyparsing import ParseFatalException
114 except ImportError:
114 except ImportError:
115 return None
115 return None
116
116
117 # mpl mathtext doesn't support display math, force inline
117 # mpl mathtext doesn't support display math, force inline
118 s = s.replace('$$', '$')
118 s = s.replace('$$', '$')
119 if wrap:
119 if wrap:
120 s = u'${0}$'.format(s)
120 s = u'${0}$'.format(s)
121
121
122 try:
122 try:
123 mt = mathtext.MathTextParser('bitmap')
123 mt = mathtext.MathTextParser('bitmap')
124 f = BytesIO()
124 f = BytesIO()
125 dpi = 120*scale
125 dpi = 120*scale
126 mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
126 mt.to_png(f, s, fontsize=12, dpi=dpi, color=color)
127 return f.getvalue()
127 return f.getvalue()
128 except (ValueError, RuntimeError, ParseFatalException):
128 except (ValueError, RuntimeError, ParseFatalException):
129 return None
129 return None
130
130
131
131
132 def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
132 def latex_to_png_dvipng(s, wrap, color='Black', scale=1.0):
133 try:
133 try:
134 find_cmd('latex')
134 find_cmd('latex')
135 find_cmd('dvipng')
135 find_cmd('dvipng')
136 except FindCmdError:
136 except FindCmdError:
137 return None
137 return None
138 try:
138 try:
139 workdir = tempfile.mkdtemp()
139 workdir = tempfile.mkdtemp()
140 tmpfile = os.path.join(workdir, "tmp.tex")
140 tmpfile = os.path.join(workdir, "tmp.tex")
141 dvifile = os.path.join(workdir, "tmp.dvi")
141 dvifile = os.path.join(workdir, "tmp.dvi")
142 outfile = os.path.join(workdir, "tmp.png")
142 outfile = os.path.join(workdir, "tmp.png")
143
143
144 with open(tmpfile, "w", encoding='utf8') as f:
144 with open(tmpfile, "w", encoding='utf8') as f:
145 f.writelines(genelatex(s, wrap))
145 f.writelines(genelatex(s, wrap))
146
146
147 with open(os.devnull, 'wb') as devnull:
147 with open(os.devnull, 'wb') as devnull:
148 subprocess.check_call(
148 subprocess.check_call(
149 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
149 ["latex", "-halt-on-error", "-interaction", "batchmode", tmpfile],
150 cwd=workdir, stdout=devnull, stderr=devnull)
150 cwd=workdir, stdout=devnull, stderr=devnull)
151
151
152 resolution = round(150*scale)
152 resolution = round(150*scale)
153 subprocess.check_call(
153 subprocess.check_call(
154 ["dvipng", "-T", "tight", "-D", str(resolution), "-z", "9",
154 ["dvipng", "-T", "tight", "-D", str(resolution), "-z", "9",
155 "-bg", "transparent", "-o", outfile, dvifile, "-fg", color],
155 "-bg", "transparent", "-o", outfile, dvifile, "-fg", color],
156 cwd=workdir, stdout=devnull, stderr=devnull)
156 cwd=workdir, stdout=devnull, stderr=devnull)
157
157
158 with open(outfile, "rb") as f:
158 with open(outfile, "rb") as f:
159 return f.read()
159 return f.read()
160 except subprocess.CalledProcessError:
160 except subprocess.CalledProcessError:
161 return None
161 return None
162 finally:
162 finally:
163 shutil.rmtree(workdir)
163 shutil.rmtree(workdir)
164
164
165
165
166 def kpsewhich(filename):
166 def kpsewhich(filename):
167 """Invoke kpsewhich command with an argument `filename`."""
167 """Invoke kpsewhich command with an argument `filename`."""
168 try:
168 try:
169 find_cmd("kpsewhich")
169 find_cmd("kpsewhich")
170 proc = subprocess.Popen(
170 proc = subprocess.Popen(
171 ["kpsewhich", filename],
171 ["kpsewhich", filename],
172 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
172 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
173 (stdout, stderr) = proc.communicate()
173 (stdout, stderr) = proc.communicate()
174 return stdout.strip().decode('utf8', 'replace')
174 return stdout.strip().decode('utf8', 'replace')
175 except FindCmdError:
175 except FindCmdError:
176 pass
176 pass
177
177
178
178
179 def genelatex(body, wrap):
179 def genelatex(body, wrap):
180 """Generate LaTeX document for dvipng backend."""
180 """Generate LaTeX document for dvipng backend."""
181 lt = LaTeXTool.instance()
181 lt = LaTeXTool.instance()
182 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
182 breqn = wrap and lt.use_breqn and kpsewhich("breqn.sty")
183 yield r'\documentclass{article}'
183 yield r'\documentclass{article}'
184 packages = lt.packages
184 packages = lt.packages
185 if breqn:
185 if breqn:
186 packages = packages + ['breqn']
186 packages = packages + ['breqn']
187 for pack in packages:
187 for pack in packages:
188 yield r'\usepackage{{{0}}}'.format(pack)
188 yield r'\usepackage{{{0}}}'.format(pack)
189 yield r'\pagestyle{empty}'
189 yield r'\pagestyle{empty}'
190 if lt.preamble:
190 if lt.preamble:
191 yield lt.preamble
191 yield lt.preamble
192 yield r'\begin{document}'
192 yield r'\begin{document}'
193 if breqn:
193 if breqn:
194 yield r'\begin{dmath*}'
194 yield r'\begin{dmath*}'
195 yield body
195 yield body
196 yield r'\end{dmath*}'
196 yield r'\end{dmath*}'
197 elif wrap:
197 elif wrap:
198 yield u'$${0}$$'.format(body)
198 yield u'$${0}$$'.format(body)
199 else:
199 else:
200 yield body
200 yield body
201 yield u'\\end{document}'
201 yield u'\\end{document}'
202
202
203
203
204 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
204 _data_uri_template_png = u"""<img src="data:image/png;base64,%s" alt=%s />"""
205
205
206 def latex_to_html(s, alt='image'):
206 def latex_to_html(s, alt='image'):
207 """Render LaTeX to HTML with embedded PNG data using data URIs.
207 """Render LaTeX to HTML with embedded PNG data using data URIs.
208
208
209 Parameters
209 Parameters
210 ----------
210 ----------
211 s : str
211 s : str
212 The raw string containing valid inline LateX.
212 The raw string containing valid inline LateX.
213 alt : str
213 alt : str
214 The alt text to use for the HTML.
214 The alt text to use for the HTML.
215 """
215 """
216 base64_data = latex_to_png(s, encode=True).decode('ascii')
216 base64_data = latex_to_png(s, encode=True).decode('ascii')
217 if base64_data:
217 if base64_data:
218 return _data_uri_template_png % (base64_data, alt)
218 return _data_uri_template_png % (base64_data, alt)
219
219
220
220
@@ -1,119 +1,119 b''
1 """Find files and directories which IPython uses.
1 """Find files and directories which IPython uses.
2 """
2 """
3 import os.path
3 import os.path
4 import shutil
4 import shutil
5 import tempfile
5 import tempfile
6 from warnings import warn
6 from warnings import warn
7
7
8 import IPython
8 import IPython
9 from IPython.utils.importstring import import_item
9 from IPython.utils.importstring import import_item
10 from IPython.utils.path import (
10 from IPython.utils.path import (
11 get_home_dir, get_xdg_dir, get_xdg_cache_dir, compress_user, _writable_dir,
11 get_home_dir, get_xdg_dir, get_xdg_cache_dir, compress_user, _writable_dir,
12 ensure_dir_exists, fs_encoding)
12 ensure_dir_exists, fs_encoding)
13 from IPython.utils import py3compat
13 from IPython.utils import py3compat
14
14
15 def get_ipython_dir() -> str:
15 def get_ipython_dir() -> str:
16 """Get the IPython directory for this platform and user.
16 """Get the IPython directory for this platform and user.
17
17
18 This uses the logic in `get_home_dir` to find the home directory
18 This uses the logic in `get_home_dir` to find the home directory
19 and then adds .ipython to the end of the path.
19 and then adds .ipython to the end of the path.
20 """
20 """
21
21
22 env = os.environ
22 env = os.environ
23 pjoin = os.path.join
23 pjoin = os.path.join
24
24
25
25
26 ipdir_def = '.ipython'
26 ipdir_def = '.ipython'
27
27
28 home_dir = get_home_dir()
28 home_dir = get_home_dir()
29 xdg_dir = get_xdg_dir()
29 xdg_dir = get_xdg_dir()
30
30
31 if 'IPYTHON_DIR' in env:
31 if 'IPYTHON_DIR' in env:
32 warn('The environment variable IPYTHON_DIR is deprecated since IPython 3.0. '
32 warn('The environment variable IPYTHON_DIR is deprecated since IPython 3.0. '
33 'Please use IPYTHONDIR instead.', DeprecationWarning)
33 'Please use IPYTHONDIR instead.', DeprecationWarning)
34 ipdir = env.get('IPYTHONDIR', env.get('IPYTHON_DIR', None))
34 ipdir = env.get('IPYTHONDIR', env.get('IPYTHON_DIR', None))
35 if ipdir is None:
35 if ipdir is None:
36 # not set explicitly, use ~/.ipython
36 # not set explicitly, use ~/.ipython
37 ipdir = pjoin(home_dir, ipdir_def)
37 ipdir = pjoin(home_dir, ipdir_def)
38 if xdg_dir:
38 if xdg_dir:
39 # Several IPython versions (up to 1.x) defaulted to .config/ipython
39 # Several IPython versions (up to 1.x) defaulted to .config/ipython
40 # on Linux. We have decided to go back to using .ipython everywhere
40 # on Linux. We have decided to go back to using .ipython everywhere
41 xdg_ipdir = pjoin(xdg_dir, 'ipython')
41 xdg_ipdir = pjoin(xdg_dir, 'ipython')
42
42
43 if _writable_dir(xdg_ipdir):
43 if _writable_dir(xdg_ipdir):
44 cu = compress_user
44 cu = compress_user
45 if os.path.exists(ipdir):
45 if os.path.exists(ipdir):
46 warn(('Ignoring {0} in favour of {1}. Remove {0} to '
46 warn(('Ignoring {0} in favour of {1}. Remove {0} to '
47 'get rid of this message').format(cu(xdg_ipdir), cu(ipdir)))
47 'get rid of this message').format(cu(xdg_ipdir), cu(ipdir)))
48 elif os.path.islink(xdg_ipdir):
48 elif os.path.islink(xdg_ipdir):
49 warn(('{0} is deprecated. Move link to {1} to '
49 warn(('{0} is deprecated. Move link to {1} to '
50 'get rid of this message').format(cu(xdg_ipdir), cu(ipdir)))
50 'get rid of this message').format(cu(xdg_ipdir), cu(ipdir)))
51 else:
51 else:
52 warn('Moving {0} to {1}'.format(cu(xdg_ipdir), cu(ipdir)))
52 warn('Moving {0} to {1}'.format(cu(xdg_ipdir), cu(ipdir)))
53 shutil.move(xdg_ipdir, ipdir)
53 shutil.move(xdg_ipdir, ipdir)
54
54
55 ipdir = os.path.normpath(os.path.expanduser(ipdir))
55 ipdir = os.path.normpath(os.path.expanduser(ipdir))
56
56
57 if os.path.exists(ipdir) and not _writable_dir(ipdir):
57 if os.path.exists(ipdir) and not _writable_dir(ipdir):
58 # ipdir exists, but is not writable
58 # ipdir exists, but is not writable
59 warn("IPython dir '{0}' is not a writable location,"
59 warn("IPython dir '{0}' is not a writable location,"
60 " using a temp directory.".format(ipdir))
60 " using a temp directory.".format(ipdir))
61 ipdir = tempfile.mkdtemp()
61 ipdir = tempfile.mkdtemp()
62 elif not os.path.exists(ipdir):
62 elif not os.path.exists(ipdir):
63 parent = os.path.dirname(ipdir)
63 parent = os.path.dirname(ipdir)
64 if not _writable_dir(parent):
64 if not _writable_dir(parent):
65 # ipdir does not exist and parent isn't writable
65 # ipdir does not exist and parent isn't writable
66 warn("IPython parent '{0}' is not a writable location,"
66 warn("IPython parent '{0}' is not a writable location,"
67 " using a temp directory.".format(parent))
67 " using a temp directory.".format(parent))
68 ipdir = tempfile.mkdtemp()
68 ipdir = tempfile.mkdtemp()
69 assert isinstance(ipdir, str), "all path manipulation should be str(unicode), but are not."
69 assert isinstance(ipdir, str), "all path manipulation should be str(unicode), but are not."
70 return ipdir
70 return ipdir
71
71
72
72
73 def get_ipython_cache_dir() -> str:
73 def get_ipython_cache_dir() -> str:
74 """Get the cache directory it is created if it does not exist."""
74 """Get the cache directory it is created if it does not exist."""
75 xdgdir = get_xdg_cache_dir()
75 xdgdir = get_xdg_cache_dir()
76 if xdgdir is None:
76 if xdgdir is None:
77 return get_ipython_dir()
77 return get_ipython_dir()
78 ipdir = os.path.join(xdgdir, "ipython")
78 ipdir = os.path.join(xdgdir, "ipython")
79 if not os.path.exists(ipdir) and _writable_dir(xdgdir):
79 if not os.path.exists(ipdir) and _writable_dir(xdgdir):
80 ensure_dir_exists(ipdir)
80 ensure_dir_exists(ipdir)
81 elif not _writable_dir(xdgdir):
81 elif not _writable_dir(xdgdir):
82 return get_ipython_dir()
82 return get_ipython_dir()
83
83
84 return ipdir
84 return ipdir
85
85
86
86
87 def get_ipython_package_dir() -> str:
87 def get_ipython_package_dir() -> str:
88 """Get the base directory where IPython itself is installed."""
88 """Get the base directory where IPython itself is installed."""
89 ipdir = os.path.dirname(IPython.__file__)
89 ipdir = os.path.dirname(IPython.__file__)
90 assert isinstance(ipdir, str)
90 assert isinstance(ipdir, str)
91 return ipdir
91 return ipdir
92
92
93
93
94 def get_ipython_module_path(module_str):
94 def get_ipython_module_path(module_str):
95 """Find the path to an IPython module in this version of IPython.
95 """Find the path to an IPython module in this version of IPython.
96
96
97 This will always find the version of the module that is in this importable
97 This will always find the version of the module that is in this importable
98 IPython package. This will always return the path to the ``.py``
98 IPython package. This will always return the path to the ``.py``
99 version of the module.
99 version of the module.
100 """
100 """
101 if module_str == 'IPython':
101 if module_str == 'IPython':
102 return os.path.join(get_ipython_package_dir(), '__init__.py')
102 return os.path.join(get_ipython_package_dir(), '__init__.py')
103 mod = import_item(module_str)
103 mod = import_item(module_str)
104 the_path = mod.__file__.replace('.pyc', '.py')
104 the_path = mod.__file__.replace('.pyc', '.py')
105 the_path = the_path.replace('.pyo', '.py')
105 the_path = the_path.replace('.pyo', '.py')
106 return py3compat.cast_unicode(the_path, fs_encoding)
106 return py3compat.cast_unicode(the_path, fs_encoding)
107
107
108 def locate_profile(profile='default'):
108 def locate_profile(profile='default'):
109 """Find the path to the folder associated with a given profile.
109 """Find the path to the folder associated with a given profile.
110
110
111 I.e. find $IPYTHONDIR/profile_whatever.
111 I.e. find $IPYTHONDIR/profile_whatever.
112 """
112 """
113 from IPython.core.profiledir import ProfileDir, ProfileDirError
113 from IPython.core.profiledir import ProfileDir, ProfileDirError
114 try:
114 try:
115 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
115 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
116 except ProfileDirError:
116 except ProfileDirError as e:
117 # IOError makes more sense when people are expecting a path
117 # IOError makes more sense when people are expecting a path
118 raise IOError("Couldn't find profile %r" % profile)
118 raise IOError("Couldn't find profile %r" % profile) from e
119 return pd.location
119 return pd.location
@@ -1,155 +1,155 b''
1 """
1 """
2 Handlers for IPythonDirective's @doctest pseudo-decorator.
2 Handlers for IPythonDirective's @doctest pseudo-decorator.
3
3
4 The Sphinx extension that provides support for embedded IPython code provides
4 The Sphinx extension that provides support for embedded IPython code provides
5 a pseudo-decorator @doctest, which treats the input/output block as a
5 a pseudo-decorator @doctest, which treats the input/output block as a
6 doctest, raising a RuntimeError during doc generation if the actual output
6 doctest, raising a RuntimeError during doc generation if the actual output
7 (after running the input) does not match the expected output.
7 (after running the input) does not match the expected output.
8
8
9 An example usage is:
9 An example usage is:
10
10
11 .. code-block:: rst
11 .. code-block:: rst
12
12
13 .. ipython::
13 .. ipython::
14
14
15 In [1]: x = 1
15 In [1]: x = 1
16
16
17 @doctest
17 @doctest
18 In [2]: x + 2
18 In [2]: x + 2
19 Out[3]: 3
19 Out[3]: 3
20
20
21 One can also provide arguments to the decorator. The first argument should be
21 One can also provide arguments to the decorator. The first argument should be
22 the name of a custom handler. The specification of any other arguments is
22 the name of a custom handler. The specification of any other arguments is
23 determined by the handler. For example,
23 determined by the handler. For example,
24
24
25 .. code-block:: rst
25 .. code-block:: rst
26
26
27 .. ipython::
27 .. ipython::
28
28
29 @doctest float
29 @doctest float
30 In [154]: 0.1 + 0.2
30 In [154]: 0.1 + 0.2
31 Out[154]: 0.3
31 Out[154]: 0.3
32
32
33 allows the actual output ``0.30000000000000004`` to match the expected output
33 allows the actual output ``0.30000000000000004`` to match the expected output
34 due to a comparison with `np.allclose`.
34 due to a comparison with `np.allclose`.
35
35
36 This module contains handlers for the @doctest pseudo-decorator. Handlers
36 This module contains handlers for the @doctest pseudo-decorator. Handlers
37 should have the following function signature::
37 should have the following function signature::
38
38
39 handler(sphinx_shell, args, input_lines, found, submitted)
39 handler(sphinx_shell, args, input_lines, found, submitted)
40
40
41 where `sphinx_shell` is the embedded Sphinx shell, `args` contains the list
41 where `sphinx_shell` is the embedded Sphinx shell, `args` contains the list
42 of arguments that follow: '@doctest handler_name', `input_lines` contains
42 of arguments that follow: '@doctest handler_name', `input_lines` contains
43 a list of the lines relevant to the current doctest, `found` is a string
43 a list of the lines relevant to the current doctest, `found` is a string
44 containing the output from the IPython shell, and `submitted` is a string
44 containing the output from the IPython shell, and `submitted` is a string
45 containing the expected output from the IPython shell.
45 containing the expected output from the IPython shell.
46
46
47 Handlers must be registered in the `doctests` dict at the end of this module.
47 Handlers must be registered in the `doctests` dict at the end of this module.
48
48
49 """
49 """
50
50
51 def str_to_array(s):
51 def str_to_array(s):
52 """
52 """
53 Simplistic converter of strings from repr to float NumPy arrays.
53 Simplistic converter of strings from repr to float NumPy arrays.
54
54
55 If the repr representation has ellipsis in it, then this will fail.
55 If the repr representation has ellipsis in it, then this will fail.
56
56
57 Parameters
57 Parameters
58 ----------
58 ----------
59 s : str
59 s : str
60 The repr version of a NumPy array.
60 The repr version of a NumPy array.
61
61
62 Examples
62 Examples
63 --------
63 --------
64 >>> s = "array([ 0.3, inf, nan])"
64 >>> s = "array([ 0.3, inf, nan])"
65 >>> a = str_to_array(s)
65 >>> a = str_to_array(s)
66
66
67 """
67 """
68 import numpy as np
68 import numpy as np
69
69
70 # Need to make sure eval() knows about inf and nan.
70 # Need to make sure eval() knows about inf and nan.
71 # This also assumes default printoptions for NumPy.
71 # This also assumes default printoptions for NumPy.
72 from numpy import inf, nan
72 from numpy import inf, nan
73
73
74 if s.startswith(u'array'):
74 if s.startswith(u'array'):
75 # Remove array( and )
75 # Remove array( and )
76 s = s[6:-1]
76 s = s[6:-1]
77
77
78 if s.startswith(u'['):
78 if s.startswith(u'['):
79 a = np.array(eval(s), dtype=float)
79 a = np.array(eval(s), dtype=float)
80 else:
80 else:
81 # Assume its a regular float. Force 1D so we can index into it.
81 # Assume its a regular float. Force 1D so we can index into it.
82 a = np.atleast_1d(float(s))
82 a = np.atleast_1d(float(s))
83 return a
83 return a
84
84
85 def float_doctest(sphinx_shell, args, input_lines, found, submitted):
85 def float_doctest(sphinx_shell, args, input_lines, found, submitted):
86 """
86 """
87 Doctest which allow the submitted output to vary slightly from the input.
87 Doctest which allow the submitted output to vary slightly from the input.
88
88
89 Here is how it might appear in an rst file:
89 Here is how it might appear in an rst file:
90
90
91 .. code-block:: rst
91 .. code-block:: rst
92
92
93 .. ipython::
93 .. ipython::
94
94
95 @doctest float
95 @doctest float
96 In [1]: 0.1 + 0.2
96 In [1]: 0.1 + 0.2
97 Out[1]: 0.3
97 Out[1]: 0.3
98
98
99 """
99 """
100 import numpy as np
100 import numpy as np
101
101
102 if len(args) == 2:
102 if len(args) == 2:
103 rtol = 1e-05
103 rtol = 1e-05
104 atol = 1e-08
104 atol = 1e-08
105 else:
105 else:
106 # Both must be specified if any are specified.
106 # Both must be specified if any are specified.
107 try:
107 try:
108 rtol = float(args[2])
108 rtol = float(args[2])
109 atol = float(args[3])
109 atol = float(args[3])
110 except IndexError:
110 except IndexError as e:
111 e = ("Both `rtol` and `atol` must be specified "
111 e = ("Both `rtol` and `atol` must be specified "
112 "if either are specified: {0}".format(args))
112 "if either are specified: {0}".format(args))
113 raise IndexError(e)
113 raise IndexError(e) from e
114
114
115 try:
115 try:
116 submitted = str_to_array(submitted)
116 submitted = str_to_array(submitted)
117 found = str_to_array(found)
117 found = str_to_array(found)
118 except:
118 except:
119 # For example, if the array is huge and there are ellipsis in it.
119 # For example, if the array is huge and there are ellipsis in it.
120 error = True
120 error = True
121 else:
121 else:
122 found_isnan = np.isnan(found)
122 found_isnan = np.isnan(found)
123 submitted_isnan = np.isnan(submitted)
123 submitted_isnan = np.isnan(submitted)
124 error = not np.allclose(found_isnan, submitted_isnan)
124 error = not np.allclose(found_isnan, submitted_isnan)
125 error |= not np.allclose(found[~found_isnan],
125 error |= not np.allclose(found[~found_isnan],
126 submitted[~submitted_isnan],
126 submitted[~submitted_isnan],
127 rtol=rtol, atol=atol)
127 rtol=rtol, atol=atol)
128
128
129 TAB = ' ' * 4
129 TAB = ' ' * 4
130 directive = sphinx_shell.directive
130 directive = sphinx_shell.directive
131 if directive is None:
131 if directive is None:
132 source = 'Unavailable'
132 source = 'Unavailable'
133 content = 'Unavailable'
133 content = 'Unavailable'
134 else:
134 else:
135 source = directive.state.document.current_source
135 source = directive.state.document.current_source
136 # Add tabs and make into a single string.
136 # Add tabs and make into a single string.
137 content = '\n'.join([TAB + line for line in directive.content])
137 content = '\n'.join([TAB + line for line in directive.content])
138
138
139 if error:
139 if error:
140
140
141 e = ('doctest float comparison failure\n\n'
141 e = ('doctest float comparison failure\n\n'
142 'Document source: {0}\n\n'
142 'Document source: {0}\n\n'
143 'Raw content: \n{1}\n\n'
143 'Raw content: \n{1}\n\n'
144 'On input line(s):\n{TAB}{2}\n\n'
144 'On input line(s):\n{TAB}{2}\n\n'
145 'we found output:\n{TAB}{3}\n\n'
145 'we found output:\n{TAB}{3}\n\n'
146 'instead of the expected:\n{TAB}{4}\n\n')
146 'instead of the expected:\n{TAB}{4}\n\n')
147 e = e.format(source, content, '\n'.join(input_lines), repr(found),
147 e = e.format(source, content, '\n'.join(input_lines), repr(found),
148 repr(submitted), TAB=TAB)
148 repr(submitted), TAB=TAB)
149 raise RuntimeError(e)
149 raise RuntimeError(e)
150
150
151 # dict of allowable doctest handlers. The key represents the first argument
151 # dict of allowable doctest handlers. The key represents the first argument
152 # that must be given to @doctest in order to activate the handler.
152 # that must be given to @doctest in order to activate the handler.
153 doctests = {
153 doctests = {
154 'float': float_doctest,
154 'float': float_doctest,
155 }
155 }
@@ -1,203 +1,203 b''
1 """Extra magics for terminal use."""
1 """Extra magics for terminal use."""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6
6
7 from logging import error
7 from logging import error
8 import os
8 import os
9 import sys
9 import sys
10
10
11 from IPython.core.error import TryNext, UsageError
11 from IPython.core.error import TryNext, UsageError
12 from IPython.core.magic import Magics, magics_class, line_magic
12 from IPython.core.magic import Magics, magics_class, line_magic
13 from IPython.lib.clipboard import ClipboardEmpty
13 from IPython.lib.clipboard import ClipboardEmpty
14 from IPython.utils.text import SList, strip_email_quotes
14 from IPython.utils.text import SList, strip_email_quotes
15 from IPython.utils import py3compat
15 from IPython.utils import py3compat
16
16
17 def get_pasted_lines(sentinel, l_input=py3compat.input, quiet=False):
17 def get_pasted_lines(sentinel, l_input=py3compat.input, quiet=False):
18 """ Yield pasted lines until the user enters the given sentinel value.
18 """ Yield pasted lines until the user enters the given sentinel value.
19 """
19 """
20 if not quiet:
20 if not quiet:
21 print("Pasting code; enter '%s' alone on the line to stop or use Ctrl-D." \
21 print("Pasting code; enter '%s' alone on the line to stop or use Ctrl-D." \
22 % sentinel)
22 % sentinel)
23 prompt = ":"
23 prompt = ":"
24 else:
24 else:
25 prompt = ""
25 prompt = ""
26 while True:
26 while True:
27 try:
27 try:
28 l = l_input(prompt)
28 l = l_input(prompt)
29 if l == sentinel:
29 if l == sentinel:
30 return
30 return
31 else:
31 else:
32 yield l
32 yield l
33 except EOFError:
33 except EOFError:
34 print('<EOF>')
34 print('<EOF>')
35 return
35 return
36
36
37
37
38 @magics_class
38 @magics_class
39 class TerminalMagics(Magics):
39 class TerminalMagics(Magics):
40 def __init__(self, shell):
40 def __init__(self, shell):
41 super(TerminalMagics, self).__init__(shell)
41 super(TerminalMagics, self).__init__(shell)
42
42
43 def store_or_execute(self, block, name):
43 def store_or_execute(self, block, name):
44 """ Execute a block, or store it in a variable, per the user's request.
44 """ Execute a block, or store it in a variable, per the user's request.
45 """
45 """
46 if name:
46 if name:
47 # If storing it for further editing
47 # If storing it for further editing
48 self.shell.user_ns[name] = SList(block.splitlines())
48 self.shell.user_ns[name] = SList(block.splitlines())
49 print("Block assigned to '%s'" % name)
49 print("Block assigned to '%s'" % name)
50 else:
50 else:
51 b = self.preclean_input(block)
51 b = self.preclean_input(block)
52 self.shell.user_ns['pasted_block'] = b
52 self.shell.user_ns['pasted_block'] = b
53 self.shell.using_paste_magics = True
53 self.shell.using_paste_magics = True
54 try:
54 try:
55 self.shell.run_cell(b)
55 self.shell.run_cell(b)
56 finally:
56 finally:
57 self.shell.using_paste_magics = False
57 self.shell.using_paste_magics = False
58
58
59 def preclean_input(self, block):
59 def preclean_input(self, block):
60 lines = block.splitlines()
60 lines = block.splitlines()
61 while lines and not lines[0].strip():
61 while lines and not lines[0].strip():
62 lines = lines[1:]
62 lines = lines[1:]
63 return strip_email_quotes('\n'.join(lines))
63 return strip_email_quotes('\n'.join(lines))
64
64
65 def rerun_pasted(self, name='pasted_block'):
65 def rerun_pasted(self, name='pasted_block'):
66 """ Rerun a previously pasted command.
66 """ Rerun a previously pasted command.
67 """
67 """
68 b = self.shell.user_ns.get(name)
68 b = self.shell.user_ns.get(name)
69
69
70 # Sanity checks
70 # Sanity checks
71 if b is None:
71 if b is None:
72 raise UsageError('No previous pasted block available')
72 raise UsageError('No previous pasted block available')
73 if not isinstance(b, str):
73 if not isinstance(b, str):
74 raise UsageError(
74 raise UsageError(
75 "Variable 'pasted_block' is not a string, can't execute")
75 "Variable 'pasted_block' is not a string, can't execute")
76
76
77 print("Re-executing '%s...' (%d chars)"% (b.split('\n',1)[0], len(b)))
77 print("Re-executing '%s...' (%d chars)"% (b.split('\n',1)[0], len(b)))
78 self.shell.run_cell(b)
78 self.shell.run_cell(b)
79
79
80 @line_magic
80 @line_magic
81 def autoindent(self, parameter_s = ''):
81 def autoindent(self, parameter_s = ''):
82 """Toggle autoindent on/off (deprecated)"""
82 """Toggle autoindent on/off (deprecated)"""
83 self.shell.set_autoindent()
83 self.shell.set_autoindent()
84 print("Automatic indentation is:",['OFF','ON'][self.shell.autoindent])
84 print("Automatic indentation is:",['OFF','ON'][self.shell.autoindent])
85
85
86 @line_magic
86 @line_magic
87 def cpaste(self, parameter_s=''):
87 def cpaste(self, parameter_s=''):
88 """Paste & execute a pre-formatted code block from clipboard.
88 """Paste & execute a pre-formatted code block from clipboard.
89
89
90 You must terminate the block with '--' (two minus-signs) or Ctrl-D
90 You must terminate the block with '--' (two minus-signs) or Ctrl-D
91 alone on the line. You can also provide your own sentinel with '%paste
91 alone on the line. You can also provide your own sentinel with '%paste
92 -s %%' ('%%' is the new sentinel for this operation).
92 -s %%' ('%%' is the new sentinel for this operation).
93
93
94 The block is dedented prior to execution to enable execution of method
94 The block is dedented prior to execution to enable execution of method
95 definitions. '>' and '+' characters at the beginning of a line are
95 definitions. '>' and '+' characters at the beginning of a line are
96 ignored, to allow pasting directly from e-mails, diff files and
96 ignored, to allow pasting directly from e-mails, diff files and
97 doctests (the '...' continuation prompt is also stripped). The
97 doctests (the '...' continuation prompt is also stripped). The
98 executed block is also assigned to variable named 'pasted_block' for
98 executed block is also assigned to variable named 'pasted_block' for
99 later editing with '%edit pasted_block'.
99 later editing with '%edit pasted_block'.
100
100
101 You can also pass a variable name as an argument, e.g. '%cpaste foo'.
101 You can also pass a variable name as an argument, e.g. '%cpaste foo'.
102 This assigns the pasted block to variable 'foo' as string, without
102 This assigns the pasted block to variable 'foo' as string, without
103 dedenting or executing it (preceding >>> and + is still stripped)
103 dedenting or executing it (preceding >>> and + is still stripped)
104
104
105 '%cpaste -r' re-executes the block previously entered by cpaste.
105 '%cpaste -r' re-executes the block previously entered by cpaste.
106 '%cpaste -q' suppresses any additional output messages.
106 '%cpaste -q' suppresses any additional output messages.
107
107
108 Do not be alarmed by garbled output on Windows (it's a readline bug).
108 Do not be alarmed by garbled output on Windows (it's a readline bug).
109 Just press enter and type -- (and press enter again) and the block
109 Just press enter and type -- (and press enter again) and the block
110 will be what was just pasted.
110 will be what was just pasted.
111
111
112 IPython statements (magics, shell escapes) are not supported (yet).
112 IPython statements (magics, shell escapes) are not supported (yet).
113
113
114 See also
114 See also
115 --------
115 --------
116 paste: automatically pull code from clipboard.
116 paste: automatically pull code from clipboard.
117
117
118 Examples
118 Examples
119 --------
119 --------
120 ::
120 ::
121
121
122 In [8]: %cpaste
122 In [8]: %cpaste
123 Pasting code; enter '--' alone on the line to stop.
123 Pasting code; enter '--' alone on the line to stop.
124 :>>> a = ["world!", "Hello"]
124 :>>> a = ["world!", "Hello"]
125 :>>> print " ".join(sorted(a))
125 :>>> print " ".join(sorted(a))
126 :--
126 :--
127 Hello world!
127 Hello world!
128 """
128 """
129 opts, name = self.parse_options(parameter_s, 'rqs:', mode='string')
129 opts, name = self.parse_options(parameter_s, 'rqs:', mode='string')
130 if 'r' in opts:
130 if 'r' in opts:
131 self.rerun_pasted()
131 self.rerun_pasted()
132 return
132 return
133
133
134 quiet = ('q' in opts)
134 quiet = ('q' in opts)
135
135
136 sentinel = opts.get('s', u'--')
136 sentinel = opts.get('s', u'--')
137 block = '\n'.join(get_pasted_lines(sentinel, quiet=quiet))
137 block = '\n'.join(get_pasted_lines(sentinel, quiet=quiet))
138 self.store_or_execute(block, name)
138 self.store_or_execute(block, name)
139
139
140 @line_magic
140 @line_magic
141 def paste(self, parameter_s=''):
141 def paste(self, parameter_s=''):
142 """Paste & execute a pre-formatted code block from clipboard.
142 """Paste & execute a pre-formatted code block from clipboard.
143
143
144 The text is pulled directly from the clipboard without user
144 The text is pulled directly from the clipboard without user
145 intervention and printed back on the screen before execution (unless
145 intervention and printed back on the screen before execution (unless
146 the -q flag is given to force quiet mode).
146 the -q flag is given to force quiet mode).
147
147
148 The block is dedented prior to execution to enable execution of method
148 The block is dedented prior to execution to enable execution of method
149 definitions. '>' and '+' characters at the beginning of a line are
149 definitions. '>' and '+' characters at the beginning of a line are
150 ignored, to allow pasting directly from e-mails, diff files and
150 ignored, to allow pasting directly from e-mails, diff files and
151 doctests (the '...' continuation prompt is also stripped). The
151 doctests (the '...' continuation prompt is also stripped). The
152 executed block is also assigned to variable named 'pasted_block' for
152 executed block is also assigned to variable named 'pasted_block' for
153 later editing with '%edit pasted_block'.
153 later editing with '%edit pasted_block'.
154
154
155 You can also pass a variable name as an argument, e.g. '%paste foo'.
155 You can also pass a variable name as an argument, e.g. '%paste foo'.
156 This assigns the pasted block to variable 'foo' as string, without
156 This assigns the pasted block to variable 'foo' as string, without
157 executing it (preceding >>> and + is still stripped).
157 executing it (preceding >>> and + is still stripped).
158
158
159 Options:
159 Options:
160
160
161 -r: re-executes the block previously entered by cpaste.
161 -r: re-executes the block previously entered by cpaste.
162
162
163 -q: quiet mode: do not echo the pasted text back to the terminal.
163 -q: quiet mode: do not echo the pasted text back to the terminal.
164
164
165 IPython statements (magics, shell escapes) are not supported (yet).
165 IPython statements (magics, shell escapes) are not supported (yet).
166
166
167 See also
167 See also
168 --------
168 --------
169 cpaste: manually paste code into terminal until you mark its end.
169 cpaste: manually paste code into terminal until you mark its end.
170 """
170 """
171 opts, name = self.parse_options(parameter_s, 'rq', mode='string')
171 opts, name = self.parse_options(parameter_s, 'rq', mode='string')
172 if 'r' in opts:
172 if 'r' in opts:
173 self.rerun_pasted()
173 self.rerun_pasted()
174 return
174 return
175 try:
175 try:
176 block = self.shell.hooks.clipboard_get()
176 block = self.shell.hooks.clipboard_get()
177 except TryNext as clipboard_exc:
177 except TryNext as clipboard_exc:
178 message = getattr(clipboard_exc, 'args')
178 message = getattr(clipboard_exc, 'args')
179 if message:
179 if message:
180 error(message[0])
180 error(message[0])
181 else:
181 else:
182 error('Could not get text from the clipboard.')
182 error('Could not get text from the clipboard.')
183 return
183 return
184 except ClipboardEmpty:
184 except ClipboardEmpty as e:
185 raise UsageError("The clipboard appears to be empty")
185 raise UsageError("The clipboard appears to be empty") from e
186
186
187 # By default, echo back to terminal unless quiet mode is requested
187 # By default, echo back to terminal unless quiet mode is requested
188 if 'q' not in opts:
188 if 'q' not in opts:
189 write = self.shell.write
189 write = self.shell.write
190 write(self.shell.pycolorize(block))
190 write(self.shell.pycolorize(block))
191 if not block.endswith('\n'):
191 if not block.endswith('\n'):
192 write('\n')
192 write('\n')
193 write("## -- End pasted text --\n")
193 write("## -- End pasted text --\n")
194
194
195 self.store_or_execute(block, name)
195 self.store_or_execute(block, name)
196
196
197 # Class-level: add a '%cls' magic only on Windows
197 # Class-level: add a '%cls' magic only on Windows
198 if sys.platform == 'win32':
198 if sys.platform == 'win32':
199 @line_magic
199 @line_magic
200 def cls(self, s):
200 def cls(self, s):
201 """Clear screen.
201 """Clear screen.
202 """
202 """
203 os.system("cls")
203 os.system("cls")
@@ -1,140 +1,140 b''
1 """GLUT Input hook for interactive use with prompt_toolkit
1 """GLUT Input hook for interactive use with prompt_toolkit
2 """
2 """
3
3
4
4
5 # GLUT is quite an old library and it is difficult to ensure proper
5 # GLUT is quite an old library and it is difficult to ensure proper
6 # integration within IPython since original GLUT does not allow to handle
6 # integration within IPython since original GLUT does not allow to handle
7 # events one by one. Instead, it requires for the mainloop to be entered
7 # events one by one. Instead, it requires for the mainloop to be entered
8 # and never returned (there is not even a function to exit he
8 # and never returned (there is not even a function to exit he
9 # mainloop). Fortunately, there are alternatives such as freeglut
9 # mainloop). Fortunately, there are alternatives such as freeglut
10 # (available for linux and windows) and the OSX implementation gives
10 # (available for linux and windows) and the OSX implementation gives
11 # access to a glutCheckLoop() function that blocks itself until a new
11 # access to a glutCheckLoop() function that blocks itself until a new
12 # event is received. This means we have to setup the idle callback to
12 # event is received. This means we have to setup the idle callback to
13 # ensure we got at least one event that will unblock the function.
13 # ensure we got at least one event that will unblock the function.
14 #
14 #
15 # Furthermore, it is not possible to install these handlers without a window
15 # Furthermore, it is not possible to install these handlers without a window
16 # being first created. We choose to make this window invisible. This means that
16 # being first created. We choose to make this window invisible. This means that
17 # display mode options are set at this level and user won't be able to change
17 # display mode options are set at this level and user won't be able to change
18 # them later without modifying the code. This should probably be made available
18 # them later without modifying the code. This should probably be made available
19 # via IPython options system.
19 # via IPython options system.
20
20
21 import sys
21 import sys
22 import time
22 import time
23 import signal
23 import signal
24 import OpenGL.GLUT as glut
24 import OpenGL.GLUT as glut
25 import OpenGL.platform as platform
25 import OpenGL.platform as platform
26 from timeit import default_timer as clock
26 from timeit import default_timer as clock
27
27
28 # Frame per second : 60
28 # Frame per second : 60
29 # Should probably be an IPython option
29 # Should probably be an IPython option
30 glut_fps = 60
30 glut_fps = 60
31
31
32 # Display mode : double buffeed + rgba + depth
32 # Display mode : double buffeed + rgba + depth
33 # Should probably be an IPython option
33 # Should probably be an IPython option
34 glut_display_mode = (glut.GLUT_DOUBLE |
34 glut_display_mode = (glut.GLUT_DOUBLE |
35 glut.GLUT_RGBA |
35 glut.GLUT_RGBA |
36 glut.GLUT_DEPTH)
36 glut.GLUT_DEPTH)
37
37
38 glutMainLoopEvent = None
38 glutMainLoopEvent = None
39 if sys.platform == 'darwin':
39 if sys.platform == 'darwin':
40 try:
40 try:
41 glutCheckLoop = platform.createBaseFunction(
41 glutCheckLoop = platform.createBaseFunction(
42 'glutCheckLoop', dll=platform.GLUT, resultType=None,
42 'glutCheckLoop', dll=platform.GLUT, resultType=None,
43 argTypes=[],
43 argTypes=[],
44 doc='glutCheckLoop( ) -> None',
44 doc='glutCheckLoop( ) -> None',
45 argNames=(),
45 argNames=(),
46 )
46 )
47 except AttributeError:
47 except AttributeError as e:
48 raise RuntimeError(
48 raise RuntimeError(
49 '''Your glut implementation does not allow interactive sessions. '''
49 '''Your glut implementation does not allow interactive sessions. '''
50 '''Consider installing freeglut.''')
50 '''Consider installing freeglut.''') from e
51 glutMainLoopEvent = glutCheckLoop
51 glutMainLoopEvent = glutCheckLoop
52 elif glut.HAVE_FREEGLUT:
52 elif glut.HAVE_FREEGLUT:
53 glutMainLoopEvent = glut.glutMainLoopEvent
53 glutMainLoopEvent = glut.glutMainLoopEvent
54 else:
54 else:
55 raise RuntimeError(
55 raise RuntimeError(
56 '''Your glut implementation does not allow interactive sessions. '''
56 '''Your glut implementation does not allow interactive sessions. '''
57 '''Consider installing freeglut.''')
57 '''Consider installing freeglut.''')
58
58
59
59
60 def glut_display():
60 def glut_display():
61 # Dummy display function
61 # Dummy display function
62 pass
62 pass
63
63
64 def glut_idle():
64 def glut_idle():
65 # Dummy idle function
65 # Dummy idle function
66 pass
66 pass
67
67
68 def glut_close():
68 def glut_close():
69 # Close function only hides the current window
69 # Close function only hides the current window
70 glut.glutHideWindow()
70 glut.glutHideWindow()
71 glutMainLoopEvent()
71 glutMainLoopEvent()
72
72
73 def glut_int_handler(signum, frame):
73 def glut_int_handler(signum, frame):
74 # Catch sigint and print the defaultipyt message
74 # Catch sigint and print the defaultipyt message
75 signal.signal(signal.SIGINT, signal.default_int_handler)
75 signal.signal(signal.SIGINT, signal.default_int_handler)
76 print('\nKeyboardInterrupt')
76 print('\nKeyboardInterrupt')
77 # Need to reprint the prompt at this stage
77 # Need to reprint the prompt at this stage
78
78
79 # Initialisation code
79 # Initialisation code
80 glut.glutInit( sys.argv )
80 glut.glutInit( sys.argv )
81 glut.glutInitDisplayMode( glut_display_mode )
81 glut.glutInitDisplayMode( glut_display_mode )
82 # This is specific to freeglut
82 # This is specific to freeglut
83 if bool(glut.glutSetOption):
83 if bool(glut.glutSetOption):
84 glut.glutSetOption( glut.GLUT_ACTION_ON_WINDOW_CLOSE,
84 glut.glutSetOption( glut.GLUT_ACTION_ON_WINDOW_CLOSE,
85 glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS )
85 glut.GLUT_ACTION_GLUTMAINLOOP_RETURNS )
86 glut.glutCreateWindow( b'ipython' )
86 glut.glutCreateWindow( b'ipython' )
87 glut.glutReshapeWindow( 1, 1 )
87 glut.glutReshapeWindow( 1, 1 )
88 glut.glutHideWindow( )
88 glut.glutHideWindow( )
89 glut.glutWMCloseFunc( glut_close )
89 glut.glutWMCloseFunc( glut_close )
90 glut.glutDisplayFunc( glut_display )
90 glut.glutDisplayFunc( glut_display )
91 glut.glutIdleFunc( glut_idle )
91 glut.glutIdleFunc( glut_idle )
92
92
93
93
94 def inputhook(context):
94 def inputhook(context):
95 """Run the pyglet event loop by processing pending events only.
95 """Run the pyglet event loop by processing pending events only.
96
96
97 This keeps processing pending events until stdin is ready. After
97 This keeps processing pending events until stdin is ready. After
98 processing all pending events, a call to time.sleep is inserted. This is
98 processing all pending events, a call to time.sleep is inserted. This is
99 needed, otherwise, CPU usage is at 100%. This sleep time should be tuned
99 needed, otherwise, CPU usage is at 100%. This sleep time should be tuned
100 though for best performance.
100 though for best performance.
101 """
101 """
102 # We need to protect against a user pressing Control-C when IPython is
102 # We need to protect against a user pressing Control-C when IPython is
103 # idle and this is running. We trap KeyboardInterrupt and pass.
103 # idle and this is running. We trap KeyboardInterrupt and pass.
104
104
105 signal.signal(signal.SIGINT, glut_int_handler)
105 signal.signal(signal.SIGINT, glut_int_handler)
106
106
107 try:
107 try:
108 t = clock()
108 t = clock()
109
109
110 # Make sure the default window is set after a window has been closed
110 # Make sure the default window is set after a window has been closed
111 if glut.glutGetWindow() == 0:
111 if glut.glutGetWindow() == 0:
112 glut.glutSetWindow( 1 )
112 glut.glutSetWindow( 1 )
113 glutMainLoopEvent()
113 glutMainLoopEvent()
114 return 0
114 return 0
115
115
116 while not context.input_is_ready():
116 while not context.input_is_ready():
117 glutMainLoopEvent()
117 glutMainLoopEvent()
118 # We need to sleep at this point to keep the idle CPU load
118 # We need to sleep at this point to keep the idle CPU load
119 # low. However, if sleep to long, GUI response is poor. As
119 # low. However, if sleep to long, GUI response is poor. As
120 # a compromise, we watch how often GUI events are being processed
120 # a compromise, we watch how often GUI events are being processed
121 # and switch between a short and long sleep time. Here are some
121 # and switch between a short and long sleep time. Here are some
122 # stats useful in helping to tune this.
122 # stats useful in helping to tune this.
123 # time CPU load
123 # time CPU load
124 # 0.001 13%
124 # 0.001 13%
125 # 0.005 3%
125 # 0.005 3%
126 # 0.01 1.5%
126 # 0.01 1.5%
127 # 0.05 0.5%
127 # 0.05 0.5%
128 used_time = clock() - t
128 used_time = clock() - t
129 if used_time > 10.0:
129 if used_time > 10.0:
130 # print 'Sleep for 1 s' # dbg
130 # print 'Sleep for 1 s' # dbg
131 time.sleep(1.0)
131 time.sleep(1.0)
132 elif used_time > 0.1:
132 elif used_time > 0.1:
133 # Few GUI events coming in, so we can sleep longer
133 # Few GUI events coming in, so we can sleep longer
134 # print 'Sleep for 0.05 s' # dbg
134 # print 'Sleep for 0.05 s' # dbg
135 time.sleep(0.05)
135 time.sleep(0.05)
136 else:
136 else:
137 # Many GUI events coming in, so sleep only very little
137 # Many GUI events coming in, so sleep only very little
138 time.sleep(0.001)
138 time.sleep(0.001)
139 except KeyboardInterrupt:
139 except KeyboardInterrupt:
140 pass
140 pass
@@ -1,471 +1,471 b''
1 """Generic testing tools.
1 """Generic testing tools.
2
2
3 Authors
3 Authors
4 -------
4 -------
5 - Fernando Perez <Fernando.Perez@berkeley.edu>
5 - Fernando Perez <Fernando.Perez@berkeley.edu>
6 """
6 """
7
7
8
8
9 # Copyright (c) IPython Development Team.
9 # Copyright (c) IPython Development Team.
10 # Distributed under the terms of the Modified BSD License.
10 # Distributed under the terms of the Modified BSD License.
11
11
12 import os
12 import os
13 import re
13 import re
14 import sys
14 import sys
15 import tempfile
15 import tempfile
16 import unittest
16 import unittest
17
17
18 from contextlib import contextmanager
18 from contextlib import contextmanager
19 from io import StringIO
19 from io import StringIO
20 from subprocess import Popen, PIPE
20 from subprocess import Popen, PIPE
21 from unittest.mock import patch
21 from unittest.mock import patch
22
22
23 try:
23 try:
24 # These tools are used by parts of the runtime, so we make the nose
24 # These tools are used by parts of the runtime, so we make the nose
25 # dependency optional at this point. Nose is a hard dependency to run the
25 # dependency optional at this point. Nose is a hard dependency to run the
26 # test suite, but NOT to use ipython itself.
26 # test suite, but NOT to use ipython itself.
27 import nose.tools as nt
27 import nose.tools as nt
28 has_nose = True
28 has_nose = True
29 except ImportError:
29 except ImportError:
30 has_nose = False
30 has_nose = False
31
31
32 from traitlets.config.loader import Config
32 from traitlets.config.loader import Config
33 from IPython.utils.process import get_output_error_code
33 from IPython.utils.process import get_output_error_code
34 from IPython.utils.text import list_strings
34 from IPython.utils.text import list_strings
35 from IPython.utils.io import temp_pyfile, Tee
35 from IPython.utils.io import temp_pyfile, Tee
36 from IPython.utils import py3compat
36 from IPython.utils import py3compat
37
37
38 from . import decorators as dec
38 from . import decorators as dec
39 from . import skipdoctest
39 from . import skipdoctest
40
40
41
41
42 # The docstring for full_path doctests differently on win32 (different path
42 # The docstring for full_path doctests differently on win32 (different path
43 # separator) so just skip the doctest there. The example remains informative.
43 # separator) so just skip the doctest there. The example remains informative.
44 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
44 doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
45
45
46 @doctest_deco
46 @doctest_deco
47 def full_path(startPath,files):
47 def full_path(startPath,files):
48 """Make full paths for all the listed files, based on startPath.
48 """Make full paths for all the listed files, based on startPath.
49
49
50 Only the base part of startPath is kept, since this routine is typically
50 Only the base part of startPath is kept, since this routine is typically
51 used with a script's ``__file__`` variable as startPath. The base of startPath
51 used with a script's ``__file__`` variable as startPath. The base of startPath
52 is then prepended to all the listed files, forming the output list.
52 is then prepended to all the listed files, forming the output list.
53
53
54 Parameters
54 Parameters
55 ----------
55 ----------
56 startPath : string
56 startPath : string
57 Initial path to use as the base for the results. This path is split
57 Initial path to use as the base for the results. This path is split
58 using os.path.split() and only its first component is kept.
58 using os.path.split() and only its first component is kept.
59
59
60 files : string or list
60 files : string or list
61 One or more files.
61 One or more files.
62
62
63 Examples
63 Examples
64 --------
64 --------
65
65
66 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
66 >>> full_path('/foo/bar.py',['a.txt','b.txt'])
67 ['/foo/a.txt', '/foo/b.txt']
67 ['/foo/a.txt', '/foo/b.txt']
68
68
69 >>> full_path('/foo',['a.txt','b.txt'])
69 >>> full_path('/foo',['a.txt','b.txt'])
70 ['/a.txt', '/b.txt']
70 ['/a.txt', '/b.txt']
71
71
72 If a single file is given, the output is still a list::
72 If a single file is given, the output is still a list::
73
73
74 >>> full_path('/foo','a.txt')
74 >>> full_path('/foo','a.txt')
75 ['/a.txt']
75 ['/a.txt']
76 """
76 """
77
77
78 files = list_strings(files)
78 files = list_strings(files)
79 base = os.path.split(startPath)[0]
79 base = os.path.split(startPath)[0]
80 return [ os.path.join(base,f) for f in files ]
80 return [ os.path.join(base,f) for f in files ]
81
81
82
82
83 def parse_test_output(txt):
83 def parse_test_output(txt):
84 """Parse the output of a test run and return errors, failures.
84 """Parse the output of a test run and return errors, failures.
85
85
86 Parameters
86 Parameters
87 ----------
87 ----------
88 txt : str
88 txt : str
89 Text output of a test run, assumed to contain a line of one of the
89 Text output of a test run, assumed to contain a line of one of the
90 following forms::
90 following forms::
91
91
92 'FAILED (errors=1)'
92 'FAILED (errors=1)'
93 'FAILED (failures=1)'
93 'FAILED (failures=1)'
94 'FAILED (errors=1, failures=1)'
94 'FAILED (errors=1, failures=1)'
95
95
96 Returns
96 Returns
97 -------
97 -------
98 nerr, nfail
98 nerr, nfail
99 number of errors and failures.
99 number of errors and failures.
100 """
100 """
101
101
102 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
102 err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
103 if err_m:
103 if err_m:
104 nerr = int(err_m.group(1))
104 nerr = int(err_m.group(1))
105 nfail = 0
105 nfail = 0
106 return nerr, nfail
106 return nerr, nfail
107
107
108 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
108 fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
109 if fail_m:
109 if fail_m:
110 nerr = 0
110 nerr = 0
111 nfail = int(fail_m.group(1))
111 nfail = int(fail_m.group(1))
112 return nerr, nfail
112 return nerr, nfail
113
113
114 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
114 both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
115 re.MULTILINE)
115 re.MULTILINE)
116 if both_m:
116 if both_m:
117 nerr = int(both_m.group(1))
117 nerr = int(both_m.group(1))
118 nfail = int(both_m.group(2))
118 nfail = int(both_m.group(2))
119 return nerr, nfail
119 return nerr, nfail
120
120
121 # If the input didn't match any of these forms, assume no error/failures
121 # If the input didn't match any of these forms, assume no error/failures
122 return 0, 0
122 return 0, 0
123
123
124
124
125 # So nose doesn't think this is a test
125 # So nose doesn't think this is a test
126 parse_test_output.__test__ = False
126 parse_test_output.__test__ = False
127
127
128
128
129 def default_argv():
129 def default_argv():
130 """Return a valid default argv for creating testing instances of ipython"""
130 """Return a valid default argv for creating testing instances of ipython"""
131
131
132 return ['--quick', # so no config file is loaded
132 return ['--quick', # so no config file is loaded
133 # Other defaults to minimize side effects on stdout
133 # Other defaults to minimize side effects on stdout
134 '--colors=NoColor', '--no-term-title','--no-banner',
134 '--colors=NoColor', '--no-term-title','--no-banner',
135 '--autocall=0']
135 '--autocall=0']
136
136
137
137
138 def default_config():
138 def default_config():
139 """Return a config object with good defaults for testing."""
139 """Return a config object with good defaults for testing."""
140 config = Config()
140 config = Config()
141 config.TerminalInteractiveShell.colors = 'NoColor'
141 config.TerminalInteractiveShell.colors = 'NoColor'
142 config.TerminalTerminalInteractiveShell.term_title = False,
142 config.TerminalTerminalInteractiveShell.term_title = False,
143 config.TerminalInteractiveShell.autocall = 0
143 config.TerminalInteractiveShell.autocall = 0
144 f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
144 f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
145 config.HistoryManager.hist_file = f.name
145 config.HistoryManager.hist_file = f.name
146 f.close()
146 f.close()
147 config.HistoryManager.db_cache_size = 10000
147 config.HistoryManager.db_cache_size = 10000
148 return config
148 return config
149
149
150
150
151 def get_ipython_cmd(as_string=False):
151 def get_ipython_cmd(as_string=False):
152 """
152 """
153 Return appropriate IPython command line name. By default, this will return
153 Return appropriate IPython command line name. By default, this will return
154 a list that can be used with subprocess.Popen, for example, but passing
154 a list that can be used with subprocess.Popen, for example, but passing
155 `as_string=True` allows for returning the IPython command as a string.
155 `as_string=True` allows for returning the IPython command as a string.
156
156
157 Parameters
157 Parameters
158 ----------
158 ----------
159 as_string: bool
159 as_string: bool
160 Flag to allow to return the command as a string.
160 Flag to allow to return the command as a string.
161 """
161 """
162 ipython_cmd = [sys.executable, "-m", "IPython"]
162 ipython_cmd = [sys.executable, "-m", "IPython"]
163
163
164 if as_string:
164 if as_string:
165 ipython_cmd = " ".join(ipython_cmd)
165 ipython_cmd = " ".join(ipython_cmd)
166
166
167 return ipython_cmd
167 return ipython_cmd
168
168
169 def ipexec(fname, options=None, commands=()):
169 def ipexec(fname, options=None, commands=()):
170 """Utility to call 'ipython filename'.
170 """Utility to call 'ipython filename'.
171
171
172 Starts IPython with a minimal and safe configuration to make startup as fast
172 Starts IPython with a minimal and safe configuration to make startup as fast
173 as possible.
173 as possible.
174
174
175 Note that this starts IPython in a subprocess!
175 Note that this starts IPython in a subprocess!
176
176
177 Parameters
177 Parameters
178 ----------
178 ----------
179 fname : str
179 fname : str
180 Name of file to be executed (should have .py or .ipy extension).
180 Name of file to be executed (should have .py or .ipy extension).
181
181
182 options : optional, list
182 options : optional, list
183 Extra command-line flags to be passed to IPython.
183 Extra command-line flags to be passed to IPython.
184
184
185 commands : optional, list
185 commands : optional, list
186 Commands to send in on stdin
186 Commands to send in on stdin
187
187
188 Returns
188 Returns
189 -------
189 -------
190 ``(stdout, stderr)`` of ipython subprocess.
190 ``(stdout, stderr)`` of ipython subprocess.
191 """
191 """
192 if options is None: options = []
192 if options is None: options = []
193
193
194 cmdargs = default_argv() + options
194 cmdargs = default_argv() + options
195
195
196 test_dir = os.path.dirname(__file__)
196 test_dir = os.path.dirname(__file__)
197
197
198 ipython_cmd = get_ipython_cmd()
198 ipython_cmd = get_ipython_cmd()
199 # Absolute path for filename
199 # Absolute path for filename
200 full_fname = os.path.join(test_dir, fname)
200 full_fname = os.path.join(test_dir, fname)
201 full_cmd = ipython_cmd + cmdargs + [full_fname]
201 full_cmd = ipython_cmd + cmdargs + [full_fname]
202 env = os.environ.copy()
202 env = os.environ.copy()
203 # FIXME: ignore all warnings in ipexec while we have shims
203 # FIXME: ignore all warnings in ipexec while we have shims
204 # should we keep suppressing warnings here, even after removing shims?
204 # should we keep suppressing warnings here, even after removing shims?
205 env['PYTHONWARNINGS'] = 'ignore'
205 env['PYTHONWARNINGS'] = 'ignore'
206 # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
206 # env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
207 for k, v in env.items():
207 for k, v in env.items():
208 # Debug a bizarre failure we've seen on Windows:
208 # Debug a bizarre failure we've seen on Windows:
209 # TypeError: environment can only contain strings
209 # TypeError: environment can only contain strings
210 if not isinstance(v, str):
210 if not isinstance(v, str):
211 print(k, v)
211 print(k, v)
212 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
212 p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
213 out, err = p.communicate(input=py3compat.encode('\n'.join(commands)) or None)
213 out, err = p.communicate(input=py3compat.encode('\n'.join(commands)) or None)
214 out, err = py3compat.decode(out), py3compat.decode(err)
214 out, err = py3compat.decode(out), py3compat.decode(err)
215 # `import readline` causes 'ESC[?1034h' to be output sometimes,
215 # `import readline` causes 'ESC[?1034h' to be output sometimes,
216 # so strip that out before doing comparisons
216 # so strip that out before doing comparisons
217 if out:
217 if out:
218 out = re.sub(r'\x1b\[[^h]+h', '', out)
218 out = re.sub(r'\x1b\[[^h]+h', '', out)
219 return out, err
219 return out, err
220
220
221
221
222 def ipexec_validate(fname, expected_out, expected_err='',
222 def ipexec_validate(fname, expected_out, expected_err='',
223 options=None, commands=()):
223 options=None, commands=()):
224 """Utility to call 'ipython filename' and validate output/error.
224 """Utility to call 'ipython filename' and validate output/error.
225
225
226 This function raises an AssertionError if the validation fails.
226 This function raises an AssertionError if the validation fails.
227
227
228 Note that this starts IPython in a subprocess!
228 Note that this starts IPython in a subprocess!
229
229
230 Parameters
230 Parameters
231 ----------
231 ----------
232 fname : str
232 fname : str
233 Name of the file to be executed (should have .py or .ipy extension).
233 Name of the file to be executed (should have .py or .ipy extension).
234
234
235 expected_out : str
235 expected_out : str
236 Expected stdout of the process.
236 Expected stdout of the process.
237
237
238 expected_err : optional, str
238 expected_err : optional, str
239 Expected stderr of the process.
239 Expected stderr of the process.
240
240
241 options : optional, list
241 options : optional, list
242 Extra command-line flags to be passed to IPython.
242 Extra command-line flags to be passed to IPython.
243
243
244 Returns
244 Returns
245 -------
245 -------
246 None
246 None
247 """
247 """
248
248
249 import nose.tools as nt
249 import nose.tools as nt
250
250
251 out, err = ipexec(fname, options, commands)
251 out, err = ipexec(fname, options, commands)
252 #print 'OUT', out # dbg
252 #print 'OUT', out # dbg
253 #print 'ERR', err # dbg
253 #print 'ERR', err # dbg
254 # If there are any errors, we must check those before stdout, as they may be
254 # If there are any errors, we must check those before stdout, as they may be
255 # more informative than simply having an empty stdout.
255 # more informative than simply having an empty stdout.
256 if err:
256 if err:
257 if expected_err:
257 if expected_err:
258 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
258 nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
259 else:
259 else:
260 raise ValueError('Running file %r produced error: %r' %
260 raise ValueError('Running file %r produced error: %r' %
261 (fname, err))
261 (fname, err))
262 # If no errors or output on stderr was expected, match stdout
262 # If no errors or output on stderr was expected, match stdout
263 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
263 nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
264
264
265
265
266 class TempFileMixin(unittest.TestCase):
266 class TempFileMixin(unittest.TestCase):
267 """Utility class to create temporary Python/IPython files.
267 """Utility class to create temporary Python/IPython files.
268
268
269 Meant as a mixin class for test cases."""
269 Meant as a mixin class for test cases."""
270
270
271 def mktmp(self, src, ext='.py'):
271 def mktmp(self, src, ext='.py'):
272 """Make a valid python temp file."""
272 """Make a valid python temp file."""
273 fname = temp_pyfile(src, ext)
273 fname = temp_pyfile(src, ext)
274 if not hasattr(self, 'tmps'):
274 if not hasattr(self, 'tmps'):
275 self.tmps=[]
275 self.tmps=[]
276 self.tmps.append(fname)
276 self.tmps.append(fname)
277 self.fname = fname
277 self.fname = fname
278
278
279 def tearDown(self):
279 def tearDown(self):
280 # If the tmpfile wasn't made because of skipped tests, like in
280 # If the tmpfile wasn't made because of skipped tests, like in
281 # win32, there's nothing to cleanup.
281 # win32, there's nothing to cleanup.
282 if hasattr(self, 'tmps'):
282 if hasattr(self, 'tmps'):
283 for fname in self.tmps:
283 for fname in self.tmps:
284 # If the tmpfile wasn't made because of skipped tests, like in
284 # If the tmpfile wasn't made because of skipped tests, like in
285 # win32, there's nothing to cleanup.
285 # win32, there's nothing to cleanup.
286 try:
286 try:
287 os.unlink(fname)
287 os.unlink(fname)
288 except:
288 except:
289 # On Windows, even though we close the file, we still can't
289 # On Windows, even though we close the file, we still can't
290 # delete it. I have no clue why
290 # delete it. I have no clue why
291 pass
291 pass
292
292
293 def __enter__(self):
293 def __enter__(self):
294 return self
294 return self
295
295
296 def __exit__(self, exc_type, exc_value, traceback):
296 def __exit__(self, exc_type, exc_value, traceback):
297 self.tearDown()
297 self.tearDown()
298
298
299
299
300 pair_fail_msg = ("Testing {0}\n\n"
300 pair_fail_msg = ("Testing {0}\n\n"
301 "In:\n"
301 "In:\n"
302 " {1!r}\n"
302 " {1!r}\n"
303 "Expected:\n"
303 "Expected:\n"
304 " {2!r}\n"
304 " {2!r}\n"
305 "Got:\n"
305 "Got:\n"
306 " {3!r}\n")
306 " {3!r}\n")
307 def check_pairs(func, pairs):
307 def check_pairs(func, pairs):
308 """Utility function for the common case of checking a function with a
308 """Utility function for the common case of checking a function with a
309 sequence of input/output pairs.
309 sequence of input/output pairs.
310
310
311 Parameters
311 Parameters
312 ----------
312 ----------
313 func : callable
313 func : callable
314 The function to be tested. Should accept a single argument.
314 The function to be tested. Should accept a single argument.
315 pairs : iterable
315 pairs : iterable
316 A list of (input, expected_output) tuples.
316 A list of (input, expected_output) tuples.
317
317
318 Returns
318 Returns
319 -------
319 -------
320 None. Raises an AssertionError if any output does not match the expected
320 None. Raises an AssertionError if any output does not match the expected
321 value.
321 value.
322 """
322 """
323 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
323 name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
324 for inp, expected in pairs:
324 for inp, expected in pairs:
325 out = func(inp)
325 out = func(inp)
326 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
326 assert out == expected, pair_fail_msg.format(name, inp, expected, out)
327
327
328
328
329 MyStringIO = StringIO
329 MyStringIO = StringIO
330
330
331 _re_type = type(re.compile(r''))
331 _re_type = type(re.compile(r''))
332
332
333 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
333 notprinted_msg = """Did not find {0!r} in printed output (on {1}):
334 -------
334 -------
335 {2!s}
335 {2!s}
336 -------
336 -------
337 """
337 """
338
338
339 class AssertPrints(object):
339 class AssertPrints(object):
340 """Context manager for testing that code prints certain text.
340 """Context manager for testing that code prints certain text.
341
341
342 Examples
342 Examples
343 --------
343 --------
344 >>> with AssertPrints("abc", suppress=False):
344 >>> with AssertPrints("abc", suppress=False):
345 ... print("abcd")
345 ... print("abcd")
346 ... print("def")
346 ... print("def")
347 ...
347 ...
348 abcd
348 abcd
349 def
349 def
350 """
350 """
351 def __init__(self, s, channel='stdout', suppress=True):
351 def __init__(self, s, channel='stdout', suppress=True):
352 self.s = s
352 self.s = s
353 if isinstance(self.s, (str, _re_type)):
353 if isinstance(self.s, (str, _re_type)):
354 self.s = [self.s]
354 self.s = [self.s]
355 self.channel = channel
355 self.channel = channel
356 self.suppress = suppress
356 self.suppress = suppress
357
357
358 def __enter__(self):
358 def __enter__(self):
359 self.orig_stream = getattr(sys, self.channel)
359 self.orig_stream = getattr(sys, self.channel)
360 self.buffer = MyStringIO()
360 self.buffer = MyStringIO()
361 self.tee = Tee(self.buffer, channel=self.channel)
361 self.tee = Tee(self.buffer, channel=self.channel)
362 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
362 setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
363
363
364 def __exit__(self, etype, value, traceback):
364 def __exit__(self, etype, value, traceback):
365 try:
365 try:
366 if value is not None:
366 if value is not None:
367 # If an error was raised, don't check anything else
367 # If an error was raised, don't check anything else
368 return False
368 return False
369 self.tee.flush()
369 self.tee.flush()
370 setattr(sys, self.channel, self.orig_stream)
370 setattr(sys, self.channel, self.orig_stream)
371 printed = self.buffer.getvalue()
371 printed = self.buffer.getvalue()
372 for s in self.s:
372 for s in self.s:
373 if isinstance(s, _re_type):
373 if isinstance(s, _re_type):
374 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
374 assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
375 else:
375 else:
376 assert s in printed, notprinted_msg.format(s, self.channel, printed)
376 assert s in printed, notprinted_msg.format(s, self.channel, printed)
377 return False
377 return False
378 finally:
378 finally:
379 self.tee.close()
379 self.tee.close()
380
380
381 printed_msg = """Found {0!r} in printed output (on {1}):
381 printed_msg = """Found {0!r} in printed output (on {1}):
382 -------
382 -------
383 {2!s}
383 {2!s}
384 -------
384 -------
385 """
385 """
386
386
387 class AssertNotPrints(AssertPrints):
387 class AssertNotPrints(AssertPrints):
388 """Context manager for checking that certain output *isn't* produced.
388 """Context manager for checking that certain output *isn't* produced.
389
389
390 Counterpart of AssertPrints"""
390 Counterpart of AssertPrints"""
391 def __exit__(self, etype, value, traceback):
391 def __exit__(self, etype, value, traceback):
392 try:
392 try:
393 if value is not None:
393 if value is not None:
394 # If an error was raised, don't check anything else
394 # If an error was raised, don't check anything else
395 self.tee.close()
395 self.tee.close()
396 return False
396 return False
397 self.tee.flush()
397 self.tee.flush()
398 setattr(sys, self.channel, self.orig_stream)
398 setattr(sys, self.channel, self.orig_stream)
399 printed = self.buffer.getvalue()
399 printed = self.buffer.getvalue()
400 for s in self.s:
400 for s in self.s:
401 if isinstance(s, _re_type):
401 if isinstance(s, _re_type):
402 assert not s.search(printed),printed_msg.format(
402 assert not s.search(printed),printed_msg.format(
403 s.pattern, self.channel, printed)
403 s.pattern, self.channel, printed)
404 else:
404 else:
405 assert s not in printed, printed_msg.format(
405 assert s not in printed, printed_msg.format(
406 s, self.channel, printed)
406 s, self.channel, printed)
407 return False
407 return False
408 finally:
408 finally:
409 self.tee.close()
409 self.tee.close()
410
410
411 @contextmanager
411 @contextmanager
412 def mute_warn():
412 def mute_warn():
413 from IPython.utils import warn
413 from IPython.utils import warn
414 save_warn = warn.warn
414 save_warn = warn.warn
415 warn.warn = lambda *a, **kw: None
415 warn.warn = lambda *a, **kw: None
416 try:
416 try:
417 yield
417 yield
418 finally:
418 finally:
419 warn.warn = save_warn
419 warn.warn = save_warn
420
420
421 @contextmanager
421 @contextmanager
422 def make_tempfile(name):
422 def make_tempfile(name):
423 """ Create an empty, named, temporary file for the duration of the context.
423 """ Create an empty, named, temporary file for the duration of the context.
424 """
424 """
425 open(name, 'w').close()
425 open(name, 'w').close()
426 try:
426 try:
427 yield
427 yield
428 finally:
428 finally:
429 os.unlink(name)
429 os.unlink(name)
430
430
431 def fake_input(inputs):
431 def fake_input(inputs):
432 """Temporarily replace the input() function to return the given values
432 """Temporarily replace the input() function to return the given values
433
433
434 Use as a context manager:
434 Use as a context manager:
435
435
436 with fake_input(['result1', 'result2']):
436 with fake_input(['result1', 'result2']):
437 ...
437 ...
438
438
439 Values are returned in order. If input() is called again after the last value
439 Values are returned in order. If input() is called again after the last value
440 was used, EOFError is raised.
440 was used, EOFError is raised.
441 """
441 """
442 it = iter(inputs)
442 it = iter(inputs)
443 def mock_input(prompt=''):
443 def mock_input(prompt=''):
444 try:
444 try:
445 return next(it)
445 return next(it)
446 except StopIteration:
446 except StopIteration as e:
447 raise EOFError('No more inputs given')
447 raise EOFError('No more inputs given') from e
448
448
449 return patch('builtins.input', mock_input)
449 return patch('builtins.input', mock_input)
450
450
451 def help_output_test(subcommand=''):
451 def help_output_test(subcommand=''):
452 """test that `ipython [subcommand] -h` works"""
452 """test that `ipython [subcommand] -h` works"""
453 cmd = get_ipython_cmd() + [subcommand, '-h']
453 cmd = get_ipython_cmd() + [subcommand, '-h']
454 out, err, rc = get_output_error_code(cmd)
454 out, err, rc = get_output_error_code(cmd)
455 nt.assert_equal(rc, 0, err)
455 nt.assert_equal(rc, 0, err)
456 nt.assert_not_in("Traceback", err)
456 nt.assert_not_in("Traceback", err)
457 nt.assert_in("Options", out)
457 nt.assert_in("Options", out)
458 nt.assert_in("--help-all", out)
458 nt.assert_in("--help-all", out)
459 return out, err
459 return out, err
460
460
461
461
462 def help_all_output_test(subcommand=''):
462 def help_all_output_test(subcommand=''):
463 """test that `ipython [subcommand] --help-all` works"""
463 """test that `ipython [subcommand] --help-all` works"""
464 cmd = get_ipython_cmd() + [subcommand, '--help-all']
464 cmd = get_ipython_cmd() + [subcommand, '--help-all']
465 out, err, rc = get_output_error_code(cmd)
465 out, err, rc = get_output_error_code(cmd)
466 nt.assert_equal(rc, 0, err)
466 nt.assert_equal(rc, 0, err)
467 nt.assert_not_in("Traceback", err)
467 nt.assert_not_in("Traceback", err)
468 nt.assert_in("Options", out)
468 nt.assert_in("Options", out)
469 nt.assert_in("Class", out)
469 nt.assert_in("Class", out)
470 return out, err
470 return out, err
471
471
@@ -1,205 +1,205 b''
1 """Windows-specific implementation of process utilities.
1 """Windows-specific implementation of process utilities.
2
2
3 This file is only meant to be imported by process.py, not by end-users.
3 This file is only meant to be imported by process.py, not by end-users.
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2010-2011 The IPython Development Team
7 # Copyright (C) 2010-2011 The IPython Development Team
8 #
8 #
9 # Distributed under the terms of the BSD License. The full license is in
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 # stdlib
17 # stdlib
18 import os
18 import os
19 import sys
19 import sys
20 import ctypes
20 import ctypes
21 import time
21 import time
22
22
23 from ctypes import c_int, POINTER
23 from ctypes import c_int, POINTER
24 from ctypes.wintypes import LPCWSTR, HLOCAL
24 from ctypes.wintypes import LPCWSTR, HLOCAL
25 from subprocess import STDOUT, TimeoutExpired
25 from subprocess import STDOUT, TimeoutExpired
26 from threading import Thread
26 from threading import Thread
27
27
28 # our own imports
28 # our own imports
29 from ._process_common import read_no_interrupt, process_handler, arg_split as py_arg_split
29 from ._process_common import read_no_interrupt, process_handler, arg_split as py_arg_split
30 from . import py3compat
30 from . import py3compat
31 from .encoding import DEFAULT_ENCODING
31 from .encoding import DEFAULT_ENCODING
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Function definitions
34 # Function definitions
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 class AvoidUNCPath(object):
37 class AvoidUNCPath(object):
38 """A context manager to protect command execution from UNC paths.
38 """A context manager to protect command execution from UNC paths.
39
39
40 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
40 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
41 This context manager temporarily changes directory to the 'C:' drive on
41 This context manager temporarily changes directory to the 'C:' drive on
42 entering, and restores the original working directory on exit.
42 entering, and restores the original working directory on exit.
43
43
44 The context manager returns the starting working directory *if* it made a
44 The context manager returns the starting working directory *if* it made a
45 change and None otherwise, so that users can apply the necessary adjustment
45 change and None otherwise, so that users can apply the necessary adjustment
46 to their system calls in the event of a change.
46 to their system calls in the event of a change.
47
47
48 Examples
48 Examples
49 --------
49 --------
50 ::
50 ::
51 cmd = 'dir'
51 cmd = 'dir'
52 with AvoidUNCPath() as path:
52 with AvoidUNCPath() as path:
53 if path is not None:
53 if path is not None:
54 cmd = '"pushd %s &&"%s' % (path, cmd)
54 cmd = '"pushd %s &&"%s' % (path, cmd)
55 os.system(cmd)
55 os.system(cmd)
56 """
56 """
57 def __enter__(self):
57 def __enter__(self):
58 self.path = os.getcwd()
58 self.path = os.getcwd()
59 self.is_unc_path = self.path.startswith(r"\\")
59 self.is_unc_path = self.path.startswith(r"\\")
60 if self.is_unc_path:
60 if self.is_unc_path:
61 # change to c drive (as cmd.exe cannot handle UNC addresses)
61 # change to c drive (as cmd.exe cannot handle UNC addresses)
62 os.chdir("C:")
62 os.chdir("C:")
63 return self.path
63 return self.path
64 else:
64 else:
65 # We return None to signal that there was no change in the working
65 # We return None to signal that there was no change in the working
66 # directory
66 # directory
67 return None
67 return None
68
68
69 def __exit__(self, exc_type, exc_value, traceback):
69 def __exit__(self, exc_type, exc_value, traceback):
70 if self.is_unc_path:
70 if self.is_unc_path:
71 os.chdir(self.path)
71 os.chdir(self.path)
72
72
73
73
74 def _find_cmd(cmd):
74 def _find_cmd(cmd):
75 """Find the full path to a .bat or .exe using the win32api module."""
75 """Find the full path to a .bat or .exe using the win32api module."""
76 try:
76 try:
77 from win32api import SearchPath
77 from win32api import SearchPath
78 except ImportError:
78 except ImportError as e:
79 raise ImportError('you need to have pywin32 installed for this to work')
79 raise ImportError('you need to have pywin32 installed for this to work') from e
80 else:
80 else:
81 PATH = os.environ['PATH']
81 PATH = os.environ['PATH']
82 extensions = ['.exe', '.com', '.bat', '.py']
82 extensions = ['.exe', '.com', '.bat', '.py']
83 path = None
83 path = None
84 for ext in extensions:
84 for ext in extensions:
85 try:
85 try:
86 path = SearchPath(PATH, cmd, ext)[0]
86 path = SearchPath(PATH, cmd, ext)[0]
87 except:
87 except:
88 pass
88 pass
89 if path is None:
89 if path is None:
90 raise OSError("command %r not found" % cmd)
90 raise OSError("command %r not found" % cmd)
91 else:
91 else:
92 return path
92 return path
93
93
94
94
95 def _system_body(p):
95 def _system_body(p):
96 """Callback for _system."""
96 """Callback for _system."""
97 enc = DEFAULT_ENCODING
97 enc = DEFAULT_ENCODING
98
98
99 def stdout_read():
99 def stdout_read():
100 for line in read_no_interrupt(p.stdout).splitlines():
100 for line in read_no_interrupt(p.stdout).splitlines():
101 line = line.decode(enc, 'replace')
101 line = line.decode(enc, 'replace')
102 print(line, file=sys.stdout)
102 print(line, file=sys.stdout)
103
103
104 def stderr_read():
104 def stderr_read():
105 for line in read_no_interrupt(p.stderr).splitlines():
105 for line in read_no_interrupt(p.stderr).splitlines():
106 line = line.decode(enc, 'replace')
106 line = line.decode(enc, 'replace')
107 print(line, file=sys.stderr)
107 print(line, file=sys.stderr)
108
108
109 Thread(target=stdout_read).start()
109 Thread(target=stdout_read).start()
110 Thread(target=stderr_read).start()
110 Thread(target=stderr_read).start()
111
111
112 # Wait to finish for returncode. Unfortunately, Python has a bug where
112 # Wait to finish for returncode. Unfortunately, Python has a bug where
113 # wait() isn't interruptible (https://bugs.python.org/issue28168) so poll in
113 # wait() isn't interruptible (https://bugs.python.org/issue28168) so poll in
114 # a loop instead of just doing `return p.wait()`.
114 # a loop instead of just doing `return p.wait()`.
115 while True:
115 while True:
116 result = p.poll()
116 result = p.poll()
117 if result is None:
117 if result is None:
118 time.sleep(0.01)
118 time.sleep(0.01)
119 else:
119 else:
120 return result
120 return result
121
121
122
122
123 def system(cmd):
123 def system(cmd):
124 """Win32 version of os.system() that works with network shares.
124 """Win32 version of os.system() that works with network shares.
125
125
126 Note that this implementation returns None, as meant for use in IPython.
126 Note that this implementation returns None, as meant for use in IPython.
127
127
128 Parameters
128 Parameters
129 ----------
129 ----------
130 cmd : str or list
130 cmd : str or list
131 A command to be executed in the system shell.
131 A command to be executed in the system shell.
132
132
133 Returns
133 Returns
134 -------
134 -------
135 int : child process' exit code.
135 int : child process' exit code.
136 """
136 """
137 # The controller provides interactivity with both
137 # The controller provides interactivity with both
138 # stdin and stdout
138 # stdin and stdout
139 #import _process_win32_controller
139 #import _process_win32_controller
140 #_process_win32_controller.system(cmd)
140 #_process_win32_controller.system(cmd)
141
141
142 with AvoidUNCPath() as path:
142 with AvoidUNCPath() as path:
143 if path is not None:
143 if path is not None:
144 cmd = '"pushd %s &&"%s' % (path, cmd)
144 cmd = '"pushd %s &&"%s' % (path, cmd)
145 return process_handler(cmd, _system_body)
145 return process_handler(cmd, _system_body)
146
146
147 def getoutput(cmd):
147 def getoutput(cmd):
148 """Return standard output of executing cmd in a shell.
148 """Return standard output of executing cmd in a shell.
149
149
150 Accepts the same arguments as os.system().
150 Accepts the same arguments as os.system().
151
151
152 Parameters
152 Parameters
153 ----------
153 ----------
154 cmd : str or list
154 cmd : str or list
155 A command to be executed in the system shell.
155 A command to be executed in the system shell.
156
156
157 Returns
157 Returns
158 -------
158 -------
159 stdout : str
159 stdout : str
160 """
160 """
161
161
162 with AvoidUNCPath() as path:
162 with AvoidUNCPath() as path:
163 if path is not None:
163 if path is not None:
164 cmd = '"pushd %s &&"%s' % (path, cmd)
164 cmd = '"pushd %s &&"%s' % (path, cmd)
165 out = process_handler(cmd, lambda p: p.communicate()[0], STDOUT)
165 out = process_handler(cmd, lambda p: p.communicate()[0], STDOUT)
166
166
167 if out is None:
167 if out is None:
168 out = b''
168 out = b''
169 return py3compat.decode(out)
169 return py3compat.decode(out)
170
170
171 try:
171 try:
172 CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
172 CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
173 CommandLineToArgvW.arg_types = [LPCWSTR, POINTER(c_int)]
173 CommandLineToArgvW.arg_types = [LPCWSTR, POINTER(c_int)]
174 CommandLineToArgvW.restype = POINTER(LPCWSTR)
174 CommandLineToArgvW.restype = POINTER(LPCWSTR)
175 LocalFree = ctypes.windll.kernel32.LocalFree
175 LocalFree = ctypes.windll.kernel32.LocalFree
176 LocalFree.res_type = HLOCAL
176 LocalFree.res_type = HLOCAL
177 LocalFree.arg_types = [HLOCAL]
177 LocalFree.arg_types = [HLOCAL]
178
178
179 def arg_split(commandline, posix=False, strict=True):
179 def arg_split(commandline, posix=False, strict=True):
180 """Split a command line's arguments in a shell-like manner.
180 """Split a command line's arguments in a shell-like manner.
181
181
182 This is a special version for windows that use a ctypes call to CommandLineToArgvW
182 This is a special version for windows that use a ctypes call to CommandLineToArgvW
183 to do the argv splitting. The posix parameter is ignored.
183 to do the argv splitting. The posix parameter is ignored.
184
184
185 If strict=False, process_common.arg_split(...strict=False) is used instead.
185 If strict=False, process_common.arg_split(...strict=False) is used instead.
186 """
186 """
187 #CommandLineToArgvW returns path to executable if called with empty string.
187 #CommandLineToArgvW returns path to executable if called with empty string.
188 if commandline.strip() == "":
188 if commandline.strip() == "":
189 return []
189 return []
190 if not strict:
190 if not strict:
191 # not really a cl-arg, fallback on _process_common
191 # not really a cl-arg, fallback on _process_common
192 return py_arg_split(commandline, posix=posix, strict=strict)
192 return py_arg_split(commandline, posix=posix, strict=strict)
193 argvn = c_int()
193 argvn = c_int()
194 result_pointer = CommandLineToArgvW(py3compat.cast_unicode(commandline.lstrip()), ctypes.byref(argvn))
194 result_pointer = CommandLineToArgvW(py3compat.cast_unicode(commandline.lstrip()), ctypes.byref(argvn))
195 result_array_type = LPCWSTR * argvn.value
195 result_array_type = LPCWSTR * argvn.value
196 result = [arg for arg in result_array_type.from_address(ctypes.addressof(result_pointer.contents))]
196 result = [arg for arg in result_array_type.from_address(ctypes.addressof(result_pointer.contents))]
197 retval = LocalFree(result_pointer)
197 retval = LocalFree(result_pointer)
198 return result
198 return result
199 except AttributeError:
199 except AttributeError:
200 arg_split = py_arg_split
200 arg_split = py_arg_split
201
201
202 def check_pid(pid):
202 def check_pid(pid):
203 # OpenProcess returns 0 if no such process (of ours) exists
203 # OpenProcess returns 0 if no such process (of ours) exists
204 # positive int otherwise
204 # positive int otherwise
205 return bool(ctypes.windll.kernel32.OpenProcess(1,0,pid))
205 return bool(ctypes.windll.kernel32.OpenProcess(1,0,pid))
@@ -1,187 +1,187 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Tools for coloring text in ANSI terminals.
2 """Tools for coloring text in ANSI terminals.
3 """
3 """
4
4
5 #*****************************************************************************
5 #*****************************************************************************
6 # Copyright (C) 2002-2006 Fernando Perez. <fperez@colorado.edu>
6 # Copyright (C) 2002-2006 Fernando Perez. <fperez@colorado.edu>
7 #
7 #
8 # Distributed under the terms of the BSD License. The full license is in
8 # Distributed under the terms of the BSD License. The full license is in
9 # the file COPYING, distributed as part of this software.
9 # the file COPYING, distributed as part of this software.
10 #*****************************************************************************
10 #*****************************************************************************
11
11
12 __all__ = ['TermColors','InputTermColors','ColorScheme','ColorSchemeTable']
12 __all__ = ['TermColors','InputTermColors','ColorScheme','ColorSchemeTable']
13
13
14 import os
14 import os
15
15
16 from IPython.utils.ipstruct import Struct
16 from IPython.utils.ipstruct import Struct
17
17
18 color_templates = (
18 color_templates = (
19 # Dark colors
19 # Dark colors
20 ("Black" , "0;30"),
20 ("Black" , "0;30"),
21 ("Red" , "0;31"),
21 ("Red" , "0;31"),
22 ("Green" , "0;32"),
22 ("Green" , "0;32"),
23 ("Brown" , "0;33"),
23 ("Brown" , "0;33"),
24 ("Blue" , "0;34"),
24 ("Blue" , "0;34"),
25 ("Purple" , "0;35"),
25 ("Purple" , "0;35"),
26 ("Cyan" , "0;36"),
26 ("Cyan" , "0;36"),
27 ("LightGray" , "0;37"),
27 ("LightGray" , "0;37"),
28 # Light colors
28 # Light colors
29 ("DarkGray" , "1;30"),
29 ("DarkGray" , "1;30"),
30 ("LightRed" , "1;31"),
30 ("LightRed" , "1;31"),
31 ("LightGreen" , "1;32"),
31 ("LightGreen" , "1;32"),
32 ("Yellow" , "1;33"),
32 ("Yellow" , "1;33"),
33 ("LightBlue" , "1;34"),
33 ("LightBlue" , "1;34"),
34 ("LightPurple" , "1;35"),
34 ("LightPurple" , "1;35"),
35 ("LightCyan" , "1;36"),
35 ("LightCyan" , "1;36"),
36 ("White" , "1;37"),
36 ("White" , "1;37"),
37 # Blinking colors. Probably should not be used in anything serious.
37 # Blinking colors. Probably should not be used in anything serious.
38 ("BlinkBlack" , "5;30"),
38 ("BlinkBlack" , "5;30"),
39 ("BlinkRed" , "5;31"),
39 ("BlinkRed" , "5;31"),
40 ("BlinkGreen" , "5;32"),
40 ("BlinkGreen" , "5;32"),
41 ("BlinkYellow" , "5;33"),
41 ("BlinkYellow" , "5;33"),
42 ("BlinkBlue" , "5;34"),
42 ("BlinkBlue" , "5;34"),
43 ("BlinkPurple" , "5;35"),
43 ("BlinkPurple" , "5;35"),
44 ("BlinkCyan" , "5;36"),
44 ("BlinkCyan" , "5;36"),
45 ("BlinkLightGray", "5;37"),
45 ("BlinkLightGray", "5;37"),
46 )
46 )
47
47
48 def make_color_table(in_class):
48 def make_color_table(in_class):
49 """Build a set of color attributes in a class.
49 """Build a set of color attributes in a class.
50
50
51 Helper function for building the :class:`TermColors` and
51 Helper function for building the :class:`TermColors` and
52 :class`InputTermColors`.
52 :class`InputTermColors`.
53 """
53 """
54 for name,value in color_templates:
54 for name,value in color_templates:
55 setattr(in_class,name,in_class._base % value)
55 setattr(in_class,name,in_class._base % value)
56
56
57 class TermColors:
57 class TermColors:
58 """Color escape sequences.
58 """Color escape sequences.
59
59
60 This class defines the escape sequences for all the standard (ANSI?)
60 This class defines the escape sequences for all the standard (ANSI?)
61 colors in terminals. Also defines a NoColor escape which is just the null
61 colors in terminals. Also defines a NoColor escape which is just the null
62 string, suitable for defining 'dummy' color schemes in terminals which get
62 string, suitable for defining 'dummy' color schemes in terminals which get
63 confused by color escapes.
63 confused by color escapes.
64
64
65 This class should be used as a mixin for building color schemes."""
65 This class should be used as a mixin for building color schemes."""
66
66
67 NoColor = '' # for color schemes in color-less terminals.
67 NoColor = '' # for color schemes in color-less terminals.
68 Normal = '\033[0m' # Reset normal coloring
68 Normal = '\033[0m' # Reset normal coloring
69 _base = '\033[%sm' # Template for all other colors
69 _base = '\033[%sm' # Template for all other colors
70
70
71 # Build the actual color table as a set of class attributes:
71 # Build the actual color table as a set of class attributes:
72 make_color_table(TermColors)
72 make_color_table(TermColors)
73
73
74 class InputTermColors:
74 class InputTermColors:
75 """Color escape sequences for input prompts.
75 """Color escape sequences for input prompts.
76
76
77 This class is similar to TermColors, but the escapes are wrapped in \001
77 This class is similar to TermColors, but the escapes are wrapped in \001
78 and \002 so that readline can properly know the length of each line and
78 and \002 so that readline can properly know the length of each line and
79 can wrap lines accordingly. Use this class for any colored text which
79 can wrap lines accordingly. Use this class for any colored text which
80 needs to be used in input prompts, such as in calls to raw_input().
80 needs to be used in input prompts, such as in calls to raw_input().
81
81
82 This class defines the escape sequences for all the standard (ANSI?)
82 This class defines the escape sequences for all the standard (ANSI?)
83 colors in terminals. Also defines a NoColor escape which is just the null
83 colors in terminals. Also defines a NoColor escape which is just the null
84 string, suitable for defining 'dummy' color schemes in terminals which get
84 string, suitable for defining 'dummy' color schemes in terminals which get
85 confused by color escapes.
85 confused by color escapes.
86
86
87 This class should be used as a mixin for building color schemes."""
87 This class should be used as a mixin for building color schemes."""
88
88
89 NoColor = '' # for color schemes in color-less terminals.
89 NoColor = '' # for color schemes in color-less terminals.
90
90
91 if os.name == 'nt' and os.environ.get('TERM','dumb') == 'emacs':
91 if os.name == 'nt' and os.environ.get('TERM','dumb') == 'emacs':
92 # (X)emacs on W32 gets confused with \001 and \002 so we remove them
92 # (X)emacs on W32 gets confused with \001 and \002 so we remove them
93 Normal = '\033[0m' # Reset normal coloring
93 Normal = '\033[0m' # Reset normal coloring
94 _base = '\033[%sm' # Template for all other colors
94 _base = '\033[%sm' # Template for all other colors
95 else:
95 else:
96 Normal = '\001\033[0m\002' # Reset normal coloring
96 Normal = '\001\033[0m\002' # Reset normal coloring
97 _base = '\001\033[%sm\002' # Template for all other colors
97 _base = '\001\033[%sm\002' # Template for all other colors
98
98
99 # Build the actual color table as a set of class attributes:
99 # Build the actual color table as a set of class attributes:
100 make_color_table(InputTermColors)
100 make_color_table(InputTermColors)
101
101
102 class NoColors:
102 class NoColors:
103 """This defines all the same names as the colour classes, but maps them to
103 """This defines all the same names as the colour classes, but maps them to
104 empty strings, so it can easily be substituted to turn off colours."""
104 empty strings, so it can easily be substituted to turn off colours."""
105 NoColor = ''
105 NoColor = ''
106 Normal = ''
106 Normal = ''
107
107
108 for name, value in color_templates:
108 for name, value in color_templates:
109 setattr(NoColors, name, '')
109 setattr(NoColors, name, '')
110
110
111 class ColorScheme:
111 class ColorScheme:
112 """Generic color scheme class. Just a name and a Struct."""
112 """Generic color scheme class. Just a name and a Struct."""
113 def __init__(self,__scheme_name_,colordict=None,**colormap):
113 def __init__(self,__scheme_name_,colordict=None,**colormap):
114 self.name = __scheme_name_
114 self.name = __scheme_name_
115 if colordict is None:
115 if colordict is None:
116 self.colors = Struct(**colormap)
116 self.colors = Struct(**colormap)
117 else:
117 else:
118 self.colors = Struct(colordict)
118 self.colors = Struct(colordict)
119
119
120 def copy(self,name=None):
120 def copy(self,name=None):
121 """Return a full copy of the object, optionally renaming it."""
121 """Return a full copy of the object, optionally renaming it."""
122 if name is None:
122 if name is None:
123 name = self.name
123 name = self.name
124 return ColorScheme(name, self.colors.dict())
124 return ColorScheme(name, self.colors.dict())
125
125
126 class ColorSchemeTable(dict):
126 class ColorSchemeTable(dict):
127 """General class to handle tables of color schemes.
127 """General class to handle tables of color schemes.
128
128
129 It's basically a dict of color schemes with a couple of shorthand
129 It's basically a dict of color schemes with a couple of shorthand
130 attributes and some convenient methods.
130 attributes and some convenient methods.
131
131
132 active_scheme_name -> obvious
132 active_scheme_name -> obvious
133 active_colors -> actual color table of the active scheme"""
133 active_colors -> actual color table of the active scheme"""
134
134
135 def __init__(self, scheme_list=None, default_scheme=''):
135 def __init__(self, scheme_list=None, default_scheme=''):
136 """Create a table of color schemes.
136 """Create a table of color schemes.
137
137
138 The table can be created empty and manually filled or it can be
138 The table can be created empty and manually filled or it can be
139 created with a list of valid color schemes AND the specification for
139 created with a list of valid color schemes AND the specification for
140 the default active scheme.
140 the default active scheme.
141 """
141 """
142
142
143 # create object attributes to be set later
143 # create object attributes to be set later
144 self.active_scheme_name = ''
144 self.active_scheme_name = ''
145 self.active_colors = None
145 self.active_colors = None
146
146
147 if scheme_list:
147 if scheme_list:
148 if default_scheme == '':
148 if default_scheme == '':
149 raise ValueError('you must specify the default color scheme')
149 raise ValueError('you must specify the default color scheme')
150 for scheme in scheme_list:
150 for scheme in scheme_list:
151 self.add_scheme(scheme)
151 self.add_scheme(scheme)
152 self.set_active_scheme(default_scheme)
152 self.set_active_scheme(default_scheme)
153
153
154 def copy(self):
154 def copy(self):
155 """Return full copy of object"""
155 """Return full copy of object"""
156 return ColorSchemeTable(self.values(),self.active_scheme_name)
156 return ColorSchemeTable(self.values(),self.active_scheme_name)
157
157
158 def add_scheme(self,new_scheme):
158 def add_scheme(self,new_scheme):
159 """Add a new color scheme to the table."""
159 """Add a new color scheme to the table."""
160 if not isinstance(new_scheme,ColorScheme):
160 if not isinstance(new_scheme,ColorScheme):
161 raise ValueError('ColorSchemeTable only accepts ColorScheme instances')
161 raise ValueError('ColorSchemeTable only accepts ColorScheme instances')
162 self[new_scheme.name] = new_scheme
162 self[new_scheme.name] = new_scheme
163
163
164 def set_active_scheme(self,scheme,case_sensitive=0):
164 def set_active_scheme(self,scheme,case_sensitive=0):
165 """Set the currently active scheme.
165 """Set the currently active scheme.
166
166
167 Names are by default compared in a case-insensitive way, but this can
167 Names are by default compared in a case-insensitive way, but this can
168 be changed by setting the parameter case_sensitive to true."""
168 be changed by setting the parameter case_sensitive to true."""
169
169
170 scheme_names = list(self.keys())
170 scheme_names = list(self.keys())
171 if case_sensitive:
171 if case_sensitive:
172 valid_schemes = scheme_names
172 valid_schemes = scheme_names
173 scheme_test = scheme
173 scheme_test = scheme
174 else:
174 else:
175 valid_schemes = [s.lower() for s in scheme_names]
175 valid_schemes = [s.lower() for s in scheme_names]
176 scheme_test = scheme.lower()
176 scheme_test = scheme.lower()
177 try:
177 try:
178 scheme_idx = valid_schemes.index(scheme_test)
178 scheme_idx = valid_schemes.index(scheme_test)
179 except ValueError:
179 except ValueError as e:
180 raise ValueError('Unrecognized color scheme: ' + scheme + \
180 raise ValueError('Unrecognized color scheme: ' + scheme + \
181 '\nValid schemes: '+str(scheme_names).replace("'', ",''))
181 '\nValid schemes: '+str(scheme_names).replace("'', ",'')) from e
182 else:
182 else:
183 active = scheme_names[scheme_idx]
183 active = scheme_names[scheme_idx]
184 self.active_scheme_name = active
184 self.active_scheme_name = active
185 self.active_colors = self[active].colors
185 self.active_colors = self[active].colors
186 # Now allow using '' as an index for the current active scheme
186 # Now allow using '' as an index for the current active scheme
187 self[''] = self[active]
187 self[''] = self[active]
@@ -1,39 +1,39 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 A simple utility to import something by its string name.
3 A simple utility to import something by its string name.
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9
9
10 def import_item(name):
10 def import_item(name):
11 """Import and return ``bar`` given the string ``foo.bar``.
11 """Import and return ``bar`` given the string ``foo.bar``.
12
12
13 Calling ``bar = import_item("foo.bar")`` is the functional equivalent of
13 Calling ``bar = import_item("foo.bar")`` is the functional equivalent of
14 executing the code ``from foo import bar``.
14 executing the code ``from foo import bar``.
15
15
16 Parameters
16 Parameters
17 ----------
17 ----------
18 name : string
18 name : string
19 The fully qualified name of the module/package being imported.
19 The fully qualified name of the module/package being imported.
20
20
21 Returns
21 Returns
22 -------
22 -------
23 mod : module object
23 mod : module object
24 The module that was imported.
24 The module that was imported.
25 """
25 """
26
26
27 parts = name.rsplit('.', 1)
27 parts = name.rsplit('.', 1)
28 if len(parts) == 2:
28 if len(parts) == 2:
29 # called with 'foo.bar....'
29 # called with 'foo.bar....'
30 package, obj = parts
30 package, obj = parts
31 module = __import__(package, fromlist=[obj])
31 module = __import__(package, fromlist=[obj])
32 try:
32 try:
33 pak = getattr(module, obj)
33 pak = getattr(module, obj)
34 except AttributeError:
34 except AttributeError as e:
35 raise ImportError('No module named %s' % obj)
35 raise ImportError('No module named %s' % obj) from e
36 return pak
36 return pak
37 else:
37 else:
38 # called with un-dotted string
38 # called with un-dotted string
39 return __import__(parts[0])
39 return __import__(parts[0])
@@ -1,391 +1,391 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """A dict subclass that supports attribute style access.
2 """A dict subclass that supports attribute style access.
3
3
4 Authors:
4 Authors:
5
5
6 * Fernando Perez (original)
6 * Fernando Perez (original)
7 * Brian Granger (refactoring to a dict subclass)
7 * Brian Granger (refactoring to a dict subclass)
8 """
8 """
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Copyright (C) 2008-2011 The IPython Development Team
11 # Copyright (C) 2008-2011 The IPython Development Team
12 #
12 #
13 # Distributed under the terms of the BSD License. The full license is in
13 # Distributed under the terms of the BSD License. The full license is in
14 # the file COPYING, distributed as part of this software.
14 # the file COPYING, distributed as part of this software.
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Imports
18 # Imports
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 __all__ = ['Struct']
21 __all__ = ['Struct']
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Code
24 # Code
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27
27
28 class Struct(dict):
28 class Struct(dict):
29 """A dict subclass with attribute style access.
29 """A dict subclass with attribute style access.
30
30
31 This dict subclass has a a few extra features:
31 This dict subclass has a a few extra features:
32
32
33 * Attribute style access.
33 * Attribute style access.
34 * Protection of class members (like keys, items) when using attribute
34 * Protection of class members (like keys, items) when using attribute
35 style access.
35 style access.
36 * The ability to restrict assignment to only existing keys.
36 * The ability to restrict assignment to only existing keys.
37 * Intelligent merging.
37 * Intelligent merging.
38 * Overloaded operators.
38 * Overloaded operators.
39 """
39 """
40 _allownew = True
40 _allownew = True
41 def __init__(self, *args, **kw):
41 def __init__(self, *args, **kw):
42 """Initialize with a dictionary, another Struct, or data.
42 """Initialize with a dictionary, another Struct, or data.
43
43
44 Parameters
44 Parameters
45 ----------
45 ----------
46 args : dict, Struct
46 args : dict, Struct
47 Initialize with one dict or Struct
47 Initialize with one dict or Struct
48 kw : dict
48 kw : dict
49 Initialize with key, value pairs.
49 Initialize with key, value pairs.
50
50
51 Examples
51 Examples
52 --------
52 --------
53
53
54 >>> s = Struct(a=10,b=30)
54 >>> s = Struct(a=10,b=30)
55 >>> s.a
55 >>> s.a
56 10
56 10
57 >>> s.b
57 >>> s.b
58 30
58 30
59 >>> s2 = Struct(s,c=30)
59 >>> s2 = Struct(s,c=30)
60 >>> sorted(s2.keys())
60 >>> sorted(s2.keys())
61 ['a', 'b', 'c']
61 ['a', 'b', 'c']
62 """
62 """
63 object.__setattr__(self, '_allownew', True)
63 object.__setattr__(self, '_allownew', True)
64 dict.__init__(self, *args, **kw)
64 dict.__init__(self, *args, **kw)
65
65
66 def __setitem__(self, key, value):
66 def __setitem__(self, key, value):
67 """Set an item with check for allownew.
67 """Set an item with check for allownew.
68
68
69 Examples
69 Examples
70 --------
70 --------
71
71
72 >>> s = Struct()
72 >>> s = Struct()
73 >>> s['a'] = 10
73 >>> s['a'] = 10
74 >>> s.allow_new_attr(False)
74 >>> s.allow_new_attr(False)
75 >>> s['a'] = 10
75 >>> s['a'] = 10
76 >>> s['a']
76 >>> s['a']
77 10
77 10
78 >>> try:
78 >>> try:
79 ... s['b'] = 20
79 ... s['b'] = 20
80 ... except KeyError:
80 ... except KeyError:
81 ... print('this is not allowed')
81 ... print('this is not allowed')
82 ...
82 ...
83 this is not allowed
83 this is not allowed
84 """
84 """
85 if not self._allownew and key not in self:
85 if not self._allownew and key not in self:
86 raise KeyError(
86 raise KeyError(
87 "can't create new attribute %s when allow_new_attr(False)" % key)
87 "can't create new attribute %s when allow_new_attr(False)" % key)
88 dict.__setitem__(self, key, value)
88 dict.__setitem__(self, key, value)
89
89
90 def __setattr__(self, key, value):
90 def __setattr__(self, key, value):
91 """Set an attr with protection of class members.
91 """Set an attr with protection of class members.
92
92
93 This calls :meth:`self.__setitem__` but convert :exc:`KeyError` to
93 This calls :meth:`self.__setitem__` but convert :exc:`KeyError` to
94 :exc:`AttributeError`.
94 :exc:`AttributeError`.
95
95
96 Examples
96 Examples
97 --------
97 --------
98
98
99 >>> s = Struct()
99 >>> s = Struct()
100 >>> s.a = 10
100 >>> s.a = 10
101 >>> s.a
101 >>> s.a
102 10
102 10
103 >>> try:
103 >>> try:
104 ... s.get = 10
104 ... s.get = 10
105 ... except AttributeError:
105 ... except AttributeError:
106 ... print("you can't set a class member")
106 ... print("you can't set a class member")
107 ...
107 ...
108 you can't set a class member
108 you can't set a class member
109 """
109 """
110 # If key is an str it might be a class member or instance var
110 # If key is an str it might be a class member or instance var
111 if isinstance(key, str):
111 if isinstance(key, str):
112 # I can't simply call hasattr here because it calls getattr, which
112 # I can't simply call hasattr here because it calls getattr, which
113 # calls self.__getattr__, which returns True for keys in
113 # calls self.__getattr__, which returns True for keys in
114 # self._data. But I only want keys in the class and in
114 # self._data. But I only want keys in the class and in
115 # self.__dict__
115 # self.__dict__
116 if key in self.__dict__ or hasattr(Struct, key):
116 if key in self.__dict__ or hasattr(Struct, key):
117 raise AttributeError(
117 raise AttributeError(
118 'attr %s is a protected member of class Struct.' % key
118 'attr %s is a protected member of class Struct.' % key
119 )
119 )
120 try:
120 try:
121 self.__setitem__(key, value)
121 self.__setitem__(key, value)
122 except KeyError as e:
122 except KeyError as e:
123 raise AttributeError(e)
123 raise AttributeError(e) from e
124
124
125 def __getattr__(self, key):
125 def __getattr__(self, key):
126 """Get an attr by calling :meth:`dict.__getitem__`.
126 """Get an attr by calling :meth:`dict.__getitem__`.
127
127
128 Like :meth:`__setattr__`, this method converts :exc:`KeyError` to
128 Like :meth:`__setattr__`, this method converts :exc:`KeyError` to
129 :exc:`AttributeError`.
129 :exc:`AttributeError`.
130
130
131 Examples
131 Examples
132 --------
132 --------
133
133
134 >>> s = Struct(a=10)
134 >>> s = Struct(a=10)
135 >>> s.a
135 >>> s.a
136 10
136 10
137 >>> type(s.get)
137 >>> type(s.get)
138 <... 'builtin_function_or_method'>
138 <... 'builtin_function_or_method'>
139 >>> try:
139 >>> try:
140 ... s.b
140 ... s.b
141 ... except AttributeError:
141 ... except AttributeError:
142 ... print("I don't have that key")
142 ... print("I don't have that key")
143 ...
143 ...
144 I don't have that key
144 I don't have that key
145 """
145 """
146 try:
146 try:
147 result = self[key]
147 result = self[key]
148 except KeyError:
148 except KeyError as e:
149 raise AttributeError(key)
149 raise AttributeError(key) from e
150 else:
150 else:
151 return result
151 return result
152
152
153 def __iadd__(self, other):
153 def __iadd__(self, other):
154 """s += s2 is a shorthand for s.merge(s2).
154 """s += s2 is a shorthand for s.merge(s2).
155
155
156 Examples
156 Examples
157 --------
157 --------
158
158
159 >>> s = Struct(a=10,b=30)
159 >>> s = Struct(a=10,b=30)
160 >>> s2 = Struct(a=20,c=40)
160 >>> s2 = Struct(a=20,c=40)
161 >>> s += s2
161 >>> s += s2
162 >>> sorted(s.keys())
162 >>> sorted(s.keys())
163 ['a', 'b', 'c']
163 ['a', 'b', 'c']
164 """
164 """
165 self.merge(other)
165 self.merge(other)
166 return self
166 return self
167
167
168 def __add__(self,other):
168 def __add__(self,other):
169 """s + s2 -> New Struct made from s.merge(s2).
169 """s + s2 -> New Struct made from s.merge(s2).
170
170
171 Examples
171 Examples
172 --------
172 --------
173
173
174 >>> s1 = Struct(a=10,b=30)
174 >>> s1 = Struct(a=10,b=30)
175 >>> s2 = Struct(a=20,c=40)
175 >>> s2 = Struct(a=20,c=40)
176 >>> s = s1 + s2
176 >>> s = s1 + s2
177 >>> sorted(s.keys())
177 >>> sorted(s.keys())
178 ['a', 'b', 'c']
178 ['a', 'b', 'c']
179 """
179 """
180 sout = self.copy()
180 sout = self.copy()
181 sout.merge(other)
181 sout.merge(other)
182 return sout
182 return sout
183
183
184 def __sub__(self,other):
184 def __sub__(self,other):
185 """s1 - s2 -> remove keys in s2 from s1.
185 """s1 - s2 -> remove keys in s2 from s1.
186
186
187 Examples
187 Examples
188 --------
188 --------
189
189
190 >>> s1 = Struct(a=10,b=30)
190 >>> s1 = Struct(a=10,b=30)
191 >>> s2 = Struct(a=40)
191 >>> s2 = Struct(a=40)
192 >>> s = s1 - s2
192 >>> s = s1 - s2
193 >>> s
193 >>> s
194 {'b': 30}
194 {'b': 30}
195 """
195 """
196 sout = self.copy()
196 sout = self.copy()
197 sout -= other
197 sout -= other
198 return sout
198 return sout
199
199
200 def __isub__(self,other):
200 def __isub__(self,other):
201 """Inplace remove keys from self that are in other.
201 """Inplace remove keys from self that are in other.
202
202
203 Examples
203 Examples
204 --------
204 --------
205
205
206 >>> s1 = Struct(a=10,b=30)
206 >>> s1 = Struct(a=10,b=30)
207 >>> s2 = Struct(a=40)
207 >>> s2 = Struct(a=40)
208 >>> s1 -= s2
208 >>> s1 -= s2
209 >>> s1
209 >>> s1
210 {'b': 30}
210 {'b': 30}
211 """
211 """
212 for k in other.keys():
212 for k in other.keys():
213 if k in self:
213 if k in self:
214 del self[k]
214 del self[k]
215 return self
215 return self
216
216
217 def __dict_invert(self, data):
217 def __dict_invert(self, data):
218 """Helper function for merge.
218 """Helper function for merge.
219
219
220 Takes a dictionary whose values are lists and returns a dict with
220 Takes a dictionary whose values are lists and returns a dict with
221 the elements of each list as keys and the original keys as values.
221 the elements of each list as keys and the original keys as values.
222 """
222 """
223 outdict = {}
223 outdict = {}
224 for k,lst in data.items():
224 for k,lst in data.items():
225 if isinstance(lst, str):
225 if isinstance(lst, str):
226 lst = lst.split()
226 lst = lst.split()
227 for entry in lst:
227 for entry in lst:
228 outdict[entry] = k
228 outdict[entry] = k
229 return outdict
229 return outdict
230
230
231 def dict(self):
231 def dict(self):
232 return self
232 return self
233
233
234 def copy(self):
234 def copy(self):
235 """Return a copy as a Struct.
235 """Return a copy as a Struct.
236
236
237 Examples
237 Examples
238 --------
238 --------
239
239
240 >>> s = Struct(a=10,b=30)
240 >>> s = Struct(a=10,b=30)
241 >>> s2 = s.copy()
241 >>> s2 = s.copy()
242 >>> type(s2) is Struct
242 >>> type(s2) is Struct
243 True
243 True
244 """
244 """
245 return Struct(dict.copy(self))
245 return Struct(dict.copy(self))
246
246
247 def hasattr(self, key):
247 def hasattr(self, key):
248 """hasattr function available as a method.
248 """hasattr function available as a method.
249
249
250 Implemented like has_key.
250 Implemented like has_key.
251
251
252 Examples
252 Examples
253 --------
253 --------
254
254
255 >>> s = Struct(a=10)
255 >>> s = Struct(a=10)
256 >>> s.hasattr('a')
256 >>> s.hasattr('a')
257 True
257 True
258 >>> s.hasattr('b')
258 >>> s.hasattr('b')
259 False
259 False
260 >>> s.hasattr('get')
260 >>> s.hasattr('get')
261 False
261 False
262 """
262 """
263 return key in self
263 return key in self
264
264
265 def allow_new_attr(self, allow = True):
265 def allow_new_attr(self, allow = True):
266 """Set whether new attributes can be created in this Struct.
266 """Set whether new attributes can be created in this Struct.
267
267
268 This can be used to catch typos by verifying that the attribute user
268 This can be used to catch typos by verifying that the attribute user
269 tries to change already exists in this Struct.
269 tries to change already exists in this Struct.
270 """
270 """
271 object.__setattr__(self, '_allownew', allow)
271 object.__setattr__(self, '_allownew', allow)
272
272
273 def merge(self, __loc_data__=None, __conflict_solve=None, **kw):
273 def merge(self, __loc_data__=None, __conflict_solve=None, **kw):
274 """Merge two Structs with customizable conflict resolution.
274 """Merge two Structs with customizable conflict resolution.
275
275
276 This is similar to :meth:`update`, but much more flexible. First, a
276 This is similar to :meth:`update`, but much more flexible. First, a
277 dict is made from data+key=value pairs. When merging this dict with
277 dict is made from data+key=value pairs. When merging this dict with
278 the Struct S, the optional dictionary 'conflict' is used to decide
278 the Struct S, the optional dictionary 'conflict' is used to decide
279 what to do.
279 what to do.
280
280
281 If conflict is not given, the default behavior is to preserve any keys
281 If conflict is not given, the default behavior is to preserve any keys
282 with their current value (the opposite of the :meth:`update` method's
282 with their current value (the opposite of the :meth:`update` method's
283 behavior).
283 behavior).
284
284
285 Parameters
285 Parameters
286 ----------
286 ----------
287 __loc_data : dict, Struct
287 __loc_data : dict, Struct
288 The data to merge into self
288 The data to merge into self
289 __conflict_solve : dict
289 __conflict_solve : dict
290 The conflict policy dict. The keys are binary functions used to
290 The conflict policy dict. The keys are binary functions used to
291 resolve the conflict and the values are lists of strings naming
291 resolve the conflict and the values are lists of strings naming
292 the keys the conflict resolution function applies to. Instead of
292 the keys the conflict resolution function applies to. Instead of
293 a list of strings a space separated string can be used, like
293 a list of strings a space separated string can be used, like
294 'a b c'.
294 'a b c'.
295 kw : dict
295 kw : dict
296 Additional key, value pairs to merge in
296 Additional key, value pairs to merge in
297
297
298 Notes
298 Notes
299 -----
299 -----
300
300
301 The `__conflict_solve` dict is a dictionary of binary functions which will be used to
301 The `__conflict_solve` dict is a dictionary of binary functions which will be used to
302 solve key conflicts. Here is an example::
302 solve key conflicts. Here is an example::
303
303
304 __conflict_solve = dict(
304 __conflict_solve = dict(
305 func1=['a','b','c'],
305 func1=['a','b','c'],
306 func2=['d','e']
306 func2=['d','e']
307 )
307 )
308
308
309 In this case, the function :func:`func1` will be used to resolve
309 In this case, the function :func:`func1` will be used to resolve
310 keys 'a', 'b' and 'c' and the function :func:`func2` will be used for
310 keys 'a', 'b' and 'c' and the function :func:`func2` will be used for
311 keys 'd' and 'e'. This could also be written as::
311 keys 'd' and 'e'. This could also be written as::
312
312
313 __conflict_solve = dict(func1='a b c',func2='d e')
313 __conflict_solve = dict(func1='a b c',func2='d e')
314
314
315 These functions will be called for each key they apply to with the
315 These functions will be called for each key they apply to with the
316 form::
316 form::
317
317
318 func1(self['a'], other['a'])
318 func1(self['a'], other['a'])
319
319
320 The return value is used as the final merged value.
320 The return value is used as the final merged value.
321
321
322 As a convenience, merge() provides five (the most commonly needed)
322 As a convenience, merge() provides five (the most commonly needed)
323 pre-defined policies: preserve, update, add, add_flip and add_s. The
323 pre-defined policies: preserve, update, add, add_flip and add_s. The
324 easiest explanation is their implementation::
324 easiest explanation is their implementation::
325
325
326 preserve = lambda old,new: old
326 preserve = lambda old,new: old
327 update = lambda old,new: new
327 update = lambda old,new: new
328 add = lambda old,new: old + new
328 add = lambda old,new: old + new
329 add_flip = lambda old,new: new + old # note change of order!
329 add_flip = lambda old,new: new + old # note change of order!
330 add_s = lambda old,new: old + ' ' + new # only for str!
330 add_s = lambda old,new: old + ' ' + new # only for str!
331
331
332 You can use those four words (as strings) as keys instead
332 You can use those four words (as strings) as keys instead
333 of defining them as functions, and the merge method will substitute
333 of defining them as functions, and the merge method will substitute
334 the appropriate functions for you.
334 the appropriate functions for you.
335
335
336 For more complicated conflict resolution policies, you still need to
336 For more complicated conflict resolution policies, you still need to
337 construct your own functions.
337 construct your own functions.
338
338
339 Examples
339 Examples
340 --------
340 --------
341
341
342 This show the default policy:
342 This show the default policy:
343
343
344 >>> s = Struct(a=10,b=30)
344 >>> s = Struct(a=10,b=30)
345 >>> s2 = Struct(a=20,c=40)
345 >>> s2 = Struct(a=20,c=40)
346 >>> s.merge(s2)
346 >>> s.merge(s2)
347 >>> sorted(s.items())
347 >>> sorted(s.items())
348 [('a', 10), ('b', 30), ('c', 40)]
348 [('a', 10), ('b', 30), ('c', 40)]
349
349
350 Now, show how to specify a conflict dict:
350 Now, show how to specify a conflict dict:
351
351
352 >>> s = Struct(a=10,b=30)
352 >>> s = Struct(a=10,b=30)
353 >>> s2 = Struct(a=20,b=40)
353 >>> s2 = Struct(a=20,b=40)
354 >>> conflict = {'update':'a','add':'b'}
354 >>> conflict = {'update':'a','add':'b'}
355 >>> s.merge(s2,conflict)
355 >>> s.merge(s2,conflict)
356 >>> sorted(s.items())
356 >>> sorted(s.items())
357 [('a', 20), ('b', 70)]
357 [('a', 20), ('b', 70)]
358 """
358 """
359
359
360 data_dict = dict(__loc_data__,**kw)
360 data_dict = dict(__loc_data__,**kw)
361
361
362 # policies for conflict resolution: two argument functions which return
362 # policies for conflict resolution: two argument functions which return
363 # the value that will go in the new struct
363 # the value that will go in the new struct
364 preserve = lambda old,new: old
364 preserve = lambda old,new: old
365 update = lambda old,new: new
365 update = lambda old,new: new
366 add = lambda old,new: old + new
366 add = lambda old,new: old + new
367 add_flip = lambda old,new: new + old # note change of order!
367 add_flip = lambda old,new: new + old # note change of order!
368 add_s = lambda old,new: old + ' ' + new
368 add_s = lambda old,new: old + ' ' + new
369
369
370 # default policy is to keep current keys when there's a conflict
370 # default policy is to keep current keys when there's a conflict
371 conflict_solve = dict.fromkeys(self, preserve)
371 conflict_solve = dict.fromkeys(self, preserve)
372
372
373 # the conflict_solve dictionary is given by the user 'inverted': we
373 # the conflict_solve dictionary is given by the user 'inverted': we
374 # need a name-function mapping, it comes as a function -> names
374 # need a name-function mapping, it comes as a function -> names
375 # dict. Make a local copy (b/c we'll make changes), replace user
375 # dict. Make a local copy (b/c we'll make changes), replace user
376 # strings for the three builtin policies and invert it.
376 # strings for the three builtin policies and invert it.
377 if __conflict_solve:
377 if __conflict_solve:
378 inv_conflict_solve_user = __conflict_solve.copy()
378 inv_conflict_solve_user = __conflict_solve.copy()
379 for name, func in [('preserve',preserve), ('update',update),
379 for name, func in [('preserve',preserve), ('update',update),
380 ('add',add), ('add_flip',add_flip),
380 ('add',add), ('add_flip',add_flip),
381 ('add_s',add_s)]:
381 ('add_s',add_s)]:
382 if name in inv_conflict_solve_user.keys():
382 if name in inv_conflict_solve_user.keys():
383 inv_conflict_solve_user[func] = inv_conflict_solve_user[name]
383 inv_conflict_solve_user[func] = inv_conflict_solve_user[name]
384 del inv_conflict_solve_user[name]
384 del inv_conflict_solve_user[name]
385 conflict_solve.update(self.__dict_invert(inv_conflict_solve_user))
385 conflict_solve.update(self.__dict_invert(inv_conflict_solve_user))
386 for key in data_dict:
386 for key in data_dict:
387 if key not in self:
387 if key not in self:
388 self[key] = data_dict[key]
388 self[key] = data_dict[key]
389 else:
389 else:
390 self[key] = conflict_solve[key](self[key],data_dict[key])
390 self[key] = conflict_solve[key](self[key],data_dict[key])
391
391
@@ -1,436 +1,436 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Utilities for path handling.
3 Utilities for path handling.
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 import os
9 import os
10 import sys
10 import sys
11 import errno
11 import errno
12 import shutil
12 import shutil
13 import random
13 import random
14 import glob
14 import glob
15 from warnings import warn
15 from warnings import warn
16
16
17 from IPython.utils.process import system
17 from IPython.utils.process import system
18 from IPython.utils.decorators import undoc
18 from IPython.utils.decorators import undoc
19
19
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21 # Code
21 # Code
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 fs_encoding = sys.getfilesystemencoding()
23 fs_encoding = sys.getfilesystemencoding()
24
24
25 def _writable_dir(path):
25 def _writable_dir(path):
26 """Whether `path` is a directory, to which the user has write access."""
26 """Whether `path` is a directory, to which the user has write access."""
27 return os.path.isdir(path) and os.access(path, os.W_OK)
27 return os.path.isdir(path) and os.access(path, os.W_OK)
28
28
29 if sys.platform == 'win32':
29 if sys.platform == 'win32':
30 def _get_long_path_name(path):
30 def _get_long_path_name(path):
31 """Get a long path name (expand ~) on Windows using ctypes.
31 """Get a long path name (expand ~) on Windows using ctypes.
32
32
33 Examples
33 Examples
34 --------
34 --------
35
35
36 >>> get_long_path_name('c:\\docume~1')
36 >>> get_long_path_name('c:\\docume~1')
37 'c:\\\\Documents and Settings'
37 'c:\\\\Documents and Settings'
38
38
39 """
39 """
40 try:
40 try:
41 import ctypes
41 import ctypes
42 except ImportError:
42 except ImportError as e:
43 raise ImportError('you need to have ctypes installed for this to work')
43 raise ImportError('you need to have ctypes installed for this to work') from e
44 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
44 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
45 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
45 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
46 ctypes.c_uint ]
46 ctypes.c_uint ]
47
47
48 buf = ctypes.create_unicode_buffer(260)
48 buf = ctypes.create_unicode_buffer(260)
49 rv = _GetLongPathName(path, buf, 260)
49 rv = _GetLongPathName(path, buf, 260)
50 if rv == 0 or rv > 260:
50 if rv == 0 or rv > 260:
51 return path
51 return path
52 else:
52 else:
53 return buf.value
53 return buf.value
54 else:
54 else:
55 def _get_long_path_name(path):
55 def _get_long_path_name(path):
56 """Dummy no-op."""
56 """Dummy no-op."""
57 return path
57 return path
58
58
59
59
60
60
61 def get_long_path_name(path):
61 def get_long_path_name(path):
62 """Expand a path into its long form.
62 """Expand a path into its long form.
63
63
64 On Windows this expands any ~ in the paths. On other platforms, it is
64 On Windows this expands any ~ in the paths. On other platforms, it is
65 a null operation.
65 a null operation.
66 """
66 """
67 return _get_long_path_name(path)
67 return _get_long_path_name(path)
68
68
69
69
70 def unquote_filename(name, win32=(sys.platform=='win32')):
70 def unquote_filename(name, win32=(sys.platform=='win32')):
71 """ On Windows, remove leading and trailing quotes from filenames.
71 """ On Windows, remove leading and trailing quotes from filenames.
72
72
73 This function has been deprecated and should not be used any more:
73 This function has been deprecated and should not be used any more:
74 unquoting is now taken care of by :func:`IPython.utils.process.arg_split`.
74 unquoting is now taken care of by :func:`IPython.utils.process.arg_split`.
75 """
75 """
76 warn("'unquote_filename' is deprecated since IPython 5.0 and should not "
76 warn("'unquote_filename' is deprecated since IPython 5.0 and should not "
77 "be used anymore", DeprecationWarning, stacklevel=2)
77 "be used anymore", DeprecationWarning, stacklevel=2)
78 if win32:
78 if win32:
79 if name.startswith(("'", '"')) and name.endswith(("'", '"')):
79 if name.startswith(("'", '"')) and name.endswith(("'", '"')):
80 name = name[1:-1]
80 name = name[1:-1]
81 return name
81 return name
82
82
83
83
84 def compress_user(path):
84 def compress_user(path):
85 """Reverse of :func:`os.path.expanduser`
85 """Reverse of :func:`os.path.expanduser`
86 """
86 """
87 home = os.path.expanduser('~')
87 home = os.path.expanduser('~')
88 if path.startswith(home):
88 if path.startswith(home):
89 path = "~" + path[len(home):]
89 path = "~" + path[len(home):]
90 return path
90 return path
91
91
92 def get_py_filename(name, force_win32=None):
92 def get_py_filename(name, force_win32=None):
93 """Return a valid python filename in the current directory.
93 """Return a valid python filename in the current directory.
94
94
95 If the given name is not a file, it adds '.py' and searches again.
95 If the given name is not a file, it adds '.py' and searches again.
96 Raises IOError with an informative message if the file isn't found.
96 Raises IOError with an informative message if the file isn't found.
97 """
97 """
98
98
99 name = os.path.expanduser(name)
99 name = os.path.expanduser(name)
100 if force_win32 is not None:
100 if force_win32 is not None:
101 warn("The 'force_win32' argument to 'get_py_filename' is deprecated "
101 warn("The 'force_win32' argument to 'get_py_filename' is deprecated "
102 "since IPython 5.0 and should not be used anymore",
102 "since IPython 5.0 and should not be used anymore",
103 DeprecationWarning, stacklevel=2)
103 DeprecationWarning, stacklevel=2)
104 if not os.path.isfile(name) and not name.endswith('.py'):
104 if not os.path.isfile(name) and not name.endswith('.py'):
105 name += '.py'
105 name += '.py'
106 if os.path.isfile(name):
106 if os.path.isfile(name):
107 return name
107 return name
108 else:
108 else:
109 raise IOError('File `%r` not found.' % name)
109 raise IOError('File `%r` not found.' % name)
110
110
111
111
112 def filefind(filename, path_dirs=None):
112 def filefind(filename, path_dirs=None):
113 """Find a file by looking through a sequence of paths.
113 """Find a file by looking through a sequence of paths.
114
114
115 This iterates through a sequence of paths looking for a file and returns
115 This iterates through a sequence of paths looking for a file and returns
116 the full, absolute path of the first occurrence of the file. If no set of
116 the full, absolute path of the first occurrence of the file. If no set of
117 path dirs is given, the filename is tested as is, after running through
117 path dirs is given, the filename is tested as is, after running through
118 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
118 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
119
119
120 filefind('myfile.txt')
120 filefind('myfile.txt')
121
121
122 will find the file in the current working dir, but::
122 will find the file in the current working dir, but::
123
123
124 filefind('~/myfile.txt')
124 filefind('~/myfile.txt')
125
125
126 Will find the file in the users home directory. This function does not
126 Will find the file in the users home directory. This function does not
127 automatically try any paths, such as the cwd or the user's home directory.
127 automatically try any paths, such as the cwd or the user's home directory.
128
128
129 Parameters
129 Parameters
130 ----------
130 ----------
131 filename : str
131 filename : str
132 The filename to look for.
132 The filename to look for.
133 path_dirs : str, None or sequence of str
133 path_dirs : str, None or sequence of str
134 The sequence of paths to look for the file in. If None, the filename
134 The sequence of paths to look for the file in. If None, the filename
135 need to be absolute or be in the cwd. If a string, the string is
135 need to be absolute or be in the cwd. If a string, the string is
136 put into a sequence and the searched. If a sequence, walk through
136 put into a sequence and the searched. If a sequence, walk through
137 each element and join with ``filename``, calling :func:`expandvars`
137 each element and join with ``filename``, calling :func:`expandvars`
138 and :func:`expanduser` before testing for existence.
138 and :func:`expanduser` before testing for existence.
139
139
140 Returns
140 Returns
141 -------
141 -------
142 Raises :exc:`IOError` or returns absolute path to file.
142 Raises :exc:`IOError` or returns absolute path to file.
143 """
143 """
144
144
145 # If paths are quoted, abspath gets confused, strip them...
145 # If paths are quoted, abspath gets confused, strip them...
146 filename = filename.strip('"').strip("'")
146 filename = filename.strip('"').strip("'")
147 # If the input is an absolute path, just check it exists
147 # If the input is an absolute path, just check it exists
148 if os.path.isabs(filename) and os.path.isfile(filename):
148 if os.path.isabs(filename) and os.path.isfile(filename):
149 return filename
149 return filename
150
150
151 if path_dirs is None:
151 if path_dirs is None:
152 path_dirs = ("",)
152 path_dirs = ("",)
153 elif isinstance(path_dirs, str):
153 elif isinstance(path_dirs, str):
154 path_dirs = (path_dirs,)
154 path_dirs = (path_dirs,)
155
155
156 for path in path_dirs:
156 for path in path_dirs:
157 if path == '.': path = os.getcwd()
157 if path == '.': path = os.getcwd()
158 testname = expand_path(os.path.join(path, filename))
158 testname = expand_path(os.path.join(path, filename))
159 if os.path.isfile(testname):
159 if os.path.isfile(testname):
160 return os.path.abspath(testname)
160 return os.path.abspath(testname)
161
161
162 raise IOError("File %r does not exist in any of the search paths: %r" %
162 raise IOError("File %r does not exist in any of the search paths: %r" %
163 (filename, path_dirs) )
163 (filename, path_dirs) )
164
164
165
165
166 class HomeDirError(Exception):
166 class HomeDirError(Exception):
167 pass
167 pass
168
168
169
169
170 def get_home_dir(require_writable=False) -> str:
170 def get_home_dir(require_writable=False) -> str:
171 """Return the 'home' directory, as a unicode string.
171 """Return the 'home' directory, as a unicode string.
172
172
173 Uses os.path.expanduser('~'), and checks for writability.
173 Uses os.path.expanduser('~'), and checks for writability.
174
174
175 See stdlib docs for how this is determined.
175 See stdlib docs for how this is determined.
176 For Python <3.8, $HOME is first priority on *ALL* platforms.
176 For Python <3.8, $HOME is first priority on *ALL* platforms.
177 For Python >=3.8 on Windows, %HOME% is no longer considered.
177 For Python >=3.8 on Windows, %HOME% is no longer considered.
178
178
179 Parameters
179 Parameters
180 ----------
180 ----------
181
181
182 require_writable : bool [default: False]
182 require_writable : bool [default: False]
183 if True:
183 if True:
184 guarantees the return value is a writable directory, otherwise
184 guarantees the return value is a writable directory, otherwise
185 raises HomeDirError
185 raises HomeDirError
186 if False:
186 if False:
187 The path is resolved, but it is not guaranteed to exist or be writable.
187 The path is resolved, but it is not guaranteed to exist or be writable.
188 """
188 """
189
189
190 homedir = os.path.expanduser('~')
190 homedir = os.path.expanduser('~')
191 # Next line will make things work even when /home/ is a symlink to
191 # Next line will make things work even when /home/ is a symlink to
192 # /usr/home as it is on FreeBSD, for example
192 # /usr/home as it is on FreeBSD, for example
193 homedir = os.path.realpath(homedir)
193 homedir = os.path.realpath(homedir)
194
194
195 if not _writable_dir(homedir) and os.name == 'nt':
195 if not _writable_dir(homedir) and os.name == 'nt':
196 # expanduser failed, use the registry to get the 'My Documents' folder.
196 # expanduser failed, use the registry to get the 'My Documents' folder.
197 try:
197 try:
198 import winreg as wreg
198 import winreg as wreg
199 with wreg.OpenKey(
199 with wreg.OpenKey(
200 wreg.HKEY_CURRENT_USER,
200 wreg.HKEY_CURRENT_USER,
201 r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
201 r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
202 ) as key:
202 ) as key:
203 homedir = wreg.QueryValueEx(key,'Personal')[0]
203 homedir = wreg.QueryValueEx(key,'Personal')[0]
204 except:
204 except:
205 pass
205 pass
206
206
207 if (not require_writable) or _writable_dir(homedir):
207 if (not require_writable) or _writable_dir(homedir):
208 assert isinstance(homedir, str), "Homedir shoudl be unicode not bytes"
208 assert isinstance(homedir, str), "Homedir shoudl be unicode not bytes"
209 return homedir
209 return homedir
210 else:
210 else:
211 raise HomeDirError('%s is not a writable dir, '
211 raise HomeDirError('%s is not a writable dir, '
212 'set $HOME environment variable to override' % homedir)
212 'set $HOME environment variable to override' % homedir)
213
213
214 def get_xdg_dir():
214 def get_xdg_dir():
215 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
215 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
216
216
217 This is only for non-OS X posix (Linux,Unix,etc.) systems.
217 This is only for non-OS X posix (Linux,Unix,etc.) systems.
218 """
218 """
219
219
220 env = os.environ
220 env = os.environ
221
221
222 if os.name == 'posix' and sys.platform != 'darwin':
222 if os.name == 'posix' and sys.platform != 'darwin':
223 # Linux, Unix, AIX, etc.
223 # Linux, Unix, AIX, etc.
224 # use ~/.config if empty OR not set
224 # use ~/.config if empty OR not set
225 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
225 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
226 if xdg and _writable_dir(xdg):
226 if xdg and _writable_dir(xdg):
227 assert isinstance(xdg, str)
227 assert isinstance(xdg, str)
228 return xdg
228 return xdg
229
229
230 return None
230 return None
231
231
232
232
233 def get_xdg_cache_dir():
233 def get_xdg_cache_dir():
234 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
234 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
235
235
236 This is only for non-OS X posix (Linux,Unix,etc.) systems.
236 This is only for non-OS X posix (Linux,Unix,etc.) systems.
237 """
237 """
238
238
239 env = os.environ
239 env = os.environ
240
240
241 if os.name == 'posix' and sys.platform != 'darwin':
241 if os.name == 'posix' and sys.platform != 'darwin':
242 # Linux, Unix, AIX, etc.
242 # Linux, Unix, AIX, etc.
243 # use ~/.cache if empty OR not set
243 # use ~/.cache if empty OR not set
244 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
244 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
245 if xdg and _writable_dir(xdg):
245 if xdg and _writable_dir(xdg):
246 assert isinstance(xdg, str)
246 assert isinstance(xdg, str)
247 return xdg
247 return xdg
248
248
249 return None
249 return None
250
250
251
251
252 @undoc
252 @undoc
253 def get_ipython_dir():
253 def get_ipython_dir():
254 warn("get_ipython_dir has moved to the IPython.paths module since IPython 4.0.", DeprecationWarning, stacklevel=2)
254 warn("get_ipython_dir has moved to the IPython.paths module since IPython 4.0.", DeprecationWarning, stacklevel=2)
255 from IPython.paths import get_ipython_dir
255 from IPython.paths import get_ipython_dir
256 return get_ipython_dir()
256 return get_ipython_dir()
257
257
258 @undoc
258 @undoc
259 def get_ipython_cache_dir():
259 def get_ipython_cache_dir():
260 warn("get_ipython_cache_dir has moved to the IPython.paths module since IPython 4.0.", DeprecationWarning, stacklevel=2)
260 warn("get_ipython_cache_dir has moved to the IPython.paths module since IPython 4.0.", DeprecationWarning, stacklevel=2)
261 from IPython.paths import get_ipython_cache_dir
261 from IPython.paths import get_ipython_cache_dir
262 return get_ipython_cache_dir()
262 return get_ipython_cache_dir()
263
263
264 @undoc
264 @undoc
265 def get_ipython_package_dir():
265 def get_ipython_package_dir():
266 warn("get_ipython_package_dir has moved to the IPython.paths module since IPython 4.0.", DeprecationWarning, stacklevel=2)
266 warn("get_ipython_package_dir has moved to the IPython.paths module since IPython 4.0.", DeprecationWarning, stacklevel=2)
267 from IPython.paths import get_ipython_package_dir
267 from IPython.paths import get_ipython_package_dir
268 return get_ipython_package_dir()
268 return get_ipython_package_dir()
269
269
270 @undoc
270 @undoc
271 def get_ipython_module_path(module_str):
271 def get_ipython_module_path(module_str):
272 warn("get_ipython_module_path has moved to the IPython.paths module since IPython 4.0.", DeprecationWarning, stacklevel=2)
272 warn("get_ipython_module_path has moved to the IPython.paths module since IPython 4.0.", DeprecationWarning, stacklevel=2)
273 from IPython.paths import get_ipython_module_path
273 from IPython.paths import get_ipython_module_path
274 return get_ipython_module_path(module_str)
274 return get_ipython_module_path(module_str)
275
275
276 @undoc
276 @undoc
277 def locate_profile(profile='default'):
277 def locate_profile(profile='default'):
278 warn("locate_profile has moved to the IPython.paths module since IPython 4.0.", DeprecationWarning, stacklevel=2)
278 warn("locate_profile has moved to the IPython.paths module since IPython 4.0.", DeprecationWarning, stacklevel=2)
279 from IPython.paths import locate_profile
279 from IPython.paths import locate_profile
280 return locate_profile(profile=profile)
280 return locate_profile(profile=profile)
281
281
282 def expand_path(s):
282 def expand_path(s):
283 """Expand $VARS and ~names in a string, like a shell
283 """Expand $VARS and ~names in a string, like a shell
284
284
285 :Examples:
285 :Examples:
286
286
287 In [2]: os.environ['FOO']='test'
287 In [2]: os.environ['FOO']='test'
288
288
289 In [3]: expand_path('variable FOO is $FOO')
289 In [3]: expand_path('variable FOO is $FOO')
290 Out[3]: 'variable FOO is test'
290 Out[3]: 'variable FOO is test'
291 """
291 """
292 # This is a pretty subtle hack. When expand user is given a UNC path
292 # This is a pretty subtle hack. When expand user is given a UNC path
293 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
293 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
294 # the $ to get (\\server\share\%username%). I think it considered $
294 # the $ to get (\\server\share\%username%). I think it considered $
295 # alone an empty var. But, we need the $ to remains there (it indicates
295 # alone an empty var. But, we need the $ to remains there (it indicates
296 # a hidden share).
296 # a hidden share).
297 if os.name=='nt':
297 if os.name=='nt':
298 s = s.replace('$\\', 'IPYTHON_TEMP')
298 s = s.replace('$\\', 'IPYTHON_TEMP')
299 s = os.path.expandvars(os.path.expanduser(s))
299 s = os.path.expandvars(os.path.expanduser(s))
300 if os.name=='nt':
300 if os.name=='nt':
301 s = s.replace('IPYTHON_TEMP', '$\\')
301 s = s.replace('IPYTHON_TEMP', '$\\')
302 return s
302 return s
303
303
304
304
305 def unescape_glob(string):
305 def unescape_glob(string):
306 """Unescape glob pattern in `string`."""
306 """Unescape glob pattern in `string`."""
307 def unescape(s):
307 def unescape(s):
308 for pattern in '*[]!?':
308 for pattern in '*[]!?':
309 s = s.replace(r'\{0}'.format(pattern), pattern)
309 s = s.replace(r'\{0}'.format(pattern), pattern)
310 return s
310 return s
311 return '\\'.join(map(unescape, string.split('\\\\')))
311 return '\\'.join(map(unescape, string.split('\\\\')))
312
312
313
313
314 def shellglob(args):
314 def shellglob(args):
315 """
315 """
316 Do glob expansion for each element in `args` and return a flattened list.
316 Do glob expansion for each element in `args` and return a flattened list.
317
317
318 Unmatched glob pattern will remain as-is in the returned list.
318 Unmatched glob pattern will remain as-is in the returned list.
319
319
320 """
320 """
321 expanded = []
321 expanded = []
322 # Do not unescape backslash in Windows as it is interpreted as
322 # Do not unescape backslash in Windows as it is interpreted as
323 # path separator:
323 # path separator:
324 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
324 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
325 for a in args:
325 for a in args:
326 expanded.extend(glob.glob(a) or [unescape(a)])
326 expanded.extend(glob.glob(a) or [unescape(a)])
327 return expanded
327 return expanded
328
328
329
329
330 def target_outdated(target,deps):
330 def target_outdated(target,deps):
331 """Determine whether a target is out of date.
331 """Determine whether a target is out of date.
332
332
333 target_outdated(target,deps) -> 1/0
333 target_outdated(target,deps) -> 1/0
334
334
335 deps: list of filenames which MUST exist.
335 deps: list of filenames which MUST exist.
336 target: single filename which may or may not exist.
336 target: single filename which may or may not exist.
337
337
338 If target doesn't exist or is older than any file listed in deps, return
338 If target doesn't exist or is older than any file listed in deps, return
339 true, otherwise return false.
339 true, otherwise return false.
340 """
340 """
341 try:
341 try:
342 target_time = os.path.getmtime(target)
342 target_time = os.path.getmtime(target)
343 except os.error:
343 except os.error:
344 return 1
344 return 1
345 for dep in deps:
345 for dep in deps:
346 dep_time = os.path.getmtime(dep)
346 dep_time = os.path.getmtime(dep)
347 if dep_time > target_time:
347 if dep_time > target_time:
348 #print "For target",target,"Dep failed:",dep # dbg
348 #print "For target",target,"Dep failed:",dep # dbg
349 #print "times (dep,tar):",dep_time,target_time # dbg
349 #print "times (dep,tar):",dep_time,target_time # dbg
350 return 1
350 return 1
351 return 0
351 return 0
352
352
353
353
354 def target_update(target,deps,cmd):
354 def target_update(target,deps,cmd):
355 """Update a target with a given command given a list of dependencies.
355 """Update a target with a given command given a list of dependencies.
356
356
357 target_update(target,deps,cmd) -> runs cmd if target is outdated.
357 target_update(target,deps,cmd) -> runs cmd if target is outdated.
358
358
359 This is just a wrapper around target_outdated() which calls the given
359 This is just a wrapper around target_outdated() which calls the given
360 command if target is outdated."""
360 command if target is outdated."""
361
361
362 if target_outdated(target,deps):
362 if target_outdated(target,deps):
363 system(cmd)
363 system(cmd)
364
364
365
365
366 ENOLINK = 1998
366 ENOLINK = 1998
367
367
368 def link(src, dst):
368 def link(src, dst):
369 """Hard links ``src`` to ``dst``, returning 0 or errno.
369 """Hard links ``src`` to ``dst``, returning 0 or errno.
370
370
371 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
371 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
372 supported by the operating system.
372 supported by the operating system.
373 """
373 """
374
374
375 if not hasattr(os, "link"):
375 if not hasattr(os, "link"):
376 return ENOLINK
376 return ENOLINK
377 link_errno = 0
377 link_errno = 0
378 try:
378 try:
379 os.link(src, dst)
379 os.link(src, dst)
380 except OSError as e:
380 except OSError as e:
381 link_errno = e.errno
381 link_errno = e.errno
382 return link_errno
382 return link_errno
383
383
384
384
385 def link_or_copy(src, dst):
385 def link_or_copy(src, dst):
386 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
386 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
387
387
388 Attempts to maintain the semantics of ``shutil.copy``.
388 Attempts to maintain the semantics of ``shutil.copy``.
389
389
390 Because ``os.link`` does not overwrite files, a unique temporary file
390 Because ``os.link`` does not overwrite files, a unique temporary file
391 will be used if the target already exists, then that file will be moved
391 will be used if the target already exists, then that file will be moved
392 into place.
392 into place.
393 """
393 """
394
394
395 if os.path.isdir(dst):
395 if os.path.isdir(dst):
396 dst = os.path.join(dst, os.path.basename(src))
396 dst = os.path.join(dst, os.path.basename(src))
397
397
398 link_errno = link(src, dst)
398 link_errno = link(src, dst)
399 if link_errno == errno.EEXIST:
399 if link_errno == errno.EEXIST:
400 if os.stat(src).st_ino == os.stat(dst).st_ino:
400 if os.stat(src).st_ino == os.stat(dst).st_ino:
401 # dst is already a hard link to the correct file, so we don't need
401 # dst is already a hard link to the correct file, so we don't need
402 # to do anything else. If we try to link and rename the file
402 # to do anything else. If we try to link and rename the file
403 # anyway, we get duplicate files - see http://bugs.python.org/issue21876
403 # anyway, we get duplicate files - see http://bugs.python.org/issue21876
404 return
404 return
405
405
406 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
406 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
407 try:
407 try:
408 link_or_copy(src, new_dst)
408 link_or_copy(src, new_dst)
409 except:
409 except:
410 try:
410 try:
411 os.remove(new_dst)
411 os.remove(new_dst)
412 except OSError:
412 except OSError:
413 pass
413 pass
414 raise
414 raise
415 os.rename(new_dst, dst)
415 os.rename(new_dst, dst)
416 elif link_errno != 0:
416 elif link_errno != 0:
417 # Either link isn't supported, or the filesystem doesn't support
417 # Either link isn't supported, or the filesystem doesn't support
418 # linking, or 'src' and 'dst' are on different filesystems.
418 # linking, or 'src' and 'dst' are on different filesystems.
419 shutil.copy(src, dst)
419 shutil.copy(src, dst)
420
420
421 def ensure_dir_exists(path, mode=0o755):
421 def ensure_dir_exists(path, mode=0o755):
422 """ensure that a directory exists
422 """ensure that a directory exists
423
423
424 If it doesn't exist, try to create it and protect against a race condition
424 If it doesn't exist, try to create it and protect against a race condition
425 if another process is doing the same.
425 if another process is doing the same.
426
426
427 The default permissions are 755, which differ from os.makedirs default of 777.
427 The default permissions are 755, which differ from os.makedirs default of 777.
428 """
428 """
429 if not os.path.exists(path):
429 if not os.path.exists(path):
430 try:
430 try:
431 os.makedirs(path, mode=mode)
431 os.makedirs(path, mode=mode)
432 except OSError as e:
432 except OSError as e:
433 if e.errno != errno.EEXIST:
433 if e.errno != errno.EEXIST:
434 raise
434 raise
435 elif not os.path.isdir(path):
435 elif not os.path.isdir(path):
436 raise IOError("%r exists but is not a directory" % path)
436 raise IOError("%r exists but is not a directory" % path)
@@ -1,94 +1,94 b''
1 """A shim module for deprecated imports
1 """A shim module for deprecated imports
2 """
2 """
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import sys
6 import sys
7 import types
7 import types
8 from importlib import import_module
8 from importlib import import_module
9
9
10 from .importstring import import_item
10 from .importstring import import_item
11
11
12
12
13 class ShimWarning(Warning):
13 class ShimWarning(Warning):
14 """A warning to show when a module has moved, and a shim is in its place."""
14 """A warning to show when a module has moved, and a shim is in its place."""
15
15
16 class ShimImporter(object):
16 class ShimImporter(object):
17 """Import hook for a shim.
17 """Import hook for a shim.
18
18
19 This ensures that submodule imports return the real target module,
19 This ensures that submodule imports return the real target module,
20 not a clone that will confuse `is` and `isinstance` checks.
20 not a clone that will confuse `is` and `isinstance` checks.
21 """
21 """
22 def __init__(self, src, mirror):
22 def __init__(self, src, mirror):
23 self.src = src
23 self.src = src
24 self.mirror = mirror
24 self.mirror = mirror
25
25
26 def _mirror_name(self, fullname):
26 def _mirror_name(self, fullname):
27 """get the name of the mirrored module"""
27 """get the name of the mirrored module"""
28
28
29 return self.mirror + fullname[len(self.src):]
29 return self.mirror + fullname[len(self.src):]
30
30
31 def find_module(self, fullname, path=None):
31 def find_module(self, fullname, path=None):
32 """Return self if we should be used to import the module."""
32 """Return self if we should be used to import the module."""
33 if fullname.startswith(self.src + '.'):
33 if fullname.startswith(self.src + '.'):
34 mirror_name = self._mirror_name(fullname)
34 mirror_name = self._mirror_name(fullname)
35 try:
35 try:
36 mod = import_item(mirror_name)
36 mod = import_item(mirror_name)
37 except ImportError:
37 except ImportError:
38 return
38 return
39 else:
39 else:
40 if not isinstance(mod, types.ModuleType):
40 if not isinstance(mod, types.ModuleType):
41 # not a module
41 # not a module
42 return None
42 return None
43 return self
43 return self
44
44
45 def load_module(self, fullname):
45 def load_module(self, fullname):
46 """Import the mirrored module, and insert it into sys.modules"""
46 """Import the mirrored module, and insert it into sys.modules"""
47 mirror_name = self._mirror_name(fullname)
47 mirror_name = self._mirror_name(fullname)
48 mod = import_item(mirror_name)
48 mod = import_item(mirror_name)
49 sys.modules[fullname] = mod
49 sys.modules[fullname] = mod
50 return mod
50 return mod
51
51
52
52
53 class ShimModule(types.ModuleType):
53 class ShimModule(types.ModuleType):
54
54
55 def __init__(self, *args, **kwargs):
55 def __init__(self, *args, **kwargs):
56 self._mirror = kwargs.pop("mirror")
56 self._mirror = kwargs.pop("mirror")
57 src = kwargs.pop("src", None)
57 src = kwargs.pop("src", None)
58 if src:
58 if src:
59 kwargs['name'] = src.rsplit('.', 1)[-1]
59 kwargs['name'] = src.rsplit('.', 1)[-1]
60 super(ShimModule, self).__init__(*args, **kwargs)
60 super(ShimModule, self).__init__(*args, **kwargs)
61 # add import hook for descendent modules
61 # add import hook for descendent modules
62 if src:
62 if src:
63 sys.meta_path.append(
63 sys.meta_path.append(
64 ShimImporter(src=src, mirror=self._mirror)
64 ShimImporter(src=src, mirror=self._mirror)
65 )
65 )
66
66
67 @property
67 @property
68 def __path__(self):
68 def __path__(self):
69 return []
69 return []
70
70
71 @property
71 @property
72 def __spec__(self):
72 def __spec__(self):
73 """Don't produce __spec__ until requested"""
73 """Don't produce __spec__ until requested"""
74 return import_module(self._mirror).__spec__
74 return import_module(self._mirror).__spec__
75
75
76 def __dir__(self):
76 def __dir__(self):
77 return dir(import_module(self._mirror))
77 return dir(import_module(self._mirror))
78
78
79 @property
79 @property
80 def __all__(self):
80 def __all__(self):
81 """Ensure __all__ is always defined"""
81 """Ensure __all__ is always defined"""
82 mod = import_module(self._mirror)
82 mod = import_module(self._mirror)
83 try:
83 try:
84 return mod.__all__
84 return mod.__all__
85 except AttributeError:
85 except AttributeError:
86 return [name for name in dir(mod) if not name.startswith('_')]
86 return [name for name in dir(mod) if not name.startswith('_')]
87
87
88 def __getattr__(self, key):
88 def __getattr__(self, key):
89 # Use the equivalent of import_item(name), see below
89 # Use the equivalent of import_item(name), see below
90 name = "%s.%s" % (self._mirror, key)
90 name = "%s.%s" % (self._mirror, key)
91 try:
91 try:
92 return import_item(name)
92 return import_item(name)
93 except ImportError:
93 except ImportError as e:
94 raise AttributeError(key)
94 raise AttributeError(key) from e
@@ -1,160 +1,160 b''
1 """Define text roles for GitHub
1 """Define text roles for GitHub
2
2
3 * ghissue - Issue
3 * ghissue - Issue
4 * ghpull - Pull Request
4 * ghpull - Pull Request
5 * ghuser - User
5 * ghuser - User
6
6
7 Adapted from bitbucket example here:
7 Adapted from bitbucket example here:
8 https://bitbucket.org/birkenfeld/sphinx-contrib/src/tip/bitbucket/sphinxcontrib/bitbucket.py
8 https://bitbucket.org/birkenfeld/sphinx-contrib/src/tip/bitbucket/sphinxcontrib/bitbucket.py
9
9
10 Authors
10 Authors
11 -------
11 -------
12
12
13 * Doug Hellmann
13 * Doug Hellmann
14 * Min RK
14 * Min RK
15 """
15 """
16 #
16 #
17 # Original Copyright (c) 2010 Doug Hellmann. All rights reserved.
17 # Original Copyright (c) 2010 Doug Hellmann. All rights reserved.
18 #
18 #
19
19
20 from docutils import nodes, utils
20 from docutils import nodes, utils
21 from docutils.parsers.rst.roles import set_classes
21 from docutils.parsers.rst.roles import set_classes
22 from sphinx.util.logging import getLogger
22 from sphinx.util.logging import getLogger
23
23
24 info = getLogger(__name__).info
24 info = getLogger(__name__).info
25
25
26 def make_link_node(rawtext, app, type, slug, options):
26 def make_link_node(rawtext, app, type, slug, options):
27 """Create a link to a github resource.
27 """Create a link to a github resource.
28
28
29 :param rawtext: Text being replaced with link node.
29 :param rawtext: Text being replaced with link node.
30 :param app: Sphinx application context
30 :param app: Sphinx application context
31 :param type: Link type (issues, changeset, etc.)
31 :param type: Link type (issues, changeset, etc.)
32 :param slug: ID of the thing to link to
32 :param slug: ID of the thing to link to
33 :param options: Options dictionary passed to role func.
33 :param options: Options dictionary passed to role func.
34 """
34 """
35
35
36 try:
36 try:
37 base = app.config.github_project_url
37 base = app.config.github_project_url
38 if not base:
38 if not base:
39 raise AttributeError
39 raise AttributeError
40 if not base.endswith('/'):
40 if not base.endswith('/'):
41 base += '/'
41 base += '/'
42 except AttributeError as err:
42 except AttributeError as err:
43 raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
43 raise ValueError('github_project_url configuration value is not set (%s)' % str(err)) from err
44
44
45 ref = base + type + '/' + slug + '/'
45 ref = base + type + '/' + slug + '/'
46 set_classes(options)
46 set_classes(options)
47 prefix = "#"
47 prefix = "#"
48 if type == 'pull':
48 if type == 'pull':
49 prefix = "PR " + prefix
49 prefix = "PR " + prefix
50 node = nodes.reference(rawtext, prefix + utils.unescape(slug), refuri=ref,
50 node = nodes.reference(rawtext, prefix + utils.unescape(slug), refuri=ref,
51 **options)
51 **options)
52 return node
52 return node
53
53
54 def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
54 def ghissue_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
55 """Link to a GitHub issue.
55 """Link to a GitHub issue.
56
56
57 Returns 2 part tuple containing list of nodes to insert into the
57 Returns 2 part tuple containing list of nodes to insert into the
58 document and a list of system messages. Both are allowed to be
58 document and a list of system messages. Both are allowed to be
59 empty.
59 empty.
60
60
61 :param name: The role name used in the document.
61 :param name: The role name used in the document.
62 :param rawtext: The entire markup snippet, with role.
62 :param rawtext: The entire markup snippet, with role.
63 :param text: The text marked with the role.
63 :param text: The text marked with the role.
64 :param lineno: The line number where rawtext appears in the input.
64 :param lineno: The line number where rawtext appears in the input.
65 :param inliner: The inliner instance that called us.
65 :param inliner: The inliner instance that called us.
66 :param options: Directive options for customization.
66 :param options: Directive options for customization.
67 :param content: The directive content for customization.
67 :param content: The directive content for customization.
68 """
68 """
69
69
70 try:
70 try:
71 issue_num = int(text)
71 issue_num = int(text)
72 if issue_num <= 0:
72 if issue_num <= 0:
73 raise ValueError
73 raise ValueError
74 except ValueError:
74 except ValueError:
75 msg = inliner.reporter.error(
75 msg = inliner.reporter.error(
76 'GitHub issue number must be a number greater than or equal to 1; '
76 'GitHub issue number must be a number greater than or equal to 1; '
77 '"%s" is invalid.' % text, line=lineno)
77 '"%s" is invalid.' % text, line=lineno)
78 prb = inliner.problematic(rawtext, rawtext, msg)
78 prb = inliner.problematic(rawtext, rawtext, msg)
79 return [prb], [msg]
79 return [prb], [msg]
80 app = inliner.document.settings.env.app
80 app = inliner.document.settings.env.app
81 #info('issue %r' % text)
81 #info('issue %r' % text)
82 if 'pull' in name.lower():
82 if 'pull' in name.lower():
83 category = 'pull'
83 category = 'pull'
84 elif 'issue' in name.lower():
84 elif 'issue' in name.lower():
85 category = 'issues'
85 category = 'issues'
86 else:
86 else:
87 msg = inliner.reporter.error(
87 msg = inliner.reporter.error(
88 'GitHub roles include "ghpull" and "ghissue", '
88 'GitHub roles include "ghpull" and "ghissue", '
89 '"%s" is invalid.' % name, line=lineno)
89 '"%s" is invalid.' % name, line=lineno)
90 prb = inliner.problematic(rawtext, rawtext, msg)
90 prb = inliner.problematic(rawtext, rawtext, msg)
91 return [prb], [msg]
91 return [prb], [msg]
92 node = make_link_node(rawtext, app, category, str(issue_num), options)
92 node = make_link_node(rawtext, app, category, str(issue_num), options)
93 return [node], []
93 return [node], []
94
94
95 def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
95 def ghuser_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
96 """Link to a GitHub user.
96 """Link to a GitHub user.
97
97
98 Returns 2 part tuple containing list of nodes to insert into the
98 Returns 2 part tuple containing list of nodes to insert into the
99 document and a list of system messages. Both are allowed to be
99 document and a list of system messages. Both are allowed to be
100 empty.
100 empty.
101
101
102 :param name: The role name used in the document.
102 :param name: The role name used in the document.
103 :param rawtext: The entire markup snippet, with role.
103 :param rawtext: The entire markup snippet, with role.
104 :param text: The text marked with the role.
104 :param text: The text marked with the role.
105 :param lineno: The line number where rawtext appears in the input.
105 :param lineno: The line number where rawtext appears in the input.
106 :param inliner: The inliner instance that called us.
106 :param inliner: The inliner instance that called us.
107 :param options: Directive options for customization.
107 :param options: Directive options for customization.
108 :param content: The directive content for customization.
108 :param content: The directive content for customization.
109 """
109 """
110 app = inliner.document.settings.env.app
110 app = inliner.document.settings.env.app
111 #info('user link %r' % text)
111 #info('user link %r' % text)
112 ref = 'https://www.github.com/' + text
112 ref = 'https://www.github.com/' + text
113 node = nodes.reference(rawtext, text, refuri=ref, **options)
113 node = nodes.reference(rawtext, text, refuri=ref, **options)
114 return [node], []
114 return [node], []
115
115
116 def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
116 def ghcommit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
117 """Link to a GitHub commit.
117 """Link to a GitHub commit.
118
118
119 Returns 2 part tuple containing list of nodes to insert into the
119 Returns 2 part tuple containing list of nodes to insert into the
120 document and a list of system messages. Both are allowed to be
120 document and a list of system messages. Both are allowed to be
121 empty.
121 empty.
122
122
123 :param name: The role name used in the document.
123 :param name: The role name used in the document.
124 :param rawtext: The entire markup snippet, with role.
124 :param rawtext: The entire markup snippet, with role.
125 :param text: The text marked with the role.
125 :param text: The text marked with the role.
126 :param lineno: The line number where rawtext appears in the input.
126 :param lineno: The line number where rawtext appears in the input.
127 :param inliner: The inliner instance that called us.
127 :param inliner: The inliner instance that called us.
128 :param options: Directive options for customization.
128 :param options: Directive options for customization.
129 :param content: The directive content for customization.
129 :param content: The directive content for customization.
130 """
130 """
131 app = inliner.document.settings.env.app
131 app = inliner.document.settings.env.app
132 #info('user link %r' % text)
132 #info('user link %r' % text)
133 try:
133 try:
134 base = app.config.github_project_url
134 base = app.config.github_project_url
135 if not base:
135 if not base:
136 raise AttributeError
136 raise AttributeError
137 if not base.endswith('/'):
137 if not base.endswith('/'):
138 base += '/'
138 base += '/'
139 except AttributeError as err:
139 except AttributeError as err:
140 raise ValueError('github_project_url configuration value is not set (%s)' % str(err))
140 raise ValueError('github_project_url configuration value is not set (%s)' % str(err)) from err
141
141
142 ref = base + text
142 ref = base + text
143 node = nodes.reference(rawtext, text[:6], refuri=ref, **options)
143 node = nodes.reference(rawtext, text[:6], refuri=ref, **options)
144 return [node], []
144 return [node], []
145
145
146
146
147 def setup(app):
147 def setup(app):
148 """Install the plugin.
148 """Install the plugin.
149
149
150 :param app: Sphinx application context.
150 :param app: Sphinx application context.
151 """
151 """
152 info('Initializing GitHub plugin')
152 info('Initializing GitHub plugin')
153 app.add_role('ghissue', ghissue_role)
153 app.add_role('ghissue', ghissue_role)
154 app.add_role('ghpull', ghissue_role)
154 app.add_role('ghpull', ghissue_role)
155 app.add_role('ghuser', ghuser_role)
155 app.add_role('ghuser', ghuser_role)
156 app.add_role('ghcommit', ghcommit_role)
156 app.add_role('ghcommit', ghcommit_role)
157 app.add_config_value('github_project_url', None, 'env')
157 app.add_config_value('github_project_url', None, 'env')
158
158
159 metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
159 metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
160 return metadata
160 return metadata
@@ -1,303 +1,303 b''
1 """Functions for Github API requests."""
1 """Functions for Github API requests."""
2
2
3 try:
3 try:
4 input = raw_input
4 input = raw_input
5 except NameError:
5 except NameError:
6 pass
6 pass
7
7
8 import os
8 import os
9 import re
9 import re
10 import sys
10 import sys
11
11
12 import requests
12 import requests
13 import getpass
13 import getpass
14 import json
14 import json
15
15
16 try:
16 try:
17 import requests_cache
17 import requests_cache
18 except ImportError:
18 except ImportError:
19 print("cache not available, install `requests_cache` for caching.", file=sys.stderr)
19 print("cache not available, install `requests_cache` for caching.", file=sys.stderr)
20 else:
20 else:
21 requests_cache.install_cache("gh_api", expire_after=3600)
21 requests_cache.install_cache("gh_api", expire_after=3600)
22
22
23 # Keyring stores passwords by a 'username', but we're not storing a username and
23 # Keyring stores passwords by a 'username', but we're not storing a username and
24 # password
24 # password
25 import socket
25 import socket
26 fake_username = 'ipython_tools_%s' % socket.gethostname().replace('.','_').replace('-','_')
26 fake_username = 'ipython_tools_%s' % socket.gethostname().replace('.','_').replace('-','_')
27
27
28 class Obj(dict):
28 class Obj(dict):
29 """Dictionary with attribute access to names."""
29 """Dictionary with attribute access to names."""
30 def __getattr__(self, name):
30 def __getattr__(self, name):
31 try:
31 try:
32 return self[name]
32 return self[name]
33 except KeyError:
33 except KeyError as e:
34 raise AttributeError(name)
34 raise AttributeError(name) from e
35
35
36 def __setattr__(self, name, val):
36 def __setattr__(self, name, val):
37 self[name] = val
37 self[name] = val
38
38
39 token = None
39 token = None
40 def get_auth_token():
40 def get_auth_token():
41 global token
41 global token
42
42
43 if token is not None:
43 if token is not None:
44 return token
44 return token
45
45
46 import keyring
46 import keyring
47 token = keyring.get_password('github', fake_username)
47 token = keyring.get_password('github', fake_username)
48 if token is not None:
48 if token is not None:
49 return token
49 return token
50
50
51 print("Please enter your github username and password. These are not "
51 print("Please enter your github username and password. These are not "
52 "stored, only used to get an oAuth token. You can revoke this at "
52 "stored, only used to get an oAuth token. You can revoke this at "
53 "any time on Github.\n"
53 "any time on Github.\n"
54 "Username: ", file=sys.stderr, end='')
54 "Username: ", file=sys.stderr, end='')
55 user = input('')
55 user = input('')
56 pw = getpass.getpass("Password: ", stream=sys.stderr)
56 pw = getpass.getpass("Password: ", stream=sys.stderr)
57
57
58 auth_request = {
58 auth_request = {
59 "scopes": [
59 "scopes": [
60 "public_repo",
60 "public_repo",
61 "gist"
61 "gist"
62 ],
62 ],
63 "note": "IPython tools %s" % socket.gethostname(),
63 "note": "IPython tools %s" % socket.gethostname(),
64 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
64 "note_url": "https://github.com/ipython/ipython/tree/master/tools",
65 }
65 }
66 response = requests.post('https://api.github.com/authorizations',
66 response = requests.post('https://api.github.com/authorizations',
67 auth=(user, pw), data=json.dumps(auth_request))
67 auth=(user, pw), data=json.dumps(auth_request))
68 if response.status_code == 401 and \
68 if response.status_code == 401 and \
69 'required;' in response.headers.get('X-GitHub-OTP', ''):
69 'required;' in response.headers.get('X-GitHub-OTP', ''):
70 print("Your login API requested a one time password", file=sys.stderr)
70 print("Your login API requested a one time password", file=sys.stderr)
71 otp = getpass.getpass("One Time Password: ", stream=sys.stderr)
71 otp = getpass.getpass("One Time Password: ", stream=sys.stderr)
72 response = requests.post('https://api.github.com/authorizations',
72 response = requests.post('https://api.github.com/authorizations',
73 auth=(user, pw),
73 auth=(user, pw),
74 data=json.dumps(auth_request),
74 data=json.dumps(auth_request),
75 headers={'X-GitHub-OTP':otp})
75 headers={'X-GitHub-OTP':otp})
76 response.raise_for_status()
76 response.raise_for_status()
77 token = json.loads(response.text)['token']
77 token = json.loads(response.text)['token']
78 keyring.set_password('github', fake_username, token)
78 keyring.set_password('github', fake_username, token)
79 return token
79 return token
80
80
81 def make_auth_header():
81 def make_auth_header():
82 return {'Authorization': 'token ' + get_auth_token()}
82 return {'Authorization': 'token ' + get_auth_token()}
83
83
84 def post_issue_comment(project, num, body):
84 def post_issue_comment(project, num, body):
85 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
85 url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
86 payload = json.dumps({'body': body})
86 payload = json.dumps({'body': body})
87 requests.post(url, data=payload, headers=make_auth_header())
87 requests.post(url, data=payload, headers=make_auth_header())
88
88
89 def post_gist(content, description='', filename='file', auth=False):
89 def post_gist(content, description='', filename='file', auth=False):
90 """Post some text to a Gist, and return the URL."""
90 """Post some text to a Gist, and return the URL."""
91 post_data = json.dumps({
91 post_data = json.dumps({
92 "description": description,
92 "description": description,
93 "public": True,
93 "public": True,
94 "files": {
94 "files": {
95 filename: {
95 filename: {
96 "content": content
96 "content": content
97 }
97 }
98 }
98 }
99 }).encode('utf-8')
99 }).encode('utf-8')
100
100
101 headers = make_auth_header() if auth else {}
101 headers = make_auth_header() if auth else {}
102 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
102 response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
103 response.raise_for_status()
103 response.raise_for_status()
104 response_data = json.loads(response.text)
104 response_data = json.loads(response.text)
105 return response_data['html_url']
105 return response_data['html_url']
106
106
107 def get_pull_request(project, num, auth=False):
107 def get_pull_request(project, num, auth=False):
108 """get pull request info by number
108 """get pull request info by number
109 """
109 """
110 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
110 url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
111 if auth:
111 if auth:
112 header = make_auth_header()
112 header = make_auth_header()
113 else:
113 else:
114 header = None
114 header = None
115 print("fetching %s" % url, file=sys.stderr)
115 print("fetching %s" % url, file=sys.stderr)
116 response = requests.get(url, headers=header)
116 response = requests.get(url, headers=header)
117 response.raise_for_status()
117 response.raise_for_status()
118 return json.loads(response.text, object_hook=Obj)
118 return json.loads(response.text, object_hook=Obj)
119
119
120 def get_pull_request_files(project, num, auth=False):
120 def get_pull_request_files(project, num, auth=False):
121 """get list of files in a pull request"""
121 """get list of files in a pull request"""
122 url = "https://api.github.com/repos/{project}/pulls/{num}/files".format(project=project, num=num)
122 url = "https://api.github.com/repos/{project}/pulls/{num}/files".format(project=project, num=num)
123 if auth:
123 if auth:
124 header = make_auth_header()
124 header = make_auth_header()
125 else:
125 else:
126 header = None
126 header = None
127 return get_paged_request(url, headers=header)
127 return get_paged_request(url, headers=header)
128
128
129 element_pat = re.compile(r'<(.+?)>')
129 element_pat = re.compile(r'<(.+?)>')
130 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
130 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
131
131
132 def get_paged_request(url, headers=None, **params):
132 def get_paged_request(url, headers=None, **params):
133 """get a full list, handling APIv3's paging"""
133 """get a full list, handling APIv3's paging"""
134 results = []
134 results = []
135 params.setdefault("per_page", 100)
135 params.setdefault("per_page", 100)
136 while True:
136 while True:
137 if '?' in url:
137 if '?' in url:
138 params = None
138 params = None
139 print("fetching %s" % url, file=sys.stderr)
139 print("fetching %s" % url, file=sys.stderr)
140 else:
140 else:
141 print("fetching %s with %s" % (url, params), file=sys.stderr)
141 print("fetching %s with %s" % (url, params), file=sys.stderr)
142 response = requests.get(url, headers=headers, params=params)
142 response = requests.get(url, headers=headers, params=params)
143 response.raise_for_status()
143 response.raise_for_status()
144 results.extend(response.json())
144 results.extend(response.json())
145 if 'next' in response.links:
145 if 'next' in response.links:
146 url = response.links['next']['url']
146 url = response.links['next']['url']
147 else:
147 else:
148 break
148 break
149 return results
149 return results
150
150
151 def get_pulls_list(project, auth=False, **params):
151 def get_pulls_list(project, auth=False, **params):
152 """get pull request list"""
152 """get pull request list"""
153 params.setdefault("state", "closed")
153 params.setdefault("state", "closed")
154 url = "https://api.github.com/repos/{project}/pulls".format(project=project)
154 url = "https://api.github.com/repos/{project}/pulls".format(project=project)
155 if auth:
155 if auth:
156 headers = make_auth_header()
156 headers = make_auth_header()
157 else:
157 else:
158 headers = None
158 headers = None
159 pages = get_paged_request(url, headers=headers, **params)
159 pages = get_paged_request(url, headers=headers, **params)
160 return pages
160 return pages
161
161
162 def get_issues_list(project, auth=False, **params):
162 def get_issues_list(project, auth=False, **params):
163 """get issues list"""
163 """get issues list"""
164 params.setdefault("state", "closed")
164 params.setdefault("state", "closed")
165 url = "https://api.github.com/repos/{project}/issues".format(project=project)
165 url = "https://api.github.com/repos/{project}/issues".format(project=project)
166 if auth:
166 if auth:
167 headers = make_auth_header()
167 headers = make_auth_header()
168 else:
168 else:
169 headers = None
169 headers = None
170 pages = get_paged_request(url, headers=headers, **params)
170 pages = get_paged_request(url, headers=headers, **params)
171 return pages
171 return pages
172
172
173 def get_milestones(project, auth=False, **params):
173 def get_milestones(project, auth=False, **params):
174 params.setdefault('state', 'all')
174 params.setdefault('state', 'all')
175 url = "https://api.github.com/repos/{project}/milestones".format(project=project)
175 url = "https://api.github.com/repos/{project}/milestones".format(project=project)
176 if auth:
176 if auth:
177 headers = make_auth_header()
177 headers = make_auth_header()
178 else:
178 else:
179 headers = None
179 headers = None
180 milestones = get_paged_request(url, headers=headers, **params)
180 milestones = get_paged_request(url, headers=headers, **params)
181 return milestones
181 return milestones
182
182
183 def get_milestone_id(project, milestone, auth=False, **params):
183 def get_milestone_id(project, milestone, auth=False, **params):
184 milestones = get_milestones(project, auth=auth, **params)
184 milestones = get_milestones(project, auth=auth, **params)
185 for mstone in milestones:
185 for mstone in milestones:
186 if mstone['title'] == milestone:
186 if mstone['title'] == milestone:
187 return mstone['number']
187 return mstone['number']
188 else:
188 else:
189 raise ValueError("milestone %s not found" % milestone)
189 raise ValueError("milestone %s not found" % milestone)
190
190
191 def is_pull_request(issue):
191 def is_pull_request(issue):
192 """Return True if the given issue is a pull request."""
192 """Return True if the given issue is a pull request."""
193 return bool(issue.get('pull_request', {}).get('html_url', None))
193 return bool(issue.get('pull_request', {}).get('html_url', None))
194
194
195 def get_authors(pr):
195 def get_authors(pr):
196 print("getting authors for #%i" % pr['number'], file=sys.stderr)
196 print("getting authors for #%i" % pr['number'], file=sys.stderr)
197 h = make_auth_header()
197 h = make_auth_header()
198 r = requests.get(pr['commits_url'], headers=h)
198 r = requests.get(pr['commits_url'], headers=h)
199 r.raise_for_status()
199 r.raise_for_status()
200 commits = r.json()
200 commits = r.json()
201 authors = []
201 authors = []
202 for commit in commits:
202 for commit in commits:
203 author = commit['commit']['author']
203 author = commit['commit']['author']
204 authors.append("%s <%s>" % (author['name'], author['email']))
204 authors.append("%s <%s>" % (author['name'], author['email']))
205 return authors
205 return authors
206
206
207 # encode_multipart_formdata is from urllib3.filepost
207 # encode_multipart_formdata is from urllib3.filepost
208 # The only change is to iter_fields, to enforce S3's required key ordering
208 # The only change is to iter_fields, to enforce S3's required key ordering
209
209
210 def iter_fields(fields):
210 def iter_fields(fields):
211 fields = fields.copy()
211 fields = fields.copy()
212 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
212 for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
213 'Policy', 'Signature', 'Content-Type', 'file'):
213 'Policy', 'Signature', 'Content-Type', 'file'):
214 yield (key, fields.pop(key))
214 yield (key, fields.pop(key))
215 for (k,v) in fields.items():
215 for (k,v) in fields.items():
216 yield k,v
216 yield k,v
217
217
218 def encode_multipart_formdata(fields, boundary=None):
218 def encode_multipart_formdata(fields, boundary=None):
219 """
219 """
220 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
220 Encode a dictionary of ``fields`` using the multipart/form-data mime format.
221
221
222 :param fields:
222 :param fields:
223 Dictionary of fields or list of (key, value) field tuples. The key is
223 Dictionary of fields or list of (key, value) field tuples. The key is
224 treated as the field name, and the value as the body of the form-data
224 treated as the field name, and the value as the body of the form-data
225 bytes. If the value is a tuple of two elements, then the first element
225 bytes. If the value is a tuple of two elements, then the first element
226 is treated as the filename of the form-data section.
226 is treated as the filename of the form-data section.
227
227
228 Field names and filenames must be unicode.
228 Field names and filenames must be unicode.
229
229
230 :param boundary:
230 :param boundary:
231 If not specified, then a random boundary will be generated using
231 If not specified, then a random boundary will be generated using
232 :func:`mimetools.choose_boundary`.
232 :func:`mimetools.choose_boundary`.
233 """
233 """
234 # copy requests imports in here:
234 # copy requests imports in here:
235 from io import BytesIO
235 from io import BytesIO
236 from requests.packages.urllib3.filepost import (
236 from requests.packages.urllib3.filepost import (
237 choose_boundary, six, writer, b, get_content_type
237 choose_boundary, six, writer, b, get_content_type
238 )
238 )
239 body = BytesIO()
239 body = BytesIO()
240 if boundary is None:
240 if boundary is None:
241 boundary = choose_boundary()
241 boundary = choose_boundary()
242
242
243 for fieldname, value in iter_fields(fields):
243 for fieldname, value in iter_fields(fields):
244 body.write(b('--%s\r\n' % (boundary)))
244 body.write(b('--%s\r\n' % (boundary)))
245
245
246 if isinstance(value, tuple):
246 if isinstance(value, tuple):
247 filename, data = value
247 filename, data = value
248 writer(body).write('Content-Disposition: form-data; name="%s"; '
248 writer(body).write('Content-Disposition: form-data; name="%s"; '
249 'filename="%s"\r\n' % (fieldname, filename))
249 'filename="%s"\r\n' % (fieldname, filename))
250 body.write(b('Content-Type: %s\r\n\r\n' %
250 body.write(b('Content-Type: %s\r\n\r\n' %
251 (get_content_type(filename))))
251 (get_content_type(filename))))
252 else:
252 else:
253 data = value
253 data = value
254 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
254 writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
255 % (fieldname))
255 % (fieldname))
256 body.write(b'Content-Type: text/plain\r\n\r\n')
256 body.write(b'Content-Type: text/plain\r\n\r\n')
257
257
258 if isinstance(data, int):
258 if isinstance(data, int):
259 data = str(data) # Backwards compatibility
259 data = str(data) # Backwards compatibility
260 if isinstance(data, six.text_type):
260 if isinstance(data, six.text_type):
261 writer(body).write(data)
261 writer(body).write(data)
262 else:
262 else:
263 body.write(data)
263 body.write(data)
264
264
265 body.write(b'\r\n')
265 body.write(b'\r\n')
266
266
267 body.write(b('--%s--\r\n' % (boundary)))
267 body.write(b('--%s--\r\n' % (boundary)))
268
268
269 content_type = b('multipart/form-data; boundary=%s' % boundary)
269 content_type = b('multipart/form-data; boundary=%s' % boundary)
270
270
271 return body.getvalue(), content_type
271 return body.getvalue(), content_type
272
272
273
273
274 def post_download(project, filename, name=None, description=""):
274 def post_download(project, filename, name=None, description=""):
275 """Upload a file to the GitHub downloads area"""
275 """Upload a file to the GitHub downloads area"""
276 if name is None:
276 if name is None:
277 name = os.path.basename(filename)
277 name = os.path.basename(filename)
278 with open(filename, 'rb') as f:
278 with open(filename, 'rb') as f:
279 filedata = f.read()
279 filedata = f.read()
280
280
281 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
281 url = "https://api.github.com/repos/{project}/downloads".format(project=project)
282
282
283 payload = json.dumps(dict(name=name, size=len(filedata),
283 payload = json.dumps(dict(name=name, size=len(filedata),
284 description=description))
284 description=description))
285 response = requests.post(url, data=payload, headers=make_auth_header())
285 response = requests.post(url, data=payload, headers=make_auth_header())
286 response.raise_for_status()
286 response.raise_for_status()
287 reply = json.loads(response.content)
287 reply = json.loads(response.content)
288 s3_url = reply['s3_url']
288 s3_url = reply['s3_url']
289
289
290 fields = dict(
290 fields = dict(
291 key=reply['path'],
291 key=reply['path'],
292 acl=reply['acl'],
292 acl=reply['acl'],
293 success_action_status=201,
293 success_action_status=201,
294 Filename=reply['name'],
294 Filename=reply['name'],
295 AWSAccessKeyId=reply['accesskeyid'],
295 AWSAccessKeyId=reply['accesskeyid'],
296 Policy=reply['policy'],
296 Policy=reply['policy'],
297 Signature=reply['signature'],
297 Signature=reply['signature'],
298 file=(reply['name'], filedata),
298 file=(reply['name'], filedata),
299 )
299 )
300 fields['Content-Type'] = reply['mime_type']
300 fields['Content-Type'] = reply['mime_type']
301 data, content_type = encode_multipart_formdata(fields)
301 data, content_type = encode_multipart_formdata(fields)
302 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
302 s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
303 return s3r
303 return s3r
General Comments 0
You need to be logged in to leave comments. Login now