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