##// END OF EJS Templates
Add tests for resolving backend name and gui loop
Ian Thomas -
Show More
@@ -1,391 +1,391 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Top-level display functions for displaying object in different formats."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7
8 8 from binascii import b2a_hex
9 9 import os
10 10 import sys
11 11 import warnings
12 12
13 13 __all__ = ['display', 'clear_output', 'publish_display_data', 'update_display', 'DisplayHandle']
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # utility functions
17 17 #-----------------------------------------------------------------------------
18 18
19 19
20 20 def _merge(d1, d2):
21 21 """Like update, but merges sub-dicts instead of clobbering at the top level.
22 22
23 23 Updates d1 in-place
24 24 """
25 25
26 26 if not isinstance(d2, dict) or not isinstance(d1, dict):
27 27 return d2
28 28 for key, value in d2.items():
29 29 d1[key] = _merge(d1.get(key), value)
30 30 return d1
31 31
32 32
33 33 #-----------------------------------------------------------------------------
34 34 # Main functions
35 35 #-----------------------------------------------------------------------------
36 36
37 37 class _Sentinel:
38 38 def __repr__(self):
39 39 return "<deprecated>"
40 40
41 41
42 42 _sentinel = _Sentinel()
43 43
44 44 # use * to indicate transient is keyword-only
45 45 def publish_display_data(
46 46 data, metadata=None, source=_sentinel, *, transient=None, **kwargs
47 47 ):
48 48 """Publish data and metadata to all frontends.
49 49
50 50 See the ``display_data`` message in the messaging documentation for
51 51 more details about this message type.
52 52
53 53 Keys of data and metadata can be any mime-type.
54 54
55 55 Parameters
56 56 ----------
57 57 data : dict
58 58 A dictionary having keys that are valid MIME types (like
59 59 'text/plain' or 'image/svg+xml') and values that are the data for
60 60 that MIME type. The data itself must be a JSON'able data
61 61 structure. Minimally all data should have the 'text/plain' data,
62 62 which can be displayed by all frontends. If more than the plain
63 63 text is given, it is up to the frontend to decide which
64 64 representation to use.
65 65 metadata : dict
66 66 A dictionary for metadata related to the data. This can contain
67 67 arbitrary key, value pairs that frontends can use to interpret
68 68 the data. mime-type keys matching those in data can be used
69 69 to specify metadata about particular representations.
70 70 source : str, deprecated
71 71 Unused.
72 72 transient : dict, keyword-only
73 73 A dictionary of transient data, such as display_id.
74 74 """
75 75 from IPython.core.interactiveshell import InteractiveShell
76 76
77 77 if source is not _sentinel:
78 78 warnings.warn(
79 79 "The `source` parameter emit a deprecation warning since"
80 80 " IPython 8.0, it had no effects for a long time and will "
81 81 " be removed in future versions.",
82 82 DeprecationWarning,
83 83 stacklevel=2,
84 84 )
85 85 display_pub = InteractiveShell.instance().display_pub
86 86
87 87 # only pass transient if supplied,
88 88 # to avoid errors with older ipykernel.
89 89 # TODO: We could check for ipykernel version and provide a detailed upgrade message.
90 90 if transient:
91 91 kwargs['transient'] = transient
92 92
93 93 display_pub.publish(
94 94 data=data,
95 95 metadata=metadata,
96 96 **kwargs
97 97 )
98 98
99 99
100 100 def _new_id():
101 101 """Generate a new random text id with urandom"""
102 102 return b2a_hex(os.urandom(16)).decode('ascii')
103 103
104 104
105 105 def display(
106 106 *objs,
107 107 include=None,
108 108 exclude=None,
109 109 metadata=None,
110 110 transient=None,
111 111 display_id=None,
112 112 raw=False,
113 113 clear=False,
114 **kwargs
114 **kwargs,
115 115 ):
116 116 """Display a Python object in all frontends.
117 117
118 118 By default all representations will be computed and sent to the frontends.
119 119 Frontends can decide which representation is used and how.
120 120
121 121 In terminal IPython this will be similar to using :func:`print`, for use in richer
122 122 frontends see Jupyter notebook examples with rich display logic.
123 123
124 124 Parameters
125 125 ----------
126 126 *objs : object
127 127 The Python objects to display.
128 128 raw : bool, optional
129 129 Are the objects to be displayed already mimetype-keyed dicts of raw display data,
130 130 or Python objects that need to be formatted before display? [default: False]
131 131 include : list, tuple or set, optional
132 132 A list of format type strings (MIME types) to include in the
133 133 format data dict. If this is set *only* the format types included
134 134 in this list will be computed.
135 135 exclude : list, tuple or set, optional
136 136 A list of format type strings (MIME types) to exclude in the format
137 137 data dict. If this is set all format types will be computed,
138 138 except for those included in this argument.
139 139 metadata : dict, optional
140 140 A dictionary of metadata to associate with the output.
141 141 mime-type keys in this dictionary will be associated with the individual
142 142 representation formats, if they exist.
143 143 transient : dict, optional
144 144 A dictionary of transient data to associate with the output.
145 145 Data in this dict should not be persisted to files (e.g. notebooks).
146 146 display_id : str, bool optional
147 147 Set an id for the display.
148 148 This id can be used for updating this display area later via update_display.
149 149 If given as `True`, generate a new `display_id`
150 150 clear : bool, optional
151 151 Should the output area be cleared before displaying anything? If True,
152 152 this will wait for additional output before clearing. [default: False]
153 153 **kwargs : additional keyword-args, optional
154 154 Additional keyword-arguments are passed through to the display publisher.
155 155
156 156 Returns
157 157 -------
158 158 handle: DisplayHandle
159 159 Returns a handle on updatable displays for use with :func:`update_display`,
160 160 if `display_id` is given. Returns :any:`None` if no `display_id` is given
161 161 (default).
162 162
163 163 Examples
164 164 --------
165 165 >>> class Json(object):
166 166 ... def __init__(self, json):
167 167 ... self.json = json
168 168 ... def _repr_pretty_(self, pp, cycle):
169 169 ... import json
170 170 ... pp.text(json.dumps(self.json, indent=2))
171 171 ... def __repr__(self):
172 172 ... return str(self.json)
173 173 ...
174 174
175 175 >>> d = Json({1:2, 3: {4:5}})
176 176
177 177 >>> print(d)
178 178 {1: 2, 3: {4: 5}}
179 179
180 180 >>> display(d)
181 181 {
182 182 "1": 2,
183 183 "3": {
184 184 "4": 5
185 185 }
186 186 }
187 187
188 188 >>> def int_formatter(integer, pp, cycle):
189 189 ... pp.text('I'*integer)
190 190
191 191 >>> plain = get_ipython().display_formatter.formatters['text/plain']
192 192 >>> plain.for_type(int, int_formatter)
193 193 <function _repr_pprint at 0x...>
194 194 >>> display(7-5)
195 195 II
196 196
197 197 >>> del plain.type_printers[int]
198 198 >>> display(7-5)
199 199 2
200 200
201 201 See Also
202 202 --------
203 203 :func:`update_display`
204 204
205 205 Notes
206 206 -----
207 207 In Python, objects can declare their textual representation using the
208 208 `__repr__` method. IPython expands on this idea and allows objects to declare
209 209 other, rich representations including:
210 210
211 211 - HTML
212 212 - JSON
213 213 - PNG
214 214 - JPEG
215 215 - SVG
216 216 - LaTeX
217 217
218 218 A single object can declare some or all of these representations; all are
219 219 handled by IPython's display system.
220 220
221 221 The main idea of the first approach is that you have to implement special
222 222 display methods when you define your class, one for each representation you
223 223 want to use. Here is a list of the names of the special methods and the
224 224 values they must return:
225 225
226 226 - `_repr_html_`: return raw HTML as a string, or a tuple (see below).
227 227 - `_repr_json_`: return a JSONable dict, or a tuple (see below).
228 228 - `_repr_jpeg_`: return raw JPEG data, or a tuple (see below).
229 229 - `_repr_png_`: return raw PNG data, or a tuple (see below).
230 230 - `_repr_svg_`: return raw SVG data as a string, or a tuple (see below).
231 231 - `_repr_latex_`: return LaTeX commands in a string surrounded by "$",
232 232 or a tuple (see below).
233 233 - `_repr_mimebundle_`: return a full mimebundle containing the mapping
234 234 from all mimetypes to data.
235 235 Use this for any mime-type not listed above.
236 236
237 237 The above functions may also return the object's metadata alonside the
238 238 data. If the metadata is available, the functions will return a tuple
239 239 containing the data and metadata, in that order. If there is no metadata
240 240 available, then the functions will return the data only.
241 241
242 242 When you are directly writing your own classes, you can adapt them for
243 243 display in IPython by following the above approach. But in practice, you
244 244 often need to work with existing classes that you can't easily modify.
245 245
246 246 You can refer to the documentation on integrating with the display system in
247 247 order to register custom formatters for already existing types
248 248 (:ref:`integrating_rich_display`).
249 249
250 250 .. versionadded:: 5.4 display available without import
251 251 .. versionadded:: 6.1 display available without import
252 252
253 253 Since IPython 5.4 and 6.1 :func:`display` is automatically made available to
254 254 the user without import. If you are using display in a document that might
255 255 be used in a pure python context or with older version of IPython, use the
256 256 following import at the top of your file::
257 257
258 258 from IPython.display import display
259 259
260 260 """
261 261 from IPython.core.interactiveshell import InteractiveShell
262 262
263 263 if not InteractiveShell.initialized():
264 264 # Directly print objects.
265 265 print(*objs)
266 266 return
267 267
268 268 if transient is None:
269 269 transient = {}
270 270 if metadata is None:
271 271 metadata={}
272 272 if display_id:
273 273 if display_id is True:
274 274 display_id = _new_id()
275 275 transient['display_id'] = display_id
276 276 if kwargs.get('update') and 'display_id' not in transient:
277 277 raise TypeError('display_id required for update_display')
278 278 if transient:
279 279 kwargs['transient'] = transient
280 280
281 281 if not objs and display_id:
282 282 # if given no objects, but still a request for a display_id,
283 283 # we assume the user wants to insert an empty output that
284 284 # can be updated later
285 285 objs = [{}]
286 286 raw = True
287 287
288 288 if not raw:
289 289 format = InteractiveShell.instance().display_formatter.format
290 290
291 291 if clear:
292 292 clear_output(wait=True)
293 293
294 294 for obj in objs:
295 295 if raw:
296 296 publish_display_data(data=obj, metadata=metadata, **kwargs)
297 297 else:
298 298 format_dict, md_dict = format(obj, include=include, exclude=exclude)
299 299 if not format_dict:
300 300 # nothing to display (e.g. _ipython_display_ took over)
301 301 continue
302 302 if metadata:
303 303 # kwarg-specified metadata gets precedence
304 304 _merge(md_dict, metadata)
305 305 publish_display_data(data=format_dict, metadata=md_dict, **kwargs)
306 306 if display_id:
307 307 return DisplayHandle(display_id)
308 308
309 309
310 310 # use * for keyword-only display_id arg
311 311 def update_display(obj, *, display_id, **kwargs):
312 312 """Update an existing display by id
313 313
314 314 Parameters
315 315 ----------
316 316 obj
317 317 The object with which to update the display
318 318 display_id : keyword-only
319 319 The id of the display to update
320 320
321 321 See Also
322 322 --------
323 323 :func:`display`
324 324 """
325 325 kwargs['update'] = True
326 326 display(obj, display_id=display_id, **kwargs)
327 327
328 328
329 329 class DisplayHandle(object):
330 330 """A handle on an updatable display
331 331
332 332 Call `.update(obj)` to display a new object.
333 333
334 334 Call `.display(obj`) to add a new instance of this display,
335 335 and update existing instances.
336 336
337 337 See Also
338 338 --------
339 339
340 340 :func:`display`, :func:`update_display`
341 341
342 342 """
343 343
344 344 def __init__(self, display_id=None):
345 345 if display_id is None:
346 346 display_id = _new_id()
347 347 self.display_id = display_id
348 348
349 349 def __repr__(self):
350 350 return "<%s display_id=%s>" % (self.__class__.__name__, self.display_id)
351 351
352 352 def display(self, obj, **kwargs):
353 353 """Make a new display with my id, updating existing instances.
354 354
355 355 Parameters
356 356 ----------
357 357 obj
358 358 object to display
359 359 **kwargs
360 360 additional keyword arguments passed to display
361 361 """
362 362 display(obj, display_id=self.display_id, **kwargs)
363 363
364 364 def update(self, obj, **kwargs):
365 365 """Update existing displays with my id
366 366
367 367 Parameters
368 368 ----------
369 369 obj
370 370 object to display
371 371 **kwargs
372 372 additional keyword arguments passed to update_display
373 373 """
374 374 update_display(obj, display_id=self.display_id, **kwargs)
375 375
376 376
377 377 def clear_output(wait=False):
378 378 """Clear the output of the current cell receiving output.
379 379
380 380 Parameters
381 381 ----------
382 382 wait : bool [default: false]
383 383 Wait to clear the output until new output is available to replace it."""
384 384 from IPython.core.interactiveshell import InteractiveShell
385 385 if InteractiveShell.initialized():
386 386 InteractiveShell.instance().display_pub.clear_output(wait)
387 387 else:
388 388 print('\033[2K\r', end='')
389 389 sys.stdout.flush()
390 390 print('\033[2K\r', end='')
391 391 sys.stderr.flush()
@@ -1,270 +1,354 b''
1 1 """Tests for pylab tools module.
2 2 """
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7
8 8 from binascii import a2b_base64
9 9 from io import BytesIO
10 10
11 11 import pytest
12 12
13 13 matplotlib = pytest.importorskip("matplotlib")
14 14 matplotlib.use('Agg')
15 15 from matplotlib.figure import Figure
16 16
17 17 from matplotlib import pyplot as plt
18 18 from matplotlib_inline import backend_inline
19 19 import numpy as np
20 20
21 21 from IPython.core.getipython import get_ipython
22 22 from IPython.core.interactiveshell import InteractiveShell
23 23 from IPython.core.display import _PNG, _JPEG
24 24 from .. import pylabtools as pt
25 25
26 26 from IPython.testing import decorators as dec
27 27
28 28
29 29 def test_figure_to_svg():
30 30 # simple empty-figure test
31 31 fig = plt.figure()
32 32 assert pt.print_figure(fig, "svg") is None
33 33
34 34 plt.close('all')
35 35
36 36 # simple check for at least svg-looking output
37 37 fig = plt.figure()
38 38 ax = fig.add_subplot(1,1,1)
39 39 ax.plot([1,2,3])
40 40 plt.draw()
41 41 svg = pt.print_figure(fig, "svg")[:100].lower()
42 42 assert "doctype svg" in svg
43 43
44 44
45 45 def _check_pil_jpeg_bytes():
46 46 """Skip if PIL can't write JPEGs to BytesIO objects"""
47 47 # PIL's JPEG plugin can't write to BytesIO objects
48 48 # Pillow fixes this
49 49 from PIL import Image
50 50 buf = BytesIO()
51 51 img = Image.new("RGB", (4,4))
52 52 try:
53 53 img.save(buf, 'jpeg')
54 54 except Exception as e:
55 55 ename = e.__class__.__name__
56 56 raise pytest.skip("PIL can't write JPEG to BytesIO: %s: %s" % (ename, e)) from e
57 57
58 58 @dec.skip_without("PIL.Image")
59 59 def test_figure_to_jpeg():
60 60 _check_pil_jpeg_bytes()
61 61 # simple check for at least jpeg-looking output
62 62 fig = plt.figure()
63 63 ax = fig.add_subplot(1,1,1)
64 64 ax.plot([1,2,3])
65 65 plt.draw()
66 66 jpeg = pt.print_figure(fig, 'jpeg', pil_kwargs={'optimize': 50})[:100].lower()
67 67 assert jpeg.startswith(_JPEG)
68 68
69 69 def test_retina_figure():
70 70 # simple empty-figure test
71 71 fig = plt.figure()
72 72 assert pt.retina_figure(fig) == None
73 73 plt.close('all')
74 74
75 75 fig = plt.figure()
76 76 ax = fig.add_subplot(1,1,1)
77 77 ax.plot([1,2,3])
78 78 plt.draw()
79 79 png, md = pt.retina_figure(fig)
80 80 assert png.startswith(_PNG)
81 81 assert "width" in md
82 82 assert "height" in md
83 83
84 84
85 85 _fmt_mime_map = {
86 86 'png': 'image/png',
87 87 'jpeg': 'image/jpeg',
88 88 'pdf': 'application/pdf',
89 89 'retina': 'image/png',
90 90 'svg': 'image/svg+xml',
91 91 }
92 92
93 93 def test_select_figure_formats_str():
94 94 ip = get_ipython()
95 95 for fmt, active_mime in _fmt_mime_map.items():
96 96 pt.select_figure_formats(ip, fmt)
97 97 for mime, f in ip.display_formatter.formatters.items():
98 98 if mime == active_mime:
99 99 assert Figure in f
100 100 else:
101 101 assert Figure not in f
102 102
103 103 def test_select_figure_formats_kwargs():
104 104 ip = get_ipython()
105 105 kwargs = dict(bbox_inches="tight")
106 106 pt.select_figure_formats(ip, "png", **kwargs)
107 107 formatter = ip.display_formatter.formatters["image/png"]
108 108 f = formatter.lookup_by_type(Figure)
109 109 cell = f.keywords
110 110 expected = kwargs
111 111 expected["base64"] = True
112 112 expected["fmt"] = "png"
113 113 assert cell == expected
114 114
115 115 # check that the formatter doesn't raise
116 116 fig = plt.figure()
117 117 ax = fig.add_subplot(1,1,1)
118 118 ax.plot([1,2,3])
119 119 plt.draw()
120 120 formatter.enabled = True
121 121 png = formatter(fig)
122 122 assert isinstance(png, str)
123 123 png_bytes = a2b_base64(png)
124 124 assert png_bytes.startswith(_PNG)
125 125
126 126 def test_select_figure_formats_set():
127 127 ip = get_ipython()
128 128 for fmts in [
129 129 {'png', 'svg'},
130 130 ['png'],
131 131 ('jpeg', 'pdf', 'retina'),
132 132 {'svg'},
133 133 ]:
134 134 active_mimes = {_fmt_mime_map[fmt] for fmt in fmts}
135 135 pt.select_figure_formats(ip, fmts)
136 136 for mime, f in ip.display_formatter.formatters.items():
137 137 if mime in active_mimes:
138 138 assert Figure in f
139 139 else:
140 140 assert Figure not in f
141 141
142 142 def test_select_figure_formats_bad():
143 143 ip = get_ipython()
144 144 with pytest.raises(ValueError):
145 145 pt.select_figure_formats(ip, 'foo')
146 146 with pytest.raises(ValueError):
147 147 pt.select_figure_formats(ip, {'png', 'foo'})
148 148 with pytest.raises(ValueError):
149 149 pt.select_figure_formats(ip, ['retina', 'pdf', 'bar', 'bad'])
150 150
151 151 def test_import_pylab():
152 152 ns = {}
153 153 pt.import_pylab(ns, import_all=False)
154 154 assert "plt" in ns
155 155 assert ns["np"] == np
156 156
157 157
158 158 class TestPylabSwitch(object):
159 159 class Shell(InteractiveShell):
160 160 def init_history(self):
161 161 """Sets up the command history, and starts regular autosaves."""
162 162 self.config.HistoryManager.hist_file = ":memory:"
163 163 super().init_history()
164 164
165 165 def enable_gui(self, gui):
166 166 pass
167 167
168 168 def setup(self):
169 169 import matplotlib
170 170 def act_mpl(backend):
171 171 matplotlib.rcParams['backend'] = backend
172 172
173 173 # Save rcParams since they get modified
174 174 self._saved_rcParams = matplotlib.rcParams
175 175 self._saved_rcParamsOrig = matplotlib.rcParamsOrig
176 176 matplotlib.rcParams = dict(backend="QtAgg")
177 177 matplotlib.rcParamsOrig = dict(backend="QtAgg")
178 178
179 179 # Mock out functions
180 180 self._save_am = pt.activate_matplotlib
181 181 pt.activate_matplotlib = act_mpl
182 182 self._save_ip = pt.import_pylab
183 183 pt.import_pylab = lambda *a,**kw:None
184 184 self._save_cis = backend_inline.configure_inline_support
185 185 backend_inline.configure_inline_support = lambda *a, **kw: None
186 186
187 187 def teardown(self):
188 188 pt.activate_matplotlib = self._save_am
189 189 pt.import_pylab = self._save_ip
190 190 backend_inline.configure_inline_support = self._save_cis
191 191 import matplotlib
192 192 matplotlib.rcParams = self._saved_rcParams
193 193 matplotlib.rcParamsOrig = self._saved_rcParamsOrig
194 194
195 195 def test_qt(self):
196 196 s = self.Shell()
197 197 gui, backend = s.enable_matplotlib(None)
198 198 assert gui == "qt"
199 199 assert s.pylab_gui_select == "qt"
200 200
201 201 gui, backend = s.enable_matplotlib("inline")
202 202 assert gui is None
203 203 assert s.pylab_gui_select == "qt"
204 204
205 205 gui, backend = s.enable_matplotlib("qt")
206 206 assert gui == "qt"
207 207 assert s.pylab_gui_select == "qt"
208 208
209 209 gui, backend = s.enable_matplotlib("inline")
210 210 assert gui is None
211 211 assert s.pylab_gui_select == "qt"
212 212
213 213 gui, backend = s.enable_matplotlib()
214 214 assert gui == "qt"
215 215 assert s.pylab_gui_select == "qt"
216 216
217 217 def test_inline(self):
218 218 s = self.Shell()
219 219 gui, backend = s.enable_matplotlib("inline")
220 220 assert gui is None
221 221 assert s.pylab_gui_select == None
222 222
223 223 gui, backend = s.enable_matplotlib("inline")
224 224 assert gui is None
225 225 assert s.pylab_gui_select == None
226 226
227 227 gui, backend = s.enable_matplotlib("qt")
228 228 assert gui == "qt"
229 229 assert s.pylab_gui_select == "qt"
230 230
231 231 def test_inline_twice(self):
232 232 "Using '%matplotlib inline' twice should not reset formatters"
233 233
234 234 ip = self.Shell()
235 235 gui, backend = ip.enable_matplotlib("inline")
236 236 assert gui is None
237 237
238 238 fmts = {'png'}
239 239 active_mimes = {_fmt_mime_map[fmt] for fmt in fmts}
240 240 pt.select_figure_formats(ip, fmts)
241 241
242 242 gui, backend = ip.enable_matplotlib("inline")
243 243 assert gui is None
244 244
245 245 for mime, f in ip.display_formatter.formatters.items():
246 246 if mime in active_mimes:
247 247 assert Figure in f
248 248 else:
249 249 assert Figure not in f
250 250
251 251 def test_qt_gtk(self):
252 252 s = self.Shell()
253 253 gui, backend = s.enable_matplotlib("qt")
254 254 assert gui == "qt"
255 255 assert s.pylab_gui_select == "qt"
256 256
257 257 gui, backend = s.enable_matplotlib("gtk3")
258 258 assert gui == "qt"
259 259 assert s.pylab_gui_select == "qt"
260 260
261 261
262 262 def test_no_gui_backends():
263 263 for k in ['agg', 'svg', 'pdf', 'ps']:
264 264 assert k not in pt.backend2gui
265 265
266 266
267 267 def test_figure_no_canvas():
268 268 fig = Figure()
269 269 fig.canvas = None
270 270 pt.print_figure(fig)
271
272
273 @pytest.mark.parametrize(
274 "name, expected_gui, expected_backend",
275 [
276 # name is gui
277 ("gtk3", "gtk3", "gtk3agg"),
278 ("gtk4", "gtk4", "gtk4agg"),
279 ("headless", "headless", "agg"),
280 ("osx", "osx", "macosx"),
281 ("qt", "qt", "qtagg"),
282 ("qt5", "qt5", "qt5agg"),
283 ("qt6", "qt6", "qt6agg"),
284 ("tk", "tk", "tkagg"),
285 ("wx", "wx", "wxagg"),
286 # name is backend
287 ("agg", None, "agg"),
288 ("cairo", None, "cairo"),
289 ("pdf", None, "pdf"),
290 ("ps", None, "ps"),
291 ("svg", None, "svg"),
292 ("template", None, "template"),
293 ("gtk3agg", "gtk3", "gtk3agg"),
294 ("gtk3cairo", "gtk3", "gtk3cairo"),
295 ("gtk4agg", "gtk4", "gtk4agg"),
296 ("gtk4cairo", "gtk4", "gtk4cairo"),
297 ("macosx", "osx", "macosx"),
298 ("nbagg", "nbagg", "nbagg"),
299 ("notebook", "nbagg", "notebook"),
300 ("qtagg", "qt", "qtagg"),
301 ("qtcairo", "qt", "qtcairo"),
302 ("qt5agg", "qt5", "qt5agg"),
303 ("qt5cairo", "qt5", "qt5cairo"),
304 ("qt6agg", "qt", "qt6agg"),
305 ("qt6cairo", "qt", "qt6cairo"),
306 ("tkagg", "tk", "tkagg"),
307 ("tkcairo", "tk", "tkcairo"),
308 ("webagg", "webagg", "webagg"),
309 ("wxagg", "wx", "wxagg"),
310 ("wxcairo", "wx", "wxcairo"),
311 ],
312 )
313 def test_backend_builtin(name, expected_gui, expected_backend):
314 # Test correct identification of Matplotlib built-in backends without importing and using them,
315 # otherwise we would need to ensure all the complex dependencies such as windowing toolkits are
316 # installed.
317
318 mpl_manages_backends = pt._matplotlib_manages_backends()
319 if not mpl_manages_backends:
320 # Backends not supported before _matplotlib_manages_backends or supported
321 # but with different expected_gui or expected_backend.
322 if (
323 name.endswith("agg")
324 or name.endswith("cairo")
325 or name in ("headless", "macosx", "pdf", "ps", "svg", "template")
326 ):
327 pytest.skip()
328 elif name == "qt6":
329 expected_backend = "qtagg"
330 elif name == "notebook":
331 expected_backend, expected_gui = expected_gui, expected_backend
332
333 gui, backend = pt.find_gui_and_backend(name)
334 if not mpl_manages_backends:
335 gui = gui.lower() if gui else None
336 backend = backend.lower() if backend else None
337 assert gui == expected_gui
338 assert backend == expected_backend
339
340
341 def test_backend_entry_point():
342 gui, backend = pt.find_gui_and_backend("inline")
343 assert gui is None
344 expected_backend = (
345 "inline"
346 if pt._matplotlib_manages_backends()
347 else "module://matplotlib_inline.backend_inline"
348 )
349 assert backend == expected_backend
350
351
352 def test_backend_unknown():
353 with pytest.raises(RuntimeError if pt._matplotlib_manages_backends() else KeyError):
354 pt.find_gui_and_backend("name-does-not-exist")
@@ -1,426 +1,426 b''
1 1 # encoding: utf-8
2 2 """
3 3 An embedded IPython shell.
4 4 """
5 5 # Copyright (c) IPython Development Team.
6 6 # Distributed under the terms of the Modified BSD License.
7 7
8 8
9 9 import sys
10 10 import warnings
11 11
12 12 from IPython.core import ultratb, compilerop
13 13 from IPython.core import magic_arguments
14 14 from IPython.core.magic import Magics, magics_class, line_magic
15 15 from IPython.core.interactiveshell import DummyMod, InteractiveShell
16 16 from IPython.terminal.interactiveshell import TerminalInteractiveShell
17 17 from IPython.terminal.ipapp import load_default_config
18 18
19 19 from traitlets import Bool, CBool, Unicode
20 20 from IPython.utils.io import ask_yes_no
21 21
22 22 from typing import Set
23 23
24 24 class KillEmbedded(Exception):pass
25 25
26 26 # kept for backward compatibility as IPython 6 was released with
27 27 # the typo. See https://github.com/ipython/ipython/pull/10706
28 28 KillEmbeded = KillEmbedded
29 29
30 30 # This is an additional magic that is exposed in embedded shells.
31 31 @magics_class
32 32 class EmbeddedMagics(Magics):
33 33
34 34 @line_magic
35 35 @magic_arguments.magic_arguments()
36 36 @magic_arguments.argument('-i', '--instance', action='store_true',
37 37 help='Kill instance instead of call location')
38 38 @magic_arguments.argument('-x', '--exit', action='store_true',
39 39 help='Also exit the current session')
40 40 @magic_arguments.argument('-y', '--yes', action='store_true',
41 41 help='Do not ask confirmation')
42 42 def kill_embedded(self, parameter_s=''):
43 43 """%kill_embedded : deactivate for good the current embedded IPython
44 44
45 45 This function (after asking for confirmation) sets an internal flag so
46 46 that an embedded IPython will never activate again for the given call
47 47 location. This is useful to permanently disable a shell that is being
48 48 called inside a loop: once you've figured out what you needed from it,
49 49 you may then kill it and the program will then continue to run without
50 50 the interactive shell interfering again.
51 51
52 52 Kill Instance Option:
53 53
54 54 If for some reasons you need to kill the location where the instance
55 55 is created and not called, for example if you create a single
56 56 instance in one place and debug in many locations, you can use the
57 57 ``--instance`` option to kill this specific instance. Like for the
58 58 ``call location`` killing an "instance" should work even if it is
59 59 recreated within a loop.
60 60
61 61 .. note::
62 62
63 63 This was the default behavior before IPython 5.2
64 64
65 65 """
66 66
67 67 args = magic_arguments.parse_argstring(self.kill_embedded, parameter_s)
68 68 print(args)
69 69 if args.instance:
70 70 # let no ask
71 71 if not args.yes:
72 72 kill = ask_yes_no(
73 73 "Are you sure you want to kill this embedded instance? [y/N] ", 'n')
74 74 else:
75 75 kill = True
76 76 if kill:
77 77 self.shell._disable_init_location()
78 78 print("This embedded IPython instance will not reactivate anymore "
79 79 "once you exit.")
80 80 else:
81 81 if not args.yes:
82 82 kill = ask_yes_no(
83 83 "Are you sure you want to kill this embedded call_location? [y/N] ", 'n')
84 84 else:
85 85 kill = True
86 86 if kill:
87 87 self.shell.embedded_active = False
88 88 print("This embedded IPython call location will not reactivate anymore "
89 89 "once you exit.")
90 90
91 91 if args.exit:
92 92 # Ask-exit does not really ask, it just set internals flags to exit
93 93 # on next loop.
94 94 self.shell.ask_exit()
95 95
96 96
97 97 @line_magic
98 98 def exit_raise(self, parameter_s=''):
99 99 """%exit_raise Make the current embedded kernel exit and raise and exception.
100 100
101 101 This function sets an internal flag so that an embedded IPython will
102 102 raise a `IPython.terminal.embed.KillEmbedded` Exception on exit, and then exit the current I. This is
103 103 useful to permanently exit a loop that create IPython embed instance.
104 104 """
105 105
106 106 self.shell.should_raise = True
107 107 self.shell.ask_exit()
108 108
109 109
110 110 class _Sentinel:
111 111 def __init__(self, repr):
112 112 assert isinstance(repr, str)
113 113 self.repr = repr
114 114
115 115 def __repr__(self):
116 116 return repr
117 117
118 118
119 119 class InteractiveShellEmbed(TerminalInteractiveShell):
120 120
121 121 dummy_mode = Bool(False)
122 122 exit_msg = Unicode('')
123 123 embedded = CBool(True)
124 124 should_raise = CBool(False)
125 125 # Like the base class display_banner is not configurable, but here it
126 126 # is True by default.
127 127 display_banner = CBool(True)
128 128 exit_msg = Unicode()
129 129
130 130 # When embedding, by default we don't change the terminal title
131 131 term_title = Bool(False,
132 132 help="Automatically set the terminal title"
133 133 ).tag(config=True)
134 134
135 135 _inactive_locations: Set[str] = set()
136 136
137 137 def _disable_init_location(self):
138 138 """Disable the current Instance creation location"""
139 139 InteractiveShellEmbed._inactive_locations.add(self._init_location_id)
140 140
141 141 @property
142 142 def embedded_active(self):
143 143 return (self._call_location_id not in InteractiveShellEmbed._inactive_locations)\
144 144 and (self._init_location_id not in InteractiveShellEmbed._inactive_locations)
145 145
146 146 @embedded_active.setter
147 147 def embedded_active(self, value):
148 148 if value:
149 149 InteractiveShellEmbed._inactive_locations.discard(
150 150 self._call_location_id)
151 151 InteractiveShellEmbed._inactive_locations.discard(
152 152 self._init_location_id)
153 153 else:
154 154 InteractiveShellEmbed._inactive_locations.add(
155 155 self._call_location_id)
156 156
157 157 def __init__(self, **kw):
158 158 assert (
159 159 "user_global_ns" not in kw
160 160 ), "Key word argument `user_global_ns` has been replaced by `user_module` since IPython 4.0."
161 161 # temporary fix for https://github.com/ipython/ipython/issues/14164
162 162 cls = type(self)
163 163 if cls._instance is None:
164 164 for subclass in cls._walk_mro():
165 165 subclass._instance = self
166 166 cls._instance = self
167 167
168 168 clid = kw.pop('_init_location_id', None)
169 169 if not clid:
170 170 frame = sys._getframe(1)
171 171 clid = '%s:%s' % (frame.f_code.co_filename, frame.f_lineno)
172 172 self._init_location_id = clid
173 173
174 174 super(InteractiveShellEmbed,self).__init__(**kw)
175 175
176 176 # don't use the ipython crash handler so that user exceptions aren't
177 177 # trapped
178 178 sys.excepthook = ultratb.FormattedTB(color_scheme=self.colors,
179 179 mode=self.xmode,
180 180 call_pdb=self.pdb)
181 181
182 182 def init_sys_modules(self):
183 183 """
184 184 Explicitly overwrite :mod:`IPython.core.interactiveshell` to do nothing.
185 185 """
186 186 pass
187 187
188 188 def init_magics(self):
189 189 super(InteractiveShellEmbed, self).init_magics()
190 190 self.register_magics(EmbeddedMagics)
191 191
192 192 def __call__(
193 193 self,
194 194 header="",
195 195 local_ns=None,
196 196 module=None,
197 197 dummy=None,
198 198 stack_depth=1,
199 199 compile_flags=None,
200 **kw
200 **kw,
201 201 ):
202 202 """Activate the interactive interpreter.
203 203
204 204 __call__(self,header='',local_ns=None,module=None,dummy=None) -> Start
205 205 the interpreter shell with the given local and global namespaces, and
206 206 optionally print a header string at startup.
207 207
208 208 The shell can be globally activated/deactivated using the
209 209 dummy_mode attribute. This allows you to turn off a shell used
210 210 for debugging globally.
211 211
212 212 However, *each* time you call the shell you can override the current
213 213 state of dummy_mode with the optional keyword parameter 'dummy'. For
214 214 example, if you set dummy mode on with IPShell.dummy_mode = True, you
215 215 can still have a specific call work by making it as IPShell(dummy=False).
216 216 """
217 217
218 218 # we are called, set the underlying interactiveshell not to exit.
219 219 self.keep_running = True
220 220
221 221 # If the user has turned it off, go away
222 222 clid = kw.pop('_call_location_id', None)
223 223 if not clid:
224 224 frame = sys._getframe(1)
225 225 clid = '%s:%s' % (frame.f_code.co_filename, frame.f_lineno)
226 226 self._call_location_id = clid
227 227
228 228 if not self.embedded_active:
229 229 return
230 230
231 231 # Normal exits from interactive mode set this flag, so the shell can't
232 232 # re-enter (it checks this variable at the start of interactive mode).
233 233 self.exit_now = False
234 234
235 235 # Allow the dummy parameter to override the global __dummy_mode
236 236 if dummy or (dummy != 0 and self.dummy_mode):
237 237 return
238 238
239 239 # self.banner is auto computed
240 240 if header:
241 241 self.old_banner2 = self.banner2
242 242 self.banner2 = self.banner2 + '\n' + header + '\n'
243 243 else:
244 244 self.old_banner2 = ''
245 245
246 246 if self.display_banner:
247 247 self.show_banner()
248 248
249 249 # Call the embedding code with a stack depth of 1 so it can skip over
250 250 # our call and get the original caller's namespaces.
251 251 self.mainloop(
252 252 local_ns, module, stack_depth=stack_depth, compile_flags=compile_flags
253 253 )
254 254
255 255 self.banner2 = self.old_banner2
256 256
257 257 if self.exit_msg is not None:
258 258 print(self.exit_msg)
259 259
260 260 if self.should_raise:
261 261 raise KillEmbedded('Embedded IPython raising error, as user requested.')
262 262
263 263 def mainloop(
264 264 self,
265 265 local_ns=None,
266 266 module=None,
267 267 stack_depth=0,
268 268 compile_flags=None,
269 269 ):
270 270 """Embeds IPython into a running python program.
271 271
272 272 Parameters
273 273 ----------
274 274 local_ns, module
275 275 Working local namespace (a dict) and module (a module or similar
276 276 object). If given as None, they are automatically taken from the scope
277 277 where the shell was called, so that program variables become visible.
278 278 stack_depth : int
279 279 How many levels in the stack to go to looking for namespaces (when
280 280 local_ns or module is None). This allows an intermediate caller to
281 281 make sure that this function gets the namespace from the intended
282 282 level in the stack. By default (0) it will get its locals and globals
283 283 from the immediate caller.
284 284 compile_flags
285 285 A bit field identifying the __future__ features
286 286 that are enabled, as passed to the builtin :func:`compile` function.
287 287 If given as None, they are automatically taken from the scope where
288 288 the shell was called.
289 289
290 290 """
291 291
292 292 # Get locals and globals from caller
293 293 if ((local_ns is None or module is None or compile_flags is None)
294 294 and self.default_user_namespaces):
295 295 call_frame = sys._getframe(stack_depth).f_back
296 296
297 297 if local_ns is None:
298 298 local_ns = call_frame.f_locals
299 299 if module is None:
300 300 global_ns = call_frame.f_globals
301 301 try:
302 302 module = sys.modules[global_ns['__name__']]
303 303 except KeyError:
304 304 warnings.warn("Failed to get module %s" % \
305 305 global_ns.get('__name__', 'unknown module')
306 306 )
307 307 module = DummyMod()
308 308 module.__dict__ = global_ns
309 309 if compile_flags is None:
310 310 compile_flags = (call_frame.f_code.co_flags &
311 311 compilerop.PyCF_MASK)
312 312
313 313 # Save original namespace and module so we can restore them after
314 314 # embedding; otherwise the shell doesn't shut down correctly.
315 315 orig_user_module = self.user_module
316 316 orig_user_ns = self.user_ns
317 317 orig_compile_flags = self.compile.flags
318 318
319 319 # Update namespaces and fire up interpreter
320 320
321 321 # The global one is easy, we can just throw it in
322 322 if module is not None:
323 323 self.user_module = module
324 324
325 325 # But the user/local one is tricky: ipython needs it to store internal
326 326 # data, but we also need the locals. We'll throw our hidden variables
327 327 # like _ih and get_ipython() into the local namespace, but delete them
328 328 # later.
329 329 if local_ns is not None:
330 330 reentrant_local_ns = {k: v for (k, v) in local_ns.items() if k not in self.user_ns_hidden.keys()}
331 331 self.user_ns = reentrant_local_ns
332 332 self.init_user_ns()
333 333
334 334 # Compiler flags
335 335 if compile_flags is not None:
336 336 self.compile.flags = compile_flags
337 337
338 338 # make sure the tab-completer has the correct frame information, so it
339 339 # actually completes using the frame's locals/globals
340 340 self.set_completer_frame()
341 341
342 342 with self.builtin_trap, self.display_trap:
343 343 self.interact()
344 344
345 345 # now, purge out the local namespace of IPython's hidden variables.
346 346 if local_ns is not None:
347 347 local_ns.update({k: v for (k, v) in self.user_ns.items() if k not in self.user_ns_hidden.keys()})
348 348
349 349
350 350 # Restore original namespace so shell can shut down when we exit.
351 351 self.user_module = orig_user_module
352 352 self.user_ns = orig_user_ns
353 353 self.compile.flags = orig_compile_flags
354 354
355 355
356 356 def embed(*, header="", compile_flags=None, **kwargs):
357 357 """Call this to embed IPython at the current point in your program.
358 358
359 359 The first invocation of this will create a :class:`terminal.embed.InteractiveShellEmbed`
360 360 instance and then call it. Consecutive calls just call the already
361 361 created instance.
362 362
363 363 If you don't want the kernel to initialize the namespace
364 364 from the scope of the surrounding function,
365 365 and/or you want to load full IPython configuration,
366 366 you probably want `IPython.start_ipython()` instead.
367 367
368 368 Here is a simple example::
369 369
370 370 from IPython import embed
371 371 a = 10
372 372 b = 20
373 373 embed(header='First time')
374 374 c = 30
375 375 d = 40
376 376 embed()
377 377
378 378 Parameters
379 379 ----------
380 380
381 381 header : str
382 382 Optional header string to print at startup.
383 383 compile_flags
384 384 Passed to the `compile_flags` parameter of :py:meth:`terminal.embed.InteractiveShellEmbed.mainloop()`,
385 385 which is called when the :class:`terminal.embed.InteractiveShellEmbed` instance is called.
386 386 **kwargs : various, optional
387 387 Any other kwargs will be passed to the :class:`terminal.embed.InteractiveShellEmbed` constructor.
388 388 Full customization can be done by passing a traitlets :class:`Config` in as the
389 389 `config` argument (see :ref:`configure_start_ipython` and :ref:`terminal_options`).
390 390 """
391 391 config = kwargs.get('config')
392 392 if config is None:
393 393 config = load_default_config()
394 394 config.InteractiveShellEmbed = config.TerminalInteractiveShell
395 395 kwargs['config'] = config
396 396 using = kwargs.get('using', 'sync')
397 397 if using :
398 398 kwargs['config'].update({'TerminalInteractiveShell':{'loop_runner':using, 'colors':'NoColor', 'autoawait': using!='sync'}})
399 399 #save ps1/ps2 if defined
400 400 ps1 = None
401 401 ps2 = None
402 402 try:
403 403 ps1 = sys.ps1
404 404 ps2 = sys.ps2
405 405 except AttributeError:
406 406 pass
407 407 #save previous instance
408 408 saved_shell_instance = InteractiveShell._instance
409 409 if saved_shell_instance is not None:
410 410 cls = type(saved_shell_instance)
411 411 cls.clear_instance()
412 412 frame = sys._getframe(1)
413 413 shell = InteractiveShellEmbed.instance(_init_location_id='%s:%s' % (
414 414 frame.f_code.co_filename, frame.f_lineno), **kwargs)
415 415 shell(header=header, stack_depth=2, compile_flags=compile_flags,
416 416 _call_location_id='%s:%s' % (frame.f_code.co_filename, frame.f_lineno))
417 417 InteractiveShellEmbed.clear_instance()
418 418 #restore previous instance
419 419 if saved_shell_instance is not None:
420 420 cls = type(saved_shell_instance)
421 421 cls.clear_instance()
422 422 for subclass in cls._walk_mro():
423 423 subclass._instance = saved_shell_instance
424 424 if ps1 is not None:
425 425 sys.ps1 = ps1
426 426 sys.ps2 = ps2
General Comments 0
You need to be logged in to leave comments. Login now