##// END OF EJS Templates
Merge branch 'ipython:master' into master
asteppke -
r27455:34efb114 merge
parent child Browse files
Show More
@@ -0,0 +1,6 b''
1 # Security Policy
2
3 ## Reporting a Vulnerability
4
5 All IPython and Jupyter security are handled via security@ipython.org.
6 You can find more informations on the Jupyter website. https://jupyter.org/security
@@ -1,76 +1,80 b''
1 name: Run tests
1 name: Run tests
2
2
3 on:
3 on:
4 push:
4 push:
5 branches:
6 - main
7 - master
8 - '*.x'
5 pull_request:
9 pull_request:
6 # Run weekly on Monday at 1:23 UTC
10 # Run weekly on Monday at 1:23 UTC
7 schedule:
11 schedule:
8 - cron: '23 1 * * 1'
12 - cron: '23 1 * * 1'
9 workflow_dispatch:
13 workflow_dispatch:
10
14
11
15
12 jobs:
16 jobs:
13 test:
17 test:
14 runs-on: ${{ matrix.os }}
18 runs-on: ${{ matrix.os }}
15 strategy:
19 strategy:
16 matrix:
20 matrix:
17 os: [ubuntu-latest, windows-latest]
21 os: [ubuntu-latest, windows-latest]
18 python-version: ["3.8", "3.9", "3.10"]
22 python-version: ["3.8", "3.9", "3.10"]
19 deps: [test_extra]
23 deps: [test_extra]
20 # Test all on ubuntu, test ends on macos
24 # Test all on ubuntu, test ends on macos
21 include:
25 include:
22 - os: macos-latest
26 - os: macos-latest
23 python-version: "3.8"
27 python-version: "3.8"
24 deps: test_extra
28 deps: test_extra
25 - os: macos-latest
29 - os: macos-latest
26 python-version: "3.10"
30 python-version: "3.10"
27 deps: test_extra
31 deps: test_extra
28 # Tests minimal dependencies set
32 # Tests minimal dependencies set
29 - os: ubuntu-latest
33 - os: ubuntu-latest
30 python-version: "3.10"
34 python-version: "3.10"
31 deps: test
35 deps: test
32 # Tests latest development Python version
36 # Tests latest development Python version
33 - os: ubuntu-latest
37 - os: ubuntu-latest
34 python-version: "3.11-dev"
38 python-version: "3.11-dev"
35 deps: test
39 deps: test
36 # Installing optional dependencies stuff takes ages on PyPy
40 # Installing optional dependencies stuff takes ages on PyPy
37 - os: ubuntu-latest
41 - os: ubuntu-latest
38 python-version: "pypy-3.8"
42 python-version: "pypy-3.8"
39 deps: test
43 deps: test
40 - os: windows-latest
44 - os: windows-latest
41 python-version: "pypy-3.8"
45 python-version: "pypy-3.8"
42 deps: test
46 deps: test
43 - os: macos-latest
47 - os: macos-latest
44 python-version: "pypy-3.8"
48 python-version: "pypy-3.8"
45 deps: test
49 deps: test
46
50
47 steps:
51 steps:
48 - uses: actions/checkout@v2
52 - uses: actions/checkout@v2
49 - name: Set up Python ${{ matrix.python-version }}
53 - name: Set up Python ${{ matrix.python-version }}
50 uses: actions/setup-python@v2
54 uses: actions/setup-python@v2
51 with:
55 with:
52 python-version: ${{ matrix.python-version }}
56 python-version: ${{ matrix.python-version }}
53 cache: pip
57 cache: pip
54 - name: Install latex
58 - name: Install latex
55 if: runner.os == 'Linux' && matrix.deps == 'test_extra'
59 if: runner.os == 'Linux' && matrix.deps == 'test_extra'
56 run: echo "disable latex for now, issues in mirros" #sudo apt-get -yq -o Acquire::Retries=3 --no-install-suggests --no-install-recommends install texlive dvipng
60 run: echo "disable latex for now, issues in mirros" #sudo apt-get -yq -o Acquire::Retries=3 --no-install-suggests --no-install-recommends install texlive dvipng
57 - name: Install and update Python dependencies
61 - name: Install and update Python dependencies
58 run: |
62 run: |
59 python -m pip install --upgrade pip setuptools wheel build
63 python -m pip install --upgrade pip setuptools wheel build
60 python -m pip install --upgrade -e .[${{ matrix.deps }}]
64 python -m pip install --upgrade -e .[${{ matrix.deps }}]
61 python -m pip install --upgrade check-manifest pytest-cov
65 python -m pip install --upgrade check-manifest pytest-cov
62 - name: Try building with Python build
66 - name: Try building with Python build
63 if: runner.os != 'Windows' # setup.py does not support sdist on Windows
67 if: runner.os != 'Windows' # setup.py does not support sdist on Windows
64 run: |
68 run: |
65 python -m build
69 python -m build
66 shasum -a 256 dist/*
70 shasum -a 256 dist/*
67 - name: Check manifest
71 - name: Check manifest
68 if: runner.os != 'Windows' # setup.py does not support sdist on Windows
72 if: runner.os != 'Windows' # setup.py does not support sdist on Windows
69 run: check-manifest
73 run: check-manifest
70 - name: pytest
74 - name: pytest
71 env:
75 env:
72 COLUMNS: 120
76 COLUMNS: 120
73 run: |
77 run: |
74 pytest --color=yes -raXxs ${{ startsWith(matrix.python-version, 'pypy') && ' ' || '--cov --cov-report=xml' }}
78 pytest --color=yes -raXxs ${{ startsWith(matrix.python-version, 'pypy') && ' ' || '--cov --cov-report=xml' }}
75 - name: Upload coverage to Codecov
79 - name: Upload coverage to Codecov
76 uses: codecov/codecov-action@v2
80 uses: codecov/codecov-action@v2
@@ -1,1272 +1,1272 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 html
9 import html
10 import json
10 import json
11 import mimetypes
11 import mimetypes
12 import os
12 import os
13 import struct
13 import struct
14 import warnings
14 import warnings
15 from copy import deepcopy
15 from copy import deepcopy
16 from os.path import splitext
16 from os.path import splitext
17 from pathlib import Path, PurePath
17 from pathlib import Path, PurePath
18
18
19 from IPython.utils.py3compat import cast_unicode
19 from IPython.utils.py3compat import cast_unicode
20 from IPython.testing.skipdoctest import skip_doctest
20 from IPython.testing.skipdoctest import skip_doctest
21 from . import display_functions
21 from . import display_functions
22
22
23
23
24 __all__ = ['display_pretty', 'display_html', 'display_markdown',
24 __all__ = ['display_pretty', 'display_html', 'display_markdown',
25 'display_svg', 'display_png', 'display_jpeg', 'display_latex', 'display_json',
25 'display_svg', 'display_png', 'display_jpeg', 'display_latex', 'display_json',
26 'display_javascript', 'display_pdf', 'DisplayObject', 'TextDisplayObject',
26 'display_javascript', 'display_pdf', 'DisplayObject', 'TextDisplayObject',
27 'Pretty', 'HTML', 'Markdown', 'Math', 'Latex', 'SVG', 'ProgressBar', 'JSON',
27 'Pretty', 'HTML', 'Markdown', 'Math', 'Latex', 'SVG', 'ProgressBar', 'JSON',
28 'GeoJSON', 'Javascript', 'Image', 'set_matplotlib_formats',
28 'GeoJSON', 'Javascript', 'Image', 'set_matplotlib_formats',
29 'set_matplotlib_close',
29 'set_matplotlib_close',
30 'Video']
30 'Video']
31
31
32 _deprecated_names = ["display", "clear_output", "publish_display_data", "update_display", "DisplayHandle"]
32 _deprecated_names = ["display", "clear_output", "publish_display_data", "update_display", "DisplayHandle"]
33
33
34 __all__ = __all__ + _deprecated_names
34 __all__ = __all__ + _deprecated_names
35
35
36
36
37 # ----- warn to import from IPython.display -----
37 # ----- warn to import from IPython.display -----
38
38
39 from warnings import warn
39 from warnings import warn
40
40
41
41
42 def __getattr__(name):
42 def __getattr__(name):
43 if name in _deprecated_names:
43 if name in _deprecated_names:
44 warn(f"Importing {name} from IPython.core.display is deprecated since IPython 7.14, please import from IPython display", DeprecationWarning, stacklevel=2)
44 warn(f"Importing {name} from IPython.core.display is deprecated since IPython 7.14, please import from IPython display", DeprecationWarning, stacklevel=2)
45 return getattr(display_functions, name)
45 return getattr(display_functions, name)
46
46
47 if name in globals().keys():
47 if name in globals().keys():
48 return globals()[name]
48 return globals()[name]
49 else:
49 else:
50 raise AttributeError(f"module {__name__} has no attribute {name}")
50 raise AttributeError(f"module {__name__} has no attribute {name}")
51
51
52
52
53 #-----------------------------------------------------------------------------
53 #-----------------------------------------------------------------------------
54 # utility functions
54 # utility functions
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56
56
57 def _safe_exists(path):
57 def _safe_exists(path):
58 """Check path, but don't let exceptions raise"""
58 """Check path, but don't let exceptions raise"""
59 try:
59 try:
60 return os.path.exists(path)
60 return os.path.exists(path)
61 except Exception:
61 except Exception:
62 return False
62 return False
63
63
64
64
65 def _display_mimetype(mimetype, objs, raw=False, metadata=None):
65 def _display_mimetype(mimetype, objs, raw=False, metadata=None):
66 """internal implementation of all display_foo methods
66 """internal implementation of all display_foo methods
67
67
68 Parameters
68 Parameters
69 ----------
69 ----------
70 mimetype : str
70 mimetype : str
71 The mimetype to be published (e.g. 'image/png')
71 The mimetype to be published (e.g. 'image/png')
72 *objs : object
72 *objs : object
73 The Python objects to display, or if raw=True raw text data to
73 The Python objects to display, or if raw=True raw text data to
74 display.
74 display.
75 raw : bool
75 raw : bool
76 Are the data objects raw data or Python objects that need to be
76 Are the data objects raw data or Python objects that need to be
77 formatted before display? [default: False]
77 formatted before display? [default: False]
78 metadata : dict (optional)
78 metadata : dict (optional)
79 Metadata to be associated with the specific mimetype output.
79 Metadata to be associated with the specific mimetype output.
80 """
80 """
81 if metadata:
81 if metadata:
82 metadata = {mimetype: metadata}
82 metadata = {mimetype: metadata}
83 if raw:
83 if raw:
84 # turn list of pngdata into list of { 'image/png': pngdata }
84 # turn list of pngdata into list of { 'image/png': pngdata }
85 objs = [ {mimetype: obj} for obj in objs ]
85 objs = [ {mimetype: obj} for obj in objs ]
86 display(*objs, raw=raw, metadata=metadata, include=[mimetype])
86 display_functions.display(*objs, raw=raw, metadata=metadata, include=[mimetype])
87
87
88 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
89 # Main functions
89 # Main functions
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91
91
92
92
93 def display_pretty(*objs, **kwargs):
93 def display_pretty(*objs, **kwargs):
94 """Display the pretty (default) representation of an object.
94 """Display the pretty (default) representation of an object.
95
95
96 Parameters
96 Parameters
97 ----------
97 ----------
98 *objs : object
98 *objs : object
99 The Python objects to display, or if raw=True raw text data to
99 The Python objects to display, or if raw=True raw text data to
100 display.
100 display.
101 raw : bool
101 raw : bool
102 Are the data objects raw data or Python objects that need to be
102 Are the data objects raw data or Python objects that need to be
103 formatted before display? [default: False]
103 formatted before display? [default: False]
104 metadata : dict (optional)
104 metadata : dict (optional)
105 Metadata to be associated with the specific mimetype output.
105 Metadata to be associated with the specific mimetype output.
106 """
106 """
107 _display_mimetype('text/plain', objs, **kwargs)
107 _display_mimetype('text/plain', objs, **kwargs)
108
108
109
109
110 def display_html(*objs, **kwargs):
110 def display_html(*objs, **kwargs):
111 """Display the HTML representation of an object.
111 """Display the HTML representation of an object.
112
112
113 Note: If raw=False and the object does not have a HTML
113 Note: If raw=False and the object does not have a HTML
114 representation, no HTML will be shown.
114 representation, no HTML will be shown.
115
115
116 Parameters
116 Parameters
117 ----------
117 ----------
118 *objs : object
118 *objs : object
119 The Python objects to display, or if raw=True raw HTML data to
119 The Python objects to display, or if raw=True raw HTML data to
120 display.
120 display.
121 raw : bool
121 raw : bool
122 Are the data objects raw data or Python objects that need to be
122 Are the data objects raw data or Python objects that need to be
123 formatted before display? [default: False]
123 formatted before display? [default: False]
124 metadata : dict (optional)
124 metadata : dict (optional)
125 Metadata to be associated with the specific mimetype output.
125 Metadata to be associated with the specific mimetype output.
126 """
126 """
127 _display_mimetype('text/html', objs, **kwargs)
127 _display_mimetype('text/html', objs, **kwargs)
128
128
129
129
130 def display_markdown(*objs, **kwargs):
130 def display_markdown(*objs, **kwargs):
131 """Displays the Markdown representation of an object.
131 """Displays the Markdown representation of an object.
132
132
133 Parameters
133 Parameters
134 ----------
134 ----------
135 *objs : object
135 *objs : object
136 The Python objects to display, or if raw=True raw markdown data to
136 The Python objects to display, or if raw=True raw markdown data to
137 display.
137 display.
138 raw : bool
138 raw : bool
139 Are the data objects raw data or Python objects that need to be
139 Are the data objects raw data or Python objects that need to be
140 formatted before display? [default: False]
140 formatted before display? [default: False]
141 metadata : dict (optional)
141 metadata : dict (optional)
142 Metadata to be associated with the specific mimetype output.
142 Metadata to be associated with the specific mimetype output.
143 """
143 """
144
144
145 _display_mimetype('text/markdown', objs, **kwargs)
145 _display_mimetype('text/markdown', objs, **kwargs)
146
146
147
147
148 def display_svg(*objs, **kwargs):
148 def display_svg(*objs, **kwargs):
149 """Display the SVG representation of an object.
149 """Display the SVG representation of an object.
150
150
151 Parameters
151 Parameters
152 ----------
152 ----------
153 *objs : object
153 *objs : object
154 The Python objects to display, or if raw=True raw svg data to
154 The Python objects to display, or if raw=True raw svg data to
155 display.
155 display.
156 raw : bool
156 raw : bool
157 Are the data objects raw data or Python objects that need to be
157 Are the data objects raw data or Python objects that need to be
158 formatted before display? [default: False]
158 formatted before display? [default: False]
159 metadata : dict (optional)
159 metadata : dict (optional)
160 Metadata to be associated with the specific mimetype output.
160 Metadata to be associated with the specific mimetype output.
161 """
161 """
162 _display_mimetype('image/svg+xml', objs, **kwargs)
162 _display_mimetype('image/svg+xml', objs, **kwargs)
163
163
164
164
165 def display_png(*objs, **kwargs):
165 def display_png(*objs, **kwargs):
166 """Display the PNG representation of an object.
166 """Display the PNG representation of an object.
167
167
168 Parameters
168 Parameters
169 ----------
169 ----------
170 *objs : object
170 *objs : object
171 The Python objects to display, or if raw=True raw png data to
171 The Python objects to display, or if raw=True raw png data to
172 display.
172 display.
173 raw : bool
173 raw : bool
174 Are the data objects raw data or Python objects that need to be
174 Are the data objects raw data or Python objects that need to be
175 formatted before display? [default: False]
175 formatted before display? [default: False]
176 metadata : dict (optional)
176 metadata : dict (optional)
177 Metadata to be associated with the specific mimetype output.
177 Metadata to be associated with the specific mimetype output.
178 """
178 """
179 _display_mimetype('image/png', objs, **kwargs)
179 _display_mimetype('image/png', objs, **kwargs)
180
180
181
181
182 def display_jpeg(*objs, **kwargs):
182 def display_jpeg(*objs, **kwargs):
183 """Display the JPEG representation of an object.
183 """Display the JPEG representation of an object.
184
184
185 Parameters
185 Parameters
186 ----------
186 ----------
187 *objs : object
187 *objs : object
188 The Python objects to display, or if raw=True raw JPEG data to
188 The Python objects to display, or if raw=True raw JPEG data to
189 display.
189 display.
190 raw : bool
190 raw : bool
191 Are the data objects raw data or Python objects that need to be
191 Are the data objects raw data or Python objects that need to be
192 formatted before display? [default: False]
192 formatted before display? [default: False]
193 metadata : dict (optional)
193 metadata : dict (optional)
194 Metadata to be associated with the specific mimetype output.
194 Metadata to be associated with the specific mimetype output.
195 """
195 """
196 _display_mimetype('image/jpeg', objs, **kwargs)
196 _display_mimetype('image/jpeg', objs, **kwargs)
197
197
198
198
199 def display_latex(*objs, **kwargs):
199 def display_latex(*objs, **kwargs):
200 """Display the LaTeX representation of an object.
200 """Display the LaTeX representation of an object.
201
201
202 Parameters
202 Parameters
203 ----------
203 ----------
204 *objs : object
204 *objs : object
205 The Python objects to display, or if raw=True raw latex data to
205 The Python objects to display, or if raw=True raw latex data to
206 display.
206 display.
207 raw : bool
207 raw : bool
208 Are the data objects raw data or Python objects that need to be
208 Are the data objects raw data or Python objects that need to be
209 formatted before display? [default: False]
209 formatted before display? [default: False]
210 metadata : dict (optional)
210 metadata : dict (optional)
211 Metadata to be associated with the specific mimetype output.
211 Metadata to be associated with the specific mimetype output.
212 """
212 """
213 _display_mimetype('text/latex', objs, **kwargs)
213 _display_mimetype('text/latex', objs, **kwargs)
214
214
215
215
216 def display_json(*objs, **kwargs):
216 def display_json(*objs, **kwargs):
217 """Display the JSON representation of an object.
217 """Display the JSON representation of an object.
218
218
219 Note that not many frontends support displaying JSON.
219 Note that not many frontends support displaying JSON.
220
220
221 Parameters
221 Parameters
222 ----------
222 ----------
223 *objs : object
223 *objs : object
224 The Python objects to display, or if raw=True raw json data to
224 The Python objects to display, or if raw=True raw json data to
225 display.
225 display.
226 raw : bool
226 raw : bool
227 Are the data objects raw data or Python objects that need to be
227 Are the data objects raw data or Python objects that need to be
228 formatted before display? [default: False]
228 formatted before display? [default: False]
229 metadata : dict (optional)
229 metadata : dict (optional)
230 Metadata to be associated with the specific mimetype output.
230 Metadata to be associated with the specific mimetype output.
231 """
231 """
232 _display_mimetype('application/json', objs, **kwargs)
232 _display_mimetype('application/json', objs, **kwargs)
233
233
234
234
235 def display_javascript(*objs, **kwargs):
235 def display_javascript(*objs, **kwargs):
236 """Display the Javascript representation of an object.
236 """Display the Javascript representation of an object.
237
237
238 Parameters
238 Parameters
239 ----------
239 ----------
240 *objs : object
240 *objs : object
241 The Python objects to display, or if raw=True raw javascript data to
241 The Python objects to display, or if raw=True raw javascript data to
242 display.
242 display.
243 raw : bool
243 raw : bool
244 Are the data objects raw data or Python objects that need to be
244 Are the data objects raw data or Python objects that need to be
245 formatted before display? [default: False]
245 formatted before display? [default: False]
246 metadata : dict (optional)
246 metadata : dict (optional)
247 Metadata to be associated with the specific mimetype output.
247 Metadata to be associated with the specific mimetype output.
248 """
248 """
249 _display_mimetype('application/javascript', objs, **kwargs)
249 _display_mimetype('application/javascript', objs, **kwargs)
250
250
251
251
252 def display_pdf(*objs, **kwargs):
252 def display_pdf(*objs, **kwargs):
253 """Display the PDF representation of an object.
253 """Display the PDF representation of an object.
254
254
255 Parameters
255 Parameters
256 ----------
256 ----------
257 *objs : object
257 *objs : object
258 The Python objects to display, or if raw=True raw javascript data to
258 The Python objects to display, or if raw=True raw javascript data to
259 display.
259 display.
260 raw : bool
260 raw : bool
261 Are the data objects raw data or Python objects that need to be
261 Are the data objects raw data or Python objects that need to be
262 formatted before display? [default: False]
262 formatted before display? [default: False]
263 metadata : dict (optional)
263 metadata : dict (optional)
264 Metadata to be associated with the specific mimetype output.
264 Metadata to be associated with the specific mimetype output.
265 """
265 """
266 _display_mimetype('application/pdf', objs, **kwargs)
266 _display_mimetype('application/pdf', objs, **kwargs)
267
267
268
268
269 #-----------------------------------------------------------------------------
269 #-----------------------------------------------------------------------------
270 # Smart classes
270 # Smart classes
271 #-----------------------------------------------------------------------------
271 #-----------------------------------------------------------------------------
272
272
273
273
274 class DisplayObject(object):
274 class DisplayObject(object):
275 """An object that wraps data to be displayed."""
275 """An object that wraps data to be displayed."""
276
276
277 _read_flags = 'r'
277 _read_flags = 'r'
278 _show_mem_addr = False
278 _show_mem_addr = False
279 metadata = None
279 metadata = None
280
280
281 def __init__(self, data=None, url=None, filename=None, metadata=None):
281 def __init__(self, data=None, url=None, filename=None, metadata=None):
282 """Create a display object given raw data.
282 """Create a display object given raw data.
283
283
284 When this object is returned by an expression or passed to the
284 When this object is returned by an expression or passed to the
285 display function, it will result in the data being displayed
285 display function, it will result in the data being displayed
286 in the frontend. The MIME type of the data should match the
286 in the frontend. The MIME type of the data should match the
287 subclasses used, so the Png subclass should be used for 'image/png'
287 subclasses used, so the Png subclass should be used for 'image/png'
288 data. If the data is a URL, the data will first be downloaded
288 data. If the data is a URL, the data will first be downloaded
289 and then displayed. If
289 and then displayed. If
290
290
291 Parameters
291 Parameters
292 ----------
292 ----------
293 data : unicode, str or bytes
293 data : unicode, str or bytes
294 The raw data or a URL or file to load the data from
294 The raw data or a URL or file to load the data from
295 url : unicode
295 url : unicode
296 A URL to download the data from.
296 A URL to download the data from.
297 filename : unicode
297 filename : unicode
298 Path to a local file to load the data from.
298 Path to a local file to load the data from.
299 metadata : dict
299 metadata : dict
300 Dict of metadata associated to be the object when displayed
300 Dict of metadata associated to be the object when displayed
301 """
301 """
302 if isinstance(data, (Path, PurePath)):
302 if isinstance(data, (Path, PurePath)):
303 data = str(data)
303 data = str(data)
304
304
305 if data is not None and isinstance(data, str):
305 if data is not None and isinstance(data, str):
306 if data.startswith('http') and url is None:
306 if data.startswith('http') and url is None:
307 url = data
307 url = data
308 filename = None
308 filename = None
309 data = None
309 data = None
310 elif _safe_exists(data) and filename is None:
310 elif _safe_exists(data) and filename is None:
311 url = None
311 url = None
312 filename = data
312 filename = data
313 data = None
313 data = None
314
314
315 self.url = url
315 self.url = url
316 self.filename = filename
316 self.filename = filename
317 # because of @data.setter methods in
317 # because of @data.setter methods in
318 # subclasses ensure url and filename are set
318 # subclasses ensure url and filename are set
319 # before assigning to self.data
319 # before assigning to self.data
320 self.data = data
320 self.data = data
321
321
322 if metadata is not None:
322 if metadata is not None:
323 self.metadata = metadata
323 self.metadata = metadata
324 elif self.metadata is None:
324 elif self.metadata is None:
325 self.metadata = {}
325 self.metadata = {}
326
326
327 self.reload()
327 self.reload()
328 self._check_data()
328 self._check_data()
329
329
330 def __repr__(self):
330 def __repr__(self):
331 if not self._show_mem_addr:
331 if not self._show_mem_addr:
332 cls = self.__class__
332 cls = self.__class__
333 r = "<%s.%s object>" % (cls.__module__, cls.__name__)
333 r = "<%s.%s object>" % (cls.__module__, cls.__name__)
334 else:
334 else:
335 r = super(DisplayObject, self).__repr__()
335 r = super(DisplayObject, self).__repr__()
336 return r
336 return r
337
337
338 def _check_data(self):
338 def _check_data(self):
339 """Override in subclasses if there's something to check."""
339 """Override in subclasses if there's something to check."""
340 pass
340 pass
341
341
342 def _data_and_metadata(self):
342 def _data_and_metadata(self):
343 """shortcut for returning metadata with shape information, if defined"""
343 """shortcut for returning metadata with shape information, if defined"""
344 if self.metadata:
344 if self.metadata:
345 return self.data, deepcopy(self.metadata)
345 return self.data, deepcopy(self.metadata)
346 else:
346 else:
347 return self.data
347 return self.data
348
348
349 def reload(self):
349 def reload(self):
350 """Reload the raw data from file or URL."""
350 """Reload the raw data from file or URL."""
351 if self.filename is not None:
351 if self.filename is not None:
352 with open(self.filename, self._read_flags) as f:
352 with open(self.filename, self._read_flags) as f:
353 self.data = f.read()
353 self.data = f.read()
354 elif self.url is not None:
354 elif self.url is not None:
355 # Deferred import
355 # Deferred import
356 from urllib.request import urlopen
356 from urllib.request import urlopen
357 response = urlopen(self.url)
357 response = urlopen(self.url)
358 data = response.read()
358 data = response.read()
359 # extract encoding from header, if there is one:
359 # extract encoding from header, if there is one:
360 encoding = None
360 encoding = None
361 if 'content-type' in response.headers:
361 if 'content-type' in response.headers:
362 for sub in response.headers['content-type'].split(';'):
362 for sub in response.headers['content-type'].split(';'):
363 sub = sub.strip()
363 sub = sub.strip()
364 if sub.startswith('charset'):
364 if sub.startswith('charset'):
365 encoding = sub.split('=')[-1].strip()
365 encoding = sub.split('=')[-1].strip()
366 break
366 break
367 if 'content-encoding' in response.headers:
367 if 'content-encoding' in response.headers:
368 # TODO: do deflate?
368 # TODO: do deflate?
369 if 'gzip' in response.headers['content-encoding']:
369 if 'gzip' in response.headers['content-encoding']:
370 import gzip
370 import gzip
371 from io import BytesIO
371 from io import BytesIO
372 with gzip.open(BytesIO(data), 'rt', encoding=encoding) as fp:
372 with gzip.open(BytesIO(data), 'rt', encoding=encoding) as fp:
373 encoding = None
373 encoding = None
374 data = fp.read()
374 data = fp.read()
375
375
376 # decode data, if an encoding was specified
376 # decode data, if an encoding was specified
377 # We only touch self.data once since
377 # We only touch self.data once since
378 # subclasses such as SVG have @data.setter methods
378 # subclasses such as SVG have @data.setter methods
379 # that transform self.data into ... well svg.
379 # that transform self.data into ... well svg.
380 if encoding:
380 if encoding:
381 self.data = data.decode(encoding, 'replace')
381 self.data = data.decode(encoding, 'replace')
382 else:
382 else:
383 self.data = data
383 self.data = data
384
384
385
385
386 class TextDisplayObject(DisplayObject):
386 class TextDisplayObject(DisplayObject):
387 """Validate that display data is text"""
387 """Validate that display data is text"""
388 def _check_data(self):
388 def _check_data(self):
389 if self.data is not None and not isinstance(self.data, str):
389 if self.data is not None and not isinstance(self.data, str):
390 raise TypeError("%s expects text, not %r" % (self.__class__.__name__, self.data))
390 raise TypeError("%s expects text, not %r" % (self.__class__.__name__, self.data))
391
391
392 class Pretty(TextDisplayObject):
392 class Pretty(TextDisplayObject):
393
393
394 def _repr_pretty_(self, pp, cycle):
394 def _repr_pretty_(self, pp, cycle):
395 return pp.text(self.data)
395 return pp.text(self.data)
396
396
397
397
398 class HTML(TextDisplayObject):
398 class HTML(TextDisplayObject):
399
399
400 def __init__(self, data=None, url=None, filename=None, metadata=None):
400 def __init__(self, data=None, url=None, filename=None, metadata=None):
401 def warn():
401 def warn():
402 if not data:
402 if not data:
403 return False
403 return False
404
404
405 #
405 #
406 # Avoid calling lower() on the entire data, because it could be a
406 # Avoid calling lower() on the entire data, because it could be a
407 # long string and we're only interested in its beginning and end.
407 # long string and we're only interested in its beginning and end.
408 #
408 #
409 prefix = data[:10].lower()
409 prefix = data[:10].lower()
410 suffix = data[-10:].lower()
410 suffix = data[-10:].lower()
411 return prefix.startswith("<iframe ") and suffix.endswith("</iframe>")
411 return prefix.startswith("<iframe ") and suffix.endswith("</iframe>")
412
412
413 if warn():
413 if warn():
414 warnings.warn("Consider using IPython.display.IFrame instead")
414 warnings.warn("Consider using IPython.display.IFrame instead")
415 super(HTML, self).__init__(data=data, url=url, filename=filename, metadata=metadata)
415 super(HTML, self).__init__(data=data, url=url, filename=filename, metadata=metadata)
416
416
417 def _repr_html_(self):
417 def _repr_html_(self):
418 return self._data_and_metadata()
418 return self._data_and_metadata()
419
419
420 def __html__(self):
420 def __html__(self):
421 """
421 """
422 This method exists to inform other HTML-using modules (e.g. Markupsafe,
422 This method exists to inform other HTML-using modules (e.g. Markupsafe,
423 htmltag, etc) that this object is HTML and does not need things like
423 htmltag, etc) that this object is HTML and does not need things like
424 special characters (<>&) escaped.
424 special characters (<>&) escaped.
425 """
425 """
426 return self._repr_html_()
426 return self._repr_html_()
427
427
428
428
429 class Markdown(TextDisplayObject):
429 class Markdown(TextDisplayObject):
430
430
431 def _repr_markdown_(self):
431 def _repr_markdown_(self):
432 return self._data_and_metadata()
432 return self._data_and_metadata()
433
433
434
434
435 class Math(TextDisplayObject):
435 class Math(TextDisplayObject):
436
436
437 def _repr_latex_(self):
437 def _repr_latex_(self):
438 s = r"$\displaystyle %s$" % self.data.strip('$')
438 s = r"$\displaystyle %s$" % self.data.strip('$')
439 if self.metadata:
439 if self.metadata:
440 return s, deepcopy(self.metadata)
440 return s, deepcopy(self.metadata)
441 else:
441 else:
442 return s
442 return s
443
443
444
444
445 class Latex(TextDisplayObject):
445 class Latex(TextDisplayObject):
446
446
447 def _repr_latex_(self):
447 def _repr_latex_(self):
448 return self._data_and_metadata()
448 return self._data_and_metadata()
449
449
450
450
451 class SVG(DisplayObject):
451 class SVG(DisplayObject):
452 """Embed an SVG into the display.
452 """Embed an SVG into the display.
453
453
454 Note if you just want to view a svg image via a URL use `:class:Image` with
454 Note if you just want to view a svg image via a URL use `:class:Image` with
455 a url=URL keyword argument.
455 a url=URL keyword argument.
456 """
456 """
457
457
458 _read_flags = 'rb'
458 _read_flags = 'rb'
459 # wrap data in a property, which extracts the <svg> tag, discarding
459 # wrap data in a property, which extracts the <svg> tag, discarding
460 # document headers
460 # document headers
461 _data = None
461 _data = None
462
462
463 @property
463 @property
464 def data(self):
464 def data(self):
465 return self._data
465 return self._data
466
466
467 @data.setter
467 @data.setter
468 def data(self, svg):
468 def data(self, svg):
469 if svg is None:
469 if svg is None:
470 self._data = None
470 self._data = None
471 return
471 return
472 # parse into dom object
472 # parse into dom object
473 from xml.dom import minidom
473 from xml.dom import minidom
474 x = minidom.parseString(svg)
474 x = minidom.parseString(svg)
475 # get svg tag (should be 1)
475 # get svg tag (should be 1)
476 found_svg = x.getElementsByTagName('svg')
476 found_svg = x.getElementsByTagName('svg')
477 if found_svg:
477 if found_svg:
478 svg = found_svg[0].toxml()
478 svg = found_svg[0].toxml()
479 else:
479 else:
480 # fallback on the input, trust the user
480 # fallback on the input, trust the user
481 # but this is probably an error.
481 # but this is probably an error.
482 pass
482 pass
483 svg = cast_unicode(svg)
483 svg = cast_unicode(svg)
484 self._data = svg
484 self._data = svg
485
485
486 def _repr_svg_(self):
486 def _repr_svg_(self):
487 return self._data_and_metadata()
487 return self._data_and_metadata()
488
488
489 class ProgressBar(DisplayObject):
489 class ProgressBar(DisplayObject):
490 """Progressbar supports displaying a progressbar like element
490 """Progressbar supports displaying a progressbar like element
491 """
491 """
492 def __init__(self, total):
492 def __init__(self, total):
493 """Creates a new progressbar
493 """Creates a new progressbar
494
494
495 Parameters
495 Parameters
496 ----------
496 ----------
497 total : int
497 total : int
498 maximum size of the progressbar
498 maximum size of the progressbar
499 """
499 """
500 self.total = total
500 self.total = total
501 self._progress = 0
501 self._progress = 0
502 self.html_width = '60ex'
502 self.html_width = '60ex'
503 self.text_width = 60
503 self.text_width = 60
504 self._display_id = hexlify(os.urandom(8)).decode('ascii')
504 self._display_id = hexlify(os.urandom(8)).decode('ascii')
505
505
506 def __repr__(self):
506 def __repr__(self):
507 fraction = self.progress / self.total
507 fraction = self.progress / self.total
508 filled = '=' * int(fraction * self.text_width)
508 filled = '=' * int(fraction * self.text_width)
509 rest = ' ' * (self.text_width - len(filled))
509 rest = ' ' * (self.text_width - len(filled))
510 return '[{}{}] {}/{}'.format(
510 return '[{}{}] {}/{}'.format(
511 filled, rest,
511 filled, rest,
512 self.progress, self.total,
512 self.progress, self.total,
513 )
513 )
514
514
515 def _repr_html_(self):
515 def _repr_html_(self):
516 return "<progress style='width:{}' max='{}' value='{}'></progress>".format(
516 return "<progress style='width:{}' max='{}' value='{}'></progress>".format(
517 self.html_width, self.total, self.progress)
517 self.html_width, self.total, self.progress)
518
518
519 def display(self):
519 def display(self):
520 display(self, display_id=self._display_id)
520 display_functions.display(self, display_id=self._display_id)
521
521
522 def update(self):
522 def update(self):
523 display(self, display_id=self._display_id, update=True)
523 display_functions.display(self, display_id=self._display_id, update=True)
524
524
525 @property
525 @property
526 def progress(self):
526 def progress(self):
527 return self._progress
527 return self._progress
528
528
529 @progress.setter
529 @progress.setter
530 def progress(self, value):
530 def progress(self, value):
531 self._progress = value
531 self._progress = value
532 self.update()
532 self.update()
533
533
534 def __iter__(self):
534 def __iter__(self):
535 self.display()
535 self.display()
536 self._progress = -1 # First iteration is 0
536 self._progress = -1 # First iteration is 0
537 return self
537 return self
538
538
539 def __next__(self):
539 def __next__(self):
540 """Returns current value and increments display by one."""
540 """Returns current value and increments display by one."""
541 self.progress += 1
541 self.progress += 1
542 if self.progress < self.total:
542 if self.progress < self.total:
543 return self.progress
543 return self.progress
544 else:
544 else:
545 raise StopIteration()
545 raise StopIteration()
546
546
547 class JSON(DisplayObject):
547 class JSON(DisplayObject):
548 """JSON expects a JSON-able dict or list
548 """JSON expects a JSON-able dict or list
549
549
550 not an already-serialized JSON string.
550 not an already-serialized JSON string.
551
551
552 Scalar types (None, number, string) are not allowed, only dict or list containers.
552 Scalar types (None, number, string) are not allowed, only dict or list containers.
553 """
553 """
554 # wrap data in a property, which warns about passing already-serialized JSON
554 # wrap data in a property, which warns about passing already-serialized JSON
555 _data = None
555 _data = None
556 def __init__(self, data=None, url=None, filename=None, expanded=False, metadata=None, root='root', **kwargs):
556 def __init__(self, data=None, url=None, filename=None, expanded=False, metadata=None, root='root', **kwargs):
557 """Create a JSON display object given raw data.
557 """Create a JSON display object given raw data.
558
558
559 Parameters
559 Parameters
560 ----------
560 ----------
561 data : dict or list
561 data : dict or list
562 JSON data to display. Not an already-serialized JSON string.
562 JSON data to display. Not an already-serialized JSON string.
563 Scalar types (None, number, string) are not allowed, only dict
563 Scalar types (None, number, string) are not allowed, only dict
564 or list containers.
564 or list containers.
565 url : unicode
565 url : unicode
566 A URL to download the data from.
566 A URL to download the data from.
567 filename : unicode
567 filename : unicode
568 Path to a local file to load the data from.
568 Path to a local file to load the data from.
569 expanded : boolean
569 expanded : boolean
570 Metadata to control whether a JSON display component is expanded.
570 Metadata to control whether a JSON display component is expanded.
571 metadata : dict
571 metadata : dict
572 Specify extra metadata to attach to the json display object.
572 Specify extra metadata to attach to the json display object.
573 root : str
573 root : str
574 The name of the root element of the JSON tree
574 The name of the root element of the JSON tree
575 """
575 """
576 self.metadata = {
576 self.metadata = {
577 'expanded': expanded,
577 'expanded': expanded,
578 'root': root,
578 'root': root,
579 }
579 }
580 if metadata:
580 if metadata:
581 self.metadata.update(metadata)
581 self.metadata.update(metadata)
582 if kwargs:
582 if kwargs:
583 self.metadata.update(kwargs)
583 self.metadata.update(kwargs)
584 super(JSON, self).__init__(data=data, url=url, filename=filename)
584 super(JSON, self).__init__(data=data, url=url, filename=filename)
585
585
586 def _check_data(self):
586 def _check_data(self):
587 if self.data is not None and not isinstance(self.data, (dict, list)):
587 if self.data is not None and not isinstance(self.data, (dict, list)):
588 raise TypeError("%s expects JSONable dict or list, not %r" % (self.__class__.__name__, self.data))
588 raise TypeError("%s expects JSONable dict or list, not %r" % (self.__class__.__name__, self.data))
589
589
590 @property
590 @property
591 def data(self):
591 def data(self):
592 return self._data
592 return self._data
593
593
594 @data.setter
594 @data.setter
595 def data(self, data):
595 def data(self, data):
596 if isinstance(data, (Path, PurePath)):
596 if isinstance(data, (Path, PurePath)):
597 data = str(data)
597 data = str(data)
598
598
599 if isinstance(data, str):
599 if isinstance(data, str):
600 if self.filename is None and self.url is None:
600 if self.filename is None and self.url is None:
601 warnings.warn("JSON expects JSONable dict or list, not JSON strings")
601 warnings.warn("JSON expects JSONable dict or list, not JSON strings")
602 data = json.loads(data)
602 data = json.loads(data)
603 self._data = data
603 self._data = data
604
604
605 def _data_and_metadata(self):
605 def _data_and_metadata(self):
606 return self.data, self.metadata
606 return self.data, self.metadata
607
607
608 def _repr_json_(self):
608 def _repr_json_(self):
609 return self._data_and_metadata()
609 return self._data_and_metadata()
610
610
611 _css_t = """var link = document.createElement("link");
611 _css_t = """var link = document.createElement("link");
612 link.ref = "stylesheet";
612 link.ref = "stylesheet";
613 link.type = "text/css";
613 link.type = "text/css";
614 link.href = "%s";
614 link.href = "%s";
615 document.head.appendChild(link);
615 document.head.appendChild(link);
616 """
616 """
617
617
618 _lib_t1 = """new Promise(function(resolve, reject) {
618 _lib_t1 = """new Promise(function(resolve, reject) {
619 var script = document.createElement("script");
619 var script = document.createElement("script");
620 script.onload = resolve;
620 script.onload = resolve;
621 script.onerror = reject;
621 script.onerror = reject;
622 script.src = "%s";
622 script.src = "%s";
623 document.head.appendChild(script);
623 document.head.appendChild(script);
624 }).then(() => {
624 }).then(() => {
625 """
625 """
626
626
627 _lib_t2 = """
627 _lib_t2 = """
628 });"""
628 });"""
629
629
630 class GeoJSON(JSON):
630 class GeoJSON(JSON):
631 """GeoJSON expects JSON-able dict
631 """GeoJSON expects JSON-able dict
632
632
633 not an already-serialized JSON string.
633 not an already-serialized JSON string.
634
634
635 Scalar types (None, number, string) are not allowed, only dict containers.
635 Scalar types (None, number, string) are not allowed, only dict containers.
636 """
636 """
637
637
638 def __init__(self, *args, **kwargs):
638 def __init__(self, *args, **kwargs):
639 """Create a GeoJSON display object given raw data.
639 """Create a GeoJSON display object given raw data.
640
640
641 Parameters
641 Parameters
642 ----------
642 ----------
643 data : dict or list
643 data : dict or list
644 VegaLite data. Not an already-serialized JSON string.
644 VegaLite data. Not an already-serialized JSON string.
645 Scalar types (None, number, string) are not allowed, only dict
645 Scalar types (None, number, string) are not allowed, only dict
646 or list containers.
646 or list containers.
647 url_template : string
647 url_template : string
648 Leaflet TileLayer URL template: http://leafletjs.com/reference.html#url-template
648 Leaflet TileLayer URL template: http://leafletjs.com/reference.html#url-template
649 layer_options : dict
649 layer_options : dict
650 Leaflet TileLayer options: http://leafletjs.com/reference.html#tilelayer-options
650 Leaflet TileLayer options: http://leafletjs.com/reference.html#tilelayer-options
651 url : unicode
651 url : unicode
652 A URL to download the data from.
652 A URL to download the data from.
653 filename : unicode
653 filename : unicode
654 Path to a local file to load the data from.
654 Path to a local file to load the data from.
655 metadata : dict
655 metadata : dict
656 Specify extra metadata to attach to the json display object.
656 Specify extra metadata to attach to the json display object.
657
657
658 Examples
658 Examples
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_functions.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__(
804 def __init__(
805 self,
805 self,
806 data=None,
806 data=None,
807 url=None,
807 url=None,
808 filename=None,
808 filename=None,
809 format=None,
809 format=None,
810 embed=None,
810 embed=None,
811 width=None,
811 width=None,
812 height=None,
812 height=None,
813 retina=False,
813 retina=False,
814 unconfined=False,
814 unconfined=False,
815 metadata=None,
815 metadata=None,
816 alt=None,
816 alt=None,
817 ):
817 ):
818 """Create a PNG/JPEG/GIF image object given raw data.
818 """Create a PNG/JPEG/GIF image object given raw data.
819
819
820 When this object is returned by an input cell or passed to the
820 When this object is returned by an input cell or passed to the
821 display function, it will result in the image being displayed
821 display function, it will result in the image being displayed
822 in the frontend.
822 in the frontend.
823
823
824 Parameters
824 Parameters
825 ----------
825 ----------
826 data : unicode, str or bytes
826 data : unicode, str or bytes
827 The raw image data or a URL or filename to load the data from.
827 The raw image data or a URL or filename to load the data from.
828 This always results in embedded image data.
828 This always results in embedded image data.
829
829
830 url : unicode
830 url : unicode
831 A URL to download the data from. If you specify `url=`,
831 A URL to download the data from. If you specify `url=`,
832 the image data will not be embedded unless you also specify `embed=True`.
832 the image data will not be embedded unless you also specify `embed=True`.
833
833
834 filename : unicode
834 filename : unicode
835 Path to a local file to load the data from.
835 Path to a local file to load the data from.
836 Images from a file are always embedded.
836 Images from a file are always embedded.
837
837
838 format : unicode
838 format : unicode
839 The format of the image data (png/jpeg/jpg/gif). If a filename or URL is given
839 The format of the image data (png/jpeg/jpg/gif). If a filename or URL is given
840 for format will be inferred from the filename extension.
840 for format will be inferred from the filename extension.
841
841
842 embed : bool
842 embed : bool
843 Should the image data be embedded using a data URI (True) or be
843 Should the image data be embedded using a data URI (True) or be
844 loaded using an <img> tag. Set this to True if you want the image
844 loaded using an <img> tag. Set this to True if you want the image
845 to be viewable later with no internet connection in the notebook.
845 to be viewable later with no internet connection in the notebook.
846
846
847 Default is `True`, unless the keyword argument `url` is set, then
847 Default is `True`, unless the keyword argument `url` is set, then
848 default value is `False`.
848 default value is `False`.
849
849
850 Note that QtConsole is not able to display images if `embed` is set to `False`
850 Note that QtConsole is not able to display images if `embed` is set to `False`
851
851
852 width : int
852 width : int
853 Width in pixels to which to constrain the image in html
853 Width in pixels to which to constrain the image in html
854
854
855 height : int
855 height : int
856 Height in pixels to which to constrain the image in html
856 Height in pixels to which to constrain the image in html
857
857
858 retina : bool
858 retina : bool
859 Automatically set the width and height to half of the measured
859 Automatically set the width and height to half of the measured
860 width and height.
860 width and height.
861 This only works for embedded images because it reads the width/height
861 This only works for embedded images because it reads the width/height
862 from image data.
862 from image data.
863 For non-embedded images, you can just set the desired display width
863 For non-embedded images, you can just set the desired display width
864 and height directly.
864 and height directly.
865
865
866 unconfined : bool
866 unconfined : bool
867 Set unconfined=True to disable max-width confinement of the image.
867 Set unconfined=True to disable max-width confinement of the image.
868
868
869 metadata : dict
869 metadata : dict
870 Specify extra metadata to attach to the image.
870 Specify extra metadata to attach to the image.
871
871
872 alt : unicode
872 alt : unicode
873 Alternative text for the image, for use by screen readers.
873 Alternative text for the image, for use by screen readers.
874
874
875 Examples
875 Examples
876 --------
876 --------
877 embedded image data, works in qtconsole and notebook
877 embedded image data, works in qtconsole and notebook
878 when passed positionally, the first arg can be any of raw image data,
878 when passed positionally, the first arg can be any of raw image data,
879 a URL, or a filename from which to load image data.
879 a URL, or a filename from which to load image data.
880 The result is always embedding image data for inline images.
880 The result is always embedding image data for inline images.
881
881
882 >>> Image('http://www.google.fr/images/srpr/logo3w.png')
882 >>> Image('http://www.google.fr/images/srpr/logo3w.png')
883 <IPython.core.display.Image object>
883 <IPython.core.display.Image object>
884
884
885 >>> Image('/path/to/image.jpg')
885 >>> Image('/path/to/image.jpg')
886 <IPython.core.display.Image object>
886 <IPython.core.display.Image object>
887
887
888 >>> Image(b'RAW_PNG_DATA...')
888 >>> Image(b'RAW_PNG_DATA...')
889 <IPython.core.display.Image object>
889 <IPython.core.display.Image object>
890
890
891 Specifying Image(url=...) does not embed the image data,
891 Specifying Image(url=...) does not embed the image data,
892 it only generates ``<img>`` tag with a link to the source.
892 it only generates ``<img>`` tag with a link to the source.
893 This will not work in the qtconsole or offline.
893 This will not work in the qtconsole or offline.
894
894
895 >>> Image(url='http://www.google.fr/images/srpr/logo3w.png')
895 >>> Image(url='http://www.google.fr/images/srpr/logo3w.png')
896 <IPython.core.display.Image object>
896 <IPython.core.display.Image object>
897
897
898 """
898 """
899 if isinstance(data, (Path, PurePath)):
899 if isinstance(data, (Path, PurePath)):
900 data = str(data)
900 data = str(data)
901
901
902 if filename is not None:
902 if filename is not None:
903 ext = self._find_ext(filename)
903 ext = self._find_ext(filename)
904 elif url is not None:
904 elif url is not None:
905 ext = self._find_ext(url)
905 ext = self._find_ext(url)
906 elif data is None:
906 elif data is None:
907 raise ValueError("No image data found. Expecting filename, url, or data.")
907 raise ValueError("No image data found. Expecting filename, url, or data.")
908 elif isinstance(data, str) and (
908 elif isinstance(data, str) and (
909 data.startswith('http') or _safe_exists(data)
909 data.startswith('http') or _safe_exists(data)
910 ):
910 ):
911 ext = self._find_ext(data)
911 ext = self._find_ext(data)
912 else:
912 else:
913 ext = None
913 ext = None
914
914
915 if format is None:
915 if format is None:
916 if ext is not None:
916 if ext is not None:
917 if ext == u'jpg' or ext == u'jpeg':
917 if ext == u'jpg' or ext == u'jpeg':
918 format = self._FMT_JPEG
918 format = self._FMT_JPEG
919 elif ext == u'png':
919 elif ext == u'png':
920 format = self._FMT_PNG
920 format = self._FMT_PNG
921 elif ext == u'gif':
921 elif ext == u'gif':
922 format = self._FMT_GIF
922 format = self._FMT_GIF
923 else:
923 else:
924 format = ext.lower()
924 format = ext.lower()
925 elif isinstance(data, bytes):
925 elif isinstance(data, bytes):
926 # infer image type from image data header,
926 # infer image type from image data header,
927 # only if format has not been specified.
927 # only if format has not been specified.
928 if data[:2] == _JPEG:
928 if data[:2] == _JPEG:
929 format = self._FMT_JPEG
929 format = self._FMT_JPEG
930
930
931 # failed to detect format, default png
931 # failed to detect format, default png
932 if format is None:
932 if format is None:
933 format = self._FMT_PNG
933 format = self._FMT_PNG
934
934
935 if format.lower() == 'jpg':
935 if format.lower() == 'jpg':
936 # jpg->jpeg
936 # jpg->jpeg
937 format = self._FMT_JPEG
937 format = self._FMT_JPEG
938
938
939 self.format = format.lower()
939 self.format = format.lower()
940 self.embed = embed if embed is not None else (url is None)
940 self.embed = embed if embed is not None else (url is None)
941
941
942 if self.embed and self.format not in self._ACCEPTABLE_EMBEDDINGS:
942 if self.embed and self.format not in self._ACCEPTABLE_EMBEDDINGS:
943 raise ValueError("Cannot embed the '%s' image format" % (self.format))
943 raise ValueError("Cannot embed the '%s' image format" % (self.format))
944 if self.embed:
944 if self.embed:
945 self._mimetype = self._MIMETYPES.get(self.format)
945 self._mimetype = self._MIMETYPES.get(self.format)
946
946
947 self.width = width
947 self.width = width
948 self.height = height
948 self.height = height
949 self.retina = retina
949 self.retina = retina
950 self.unconfined = unconfined
950 self.unconfined = unconfined
951 self.alt = alt
951 self.alt = alt
952 super(Image, self).__init__(data=data, url=url, filename=filename,
952 super(Image, self).__init__(data=data, url=url, filename=filename,
953 metadata=metadata)
953 metadata=metadata)
954
954
955 if self.width is None and self.metadata.get('width', {}):
955 if self.width is None and self.metadata.get('width', {}):
956 self.width = metadata['width']
956 self.width = metadata['width']
957
957
958 if self.height is None and self.metadata.get('height', {}):
958 if self.height is None and self.metadata.get('height', {}):
959 self.height = metadata['height']
959 self.height = metadata['height']
960
960
961 if self.alt is None and self.metadata.get("alt", {}):
961 if self.alt is None and self.metadata.get("alt", {}):
962 self.alt = metadata["alt"]
962 self.alt = metadata["alt"]
963
963
964 if retina:
964 if retina:
965 self._retina_shape()
965 self._retina_shape()
966
966
967
967
968 def _retina_shape(self):
968 def _retina_shape(self):
969 """load pixel-doubled width and height from image data"""
969 """load pixel-doubled width and height from image data"""
970 if not self.embed:
970 if not self.embed:
971 return
971 return
972 if self.format == self._FMT_PNG:
972 if self.format == self._FMT_PNG:
973 w, h = _pngxy(self.data)
973 w, h = _pngxy(self.data)
974 elif self.format == self._FMT_JPEG:
974 elif self.format == self._FMT_JPEG:
975 w, h = _jpegxy(self.data)
975 w, h = _jpegxy(self.data)
976 elif self.format == self._FMT_GIF:
976 elif self.format == self._FMT_GIF:
977 w, h = _gifxy(self.data)
977 w, h = _gifxy(self.data)
978 else:
978 else:
979 # retina only supports png
979 # retina only supports png
980 return
980 return
981 self.width = w // 2
981 self.width = w // 2
982 self.height = h // 2
982 self.height = h // 2
983
983
984 def reload(self):
984 def reload(self):
985 """Reload the raw data from file or URL."""
985 """Reload the raw data from file or URL."""
986 if self.embed:
986 if self.embed:
987 super(Image,self).reload()
987 super(Image,self).reload()
988 if self.retina:
988 if self.retina:
989 self._retina_shape()
989 self._retina_shape()
990
990
991 def _repr_html_(self):
991 def _repr_html_(self):
992 if not self.embed:
992 if not self.embed:
993 width = height = klass = alt = ""
993 width = height = klass = alt = ""
994 if self.width:
994 if self.width:
995 width = ' width="%d"' % self.width
995 width = ' width="%d"' % self.width
996 if self.height:
996 if self.height:
997 height = ' height="%d"' % self.height
997 height = ' height="%d"' % self.height
998 if self.unconfined:
998 if self.unconfined:
999 klass = ' class="unconfined"'
999 klass = ' class="unconfined"'
1000 if self.alt:
1000 if self.alt:
1001 alt = ' alt="%s"' % html.escape(self.alt)
1001 alt = ' alt="%s"' % html.escape(self.alt)
1002 return '<img src="{url}"{width}{height}{klass}{alt}/>'.format(
1002 return '<img src="{url}"{width}{height}{klass}{alt}/>'.format(
1003 url=self.url,
1003 url=self.url,
1004 width=width,
1004 width=width,
1005 height=height,
1005 height=height,
1006 klass=klass,
1006 klass=klass,
1007 alt=alt,
1007 alt=alt,
1008 )
1008 )
1009
1009
1010 def _repr_mimebundle_(self, include=None, exclude=None):
1010 def _repr_mimebundle_(self, include=None, exclude=None):
1011 """Return the image as a mimebundle
1011 """Return the image as a mimebundle
1012
1012
1013 Any new mimetype support should be implemented here.
1013 Any new mimetype support should be implemented here.
1014 """
1014 """
1015 if self.embed:
1015 if self.embed:
1016 mimetype = self._mimetype
1016 mimetype = self._mimetype
1017 data, metadata = self._data_and_metadata(always_both=True)
1017 data, metadata = self._data_and_metadata(always_both=True)
1018 if metadata:
1018 if metadata:
1019 metadata = {mimetype: metadata}
1019 metadata = {mimetype: metadata}
1020 return {mimetype: data}, metadata
1020 return {mimetype: data}, metadata
1021 else:
1021 else:
1022 return {'text/html': self._repr_html_()}
1022 return {'text/html': self._repr_html_()}
1023
1023
1024 def _data_and_metadata(self, always_both=False):
1024 def _data_and_metadata(self, always_both=False):
1025 """shortcut for returning metadata with shape information, if defined"""
1025 """shortcut for returning metadata with shape information, if defined"""
1026 try:
1026 try:
1027 b64_data = b2a_base64(self.data).decode('ascii')
1027 b64_data = b2a_base64(self.data).decode('ascii')
1028 except TypeError as e:
1028 except TypeError as e:
1029 raise FileNotFoundError(
1029 raise FileNotFoundError(
1030 "No such file or directory: '%s'" % (self.data)) from e
1030 "No such file or directory: '%s'" % (self.data)) from e
1031 md = {}
1031 md = {}
1032 if self.metadata:
1032 if self.metadata:
1033 md.update(self.metadata)
1033 md.update(self.metadata)
1034 if self.width:
1034 if self.width:
1035 md['width'] = self.width
1035 md['width'] = self.width
1036 if self.height:
1036 if self.height:
1037 md['height'] = self.height
1037 md['height'] = self.height
1038 if self.unconfined:
1038 if self.unconfined:
1039 md['unconfined'] = self.unconfined
1039 md['unconfined'] = self.unconfined
1040 if self.alt:
1040 if self.alt:
1041 md["alt"] = self.alt
1041 md["alt"] = self.alt
1042 if md or always_both:
1042 if md or always_both:
1043 return b64_data, md
1043 return b64_data, md
1044 else:
1044 else:
1045 return b64_data
1045 return b64_data
1046
1046
1047 def _repr_png_(self):
1047 def _repr_png_(self):
1048 if self.embed and self.format == self._FMT_PNG:
1048 if self.embed and self.format == self._FMT_PNG:
1049 return self._data_and_metadata()
1049 return self._data_and_metadata()
1050
1050
1051 def _repr_jpeg_(self):
1051 def _repr_jpeg_(self):
1052 if self.embed and self.format == self._FMT_JPEG:
1052 if self.embed and self.format == self._FMT_JPEG:
1053 return self._data_and_metadata()
1053 return self._data_and_metadata()
1054
1054
1055 def _find_ext(self, s):
1055 def _find_ext(self, s):
1056 base, ext = splitext(s)
1056 base, ext = splitext(s)
1057
1057
1058 if not ext:
1058 if not ext:
1059 return base
1059 return base
1060
1060
1061 # `splitext` includes leading period, so we skip it
1061 # `splitext` includes leading period, so we skip it
1062 return ext[1:].lower()
1062 return ext[1:].lower()
1063
1063
1064
1064
1065 class Video(DisplayObject):
1065 class Video(DisplayObject):
1066
1066
1067 def __init__(self, data=None, url=None, filename=None, embed=False,
1067 def __init__(self, data=None, url=None, filename=None, embed=False,
1068 mimetype=None, width=None, height=None, html_attributes="controls"):
1068 mimetype=None, width=None, height=None, html_attributes="controls"):
1069 """Create a video object given raw data or an URL.
1069 """Create a video object given raw data or an URL.
1070
1070
1071 When this object is returned by an input cell or passed to the
1071 When this object is returned by an input cell or passed to the
1072 display function, it will result in the video being displayed
1072 display function, it will result in the video being displayed
1073 in the frontend.
1073 in the frontend.
1074
1074
1075 Parameters
1075 Parameters
1076 ----------
1076 ----------
1077 data : unicode, str or bytes
1077 data : unicode, str or bytes
1078 The raw video data or a URL or filename to load the data from.
1078 The raw video data or a URL or filename to load the data from.
1079 Raw data will require passing ``embed=True``.
1079 Raw data will require passing ``embed=True``.
1080
1080
1081 url : unicode
1081 url : unicode
1082 A URL for the video. If you specify ``url=``,
1082 A URL for the video. If you specify ``url=``,
1083 the image data will not be embedded.
1083 the image data will not be embedded.
1084
1084
1085 filename : unicode
1085 filename : unicode
1086 Path to a local file containing the video.
1086 Path to a local file containing the video.
1087 Will be interpreted as a local URL unless ``embed=True``.
1087 Will be interpreted as a local URL unless ``embed=True``.
1088
1088
1089 embed : bool
1089 embed : bool
1090 Should the video be embedded using a data URI (True) or be
1090 Should the video be embedded using a data URI (True) or be
1091 loaded using a <video> tag (False).
1091 loaded using a <video> tag (False).
1092
1092
1093 Since videos are large, embedding them should be avoided, if possible.
1093 Since videos are large, embedding them should be avoided, if possible.
1094 You must confirm embedding as your intention by passing ``embed=True``.
1094 You must confirm embedding as your intention by passing ``embed=True``.
1095
1095
1096 Local files can be displayed with URLs without embedding the content, via::
1096 Local files can be displayed with URLs without embedding the content, via::
1097
1097
1098 Video('./video.mp4')
1098 Video('./video.mp4')
1099
1099
1100 mimetype : unicode
1100 mimetype : unicode
1101 Specify the mimetype for embedded videos.
1101 Specify the mimetype for embedded videos.
1102 Default will be guessed from file extension, if available.
1102 Default will be guessed from file extension, if available.
1103
1103
1104 width : int
1104 width : int
1105 Width in pixels to which to constrain the video in HTML.
1105 Width in pixels to which to constrain the video in HTML.
1106 If not supplied, defaults to the width of the video.
1106 If not supplied, defaults to the width of the video.
1107
1107
1108 height : int
1108 height : int
1109 Height in pixels to which to constrain the video in html.
1109 Height in pixels to which to constrain the video in html.
1110 If not supplied, defaults to the height of the video.
1110 If not supplied, defaults to the height of the video.
1111
1111
1112 html_attributes : str
1112 html_attributes : str
1113 Attributes for the HTML ``<video>`` block.
1113 Attributes for the HTML ``<video>`` block.
1114 Default: ``"controls"`` to get video controls.
1114 Default: ``"controls"`` to get video controls.
1115 Other examples: ``"controls muted"`` for muted video with controls,
1115 Other examples: ``"controls muted"`` for muted video with controls,
1116 ``"loop autoplay"`` for looping autoplaying video without controls.
1116 ``"loop autoplay"`` for looping autoplaying video without controls.
1117
1117
1118 Examples
1118 Examples
1119 --------
1119 --------
1120 ::
1120 ::
1121
1121
1122 Video('https://archive.org/download/Sita_Sings_the_Blues/Sita_Sings_the_Blues_small.mp4')
1122 Video('https://archive.org/download/Sita_Sings_the_Blues/Sita_Sings_the_Blues_small.mp4')
1123 Video('path/to/video.mp4')
1123 Video('path/to/video.mp4')
1124 Video('path/to/video.mp4', embed=True)
1124 Video('path/to/video.mp4', embed=True)
1125 Video('path/to/video.mp4', embed=True, html_attributes="controls muted autoplay")
1125 Video('path/to/video.mp4', embed=True, html_attributes="controls muted autoplay")
1126 Video(b'raw-videodata', embed=True)
1126 Video(b'raw-videodata', embed=True)
1127 """
1127 """
1128 if isinstance(data, (Path, PurePath)):
1128 if isinstance(data, (Path, PurePath)):
1129 data = str(data)
1129 data = str(data)
1130
1130
1131 if url is None and isinstance(data, str) and data.startswith(('http:', 'https:')):
1131 if url is None and isinstance(data, str) and data.startswith(('http:', 'https:')):
1132 url = data
1132 url = data
1133 data = None
1133 data = None
1134 elif data is not None and os.path.exists(data):
1134 elif data is not None and os.path.exists(data):
1135 filename = data
1135 filename = data
1136 data = None
1136 data = None
1137
1137
1138 if data and not embed:
1138 if data and not embed:
1139 msg = ''.join([
1139 msg = ''.join([
1140 "To embed videos, you must pass embed=True ",
1140 "To embed videos, you must pass embed=True ",
1141 "(this may make your notebook files huge)\n",
1141 "(this may make your notebook files huge)\n",
1142 "Consider passing Video(url='...')",
1142 "Consider passing Video(url='...')",
1143 ])
1143 ])
1144 raise ValueError(msg)
1144 raise ValueError(msg)
1145
1145
1146 self.mimetype = mimetype
1146 self.mimetype = mimetype
1147 self.embed = embed
1147 self.embed = embed
1148 self.width = width
1148 self.width = width
1149 self.height = height
1149 self.height = height
1150 self.html_attributes = html_attributes
1150 self.html_attributes = html_attributes
1151 super(Video, self).__init__(data=data, url=url, filename=filename)
1151 super(Video, self).__init__(data=data, url=url, filename=filename)
1152
1152
1153 def _repr_html_(self):
1153 def _repr_html_(self):
1154 width = height = ''
1154 width = height = ''
1155 if self.width:
1155 if self.width:
1156 width = ' width="%d"' % self.width
1156 width = ' width="%d"' % self.width
1157 if self.height:
1157 if self.height:
1158 height = ' height="%d"' % self.height
1158 height = ' height="%d"' % self.height
1159
1159
1160 # External URLs and potentially local files are not embedded into the
1160 # External URLs and potentially local files are not embedded into the
1161 # notebook output.
1161 # notebook output.
1162 if not self.embed:
1162 if not self.embed:
1163 url = self.url if self.url is not None else self.filename
1163 url = self.url if self.url is not None else self.filename
1164 output = """<video src="{0}" {1} {2} {3}>
1164 output = """<video src="{0}" {1} {2} {3}>
1165 Your browser does not support the <code>video</code> element.
1165 Your browser does not support the <code>video</code> element.
1166 </video>""".format(url, self.html_attributes, width, height)
1166 </video>""".format(url, self.html_attributes, width, height)
1167 return output
1167 return output
1168
1168
1169 # Embedded videos are base64-encoded.
1169 # Embedded videos are base64-encoded.
1170 mimetype = self.mimetype
1170 mimetype = self.mimetype
1171 if self.filename is not None:
1171 if self.filename is not None:
1172 if not mimetype:
1172 if not mimetype:
1173 mimetype, _ = mimetypes.guess_type(self.filename)
1173 mimetype, _ = mimetypes.guess_type(self.filename)
1174
1174
1175 with open(self.filename, 'rb') as f:
1175 with open(self.filename, 'rb') as f:
1176 video = f.read()
1176 video = f.read()
1177 else:
1177 else:
1178 video = self.data
1178 video = self.data
1179 if isinstance(video, str):
1179 if isinstance(video, str):
1180 # unicode input is already b64-encoded
1180 # unicode input is already b64-encoded
1181 b64_video = video
1181 b64_video = video
1182 else:
1182 else:
1183 b64_video = b2a_base64(video).decode('ascii').rstrip()
1183 b64_video = b2a_base64(video).decode('ascii').rstrip()
1184
1184
1185 output = """<video {0} {1} {2}>
1185 output = """<video {0} {1} {2}>
1186 <source src="data:{3};base64,{4}" type="{3}">
1186 <source src="data:{3};base64,{4}" type="{3}">
1187 Your browser does not support the video tag.
1187 Your browser does not support the video tag.
1188 </video>""".format(self.html_attributes, width, height, mimetype, b64_video)
1188 </video>""".format(self.html_attributes, width, height, mimetype, b64_video)
1189 return output
1189 return output
1190
1190
1191 def reload(self):
1191 def reload(self):
1192 # TODO
1192 # TODO
1193 pass
1193 pass
1194
1194
1195
1195
1196 @skip_doctest
1196 @skip_doctest
1197 def set_matplotlib_formats(*formats, **kwargs):
1197 def set_matplotlib_formats(*formats, **kwargs):
1198 """
1198 """
1199 .. deprecated:: 7.23
1199 .. deprecated:: 7.23
1200
1200
1201 use `matplotlib_inline.backend_inline.set_matplotlib_formats()`
1201 use `matplotlib_inline.backend_inline.set_matplotlib_formats()`
1202
1202
1203 Select figure formats for the inline backend. Optionally pass quality for JPEG.
1203 Select figure formats for the inline backend. Optionally pass quality for JPEG.
1204
1204
1205 For example, this enables PNG and JPEG output with a JPEG quality of 90%::
1205 For example, this enables PNG and JPEG output with a JPEG quality of 90%::
1206
1206
1207 In [1]: set_matplotlib_formats('png', 'jpeg', quality=90)
1207 In [1]: set_matplotlib_formats('png', 'jpeg', quality=90)
1208
1208
1209 To set this in your config files use the following::
1209 To set this in your config files use the following::
1210
1210
1211 c.InlineBackend.figure_formats = {'png', 'jpeg'}
1211 c.InlineBackend.figure_formats = {'png', 'jpeg'}
1212 c.InlineBackend.print_figure_kwargs.update({'quality' : 90})
1212 c.InlineBackend.print_figure_kwargs.update({'quality' : 90})
1213
1213
1214 Parameters
1214 Parameters
1215 ----------
1215 ----------
1216 *formats : strs
1216 *formats : strs
1217 One or more figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
1217 One or more figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
1218 **kwargs
1218 **kwargs
1219 Keyword args will be relayed to ``figure.canvas.print_figure``.
1219 Keyword args will be relayed to ``figure.canvas.print_figure``.
1220 """
1220 """
1221 warnings.warn(
1221 warnings.warn(
1222 "`set_matplotlib_formats` is deprecated since IPython 7.23, directly "
1222 "`set_matplotlib_formats` is deprecated since IPython 7.23, directly "
1223 "use `matplotlib_inline.backend_inline.set_matplotlib_formats()`",
1223 "use `matplotlib_inline.backend_inline.set_matplotlib_formats()`",
1224 DeprecationWarning,
1224 DeprecationWarning,
1225 stacklevel=2,
1225 stacklevel=2,
1226 )
1226 )
1227
1227
1228 from matplotlib_inline.backend_inline import (
1228 from matplotlib_inline.backend_inline import (
1229 set_matplotlib_formats as set_matplotlib_formats_orig,
1229 set_matplotlib_formats as set_matplotlib_formats_orig,
1230 )
1230 )
1231
1231
1232 set_matplotlib_formats_orig(*formats, **kwargs)
1232 set_matplotlib_formats_orig(*formats, **kwargs)
1233
1233
1234 @skip_doctest
1234 @skip_doctest
1235 def set_matplotlib_close(close=True):
1235 def set_matplotlib_close(close=True):
1236 """
1236 """
1237 .. deprecated:: 7.23
1237 .. deprecated:: 7.23
1238
1238
1239 use `matplotlib_inline.backend_inline.set_matplotlib_close()`
1239 use `matplotlib_inline.backend_inline.set_matplotlib_close()`
1240
1240
1241 Set whether the inline backend closes all figures automatically or not.
1241 Set whether the inline backend closes all figures automatically or not.
1242
1242
1243 By default, the inline backend used in the IPython Notebook will close all
1243 By default, the inline backend used in the IPython Notebook will close all
1244 matplotlib figures automatically after each cell is run. This means that
1244 matplotlib figures automatically after each cell is run. This means that
1245 plots in different cells won't interfere. Sometimes, you may want to make
1245 plots in different cells won't interfere. Sometimes, you may want to make
1246 a plot in one cell and then refine it in later cells. This can be accomplished
1246 a plot in one cell and then refine it in later cells. This can be accomplished
1247 by::
1247 by::
1248
1248
1249 In [1]: set_matplotlib_close(False)
1249 In [1]: set_matplotlib_close(False)
1250
1250
1251 To set this in your config files use the following::
1251 To set this in your config files use the following::
1252
1252
1253 c.InlineBackend.close_figures = False
1253 c.InlineBackend.close_figures = False
1254
1254
1255 Parameters
1255 Parameters
1256 ----------
1256 ----------
1257 close : bool
1257 close : bool
1258 Should all matplotlib figures be automatically closed after each cell is
1258 Should all matplotlib figures be automatically closed after each cell is
1259 run?
1259 run?
1260 """
1260 """
1261 warnings.warn(
1261 warnings.warn(
1262 "`set_matplotlib_close` is deprecated since IPython 7.23, directly "
1262 "`set_matplotlib_close` is deprecated since IPython 7.23, directly "
1263 "use `matplotlib_inline.backend_inline.set_matplotlib_close()`",
1263 "use `matplotlib_inline.backend_inline.set_matplotlib_close()`",
1264 DeprecationWarning,
1264 DeprecationWarning,
1265 stacklevel=2,
1265 stacklevel=2,
1266 )
1266 )
1267
1267
1268 from matplotlib_inline.backend_inline import (
1268 from matplotlib_inline.backend_inline import (
1269 set_matplotlib_close as set_matplotlib_close_orig,
1269 set_matplotlib_close as set_matplotlib_close_orig,
1270 )
1270 )
1271
1271
1272 set_matplotlib_close_orig(close)
1272 set_matplotlib_close_orig(close)
@@ -1,796 +1,796 b''
1 """Input transformer machinery to support IPython special syntax.
1 """Input transformer machinery to support IPython special syntax.
2
2
3 This includes the machinery to recognise and transform ``%magic`` commands,
3 This includes the machinery to recognise and transform ``%magic`` commands,
4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
5
5
6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
7 deprecated in 7.0.
7 deprecated in 7.0.
8 """
8 """
9
9
10 # Copyright (c) IPython Development Team.
10 # Copyright (c) IPython Development Team.
11 # Distributed under the terms of the Modified BSD License.
11 # Distributed under the terms of the Modified BSD License.
12
12
13 import ast
13 import ast
14 import sys
14 import sys
15 from codeop import CommandCompiler, Compile
15 from codeop import CommandCompiler, Compile
16 import re
16 import re
17 import tokenize
17 import tokenize
18 from typing import List, Tuple, Optional, Any
18 from typing import List, Tuple, Optional, Any
19 import warnings
19 import warnings
20
20
21 _indent_re = re.compile(r'^[ \t]+')
21 _indent_re = re.compile(r'^[ \t]+')
22
22
23 def leading_empty_lines(lines):
23 def leading_empty_lines(lines):
24 """Remove leading empty lines
24 """Remove leading empty lines
25
25
26 If the leading lines are empty or contain only whitespace, they will be
26 If the leading lines are empty or contain only whitespace, they will be
27 removed.
27 removed.
28 """
28 """
29 if not lines:
29 if not lines:
30 return lines
30 return lines
31 for i, line in enumerate(lines):
31 for i, line in enumerate(lines):
32 if line and not line.isspace():
32 if line and not line.isspace():
33 return lines[i:]
33 return lines[i:]
34 return lines
34 return lines
35
35
36 def leading_indent(lines):
36 def leading_indent(lines):
37 """Remove leading indentation.
37 """Remove leading indentation.
38
38
39 If the first line starts with a spaces or tabs, the same whitespace will be
39 If the first line starts with a spaces or tabs, the same whitespace will be
40 removed from each following line in the cell.
40 removed from each following line in the cell.
41 """
41 """
42 if not lines:
42 if not lines:
43 return lines
43 return lines
44 m = _indent_re.match(lines[0])
44 m = _indent_re.match(lines[0])
45 if not m:
45 if not m:
46 return lines
46 return lines
47 space = m.group(0)
47 space = m.group(0)
48 n = len(space)
48 n = len(space)
49 return [l[n:] if l.startswith(space) else l
49 return [l[n:] if l.startswith(space) else l
50 for l in lines]
50 for l in lines]
51
51
52 class PromptStripper:
52 class PromptStripper:
53 """Remove matching input prompts from a block of input.
53 """Remove matching input prompts from a block of input.
54
54
55 Parameters
55 Parameters
56 ----------
56 ----------
57 prompt_re : regular expression
57 prompt_re : regular expression
58 A regular expression matching any input prompt (including continuation,
58 A regular expression matching any input prompt (including continuation,
59 e.g. ``...``)
59 e.g. ``...``)
60 initial_re : regular expression, optional
60 initial_re : regular expression, optional
61 A regular expression matching only the initial prompt, but not continuation.
61 A regular expression matching only the initial prompt, but not continuation.
62 If no initial expression is given, prompt_re will be used everywhere.
62 If no initial expression is given, prompt_re will be used everywhere.
63 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
63 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
64 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
64 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
65
65
66 Notes
66 Notes
67 -----
67 -----
68
68
69 If initial_re and prompt_re differ,
69 If initial_re and prompt_re differ,
70 only initial_re will be tested against the first line.
70 only initial_re will be tested against the first line.
71 If any prompt is found on the first two lines,
71 If any prompt is found on the first two lines,
72 prompts will be stripped from the rest of the block.
72 prompts will be stripped from the rest of the block.
73 """
73 """
74 def __init__(self, prompt_re, initial_re=None):
74 def __init__(self, prompt_re, initial_re=None):
75 self.prompt_re = prompt_re
75 self.prompt_re = prompt_re
76 self.initial_re = initial_re or prompt_re
76 self.initial_re = initial_re or prompt_re
77
77
78 def _strip(self, lines):
78 def _strip(self, lines):
79 return [self.prompt_re.sub('', l, count=1) for l in lines]
79 return [self.prompt_re.sub('', l, count=1) for l in lines]
80
80
81 def __call__(self, lines):
81 def __call__(self, lines):
82 if not lines:
82 if not lines:
83 return lines
83 return lines
84 if self.initial_re.match(lines[0]) or \
84 if self.initial_re.match(lines[0]) or \
85 (len(lines) > 1 and self.prompt_re.match(lines[1])):
85 (len(lines) > 1 and self.prompt_re.match(lines[1])):
86 return self._strip(lines)
86 return self._strip(lines)
87 return lines
87 return lines
88
88
89 classic_prompt = PromptStripper(
89 classic_prompt = PromptStripper(
90 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
90 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
91 initial_re=re.compile(r'^>>>( |$)')
91 initial_re=re.compile(r'^>>>( |$)')
92 )
92 )
93
93
94 ipython_prompt = PromptStripper(
94 ipython_prompt = PromptStripper(
95 re.compile(
95 re.compile(
96 r"""
96 r"""
97 ^( # Match from the beginning of a line, either:
97 ^( # Match from the beginning of a line, either:
98
98
99 # 1. First-line prompt:
99 # 1. First-line prompt:
100 ((\[nav\]|\[ins\])?\ )? # Vi editing mode prompt, if it's there
100 ((\[nav\]|\[ins\])?\ )? # Vi editing mode prompt, if it's there
101 In\ # The 'In' of the prompt, with a space
101 In\ # The 'In' of the prompt, with a space
102 \[\d+\]: # Command index, as displayed in the prompt
102 \[\d+\]: # Command index, as displayed in the prompt
103 \ # With a mandatory trailing space
103 \ # With a mandatory trailing space
104
104
105 | # ... or ...
105 | # ... or ...
106
106
107 # 2. The three dots of the multiline prompt
107 # 2. The three dots of the multiline prompt
108 \s* # All leading whitespace characters
108 \s* # All leading whitespace characters
109 \.{3,}: # The three (or more) dots
109 \.{3,}: # The three (or more) dots
110 \ ? # With an optional trailing space
110 \ ? # With an optional trailing space
111
111
112 )
112 )
113 """,
113 """,
114 re.VERBOSE,
114 re.VERBOSE,
115 )
115 )
116 )
116 )
117
117
118
118
119 def cell_magic(lines):
119 def cell_magic(lines):
120 if not lines or not lines[0].startswith('%%'):
120 if not lines or not lines[0].startswith('%%'):
121 return lines
121 return lines
122 if re.match(r'%%\w+\?', lines[0]):
122 if re.match(r'%%\w+\?', lines[0]):
123 # This case will be handled by help_end
123 # This case will be handled by help_end
124 return lines
124 return lines
125 magic_name, _, first_line = lines[0][2:].rstrip().partition(' ')
125 magic_name, _, first_line = lines[0][2:].rstrip().partition(' ')
126 body = ''.join(lines[1:])
126 body = ''.join(lines[1:])
127 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
127 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
128 % (magic_name, first_line, body)]
128 % (magic_name, first_line, body)]
129
129
130
130
131 def _find_assign_op(token_line) -> Optional[int]:
131 def _find_assign_op(token_line) -> Optional[int]:
132 """Get the index of the first assignment in the line ('=' not inside brackets)
132 """Get the index of the first assignment in the line ('=' not inside brackets)
133
133
134 Note: We don't try to support multiple special assignment (a = b = %foo)
134 Note: We don't try to support multiple special assignment (a = b = %foo)
135 """
135 """
136 paren_level = 0
136 paren_level = 0
137 for i, ti in enumerate(token_line):
137 for i, ti in enumerate(token_line):
138 s = ti.string
138 s = ti.string
139 if s == '=' and paren_level == 0:
139 if s == '=' and paren_level == 0:
140 return i
140 return i
141 if s in {'(','[','{'}:
141 if s in {'(','[','{'}:
142 paren_level += 1
142 paren_level += 1
143 elif s in {')', ']', '}'}:
143 elif s in {')', ']', '}'}:
144 if paren_level > 0:
144 if paren_level > 0:
145 paren_level -= 1
145 paren_level -= 1
146 return None
146 return None
147
147
148 def find_end_of_continued_line(lines, start_line: int):
148 def find_end_of_continued_line(lines, start_line: int):
149 """Find the last line of a line explicitly extended using backslashes.
149 """Find the last line of a line explicitly extended using backslashes.
150
150
151 Uses 0-indexed line numbers.
151 Uses 0-indexed line numbers.
152 """
152 """
153 end_line = start_line
153 end_line = start_line
154 while lines[end_line].endswith('\\\n'):
154 while lines[end_line].endswith('\\\n'):
155 end_line += 1
155 end_line += 1
156 if end_line >= len(lines):
156 if end_line >= len(lines):
157 break
157 break
158 return end_line
158 return end_line
159
159
160 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
160 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
161 r"""Assemble a single line from multiple continued line pieces
161 r"""Assemble a single line from multiple continued line pieces
162
162
163 Continued lines are lines ending in ``\``, and the line following the last
163 Continued lines are lines ending in ``\``, and the line following the last
164 ``\`` in the block.
164 ``\`` in the block.
165
165
166 For example, this code continues over multiple lines::
166 For example, this code continues over multiple lines::
167
167
168 if (assign_ix is not None) \
168 if (assign_ix is not None) \
169 and (len(line) >= assign_ix + 2) \
169 and (len(line) >= assign_ix + 2) \
170 and (line[assign_ix+1].string == '%') \
170 and (line[assign_ix+1].string == '%') \
171 and (line[assign_ix+2].type == tokenize.NAME):
171 and (line[assign_ix+2].type == tokenize.NAME):
172
172
173 This statement contains four continued line pieces.
173 This statement contains four continued line pieces.
174 Assembling these pieces into a single line would give::
174 Assembling these pieces into a single line would give::
175
175
176 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
176 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
177
177
178 This uses 0-indexed line numbers. *start* is (lineno, colno).
178 This uses 0-indexed line numbers. *start* is (lineno, colno).
179
179
180 Used to allow ``%magic`` and ``!system`` commands to be continued over
180 Used to allow ``%magic`` and ``!system`` commands to be continued over
181 multiple lines.
181 multiple lines.
182 """
182 """
183 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
183 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
184 return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
184 return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
185 + [parts[-1].rstrip()]) # Strip newline from last line
185 + [parts[-1].rstrip()]) # Strip newline from last line
186
186
187 class TokenTransformBase:
187 class TokenTransformBase:
188 """Base class for transformations which examine tokens.
188 """Base class for transformations which examine tokens.
189
189
190 Special syntax should not be transformed when it occurs inside strings or
190 Special syntax should not be transformed when it occurs inside strings or
191 comments. This is hard to reliably avoid with regexes. The solution is to
191 comments. This is hard to reliably avoid with regexes. The solution is to
192 tokenise the code as Python, and recognise the special syntax in the tokens.
192 tokenise the code as Python, and recognise the special syntax in the tokens.
193
193
194 IPython's special syntax is not valid Python syntax, so tokenising may go
194 IPython's special syntax is not valid Python syntax, so tokenising may go
195 wrong after the special syntax starts. These classes therefore find and
195 wrong after the special syntax starts. These classes therefore find and
196 transform *one* instance of special syntax at a time into regular Python
196 transform *one* instance of special syntax at a time into regular Python
197 syntax. After each transformation, tokens are regenerated to find the next
197 syntax. After each transformation, tokens are regenerated to find the next
198 piece of special syntax.
198 piece of special syntax.
199
199
200 Subclasses need to implement one class method (find)
200 Subclasses need to implement one class method (find)
201 and one regular method (transform).
201 and one regular method (transform).
202
202
203 The priority attribute can select which transformation to apply if multiple
203 The priority attribute can select which transformation to apply if multiple
204 transformers match in the same place. Lower numbers have higher priority.
204 transformers match in the same place. Lower numbers have higher priority.
205 This allows "%magic?" to be turned into a help call rather than a magic call.
205 This allows "%magic?" to be turned into a help call rather than a magic call.
206 """
206 """
207 # Lower numbers -> higher priority (for matches in the same location)
207 # Lower numbers -> higher priority (for matches in the same location)
208 priority = 10
208 priority = 10
209
209
210 def sortby(self):
210 def sortby(self):
211 return self.start_line, self.start_col, self.priority
211 return self.start_line, self.start_col, self.priority
212
212
213 def __init__(self, start):
213 def __init__(self, start):
214 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
214 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
215 self.start_col = start[1]
215 self.start_col = start[1]
216
216
217 @classmethod
217 @classmethod
218 def find(cls, tokens_by_line):
218 def find(cls, tokens_by_line):
219 """Find one instance of special syntax in the provided tokens.
219 """Find one instance of special syntax in the provided tokens.
220
220
221 Tokens are grouped into logical lines for convenience,
221 Tokens are grouped into logical lines for convenience,
222 so it is easy to e.g. look at the first token of each line.
222 so it is easy to e.g. look at the first token of each line.
223 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
223 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
224
224
225 This should return an instance of its class, pointing to the start
225 This should return an instance of its class, pointing to the start
226 position it has found, or None if it found no match.
226 position it has found, or None if it found no match.
227 """
227 """
228 raise NotImplementedError
228 raise NotImplementedError
229
229
230 def transform(self, lines: List[str]):
230 def transform(self, lines: List[str]):
231 """Transform one instance of special syntax found by ``find()``
231 """Transform one instance of special syntax found by ``find()``
232
232
233 Takes a list of strings representing physical lines,
233 Takes a list of strings representing physical lines,
234 returns a similar list of transformed lines.
234 returns a similar list of transformed lines.
235 """
235 """
236 raise NotImplementedError
236 raise NotImplementedError
237
237
238 class MagicAssign(TokenTransformBase):
238 class MagicAssign(TokenTransformBase):
239 """Transformer for assignments from magics (a = %foo)"""
239 """Transformer for assignments from magics (a = %foo)"""
240 @classmethod
240 @classmethod
241 def find(cls, tokens_by_line):
241 def find(cls, tokens_by_line):
242 """Find the first magic assignment (a = %foo) in the cell.
242 """Find the first magic assignment (a = %foo) in the cell.
243 """
243 """
244 for line in tokens_by_line:
244 for line in tokens_by_line:
245 assign_ix = _find_assign_op(line)
245 assign_ix = _find_assign_op(line)
246 if (assign_ix is not None) \
246 if (assign_ix is not None) \
247 and (len(line) >= assign_ix + 2) \
247 and (len(line) >= assign_ix + 2) \
248 and (line[assign_ix+1].string == '%') \
248 and (line[assign_ix+1].string == '%') \
249 and (line[assign_ix+2].type == tokenize.NAME):
249 and (line[assign_ix+2].type == tokenize.NAME):
250 return cls(line[assign_ix+1].start)
250 return cls(line[assign_ix+1].start)
251
251
252 def transform(self, lines: List[str]):
252 def transform(self, lines: List[str]):
253 """Transform a magic assignment found by the ``find()`` classmethod.
253 """Transform a magic assignment found by the ``find()`` classmethod.
254 """
254 """
255 start_line, start_col = self.start_line, self.start_col
255 start_line, start_col = self.start_line, self.start_col
256 lhs = lines[start_line][:start_col]
256 lhs = lines[start_line][:start_col]
257 end_line = find_end_of_continued_line(lines, start_line)
257 end_line = find_end_of_continued_line(lines, start_line)
258 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
258 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
259 assert rhs.startswith('%'), rhs
259 assert rhs.startswith('%'), rhs
260 magic_name, _, args = rhs[1:].partition(' ')
260 magic_name, _, args = rhs[1:].partition(' ')
261
261
262 lines_before = lines[:start_line]
262 lines_before = lines[:start_line]
263 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
263 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
264 new_line = lhs + call + '\n'
264 new_line = lhs + call + '\n'
265 lines_after = lines[end_line+1:]
265 lines_after = lines[end_line+1:]
266
266
267 return lines_before + [new_line] + lines_after
267 return lines_before + [new_line] + lines_after
268
268
269
269
270 class SystemAssign(TokenTransformBase):
270 class SystemAssign(TokenTransformBase):
271 """Transformer for assignments from system commands (a = !foo)"""
271 """Transformer for assignments from system commands (a = !foo)"""
272 @classmethod
272 @classmethod
273 def find(cls, tokens_by_line):
273 def find(cls, tokens_by_line):
274 """Find the first system assignment (a = !foo) in the cell.
274 """Find the first system assignment (a = !foo) in the cell.
275 """
275 """
276 for line in tokens_by_line:
276 for line in tokens_by_line:
277 assign_ix = _find_assign_op(line)
277 assign_ix = _find_assign_op(line)
278 if (assign_ix is not None) \
278 if (assign_ix is not None) \
279 and not line[assign_ix].line.strip().startswith('=') \
279 and not line[assign_ix].line.strip().startswith('=') \
280 and (len(line) >= assign_ix + 2) \
280 and (len(line) >= assign_ix + 2) \
281 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
281 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
282 ix = assign_ix + 1
282 ix = assign_ix + 1
283
283
284 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
284 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
285 if line[ix].string == '!':
285 if line[ix].string == '!':
286 return cls(line[ix].start)
286 return cls(line[ix].start)
287 elif not line[ix].string.isspace():
287 elif not line[ix].string.isspace():
288 break
288 break
289 ix += 1
289 ix += 1
290
290
291 def transform(self, lines: List[str]):
291 def transform(self, lines: List[str]):
292 """Transform a system assignment found by the ``find()`` classmethod.
292 """Transform a system assignment found by the ``find()`` classmethod.
293 """
293 """
294 start_line, start_col = self.start_line, self.start_col
294 start_line, start_col = self.start_line, self.start_col
295
295
296 lhs = lines[start_line][:start_col]
296 lhs = lines[start_line][:start_col]
297 end_line = find_end_of_continued_line(lines, start_line)
297 end_line = find_end_of_continued_line(lines, start_line)
298 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
298 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
299 assert rhs.startswith('!'), rhs
299 assert rhs.startswith('!'), rhs
300 cmd = rhs[1:]
300 cmd = rhs[1:]
301
301
302 lines_before = lines[:start_line]
302 lines_before = lines[:start_line]
303 call = "get_ipython().getoutput({!r})".format(cmd)
303 call = "get_ipython().getoutput({!r})".format(cmd)
304 new_line = lhs + call + '\n'
304 new_line = lhs + call + '\n'
305 lines_after = lines[end_line + 1:]
305 lines_after = lines[end_line + 1:]
306
306
307 return lines_before + [new_line] + lines_after
307 return lines_before + [new_line] + lines_after
308
308
309 # The escape sequences that define the syntax transformations IPython will
309 # The escape sequences that define the syntax transformations IPython will
310 # apply to user input. These can NOT be just changed here: many regular
310 # apply to user input. These can NOT be just changed here: many regular
311 # expressions and other parts of the code may use their hardcoded values, and
311 # expressions and other parts of the code may use their hardcoded values, and
312 # for all intents and purposes they constitute the 'IPython syntax', so they
312 # for all intents and purposes they constitute the 'IPython syntax', so they
313 # should be considered fixed.
313 # should be considered fixed.
314
314
315 ESC_SHELL = '!' # Send line to underlying system shell
315 ESC_SHELL = '!' # Send line to underlying system shell
316 ESC_SH_CAP = '!!' # Send line to system shell and capture output
316 ESC_SH_CAP = '!!' # Send line to system shell and capture output
317 ESC_HELP = '?' # Find information about object
317 ESC_HELP = '?' # Find information about object
318 ESC_HELP2 = '??' # Find extra-detailed information about object
318 ESC_HELP2 = '??' # Find extra-detailed information about object
319 ESC_MAGIC = '%' # Call magic function
319 ESC_MAGIC = '%' # Call magic function
320 ESC_MAGIC2 = '%%' # Call cell-magic function
320 ESC_MAGIC2 = '%%' # Call cell-magic function
321 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
321 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
322 ESC_QUOTE2 = ';' # Quote all args as a single string, call
322 ESC_QUOTE2 = ';' # Quote all args as a single string, call
323 ESC_PAREN = '/' # Call first argument with rest of line as arguments
323 ESC_PAREN = '/' # Call first argument with rest of line as arguments
324
324
325 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
325 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
326 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
326 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
327
327
328 def _make_help_call(target, esc, next_input=None):
328 def _make_help_call(target, esc, next_input=None):
329 """Prepares a pinfo(2)/psearch call from a target name and the escape
329 """Prepares a pinfo(2)/psearch call from a target name and the escape
330 (i.e. ? or ??)"""
330 (i.e. ? or ??)"""
331 method = 'pinfo2' if esc == '??' \
331 method = 'pinfo2' if esc == '??' \
332 else 'psearch' if '*' in target \
332 else 'psearch' if '*' in target \
333 else 'pinfo'
333 else 'pinfo'
334 arg = " ".join([method, target])
334 arg = " ".join([method, target])
335 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
335 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
336 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
336 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
337 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
337 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
338 if next_input is None:
338 if next_input is None:
339 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
339 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
340 else:
340 else:
341 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
341 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
342 (next_input, t_magic_name, t_magic_arg_s)
342 (next_input, t_magic_name, t_magic_arg_s)
343
343
344 def _tr_help(content):
344 def _tr_help(content):
345 """Translate lines escaped with: ?
345 """Translate lines escaped with: ?
346
346
347 A naked help line should fire the intro help screen (shell.show_usage())
347 A naked help line should fire the intro help screen (shell.show_usage())
348 """
348 """
349 if not content:
349 if not content:
350 return 'get_ipython().show_usage()'
350 return 'get_ipython().show_usage()'
351
351
352 return _make_help_call(content, '?')
352 return _make_help_call(content, '?')
353
353
354 def _tr_help2(content):
354 def _tr_help2(content):
355 """Translate lines escaped with: ??
355 """Translate lines escaped with: ??
356
356
357 A naked help line should fire the intro help screen (shell.show_usage())
357 A naked help line should fire the intro help screen (shell.show_usage())
358 """
358 """
359 if not content:
359 if not content:
360 return 'get_ipython().show_usage()'
360 return 'get_ipython().show_usage()'
361
361
362 return _make_help_call(content, '??')
362 return _make_help_call(content, '??')
363
363
364 def _tr_magic(content):
364 def _tr_magic(content):
365 "Translate lines escaped with a percent sign: %"
365 "Translate lines escaped with a percent sign: %"
366 name, _, args = content.partition(' ')
366 name, _, args = content.partition(' ')
367 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
367 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
368
368
369 def _tr_quote(content):
369 def _tr_quote(content):
370 "Translate lines escaped with a comma: ,"
370 "Translate lines escaped with a comma: ,"
371 name, _, args = content.partition(' ')
371 name, _, args = content.partition(' ')
372 return '%s("%s")' % (name, '", "'.join(args.split()) )
372 return '%s("%s")' % (name, '", "'.join(args.split()) )
373
373
374 def _tr_quote2(content):
374 def _tr_quote2(content):
375 "Translate lines escaped with a semicolon: ;"
375 "Translate lines escaped with a semicolon: ;"
376 name, _, args = content.partition(' ')
376 name, _, args = content.partition(' ')
377 return '%s("%s")' % (name, args)
377 return '%s("%s")' % (name, args)
378
378
379 def _tr_paren(content):
379 def _tr_paren(content):
380 "Translate lines escaped with a slash: /"
380 "Translate lines escaped with a slash: /"
381 name, _, args = content.partition(' ')
381 name, _, args = content.partition(' ')
382 return '%s(%s)' % (name, ", ".join(args.split()))
382 return '%s(%s)' % (name, ", ".join(args.split()))
383
383
384 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
384 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
385 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
385 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
386 ESC_HELP : _tr_help,
386 ESC_HELP : _tr_help,
387 ESC_HELP2 : _tr_help2,
387 ESC_HELP2 : _tr_help2,
388 ESC_MAGIC : _tr_magic,
388 ESC_MAGIC : _tr_magic,
389 ESC_QUOTE : _tr_quote,
389 ESC_QUOTE : _tr_quote,
390 ESC_QUOTE2 : _tr_quote2,
390 ESC_QUOTE2 : _tr_quote2,
391 ESC_PAREN : _tr_paren }
391 ESC_PAREN : _tr_paren }
392
392
393 class EscapedCommand(TokenTransformBase):
393 class EscapedCommand(TokenTransformBase):
394 """Transformer for escaped commands like %foo, !foo, or /foo"""
394 """Transformer for escaped commands like %foo, !foo, or /foo"""
395 @classmethod
395 @classmethod
396 def find(cls, tokens_by_line):
396 def find(cls, tokens_by_line):
397 """Find the first escaped command (%foo, !foo, etc.) in the cell.
397 """Find the first escaped command (%foo, !foo, etc.) in the cell.
398 """
398 """
399 for line in tokens_by_line:
399 for line in tokens_by_line:
400 if not line:
400 if not line:
401 continue
401 continue
402 ix = 0
402 ix = 0
403 ll = len(line)
403 ll = len(line)
404 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
404 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
405 ix += 1
405 ix += 1
406 if ix >= ll:
406 if ix >= ll:
407 continue
407 continue
408 if line[ix].string in ESCAPE_SINGLES:
408 if line[ix].string in ESCAPE_SINGLES:
409 return cls(line[ix].start)
409 return cls(line[ix].start)
410
410
411 def transform(self, lines):
411 def transform(self, lines):
412 """Transform an escaped line found by the ``find()`` classmethod.
412 """Transform an escaped line found by the ``find()`` classmethod.
413 """
413 """
414 start_line, start_col = self.start_line, self.start_col
414 start_line, start_col = self.start_line, self.start_col
415
415
416 indent = lines[start_line][:start_col]
416 indent = lines[start_line][:start_col]
417 end_line = find_end_of_continued_line(lines, start_line)
417 end_line = find_end_of_continued_line(lines, start_line)
418 line = assemble_continued_line(lines, (start_line, start_col), end_line)
418 line = assemble_continued_line(lines, (start_line, start_col), end_line)
419
419
420 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
420 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
421 escape, content = line[:2], line[2:]
421 escape, content = line[:2], line[2:]
422 else:
422 else:
423 escape, content = line[:1], line[1:]
423 escape, content = line[:1], line[1:]
424
424
425 if escape in tr:
425 if escape in tr:
426 call = tr[escape](content)
426 call = tr[escape](content)
427 else:
427 else:
428 call = ''
428 call = ''
429
429
430 lines_before = lines[:start_line]
430 lines_before = lines[:start_line]
431 new_line = indent + call + '\n'
431 new_line = indent + call + '\n'
432 lines_after = lines[end_line + 1:]
432 lines_after = lines[end_line + 1:]
433
433
434 return lines_before + [new_line] + lines_after
434 return lines_before + [new_line] + lines_after
435
435
436 _help_end_re = re.compile(r"""(%{0,2}
436 _help_end_re = re.compile(r"""(%{0,2}
437 (?!\d)[\w*]+ # Variable name
437 (?!\d)[\w*]+ # Variable name
438 (\.(?!\d)[\w*]+)* # .etc.etc
438 (\.(?!\d)[\w*]+)* # .etc.etc
439 )
439 )
440 (\?\??)$ # ? or ??
440 (\?\??)$ # ? or ??
441 """,
441 """,
442 re.VERBOSE)
442 re.VERBOSE)
443
443
444 class HelpEnd(TokenTransformBase):
444 class HelpEnd(TokenTransformBase):
445 """Transformer for help syntax: obj? and obj??"""
445 """Transformer for help syntax: obj? and obj??"""
446 # This needs to be higher priority (lower number) than EscapedCommand so
446 # This needs to be higher priority (lower number) than EscapedCommand so
447 # that inspecting magics (%foo?) works.
447 # that inspecting magics (%foo?) works.
448 priority = 5
448 priority = 5
449
449
450 def __init__(self, start, q_locn):
450 def __init__(self, start, q_locn):
451 super().__init__(start)
451 super().__init__(start)
452 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
452 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
453 self.q_col = q_locn[1]
453 self.q_col = q_locn[1]
454
454
455 @classmethod
455 @classmethod
456 def find(cls, tokens_by_line):
456 def find(cls, tokens_by_line):
457 """Find the first help command (foo?) in the cell.
457 """Find the first help command (foo?) in the cell.
458 """
458 """
459 for line in tokens_by_line:
459 for line in tokens_by_line:
460 # Last token is NEWLINE; look at last but one
460 # Last token is NEWLINE; look at last but one
461 if len(line) > 2 and line[-2].string == '?':
461 if len(line) > 2 and line[-2].string == '?':
462 # Find the first token that's not INDENT/DEDENT
462 # Find the first token that's not INDENT/DEDENT
463 ix = 0
463 ix = 0
464 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
464 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
465 ix += 1
465 ix += 1
466 return cls(line[ix].start, line[-2].start)
466 return cls(line[ix].start, line[-2].start)
467
467
468 def transform(self, lines):
468 def transform(self, lines):
469 """Transform a help command found by the ``find()`` classmethod.
469 """Transform a help command found by the ``find()`` classmethod.
470 """
470 """
471 piece = ''.join(lines[self.start_line:self.q_line+1])
471 piece = ''.join(lines[self.start_line:self.q_line+1])
472 indent, content = piece[:self.start_col], piece[self.start_col:]
472 indent, content = piece[:self.start_col], piece[self.start_col:]
473 lines_before = lines[:self.start_line]
473 lines_before = lines[:self.start_line]
474 lines_after = lines[self.q_line + 1:]
474 lines_after = lines[self.q_line + 1:]
475
475
476 m = _help_end_re.search(content)
476 m = _help_end_re.search(content)
477 if not m:
477 if not m:
478 raise SyntaxError(content)
478 raise SyntaxError(content)
479 assert m is not None, content
479 assert m is not None, content
480 target = m.group(1)
480 target = m.group(1)
481 esc = m.group(3)
481 esc = m.group(3)
482
482
483 # If we're mid-command, put it back on the next prompt for the user.
483 # If we're mid-command, put it back on the next prompt for the user.
484 next_input = None
484 next_input = None
485 if (not lines_before) and (not lines_after) \
485 if (not lines_before) and (not lines_after) \
486 and content.strip() != m.group(0):
486 and content.strip() != m.group(0):
487 next_input = content.rstrip('?\n')
487 next_input = content.rstrip('?\n')
488
488
489 call = _make_help_call(target, esc, next_input=next_input)
489 call = _make_help_call(target, esc, next_input=next_input)
490 new_line = indent + call + '\n'
490 new_line = indent + call + '\n'
491
491
492 return lines_before + [new_line] + lines_after
492 return lines_before + [new_line] + lines_after
493
493
494 def make_tokens_by_line(lines:List[str]):
494 def make_tokens_by_line(lines:List[str]):
495 """Tokenize a series of lines and group tokens by line.
495 """Tokenize a series of lines and group tokens by line.
496
496
497 The tokens for a multiline Python string or expression are grouped as one
497 The tokens for a multiline Python string or expression are grouped as one
498 line. All lines except the last lines should keep their line ending ('\\n',
498 line. All lines except the last lines should keep their line ending ('\\n',
499 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
499 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
500 for example when passing block of text to this function.
500 for example when passing block of text to this function.
501
501
502 """
502 """
503 # NL tokens are used inside multiline expressions, but also after blank
503 # NL tokens are used inside multiline expressions, but also after blank
504 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
504 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
505 # We want to group the former case together but split the latter, so we
505 # We want to group the former case together but split the latter, so we
506 # track parentheses level, similar to the internals of tokenize.
506 # track parentheses level, similar to the internals of tokenize.
507
507
508 # reexported from token on 3.7+
508 # reexported from token on 3.7+
509 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore
509 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore
510 tokens_by_line:List[List[Any]] = [[]]
510 tokens_by_line: List[List[Any]] = [[]]
511 if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')):
511 if len(lines) > 1 and not lines[0].endswith(("\n", "\r", "\r\n", "\x0b", "\x0c")):
512 warnings.warn("`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified")
512 warnings.warn(
513 "`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified",
514 stacklevel=2,
515 )
513 parenlev = 0
516 parenlev = 0
514 try:
517 try:
515 for token in tokenize.generate_tokens(iter(lines).__next__):
518 for token in tokenize.generate_tokens(iter(lines).__next__):
516 tokens_by_line[-1].append(token)
519 tokens_by_line[-1].append(token)
517 if (token.type == NEWLINE) \
520 if (token.type == NEWLINE) \
518 or ((token.type == NL) and (parenlev <= 0)):
521 or ((token.type == NL) and (parenlev <= 0)):
519 tokens_by_line.append([])
522 tokens_by_line.append([])
520 elif token.string in {'(', '[', '{'}:
523 elif token.string in {'(', '[', '{'}:
521 parenlev += 1
524 parenlev += 1
522 elif token.string in {')', ']', '}'}:
525 elif token.string in {')', ']', '}'}:
523 if parenlev > 0:
526 if parenlev > 0:
524 parenlev -= 1
527 parenlev -= 1
525 except tokenize.TokenError:
528 except tokenize.TokenError:
526 # Input ended in a multiline string or expression. That's OK for us.
529 # Input ended in a multiline string or expression. That's OK for us.
527 pass
530 pass
528
531
529
532
530 if not tokens_by_line[-1]:
533 if not tokens_by_line[-1]:
531 tokens_by_line.pop()
534 tokens_by_line.pop()
532
535
533
536
534 return tokens_by_line
537 return tokens_by_line
535
538
536
539
537 def has_sunken_brackets(tokens: List[tokenize.TokenInfo]):
540 def has_sunken_brackets(tokens: List[tokenize.TokenInfo]):
538 """Check if the depth of brackets in the list of tokens drops below 0"""
541 """Check if the depth of brackets in the list of tokens drops below 0"""
539 parenlev = 0
542 parenlev = 0
540 for token in tokens:
543 for token in tokens:
541 if token.string in {"(", "[", "{"}:
544 if token.string in {"(", "[", "{"}:
542 parenlev += 1
545 parenlev += 1
543 elif token.string in {")", "]", "}"}:
546 elif token.string in {")", "]", "}"}:
544 parenlev -= 1
547 parenlev -= 1
545 if parenlev < 0:
548 if parenlev < 0:
546 return True
549 return True
547 return False
550 return False
548
551
549
552
550 def show_linewise_tokens(s: str):
553 def show_linewise_tokens(s: str):
551 """For investigation and debugging"""
554 """For investigation and debugging"""
552 if not s.endswith('\n'):
555 if not s.endswith('\n'):
553 s += '\n'
556 s += '\n'
554 lines = s.splitlines(keepends=True)
557 lines = s.splitlines(keepends=True)
555 for line in make_tokens_by_line(lines):
558 for line in make_tokens_by_line(lines):
556 print("Line -------")
559 print("Line -------")
557 for tokinfo in line:
560 for tokinfo in line:
558 print(" ", tokinfo)
561 print(" ", tokinfo)
559
562
560 # Arbitrary limit to prevent getting stuck in infinite loops
563 # Arbitrary limit to prevent getting stuck in infinite loops
561 TRANSFORM_LOOP_LIMIT = 500
564 TRANSFORM_LOOP_LIMIT = 500
562
565
563 class TransformerManager:
566 class TransformerManager:
564 """Applies various transformations to a cell or code block.
567 """Applies various transformations to a cell or code block.
565
568
566 The key methods for external use are ``transform_cell()``
569 The key methods for external use are ``transform_cell()``
567 and ``check_complete()``.
570 and ``check_complete()``.
568 """
571 """
569 def __init__(self):
572 def __init__(self):
570 self.cleanup_transforms = [
573 self.cleanup_transforms = [
571 leading_empty_lines,
574 leading_empty_lines,
572 leading_indent,
575 leading_indent,
573 classic_prompt,
576 classic_prompt,
574 ipython_prompt,
577 ipython_prompt,
575 ]
578 ]
576 self.line_transforms = [
579 self.line_transforms = [
577 cell_magic,
580 cell_magic,
578 ]
581 ]
579 self.token_transformers = [
582 self.token_transformers = [
580 MagicAssign,
583 MagicAssign,
581 SystemAssign,
584 SystemAssign,
582 EscapedCommand,
585 EscapedCommand,
583 HelpEnd,
586 HelpEnd,
584 ]
587 ]
585
588
586 def do_one_token_transform(self, lines):
589 def do_one_token_transform(self, lines):
587 """Find and run the transform earliest in the code.
590 """Find and run the transform earliest in the code.
588
591
589 Returns (changed, lines).
592 Returns (changed, lines).
590
593
591 This method is called repeatedly until changed is False, indicating
594 This method is called repeatedly until changed is False, indicating
592 that all available transformations are complete.
595 that all available transformations are complete.
593
596
594 The tokens following IPython special syntax might not be valid, so
597 The tokens following IPython special syntax might not be valid, so
595 the transformed code is retokenised every time to identify the next
598 the transformed code is retokenised every time to identify the next
596 piece of special syntax. Hopefully long code cells are mostly valid
599 piece of special syntax. Hopefully long code cells are mostly valid
597 Python, not using lots of IPython special syntax, so this shouldn't be
600 Python, not using lots of IPython special syntax, so this shouldn't be
598 a performance issue.
601 a performance issue.
599 """
602 """
600 tokens_by_line = make_tokens_by_line(lines)
603 tokens_by_line = make_tokens_by_line(lines)
601 candidates = []
604 candidates = []
602 for transformer_cls in self.token_transformers:
605 for transformer_cls in self.token_transformers:
603 transformer = transformer_cls.find(tokens_by_line)
606 transformer = transformer_cls.find(tokens_by_line)
604 if transformer:
607 if transformer:
605 candidates.append(transformer)
608 candidates.append(transformer)
606
609
607 if not candidates:
610 if not candidates:
608 # Nothing to transform
611 # Nothing to transform
609 return False, lines
612 return False, lines
610 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
613 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
611 for transformer in ordered_transformers:
614 for transformer in ordered_transformers:
612 try:
615 try:
613 return True, transformer.transform(lines)
616 return True, transformer.transform(lines)
614 except SyntaxError:
617 except SyntaxError:
615 pass
618 pass
616 return False, lines
619 return False, lines
617
620
618 def do_token_transforms(self, lines):
621 def do_token_transforms(self, lines):
619 for _ in range(TRANSFORM_LOOP_LIMIT):
622 for _ in range(TRANSFORM_LOOP_LIMIT):
620 changed, lines = self.do_one_token_transform(lines)
623 changed, lines = self.do_one_token_transform(lines)
621 if not changed:
624 if not changed:
622 return lines
625 return lines
623
626
624 raise RuntimeError("Input transformation still changing after "
627 raise RuntimeError("Input transformation still changing after "
625 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
628 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
626
629
627 def transform_cell(self, cell: str) -> str:
630 def transform_cell(self, cell: str) -> str:
628 """Transforms a cell of input code"""
631 """Transforms a cell of input code"""
629 if not cell.endswith('\n'):
632 if not cell.endswith('\n'):
630 cell += '\n' # Ensure the cell has a trailing newline
633 cell += '\n' # Ensure the cell has a trailing newline
631 lines = cell.splitlines(keepends=True)
634 lines = cell.splitlines(keepends=True)
632 for transform in self.cleanup_transforms + self.line_transforms:
635 for transform in self.cleanup_transforms + self.line_transforms:
633 lines = transform(lines)
636 lines = transform(lines)
634
637
635 lines = self.do_token_transforms(lines)
638 lines = self.do_token_transforms(lines)
636 return ''.join(lines)
639 return ''.join(lines)
637
640
638 def check_complete(self, cell: str):
641 def check_complete(self, cell: str):
639 """Return whether a block of code is ready to execute, or should be continued
642 """Return whether a block of code is ready to execute, or should be continued
640
643
641 Parameters
644 Parameters
642 ----------
645 ----------
643 cell : string
646 cell : string
644 Python input code, which can be multiline.
647 Python input code, which can be multiline.
645
648
646 Returns
649 Returns
647 -------
650 -------
648 status : str
651 status : str
649 One of 'complete', 'incomplete', or 'invalid' if source is not a
652 One of 'complete', 'incomplete', or 'invalid' if source is not a
650 prefix of valid code.
653 prefix of valid code.
651 indent_spaces : int or None
654 indent_spaces : int or None
652 The number of spaces by which to indent the next line of code. If
655 The number of spaces by which to indent the next line of code. If
653 status is not 'incomplete', this is None.
656 status is not 'incomplete', this is None.
654 """
657 """
655 # Remember if the lines ends in a new line.
658 # Remember if the lines ends in a new line.
656 ends_with_newline = False
659 ends_with_newline = False
657 for character in reversed(cell):
660 for character in reversed(cell):
658 if character == '\n':
661 if character == '\n':
659 ends_with_newline = True
662 ends_with_newline = True
660 break
663 break
661 elif character.strip():
664 elif character.strip():
662 break
665 break
663 else:
666 else:
664 continue
667 continue
665
668
666 if not ends_with_newline:
669 if not ends_with_newline:
667 # Append an newline for consistent tokenization
670 # Append an newline for consistent tokenization
668 # See https://bugs.python.org/issue33899
671 # See https://bugs.python.org/issue33899
669 cell += '\n'
672 cell += '\n'
670
673
671 lines = cell.splitlines(keepends=True)
674 lines = cell.splitlines(keepends=True)
672
675
673 if not lines:
676 if not lines:
674 return 'complete', None
677 return 'complete', None
675
678
676 if lines[-1].endswith('\\'):
679 if lines[-1].endswith('\\'):
677 # Explicit backslash continuation
680 # Explicit backslash continuation
678 return 'incomplete', find_last_indent(lines)
681 return 'incomplete', find_last_indent(lines)
679
682
680 try:
683 try:
681 for transform in self.cleanup_transforms:
684 for transform in self.cleanup_transforms:
682 if not getattr(transform, 'has_side_effects', False):
685 if not getattr(transform, 'has_side_effects', False):
683 lines = transform(lines)
686 lines = transform(lines)
684 except SyntaxError:
687 except SyntaxError:
685 return 'invalid', None
688 return 'invalid', None
686
689
687 if lines[0].startswith('%%'):
690 if lines[0].startswith('%%'):
688 # Special case for cell magics - completion marked by blank line
691 # Special case for cell magics - completion marked by blank line
689 if lines[-1].strip():
692 if lines[-1].strip():
690 return 'incomplete', find_last_indent(lines)
693 return 'incomplete', find_last_indent(lines)
691 else:
694 else:
692 return 'complete', None
695 return 'complete', None
693
696
694 try:
697 try:
695 for transform in self.line_transforms:
698 for transform in self.line_transforms:
696 if not getattr(transform, 'has_side_effects', False):
699 if not getattr(transform, 'has_side_effects', False):
697 lines = transform(lines)
700 lines = transform(lines)
698 lines = self.do_token_transforms(lines)
701 lines = self.do_token_transforms(lines)
699 except SyntaxError:
702 except SyntaxError:
700 return 'invalid', None
703 return 'invalid', None
701
704
702 tokens_by_line = make_tokens_by_line(lines)
705 tokens_by_line = make_tokens_by_line(lines)
703
706
704 # Bail if we got one line and there are more closing parentheses than
707 # Bail if we got one line and there are more closing parentheses than
705 # the opening ones
708 # the opening ones
706 if (
709 if (
707 len(lines) == 1
710 len(lines) == 1
708 and tokens_by_line
711 and tokens_by_line
709 and has_sunken_brackets(tokens_by_line[0])
712 and has_sunken_brackets(tokens_by_line[0])
710 ):
713 ):
711 return "invalid", None
714 return "invalid", None
712
715
713 if not tokens_by_line:
716 if not tokens_by_line:
714 return 'incomplete', find_last_indent(lines)
717 return 'incomplete', find_last_indent(lines)
715
718
716 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
719 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
717 # We're in a multiline string or expression
720 # We're in a multiline string or expression
718 return 'incomplete', find_last_indent(lines)
721 return 'incomplete', find_last_indent(lines)
719
722
720 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER} # type: ignore
723 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER} # type: ignore
721
724
722 # Pop the last line which only contains DEDENTs and ENDMARKER
725 # Pop the last line which only contains DEDENTs and ENDMARKER
723 last_token_line = None
726 last_token_line = None
724 if {t.type for t in tokens_by_line[-1]} in [
727 if {t.type for t in tokens_by_line[-1]} in [
725 {tokenize.DEDENT, tokenize.ENDMARKER},
728 {tokenize.DEDENT, tokenize.ENDMARKER},
726 {tokenize.ENDMARKER}
729 {tokenize.ENDMARKER}
727 ] and len(tokens_by_line) > 1:
730 ] and len(tokens_by_line) > 1:
728 last_token_line = tokens_by_line.pop()
731 last_token_line = tokens_by_line.pop()
729
732
730 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
733 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
731 tokens_by_line[-1].pop()
734 tokens_by_line[-1].pop()
732
735
733 if not tokens_by_line[-1]:
736 if not tokens_by_line[-1]:
734 return 'incomplete', find_last_indent(lines)
737 return 'incomplete', find_last_indent(lines)
735
738
736 if tokens_by_line[-1][-1].string == ':':
739 if tokens_by_line[-1][-1].string == ':':
737 # The last line starts a block (e.g. 'if foo:')
740 # The last line starts a block (e.g. 'if foo:')
738 ix = 0
741 ix = 0
739 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
742 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
740 ix += 1
743 ix += 1
741
744
742 indent = tokens_by_line[-1][ix].start[1]
745 indent = tokens_by_line[-1][ix].start[1]
743 return 'incomplete', indent + 4
746 return 'incomplete', indent + 4
744
747
745 if tokens_by_line[-1][0].line.endswith('\\'):
748 if tokens_by_line[-1][0].line.endswith('\\'):
746 return 'incomplete', None
749 return 'incomplete', None
747
750
748 # At this point, our checks think the code is complete (or invalid).
751 # At this point, our checks think the code is complete (or invalid).
749 # We'll use codeop.compile_command to check this with the real parser
752 # We'll use codeop.compile_command to check this with the real parser
750 try:
753 try:
751 with warnings.catch_warnings():
754 with warnings.catch_warnings():
752 warnings.simplefilter('error', SyntaxWarning)
755 warnings.simplefilter('error', SyntaxWarning)
753 res = compile_command(''.join(lines), symbol='exec')
756 res = compile_command(''.join(lines), symbol='exec')
754 except (SyntaxError, OverflowError, ValueError, TypeError,
757 except (SyntaxError, OverflowError, ValueError, TypeError,
755 MemoryError, SyntaxWarning):
758 MemoryError, SyntaxWarning):
756 return 'invalid', None
759 return 'invalid', None
757 else:
760 else:
758 if res is None:
761 if res is None:
759 return 'incomplete', find_last_indent(lines)
762 return 'incomplete', find_last_indent(lines)
760
763
761 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
764 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
762 if ends_with_newline:
765 if ends_with_newline:
763 return 'complete', None
766 return 'complete', None
764 return 'incomplete', find_last_indent(lines)
767 return 'incomplete', find_last_indent(lines)
765
768
766 # If there's a blank line at the end, assume we're ready to execute
769 # If there's a blank line at the end, assume we're ready to execute
767 if not lines[-1].strip():
770 if not lines[-1].strip():
768 return 'complete', None
771 return 'complete', None
769
772
770 return 'complete', None
773 return 'complete', None
771
774
772
775
773 def find_last_indent(lines):
776 def find_last_indent(lines):
774 m = _indent_re.match(lines[-1])
777 m = _indent_re.match(lines[-1])
775 if not m:
778 if not m:
776 return 0
779 return 0
777 return len(m.group(0).replace('\t', ' '*4))
780 return len(m.group(0).replace('\t', ' '*4))
778
781
779
782
780 class MaybeAsyncCompile(Compile):
783 class MaybeAsyncCompile(Compile):
781 def __init__(self, extra_flags=0):
784 def __init__(self, extra_flags=0):
782 super().__init__()
785 super().__init__()
783 self.flags |= extra_flags
786 self.flags |= extra_flags
784
787
785 def __call__(self, *args, **kwds):
786 return compile(*args, **kwds)
787
788
788
789 class MaybeAsyncCommandCompiler(CommandCompiler):
789 class MaybeAsyncCommandCompiler(CommandCompiler):
790 def __init__(self, extra_flags=0):
790 def __init__(self, extra_flags=0):
791 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags)
791 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags)
792
792
793
793
794 _extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
794 _extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
795
795
796 compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)
796 compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)
@@ -1,278 +1,310 b''
1 ''' A decorator-based method of constructing IPython magics with `argparse`
1 ''' A decorator-based method of constructing IPython magics with `argparse`
2 option handling.
2 option handling.
3
3
4 New magic functions can be defined like so::
4 New magic functions can be defined like so::
5
5
6 from IPython.core.magic_arguments import (argument, magic_arguments,
6 from IPython.core.magic_arguments import (argument, magic_arguments,
7 parse_argstring)
7 parse_argstring)
8
8
9 @magic_arguments()
9 @magic_arguments()
10 @argument('-o', '--option', help='An optional argument.')
10 @argument('-o', '--option', help='An optional argument.')
11 @argument('arg', type=int, help='An integer positional argument.')
11 @argument('arg', type=int, help='An integer positional argument.')
12 def magic_cool(self, arg):
12 def magic_cool(self, arg):
13 """ A really cool magic command.
13 """ A really cool magic command.
14
14
15 """
15 """
16 args = parse_argstring(magic_cool, arg)
16 args = parse_argstring(magic_cool, arg)
17 ...
17 ...
18
18
19 The `@magic_arguments` decorator marks the function as having argparse arguments.
19 The `@magic_arguments` decorator marks the function as having argparse arguments.
20 The `@argument` decorator adds an argument using the same syntax as argparse's
20 The `@argument` decorator adds an argument using the same syntax as argparse's
21 `add_argument()` method. More sophisticated uses may also require the
21 `add_argument()` method. More sophisticated uses may also require the
22 `@argument_group` or `@kwds` decorator to customize the formatting and the
22 `@argument_group` or `@kwds` decorator to customize the formatting and the
23 parsing.
23 parsing.
24
24
25 Help text for the magic is automatically generated from the docstring and the
25 Help text for the magic is automatically generated from the docstring and the
26 arguments::
26 arguments::
27
27
28 In[1]: %cool?
28 In[1]: %cool?
29 %cool [-o OPTION] arg
29 %cool [-o OPTION] arg
30
30
31 A really cool magic command.
31 A really cool magic command.
32
32
33 positional arguments:
33 positional arguments:
34 arg An integer positional argument.
34 arg An integer positional argument.
35
35
36 optional arguments:
36 optional arguments:
37 -o OPTION, --option OPTION
37 -o OPTION, --option OPTION
38 An optional argument.
38 An optional argument.
39
39
40 Here is an elaborated example that uses default parameters in `argument` and calls the `args` in the cell magic::
41
42 from IPython.core.magic import register_cell_magic
43 from IPython.core.magic_arguments import (argument, magic_arguments,
44 parse_argstring)
45
46
47 @magic_arguments()
48 @argument(
49 "--option",
50 "-o",
51 help=("Add an option here"),
52 )
53 @argument(
54 "--style",
55 "-s",
56 default="foo",
57 help=("Add some style arguments"),
58 )
59 @register_cell_magic
60 def my_cell_magic(line, cell):
61 args = parse_argstring(my_cell_magic, line)
62 print(f"{args.option=}")
63 print(f"{args.style=}")
64 print(f"{cell=}")
65
66 In a jupyter notebook, this cell magic can be executed like this::
67
68 %%my_cell_magic -o Hello
69 print("bar")
70 i = 42
71
40 Inheritance diagram:
72 Inheritance diagram:
41
73
42 .. inheritance-diagram:: IPython.core.magic_arguments
74 .. inheritance-diagram:: IPython.core.magic_arguments
43 :parts: 3
75 :parts: 3
44
76
45 '''
77 '''
46 #-----------------------------------------------------------------------------
78 #-----------------------------------------------------------------------------
47 # Copyright (C) 2010-2011, IPython Development Team.
79 # Copyright (C) 2010-2011, IPython Development Team.
48 #
80 #
49 # Distributed under the terms of the Modified BSD License.
81 # Distributed under the terms of the Modified BSD License.
50 #
82 #
51 # The full license is in the file COPYING.txt, distributed with this software.
83 # The full license is in the file COPYING.txt, distributed with this software.
52 #-----------------------------------------------------------------------------
84 #-----------------------------------------------------------------------------
53 import argparse
85 import argparse
54 import re
86 import re
55
87
56 # Our own imports
88 # Our own imports
57 from IPython.core.error import UsageError
89 from IPython.core.error import UsageError
58 from IPython.utils.decorators import undoc
90 from IPython.utils.decorators import undoc
59 from IPython.utils.process import arg_split
91 from IPython.utils.process import arg_split
60 from IPython.utils.text import dedent
92 from IPython.utils.text import dedent
61
93
62 NAME_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*$")
94 NAME_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*$")
63
95
64 @undoc
96 @undoc
65 class MagicHelpFormatter(argparse.RawDescriptionHelpFormatter):
97 class MagicHelpFormatter(argparse.RawDescriptionHelpFormatter):
66 """A HelpFormatter with a couple of changes to meet our needs.
98 """A HelpFormatter with a couple of changes to meet our needs.
67 """
99 """
68 # Modified to dedent text.
100 # Modified to dedent text.
69 def _fill_text(self, text, width, indent):
101 def _fill_text(self, text, width, indent):
70 return argparse.RawDescriptionHelpFormatter._fill_text(self, dedent(text), width, indent)
102 return argparse.RawDescriptionHelpFormatter._fill_text(self, dedent(text), width, indent)
71
103
72 # Modified to wrap argument placeholders in <> where necessary.
104 # Modified to wrap argument placeholders in <> where necessary.
73 def _format_action_invocation(self, action):
105 def _format_action_invocation(self, action):
74 if not action.option_strings:
106 if not action.option_strings:
75 metavar, = self._metavar_formatter(action, action.dest)(1)
107 metavar, = self._metavar_formatter(action, action.dest)(1)
76 return metavar
108 return metavar
77
109
78 else:
110 else:
79 parts = []
111 parts = []
80
112
81 # if the Optional doesn't take a value, format is:
113 # if the Optional doesn't take a value, format is:
82 # -s, --long
114 # -s, --long
83 if action.nargs == 0:
115 if action.nargs == 0:
84 parts.extend(action.option_strings)
116 parts.extend(action.option_strings)
85
117
86 # if the Optional takes a value, format is:
118 # if the Optional takes a value, format is:
87 # -s ARGS, --long ARGS
119 # -s ARGS, --long ARGS
88 else:
120 else:
89 default = action.dest.upper()
121 default = action.dest.upper()
90 args_string = self._format_args(action, default)
122 args_string = self._format_args(action, default)
91 # IPYTHON MODIFICATION: If args_string is not a plain name, wrap
123 # IPYTHON MODIFICATION: If args_string is not a plain name, wrap
92 # it in <> so it's valid RST.
124 # it in <> so it's valid RST.
93 if not NAME_RE.match(args_string):
125 if not NAME_RE.match(args_string):
94 args_string = "<%s>" % args_string
126 args_string = "<%s>" % args_string
95 for option_string in action.option_strings:
127 for option_string in action.option_strings:
96 parts.append('%s %s' % (option_string, args_string))
128 parts.append('%s %s' % (option_string, args_string))
97
129
98 return ', '.join(parts)
130 return ', '.join(parts)
99
131
100 # Override the default prefix ('usage') to our % magic escape,
132 # Override the default prefix ('usage') to our % magic escape,
101 # in a code block.
133 # in a code block.
102 def add_usage(self, usage, actions, groups, prefix="::\n\n %"):
134 def add_usage(self, usage, actions, groups, prefix="::\n\n %"):
103 super(MagicHelpFormatter, self).add_usage(usage, actions, groups, prefix)
135 super(MagicHelpFormatter, self).add_usage(usage, actions, groups, prefix)
104
136
105 class MagicArgumentParser(argparse.ArgumentParser):
137 class MagicArgumentParser(argparse.ArgumentParser):
106 """ An ArgumentParser tweaked for use by IPython magics.
138 """ An ArgumentParser tweaked for use by IPython magics.
107 """
139 """
108 def __init__(self,
140 def __init__(self,
109 prog=None,
141 prog=None,
110 usage=None,
142 usage=None,
111 description=None,
143 description=None,
112 epilog=None,
144 epilog=None,
113 parents=None,
145 parents=None,
114 formatter_class=MagicHelpFormatter,
146 formatter_class=MagicHelpFormatter,
115 prefix_chars='-',
147 prefix_chars='-',
116 argument_default=None,
148 argument_default=None,
117 conflict_handler='error',
149 conflict_handler='error',
118 add_help=False):
150 add_help=False):
119 if parents is None:
151 if parents is None:
120 parents = []
152 parents = []
121 super(MagicArgumentParser, self).__init__(prog=prog, usage=usage,
153 super(MagicArgumentParser, self).__init__(prog=prog, usage=usage,
122 description=description, epilog=epilog,
154 description=description, epilog=epilog,
123 parents=parents, formatter_class=formatter_class,
155 parents=parents, formatter_class=formatter_class,
124 prefix_chars=prefix_chars, argument_default=argument_default,
156 prefix_chars=prefix_chars, argument_default=argument_default,
125 conflict_handler=conflict_handler, add_help=add_help)
157 conflict_handler=conflict_handler, add_help=add_help)
126
158
127 def error(self, message):
159 def error(self, message):
128 """ Raise a catchable error instead of exiting.
160 """ Raise a catchable error instead of exiting.
129 """
161 """
130 raise UsageError(message)
162 raise UsageError(message)
131
163
132 def parse_argstring(self, argstring):
164 def parse_argstring(self, argstring):
133 """ Split a string into an argument list and parse that argument list.
165 """ Split a string into an argument list and parse that argument list.
134 """
166 """
135 argv = arg_split(argstring)
167 argv = arg_split(argstring)
136 return self.parse_args(argv)
168 return self.parse_args(argv)
137
169
138
170
139 def construct_parser(magic_func):
171 def construct_parser(magic_func):
140 """ Construct an argument parser using the function decorations.
172 """ Construct an argument parser using the function decorations.
141 """
173 """
142 kwds = getattr(magic_func, 'argcmd_kwds', {})
174 kwds = getattr(magic_func, 'argcmd_kwds', {})
143 if 'description' not in kwds:
175 if 'description' not in kwds:
144 kwds['description'] = getattr(magic_func, '__doc__', None)
176 kwds['description'] = getattr(magic_func, '__doc__', None)
145 arg_name = real_name(magic_func)
177 arg_name = real_name(magic_func)
146 parser = MagicArgumentParser(arg_name, **kwds)
178 parser = MagicArgumentParser(arg_name, **kwds)
147 # Reverse the list of decorators in order to apply them in the
179 # Reverse the list of decorators in order to apply them in the
148 # order in which they appear in the source.
180 # order in which they appear in the source.
149 group = None
181 group = None
150 for deco in magic_func.decorators[::-1]:
182 for deco in magic_func.decorators[::-1]:
151 result = deco.add_to_parser(parser, group)
183 result = deco.add_to_parser(parser, group)
152 if result is not None:
184 if result is not None:
153 group = result
185 group = result
154
186
155 # Replace the magic function's docstring with the full help text.
187 # Replace the magic function's docstring with the full help text.
156 magic_func.__doc__ = parser.format_help()
188 magic_func.__doc__ = parser.format_help()
157
189
158 return parser
190 return parser
159
191
160
192
161 def parse_argstring(magic_func, argstring):
193 def parse_argstring(magic_func, argstring):
162 """ Parse the string of arguments for the given magic function.
194 """ Parse the string of arguments for the given magic function.
163 """
195 """
164 return magic_func.parser.parse_argstring(argstring)
196 return magic_func.parser.parse_argstring(argstring)
165
197
166
198
167 def real_name(magic_func):
199 def real_name(magic_func):
168 """ Find the real name of the magic.
200 """ Find the real name of the magic.
169 """
201 """
170 magic_name = magic_func.__name__
202 magic_name = magic_func.__name__
171 if magic_name.startswith('magic_'):
203 if magic_name.startswith('magic_'):
172 magic_name = magic_name[len('magic_'):]
204 magic_name = magic_name[len('magic_'):]
173 return getattr(magic_func, 'argcmd_name', magic_name)
205 return getattr(magic_func, 'argcmd_name', magic_name)
174
206
175
207
176 class ArgDecorator(object):
208 class ArgDecorator(object):
177 """ Base class for decorators to add ArgumentParser information to a method.
209 """ Base class for decorators to add ArgumentParser information to a method.
178 """
210 """
179
211
180 def __call__(self, func):
212 def __call__(self, func):
181 if not getattr(func, 'has_arguments', False):
213 if not getattr(func, 'has_arguments', False):
182 func.has_arguments = True
214 func.has_arguments = True
183 func.decorators = []
215 func.decorators = []
184 func.decorators.append(self)
216 func.decorators.append(self)
185 return func
217 return func
186
218
187 def add_to_parser(self, parser, group):
219 def add_to_parser(self, parser, group):
188 """ Add this object's information to the parser, if necessary.
220 """ Add this object's information to the parser, if necessary.
189 """
221 """
190 pass
222 pass
191
223
192
224
193 class magic_arguments(ArgDecorator):
225 class magic_arguments(ArgDecorator):
194 """ Mark the magic as having argparse arguments and possibly adjust the
226 """ Mark the magic as having argparse arguments and possibly adjust the
195 name.
227 name.
196 """
228 """
197
229
198 def __init__(self, name=None):
230 def __init__(self, name=None):
199 self.name = name
231 self.name = name
200
232
201 def __call__(self, func):
233 def __call__(self, func):
202 if not getattr(func, 'has_arguments', False):
234 if not getattr(func, 'has_arguments', False):
203 func.has_arguments = True
235 func.has_arguments = True
204 func.decorators = []
236 func.decorators = []
205 if self.name is not None:
237 if self.name is not None:
206 func.argcmd_name = self.name
238 func.argcmd_name = self.name
207 # This should be the first decorator in the list of decorators, thus the
239 # This should be the first decorator in the list of decorators, thus the
208 # last to execute. Build the parser.
240 # last to execute. Build the parser.
209 func.parser = construct_parser(func)
241 func.parser = construct_parser(func)
210 return func
242 return func
211
243
212
244
213 class ArgMethodWrapper(ArgDecorator):
245 class ArgMethodWrapper(ArgDecorator):
214
246
215 """
247 """
216 Base class to define a wrapper for ArgumentParser method.
248 Base class to define a wrapper for ArgumentParser method.
217
249
218 Child class must define either `_method_name` or `add_to_parser`.
250 Child class must define either `_method_name` or `add_to_parser`.
219
251
220 """
252 """
221
253
222 _method_name = None
254 _method_name = None
223
255
224 def __init__(self, *args, **kwds):
256 def __init__(self, *args, **kwds):
225 self.args = args
257 self.args = args
226 self.kwds = kwds
258 self.kwds = kwds
227
259
228 def add_to_parser(self, parser, group):
260 def add_to_parser(self, parser, group):
229 """ Add this object's information to the parser.
261 """ Add this object's information to the parser.
230 """
262 """
231 if group is not None:
263 if group is not None:
232 parser = group
264 parser = group
233 getattr(parser, self._method_name)(*self.args, **self.kwds)
265 getattr(parser, self._method_name)(*self.args, **self.kwds)
234 return None
266 return None
235
267
236
268
237 class argument(ArgMethodWrapper):
269 class argument(ArgMethodWrapper):
238 """ Store arguments and keywords to pass to add_argument().
270 """ Store arguments and keywords to pass to add_argument().
239
271
240 Instances also serve to decorate command methods.
272 Instances also serve to decorate command methods.
241 """
273 """
242 _method_name = 'add_argument'
274 _method_name = 'add_argument'
243
275
244
276
245 class defaults(ArgMethodWrapper):
277 class defaults(ArgMethodWrapper):
246 """ Store arguments and keywords to pass to set_defaults().
278 """ Store arguments and keywords to pass to set_defaults().
247
279
248 Instances also serve to decorate command methods.
280 Instances also serve to decorate command methods.
249 """
281 """
250 _method_name = 'set_defaults'
282 _method_name = 'set_defaults'
251
283
252
284
253 class argument_group(ArgMethodWrapper):
285 class argument_group(ArgMethodWrapper):
254 """ Store arguments and keywords to pass to add_argument_group().
286 """ Store arguments and keywords to pass to add_argument_group().
255
287
256 Instances also serve to decorate command methods.
288 Instances also serve to decorate command methods.
257 """
289 """
258
290
259 def add_to_parser(self, parser, group):
291 def add_to_parser(self, parser, group):
260 """ Add this object's information to the parser.
292 """ Add this object's information to the parser.
261 """
293 """
262 return parser.add_argument_group(*self.args, **self.kwds)
294 return parser.add_argument_group(*self.args, **self.kwds)
263
295
264
296
265 class kwds(ArgDecorator):
297 class kwds(ArgDecorator):
266 """ Provide other keywords to the sub-parser constructor.
298 """ Provide other keywords to the sub-parser constructor.
267 """
299 """
268 def __init__(self, **kwds):
300 def __init__(self, **kwds):
269 self.kwds = kwds
301 self.kwds = kwds
270
302
271 def __call__(self, func):
303 def __call__(self, func):
272 func = super(kwds, self).__call__(func)
304 func = super(kwds, self).__call__(func)
273 func.argcmd_kwds = self.kwds
305 func.argcmd_kwds = self.kwds
274 return func
306 return func
275
307
276
308
277 __all__ = ['magic_arguments', 'argument', 'argument_group', 'kwds',
309 __all__ = ['magic_arguments', 'argument', 'argument_group', 'kwds',
278 'parse_argstring']
310 'parse_argstring']
@@ -1,388 +1,405 b''
1 """Tests for the token-based transformers in IPython.core.inputtransformer2
1 """Tests for the token-based transformers in IPython.core.inputtransformer2
2
2
3 Line-based transformers are the simpler ones; token-based transformers are
3 Line-based transformers are the simpler ones; token-based transformers are
4 more complex. See test_inputtransformer2_line for tests for line-based
4 more complex. See test_inputtransformer2_line for tests for line-based
5 transformations.
5 transformations.
6 """
6 """
7 import platform
7 import string
8 import string
8 import sys
9 import sys
9 from textwrap import dedent
10 from textwrap import dedent
10
11
11 import pytest
12 import pytest
12
13
13 from IPython.core import inputtransformer2 as ipt2
14 from IPython.core import inputtransformer2 as ipt2
14 from IPython.core.inputtransformer2 import _find_assign_op, make_tokens_by_line
15 from IPython.core.inputtransformer2 import _find_assign_op, make_tokens_by_line
15
16
16 MULTILINE_MAGIC = ("""\
17 MULTILINE_MAGIC = ("""\
17 a = f()
18 a = f()
18 %foo \\
19 %foo \\
19 bar
20 bar
20 g()
21 g()
21 """.splitlines(keepends=True), (2, 0), """\
22 """.splitlines(keepends=True), (2, 0), """\
22 a = f()
23 a = f()
23 get_ipython().run_line_magic('foo', ' bar')
24 get_ipython().run_line_magic('foo', ' bar')
24 g()
25 g()
25 """.splitlines(keepends=True))
26 """.splitlines(keepends=True))
26
27
27 INDENTED_MAGIC = ("""\
28 INDENTED_MAGIC = ("""\
28 for a in range(5):
29 for a in range(5):
29 %ls
30 %ls
30 """.splitlines(keepends=True), (2, 4), """\
31 """.splitlines(keepends=True), (2, 4), """\
31 for a in range(5):
32 for a in range(5):
32 get_ipython().run_line_magic('ls', '')
33 get_ipython().run_line_magic('ls', '')
33 """.splitlines(keepends=True))
34 """.splitlines(keepends=True))
34
35
35 CRLF_MAGIC = ([
36 CRLF_MAGIC = ([
36 "a = f()\n",
37 "a = f()\n",
37 "%ls\r\n",
38 "%ls\r\n",
38 "g()\n"
39 "g()\n"
39 ], (2, 0), [
40 ], (2, 0), [
40 "a = f()\n",
41 "a = f()\n",
41 "get_ipython().run_line_magic('ls', '')\n",
42 "get_ipython().run_line_magic('ls', '')\n",
42 "g()\n"
43 "g()\n"
43 ])
44 ])
44
45
45 MULTILINE_MAGIC_ASSIGN = ("""\
46 MULTILINE_MAGIC_ASSIGN = ("""\
46 a = f()
47 a = f()
47 b = %foo \\
48 b = %foo \\
48 bar
49 bar
49 g()
50 g()
50 """.splitlines(keepends=True), (2, 4), """\
51 """.splitlines(keepends=True), (2, 4), """\
51 a = f()
52 a = f()
52 b = get_ipython().run_line_magic('foo', ' bar')
53 b = get_ipython().run_line_magic('foo', ' bar')
53 g()
54 g()
54 """.splitlines(keepends=True))
55 """.splitlines(keepends=True))
55
56
56 MULTILINE_SYSTEM_ASSIGN = ("""\
57 MULTILINE_SYSTEM_ASSIGN = ("""\
57 a = f()
58 a = f()
58 b = !foo \\
59 b = !foo \\
59 bar
60 bar
60 g()
61 g()
61 """.splitlines(keepends=True), (2, 4), """\
62 """.splitlines(keepends=True), (2, 4), """\
62 a = f()
63 a = f()
63 b = get_ipython().getoutput('foo bar')
64 b = get_ipython().getoutput('foo bar')
64 g()
65 g()
65 """.splitlines(keepends=True))
66 """.splitlines(keepends=True))
66
67
67 #####
68 #####
68
69
69 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = ("""\
70 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = ("""\
70 def test():
71 def test():
71 for i in range(1):
72 for i in range(1):
72 print(i)
73 print(i)
73 res =! ls
74 res =! ls
74 """.splitlines(keepends=True), (4, 7), '''\
75 """.splitlines(keepends=True), (4, 7), '''\
75 def test():
76 def test():
76 for i in range(1):
77 for i in range(1):
77 print(i)
78 print(i)
78 res =get_ipython().getoutput(\' ls\')
79 res =get_ipython().getoutput(\' ls\')
79 '''.splitlines(keepends=True))
80 '''.splitlines(keepends=True))
80
81
81 ######
82 ######
82
83
83 AUTOCALL_QUOTE = (
84 AUTOCALL_QUOTE = (
84 [",f 1 2 3\n"], (1, 0),
85 [",f 1 2 3\n"], (1, 0),
85 ['f("1", "2", "3")\n']
86 ['f("1", "2", "3")\n']
86 )
87 )
87
88
88 AUTOCALL_QUOTE2 = (
89 AUTOCALL_QUOTE2 = (
89 [";f 1 2 3\n"], (1, 0),
90 [";f 1 2 3\n"], (1, 0),
90 ['f("1 2 3")\n']
91 ['f("1 2 3")\n']
91 )
92 )
92
93
93 AUTOCALL_PAREN = (
94 AUTOCALL_PAREN = (
94 ["/f 1 2 3\n"], (1, 0),
95 ["/f 1 2 3\n"], (1, 0),
95 ['f(1, 2, 3)\n']
96 ['f(1, 2, 3)\n']
96 )
97 )
97
98
98 SIMPLE_HELP = (
99 SIMPLE_HELP = (
99 ["foo?\n"], (1, 0),
100 ["foo?\n"], (1, 0),
100 ["get_ipython().run_line_magic('pinfo', 'foo')\n"]
101 ["get_ipython().run_line_magic('pinfo', 'foo')\n"]
101 )
102 )
102
103
103 DETAILED_HELP = (
104 DETAILED_HELP = (
104 ["foo??\n"], (1, 0),
105 ["foo??\n"], (1, 0),
105 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"]
106 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"]
106 )
107 )
107
108
108 MAGIC_HELP = (
109 MAGIC_HELP = (
109 ["%foo?\n"], (1, 0),
110 ["%foo?\n"], (1, 0),
110 ["get_ipython().run_line_magic('pinfo', '%foo')\n"]
111 ["get_ipython().run_line_magic('pinfo', '%foo')\n"]
111 )
112 )
112
113
113 HELP_IN_EXPR = (
114 HELP_IN_EXPR = (
114 ["a = b + c?\n"], (1, 0),
115 ["a = b + c?\n"], (1, 0),
115 ["get_ipython().set_next_input('a = b + c');"
116 ["get_ipython().set_next_input('a = b + c');"
116 "get_ipython().run_line_magic('pinfo', 'c')\n"]
117 "get_ipython().run_line_magic('pinfo', 'c')\n"]
117 )
118 )
118
119
119 HELP_CONTINUED_LINE = ("""\
120 HELP_CONTINUED_LINE = ("""\
120 a = \\
121 a = \\
121 zip?
122 zip?
122 """.splitlines(keepends=True), (1, 0),
123 """.splitlines(keepends=True), (1, 0),
123 [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
124 [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
124 )
125 )
125
126
126 HELP_MULTILINE = ("""\
127 HELP_MULTILINE = ("""\
127 (a,
128 (a,
128 b) = zip?
129 b) = zip?
129 """.splitlines(keepends=True), (1, 0),
130 """.splitlines(keepends=True), (1, 0),
130 [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
131 [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
131 )
132 )
132
133
133 HELP_UNICODE = (
134 HELP_UNICODE = (
134 ["Ο€.foo?\n"], (1, 0),
135 ["Ο€.foo?\n"], (1, 0),
135 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"]
136 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"]
136 )
137 )
137
138
138
139
139 def null_cleanup_transformer(lines):
140 def null_cleanup_transformer(lines):
140 """
141 """
141 A cleanup transform that returns an empty list.
142 A cleanup transform that returns an empty list.
142 """
143 """
143 return []
144 return []
144
145
145
146
146 def test_check_make_token_by_line_never_ends_empty():
147 def test_check_make_token_by_line_never_ends_empty():
147 """
148 """
148 Check that not sequence of single or double characters ends up leading to en empty list of tokens
149 Check that not sequence of single or double characters ends up leading to en empty list of tokens
149 """
150 """
150 from string import printable
151 from string import printable
151 for c in printable:
152 for c in printable:
152 assert make_tokens_by_line(c)[-1] != []
153 assert make_tokens_by_line(c)[-1] != []
153 for k in printable:
154 for k in printable:
154 assert make_tokens_by_line(c + k)[-1] != []
155 assert make_tokens_by_line(c + k)[-1] != []
155
156
156
157
157 def check_find(transformer, case, match=True):
158 def check_find(transformer, case, match=True):
158 sample, expected_start, _ = case
159 sample, expected_start, _ = case
159 tbl = make_tokens_by_line(sample)
160 tbl = make_tokens_by_line(sample)
160 res = transformer.find(tbl)
161 res = transformer.find(tbl)
161 if match:
162 if match:
162 # start_line is stored 0-indexed, expected values are 1-indexed
163 # start_line is stored 0-indexed, expected values are 1-indexed
163 assert (res.start_line + 1, res.start_col) == expected_start
164 assert (res.start_line + 1, res.start_col) == expected_start
164 return res
165 return res
165 else:
166 else:
166 assert res is None
167 assert res is None
167
168
168 def check_transform(transformer_cls, case):
169 def check_transform(transformer_cls, case):
169 lines, start, expected = case
170 lines, start, expected = case
170 transformer = transformer_cls(start)
171 transformer = transformer_cls(start)
171 assert transformer.transform(lines) == expected
172 assert transformer.transform(lines) == expected
172
173
173 def test_continued_line():
174 def test_continued_line():
174 lines = MULTILINE_MAGIC_ASSIGN[0]
175 lines = MULTILINE_MAGIC_ASSIGN[0]
175 assert ipt2.find_end_of_continued_line(lines, 1) == 2
176 assert ipt2.find_end_of_continued_line(lines, 1) == 2
176
177
177 assert ipt2.assemble_continued_line(lines, (1, 5), 2) == "foo bar"
178 assert ipt2.assemble_continued_line(lines, (1, 5), 2) == "foo bar"
178
179
179 def test_find_assign_magic():
180 def test_find_assign_magic():
180 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
181 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
181 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
182 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
182 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
183 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
183
184
184 def test_transform_assign_magic():
185 def test_transform_assign_magic():
185 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
186 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
186
187
187 def test_find_assign_system():
188 def test_find_assign_system():
188 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
189 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
189 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
190 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
190 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
191 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
191 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
192 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
192 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
193 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
193
194
194 def test_transform_assign_system():
195 def test_transform_assign_system():
195 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
196 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
196 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
197 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
197
198
198 def test_find_magic_escape():
199 def test_find_magic_escape():
199 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
200 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
200 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
201 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
201 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
202 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
202
203
203 def test_transform_magic_escape():
204 def test_transform_magic_escape():
204 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
205 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
205 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
206 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
206 check_transform(ipt2.EscapedCommand, CRLF_MAGIC)
207 check_transform(ipt2.EscapedCommand, CRLF_MAGIC)
207
208
208 def test_find_autocalls():
209 def test_find_autocalls():
209 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
210 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
210 print("Testing %r" % case[0])
211 print("Testing %r" % case[0])
211 check_find(ipt2.EscapedCommand, case)
212 check_find(ipt2.EscapedCommand, case)
212
213
213 def test_transform_autocall():
214 def test_transform_autocall():
214 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
215 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
215 print("Testing %r" % case[0])
216 print("Testing %r" % case[0])
216 check_transform(ipt2.EscapedCommand, case)
217 check_transform(ipt2.EscapedCommand, case)
217
218
218 def test_find_help():
219 def test_find_help():
219 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
220 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
220 check_find(ipt2.HelpEnd, case)
221 check_find(ipt2.HelpEnd, case)
221
222
222 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
223 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
223 assert tf.q_line == 1
224 assert tf.q_line == 1
224 assert tf.q_col == 3
225 assert tf.q_col == 3
225
226
226 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
227 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
227 assert tf.q_line == 1
228 assert tf.q_line == 1
228 assert tf.q_col == 8
229 assert tf.q_col == 8
229
230
230 # ? in a comment does not trigger help
231 # ? in a comment does not trigger help
231 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
232 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
232 # Nor in a string
233 # Nor in a string
233 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
234 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
234
235
235 def test_transform_help():
236 def test_transform_help():
236 tf = ipt2.HelpEnd((1, 0), (1, 9))
237 tf = ipt2.HelpEnd((1, 0), (1, 9))
237 assert tf.transform(HELP_IN_EXPR[0]) == HELP_IN_EXPR[2]
238 assert tf.transform(HELP_IN_EXPR[0]) == HELP_IN_EXPR[2]
238
239
239 tf = ipt2.HelpEnd((1, 0), (2, 3))
240 tf = ipt2.HelpEnd((1, 0), (2, 3))
240 assert tf.transform(HELP_CONTINUED_LINE[0]) == HELP_CONTINUED_LINE[2]
241 assert tf.transform(HELP_CONTINUED_LINE[0]) == HELP_CONTINUED_LINE[2]
241
242
242 tf = ipt2.HelpEnd((1, 0), (2, 8))
243 tf = ipt2.HelpEnd((1, 0), (2, 8))
243 assert tf.transform(HELP_MULTILINE[0]) == HELP_MULTILINE[2]
244 assert tf.transform(HELP_MULTILINE[0]) == HELP_MULTILINE[2]
244
245
245 tf = ipt2.HelpEnd((1, 0), (1, 0))
246 tf = ipt2.HelpEnd((1, 0), (1, 0))
246 assert tf.transform(HELP_UNICODE[0]) == HELP_UNICODE[2]
247 assert tf.transform(HELP_UNICODE[0]) == HELP_UNICODE[2]
247
248
248 def test_find_assign_op_dedent():
249 def test_find_assign_op_dedent():
249 """
250 """
250 be careful that empty token like dedent are not counted as parens
251 be careful that empty token like dedent are not counted as parens
251 """
252 """
252 class Tk:
253 class Tk:
253 def __init__(self, s):
254 def __init__(self, s):
254 self.string = s
255 self.string = s
255
256
256 assert _find_assign_op([Tk(s) for s in ("", "a", "=", "b")]) == 2
257 assert _find_assign_op([Tk(s) for s in ("", "a", "=", "b")]) == 2
257 assert (
258 assert (
258 _find_assign_op([Tk(s) for s in ("", "(", "a", "=", "b", ")", "=", "5")]) == 6
259 _find_assign_op([Tk(s) for s in ("", "(", "a", "=", "b", ")", "=", "5")]) == 6
259 )
260 )
260
261
261
262
262 examples = [
263 examples = [
263 pytest.param("a = 1", "complete", None),
264 pytest.param("a = 1", "complete", None),
264 pytest.param("for a in range(5):", "incomplete", 4),
265 pytest.param("for a in range(5):", "incomplete", 4),
265 pytest.param("for a in range(5):\n if a > 0:", "incomplete", 8),
266 pytest.param("for a in range(5):\n if a > 0:", "incomplete", 8),
266 pytest.param("raise = 2", "invalid", None),
267 pytest.param("raise = 2", "invalid", None),
267 pytest.param("a = [1,\n2,", "incomplete", 0),
268 pytest.param("a = [1,\n2,", "incomplete", 0),
268 pytest.param("(\n))", "incomplete", 0),
269 pytest.param("(\n))", "incomplete", 0),
269 pytest.param("\\\r\n", "incomplete", 0),
270 pytest.param("\\\r\n", "incomplete", 0),
270 pytest.param("a = '''\n hi", "incomplete", 3),
271 pytest.param("a = '''\n hi", "incomplete", 3),
271 pytest.param("def a():\n x=1\n global x", "invalid", None),
272 pytest.param("def a():\n x=1\n global x", "invalid", None),
272 pytest.param(
273 pytest.param(
273 "a \\ ",
274 "a \\ ",
274 "invalid",
275 "invalid",
275 None,
276 None,
276 marks=pytest.mark.xfail(
277 marks=pytest.mark.xfail(
277 reason="Bug in python 3.9.8 – bpo 45738",
278 reason="Bug in python 3.9.8 – bpo 45738",
278 condition=sys.version_info
279 condition=sys.version_info
279 in [(3, 9, 8, "final", 0), (3, 11, 0, "alpha", 2)],
280 in [(3, 9, 8, "final", 0), (3, 11, 0, "alpha", 2)],
280 raises=SystemError,
281 raises=SystemError,
281 strict=True,
282 strict=True,
282 ),
283 ),
283 ), # Nothing allowed after backslash,
284 ), # Nothing allowed after backslash,
284 pytest.param("1\\\n+2", "complete", None),
285 pytest.param("1\\\n+2", "complete", None),
285 ]
286 ]
286
287
287
288
288 @pytest.mark.parametrize("code, expected, number", examples)
289 @pytest.mark.parametrize("code, expected, number", examples)
289 def test_check_complete_param(code, expected, number):
290 def test_check_complete_param(code, expected, number):
290 cc = ipt2.TransformerManager().check_complete
291 cc = ipt2.TransformerManager().check_complete
291 assert cc(code) == (expected, number)
292 assert cc(code) == (expected, number)
292
293
293
294
295 @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
294 @pytest.mark.xfail(
296 @pytest.mark.xfail(
295 reason="Bug in python 3.9.8 – bpo 45738",
297 reason="Bug in python 3.9.8 – bpo 45738",
296 condition=sys.version_info in [(3, 9, 8, "final", 0), (3, 11, 0, "alpha", 2)],
298 condition=sys.version_info in [(3, 9, 8, "final", 0), (3, 11, 0, "alpha", 2)],
297 raises=SystemError,
299 raises=SystemError,
298 strict=True,
300 strict=True,
299 )
301 )
300 def test_check_complete():
302 def test_check_complete():
301 cc = ipt2.TransformerManager().check_complete
303 cc = ipt2.TransformerManager().check_complete
302
304
303 example = dedent("""
305 example = dedent("""
304 if True:
306 if True:
305 a=1""" )
307 a=1""" )
306
308
307 assert cc(example) == ("incomplete", 4)
309 assert cc(example) == ("incomplete", 4)
308 assert cc(example + "\n") == ("complete", None)
310 assert cc(example + "\n") == ("complete", None)
309 assert cc(example + "\n ") == ("complete", None)
311 assert cc(example + "\n ") == ("complete", None)
310
312
311 # no need to loop on all the letters/numbers.
313 # no need to loop on all the letters/numbers.
312 short = '12abAB'+string.printable[62:]
314 short = '12abAB'+string.printable[62:]
313 for c in short:
315 for c in short:
314 # test does not raise:
316 # test does not raise:
315 cc(c)
317 cc(c)
316 for k in short:
318 for k in short:
317 cc(c+k)
319 cc(c+k)
318
320
319 assert cc("def f():\n x=0\n \\\n ") == ("incomplete", 2)
321 assert cc("def f():\n x=0\n \\\n ") == ("incomplete", 2)
320
322
321
323
322 def test_check_complete_II():
324 @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
325 @pytest.mark.parametrize(
326 "value, expected",
327 [
328 ('''def foo():\n """''', ("incomplete", 4)),
329 ("""async with example:\n pass""", ("incomplete", 4)),
330 ("""async with example:\n pass\n """, ("complete", None)),
331 ],
332 )
333 def test_check_complete_II(value, expected):
323 """
334 """
324 Test that multiple line strings are properly handled.
335 Test that multiple line strings are properly handled.
325
336
326 Separate test function for convenience
337 Separate test function for convenience
327
338
328 """
339 """
329 cc = ipt2.TransformerManager().check_complete
340 cc = ipt2.TransformerManager().check_complete
330 assert cc('''def foo():\n """''') == ("incomplete", 4)
341 assert cc(value) == expected
331
342
332
343
333 def test_check_complete_invalidates_sunken_brackets():
344 @pytest.mark.parametrize(
345 "value, expected",
346 [
347 (")", ("invalid", None)),
348 ("]", ("invalid", None)),
349 ("}", ("invalid", None)),
350 (")(", ("invalid", None)),
351 ("][", ("invalid", None)),
352 ("}{", ("invalid", None)),
353 ("]()(", ("invalid", None)),
354 ("())(", ("invalid", None)),
355 (")[](", ("invalid", None)),
356 ("()](", ("invalid", None)),
357 ],
358 )
359 def test_check_complete_invalidates_sunken_brackets(value, expected):
334 """
360 """
335 Test that a single line with more closing brackets than the opening ones is
361 Test that a single line with more closing brackets than the opening ones is
336 interpreted as invalid
362 interpreted as invalid
337 """
363 """
338 cc = ipt2.TransformerManager().check_complete
364 cc = ipt2.TransformerManager().check_complete
339 assert cc(")") == ("invalid", None)
365 assert cc(value) == expected
340 assert cc("]") == ("invalid", None)
341 assert cc("}") == ("invalid", None)
342 assert cc(")(") == ("invalid", None)
343 assert cc("][") == ("invalid", None)
344 assert cc("}{") == ("invalid", None)
345 assert cc("]()(") == ("invalid", None)
346 assert cc("())(") == ("invalid", None)
347 assert cc(")[](") == ("invalid", None)
348 assert cc("()](") == ("invalid", None)
349
366
350
367
351 def test_null_cleanup_transformer():
368 def test_null_cleanup_transformer():
352 manager = ipt2.TransformerManager()
369 manager = ipt2.TransformerManager()
353 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
370 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
354 assert manager.transform_cell("") == ""
371 assert manager.transform_cell("") == ""
355
372
356
373
357
374
358
375
359 def test_side_effects_I():
376 def test_side_effects_I():
360 count = 0
377 count = 0
361 def counter(lines):
378 def counter(lines):
362 nonlocal count
379 nonlocal count
363 count += 1
380 count += 1
364 return lines
381 return lines
365
382
366 counter.has_side_effects = True
383 counter.has_side_effects = True
367
384
368 manager = ipt2.TransformerManager()
385 manager = ipt2.TransformerManager()
369 manager.cleanup_transforms.insert(0, counter)
386 manager.cleanup_transforms.insert(0, counter)
370 assert manager.check_complete("a=1\n") == ('complete', None)
387 assert manager.check_complete("a=1\n") == ('complete', None)
371 assert count == 0
388 assert count == 0
372
389
373
390
374
391
375
392
376 def test_side_effects_II():
393 def test_side_effects_II():
377 count = 0
394 count = 0
378 def counter(lines):
395 def counter(lines):
379 nonlocal count
396 nonlocal count
380 count += 1
397 count += 1
381 return lines
398 return lines
382
399
383 counter.has_side_effects = True
400 counter.has_side_effects = True
384
401
385 manager = ipt2.TransformerManager()
402 manager = ipt2.TransformerManager()
386 manager.line_transforms.insert(0, counter)
403 manager.line_transforms.insert(0, counter)
387 assert manager.check_complete("b=1\n") == ('complete', None)
404 assert manager.check_complete("b=1\n") == ('complete', None)
388 assert count == 0
405 assert count == 0
@@ -1,504 +1,509 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """Tests for IPython.utils.path.py"""
2 """Tests for IPython.utils.path.py"""
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 import os
7 import os
8 import shutil
8 import shutil
9 import sys
9 import sys
10 import tempfile
10 import tempfile
11 import unittest
11 import unittest
12 from contextlib import contextmanager
12 from contextlib import contextmanager
13 from unittest.mock import patch
13 from unittest.mock import patch
14 from os.path import join, abspath
14 from os.path import join, abspath
15 from importlib import reload
15 from importlib import reload
16
16
17 import pytest
17 import pytest
18
18
19 import IPython
19 import IPython
20 from IPython import paths
20 from IPython import paths
21 from IPython.testing import decorators as dec
21 from IPython.testing import decorators as dec
22 from IPython.testing.decorators import (
22 from IPython.testing.decorators import (
23 skip_if_not_win32,
23 skip_if_not_win32,
24 skip_win32,
24 skip_win32,
25 onlyif_unicode_paths,
25 onlyif_unicode_paths,
26 )
26 )
27 from IPython.testing.tools import make_tempfile
27 from IPython.testing.tools import make_tempfile
28 from IPython.utils import path
28 from IPython.utils import path
29 from IPython.utils.tempdir import TemporaryDirectory
29 from IPython.utils.tempdir import TemporaryDirectory
30
30
31
31
32 # Platform-dependent imports
32 # Platform-dependent imports
33 try:
33 try:
34 import winreg as wreg
34 import winreg as wreg
35 except ImportError:
35 except ImportError:
36 #Fake _winreg module on non-windows platforms
36 #Fake _winreg module on non-windows platforms
37 import types
37 import types
38 wr_name = "winreg"
38 wr_name = "winreg"
39 sys.modules[wr_name] = types.ModuleType(wr_name)
39 sys.modules[wr_name] = types.ModuleType(wr_name)
40 try:
40 try:
41 import winreg as wreg
41 import winreg as wreg
42 except ImportError:
42 except ImportError:
43 import _winreg as wreg
43 import _winreg as wreg
44 #Add entries that needs to be stubbed by the testing code
44 #Add entries that needs to be stubbed by the testing code
45 (wreg.OpenKey, wreg.QueryValueEx,) = (None, None)
45 (wreg.OpenKey, wreg.QueryValueEx,) = (None, None)
46
46
47 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
48 # Globals
48 # Globals
49 #-----------------------------------------------------------------------------
49 #-----------------------------------------------------------------------------
50 env = os.environ
50 env = os.environ
51 TMP_TEST_DIR = tempfile.mkdtemp()
51 TMP_TEST_DIR = tempfile.mkdtemp()
52 HOME_TEST_DIR = join(TMP_TEST_DIR, "home_test_dir")
52 HOME_TEST_DIR = join(TMP_TEST_DIR, "home_test_dir")
53 #
53 #
54 # Setup/teardown functions/decorators
54 # Setup/teardown functions/decorators
55 #
55 #
56
56
57 def setup_module():
57 def setup_module():
58 """Setup testenvironment for the module:
58 """Setup testenvironment for the module:
59
59
60 - Adds dummy home dir tree
60 - Adds dummy home dir tree
61 """
61 """
62 # Do not mask exceptions here. In particular, catching WindowsError is a
62 # Do not mask exceptions here. In particular, catching WindowsError is a
63 # problem because that exception is only defined on Windows...
63 # problem because that exception is only defined on Windows...
64 os.makedirs(os.path.join(HOME_TEST_DIR, 'ipython'))
64 os.makedirs(os.path.join(HOME_TEST_DIR, 'ipython'))
65
65
66
66
67 def teardown_module():
67 def teardown_module():
68 """Teardown testenvironment for the module:
68 """Teardown testenvironment for the module:
69
69
70 - Remove dummy home dir tree
70 - Remove dummy home dir tree
71 """
71 """
72 # Note: we remove the parent test dir, which is the root of all test
72 # Note: we remove the parent test dir, which is the root of all test
73 # subdirs we may have created. Use shutil instead of os.removedirs, so
73 # subdirs we may have created. Use shutil instead of os.removedirs, so
74 # that non-empty directories are all recursively removed.
74 # that non-empty directories are all recursively removed.
75 shutil.rmtree(TMP_TEST_DIR)
75 shutil.rmtree(TMP_TEST_DIR)
76
76
77
77
78 def setup_environment():
78 def setup_environment():
79 """Setup testenvironment for some functions that are tested
79 """Setup testenvironment for some functions that are tested
80 in this module. In particular this functions stores attributes
80 in this module. In particular this functions stores attributes
81 and other things that we need to stub in some test functions.
81 and other things that we need to stub in some test functions.
82 This needs to be done on a function level and not module level because
82 This needs to be done on a function level and not module level because
83 each testfunction needs a pristine environment.
83 each testfunction needs a pristine environment.
84 """
84 """
85 global oldstuff, platformstuff
85 global oldstuff, platformstuff
86 oldstuff = (env.copy(), os.name, sys.platform, path.get_home_dir, IPython.__file__, os.getcwd())
86 oldstuff = (env.copy(), os.name, sys.platform, path.get_home_dir, IPython.__file__, os.getcwd())
87
87
88 def teardown_environment():
88 def teardown_environment():
89 """Restore things that were remembered by the setup_environment function
89 """Restore things that were remembered by the setup_environment function
90 """
90 """
91 (oldenv, os.name, sys.platform, path.get_home_dir, IPython.__file__, old_wd) = oldstuff
91 (oldenv, os.name, sys.platform, path.get_home_dir, IPython.__file__, old_wd) = oldstuff
92 os.chdir(old_wd)
92 os.chdir(old_wd)
93 reload(path)
93 reload(path)
94
94
95 for key in list(env):
95 for key in list(env):
96 if key not in oldenv:
96 if key not in oldenv:
97 del env[key]
97 del env[key]
98 env.update(oldenv)
98 env.update(oldenv)
99 if hasattr(sys, 'frozen'):
99 if hasattr(sys, 'frozen'):
100 del sys.frozen
100 del sys.frozen
101
101
102
102
103 # Build decorator that uses the setup_environment/setup_environment
103 # Build decorator that uses the setup_environment/setup_environment
104 @pytest.fixture
104 @pytest.fixture
105 def environment():
105 def environment():
106 setup_environment()
106 setup_environment()
107 yield
107 yield
108 teardown_environment()
108 teardown_environment()
109
109
110
110
111 with_environment = pytest.mark.usefixtures("environment")
111 with_environment = pytest.mark.usefixtures("environment")
112
112
113
113
114 @skip_if_not_win32
114 @skip_if_not_win32
115 @with_environment
115 @with_environment
116 def test_get_home_dir_1():
116 def test_get_home_dir_1():
117 """Testcase for py2exe logic, un-compressed lib
117 """Testcase for py2exe logic, un-compressed lib
118 """
118 """
119 unfrozen = path.get_home_dir()
119 unfrozen = path.get_home_dir()
120 sys.frozen = True
120 sys.frozen = True
121
121
122 #fake filename for IPython.__init__
122 #fake filename for IPython.__init__
123 IPython.__file__ = abspath(join(HOME_TEST_DIR, "Lib/IPython/__init__.py"))
123 IPython.__file__ = abspath(join(HOME_TEST_DIR, "Lib/IPython/__init__.py"))
124
124
125 home_dir = path.get_home_dir()
125 home_dir = path.get_home_dir()
126 assert home_dir == unfrozen
126 assert home_dir == unfrozen
127
127
128
128
129 @skip_if_not_win32
129 @skip_if_not_win32
130 @with_environment
130 @with_environment
131 def test_get_home_dir_2():
131 def test_get_home_dir_2():
132 """Testcase for py2exe logic, compressed lib
132 """Testcase for py2exe logic, compressed lib
133 """
133 """
134 unfrozen = path.get_home_dir()
134 unfrozen = path.get_home_dir()
135 sys.frozen = True
135 sys.frozen = True
136 #fake filename for IPython.__init__
136 #fake filename for IPython.__init__
137 IPython.__file__ = abspath(join(HOME_TEST_DIR, "Library.zip/IPython/__init__.py")).lower()
137 IPython.__file__ = abspath(join(HOME_TEST_DIR, "Library.zip/IPython/__init__.py")).lower()
138
138
139 home_dir = path.get_home_dir(True)
139 home_dir = path.get_home_dir(True)
140 assert home_dir == unfrozen
140 assert home_dir == unfrozen
141
141
142
142
143 @skip_win32
143 @skip_win32
144 @with_environment
144 @with_environment
145 def test_get_home_dir_3():
145 def test_get_home_dir_3():
146 """get_home_dir() uses $HOME if set"""
146 """get_home_dir() uses $HOME if set"""
147 env["HOME"] = HOME_TEST_DIR
147 env["HOME"] = HOME_TEST_DIR
148 home_dir = path.get_home_dir(True)
148 home_dir = path.get_home_dir(True)
149 # get_home_dir expands symlinks
149 # get_home_dir expands symlinks
150 assert home_dir == os.path.realpath(env["HOME"])
150 assert home_dir == os.path.realpath(env["HOME"])
151
151
152
152
153 @with_environment
153 @with_environment
154 def test_get_home_dir_4():
154 def test_get_home_dir_4():
155 """get_home_dir() still works if $HOME is not set"""
155 """get_home_dir() still works if $HOME is not set"""
156
156
157 if 'HOME' in env: del env['HOME']
157 if 'HOME' in env: del env['HOME']
158 # this should still succeed, but we don't care what the answer is
158 # this should still succeed, but we don't care what the answer is
159 home = path.get_home_dir(False)
159 home = path.get_home_dir(False)
160
160
161 @skip_win32
161 @skip_win32
162 @with_environment
162 @with_environment
163 def test_get_home_dir_5():
163 def test_get_home_dir_5():
164 """raise HomeDirError if $HOME is specified, but not a writable dir"""
164 """raise HomeDirError if $HOME is specified, but not a writable dir"""
165 env['HOME'] = abspath(HOME_TEST_DIR+'garbage')
165 env['HOME'] = abspath(HOME_TEST_DIR+'garbage')
166 # set os.name = posix, to prevent My Documents fallback on Windows
166 # set os.name = posix, to prevent My Documents fallback on Windows
167 os.name = 'posix'
167 os.name = 'posix'
168 pytest.raises(path.HomeDirError, path.get_home_dir, True)
168 pytest.raises(path.HomeDirError, path.get_home_dir, True)
169
169
170 # Should we stub wreg fully so we can run the test on all platforms?
170 # Should we stub wreg fully so we can run the test on all platforms?
171 @skip_if_not_win32
171 @skip_if_not_win32
172 @with_environment
172 @with_environment
173 def test_get_home_dir_8():
173 def test_get_home_dir_8():
174 """Using registry hack for 'My Documents', os=='nt'
174 """Using registry hack for 'My Documents', os=='nt'
175
175
176 HOMESHARE, HOMEDRIVE, HOMEPATH, USERPROFILE and others are missing.
176 HOMESHARE, HOMEDRIVE, HOMEPATH, USERPROFILE and others are missing.
177 """
177 """
178 os.name = 'nt'
178 os.name = 'nt'
179 # Remove from stub environment all keys that may be set
179 # Remove from stub environment all keys that may be set
180 for key in ['HOME', 'HOMESHARE', 'HOMEDRIVE', 'HOMEPATH', 'USERPROFILE']:
180 for key in ['HOME', 'HOMESHARE', 'HOMEDRIVE', 'HOMEPATH', 'USERPROFILE']:
181 env.pop(key, None)
181 env.pop(key, None)
182
182
183 class key:
183 class key:
184 def __enter__(self):
184 def __enter__(self):
185 pass
185 pass
186 def Close(self):
186 def Close(self):
187 pass
187 pass
188 def __exit__(*args, **kwargs):
188 def __exit__(*args, **kwargs):
189 pass
189 pass
190
190
191 with patch.object(wreg, 'OpenKey', return_value=key()), \
191 with patch.object(wreg, 'OpenKey', return_value=key()), \
192 patch.object(wreg, 'QueryValueEx', return_value=[abspath(HOME_TEST_DIR)]):
192 patch.object(wreg, 'QueryValueEx', return_value=[abspath(HOME_TEST_DIR)]):
193 home_dir = path.get_home_dir()
193 home_dir = path.get_home_dir()
194 assert home_dir == abspath(HOME_TEST_DIR)
194 assert home_dir == abspath(HOME_TEST_DIR)
195
195
196 @with_environment
196 @with_environment
197 def test_get_xdg_dir_0():
197 def test_get_xdg_dir_0():
198 """test_get_xdg_dir_0, check xdg_dir"""
198 """test_get_xdg_dir_0, check xdg_dir"""
199 reload(path)
199 reload(path)
200 path._writable_dir = lambda path: True
200 path._writable_dir = lambda path: True
201 path.get_home_dir = lambda : 'somewhere'
201 path.get_home_dir = lambda : 'somewhere'
202 os.name = "posix"
202 os.name = "posix"
203 sys.platform = "linux2"
203 sys.platform = "linux2"
204 env.pop('IPYTHON_DIR', None)
204 env.pop('IPYTHON_DIR', None)
205 env.pop('IPYTHONDIR', None)
205 env.pop('IPYTHONDIR', None)
206 env.pop('XDG_CONFIG_HOME', None)
206 env.pop('XDG_CONFIG_HOME', None)
207
207
208 assert path.get_xdg_dir() == os.path.join("somewhere", ".config")
208 assert path.get_xdg_dir() == os.path.join("somewhere", ".config")
209
209
210
210
211 @with_environment
211 @with_environment
212 def test_get_xdg_dir_1():
212 def test_get_xdg_dir_1():
213 """test_get_xdg_dir_1, check nonexistent xdg_dir"""
213 """test_get_xdg_dir_1, check nonexistent xdg_dir"""
214 reload(path)
214 reload(path)
215 path.get_home_dir = lambda : HOME_TEST_DIR
215 path.get_home_dir = lambda : HOME_TEST_DIR
216 os.name = "posix"
216 os.name = "posix"
217 sys.platform = "linux2"
217 sys.platform = "linux2"
218 env.pop('IPYTHON_DIR', None)
218 env.pop('IPYTHON_DIR', None)
219 env.pop('IPYTHONDIR', None)
219 env.pop('IPYTHONDIR', None)
220 env.pop('XDG_CONFIG_HOME', None)
220 env.pop('XDG_CONFIG_HOME', None)
221 assert path.get_xdg_dir() is None
221 assert path.get_xdg_dir() is None
222
222
223 @with_environment
223 @with_environment
224 def test_get_xdg_dir_2():
224 def test_get_xdg_dir_2():
225 """test_get_xdg_dir_2, check xdg_dir default to ~/.config"""
225 """test_get_xdg_dir_2, check xdg_dir default to ~/.config"""
226 reload(path)
226 reload(path)
227 path.get_home_dir = lambda : HOME_TEST_DIR
227 path.get_home_dir = lambda : HOME_TEST_DIR
228 os.name = "posix"
228 os.name = "posix"
229 sys.platform = "linux2"
229 sys.platform = "linux2"
230 env.pop('IPYTHON_DIR', None)
230 env.pop('IPYTHON_DIR', None)
231 env.pop('IPYTHONDIR', None)
231 env.pop('IPYTHONDIR', None)
232 env.pop('XDG_CONFIG_HOME', None)
232 env.pop('XDG_CONFIG_HOME', None)
233 cfgdir=os.path.join(path.get_home_dir(), '.config')
233 cfgdir=os.path.join(path.get_home_dir(), '.config')
234 if not os.path.exists(cfgdir):
234 if not os.path.exists(cfgdir):
235 os.makedirs(cfgdir)
235 os.makedirs(cfgdir)
236
236
237 assert path.get_xdg_dir() == cfgdir
237 assert path.get_xdg_dir() == cfgdir
238
238
239 @with_environment
239 @with_environment
240 def test_get_xdg_dir_3():
240 def test_get_xdg_dir_3():
241 """test_get_xdg_dir_3, check xdg_dir not used on non-posix systems"""
241 """test_get_xdg_dir_3, check xdg_dir not used on non-posix systems"""
242 reload(path)
242 reload(path)
243 path.get_home_dir = lambda : HOME_TEST_DIR
243 path.get_home_dir = lambda : HOME_TEST_DIR
244 os.name = "nt"
244 os.name = "nt"
245 sys.platform = "win32"
245 sys.platform = "win32"
246 env.pop('IPYTHON_DIR', None)
246 env.pop('IPYTHON_DIR', None)
247 env.pop('IPYTHONDIR', None)
247 env.pop('IPYTHONDIR', None)
248 env.pop('XDG_CONFIG_HOME', None)
248 env.pop('XDG_CONFIG_HOME', None)
249 cfgdir=os.path.join(path.get_home_dir(), '.config')
249 cfgdir=os.path.join(path.get_home_dir(), '.config')
250 os.makedirs(cfgdir, exist_ok=True)
250 os.makedirs(cfgdir, exist_ok=True)
251
251
252 assert path.get_xdg_dir() is None
252 assert path.get_xdg_dir() is None
253
253
254 def test_filefind():
254 def test_filefind():
255 """Various tests for filefind"""
255 """Various tests for filefind"""
256 f = tempfile.NamedTemporaryFile()
256 f = tempfile.NamedTemporaryFile()
257 # print 'fname:',f.name
257 # print 'fname:',f.name
258 alt_dirs = paths.get_ipython_dir()
258 alt_dirs = paths.get_ipython_dir()
259 t = path.filefind(f.name, alt_dirs)
259 t = path.filefind(f.name, alt_dirs)
260 # print 'found:',t
260 # print 'found:',t
261
261
262
262
263 @dec.skip_if_not_win32
263 @dec.skip_if_not_win32
264 def test_get_long_path_name_win32():
264 def test_get_long_path_name_win32():
265 with TemporaryDirectory() as tmpdir:
265 with TemporaryDirectory() as tmpdir:
266
266
267 # Make a long path. Expands the path of tmpdir prematurely as it may already have a long
267 # Make a long path. Expands the path of tmpdir prematurely as it may already have a long
268 # path component, so ensure we include the long form of it
268 # path component, so ensure we include the long form of it
269 long_path = os.path.join(path.get_long_path_name(tmpdir), 'this is my long path name')
269 long_path = os.path.join(path.get_long_path_name(tmpdir), 'this is my long path name')
270 os.makedirs(long_path)
270 os.makedirs(long_path)
271
271
272 # Test to see if the short path evaluates correctly.
272 # Test to see if the short path evaluates correctly.
273 short_path = os.path.join(tmpdir, 'THISIS~1')
273 short_path = os.path.join(tmpdir, 'THISIS~1')
274 evaluated_path = path.get_long_path_name(short_path)
274 evaluated_path = path.get_long_path_name(short_path)
275 assert evaluated_path.lower() == long_path.lower()
275 assert evaluated_path.lower() == long_path.lower()
276
276
277
277
278 @dec.skip_win32
278 @dec.skip_win32
279 def test_get_long_path_name():
279 def test_get_long_path_name():
280 p = path.get_long_path_name("/usr/local")
280 p = path.get_long_path_name("/usr/local")
281 assert p == "/usr/local"
281 assert p == "/usr/local"
282
282
283
283
284 class TestRaiseDeprecation(unittest.TestCase):
284 class TestRaiseDeprecation(unittest.TestCase):
285
285
286 @dec.skip_win32 # can't create not-user-writable dir on win
286 @dec.skip_win32 # can't create not-user-writable dir on win
287 @with_environment
287 @with_environment
288 def test_not_writable_ipdir(self):
288 def test_not_writable_ipdir(self):
289 tmpdir = tempfile.mkdtemp()
289 tmpdir = tempfile.mkdtemp()
290 os.name = "posix"
290 os.name = "posix"
291 env.pop('IPYTHON_DIR', None)
291 env.pop('IPYTHON_DIR', None)
292 env.pop('IPYTHONDIR', None)
292 env.pop('IPYTHONDIR', None)
293 env.pop('XDG_CONFIG_HOME', None)
293 env.pop('XDG_CONFIG_HOME', None)
294 env['HOME'] = tmpdir
294 env['HOME'] = tmpdir
295 ipdir = os.path.join(tmpdir, '.ipython')
295 ipdir = os.path.join(tmpdir, '.ipython')
296 os.mkdir(ipdir, 0o555)
296 os.mkdir(ipdir, 0o555)
297 try:
297 try:
298 open(os.path.join(ipdir, "_foo_"), 'w').close()
298 open(os.path.join(ipdir, "_foo_"), 'w').close()
299 except IOError:
299 except IOError:
300 pass
300 pass
301 else:
301 else:
302 # I can still write to an unwritable dir,
302 # I can still write to an unwritable dir,
303 # assume I'm root and skip the test
303 # assume I'm root and skip the test
304 pytest.skip("I can't create directories that I can't write to")
304 pytest.skip("I can't create directories that I can't write to")
305
305
306 with self.assertWarnsRegex(UserWarning, 'is not a writable location'):
306 with self.assertWarnsRegex(UserWarning, 'is not a writable location'):
307 ipdir = paths.get_ipython_dir()
307 ipdir = paths.get_ipython_dir()
308 env.pop('IPYTHON_DIR', None)
308 env.pop('IPYTHON_DIR', None)
309
309
310 @with_environment
310 @with_environment
311 def test_get_py_filename():
311 def test_get_py_filename():
312 os.chdir(TMP_TEST_DIR)
312 os.chdir(TMP_TEST_DIR)
313 with make_tempfile("foo.py"):
313 with make_tempfile("foo.py"):
314 assert path.get_py_filename("foo.py") == "foo.py"
314 assert path.get_py_filename("foo.py") == "foo.py"
315 assert path.get_py_filename("foo") == "foo.py"
315 assert path.get_py_filename("foo") == "foo.py"
316 with make_tempfile("foo"):
316 with make_tempfile("foo"):
317 assert path.get_py_filename("foo") == "foo"
317 assert path.get_py_filename("foo") == "foo"
318 pytest.raises(IOError, path.get_py_filename, "foo.py")
318 pytest.raises(IOError, path.get_py_filename, "foo.py")
319 pytest.raises(IOError, path.get_py_filename, "foo")
319 pytest.raises(IOError, path.get_py_filename, "foo")
320 pytest.raises(IOError, path.get_py_filename, "foo.py")
320 pytest.raises(IOError, path.get_py_filename, "foo.py")
321 true_fn = "foo with spaces.py"
321 true_fn = "foo with spaces.py"
322 with make_tempfile(true_fn):
322 with make_tempfile(true_fn):
323 assert path.get_py_filename("foo with spaces") == true_fn
323 assert path.get_py_filename("foo with spaces") == true_fn
324 assert path.get_py_filename("foo with spaces.py") == true_fn
324 assert path.get_py_filename("foo with spaces.py") == true_fn
325 pytest.raises(IOError, path.get_py_filename, '"foo with spaces.py"')
325 pytest.raises(IOError, path.get_py_filename, '"foo with spaces.py"')
326 pytest.raises(IOError, path.get_py_filename, "'foo with spaces.py'")
326 pytest.raises(IOError, path.get_py_filename, "'foo with spaces.py'")
327
327
328 @onlyif_unicode_paths
328 @onlyif_unicode_paths
329 def test_unicode_in_filename():
329 def test_unicode_in_filename():
330 """When a file doesn't exist, the exception raised should be safe to call
330 """When a file doesn't exist, the exception raised should be safe to call
331 str() on - i.e. in Python 2 it must only have ASCII characters.
331 str() on - i.e. in Python 2 it must only have ASCII characters.
332
332
333 https://github.com/ipython/ipython/issues/875
333 https://github.com/ipython/ipython/issues/875
334 """
334 """
335 try:
335 try:
336 # these calls should not throw unicode encode exceptions
336 # these calls should not throw unicode encode exceptions
337 path.get_py_filename('fooéè.py')
337 path.get_py_filename('fooéè.py')
338 except IOError as ex:
338 except IOError as ex:
339 str(ex)
339 str(ex)
340
340
341
341
342 class TestShellGlob(unittest.TestCase):
342 class TestShellGlob(unittest.TestCase):
343
343
344 @classmethod
344 @classmethod
345 def setUpClass(cls):
345 def setUpClass(cls):
346 cls.filenames_start_with_a = ['a0', 'a1', 'a2']
346 cls.filenames_start_with_a = ['a0', 'a1', 'a2']
347 cls.filenames_end_with_b = ['0b', '1b', '2b']
347 cls.filenames_end_with_b = ['0b', '1b', '2b']
348 cls.filenames = cls.filenames_start_with_a + cls.filenames_end_with_b
348 cls.filenames = cls.filenames_start_with_a + cls.filenames_end_with_b
349 cls.tempdir = TemporaryDirectory()
349 cls.tempdir = TemporaryDirectory()
350 td = cls.tempdir.name
350 td = cls.tempdir.name
351
351
352 with cls.in_tempdir():
352 with cls.in_tempdir():
353 # Create empty files
353 # Create empty files
354 for fname in cls.filenames:
354 for fname in cls.filenames:
355 open(os.path.join(td, fname), 'w').close()
355 open(os.path.join(td, fname), 'w').close()
356
356
357 @classmethod
357 @classmethod
358 def tearDownClass(cls):
358 def tearDownClass(cls):
359 cls.tempdir.cleanup()
359 cls.tempdir.cleanup()
360
360
361 @classmethod
361 @classmethod
362 @contextmanager
362 @contextmanager
363 def in_tempdir(cls):
363 def in_tempdir(cls):
364 save = os.getcwd()
364 save = os.getcwd()
365 try:
365 try:
366 os.chdir(cls.tempdir.name)
366 os.chdir(cls.tempdir.name)
367 yield
367 yield
368 finally:
368 finally:
369 os.chdir(save)
369 os.chdir(save)
370
370
371 def check_match(self, patterns, matches):
371 def check_match(self, patterns, matches):
372 with self.in_tempdir():
372 with self.in_tempdir():
373 # glob returns unordered list. that's why sorted is required.
373 # glob returns unordered list. that's why sorted is required.
374 assert sorted(path.shellglob(patterns)) == sorted(matches)
374 assert sorted(path.shellglob(patterns)) == sorted(matches)
375
375
376 def common_cases(self):
376 def common_cases(self):
377 return [
377 return [
378 (['*'], self.filenames),
378 (['*'], self.filenames),
379 (['a*'], self.filenames_start_with_a),
379 (['a*'], self.filenames_start_with_a),
380 (['*c'], ['*c']),
380 (['*c'], ['*c']),
381 (['*', 'a*', '*b', '*c'], self.filenames
381 (['*', 'a*', '*b', '*c'], self.filenames
382 + self.filenames_start_with_a
382 + self.filenames_start_with_a
383 + self.filenames_end_with_b
383 + self.filenames_end_with_b
384 + ['*c']),
384 + ['*c']),
385 (['a[012]'], self.filenames_start_with_a),
385 (['a[012]'], self.filenames_start_with_a),
386 ]
386 ]
387
387
388 @skip_win32
388 @skip_win32
389 def test_match_posix(self):
389 def test_match_posix(self):
390 for (patterns, matches) in self.common_cases() + [
390 for (patterns, matches) in self.common_cases() + [
391 ([r'\*'], ['*']),
391 ([r'\*'], ['*']),
392 ([r'a\*', 'a*'], ['a*'] + self.filenames_start_with_a),
392 ([r'a\*', 'a*'], ['a*'] + self.filenames_start_with_a),
393 ([r'a\[012]'], ['a[012]']),
393 ([r'a\[012]'], ['a[012]']),
394 ]:
394 ]:
395 self.check_match(patterns, matches)
395 self.check_match(patterns, matches)
396
396
397 @skip_if_not_win32
397 @skip_if_not_win32
398 def test_match_windows(self):
398 def test_match_windows(self):
399 for (patterns, matches) in self.common_cases() + [
399 for (patterns, matches) in self.common_cases() + [
400 # In windows, backslash is interpreted as path
400 # In windows, backslash is interpreted as path
401 # separator. Therefore, you can't escape glob
401 # separator. Therefore, you can't escape glob
402 # using it.
402 # using it.
403 ([r'a\*', 'a*'], [r'a\*'] + self.filenames_start_with_a),
403 ([r'a\*', 'a*'], [r'a\*'] + self.filenames_start_with_a),
404 ([r'a\[012]'], [r'a\[012]']),
404 ([r'a\[012]'], [r'a\[012]']),
405 ]:
405 ]:
406 self.check_match(patterns, matches)
406 self.check_match(patterns, matches)
407
407
408
408
409 # TODO : pytest.mark.parametrise once nose is gone.
409 @pytest.mark.parametrize(
410 def test_unescape_glob():
410 "globstr, unescaped_globstr",
411 assert path.unescape_glob(r"\*\[\!\]\?") == "*[!]?"
411 [
412 assert path.unescape_glob(r"\\*") == r"\*"
412 (r"\*\[\!\]\?", "*[!]?"),
413 assert path.unescape_glob(r"\\\*") == r"\*"
413 (r"\\*", r"\*"),
414 assert path.unescape_glob(r"\\a") == r"\a"
414 (r"\\\*", r"\*"),
415 assert path.unescape_glob(r"\a") == r"\a"
415 (r"\\a", r"\a"),
416 (r"\a", r"\a"),
417 ],
418 )
419 def test_unescape_glob(globstr, unescaped_globstr):
420 assert path.unescape_glob(globstr) == unescaped_globstr
416
421
417
422
418 @onlyif_unicode_paths
423 @onlyif_unicode_paths
419 def test_ensure_dir_exists():
424 def test_ensure_dir_exists():
420 with TemporaryDirectory() as td:
425 with TemporaryDirectory() as td:
421 d = os.path.join(td, 'βˆ‚ir')
426 d = os.path.join(td, 'βˆ‚ir')
422 path.ensure_dir_exists(d) # create it
427 path.ensure_dir_exists(d) # create it
423 assert os.path.isdir(d)
428 assert os.path.isdir(d)
424 path.ensure_dir_exists(d) # no-op
429 path.ensure_dir_exists(d) # no-op
425 f = os.path.join(td, 'Ζ’ile')
430 f = os.path.join(td, 'Ζ’ile')
426 open(f, 'w').close() # touch
431 open(f, 'w').close() # touch
427 with pytest.raises(IOError):
432 with pytest.raises(IOError):
428 path.ensure_dir_exists(f)
433 path.ensure_dir_exists(f)
429
434
430 class TestLinkOrCopy(unittest.TestCase):
435 class TestLinkOrCopy(unittest.TestCase):
431 def setUp(self):
436 def setUp(self):
432 self.tempdir = TemporaryDirectory()
437 self.tempdir = TemporaryDirectory()
433 self.src = self.dst("src")
438 self.src = self.dst("src")
434 with open(self.src, "w") as f:
439 with open(self.src, "w") as f:
435 f.write("Hello, world!")
440 f.write("Hello, world!")
436
441
437 def tearDown(self):
442 def tearDown(self):
438 self.tempdir.cleanup()
443 self.tempdir.cleanup()
439
444
440 def dst(self, *args):
445 def dst(self, *args):
441 return os.path.join(self.tempdir.name, *args)
446 return os.path.join(self.tempdir.name, *args)
442
447
443 def assert_inode_not_equal(self, a, b):
448 def assert_inode_not_equal(self, a, b):
444 assert (
449 assert (
445 os.stat(a).st_ino != os.stat(b).st_ino
450 os.stat(a).st_ino != os.stat(b).st_ino
446 ), "%r and %r do reference the same indoes" % (a, b)
451 ), "%r and %r do reference the same indoes" % (a, b)
447
452
448 def assert_inode_equal(self, a, b):
453 def assert_inode_equal(self, a, b):
449 assert (
454 assert (
450 os.stat(a).st_ino == os.stat(b).st_ino
455 os.stat(a).st_ino == os.stat(b).st_ino
451 ), "%r and %r do not reference the same indoes" % (a, b)
456 ), "%r and %r do not reference the same indoes" % (a, b)
452
457
453 def assert_content_equal(self, a, b):
458 def assert_content_equal(self, a, b):
454 with open(a) as a_f:
459 with open(a) as a_f:
455 with open(b) as b_f:
460 with open(b) as b_f:
456 assert a_f.read() == b_f.read()
461 assert a_f.read() == b_f.read()
457
462
458 @skip_win32
463 @skip_win32
459 def test_link_successful(self):
464 def test_link_successful(self):
460 dst = self.dst("target")
465 dst = self.dst("target")
461 path.link_or_copy(self.src, dst)
466 path.link_or_copy(self.src, dst)
462 self.assert_inode_equal(self.src, dst)
467 self.assert_inode_equal(self.src, dst)
463
468
464 @skip_win32
469 @skip_win32
465 def test_link_into_dir(self):
470 def test_link_into_dir(self):
466 dst = self.dst("some_dir")
471 dst = self.dst("some_dir")
467 os.mkdir(dst)
472 os.mkdir(dst)
468 path.link_or_copy(self.src, dst)
473 path.link_or_copy(self.src, dst)
469 expected_dst = self.dst("some_dir", os.path.basename(self.src))
474 expected_dst = self.dst("some_dir", os.path.basename(self.src))
470 self.assert_inode_equal(self.src, expected_dst)
475 self.assert_inode_equal(self.src, expected_dst)
471
476
472 @skip_win32
477 @skip_win32
473 def test_target_exists(self):
478 def test_target_exists(self):
474 dst = self.dst("target")
479 dst = self.dst("target")
475 open(dst, "w").close()
480 open(dst, "w").close()
476 path.link_or_copy(self.src, dst)
481 path.link_or_copy(self.src, dst)
477 self.assert_inode_equal(self.src, dst)
482 self.assert_inode_equal(self.src, dst)
478
483
479 @skip_win32
484 @skip_win32
480 def test_no_link(self):
485 def test_no_link(self):
481 real_link = os.link
486 real_link = os.link
482 try:
487 try:
483 del os.link
488 del os.link
484 dst = self.dst("target")
489 dst = self.dst("target")
485 path.link_or_copy(self.src, dst)
490 path.link_or_copy(self.src, dst)
486 self.assert_content_equal(self.src, dst)
491 self.assert_content_equal(self.src, dst)
487 self.assert_inode_not_equal(self.src, dst)
492 self.assert_inode_not_equal(self.src, dst)
488 finally:
493 finally:
489 os.link = real_link
494 os.link = real_link
490
495
491 @skip_if_not_win32
496 @skip_if_not_win32
492 def test_windows(self):
497 def test_windows(self):
493 dst = self.dst("target")
498 dst = self.dst("target")
494 path.link_or_copy(self.src, dst)
499 path.link_or_copy(self.src, dst)
495 self.assert_content_equal(self.src, dst)
500 self.assert_content_equal(self.src, dst)
496
501
497 def test_link_twice(self):
502 def test_link_twice(self):
498 # Linking the same file twice shouldn't leave duplicates around.
503 # Linking the same file twice shouldn't leave duplicates around.
499 # See https://github.com/ipython/ipython/issues/6450
504 # See https://github.com/ipython/ipython/issues/6450
500 dst = self.dst('target')
505 dst = self.dst('target')
501 path.link_or_copy(self.src, dst)
506 path.link_or_copy(self.src, dst)
502 path.link_or_copy(self.src, dst)
507 path.link_or_copy(self.src, dst)
503 self.assert_inode_equal(self.src, dst)
508 self.assert_inode_equal(self.src, dst)
504 assert sorted(os.listdir(self.tempdir.name)) == ["src", "target"]
509 assert sorted(os.listdir(self.tempdir.name)) == ["src", "target"]
@@ -1,187 +1,188 b''
1 # encoding: utf-8
2 """
1 """
3 Tests for platutils.py
2 Tests for platutils.py
4 """
3 """
5
4
6 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
7 # Copyright (C) 2008-2011 The IPython Development Team
6 # Copyright (C) 2008-2011 The IPython Development Team
8 #
7 #
9 # 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
10 # the file COPYING, distributed as part of this software.
9 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
12
11
13 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
14 # Imports
13 # Imports
15 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
16
15
17 import sys
16 import sys
18 import signal
17 import signal
19 import os
18 import os
20 import time
19 import time
21 from _thread import interrupt_main # Py 3
20 from _thread import interrupt_main # Py 3
22 import threading
21 import threading
23
22
24 import pytest
23 import pytest
25
24
26 from IPython.utils.process import (find_cmd, FindCmdError, arg_split,
25 from IPython.utils.process import (find_cmd, FindCmdError, arg_split,
27 system, getoutput, getoutputerror,
26 system, getoutput, getoutputerror,
28 get_output_error_code)
27 get_output_error_code)
29 from IPython.utils.capture import capture_output
28 from IPython.utils.capture import capture_output
30 from IPython.testing import decorators as dec
29 from IPython.testing import decorators as dec
31 from IPython.testing import tools as tt
30 from IPython.testing import tools as tt
32
31
33 python = os.path.basename(sys.executable)
32 python = os.path.basename(sys.executable)
34
33
35 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
36 # Tests
35 # Tests
37 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
38
37
39
38
40 @dec.skip_win32
39 @dec.skip_win32
41 def test_find_cmd_ls():
40 def test_find_cmd_ls():
42 """Make sure we can find the full path to ls."""
41 """Make sure we can find the full path to ls."""
43 path = find_cmd("ls")
42 path = find_cmd("ls")
44 assert path.endswith("ls")
43 assert path.endswith("ls")
45
44
46
45
47 @dec.skip_if_not_win32
46 @dec.skip_if_not_win32
48 def test_find_cmd_pythonw():
47 def test_find_cmd_pythonw():
49 """Try to find pythonw on Windows."""
48 """Try to find pythonw on Windows."""
50 path = find_cmd('pythonw')
49 path = find_cmd('pythonw')
51 assert path.lower().endswith('pythonw.exe'), path
50 assert path.lower().endswith('pythonw.exe'), path
52
51
53
52
54 def test_find_cmd_fail():
53 def test_find_cmd_fail():
55 """Make sure that FindCmdError is raised if we can't find the cmd."""
54 """Make sure that FindCmdError is raised if we can't find the cmd."""
56 pytest.raises(FindCmdError, find_cmd, "asdfasdf")
55 pytest.raises(FindCmdError, find_cmd, "asdfasdf")
57
56
58
57
59 # TODO: move to pytest.mark.parametrize once nose gone
60 @dec.skip_win32
58 @dec.skip_win32
61 def test_arg_split():
59 @pytest.mark.parametrize(
60 "argstr, argv",
61 [
62 ("hi", ["hi"]),
63 ("hello there", ["hello", "there"]),
64 # \u01ce == \N{LATIN SMALL LETTER A WITH CARON}
65 # Do not use \N because the tests crash with syntax error in
66 # some cases, for example windows python2.6.
67 ("h\u01cello", ["h\u01cello"]),
68 ('something "with quotes"', ["something", '"with quotes"']),
69 ],
70 )
71 def test_arg_split(argstr, argv):
62 """Ensure that argument lines are correctly split like in a shell."""
72 """Ensure that argument lines are correctly split like in a shell."""
63 tests = [['hi', ['hi']],
73 assert arg_split(argstr) == argv
64 [u'hi', [u'hi']],
74
65 ['hello there', ['hello', 'there']],
75
66 # \u01ce == \N{LATIN SMALL LETTER A WITH CARON}
67 # Do not use \N because the tests crash with syntax error in
68 # some cases, for example windows python2.6.
69 [u'h\u01cello', [u'h\u01cello']],
70 ['something "with quotes"', ['something', '"with quotes"']],
71 ]
72 for argstr, argv in tests:
73 assert arg_split(argstr) == argv
74
75
76 # TODO: move to pytest.mark.parametrize once nose gone
77 @dec.skip_if_not_win32
76 @dec.skip_if_not_win32
78 def test_arg_split_win32():
77 @pytest.mark.parametrize(
78 "argstr,argv",
79 [
80 ("hi", ["hi"]),
81 ("hello there", ["hello", "there"]),
82 ("h\u01cello", ["h\u01cello"]),
83 ('something "with quotes"', ["something", "with quotes"]),
84 ],
85 )
86 def test_arg_split_win32(argstr, argv):
79 """Ensure that argument lines are correctly split like in a shell."""
87 """Ensure that argument lines are correctly split like in a shell."""
80 tests = [['hi', ['hi']],
88 assert arg_split(argstr) == argv
81 [u'hi', [u'hi']],
82 ['hello there', ['hello', 'there']],
83 [u'h\u01cello', [u'h\u01cello']],
84 ['something "with quotes"', ['something', 'with quotes']],
85 ]
86 for argstr, argv in tests:
87 assert arg_split(argstr) == argv
88
89
89
90
90 class SubProcessTestCase(tt.TempFileMixin):
91 class SubProcessTestCase(tt.TempFileMixin):
91 def setUp(self):
92 def setUp(self):
92 """Make a valid python temp file."""
93 """Make a valid python temp file."""
93 lines = [ "import sys",
94 lines = [ "import sys",
94 "print('on stdout', end='', file=sys.stdout)",
95 "print('on stdout', end='', file=sys.stdout)",
95 "print('on stderr', end='', file=sys.stderr)",
96 "print('on stderr', end='', file=sys.stderr)",
96 "sys.stdout.flush()",
97 "sys.stdout.flush()",
97 "sys.stderr.flush()"]
98 "sys.stderr.flush()"]
98 self.mktmp('\n'.join(lines))
99 self.mktmp('\n'.join(lines))
99
100
100 def test_system(self):
101 def test_system(self):
101 status = system('%s "%s"' % (python, self.fname))
102 status = system(f'{python} "{self.fname}"')
102 self.assertEqual(status, 0)
103 self.assertEqual(status, 0)
103
104
104 def test_system_quotes(self):
105 def test_system_quotes(self):
105 status = system('%s -c "import sys"' % python)
106 status = system('%s -c "import sys"' % python)
106 self.assertEqual(status, 0)
107 self.assertEqual(status, 0)
107
108
108 def assert_interrupts(self, command):
109 def assert_interrupts(self, command):
109 """
110 """
110 Interrupt a subprocess after a second.
111 Interrupt a subprocess after a second.
111 """
112 """
112 if threading.main_thread() != threading.current_thread():
113 if threading.main_thread() != threading.current_thread():
113 raise pytest.skip("Can't run this test if not in main thread.")
114 raise pytest.skip("Can't run this test if not in main thread.")
114
115
115 # Some tests can overwrite SIGINT handler (by using pdb for example),
116 # Some tests can overwrite SIGINT handler (by using pdb for example),
116 # which then breaks this test, so just make sure it's operating
117 # which then breaks this test, so just make sure it's operating
117 # normally.
118 # normally.
118 signal.signal(signal.SIGINT, signal.default_int_handler)
119 signal.signal(signal.SIGINT, signal.default_int_handler)
119
120
120 def interrupt():
121 def interrupt():
121 # Wait for subprocess to start:
122 # Wait for subprocess to start:
122 time.sleep(0.5)
123 time.sleep(0.5)
123 interrupt_main()
124 interrupt_main()
124
125
125 threading.Thread(target=interrupt).start()
126 threading.Thread(target=interrupt).start()
126 start = time.time()
127 start = time.time()
127 try:
128 try:
128 result = command()
129 result = command()
129 except KeyboardInterrupt:
130 except KeyboardInterrupt:
130 # Success!
131 # Success!
131 pass
132 pass
132 end = time.time()
133 end = time.time()
133 self.assertTrue(
134 self.assertTrue(
134 end - start < 2, "Process didn't die quickly: %s" % (end - start)
135 end - start < 2, "Process didn't die quickly: %s" % (end - start)
135 )
136 )
136 return result
137 return result
137
138
138 def test_system_interrupt(self):
139 def test_system_interrupt(self):
139 """
140 """
140 When interrupted in the way ipykernel interrupts IPython, the
141 When interrupted in the way ipykernel interrupts IPython, the
141 subprocess is interrupted.
142 subprocess is interrupted.
142 """
143 """
143 def command():
144 def command():
144 return system('%s -c "import time; time.sleep(5)"' % python)
145 return system('%s -c "import time; time.sleep(5)"' % python)
145
146
146 status = self.assert_interrupts(command)
147 status = self.assert_interrupts(command)
147 self.assertNotEqual(
148 self.assertNotEqual(
148 status, 0, "The process wasn't interrupted. Status: %s" % (status,)
149 status, 0, f"The process wasn't interrupted. Status: {status}"
149 )
150 )
150
151
151 def test_getoutput(self):
152 def test_getoutput(self):
152 out = getoutput('%s "%s"' % (python, self.fname))
153 out = getoutput(f'{python} "{self.fname}"')
153 # we can't rely on the order the line buffered streams are flushed
154 # we can't rely on the order the line buffered streams are flushed
154 try:
155 try:
155 self.assertEqual(out, 'on stderron stdout')
156 self.assertEqual(out, 'on stderron stdout')
156 except AssertionError:
157 except AssertionError:
157 self.assertEqual(out, 'on stdouton stderr')
158 self.assertEqual(out, 'on stdouton stderr')
158
159
159 def test_getoutput_quoted(self):
160 def test_getoutput_quoted(self):
160 out = getoutput('%s -c "print (1)"' % python)
161 out = getoutput('%s -c "print (1)"' % python)
161 self.assertEqual(out.strip(), '1')
162 self.assertEqual(out.strip(), '1')
162
163
163 #Invalid quoting on windows
164 #Invalid quoting on windows
164 @dec.skip_win32
165 @dec.skip_win32
165 def test_getoutput_quoted2(self):
166 def test_getoutput_quoted2(self):
166 out = getoutput("%s -c 'print (1)'" % python)
167 out = getoutput("%s -c 'print (1)'" % python)
167 self.assertEqual(out.strip(), '1')
168 self.assertEqual(out.strip(), '1')
168 out = getoutput("%s -c 'print (\"1\")'" % python)
169 out = getoutput("%s -c 'print (\"1\")'" % python)
169 self.assertEqual(out.strip(), '1')
170 self.assertEqual(out.strip(), '1')
170
171
171 def test_getoutput_error(self):
172 def test_getoutput_error(self):
172 out, err = getoutputerror('%s "%s"' % (python, self.fname))
173 out, err = getoutputerror(f'{python} "{self.fname}"')
173 self.assertEqual(out, 'on stdout')
174 self.assertEqual(out, 'on stdout')
174 self.assertEqual(err, 'on stderr')
175 self.assertEqual(err, 'on stderr')
175
176
176 def test_get_output_error_code(self):
177 def test_get_output_error_code(self):
177 quiet_exit = '%s -c "import sys; sys.exit(1)"' % python
178 quiet_exit = '%s -c "import sys; sys.exit(1)"' % python
178 out, err, code = get_output_error_code(quiet_exit)
179 out, err, code = get_output_error_code(quiet_exit)
179 self.assertEqual(out, '')
180 self.assertEqual(out, '')
180 self.assertEqual(err, '')
181 self.assertEqual(err, '')
181 self.assertEqual(code, 1)
182 self.assertEqual(code, 1)
182 out, err, code = get_output_error_code('%s "%s"' % (python, self.fname))
183 out, err, code = get_output_error_code(f'{python} "{self.fname}"')
183 self.assertEqual(out, 'on stdout')
184 self.assertEqual(out, 'on stdout')
184 self.assertEqual(err, 'on stderr')
185 self.assertEqual(err, 'on stderr')
185 self.assertEqual(code, 0)
186 self.assertEqual(code, 0)
186
187
187
188
@@ -1,210 +1,208 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """Tests for IPython.utils.text"""
2 """Tests for IPython.utils.text"""
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2011 The IPython Development Team
5 # Copyright (C) 2011 The IPython Development Team
6 #
6 #
7 # Distributed under the terms of the BSD License. The full license is in
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Imports
12 # Imports
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 import os
15 import os
16 import math
16 import math
17 import random
17 import random
18 import sys
18 import sys
19
19
20 from pathlib import Path
20 from pathlib import Path
21
21
22 import pytest
22 import pytest
23
23
24 from IPython.utils import text
24 from IPython.utils import text
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Globals
27 # Globals
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29
29
30 def test_columnize():
30 def test_columnize():
31 """Basic columnize tests."""
31 """Basic columnize tests."""
32 size = 5
32 size = 5
33 items = [l*size for l in 'abcd']
33 items = [l*size for l in 'abcd']
34
34
35 out = text.columnize(items, displaywidth=80)
35 out = text.columnize(items, displaywidth=80)
36 assert out == "aaaaa bbbbb ccccc ddddd\n"
36 assert out == "aaaaa bbbbb ccccc ddddd\n"
37 out = text.columnize(items, displaywidth=25)
37 out = text.columnize(items, displaywidth=25)
38 assert out == "aaaaa ccccc\nbbbbb ddddd\n"
38 assert out == "aaaaa ccccc\nbbbbb ddddd\n"
39 out = text.columnize(items, displaywidth=12)
39 out = text.columnize(items, displaywidth=12)
40 assert out == "aaaaa ccccc\nbbbbb ddddd\n"
40 assert out == "aaaaa ccccc\nbbbbb ddddd\n"
41 out = text.columnize(items, displaywidth=10)
41 out = text.columnize(items, displaywidth=10)
42 assert out == "aaaaa\nbbbbb\nccccc\nddddd\n"
42 assert out == "aaaaa\nbbbbb\nccccc\nddddd\n"
43
43
44 out = text.columnize(items, row_first=True, displaywidth=80)
44 out = text.columnize(items, row_first=True, displaywidth=80)
45 assert out == "aaaaa bbbbb ccccc ddddd\n"
45 assert out == "aaaaa bbbbb ccccc ddddd\n"
46 out = text.columnize(items, row_first=True, displaywidth=25)
46 out = text.columnize(items, row_first=True, displaywidth=25)
47 assert out == "aaaaa bbbbb\nccccc ddddd\n"
47 assert out == "aaaaa bbbbb\nccccc ddddd\n"
48 out = text.columnize(items, row_first=True, displaywidth=12)
48 out = text.columnize(items, row_first=True, displaywidth=12)
49 assert out == "aaaaa bbbbb\nccccc ddddd\n"
49 assert out == "aaaaa bbbbb\nccccc ddddd\n"
50 out = text.columnize(items, row_first=True, displaywidth=10)
50 out = text.columnize(items, row_first=True, displaywidth=10)
51 assert out == "aaaaa\nbbbbb\nccccc\nddddd\n"
51 assert out == "aaaaa\nbbbbb\nccccc\nddddd\n"
52
52
53 out = text.columnize(items, displaywidth=40, spread=True)
53 out = text.columnize(items, displaywidth=40, spread=True)
54 assert out == "aaaaa bbbbb ccccc ddddd\n"
54 assert out == "aaaaa bbbbb ccccc ddddd\n"
55 out = text.columnize(items, displaywidth=20, spread=True)
55 out = text.columnize(items, displaywidth=20, spread=True)
56 assert out == "aaaaa ccccc\nbbbbb ddddd\n"
56 assert out == "aaaaa ccccc\nbbbbb ddddd\n"
57 out = text.columnize(items, displaywidth=12, spread=True)
57 out = text.columnize(items, displaywidth=12, spread=True)
58 assert out == "aaaaa ccccc\nbbbbb ddddd\n"
58 assert out == "aaaaa ccccc\nbbbbb ddddd\n"
59 out = text.columnize(items, displaywidth=10, spread=True)
59 out = text.columnize(items, displaywidth=10, spread=True)
60 assert out == "aaaaa\nbbbbb\nccccc\nddddd\n"
60 assert out == "aaaaa\nbbbbb\nccccc\nddddd\n"
61
61
62
62
63 def test_columnize_random():
63 def test_columnize_random():
64 """Test with random input to hopefully catch edge case """
64 """Test with random input to hopefully catch edge case """
65 for row_first in [True, False]:
65 for row_first in [True, False]:
66 for nitems in [random.randint(2,70) for i in range(2,20)]:
66 for nitems in [random.randint(2,70) for i in range(2,20)]:
67 displaywidth = random.randint(20,200)
67 displaywidth = random.randint(20,200)
68 rand_len = [random.randint(2,displaywidth) for i in range(nitems)]
68 rand_len = [random.randint(2,displaywidth) for i in range(nitems)]
69 items = ['x'*l for l in rand_len]
69 items = ['x'*l for l in rand_len]
70 out = text.columnize(items, row_first=row_first, displaywidth=displaywidth)
70 out = text.columnize(items, row_first=row_first, displaywidth=displaywidth)
71 longer_line = max([len(x) for x in out.split('\n')])
71 longer_line = max([len(x) for x in out.split('\n')])
72 longer_element = max(rand_len)
72 longer_element = max(rand_len)
73 assert longer_line <= displaywidth, (
73 assert longer_line <= displaywidth, (
74 f"Columnize displayed something lager than displaywidth : {longer_line}\n"
74 f"Columnize displayed something lager than displaywidth : {longer_line}\n"
75 f"longer element : {longer_element}\n"
75 f"longer element : {longer_element}\n"
76 f"displaywidth : {displaywidth}\n"
76 f"displaywidth : {displaywidth}\n"
77 f"number of element : {nitems}\n"
77 f"number of element : {nitems}\n"
78 f"size of each element : {rand_len}\n"
78 f"size of each element : {rand_len}\n"
79 f"row_first={row_first}\n"
79 f"row_first={row_first}\n"
80 )
80 )
81
81
82
82
83 # TODO: pytest mark.parametrize once nose removed.
83 @pytest.mark.parametrize("row_first", [True, False])
84 def test_columnize_medium():
84 def test_columnize_medium(row_first):
85 """Test with inputs than shouldn't be wider than 80"""
85 """Test with inputs than shouldn't be wider than 80"""
86 size = 40
86 size = 40
87 items = [l*size for l in 'abc']
87 items = [l*size for l in 'abc']
88 for row_first in [True, False]:
88 out = text.columnize(items, row_first=row_first, displaywidth=80)
89 out = text.columnize(items, row_first=row_first, displaywidth=80)
89 assert out == "\n".join(items + [""]), "row_first={0}".format(row_first)
90 assert out == "\n".join(items + [""]), "row_first={0}".format(row_first)
91
90
92
91
93 # TODO: pytest mark.parametrize once nose removed.
92 @pytest.mark.parametrize("row_first", [True, False])
94 def test_columnize_long():
93 def test_columnize_long(row_first):
95 """Test columnize with inputs longer than the display window"""
94 """Test columnize with inputs longer than the display window"""
96 size = 11
95 size = 11
97 items = [l*size for l in 'abc']
96 items = [l*size for l in 'abc']
98 for row_first in [True, False]:
97 out = text.columnize(items, row_first=row_first, displaywidth=size - 1)
99 out = text.columnize(items, row_first=row_first, displaywidth=size - 1)
98 assert out == "\n".join(items + [""]), "row_first={0}".format(row_first)
100 assert out == "\n".join(items + [""]), "row_first={0}".format(row_first)
101
99
102
100
103 def eval_formatter_check(f):
101 def eval_formatter_check(f):
104 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os, u=u"cafΓ©", b="cafΓ©")
102 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os, u=u"cafΓ©", b="cafΓ©")
105 s = f.format("{n} {n//4} {stuff.split()[0]}", **ns)
103 s = f.format("{n} {n//4} {stuff.split()[0]}", **ns)
106 assert s == "12 3 hello"
104 assert s == "12 3 hello"
107 s = f.format(" ".join(["{n//%i}" % i for i in range(1, 8)]), **ns)
105 s = f.format(" ".join(["{n//%i}" % i for i in range(1, 8)]), **ns)
108 assert s == "12 6 4 3 2 2 1"
106 assert s == "12 6 4 3 2 2 1"
109 s = f.format("{[n//i for i in range(1,8)]}", **ns)
107 s = f.format("{[n//i for i in range(1,8)]}", **ns)
110 assert s == "[12, 6, 4, 3, 2, 2, 1]"
108 assert s == "[12, 6, 4, 3, 2, 2, 1]"
111 s = f.format("{stuff!s}", **ns)
109 s = f.format("{stuff!s}", **ns)
112 assert s == ns["stuff"]
110 assert s == ns["stuff"]
113 s = f.format("{stuff!r}", **ns)
111 s = f.format("{stuff!r}", **ns)
114 assert s == repr(ns["stuff"])
112 assert s == repr(ns["stuff"])
115
113
116 # Check with unicode:
114 # Check with unicode:
117 s = f.format("{u}", **ns)
115 s = f.format("{u}", **ns)
118 assert s == ns["u"]
116 assert s == ns["u"]
119 # This decodes in a platform dependent manner, but it shouldn't error out
117 # This decodes in a platform dependent manner, but it shouldn't error out
120 s = f.format("{b}", **ns)
118 s = f.format("{b}", **ns)
121
119
122 pytest.raises(NameError, f.format, "{dne}", **ns)
120 pytest.raises(NameError, f.format, "{dne}", **ns)
123
121
124
122
125 def eval_formatter_slicing_check(f):
123 def eval_formatter_slicing_check(f):
126 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
124 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
127 s = f.format(" {stuff.split()[:]} ", **ns)
125 s = f.format(" {stuff.split()[:]} ", **ns)
128 assert s == " ['hello', 'there'] "
126 assert s == " ['hello', 'there'] "
129 s = f.format(" {stuff.split()[::-1]} ", **ns)
127 s = f.format(" {stuff.split()[::-1]} ", **ns)
130 assert s == " ['there', 'hello'] "
128 assert s == " ['there', 'hello'] "
131 s = f.format("{stuff[::2]}", **ns)
129 s = f.format("{stuff[::2]}", **ns)
132 assert s == ns["stuff"][::2]
130 assert s == ns["stuff"][::2]
133
131
134 pytest.raises(SyntaxError, f.format, "{n:x}", **ns)
132 pytest.raises(SyntaxError, f.format, "{n:x}", **ns)
135
133
136 def eval_formatter_no_slicing_check(f):
134 def eval_formatter_no_slicing_check(f):
137 ns = dict(n=12, pi=math.pi, stuff="hello there", os=os)
135 ns = dict(n=12, pi=math.pi, stuff="hello there", os=os)
138
136
139 s = f.format("{n:x} {pi**2:+f}", **ns)
137 s = f.format("{n:x} {pi**2:+f}", **ns)
140 assert s == "c +9.869604"
138 assert s == "c +9.869604"
141
139
142 s = f.format("{stuff[slice(1,4)]}", **ns)
140 s = f.format("{stuff[slice(1,4)]}", **ns)
143 assert s == "ell"
141 assert s == "ell"
144
142
145 s = f.format("{a[:]}", a=[1, 2])
143 s = f.format("{a[:]}", a=[1, 2])
146 assert s == "[1, 2]"
144 assert s == "[1, 2]"
147
145
148 def test_eval_formatter():
146 def test_eval_formatter():
149 f = text.EvalFormatter()
147 f = text.EvalFormatter()
150 eval_formatter_check(f)
148 eval_formatter_check(f)
151 eval_formatter_no_slicing_check(f)
149 eval_formatter_no_slicing_check(f)
152
150
153 def test_full_eval_formatter():
151 def test_full_eval_formatter():
154 f = text.FullEvalFormatter()
152 f = text.FullEvalFormatter()
155 eval_formatter_check(f)
153 eval_formatter_check(f)
156 eval_formatter_slicing_check(f)
154 eval_formatter_slicing_check(f)
157
155
158 def test_dollar_formatter():
156 def test_dollar_formatter():
159 f = text.DollarFormatter()
157 f = text.DollarFormatter()
160 eval_formatter_check(f)
158 eval_formatter_check(f)
161 eval_formatter_slicing_check(f)
159 eval_formatter_slicing_check(f)
162
160
163 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
161 ns = dict(n=12, pi=math.pi, stuff='hello there', os=os)
164 s = f.format("$n", **ns)
162 s = f.format("$n", **ns)
165 assert s == "12"
163 assert s == "12"
166 s = f.format("$n.real", **ns)
164 s = f.format("$n.real", **ns)
167 assert s == "12"
165 assert s == "12"
168 s = f.format("$n/{stuff[:5]}", **ns)
166 s = f.format("$n/{stuff[:5]}", **ns)
169 assert s == "12/hello"
167 assert s == "12/hello"
170 s = f.format("$n $$HOME", **ns)
168 s = f.format("$n $$HOME", **ns)
171 assert s == "12 $HOME"
169 assert s == "12 $HOME"
172 s = f.format("${foo}", foo="HOME")
170 s = f.format("${foo}", foo="HOME")
173 assert s == "$HOME"
171 assert s == "$HOME"
174
172
175
173
176 def test_strip_email():
174 def test_strip_email():
177 src = """\
175 src = """\
178 >> >>> def f(x):
176 >> >>> def f(x):
179 >> ... return x+1
177 >> ... return x+1
180 >> ...
178 >> ...
181 >> >>> zz = f(2.5)"""
179 >> >>> zz = f(2.5)"""
182 cln = """\
180 cln = """\
183 >>> def f(x):
181 >>> def f(x):
184 ... return x+1
182 ... return x+1
185 ...
183 ...
186 >>> zz = f(2.5)"""
184 >>> zz = f(2.5)"""
187 assert text.strip_email_quotes(src) == cln
185 assert text.strip_email_quotes(src) == cln
188
186
189
187
190 def test_strip_email2():
188 def test_strip_email2():
191 src = "> > > list()"
189 src = "> > > list()"
192 cln = "list()"
190 cln = "list()"
193 assert text.strip_email_quotes(src) == cln
191 assert text.strip_email_quotes(src) == cln
194
192
195
193
196 def test_LSString():
194 def test_LSString():
197 lss = text.LSString("abc\ndef")
195 lss = text.LSString("abc\ndef")
198 assert lss.l == ["abc", "def"]
196 assert lss.l == ["abc", "def"]
199 assert lss.s == "abc def"
197 assert lss.s == "abc def"
200 lss = text.LSString(os.getcwd())
198 lss = text.LSString(os.getcwd())
201 assert isinstance(lss.p[0], Path)
199 assert isinstance(lss.p[0], Path)
202
200
203
201
204 def test_SList():
202 def test_SList():
205 sl = text.SList(["a 11", "b 1", "a 2"])
203 sl = text.SList(["a 11", "b 1", "a 2"])
206 assert sl.n == "a 11\nb 1\na 2"
204 assert sl.n == "a 11\nb 1\na 2"
207 assert sl.s == "a 11 b 1 a 2"
205 assert sl.s == "a 11 b 1 a 2"
208 assert sl.grep(lambda x: x.startswith("a")) == text.SList(["a 11", "a 2"])
206 assert sl.grep(lambda x: x.startswith("a")) == text.SList(["a 11", "a 2"])
209 assert sl.fields(0) == text.SList(["a", "b", "a"])
207 assert sl.fields(0) == text.SList(["a", "b", "a"])
210 assert sl.sort(field=1, nums=True) == text.SList(["b 1", "a 2", "a 11"])
208 assert sl.sort(field=1, nums=True) == text.SList(["b 1", "a 2", "a 11"])
@@ -1,46 +1,47 b''
1 include README.rst
1 include README.rst
2 include COPYING.rst
2 include COPYING.rst
3 include LICENSE
3 include LICENSE
4 include setupbase.py
4 include setupbase.py
5 include MANIFEST.in
5 include MANIFEST.in
6 include pytest.ini
6 include pytest.ini
7 include mypy.ini
7 include mypy.ini
8 include .mailmap
8 include .mailmap
9 include .flake8
9 include .flake8
10 include .pre-commit-config.yaml
10 include .pre-commit-config.yaml
11 include long_description.rst
11 include long_description.rst
12
12
13 recursive-exclude tools *
13 recursive-exclude tools *
14 exclude tools
14 exclude tools
15 exclude CONTRIBUTING.md
15 exclude CONTRIBUTING.md
16 exclude .editorconfig
16 exclude .editorconfig
17 exclude SECURITY.md
17
18
18 graft scripts
19 graft scripts
19
20
20 # Load main dir but exclude things we don't want in the distro
21 # Load main dir but exclude things we don't want in the distro
21 graft IPython
22 graft IPython
22
23
23 # Documentation
24 # Documentation
24 graft docs
25 graft docs
25 exclude docs/\#*
26 exclude docs/\#*
26 exclude docs/man/*.1.gz
27 exclude docs/man/*.1.gz
27
28
28 exclude .git-blame-ignore-revs
29 exclude .git-blame-ignore-revs
29
30
30 # Examples
31 # Examples
31 graft examples
32 graft examples
32
33
33 # docs subdirs we want to skip
34 # docs subdirs we want to skip
34 prune docs/build
35 prune docs/build
35 prune docs/gh-pages
36 prune docs/gh-pages
36 prune docs/dist
37 prune docs/dist
37
38
38 # Patterns to exclude from any directory
39 # Patterns to exclude from any directory
39 global-exclude *~
40 global-exclude *~
40 global-exclude *.flc
41 global-exclude *.flc
41 global-exclude *.yml
42 global-exclude *.yml
42 global-exclude *.pyc
43 global-exclude *.pyc
43 global-exclude *.pyo
44 global-exclude *.pyo
44 global-exclude .dircopy.log
45 global-exclude .dircopy.log
45 global-exclude .git
46 global-exclude .git
46 global-exclude .ipynb_checkpoints
47 global-exclude .ipynb_checkpoints
@@ -1,128 +1,128 b''
1 .. _config_overview:
1 .. _config_overview:
2
2
3 ============================================
3 ============================================
4 Overview of the IPython configuration system
4 Overview of the IPython configuration system
5 ============================================
5 ============================================
6
6
7 This section describes the IPython configuration system. This is based on
7 This section describes the IPython configuration system. This is based on
8 :mod:`traitlets.config`; see that documentation for more information
8 :mod:`traitlets.config`; see that documentation for more information
9 about the overall architecture.
9 about the overall architecture.
10
10
11 Configuration file location
11 Configuration file location
12 ===========================
12 ===========================
13
13
14 So where should you put your configuration files? IPython uses "profiles" for
14 So where should you put your configuration files? IPython uses "profiles" for
15 configuration, and by default, all profiles will be stored in the so called
15 configuration, and by default, all profiles will be stored in the so called
16 "IPython directory". The location of this directory is determined by the
16 "IPython directory". The location of this directory is determined by the
17 following algorithm:
17 following algorithm:
18
18
19 * If the ``ipython-dir`` command line flag is given, its value is used.
19 * If the ``ipython-dir`` command line flag is given, its value is used.
20
20
21 * If not, the value returned by :func:`IPython.paths.get_ipython_dir`
21 * If not, the value returned by :func:`IPython.paths.get_ipython_dir`
22 is used. This function will first look at the :envvar:`IPYTHONDIR`
22 is used. This function will first look at the :envvar:`IPYTHONDIR`
23 environment variable and then default to :file:`~/.ipython`.
23 environment variable and then default to :file:`~/.ipython`.
24 Historical support for the :envvar:`IPYTHON_DIR` environment variable will
24 Historical support for the :envvar:`IPYTHON_DIR` environment variable will
25 be removed in a future release.
25 be removed in a future release.
26
26
27 For most users, the configuration directory will be :file:`~/.ipython`.
27 For most users, the configuration directory will be :file:`~/.ipython`.
28
28
29 Previous versions of IPython on Linux would use the XDG config directory,
29 Previous versions of IPython on Linux would use the XDG config directory,
30 creating :file:`~/.config/ipython` by default. We have decided to go
30 creating :file:`~/.config/ipython` by default. We have decided to go
31 back to :file:`~/.ipython` for consistency among systems. IPython will
31 back to :file:`~/.ipython` for consistency among systems. IPython will
32 issue a warning if it finds the XDG location, and will move it to the new
32 issue a warning if it finds the XDG location, and will move it to the new
33 location if there isn't already a directory there.
33 location if there isn't already a directory there.
34
34
35 Once the location of the IPython directory has been determined, you need to know
35 Once the location of the IPython directory has been determined, you need to know
36 which profile you are using. For users with a single configuration, this will
36 which profile you are using. For users with a single configuration, this will
37 simply be 'default', and will be located in
37 simply be 'default', and will be located in
38 :file:`<IPYTHONDIR>/profile_default`.
38 :file:`<IPYTHONDIR>/profile_default`.
39
39
40 The next thing you need to know is what to call your configuration file. The
40 The next thing you need to know is what to call your configuration file. The
41 basic idea is that each application has its own default configuration filename.
41 basic idea is that each application has its own default configuration filename.
42 The default named used by the :command:`ipython` command line program is
42 The default named used by the :command:`ipython` command line program is
43 :file:`ipython_config.py`, and *all* IPython applications will use this file.
43 :file:`ipython_config.py`, and *all* IPython applications will use this file.
44 The IPython kernel will load its own config file *after*
44 The IPython kernel will load its own config file *after*
45 :file:`ipython_config.py`. To load a particular configuration file instead of
45 :file:`ipython_config.py`. To load a particular configuration file instead of
46 the default, the name can be overridden by the ``config_file`` command line
46 the default, the name can be overridden by the ``config_file`` command line
47 flag.
47 flag.
48
48
49 To generate the default configuration files, do::
49 To generate the default configuration files, do::
50
50
51 $ ipython profile create
51 $ ipython profile create
52
52
53 and you will have a default :file:`ipython_config.py` in your IPython directory
53 and you will have a default :file:`ipython_config.py` in your IPython directory
54 under :file:`profile_default`.
54 under :file:`profile_default`.
55
55
56 .. note::
56 .. note::
57
57
58 IPython configuration options are case sensitive, and IPython cannot
58 IPython configuration options are case sensitive, and IPython cannot
59 catch misnamed keys or invalid values.
59 catch misnamed keys or invalid values.
60
60
61 By default IPython will also ignore any invalid configuration files.
61 By default IPython will also ignore any invalid configuration files.
62
62
63 .. versionadded:: 5.0
63 .. versionadded:: 5.0
64
64
65 IPython can be configured to abort in case of invalid configuration file.
65 IPython can be configured to abort in case of invalid configuration file.
66 To do so set the environment variable ``IPYTHON_SUPPRESS_CONFIG_ERRORS`` to
66 To do so set the environment variable ``IPYTHON_SUPPRESS_CONFIG_ERRORS`` to
67 `'1'` or `'true'`
67 `'1'` or `'true'`
68
68
69
69
70 Locating these files
70 Locating these files
71 --------------------
71 --------------------
72
72
73 From the command-line, you can quickly locate the IPYTHONDIR or a specific
73 From the command-line, you can quickly locate the IPYTHONDIR or a specific
74 profile with:
74 profile with:
75
75
76 .. sourcecode:: bash
76 .. sourcecode:: bash
77
77
78 $ ipython locate
78 $ ipython locate
79 /home/you/.ipython
79 /home/you/.ipython
80
80
81 $ ipython locate profile foo
81 $ ipython locate profile foo
82 /home/you/.ipython/profile_foo
82 /home/you/.ipython/profile_foo
83
83
84 These map to the utility functions: :func:`IPython.utils.path.get_ipython_dir`
84 These map to the utility functions: :func:`IPython.paths.get_ipython_dir`
85 and :func:`IPython.utils.path.locate_profile` respectively.
85 and :func:`IPython.paths.locate_profile` respectively.
86
86
87
87
88 .. _profiles_dev:
88 .. _profiles_dev:
89
89
90 Profiles
90 Profiles
91 ========
91 ========
92
92
93 A profile is a directory containing configuration and runtime files, such as
93 A profile is a directory containing configuration and runtime files, such as
94 logs, connection info for the parallel apps, and your IPython command history.
94 logs, connection info for the parallel apps, and your IPython command history.
95
95
96 The idea is that users often want to maintain a set of configuration files for
96 The idea is that users often want to maintain a set of configuration files for
97 different purposes: one for doing numerical computing with NumPy and SciPy and
97 different purposes: one for doing numerical computing with NumPy and SciPy and
98 another for doing symbolic computing with SymPy. Profiles make it easy to keep a
98 another for doing symbolic computing with SymPy. Profiles make it easy to keep a
99 separate configuration files, logs, and histories for each of these purposes.
99 separate configuration files, logs, and histories for each of these purposes.
100
100
101 Let's start by showing how a profile is used:
101 Let's start by showing how a profile is used:
102
102
103 .. code-block:: bash
103 .. code-block:: bash
104
104
105 $ ipython --profile=sympy
105 $ ipython --profile=sympy
106
106
107 This tells the :command:`ipython` command line program to get its configuration
107 This tells the :command:`ipython` command line program to get its configuration
108 from the "sympy" profile. The file names for various profiles do not change. The
108 from the "sympy" profile. The file names for various profiles do not change. The
109 only difference is that profiles are named in a special way. In the case above,
109 only difference is that profiles are named in a special way. In the case above,
110 the "sympy" profile means looking for :file:`ipython_config.py` in :file:`<IPYTHONDIR>/profile_sympy`.
110 the "sympy" profile means looking for :file:`ipython_config.py` in :file:`<IPYTHONDIR>/profile_sympy`.
111
111
112 The general pattern is this: simply create a new profile with:
112 The general pattern is this: simply create a new profile with:
113
113
114 .. code-block:: bash
114 .. code-block:: bash
115
115
116 $ ipython profile create <name>
116 $ ipython profile create <name>
117
117
118 which adds a directory called ``profile_<name>`` to your IPython directory. Then
118 which adds a directory called ``profile_<name>`` to your IPython directory. Then
119 you can load this profile by adding ``--profile=<name>`` to your command line
119 you can load this profile by adding ``--profile=<name>`` to your command line
120 options. Profiles are supported by all IPython applications.
120 options. Profiles are supported by all IPython applications.
121
121
122 IPython extends the config loader for Python files so that you can inherit
122 IPython extends the config loader for Python files so that you can inherit
123 config from another profile. To do this, use a line like this in your Python
123 config from another profile. To do this, use a line like this in your Python
124 config file:
124 config file:
125
125
126 .. sourcecode:: python
126 .. sourcecode:: python
127
127
128 load_subconfig('ipython_config.py', profile='default')
128 load_subconfig('ipython_config.py', profile='default')
@@ -1,897 +1,897 b''
1 ============
1 ============
2 8.x Series
2 8.x Series
3 ============
3 ============
4
4
5 IPython 8.0
5 IPython 8.0
6 -----------
6 -----------
7
7
8 IPython 8.0 is bringing a large number of new features and improvements to both the
8 IPython 8.0 is bringing a large number of new features and improvements to both the
9 user of the terminal and of the kernel via Jupyter. The removal of compatibility
9 user of the terminal and of the kernel via Jupyter. The removal of compatibility
10 with older version of Python is also the opportunity to do a couple of
10 with older version of Python is also the opportunity to do a couple of
11 performance improvement in particular with respect to startup time.
11 performance improvement in particular with respect to startup time.
12 The 8.x branch started diverging from its predecessor around IPython 7.12
12 The 8.x branch started diverging from its predecessor around IPython 7.12
13 (January 2020).
13 (January 2020).
14
14
15 This release contains 250+ pull requests, in addition to many of the features
15 This release contains 250+ pull requests, in addition to many of the features
16 and backports that have made it to the 7.x branch. Please see the
16 and backports that have made it to the 7.x branch. Please see the
17 `8.0 milestone <https://github.com/ipython/ipython/milestone/73?closed=1>`__ for the full list of pull requests.
17 `8.0 milestone <https://github.com/ipython/ipython/milestone/73?closed=1>`__ for the full list of pull requests.
18
18
19 Please fell free to send pull requests to updates those notes after release,
19 Please fell free to send pull requests to updates those notes after release,
20 I have likely forgotten a few things reviewing 250+ PRs.
20 I have likely forgotten a few things reviewing 250+ PRs.
21
21
22 Dependencies changes/downstream packaging
22 Dependencies changes/downstream packaging
23 -----------------------------------------
23 -----------------------------------------
24
24
25 Most of our building steps have been changed to be (mostly) declarative
25 Most of our building steps have been changed to be (mostly) declarative
26 and follow PEP 517. We are trying to completely remove ``setup.py`` (:ghpull:`13238`) and are
26 and follow PEP 517. We are trying to completely remove ``setup.py`` (:ghpull:`13238`) and are
27 looking for help to do so.
27 looking for help to do so.
28
28
29 - minimum supported ``traitlets`` version is now 5+
29 - minimum supported ``traitlets`` version is now 5+
30 - we now require ``stack_data``
30 - we now require ``stack_data``
31 - minimal Python is now 3.8
31 - minimal Python is now 3.8
32 - ``nose`` is not a testing requirement anymore
32 - ``nose`` is not a testing requirement anymore
33 - ``pytest`` replaces nose.
33 - ``pytest`` replaces nose.
34 - ``iptest``/``iptest3`` cli entrypoints do not exists anymore.
34 - ``iptest``/``iptest3`` cli entrypoints do not exists anymore.
35 - minimum officially support ``numpy`` version has been bumped, but this should
35 - minimum officially support ``numpy`` version has been bumped, but this should
36 not have much effect on packaging.
36 not have much effect on packaging.
37
37
38
38
39 Deprecation and removal
39 Deprecation and removal
40 -----------------------
40 -----------------------
41
41
42 We removed almost all features, arguments, functions, and modules that were
42 We removed almost all features, arguments, functions, and modules that were
43 marked as deprecated between IPython 1.0 and 5.0. As a reminder, 5.0 was released
43 marked as deprecated between IPython 1.0 and 5.0. As a reminder, 5.0 was released
44 in 2016, and 1.0 in 2013. Last release of the 5 branch was 5.10.0, in May 2020.
44 in 2016, and 1.0 in 2013. Last release of the 5 branch was 5.10.0, in May 2020.
45 The few remaining deprecated features we left have better deprecation warnings
45 The few remaining deprecated features we left have better deprecation warnings
46 or have been turned into explicit errors for better error messages.
46 or have been turned into explicit errors for better error messages.
47
47
48 I will use this occasion to add the following requests to anyone emitting a
48 I will use this occasion to add the following requests to anyone emitting a
49 deprecation warning:
49 deprecation warning:
50
50
51 - Please use at least ``stacklevel=2`` so that the warning is emitted into the
51 - Please add at least ``stacklevel=2`` so that the warning is emitted into the
52 caller context, and not the callee one.
52 caller context, and not the callee one.
53 - Please add **since which version** something is deprecated.
53 - Please add **since which version** something is deprecated.
54
54
55 As a side note, it is much easier to conditionally compare version
55 As a side note, it is much easier to conditionally compare version
56 numbers rather than using ``try/except`` when functionality changes with a version.
56 numbers rather than using ``try/except`` when functionality changes with a version.
57
57
58 I won't list all the removed features here, but modules like ``IPython.kernel``,
58 I won't list all the removed features here, but modules like ``IPython.kernel``,
59 which was just a shim module around ``ipykernel`` for the past 8 years, have been
59 which was just a shim module around ``ipykernel`` for the past 8 years, have been
60 removed, and so many other similar things that pre-date the name **Jupyter**
60 removed, and so many other similar things that pre-date the name **Jupyter**
61 itself.
61 itself.
62
62
63 We no longer need to add ``IPython.extensions`` to the PYTHONPATH because that is being
63 We no longer need to add ``IPython.extensions`` to the PYTHONPATH because that is being
64 handled by ``load_extension``.
64 handled by ``load_extension``.
65
65
66 We are also removing ``Cythonmagic``, ``sympyprinting`` and ``rmagic`` as they are now in
66 We are also removing ``Cythonmagic``, ``sympyprinting`` and ``rmagic`` as they are now in
67 other packages and no longer need to be inside IPython.
67 other packages and no longer need to be inside IPython.
68
68
69
69
70 Documentation
70 Documentation
71 -------------
71 -------------
72
72
73 The majority of our docstrings have now been reformatted and automatically fixed by
73 The majority of our docstrings have now been reformatted and automatically fixed by
74 the experimental `VΓ©lin <https://pypi.org/project/velin/>`_ project to conform
74 the experimental `VΓ©lin <https://pypi.org/project/velin/>`_ project to conform
75 to numpydoc.
75 to numpydoc.
76
76
77 Type annotations
77 Type annotations
78 ----------------
78 ----------------
79
79
80 While IPython itself is highly dynamic and can't be completely typed, many of
80 While IPython itself is highly dynamic and can't be completely typed, many of
81 the functions now have type annotations, and part of the codebase is now checked
81 the functions now have type annotations, and part of the codebase is now checked
82 by mypy.
82 by mypy.
83
83
84
84
85 Featured changes
85 Featured changes
86 ----------------
86 ----------------
87
87
88 Here is a features list of changes in IPython 8.0. This is of course non-exhaustive.
88 Here is a features list of changes in IPython 8.0. This is of course non-exhaustive.
89 Please note as well that many features have been added in the 7.x branch as well
89 Please note as well that many features have been added in the 7.x branch as well
90 (and hence why you want to read the 7.x what's new notes), in particular
90 (and hence why you want to read the 7.x what's new notes), in particular
91 features contributed by QuantStack (with respect to debugger protocol and Xeus
91 features contributed by QuantStack (with respect to debugger protocol and Xeus
92 Python), as well as many debugger features that I was pleased to implement as
92 Python), as well as many debugger features that I was pleased to implement as
93 part of my work at QuanSight and sponsored by DE Shaw.
93 part of my work at QuanSight and sponsored by DE Shaw.
94
94
95 Traceback improvements
95 Traceback improvements
96 ~~~~~~~~~~~~~~~~~~~~~~
96 ~~~~~~~~~~~~~~~~~~~~~~
97
97
98 Previously, error tracebacks for errors happening in code cells were showing a
98 Previously, error tracebacks for errors happening in code cells were showing a
99 hash, the one used for compiling the Python AST::
99 hash, the one used for compiling the Python AST::
100
100
101 In [1]: def foo():
101 In [1]: def foo():
102 ...: return 3 / 0
102 ...: return 3 / 0
103 ...:
103 ...:
104
104
105 In [2]: foo()
105 In [2]: foo()
106 ---------------------------------------------------------------------------
106 ---------------------------------------------------------------------------
107 ZeroDivisionError Traceback (most recent call last)
107 ZeroDivisionError Traceback (most recent call last)
108 <ipython-input-2-c19b6d9633cf> in <module>
108 <ipython-input-2-c19b6d9633cf> in <module>
109 ----> 1 foo()
109 ----> 1 foo()
110
110
111 <ipython-input-1-1595a74c32d5> in foo()
111 <ipython-input-1-1595a74c32d5> in foo()
112 1 def foo():
112 1 def foo():
113 ----> 2 return 3 / 0
113 ----> 2 return 3 / 0
114 3
114 3
115
115
116 ZeroDivisionError: division by zero
116 ZeroDivisionError: division by zero
117
117
118 The error traceback is now correctly formatted, showing the cell number in which the error happened::
118 The error traceback is now correctly formatted, showing the cell number in which the error happened::
119
119
120 In [1]: def foo():
120 In [1]: def foo():
121 ...: return 3 / 0
121 ...: return 3 / 0
122 ...:
122 ...:
123
123
124 Input In [2]: foo()
124 Input In [2]: foo()
125 ---------------------------------------------------------------------------
125 ---------------------------------------------------------------------------
126 ZeroDivisionError Traceback (most recent call last)
126 ZeroDivisionError Traceback (most recent call last)
127 input In [2], in <module>
127 input In [2], in <module>
128 ----> 1 foo()
128 ----> 1 foo()
129
129
130 Input In [1], in foo()
130 Input In [1], in foo()
131 1 def foo():
131 1 def foo():
132 ----> 2 return 3 / 0
132 ----> 2 return 3 / 0
133
133
134 ZeroDivisionError: division by zero
134 ZeroDivisionError: division by zero
135
135
136 The ``stack_data`` package has been integrated, which provides smarter information in the traceback;
136 The ``stack_data`` package has been integrated, which provides smarter information in the traceback;
137 in particular it will highlight the AST node where an error occurs which can help to quickly narrow down errors.
137 in particular it will highlight the AST node where an error occurs which can help to quickly narrow down errors.
138
138
139 For example in the following snippet::
139 For example in the following snippet::
140
140
141 def foo(i):
141 def foo(i):
142 x = [[[0]]]
142 x = [[[0]]]
143 return x[0][i][0]
143 return x[0][i][0]
144
144
145
145
146 def bar():
146 def bar():
147 return foo(0) + foo(
147 return foo(0) + foo(
148 1
148 1
149 ) + foo(2)
149 ) + foo(2)
150
150
151
151
152 calling ``bar()`` would raise an ``IndexError`` on the return line of ``foo``,
152 calling ``bar()`` would raise an ``IndexError`` on the return line of ``foo``,
153 and IPython 8.0 is capable of telling you where the index error occurs::
153 and IPython 8.0 is capable of telling you where the index error occurs::
154
154
155
155
156 IndexError
156 IndexError
157 Input In [2], in <module>
157 Input In [2], in <module>
158 ----> 1 bar()
158 ----> 1 bar()
159 ^^^^^
159 ^^^^^
160
160
161 Input In [1], in bar()
161 Input In [1], in bar()
162 6 def bar():
162 6 def bar():
163 ----> 7 return foo(0) + foo(
163 ----> 7 return foo(0) + foo(
164 ^^^^
164 ^^^^
165 8 1
165 8 1
166 ^^^^^^^^
166 ^^^^^^^^
167 9 ) + foo(2)
167 9 ) + foo(2)
168 ^^^^
168 ^^^^
169
169
170 Input In [1], in foo(i)
170 Input In [1], in foo(i)
171 1 def foo(i):
171 1 def foo(i):
172 2 x = [[[0]]]
172 2 x = [[[0]]]
173 ----> 3 return x[0][i][0]
173 ----> 3 return x[0][i][0]
174 ^^^^^^^
174 ^^^^^^^
175
175
176 The corresponding locations marked here with ``^`` will show up highlighted in
176 The corresponding locations marked here with ``^`` will show up highlighted in
177 the terminal and notebooks.
177 the terminal and notebooks.
178
178
179 Finally, a colon ``::`` and line number is appended after a filename in
179 Finally, a colon ``::`` and line number is appended after a filename in
180 traceback::
180 traceback::
181
181
182
182
183 ZeroDivisionError Traceback (most recent call last)
183 ZeroDivisionError Traceback (most recent call last)
184 File ~/error.py:4, in <module>
184 File ~/error.py:4, in <module>
185 1 def f():
185 1 def f():
186 2 1/0
186 2 1/0
187 ----> 4 f()
187 ----> 4 f()
188
188
189 File ~/error.py:2, in f()
189 File ~/error.py:2, in f()
190 1 def f():
190 1 def f():
191 ----> 2 1/0
191 ----> 2 1/0
192
192
193 Many terminals and editors have integrations enabling you to directly jump to the
193 Many terminals and editors have integrations enabling you to directly jump to the
194 relevant file/line when this syntax is used, so this small addition may have a high
194 relevant file/line when this syntax is used, so this small addition may have a high
195 impact on productivity.
195 impact on productivity.
196
196
197
197
198 Autosuggestons
198 Autosuggestons
199 ~~~~~~~~~~~~~~
199 ~~~~~~~~~~~~~~
200
200
201 Autosuggestion is a very useful feature available in `fish <https://fishshell.com/>`__, `zsh <https://en.wikipedia.org/wiki/Z_shell>`__, and `prompt-toolkit <https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#auto-suggestion>`__.
201 Autosuggestion is a very useful feature available in `fish <https://fishshell.com/>`__, `zsh <https://en.wikipedia.org/wiki/Z_shell>`__, and `prompt-toolkit <https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#auto-suggestion>`__.
202
202
203 `Ptpython <https://github.com/prompt-toolkit/ptpython#ptpython>`__ allows users to enable this feature in
203 `Ptpython <https://github.com/prompt-toolkit/ptpython#ptpython>`__ allows users to enable this feature in
204 `ptpython/config.py <https://github.com/prompt-toolkit/ptpython/blob/master/examples/ptpython_config/config.py#L90>`__.
204 `ptpython/config.py <https://github.com/prompt-toolkit/ptpython/blob/master/examples/ptpython_config/config.py#L90>`__.
205
205
206 This feature allows users to accept autosuggestions with ctrl e, ctrl f,
206 This feature allows users to accept autosuggestions with ctrl e, ctrl f,
207 or right arrow as described below.
207 or right arrow as described below.
208
208
209 1. Start ipython
209 1. Start ipython
210
210
211 .. image:: ../_images/8.0/auto_suggest_1_prompt_no_text.png
211 .. image:: ../_images/8.0/auto_suggest_1_prompt_no_text.png
212
212
213 2. Run ``print("hello")``
213 2. Run ``print("hello")``
214
214
215 .. image:: ../_images/8.0/auto_suggest_2_print_hello_suggest.png
215 .. image:: ../_images/8.0/auto_suggest_2_print_hello_suggest.png
216
216
217 3. start typing ``print`` again to see the autosuggestion
217 3. start typing ``print`` again to see the autosuggestion
218
218
219 .. image:: ../_images/8.0/auto_suggest_3_print_hello_suggest.png
219 .. image:: ../_images/8.0/auto_suggest_3_print_hello_suggest.png
220
220
221 4. Press ``ctrl-f``, or ``ctrl-e``, or ``right-arrow`` to accept the suggestion
221 4. Press ``ctrl-f``, or ``ctrl-e``, or ``right-arrow`` to accept the suggestion
222
222
223 .. image:: ../_images/8.0/auto_suggest_4_print_hello.png
223 .. image:: ../_images/8.0/auto_suggest_4_print_hello.png
224
224
225 You can also complete word by word:
225 You can also complete word by word:
226
226
227 1. Run ``def say_hello(): print("hello")``
227 1. Run ``def say_hello(): print("hello")``
228
228
229 .. image:: ../_images/8.0/auto_suggest_second_prompt.png
229 .. image:: ../_images/8.0/auto_suggest_second_prompt.png
230
230
231 2. Start typing the first letter if ``def`` to see the autosuggestion
231 2. Start typing the first letter if ``def`` to see the autosuggestion
232
232
233 .. image:: ../_images/8.0/auto_suggest_d_phantom.png
233 .. image:: ../_images/8.0/auto_suggest_d_phantom.png
234
234
235 3. Press ``alt-f`` (or ``escape`` followed by ``f``), to accept the first word of the suggestion
235 3. Press ``alt-f`` (or ``escape`` followed by ``f``), to accept the first word of the suggestion
236
236
237 .. image:: ../_images/8.0/auto_suggest_def_phantom.png
237 .. image:: ../_images/8.0/auto_suggest_def_phantom.png
238
238
239 Importantly, this feature does not interfere with tab completion:
239 Importantly, this feature does not interfere with tab completion:
240
240
241 1. After running ``def say_hello(): print("hello")``, press d
241 1. After running ``def say_hello(): print("hello")``, press d
242
242
243 .. image:: ../_images/8.0/auto_suggest_d_phantom.png
243 .. image:: ../_images/8.0/auto_suggest_d_phantom.png
244
244
245 2. Press Tab to start tab completion
245 2. Press Tab to start tab completion
246
246
247 .. image:: ../_images/8.0/auto_suggest_d_completions.png
247 .. image:: ../_images/8.0/auto_suggest_d_completions.png
248
248
249 3A. Press Tab again to select the first option
249 3A. Press Tab again to select the first option
250
250
251 .. image:: ../_images/8.0/auto_suggest_def_completions.png
251 .. image:: ../_images/8.0/auto_suggest_def_completions.png
252
252
253 3B. Press ``alt f`` (``escape``, ``f``) to accept to accept the first word of the suggestion
253 3B. Press ``alt f`` (``escape``, ``f``) to accept to accept the first word of the suggestion
254
254
255 .. image:: ../_images/8.0/auto_suggest_def_phantom.png
255 .. image:: ../_images/8.0/auto_suggest_def_phantom.png
256
256
257 3C. Press ``ctrl-f`` or ``ctrl-e`` to accept the entire suggestion
257 3C. Press ``ctrl-f`` or ``ctrl-e`` to accept the entire suggestion
258
258
259 .. image:: ../_images/8.0/auto_suggest_match_parens.png
259 .. image:: ../_images/8.0/auto_suggest_match_parens.png
260
260
261
261
262 Currently, autosuggestions are only shown in the emacs or vi insert editing modes:
262 Currently, autosuggestions are only shown in the emacs or vi insert editing modes:
263
263
264 - The ctrl e, ctrl f, and alt f shortcuts work by default in emacs mode.
264 - The ctrl e, ctrl f, and alt f shortcuts work by default in emacs mode.
265 - To use these shortcuts in vi insert mode, you will have to create `custom keybindings in your config.py <https://github.com/mskar/setup/commit/2892fcee46f9f80ef7788f0749edc99daccc52f4/>`__.
265 - To use these shortcuts in vi insert mode, you will have to create `custom keybindings in your config.py <https://github.com/mskar/setup/commit/2892fcee46f9f80ef7788f0749edc99daccc52f4/>`__.
266
266
267
267
268 Show pinfo information in ipdb using "?" and "??"
268 Show pinfo information in ipdb using "?" and "??"
269 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
269 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
270
270
271 In IPDB, it is now possible to show the information about an object using "?"
271 In IPDB, it is now possible to show the information about an object using "?"
272 and "??", in much the same way that it can be done when using the IPython prompt::
272 and "??", in much the same way that it can be done when using the IPython prompt::
273
273
274 ipdb> partial?
274 ipdb> partial?
275 Init signature: partial(self, /, *args, **kwargs)
275 Init signature: partial(self, /, *args, **kwargs)
276 Docstring:
276 Docstring:
277 partial(func, *args, **keywords) - new function with partial application
277 partial(func, *args, **keywords) - new function with partial application
278 of the given arguments and keywords.
278 of the given arguments and keywords.
279 File: ~/.pyenv/versions/3.8.6/lib/python3.8/functools.py
279 File: ~/.pyenv/versions/3.8.6/lib/python3.8/functools.py
280 Type: type
280 Type: type
281 Subclasses:
281 Subclasses:
282
282
283 Previously, ``pinfo`` or ``pinfo2`` command had to be used for this purpose.
283 Previously, ``pinfo`` or ``pinfo2`` command had to be used for this purpose.
284
284
285
285
286 Autoreload 3 feature
286 Autoreload 3 feature
287 ~~~~~~~~~~~~~~~~~~~~
287 ~~~~~~~~~~~~~~~~~~~~
288
288
289 Example: When an IPython session is run with the 'autoreload' extension loaded,
289 Example: When an IPython session is run with the 'autoreload' extension loaded,
290 you will now have the option '3' to select, which means the following:
290 you will now have the option '3' to select, which means the following:
291
291
292 1. replicate all functionality from option 2
292 1. replicate all functionality from option 2
293 2. autoload all new funcs/classes/enums/globals from the module when they are added
293 2. autoload all new funcs/classes/enums/globals from the module when they are added
294 3. autoload all newly imported funcs/classes/enums/globals from external modules
294 3. autoload all newly imported funcs/classes/enums/globals from external modules
295
295
296 Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload``.
296 Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload``.
297
297
298 For more information please see the following unit test : ``extensions/tests/test_autoreload.py:test_autoload_newly_added_objects``
298 For more information please see the following unit test : ``extensions/tests/test_autoreload.py:test_autoload_newly_added_objects``
299
299
300 Auto formatting with black in the CLI
300 Auto formatting with black in the CLI
301 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
301 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
302
302
303 If ``black`` is installed in the same environment as IPython, terminal IPython
303 If ``black`` is installed in the same environment as IPython, terminal IPython
304 will now *by default* reformat the code in the CLI when possible. You can
304 will now *by default* reformat the code in the CLI when possible. You can
305 disable this with ``--TerminalInteractiveShell.autoformatter=None``.
305 disable this with ``--TerminalInteractiveShell.autoformatter=None``.
306
306
307 This feature was present in 7.x, but disabled by default.
307 This feature was present in 7.x, but disabled by default.
308
308
309
309
310 History Range Glob feature
310 History Range Glob feature
311 ~~~~~~~~~~~~~~~~~~~~~~~~~~
311 ~~~~~~~~~~~~~~~~~~~~~~~~~~
312
312
313 Previously, when using ``%history``, users could specify either
313 Previously, when using ``%history``, users could specify either
314 a range of sessions and lines, for example:
314 a range of sessions and lines, for example:
315
315
316 .. code-block:: python
316 .. code-block:: python
317
317
318 ~8/1-~6/5 # see history from the first line of 8 sessions ago,
318 ~8/1-~6/5 # see history from the first line of 8 sessions ago,
319 # to the fifth line of 6 sessions ago.``
319 # to the fifth line of 6 sessions ago.``
320
320
321 Or users could specify a glob pattern:
321 Or users could specify a glob pattern:
322
322
323 .. code-block:: python
323 .. code-block:: python
324
324
325 -g <pattern> # glob ALL history for the specified pattern.
325 -g <pattern> # glob ALL history for the specified pattern.
326
326
327 However users could *not* specify both.
327 However users could *not* specify both.
328
328
329 If a user *did* specify both a range and a glob pattern,
329 If a user *did* specify both a range and a glob pattern,
330 then the glob pattern would be used (globbing *all* history) *and the range would be ignored*.
330 then the glob pattern would be used (globbing *all* history) *and the range would be ignored*.
331
331
332 With this enhancement, if a user specifies both a range and a glob pattern, then the glob pattern will be applied to the specified range of history.
332 With this enhancement, if a user specifies both a range and a glob pattern, then the glob pattern will be applied to the specified range of history.
333
333
334 Don't start a multi-line cell with sunken parenthesis
334 Don't start a multi-line cell with sunken parenthesis
335 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
335 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
336
336
337 From now on, IPython will not ask for the next line of input when given a single
337 From now on, IPython will not ask for the next line of input when given a single
338 line with more closing than opening brackets. For example, this means that if
338 line with more closing than opening brackets. For example, this means that if
339 you (mis)type ``]]`` instead of ``[]``, a ``SyntaxError`` will show up, instead of
339 you (mis)type ``]]`` instead of ``[]``, a ``SyntaxError`` will show up, instead of
340 the ``...:`` prompt continuation.
340 the ``...:`` prompt continuation.
341
341
342 IPython shell for ipdb interact
342 IPython shell for ipdb interact
343 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
343 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
344
344
345 The ipdb ``interact`` starts an IPython shell instead of Python's built-in ``code.interact()``.
345 The ipdb ``interact`` starts an IPython shell instead of Python's built-in ``code.interact()``.
346
346
347 Automatic Vi prompt stripping
347 Automatic Vi prompt stripping
348 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
348 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
349
349
350 When pasting code into IPython, it will strip the leading prompt characters if
350 When pasting code into IPython, it will strip the leading prompt characters if
351 there are any. For example, you can paste the following code into the console -
351 there are any. For example, you can paste the following code into the console -
352 it will still work, even though each line is prefixed with prompts (`In`,
352 it will still work, even though each line is prefixed with prompts (`In`,
353 `Out`)::
353 `Out`)::
354
354
355 In [1]: 2 * 2 == 4
355 In [1]: 2 * 2 == 4
356 Out[1]: True
356 Out[1]: True
357
357
358 In [2]: print("This still works as pasted")
358 In [2]: print("This still works as pasted")
359
359
360
360
361 Previously, this was not the case for the Vi-mode prompts::
361 Previously, this was not the case for the Vi-mode prompts::
362
362
363 In [1]: [ins] In [13]: 2 * 2 == 4
363 In [1]: [ins] In [13]: 2 * 2 == 4
364 ...: Out[13]: True
364 ...: Out[13]: True
365 ...:
365 ...:
366 File "<ipython-input-1-727bb88eaf33>", line 1
366 File "<ipython-input-1-727bb88eaf33>", line 1
367 [ins] In [13]: 2 * 2 == 4
367 [ins] In [13]: 2 * 2 == 4
368 ^
368 ^
369 SyntaxError: invalid syntax
369 SyntaxError: invalid syntax
370
370
371 This is now fixed, and Vi prompt prefixes - ``[ins]`` and ``[nav]`` - are
371 This is now fixed, and Vi prompt prefixes - ``[ins]`` and ``[nav]`` - are
372 skipped just as the normal ``In`` would be.
372 skipped just as the normal ``In`` would be.
373
373
374 IPython shell can be started in the Vi mode using ``ipython --TerminalInteractiveShell.editing_mode=vi``,
374 IPython shell can be started in the Vi mode using ``ipython --TerminalInteractiveShell.editing_mode=vi``,
375 You should be able to change mode dynamically with ``%config TerminalInteractiveShell.editing_mode='vi'``
375 You should be able to change mode dynamically with ``%config TerminalInteractiveShell.editing_mode='vi'``
376
376
377 Empty History Ranges
377 Empty History Ranges
378 ~~~~~~~~~~~~~~~~~~~~
378 ~~~~~~~~~~~~~~~~~~~~
379
379
380 A number of magics that take history ranges can now be used with an empty
380 A number of magics that take history ranges can now be used with an empty
381 range. These magics are:
381 range. These magics are:
382
382
383 * ``%save``
383 * ``%save``
384 * ``%load``
384 * ``%load``
385 * ``%pastebin``
385 * ``%pastebin``
386 * ``%pycat``
386 * ``%pycat``
387
387
388 Using them this way will make them take the history of the current session up
388 Using them this way will make them take the history of the current session up
389 to the point of the magic call (such that the magic itself will not be
389 to the point of the magic call (such that the magic itself will not be
390 included).
390 included).
391
391
392 Therefore it is now possible to save the whole history to a file using
392 Therefore it is now possible to save the whole history to a file using
393 ``%save <filename>``, load and edit it using ``%load`` (makes for a nice usage
393 ``%save <filename>``, load and edit it using ``%load`` (makes for a nice usage
394 when followed with :kbd:`F2`), send it to `dpaste.org <http://dpast.org>`_ using
394 when followed with :kbd:`F2`), send it to `dpaste.org <http://dpast.org>`_ using
395 ``%pastebin``, or view the whole thing syntax-highlighted with a single
395 ``%pastebin``, or view the whole thing syntax-highlighted with a single
396 ``%pycat``.
396 ``%pycat``.
397
397
398
398
399 Windows timing implementation: Switch to process_time
399 Windows timing implementation: Switch to process_time
400 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
400 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
401 Timing on Windows, for example with ``%%time``, was changed from being based on ``time.perf_counter``
401 Timing on Windows, for example with ``%%time``, was changed from being based on ``time.perf_counter``
402 (which counted time even when the process was sleeping) to being based on ``time.process_time`` instead
402 (which counted time even when the process was sleeping) to being based on ``time.process_time`` instead
403 (which only counts CPU time). This brings it closer to the behavior on Linux. See :ghpull:`12984`.
403 (which only counts CPU time). This brings it closer to the behavior on Linux. See :ghpull:`12984`.
404
404
405 Miscellaneous
405 Miscellaneous
406 ~~~~~~~~~~~~~
406 ~~~~~~~~~~~~~
407 - Non-text formatters are not disabled in the terminal, which should simplify
407 - Non-text formatters are not disabled in the terminal, which should simplify
408 writing extensions displaying images or other mimetypes in supporting terminals.
408 writing extensions displaying images or other mimetypes in supporting terminals.
409 :ghpull:`12315`
409 :ghpull:`12315`
410 - It is now possible to automatically insert matching brackets in Terminal IPython using the
410 - It is now possible to automatically insert matching brackets in Terminal IPython using the
411 ``TerminalInteractiveShell.auto_match=True`` option. :ghpull:`12586`
411 ``TerminalInteractiveShell.auto_match=True`` option. :ghpull:`12586`
412 - We are thinking of deprecating the current ``%%javascript`` magic in favor of a better replacement. See :ghpull:`13376`.
412 - We are thinking of deprecating the current ``%%javascript`` magic in favor of a better replacement. See :ghpull:`13376`.
413 - ``~`` is now expanded when part of a path in most magics :ghpull:`13385`
413 - ``~`` is now expanded when part of a path in most magics :ghpull:`13385`
414 - ``%/%%timeit`` magic now adds a comma every thousands to make reading a long number easier :ghpull:`13379`
414 - ``%/%%timeit`` magic now adds a comma every thousands to make reading a long number easier :ghpull:`13379`
415 - ``"info"`` messages can now be customised to hide some fields :ghpull:`13343`
415 - ``"info"`` messages can now be customised to hide some fields :ghpull:`13343`
416 - ``collections.UserList`` now pretty-prints :ghpull:`13320`
416 - ``collections.UserList`` now pretty-prints :ghpull:`13320`
417 - The debugger now has a persistent history, which should make it less
417 - The debugger now has a persistent history, which should make it less
418 annoying to retype commands :ghpull:`13246`
418 annoying to retype commands :ghpull:`13246`
419 - ``!pip`` ``!conda`` ``!cd`` or ``!ls`` are likely doing the wrong thing. We
419 - ``!pip`` ``!conda`` ``!cd`` or ``!ls`` are likely doing the wrong thing. We
420 now warn users if they use one of those commands. :ghpull:`12954`
420 now warn users if they use one of those commands. :ghpull:`12954`
421 - Make ``%precision`` work for ``numpy.float64`` type :ghpull:`12902`
421 - Make ``%precision`` work for ``numpy.float64`` type :ghpull:`12902`
422
422
423 Re-added support for XDG config directories
423 Re-added support for XDG config directories
424 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
424 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
425
425
426 XDG support through the years comes and goes. There is a tension between having
426 XDG support through the years comes and goes. There is a tension between having
427 an identical location for configuration in all platforms versus having simple instructions.
427 an identical location for configuration in all platforms versus having simple instructions.
428 After initial failures a couple of years ago, IPython was modified to automatically migrate XDG
428 After initial failures a couple of years ago, IPython was modified to automatically migrate XDG
429 config files back into ``~/.ipython``. That migration code has now been removed.
429 config files back into ``~/.ipython``. That migration code has now been removed.
430 IPython now checks the XDG locations, so if you _manually_ move your config
430 IPython now checks the XDG locations, so if you _manually_ move your config
431 files to your preferred location, IPython will not move them back.
431 files to your preferred location, IPython will not move them back.
432
432
433
433
434 Preparing for Python 3.10
434 Preparing for Python 3.10
435 -------------------------
435 -------------------------
436
436
437 To prepare for Python 3.10, we have started working on removing reliance and
437 To prepare for Python 3.10, we have started working on removing reliance and
438 any dependency that is not compatible with Python 3.10. This includes migrating our
438 any dependency that is not compatible with Python 3.10. This includes migrating our
439 test suite to pytest and starting to remove nose. This also means that the
439 test suite to pytest and starting to remove nose. This also means that the
440 ``iptest`` command is now gone and all testing is via pytest.
440 ``iptest`` command is now gone and all testing is via pytest.
441
441
442 This was in large part thanks to the NumFOCUS Small Developer grant, which enabled us to
442 This was in large part thanks to the NumFOCUS Small Developer grant, which enabled us to
443 allocate \$4000 to hire `Nikita Kniazev (@Kojoley) <https://github.com/Kojoley>`_,
443 allocate \$4000 to hire `Nikita Kniazev (@Kojoley) <https://github.com/Kojoley>`_,
444 who did a fantastic job at updating our code base, migrating to pytest, pushing
444 who did a fantastic job at updating our code base, migrating to pytest, pushing
445 our coverage, and fixing a large number of bugs. I highly recommend contacting
445 our coverage, and fixing a large number of bugs. I highly recommend contacting
446 them if you need help with C++ and Python projects.
446 them if you need help with C++ and Python projects.
447
447
448 You can find all relevant issues and PRs with the SDG 2021 tag `<https://github.com/ipython/ipython/issues?q=label%3A%22Numfocus+SDG+2021%22+>`__
448 You can find all relevant issues and PRs with the SDG 2021 tag `<https://github.com/ipython/ipython/issues?q=label%3A%22Numfocus+SDG+2021%22+>`__
449
449
450 Removing support for older Python versions
450 Removing support for older Python versions
451 ------------------------------------------
451 ------------------------------------------
452
452
453
453
454 We are removing support for Python up through 3.7, allowing internal code to use the more
454 We are removing support for Python up through 3.7, allowing internal code to use the more
455 efficient ``pathlib`` and to make better use of type annotations.
455 efficient ``pathlib`` and to make better use of type annotations.
456
456
457 .. image:: ../_images/8.0/pathlib_pathlib_everywhere.jpg
457 .. image:: ../_images/8.0/pathlib_pathlib_everywhere.jpg
458 :alt: "Meme image of Toy Story with Woody and Buzz, with the text 'pathlib, pathlib everywhere'"
458 :alt: "Meme image of Toy Story with Woody and Buzz, with the text 'pathlib, pathlib everywhere'"
459
459
460
460
461 We had about 34 PRs only to update some logic to update some functions from managing strings to
461 We had about 34 PRs only to update some logic to update some functions from managing strings to
462 using Pathlib.
462 using Pathlib.
463
463
464 The completer has also seen significant updates and now makes use of newer Jedi APIs,
464 The completer has also seen significant updates and now makes use of newer Jedi APIs,
465 offering faster and more reliable tab completion.
465 offering faster and more reliable tab completion.
466
466
467 Misc Statistics
467 Misc Statistics
468 ---------------
468 ---------------
469
469
470 Here are some numbers::
470 Here are some numbers::
471
471
472 7.x: 296 files, 12561 blank lines, 20282 comments, 35142 line of code.
472 7.x: 296 files, 12561 blank lines, 20282 comments, 35142 line of code.
473 8.0: 252 files, 12053 blank lines, 19232 comments, 34505 line of code.
473 8.0: 252 files, 12053 blank lines, 19232 comments, 34505 line of code.
474
474
475 $ git diff --stat 7.x...master | tail -1
475 $ git diff --stat 7.x...master | tail -1
476 340 files changed, 13399 insertions(+), 12421 deletions(-)
476 340 files changed, 13399 insertions(+), 12421 deletions(-)
477
477
478 We have commits from 162 authors, who contributed 1916 commits in 23 month, excluding merges (to not bias toward
478 We have commits from 162 authors, who contributed 1916 commits in 23 month, excluding merges (to not bias toward
479 maintainers pushing buttons).::
479 maintainers pushing buttons).::
480
480
481 $ git shortlog -s --no-merges 7.x...master | sort -nr
481 $ git shortlog -s --no-merges 7.x...master | sort -nr
482 535 Matthias Bussonnier
482 535 Matthias Bussonnier
483 86 Nikita Kniazev
483 86 Nikita Kniazev
484 69 Blazej Michalik
484 69 Blazej Michalik
485 49 Samuel Gaist
485 49 Samuel Gaist
486 27 Itamar Turner-Trauring
486 27 Itamar Turner-Trauring
487 18 Spas Kalaydzhisyki
487 18 Spas Kalaydzhisyki
488 17 Thomas Kluyver
488 17 Thomas Kluyver
489 17 Quentin Peter
489 17 Quentin Peter
490 17 James Morris
490 17 James Morris
491 17 Artur Svistunov
491 17 Artur Svistunov
492 15 Bart Skowron
492 15 Bart Skowron
493 14 Alex Hall
493 14 Alex Hall
494 13 rushabh-v
494 13 rushabh-v
495 13 Terry Davis
495 13 Terry Davis
496 13 Benjamin Ragan-Kelley
496 13 Benjamin Ragan-Kelley
497 8 martinRenou
497 8 martinRenou
498 8 farisachugthai
498 8 farisachugthai
499 7 dswij
499 7 dswij
500 7 Gal B
500 7 Gal B
501 7 Corentin Cadiou
501 7 Corentin Cadiou
502 6 yuji96
502 6 yuji96
503 6 Martin Skarzynski
503 6 Martin Skarzynski
504 6 Justin Palmer
504 6 Justin Palmer
505 6 Daniel Goldfarb
505 6 Daniel Goldfarb
506 6 Ben Greiner
506 6 Ben Greiner
507 5 Sammy Al Hashemi
507 5 Sammy Al Hashemi
508 5 Paul Ivanov
508 5 Paul Ivanov
509 5 Inception95
509 5 Inception95
510 5 Eyenpi
510 5 Eyenpi
511 5 Douglas Blank
511 5 Douglas Blank
512 5 Coco Mishra
512 5 Coco Mishra
513 5 Bibo Hao
513 5 Bibo Hao
514 5 AndrΓ© A. Gomes
514 5 AndrΓ© A. Gomes
515 5 Ahmed Fasih
515 5 Ahmed Fasih
516 4 takuya fujiwara
516 4 takuya fujiwara
517 4 palewire
517 4 palewire
518 4 Thomas A Caswell
518 4 Thomas A Caswell
519 4 Talley Lambert
519 4 Talley Lambert
520 4 Scott Sanderson
520 4 Scott Sanderson
521 4 Ram Rachum
521 4 Ram Rachum
522 4 Nick Muoh
522 4 Nick Muoh
523 4 Nathan Goldbaum
523 4 Nathan Goldbaum
524 4 Mithil Poojary
524 4 Mithil Poojary
525 4 Michael T
525 4 Michael T
526 4 Jakub Klus
526 4 Jakub Klus
527 4 Ian Castleden
527 4 Ian Castleden
528 4 Eli Rykoff
528 4 Eli Rykoff
529 4 Ashwin Vishnu
529 4 Ashwin Vishnu
530 3 谭九鼎
530 3 谭九鼎
531 3 sleeping
531 3 sleeping
532 3 Sylvain Corlay
532 3 Sylvain Corlay
533 3 Peter Corke
533 3 Peter Corke
534 3 Paul Bissex
534 3 Paul Bissex
535 3 Matthew Feickert
535 3 Matthew Feickert
536 3 Fernando Perez
536 3 Fernando Perez
537 3 Eric Wieser
537 3 Eric Wieser
538 3 Daniel Mietchen
538 3 Daniel Mietchen
539 3 Aditya Sathe
539 3 Aditya Sathe
540 3 007vedant
540 3 007vedant
541 2 rchiodo
541 2 rchiodo
542 2 nicolaslazo
542 2 nicolaslazo
543 2 luttik
543 2 luttik
544 2 gorogoroumaru
544 2 gorogoroumaru
545 2 foobarbyte
545 2 foobarbyte
546 2 bar-hen
546 2 bar-hen
547 2 Theo Ouzhinski
547 2 Theo Ouzhinski
548 2 Strawkage
548 2 Strawkage
549 2 Samreen Zarroug
549 2 Samreen Zarroug
550 2 Pete Blois
550 2 Pete Blois
551 2 Meysam Azad
551 2 Meysam Azad
552 2 Matthieu Ancellin
552 2 Matthieu Ancellin
553 2 Mark Schmitz
553 2 Mark Schmitz
554 2 Maor Kleinberger
554 2 Maor Kleinberger
555 2 MRCWirtz
555 2 MRCWirtz
556 2 Lumir Balhar
556 2 Lumir Balhar
557 2 Julien Rabinow
557 2 Julien Rabinow
558 2 Juan Luis Cano RodrΓ­guez
558 2 Juan Luis Cano RodrΓ­guez
559 2 Joyce Er
559 2 Joyce Er
560 2 Jakub
560 2 Jakub
561 2 Faris A Chugthai
561 2 Faris A Chugthai
562 2 Ethan Madden
562 2 Ethan Madden
563 2 Dimitri Papadopoulos
563 2 Dimitri Papadopoulos
564 2 Diego Fernandez
564 2 Diego Fernandez
565 2 Daniel Shimon
565 2 Daniel Shimon
566 2 Coco Bennett
566 2 Coco Bennett
567 2 Carlos Cordoba
567 2 Carlos Cordoba
568 2 Boyuan Liu
568 2 Boyuan Liu
569 2 BaoGiang HoangVu
569 2 BaoGiang HoangVu
570 2 Augusto
570 2 Augusto
571 2 Arthur Svistunov
571 2 Arthur Svistunov
572 2 Arthur Moreira
572 2 Arthur Moreira
573 2 Ali Nabipour
573 2 Ali Nabipour
574 2 Adam Hackbarth
574 2 Adam Hackbarth
575 1 richard
575 1 richard
576 1 linar-jether
576 1 linar-jether
577 1 lbennett
577 1 lbennett
578 1 juacrumar
578 1 juacrumar
579 1 gpotter2
579 1 gpotter2
580 1 digitalvirtuoso
580 1 digitalvirtuoso
581 1 dalthviz
581 1 dalthviz
582 1 Yonatan Goldschmidt
582 1 Yonatan Goldschmidt
583 1 Tomasz KΕ‚oczko
583 1 Tomasz KΕ‚oczko
584 1 Tobias Bengfort
584 1 Tobias Bengfort
585 1 Timur Kushukov
585 1 Timur Kushukov
586 1 Thomas
586 1 Thomas
587 1 Snir Broshi
587 1 Snir Broshi
588 1 Shao Yang Hong
588 1 Shao Yang Hong
589 1 Sanjana-03
589 1 Sanjana-03
590 1 Romulo Filho
590 1 Romulo Filho
591 1 Rodolfo Carvalho
591 1 Rodolfo Carvalho
592 1 Richard Shadrach
592 1 Richard Shadrach
593 1 Reilly Tucker Siemens
593 1 Reilly Tucker Siemens
594 1 Rakessh Roshan
594 1 Rakessh Roshan
595 1 Piers Titus van der Torren
595 1 Piers Titus van der Torren
596 1 PhanatosZou
596 1 PhanatosZou
597 1 Pavel Safronov
597 1 Pavel Safronov
598 1 Paulo S. Costa
598 1 Paulo S. Costa
599 1 Paul McCarthy
599 1 Paul McCarthy
600 1 NotWearingPants
600 1 NotWearingPants
601 1 Naelson Douglas
601 1 Naelson Douglas
602 1 Michael Tiemann
602 1 Michael Tiemann
603 1 Matt Wozniski
603 1 Matt Wozniski
604 1 Markus Wageringel
604 1 Markus Wageringel
605 1 Marcus Wirtz
605 1 Marcus Wirtz
606 1 Marcio Mazza
606 1 Marcio Mazza
607 1 LumΓ­r 'Frenzy' Balhar
607 1 LumΓ­r 'Frenzy' Balhar
608 1 Lightyagami1
608 1 Lightyagami1
609 1 Leon Anavi
609 1 Leon Anavi
610 1 LeafyLi
610 1 LeafyLi
611 1 L0uisJ0shua
611 1 L0uisJ0shua
612 1 Kyle Cutler
612 1 Kyle Cutler
613 1 Krzysztof Cybulski
613 1 Krzysztof Cybulski
614 1 Kevin Kirsche
614 1 Kevin Kirsche
615 1 KIU Shueng Chuan
615 1 KIU Shueng Chuan
616 1 Jonathan Slenders
616 1 Jonathan Slenders
617 1 Jay Qi
617 1 Jay Qi
618 1 Jake VanderPlas
618 1 Jake VanderPlas
619 1 Iwan Briquemont
619 1 Iwan Briquemont
620 1 Hussaina Begum Nandyala
620 1 Hussaina Begum Nandyala
621 1 Gordon Ball
621 1 Gordon Ball
622 1 Gabriel Simonetto
622 1 Gabriel Simonetto
623 1 Frank Tobia
623 1 Frank Tobia
624 1 Erik
624 1 Erik
625 1 Elliott Sales de Andrade
625 1 Elliott Sales de Andrade
626 1 Daniel Hahler
626 1 Daniel Hahler
627 1 Dan Green-Leipciger
627 1 Dan Green-Leipciger
628 1 Dan Green
628 1 Dan Green
629 1 Damian Yurzola
629 1 Damian Yurzola
630 1 Coon, Ethan T
630 1 Coon, Ethan T
631 1 Carol Willing
631 1 Carol Willing
632 1 Brian Lee
632 1 Brian Lee
633 1 Brendan Gerrity
633 1 Brendan Gerrity
634 1 Blake Griffin
634 1 Blake Griffin
635 1 Bastian Ebeling
635 1 Bastian Ebeling
636 1 Bartosz Telenczuk
636 1 Bartosz Telenczuk
637 1 Ankitsingh6299
637 1 Ankitsingh6299
638 1 Andrew Port
638 1 Andrew Port
639 1 Andrew J. Hesford
639 1 Andrew J. Hesford
640 1 Albert Zhang
640 1 Albert Zhang
641 1 Adam Johnson
641 1 Adam Johnson
642
642
643 This does not, of course, represent non-code contributions, for which we are also grateful.
643 This does not, of course, represent non-code contributions, for which we are also grateful.
644
644
645
645
646 API Changes using Frappuccino
646 API Changes using Frappuccino
647 -----------------------------
647 -----------------------------
648
648
649 This is an experimental exhaustive API difference using `Frappuccino <https://pypi.org/project/frappuccino/>`_
649 This is an experimental exhaustive API difference using `Frappuccino <https://pypi.org/project/frappuccino/>`_
650
650
651
651
652 The following items are new in IPython 8.0 ::
652 The following items are new in IPython 8.0 ::
653
653
654 + IPython.core.async_helpers.get_asyncio_loop()
654 + IPython.core.async_helpers.get_asyncio_loop()
655 + IPython.core.completer.Dict
655 + IPython.core.completer.Dict
656 + IPython.core.completer.Pattern
656 + IPython.core.completer.Pattern
657 + IPython.core.completer.Sequence
657 + IPython.core.completer.Sequence
658 + IPython.core.completer.__skip_doctest__
658 + IPython.core.completer.__skip_doctest__
659 + IPython.core.debugger.Pdb.precmd(self, line)
659 + IPython.core.debugger.Pdb.precmd(self, line)
660 + IPython.core.debugger.__skip_doctest__
660 + IPython.core.debugger.__skip_doctest__
661 + IPython.core.display.__getattr__(name)
661 + IPython.core.display.__getattr__(name)
662 + IPython.core.display.warn
662 + IPython.core.display.warn
663 + IPython.core.display_functions
663 + IPython.core.display_functions
664 + IPython.core.display_functions.DisplayHandle
664 + IPython.core.display_functions.DisplayHandle
665 + IPython.core.display_functions.DisplayHandle.display(self, obj, **kwargs)
665 + IPython.core.display_functions.DisplayHandle.display(self, obj, **kwargs)
666 + IPython.core.display_functions.DisplayHandle.update(self, obj, **kwargs)
666 + IPython.core.display_functions.DisplayHandle.update(self, obj, **kwargs)
667 + IPython.core.display_functions.__all__
667 + IPython.core.display_functions.__all__
668 + IPython.core.display_functions.__builtins__
668 + IPython.core.display_functions.__builtins__
669 + IPython.core.display_functions.__cached__
669 + IPython.core.display_functions.__cached__
670 + IPython.core.display_functions.__doc__
670 + IPython.core.display_functions.__doc__
671 + IPython.core.display_functions.__file__
671 + IPython.core.display_functions.__file__
672 + IPython.core.display_functions.__loader__
672 + IPython.core.display_functions.__loader__
673 + IPython.core.display_functions.__name__
673 + IPython.core.display_functions.__name__
674 + IPython.core.display_functions.__package__
674 + IPython.core.display_functions.__package__
675 + IPython.core.display_functions.__spec__
675 + IPython.core.display_functions.__spec__
676 + IPython.core.display_functions.b2a_hex
676 + IPython.core.display_functions.b2a_hex
677 + IPython.core.display_functions.clear_output(wait=False)
677 + IPython.core.display_functions.clear_output(wait=False)
678 + IPython.core.display_functions.display(*objs, include='None', exclude='None', metadata='None', transient='None', display_id='None', raw=False, clear=False, **kwargs)
678 + IPython.core.display_functions.display(*objs, include='None', exclude='None', metadata='None', transient='None', display_id='None', raw=False, clear=False, **kwargs)
679 + IPython.core.display_functions.publish_display_data(data, metadata='None', source='<deprecated>', *, transient='None', **kwargs)
679 + IPython.core.display_functions.publish_display_data(data, metadata='None', source='<deprecated>', *, transient='None', **kwargs)
680 + IPython.core.display_functions.update_display(obj, *, display_id, **kwargs)
680 + IPython.core.display_functions.update_display(obj, *, display_id, **kwargs)
681 + IPython.core.extensions.BUILTINS_EXTS
681 + IPython.core.extensions.BUILTINS_EXTS
682 + IPython.core.inputtransformer2.has_sunken_brackets(tokens)
682 + IPython.core.inputtransformer2.has_sunken_brackets(tokens)
683 + IPython.core.interactiveshell.Callable
683 + IPython.core.interactiveshell.Callable
684 + IPython.core.interactiveshell.__annotations__
684 + IPython.core.interactiveshell.__annotations__
685 + IPython.core.ultratb.List
685 + IPython.core.ultratb.List
686 + IPython.core.ultratb.Tuple
686 + IPython.core.ultratb.Tuple
687 + IPython.lib.pretty.CallExpression
687 + IPython.lib.pretty.CallExpression
688 + IPython.lib.pretty.CallExpression.factory(name)
688 + IPython.lib.pretty.CallExpression.factory(name)
689 + IPython.lib.pretty.RawStringLiteral
689 + IPython.lib.pretty.RawStringLiteral
690 + IPython.lib.pretty.RawText
690 + IPython.lib.pretty.RawText
691 + IPython.terminal.debugger.TerminalPdb.do_interact(self, arg)
691 + IPython.terminal.debugger.TerminalPdb.do_interact(self, arg)
692 + IPython.terminal.embed.Set
692 + IPython.terminal.embed.Set
693
693
694 The following items have been removed (or moved to superclass)::
694 The following items have been removed (or moved to superclass)::
695
695
696 - IPython.core.application.BaseIPythonApplication.initialize_subcommand
696 - IPython.core.application.BaseIPythonApplication.initialize_subcommand
697 - IPython.core.completer.Sentinel
697 - IPython.core.completer.Sentinel
698 - IPython.core.completer.skip_doctest
698 - IPython.core.completer.skip_doctest
699 - IPython.core.debugger.Tracer
699 - IPython.core.debugger.Tracer
700 - IPython.core.display.DisplayHandle
700 - IPython.core.display.DisplayHandle
701 - IPython.core.display.DisplayHandle.display
701 - IPython.core.display.DisplayHandle.display
702 - IPython.core.display.DisplayHandle.update
702 - IPython.core.display.DisplayHandle.update
703 - IPython.core.display.b2a_hex
703 - IPython.core.display.b2a_hex
704 - IPython.core.display.clear_output
704 - IPython.core.display.clear_output
705 - IPython.core.display.display
705 - IPython.core.display.display
706 - IPython.core.display.publish_display_data
706 - IPython.core.display.publish_display_data
707 - IPython.core.display.update_display
707 - IPython.core.display.update_display
708 - IPython.core.excolors.Deprec
708 - IPython.core.excolors.Deprec
709 - IPython.core.excolors.ExceptionColors
709 - IPython.core.excolors.ExceptionColors
710 - IPython.core.history.warn
710 - IPython.core.history.warn
711 - IPython.core.hooks.late_startup_hook
711 - IPython.core.hooks.late_startup_hook
712 - IPython.core.hooks.pre_run_code_hook
712 - IPython.core.hooks.pre_run_code_hook
713 - IPython.core.hooks.shutdown_hook
713 - IPython.core.hooks.shutdown_hook
714 - IPython.core.interactiveshell.InteractiveShell.init_deprecation_warnings
714 - IPython.core.interactiveshell.InteractiveShell.init_deprecation_warnings
715 - IPython.core.interactiveshell.InteractiveShell.init_readline
715 - IPython.core.interactiveshell.InteractiveShell.init_readline
716 - IPython.core.interactiveshell.InteractiveShell.write
716 - IPython.core.interactiveshell.InteractiveShell.write
717 - IPython.core.interactiveshell.InteractiveShell.write_err
717 - IPython.core.interactiveshell.InteractiveShell.write_err
718 - IPython.core.interactiveshell.get_default_colors
718 - IPython.core.interactiveshell.get_default_colors
719 - IPython.core.interactiveshell.removed_co_newlocals
719 - IPython.core.interactiveshell.removed_co_newlocals
720 - IPython.core.magics.execution.ExecutionMagics.profile_missing_notice
720 - IPython.core.magics.execution.ExecutionMagics.profile_missing_notice
721 - IPython.core.magics.script.PIPE
721 - IPython.core.magics.script.PIPE
722 - IPython.core.prefilter.PrefilterManager.init_transformers
722 - IPython.core.prefilter.PrefilterManager.init_transformers
723 - IPython.core.release.classifiers
723 - IPython.core.release.classifiers
724 - IPython.core.release.description
724 - IPython.core.release.description
725 - IPython.core.release.keywords
725 - IPython.core.release.keywords
726 - IPython.core.release.long_description
726 - IPython.core.release.long_description
727 - IPython.core.release.name
727 - IPython.core.release.name
728 - IPython.core.release.platforms
728 - IPython.core.release.platforms
729 - IPython.core.release.url
729 - IPython.core.release.url
730 - IPython.core.ultratb.VerboseTB.format_records
730 - IPython.core.ultratb.VerboseTB.format_records
731 - IPython.core.ultratb.find_recursion
731 - IPython.core.ultratb.find_recursion
732 - IPython.core.ultratb.findsource
732 - IPython.core.ultratb.findsource
733 - IPython.core.ultratb.fix_frame_records_filenames
733 - IPython.core.ultratb.fix_frame_records_filenames
734 - IPython.core.ultratb.inspect_error
734 - IPython.core.ultratb.inspect_error
735 - IPython.core.ultratb.is_recursion_error
735 - IPython.core.ultratb.is_recursion_error
736 - IPython.core.ultratb.with_patch_inspect
736 - IPython.core.ultratb.with_patch_inspect
737 - IPython.external.__all__
737 - IPython.external.__all__
738 - IPython.external.__builtins__
738 - IPython.external.__builtins__
739 - IPython.external.__cached__
739 - IPython.external.__cached__
740 - IPython.external.__doc__
740 - IPython.external.__doc__
741 - IPython.external.__file__
741 - IPython.external.__file__
742 - IPython.external.__loader__
742 - IPython.external.__loader__
743 - IPython.external.__name__
743 - IPython.external.__name__
744 - IPython.external.__package__
744 - IPython.external.__package__
745 - IPython.external.__path__
745 - IPython.external.__path__
746 - IPython.external.__spec__
746 - IPython.external.__spec__
747 - IPython.kernel.KernelConnectionInfo
747 - IPython.kernel.KernelConnectionInfo
748 - IPython.kernel.__builtins__
748 - IPython.kernel.__builtins__
749 - IPython.kernel.__cached__
749 - IPython.kernel.__cached__
750 - IPython.kernel.__warningregistry__
750 - IPython.kernel.__warningregistry__
751 - IPython.kernel.pkg
751 - IPython.kernel.pkg
752 - IPython.kernel.protocol_version
752 - IPython.kernel.protocol_version
753 - IPython.kernel.protocol_version_info
753 - IPython.kernel.protocol_version_info
754 - IPython.kernel.src
754 - IPython.kernel.src
755 - IPython.kernel.version_info
755 - IPython.kernel.version_info
756 - IPython.kernel.warn
756 - IPython.kernel.warn
757 - IPython.lib.backgroundjobs
757 - IPython.lib.backgroundjobs
758 - IPython.lib.backgroundjobs.BackgroundJobBase
758 - IPython.lib.backgroundjobs.BackgroundJobBase
759 - IPython.lib.backgroundjobs.BackgroundJobBase.run
759 - IPython.lib.backgroundjobs.BackgroundJobBase.run
760 - IPython.lib.backgroundjobs.BackgroundJobBase.traceback
760 - IPython.lib.backgroundjobs.BackgroundJobBase.traceback
761 - IPython.lib.backgroundjobs.BackgroundJobExpr
761 - IPython.lib.backgroundjobs.BackgroundJobExpr
762 - IPython.lib.backgroundjobs.BackgroundJobExpr.call
762 - IPython.lib.backgroundjobs.BackgroundJobExpr.call
763 - IPython.lib.backgroundjobs.BackgroundJobFunc
763 - IPython.lib.backgroundjobs.BackgroundJobFunc
764 - IPython.lib.backgroundjobs.BackgroundJobFunc.call
764 - IPython.lib.backgroundjobs.BackgroundJobFunc.call
765 - IPython.lib.backgroundjobs.BackgroundJobManager
765 - IPython.lib.backgroundjobs.BackgroundJobManager
766 - IPython.lib.backgroundjobs.BackgroundJobManager.flush
766 - IPython.lib.backgroundjobs.BackgroundJobManager.flush
767 - IPython.lib.backgroundjobs.BackgroundJobManager.new
767 - IPython.lib.backgroundjobs.BackgroundJobManager.new
768 - IPython.lib.backgroundjobs.BackgroundJobManager.remove
768 - IPython.lib.backgroundjobs.BackgroundJobManager.remove
769 - IPython.lib.backgroundjobs.BackgroundJobManager.result
769 - IPython.lib.backgroundjobs.BackgroundJobManager.result
770 - IPython.lib.backgroundjobs.BackgroundJobManager.status
770 - IPython.lib.backgroundjobs.BackgroundJobManager.status
771 - IPython.lib.backgroundjobs.BackgroundJobManager.traceback
771 - IPython.lib.backgroundjobs.BackgroundJobManager.traceback
772 - IPython.lib.backgroundjobs.__builtins__
772 - IPython.lib.backgroundjobs.__builtins__
773 - IPython.lib.backgroundjobs.__cached__
773 - IPython.lib.backgroundjobs.__cached__
774 - IPython.lib.backgroundjobs.__doc__
774 - IPython.lib.backgroundjobs.__doc__
775 - IPython.lib.backgroundjobs.__file__
775 - IPython.lib.backgroundjobs.__file__
776 - IPython.lib.backgroundjobs.__loader__
776 - IPython.lib.backgroundjobs.__loader__
777 - IPython.lib.backgroundjobs.__name__
777 - IPython.lib.backgroundjobs.__name__
778 - IPython.lib.backgroundjobs.__package__
778 - IPython.lib.backgroundjobs.__package__
779 - IPython.lib.backgroundjobs.__spec__
779 - IPython.lib.backgroundjobs.__spec__
780 - IPython.lib.kernel.__builtins__
780 - IPython.lib.kernel.__builtins__
781 - IPython.lib.kernel.__cached__
781 - IPython.lib.kernel.__cached__
782 - IPython.lib.kernel.__doc__
782 - IPython.lib.kernel.__doc__
783 - IPython.lib.kernel.__file__
783 - IPython.lib.kernel.__file__
784 - IPython.lib.kernel.__loader__
784 - IPython.lib.kernel.__loader__
785 - IPython.lib.kernel.__name__
785 - IPython.lib.kernel.__name__
786 - IPython.lib.kernel.__package__
786 - IPython.lib.kernel.__package__
787 - IPython.lib.kernel.__spec__
787 - IPython.lib.kernel.__spec__
788 - IPython.lib.kernel.__warningregistry__
788 - IPython.lib.kernel.__warningregistry__
789 - IPython.paths.fs_encoding
789 - IPython.paths.fs_encoding
790 - IPython.terminal.debugger.DEFAULT_BUFFER
790 - IPython.terminal.debugger.DEFAULT_BUFFER
791 - IPython.terminal.debugger.cursor_in_leading_ws
791 - IPython.terminal.debugger.cursor_in_leading_ws
792 - IPython.terminal.debugger.emacs_insert_mode
792 - IPython.terminal.debugger.emacs_insert_mode
793 - IPython.terminal.debugger.has_selection
793 - IPython.terminal.debugger.has_selection
794 - IPython.terminal.debugger.vi_insert_mode
794 - IPython.terminal.debugger.vi_insert_mode
795 - IPython.terminal.interactiveshell.DISPLAY_BANNER_DEPRECATED
795 - IPython.terminal.interactiveshell.DISPLAY_BANNER_DEPRECATED
796 - IPython.terminal.ipapp.TerminalIPythonApp.parse_command_line
796 - IPython.terminal.ipapp.TerminalIPythonApp.parse_command_line
797 - IPython.testing.test
797 - IPython.testing.test
798 - IPython.utils.contexts.NoOpContext
798 - IPython.utils.contexts.NoOpContext
799 - IPython.utils.io.IOStream
799 - IPython.utils.io.IOStream
800 - IPython.utils.io.IOStream.close
800 - IPython.utils.io.IOStream.close
801 - IPython.utils.io.IOStream.write
801 - IPython.utils.io.IOStream.write
802 - IPython.utils.io.IOStream.writelines
802 - IPython.utils.io.IOStream.writelines
803 - IPython.utils.io.__warningregistry__
803 - IPython.utils.io.__warningregistry__
804 - IPython.utils.io.atomic_writing
804 - IPython.utils.io.atomic_writing
805 - IPython.utils.io.stderr
805 - IPython.utils.io.stderr
806 - IPython.utils.io.stdin
806 - IPython.utils.io.stdin
807 - IPython.utils.io.stdout
807 - IPython.utils.io.stdout
808 - IPython.utils.io.unicode_std_stream
808 - IPython.utils.io.unicode_std_stream
809 - IPython.utils.path.get_ipython_cache_dir
809 - IPython.utils.path.get_ipython_cache_dir
810 - IPython.utils.path.get_ipython_dir
810 - IPython.utils.path.get_ipython_dir
811 - IPython.utils.path.get_ipython_module_path
811 - IPython.utils.path.get_ipython_module_path
812 - IPython.utils.path.get_ipython_package_dir
812 - IPython.utils.path.get_ipython_package_dir
813 - IPython.utils.path.locate_profile
813 - IPython.utils.path.locate_profile
814 - IPython.utils.path.unquote_filename
814 - IPython.utils.path.unquote_filename
815 - IPython.utils.py3compat.PY2
815 - IPython.utils.py3compat.PY2
816 - IPython.utils.py3compat.PY3
816 - IPython.utils.py3compat.PY3
817 - IPython.utils.py3compat.buffer_to_bytes
817 - IPython.utils.py3compat.buffer_to_bytes
818 - IPython.utils.py3compat.builtin_mod_name
818 - IPython.utils.py3compat.builtin_mod_name
819 - IPython.utils.py3compat.cast_bytes
819 - IPython.utils.py3compat.cast_bytes
820 - IPython.utils.py3compat.getcwd
820 - IPython.utils.py3compat.getcwd
821 - IPython.utils.py3compat.isidentifier
821 - IPython.utils.py3compat.isidentifier
822 - IPython.utils.py3compat.u_format
822 - IPython.utils.py3compat.u_format
823
823
824 The following signatures differ between 7.x and 8.0::
824 The following signatures differ between 7.x and 8.0::
825
825
826 - IPython.core.completer.IPCompleter.unicode_name_matches(self, text)
826 - IPython.core.completer.IPCompleter.unicode_name_matches(self, text)
827 + IPython.core.completer.IPCompleter.unicode_name_matches(text)
827 + IPython.core.completer.IPCompleter.unicode_name_matches(text)
828
828
829 - IPython.core.completer.match_dict_keys(keys, prefix, delims)
829 - IPython.core.completer.match_dict_keys(keys, prefix, delims)
830 + IPython.core.completer.match_dict_keys(keys, prefix, delims, extra_prefix='None')
830 + IPython.core.completer.match_dict_keys(keys, prefix, delims, extra_prefix='None')
831
831
832 - IPython.core.interactiveshell.InteractiveShell.object_inspect_mime(self, oname, detail_level=0)
832 - IPython.core.interactiveshell.InteractiveShell.object_inspect_mime(self, oname, detail_level=0)
833 + IPython.core.interactiveshell.InteractiveShell.object_inspect_mime(self, oname, detail_level=0, omit_sections='()')
833 + IPython.core.interactiveshell.InteractiveShell.object_inspect_mime(self, oname, detail_level=0, omit_sections='()')
834
834
835 - IPython.core.interactiveshell.InteractiveShell.set_hook(self, name, hook, priority=50, str_key='None', re_key='None', _warn_deprecated=True)
835 - IPython.core.interactiveshell.InteractiveShell.set_hook(self, name, hook, priority=50, str_key='None', re_key='None', _warn_deprecated=True)
836 + IPython.core.interactiveshell.InteractiveShell.set_hook(self, name, hook, priority=50, str_key='None', re_key='None')
836 + IPython.core.interactiveshell.InteractiveShell.set_hook(self, name, hook, priority=50, str_key='None', re_key='None')
837
837
838 - IPython.core.oinspect.Inspector.info(self, obj, oname='', formatter='None', info='None', detail_level=0)
838 - IPython.core.oinspect.Inspector.info(self, obj, oname='', formatter='None', info='None', detail_level=0)
839 + IPython.core.oinspect.Inspector.info(self, obj, oname='', info='None', detail_level=0)
839 + IPython.core.oinspect.Inspector.info(self, obj, oname='', info='None', detail_level=0)
840
840
841 - IPython.core.oinspect.Inspector.pinfo(self, obj, oname='', formatter='None', info='None', detail_level=0, enable_html_pager=True)
841 - IPython.core.oinspect.Inspector.pinfo(self, obj, oname='', formatter='None', info='None', detail_level=0, enable_html_pager=True)
842 + IPython.core.oinspect.Inspector.pinfo(self, obj, oname='', formatter='None', info='None', detail_level=0, enable_html_pager=True, omit_sections='()')
842 + IPython.core.oinspect.Inspector.pinfo(self, obj, oname='', formatter='None', info='None', detail_level=0, enable_html_pager=True, omit_sections='()')
843
843
844 - IPython.core.profiledir.ProfileDir.copy_config_file(self, config_file, path='None', overwrite=False)
844 - IPython.core.profiledir.ProfileDir.copy_config_file(self, config_file, path='None', overwrite=False)
845 + IPython.core.profiledir.ProfileDir.copy_config_file(self, config_file, path, overwrite=False)
845 + IPython.core.profiledir.ProfileDir.copy_config_file(self, config_file, path, overwrite=False)
846
846
847 - IPython.core.ultratb.VerboseTB.format_record(self, frame, file, lnum, func, lines, index)
847 - IPython.core.ultratb.VerboseTB.format_record(self, frame, file, lnum, func, lines, index)
848 + IPython.core.ultratb.VerboseTB.format_record(self, frame_info)
848 + IPython.core.ultratb.VerboseTB.format_record(self, frame_info)
849
849
850 - IPython.terminal.embed.InteractiveShellEmbed.mainloop(self, local_ns='None', module='None', stack_depth=0, display_banner='None', global_ns='None', compile_flags='None')
850 - IPython.terminal.embed.InteractiveShellEmbed.mainloop(self, local_ns='None', module='None', stack_depth=0, display_banner='None', global_ns='None', compile_flags='None')
851 + IPython.terminal.embed.InteractiveShellEmbed.mainloop(self, local_ns='None', module='None', stack_depth=0, compile_flags='None')
851 + IPython.terminal.embed.InteractiveShellEmbed.mainloop(self, local_ns='None', module='None', stack_depth=0, compile_flags='None')
852
852
853 - IPython.terminal.embed.embed(**kwargs)
853 - IPython.terminal.embed.embed(**kwargs)
854 + IPython.terminal.embed.embed(*, header='', compile_flags='None', **kwargs)
854 + IPython.terminal.embed.embed(*, header='', compile_flags='None', **kwargs)
855
855
856 - IPython.terminal.interactiveshell.TerminalInteractiveShell.interact(self, display_banner='<object object at 0xffffff>')
856 - IPython.terminal.interactiveshell.TerminalInteractiveShell.interact(self, display_banner='<object object at 0xffffff>')
857 + IPython.terminal.interactiveshell.TerminalInteractiveShell.interact(self)
857 + IPython.terminal.interactiveshell.TerminalInteractiveShell.interact(self)
858
858
859 - IPython.terminal.interactiveshell.TerminalInteractiveShell.mainloop(self, display_banner='<object object at 0xffffff>')
859 - IPython.terminal.interactiveshell.TerminalInteractiveShell.mainloop(self, display_banner='<object object at 0xffffff>')
860 + IPython.terminal.interactiveshell.TerminalInteractiveShell.mainloop(self)
860 + IPython.terminal.interactiveshell.TerminalInteractiveShell.mainloop(self)
861
861
862 - IPython.utils.path.get_py_filename(name, force_win32='None')
862 - IPython.utils.path.get_py_filename(name, force_win32='None')
863 + IPython.utils.path.get_py_filename(name)
863 + IPython.utils.path.get_py_filename(name)
864
864
865 The following are new attributes (that might be inherited)::
865 The following are new attributes (that might be inherited)::
866
866
867 + IPython.core.completer.IPCompleter.unicode_names
867 + IPython.core.completer.IPCompleter.unicode_names
868 + IPython.core.debugger.InterruptiblePdb.precmd
868 + IPython.core.debugger.InterruptiblePdb.precmd
869 + IPython.core.debugger.Pdb.precmd
869 + IPython.core.debugger.Pdb.precmd
870 + IPython.core.ultratb.AutoFormattedTB.has_colors
870 + IPython.core.ultratb.AutoFormattedTB.has_colors
871 + IPython.core.ultratb.ColorTB.has_colors
871 + IPython.core.ultratb.ColorTB.has_colors
872 + IPython.core.ultratb.FormattedTB.has_colors
872 + IPython.core.ultratb.FormattedTB.has_colors
873 + IPython.core.ultratb.ListTB.has_colors
873 + IPython.core.ultratb.ListTB.has_colors
874 + IPython.core.ultratb.SyntaxTB.has_colors
874 + IPython.core.ultratb.SyntaxTB.has_colors
875 + IPython.core.ultratb.TBTools.has_colors
875 + IPython.core.ultratb.TBTools.has_colors
876 + IPython.core.ultratb.VerboseTB.has_colors
876 + IPython.core.ultratb.VerboseTB.has_colors
877 + IPython.terminal.debugger.TerminalPdb.do_interact
877 + IPython.terminal.debugger.TerminalPdb.do_interact
878 + IPython.terminal.debugger.TerminalPdb.precmd
878 + IPython.terminal.debugger.TerminalPdb.precmd
879
879
880 The following attribute/methods have been removed::
880 The following attribute/methods have been removed::
881
881
882 - IPython.core.application.BaseIPythonApplication.deprecated_subcommands
882 - IPython.core.application.BaseIPythonApplication.deprecated_subcommands
883 - IPython.core.ultratb.AutoFormattedTB.format_records
883 - IPython.core.ultratb.AutoFormattedTB.format_records
884 - IPython.core.ultratb.ColorTB.format_records
884 - IPython.core.ultratb.ColorTB.format_records
885 - IPython.core.ultratb.FormattedTB.format_records
885 - IPython.core.ultratb.FormattedTB.format_records
886 - IPython.terminal.embed.InteractiveShellEmbed.init_deprecation_warnings
886 - IPython.terminal.embed.InteractiveShellEmbed.init_deprecation_warnings
887 - IPython.terminal.embed.InteractiveShellEmbed.init_readline
887 - IPython.terminal.embed.InteractiveShellEmbed.init_readline
888 - IPython.terminal.embed.InteractiveShellEmbed.write
888 - IPython.terminal.embed.InteractiveShellEmbed.write
889 - IPython.terminal.embed.InteractiveShellEmbed.write_err
889 - IPython.terminal.embed.InteractiveShellEmbed.write_err
890 - IPython.terminal.interactiveshell.TerminalInteractiveShell.init_deprecation_warnings
890 - IPython.terminal.interactiveshell.TerminalInteractiveShell.init_deprecation_warnings
891 - IPython.terminal.interactiveshell.TerminalInteractiveShell.init_readline
891 - IPython.terminal.interactiveshell.TerminalInteractiveShell.init_readline
892 - IPython.terminal.interactiveshell.TerminalInteractiveShell.write
892 - IPython.terminal.interactiveshell.TerminalInteractiveShell.write
893 - IPython.terminal.interactiveshell.TerminalInteractiveShell.write_err
893 - IPython.terminal.interactiveshell.TerminalInteractiveShell.write_err
894 - IPython.terminal.ipapp.LocateIPythonApp.deprecated_subcommands
894 - IPython.terminal.ipapp.LocateIPythonApp.deprecated_subcommands
895 - IPython.terminal.ipapp.LocateIPythonApp.initialize_subcommand
895 - IPython.terminal.ipapp.LocateIPythonApp.initialize_subcommand
896 - IPython.terminal.ipapp.TerminalIPythonApp.deprecated_subcommands
896 - IPython.terminal.ipapp.TerminalIPythonApp.deprecated_subcommands
897 - IPython.terminal.ipapp.TerminalIPythonApp.initialize_subcommand
897 - IPython.terminal.ipapp.TerminalIPythonApp.initialize_subcommand
@@ -1,72 +1,116 b''
1 [metadata]
1 [metadata]
2 name = ipython
2 name = ipython
3 version = attr: IPython.core.release.__version__
3 version = attr: IPython.core.release.__version__
4 url = https://ipython.org
4 url = https://ipython.org
5 description = IPython: Productive Interactive Computing
5 description = IPython: Productive Interactive Computing
6 long_description_content_type = text/x-rst
6 long_description_content_type = text/x-rst
7 long_description = file: long_description.rst
7 long_description = file: long_description.rst
8 license_file = LICENSE
8 license_file = LICENSE
9 project_urls =
9 project_urls =
10 Documentation = https://ipython.readthedocs.io/
10 Documentation = https://ipython.readthedocs.io/
11 Funding = https://numfocus.org/
11 Funding = https://numfocus.org/
12 Source = https://github.com/ipython/ipython
12 Source = https://github.com/ipython/ipython
13 Tracker = https://github.com/ipython/ipython/issues
13 Tracker = https://github.com/ipython/ipython/issues
14 keywords = Interactive, Interpreter, Shell, Embedding
14 keywords = Interactive, Interpreter, Shell, Embedding
15 platforms = Linux, Mac OSX, Windows
15 platforms = Linux, Mac OSX, Windows
16 classifiers =
16 classifiers =
17 Framework :: IPython
17 Framework :: IPython
18 Framework :: Jupyter
18 Intended Audience :: Developers
19 Intended Audience :: Developers
19 Intended Audience :: Science/Research
20 Intended Audience :: Science/Research
20 License :: OSI Approved :: BSD License
21 License :: OSI Approved :: BSD License
21 Programming Language :: Python
22 Programming Language :: Python
22 Programming Language :: Python :: 3
23 Programming Language :: Python :: 3
23 Programming Language :: Python :: 3 :: Only
24 Programming Language :: Python :: 3 :: Only
24 Topic :: System :: Shells
25 Topic :: System :: Shells
25
26
26
27 [options]
27 [options]
28 packages = find:
28 packages = find:
29 python_requires = >=3.8
29 python_requires = >=3.8
30 zip_safe = False
30 zip_safe = False
31 install_requires =
31 install_requires =
32 setuptools>=18.5
32 appnope; sys_platform == "darwin"
33 jedi>=0.16
33 backcall
34 black
34 colorama; sys_platform == "win32"
35 decorator
35 decorator
36 jedi>=0.16
37 matplotlib-inline
38 pexpect>4.3; sys_platform != "win32"
36 pickleshare
39 pickleshare
37 traitlets>=5
38 prompt_toolkit>=2.0.0,<3.1.0,!=3.0.0,!=3.0.1
40 prompt_toolkit>=2.0.0,<3.1.0,!=3.0.0,!=3.0.1
39 pygments>=2.4.0
41 pygments>=2.4.0
40 backcall
42 setuptools>=18.5
41 stack_data
43 stack_data
42 matplotlib-inline
44 traitlets>=5
43 pexpect>4.3; sys_platform != "win32"
45
44 appnope; sys_platform == "darwin"
46 [options.extras_require]
45 colorama; sys_platform == "win32"
47 black =
48 black
49 doc =
50 Sphinx>=1.3
51 kernel =
52 ipykernel
53 nbconvert =
54 nbconvert
55 nbformat =
56 nbformat
57 notebook =
58 ipywidgets
59 notebook
60 parallel =
61 ipyparallel
62 qtconsole =
63 qtconsole
64 terminal =
65 test =
66 pytest
67 pytest-asyncio
68 testpath
69 test_extra =
70 curio
71 matplotlib!=3.2.0
72 nbformat
73 numpy>=1.19
74 pandas
75 pytest
76 testpath
77 trio
78 all =
79 %(black)s
80 %(doc)s
81 %(kernel)s
82 %(nbconvert)s
83 %(nbformat)s
84 %(notebook)s
85 %(parallel)s
86 %(qtconsole)s
87 %(terminal)s
88 %(test_extra)s
89 %(test)s
46
90
47 [options.packages.find]
91 [options.packages.find]
48 exclude =
92 exclude =
49 setupext
93 setupext
50
94
51 [options.package_data]
95 [options.package_data]
52 IPython.core = profile/README*
96 IPython.core = profile/README*
53 IPython.core.tests = *.png, *.jpg, daft_extension/*.py
97 IPython.core.tests = *.png, *.jpg, daft_extension/*.py
54 IPython.lib.tests = *.wav
98 IPython.lib.tests = *.wav
55 IPython.testing.plugin = *.txt
99 IPython.testing.plugin = *.txt
56
100
57 [options.entry_points]
101 [options.entry_points]
58 console_scripts =
102 console_scripts =
59 ipython = IPython:start_ipython
103 ipython = IPython:start_ipython
60 ipython3 = IPython:start_ipython
104 ipython3 = IPython:start_ipython
61 pygments.lexers =
105 pygments.lexers =
62 ipythonconsole = IPython.lib.lexers:IPythonConsoleLexer
106 ipythonconsole = IPython.lib.lexers:IPythonConsoleLexer
63 ipython = IPython.lib.lexers:IPythonLexer
107 ipython = IPython.lib.lexers:IPythonLexer
64 ipython3 = IPython.lib.lexers:IPython3Lexer
108 ipython3 = IPython.lib.lexers:IPython3Lexer
65
109
66 [velin]
110 [velin]
67 ignore_patterns =
111 ignore_patterns =
68 IPython/core/tests
112 IPython/core/tests
69 IPython/testing
113 IPython/testing
70
114
71 [tool.black]
115 [tool.black]
72 exclude = 'timing\.py'
116 exclude = 'timing\.py'
@@ -1,185 +1,145 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Setup script for IPython.
2 """Setup script for IPython.
3
3
4 Under Posix environments it works like a typical setup.py script.
4 Under Posix environments it works like a typical setup.py script.
5 Under Windows, the command sdist is not supported, since IPython
5 Under Windows, the command sdist is not supported, since IPython
6 requires utilities which are not available under Windows."""
6 requires utilities which are not available under Windows."""
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (c) 2008-2011, IPython Development Team.
9 # Copyright (c) 2008-2011, IPython Development Team.
10 # Copyright (c) 2001-2007, Fernando Perez <fernando.perez@colorado.edu>
10 # Copyright (c) 2001-2007, Fernando Perez <fernando.perez@colorado.edu>
11 # Copyright (c) 2001, Janko Hauser <jhauser@zscout.de>
11 # Copyright (c) 2001, Janko Hauser <jhauser@zscout.de>
12 # Copyright (c) 2001, Nathaniel Gray <n8gray@caltech.edu>
12 # Copyright (c) 2001, Nathaniel Gray <n8gray@caltech.edu>
13 #
13 #
14 # Distributed under the terms of the Modified BSD License.
14 # Distributed under the terms of the Modified BSD License.
15 #
15 #
16 # The full license is in the file COPYING.rst, distributed with this software.
16 # The full license is in the file COPYING.rst, distributed with this software.
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import os
19 import os
20 import sys
20 import sys
21 from itertools import chain
21 from itertools import chain
22
22
23 # **Python version check**
23 # **Python version check**
24 #
24 #
25 # This check is also made in IPython/__init__, don't forget to update both when
25 # This check is also made in IPython/__init__, don't forget to update both when
26 # changing Python version requirements.
26 # changing Python version requirements.
27 if sys.version_info < (3, 8):
27 if sys.version_info < (3, 8):
28 pip_message = 'This may be due to an out of date pip. Make sure you have pip >= 9.0.1.'
28 pip_message = 'This may be due to an out of date pip. Make sure you have pip >= 9.0.1.'
29 try:
29 try:
30 import pip
30 import pip
31 pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
31 pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
32 if pip_version < (9, 0, 1) :
32 if pip_version < (9, 0, 1) :
33 pip_message = 'Your pip version is out of date, please install pip >= 9.0.1. '\
33 pip_message = 'Your pip version is out of date, please install pip >= 9.0.1. '\
34 'pip {} detected.'.format(pip.__version__)
34 'pip {} detected.'.format(pip.__version__)
35 else:
35 else:
36 # pip is new enough - it must be something else
36 # pip is new enough - it must be something else
37 pip_message = ''
37 pip_message = ''
38 except Exception:
38 except Exception:
39 pass
39 pass
40
40
41
41
42 error = """
42 error = """
43 IPython 8+ supports Python 3.8 and above, following NEP 29.
43 IPython 8+ supports Python 3.8 and above, following NEP 29.
44 When using Python 2.7, please install IPython 5.x LTS Long Term Support version.
44 When using Python 2.7, please install IPython 5.x LTS Long Term Support version.
45 Python 3.3 and 3.4 were supported up to IPython 6.x.
45 Python 3.3 and 3.4 were supported up to IPython 6.x.
46 Python 3.5 was supported with IPython 7.0 to 7.9.
46 Python 3.5 was supported with IPython 7.0 to 7.9.
47 Python 3.6 was supported with IPython up to 7.16.
47 Python 3.6 was supported with IPython up to 7.16.
48 Python 3.7 was still supported with the 7.x branch.
48 Python 3.7 was still supported with the 7.x branch.
49
49
50 See IPython `README.rst` file for more information:
50 See IPython `README.rst` file for more information:
51
51
52 https://github.com/ipython/ipython/blob/master/README.rst
52 https://github.com/ipython/ipython/blob/master/README.rst
53
53
54 Python {py} detected.
54 Python {py} detected.
55 {pip}
55 {pip}
56 """.format(py=sys.version_info, pip=pip_message )
56 """.format(py=sys.version_info, pip=pip_message )
57
57
58 print(error, file=sys.stderr)
58 print(error, file=sys.stderr)
59 sys.exit(1)
59 sys.exit(1)
60
60
61 # At least we're on the python version we need, move on.
61 # At least we're on the python version we need, move on.
62
62
63 from setuptools import setup
63 from setuptools import setup
64
64
65 # Our own imports
65 # Our own imports
66 from setupbase import target_update
66 from setupbase import target_update
67
67
68 from setupbase import (
68 from setupbase import (
69 setup_args,
69 setup_args,
70 check_package_data_first,
70 check_package_data_first,
71 find_data_files,
71 find_data_files,
72 git_prebuild,
72 git_prebuild,
73 install_symlinked,
73 install_symlinked,
74 install_lib_symlink,
74 install_lib_symlink,
75 install_scripts_for_symlink,
75 install_scripts_for_symlink,
76 unsymlink,
76 unsymlink,
77 )
77 )
78
78
79 #-------------------------------------------------------------------------------
79 #-------------------------------------------------------------------------------
80 # Handle OS specific things
80 # Handle OS specific things
81 #-------------------------------------------------------------------------------
81 #-------------------------------------------------------------------------------
82
82
83 if os.name in ('nt','dos'):
83 if os.name in ('nt','dos'):
84 os_name = 'windows'
84 os_name = 'windows'
85 else:
85 else:
86 os_name = os.name
86 os_name = os.name
87
87
88 # Under Windows, 'sdist' has not been supported. Now that the docs build with
88 # Under Windows, 'sdist' has not been supported. Now that the docs build with
89 # Sphinx it might work, but let's not turn it on until someone confirms that it
89 # Sphinx it might work, but let's not turn it on until someone confirms that it
90 # actually works.
90 # actually works.
91 if os_name == 'windows' and 'sdist' in sys.argv:
91 if os_name == 'windows' and 'sdist' in sys.argv:
92 print('The sdist command is not available under Windows. Exiting.')
92 print('The sdist command is not available under Windows. Exiting.')
93 sys.exit(1)
93 sys.exit(1)
94
94
95
95
96 #-------------------------------------------------------------------------------
96 #-------------------------------------------------------------------------------
97 # Things related to the IPython documentation
97 # Things related to the IPython documentation
98 #-------------------------------------------------------------------------------
98 #-------------------------------------------------------------------------------
99
99
100 # update the manuals when building a source dist
100 # update the manuals when building a source dist
101 if len(sys.argv) >= 2 and sys.argv[1] in ('sdist','bdist_rpm'):
101 if len(sys.argv) >= 2 and sys.argv[1] in ('sdist','bdist_rpm'):
102
102
103 # List of things to be updated. Each entry is a triplet of args for
103 # List of things to be updated. Each entry is a triplet of args for
104 # target_update()
104 # target_update()
105 to_update = [
105 to_update = [
106 (
106 (
107 "docs/man/ipython.1.gz",
107 "docs/man/ipython.1.gz",
108 ["docs/man/ipython.1"],
108 ["docs/man/ipython.1"],
109 "cd docs/man && python -m gzip --best ipython.1",
109 "cd docs/man && python -m gzip --best ipython.1",
110 ),
110 ),
111 ]
111 ]
112
112
113
113
114 [ target_update(*t) for t in to_update ]
114 [ target_update(*t) for t in to_update ]
115
115
116 #---------------------------------------------------------------------------
116 #---------------------------------------------------------------------------
117 # Find all the packages, package data, and data_files
117 # Find all the packages, package data, and data_files
118 #---------------------------------------------------------------------------
118 #---------------------------------------------------------------------------
119
119
120 data_files = find_data_files()
120 data_files = find_data_files()
121
121
122 setup_args['data_files'] = data_files
122 setup_args['data_files'] = data_files
123
123
124 #---------------------------------------------------------------------------
124 #---------------------------------------------------------------------------
125 # custom distutils commands
125 # custom distutils commands
126 #---------------------------------------------------------------------------
126 #---------------------------------------------------------------------------
127 # imports here, so they are after setuptools import if there was one
127 # imports here, so they are after setuptools import if there was one
128 from setuptools.command.sdist import sdist
128 from setuptools.command.sdist import sdist
129
129
130 setup_args['cmdclass'] = {
130 setup_args['cmdclass'] = {
131 'build_py': \
131 'build_py': \
132 check_package_data_first(git_prebuild('IPython')),
132 check_package_data_first(git_prebuild('IPython')),
133 'sdist' : git_prebuild('IPython', sdist),
133 'sdist' : git_prebuild('IPython', sdist),
134 'symlink': install_symlinked,
134 'symlink': install_symlinked,
135 'install_lib_symlink': install_lib_symlink,
135 'install_lib_symlink': install_lib_symlink,
136 'install_scripts_sym': install_scripts_for_symlink,
136 'install_scripts_sym': install_scripts_for_symlink,
137 'unsymlink': unsymlink,
137 'unsymlink': unsymlink,
138 }
138 }
139
139
140
141 #---------------------------------------------------------------------------
142 # Handle scripts, dependencies, and setuptools specific things
143 #---------------------------------------------------------------------------
144
145 # setuptools requirements
146
147 extras_require = dict(
148 parallel=["ipyparallel"],
149 qtconsole=["qtconsole"],
150 doc=["Sphinx>=1.3"],
151 test=[
152 "pytest",
153 "pytest-asyncio",
154 "testpath",
155 "pygments>=2.4.0",
156 ],
157 test_extra=[
158 "pytest",
159 "testpath",
160 "curio",
161 "matplotlib!=3.2.0",
162 "nbformat",
163 "numpy>=1.19",
164 "pandas",
165 "pygments>=2.4.0",
166 "trio",
167 ],
168 terminal=[],
169 kernel=["ipykernel"],
170 nbformat=["nbformat"],
171 notebook=["notebook", "ipywidgets"],
172 nbconvert=["nbconvert"],
173 )
174
175 everything = set(chain.from_iterable(extras_require.values()))
176 extras_require['all'] = list(sorted(everything))
177
178 setup_args["extras_require"] = extras_require
179
180 #---------------------------------------------------------------------------
140 #---------------------------------------------------------------------------
181 # Do the actual setup now
141 # Do the actual setup now
182 #---------------------------------------------------------------------------
142 #---------------------------------------------------------------------------
183
143
184 if __name__ == "__main__":
144 if __name__ == "__main__":
185 setup(**setup_args)
145 setup(**setup_args)
General Comments 0
You need to be logged in to leave comments. Login now