##// END OF EJS Templates
FIX: Typing annotations (#12683)...
M Bussonnier -
r28949:5d95565e merge
parent child Browse files
Show More
@@ -1,798 +1,799
1 1 """DEPRECATED: Input handling and transformation machinery.
2 2
3 3 This module was deprecated in IPython 7.0, in favour of inputtransformer2.
4 4
5 5 The first class in this module, :class:`InputSplitter`, is designed to tell when
6 6 input from a line-oriented frontend is complete and should be executed, and when
7 7 the user should be prompted for another line of code instead. The name 'input
8 8 splitter' is largely for historical reasons.
9 9
10 10 A companion, :class:`IPythonInputSplitter`, provides the same functionality but
11 11 with full support for the extended IPython syntax (magics, system calls, etc).
12 12 The code to actually do these transformations is in :mod:`IPython.core.inputtransformer`.
13 13 :class:`IPythonInputSplitter` feeds the raw code to the transformers in order
14 14 and stores the results.
15 15
16 16 For more details, see the class docstrings below.
17 17 """
18
18 19 from __future__ import annotations
19 20
20 21 from warnings import warn
21 22
22 23 warn('IPython.core.inputsplitter is deprecated since IPython 7 in favor of `IPython.core.inputtransformer2`',
23 24 DeprecationWarning)
24 25
25 26 # Copyright (c) IPython Development Team.
26 27 # Distributed under the terms of the Modified BSD License.
27 28 import ast
28 29 import codeop
29 30 import io
30 31 import re
31 32 import sys
32 33 import tokenize
33 34 import warnings
34 35
35 36 from typing import List, Tuple, Union, Optional, TYPE_CHECKING
36 37 from types import CodeType
37 38
38 39 from IPython.core.inputtransformer import (leading_indent,
39 40 classic_prompt,
40 41 ipy_prompt,
41 42 cellmagic,
42 43 assemble_logical_lines,
43 44 help_end,
44 45 escaped_commands,
45 46 assign_from_magic,
46 47 assign_from_system,
47 48 assemble_python_lines,
48 49 )
49 50 from IPython.utils import tokenutil
50 51
51 52 # These are available in this module for backwards compatibility.
52 53 from IPython.core.inputtransformer import (ESC_SHELL, ESC_SH_CAP, ESC_HELP,
53 54 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,
54 55 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN, ESC_SEQUENCES)
55 56
56 57 if TYPE_CHECKING:
57 58 from typing_extensions import Self
58 59 #-----------------------------------------------------------------------------
59 60 # Utilities
60 61 #-----------------------------------------------------------------------------
61 62
62 63 # FIXME: These are general-purpose utilities that later can be moved to the
63 64 # general ward. Kept here for now because we're being very strict about test
64 65 # coverage with this code, and this lets us ensure that we keep 100% coverage
65 66 # while developing.
66 67
67 68 # compiled regexps for autoindent management
68 69 dedent_re = re.compile('|'.join([
69 70 r'^\s+raise(\s.*)?$', # raise statement (+ space + other stuff, maybe)
70 71 r'^\s+raise\([^\)]*\).*$', # wacky raise with immediate open paren
71 72 r'^\s+return(\s.*)?$', # normal return (+ space + other stuff, maybe)
72 73 r'^\s+return\([^\)]*\).*$', # wacky return with immediate open paren
73 74 r'^\s+pass\s*$', # pass (optionally followed by trailing spaces)
74 75 r'^\s+break\s*$', # break (optionally followed by trailing spaces)
75 76 r'^\s+continue\s*$', # continue (optionally followed by trailing spaces)
76 77 ]))
77 78 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
78 79
79 80 # regexp to match pure comment lines so we don't accidentally insert 'if 1:'
80 81 # before pure comments
81 82 comment_line_re = re.compile(r'^\s*\#')
82 83
83 84
84 85 def num_ini_spaces(s):
85 86 """Return the number of initial spaces in a string.
86 87
87 88 Note that tabs are counted as a single space. For now, we do *not* support
88 89 mixing of tabs and spaces in the user's input.
89 90
90 91 Parameters
91 92 ----------
92 93 s : string
93 94
94 95 Returns
95 96 -------
96 97 n : int
97 98 """
98 99 warnings.warn(
99 100 "`num_ini_spaces` is Pending Deprecation since IPython 8.17."
100 101 "It is considered for removal in in future version. "
101 102 "Please open an issue if you believe it should be kept.",
102 103 stacklevel=2,
103 104 category=PendingDeprecationWarning,
104 105 )
105 106 ini_spaces = ini_spaces_re.match(s)
106 107 if ini_spaces:
107 108 return ini_spaces.end()
108 109 else:
109 110 return 0
110 111
111 112 # Fake token types for partial_tokenize:
112 113 INCOMPLETE_STRING = tokenize.N_TOKENS
113 114 IN_MULTILINE_STATEMENT = tokenize.N_TOKENS + 1
114 115
115 116 # The 2 classes below have the same API as TokenInfo, but don't try to look up
116 117 # a token type name that they won't find.
117 118 class IncompleteString:
118 119 type = exact_type = INCOMPLETE_STRING
119 120 def __init__(self, s, start, end, line):
120 121 self.s = s
121 122 self.start = start
122 123 self.end = end
123 124 self.line = line
124 125
125 126 class InMultilineStatement:
126 127 type = exact_type = IN_MULTILINE_STATEMENT
127 128 def __init__(self, pos, line):
128 129 self.s = ''
129 130 self.start = self.end = pos
130 131 self.line = line
131 132
132 133 def partial_tokens(s):
133 134 """Iterate over tokens from a possibly-incomplete string of code.
134 135
135 136 This adds two special token types: INCOMPLETE_STRING and
136 137 IN_MULTILINE_STATEMENT. These can only occur as the last token yielded, and
137 138 represent the two main ways for code to be incomplete.
138 139 """
139 140 readline = io.StringIO(s).readline
140 141 token = tokenize.TokenInfo(tokenize.NEWLINE, '', (1, 0), (1, 0), '')
141 142 try:
142 143 for token in tokenutil.generate_tokens_catch_errors(readline):
143 144 yield token
144 145 except tokenize.TokenError as e:
145 146 # catch EOF error
146 147 lines = s.splitlines(keepends=True)
147 148 end = len(lines), len(lines[-1])
148 149 if 'multi-line string' in e.args[0]:
149 150 l, c = start = token.end
150 151 s = lines[l-1][c:] + ''.join(lines[l:])
151 152 yield IncompleteString(s, start, end, lines[-1])
152 153 elif 'multi-line statement' in e.args[0]:
153 154 yield InMultilineStatement(end, lines[-1])
154 155 else:
155 156 raise
156 157
157 158 def find_next_indent(code) -> int:
158 159 """Find the number of spaces for the next line of indentation"""
159 160 tokens = list(partial_tokens(code))
160 161 if tokens[-1].type == tokenize.ENDMARKER:
161 162 tokens.pop()
162 163 if not tokens:
163 164 return 0
164 165
165 166 while tokens[-1].type in {
166 167 tokenize.DEDENT,
167 168 tokenize.NEWLINE,
168 169 tokenize.COMMENT,
169 170 tokenize.ERRORTOKEN,
170 171 }:
171 172 tokens.pop()
172 173
173 174 # Starting in Python 3.12, the tokenize module adds implicit newlines at the end
174 175 # of input. We need to remove those if we're in a multiline statement
175 176 if tokens[-1].type == IN_MULTILINE_STATEMENT:
176 177 while tokens[-2].type in {tokenize.NL}:
177 178 tokens.pop(-2)
178 179
179 180
180 181 if tokens[-1].type == INCOMPLETE_STRING:
181 182 # Inside a multiline string
182 183 return 0
183 184
184 185 # Find the indents used before
185 186 prev_indents = [0]
186 187 def _add_indent(n):
187 188 if n != prev_indents[-1]:
188 189 prev_indents.append(n)
189 190
190 191 tokiter = iter(tokens)
191 192 for tok in tokiter:
192 193 if tok.type in {tokenize.INDENT, tokenize.DEDENT}:
193 194 _add_indent(tok.end[1])
194 195 elif (tok.type == tokenize.NL):
195 196 try:
196 197 _add_indent(next(tokiter).start[1])
197 198 except StopIteration:
198 199 break
199 200
200 201 last_indent = prev_indents.pop()
201 202
202 203 # If we've just opened a multiline statement (e.g. 'a = ['), indent more
203 204 if tokens[-1].type == IN_MULTILINE_STATEMENT:
204 205 if tokens[-2].exact_type in {tokenize.LPAR, tokenize.LSQB, tokenize.LBRACE}:
205 206 return last_indent + 4
206 207 return last_indent
207 208
208 209 if tokens[-1].exact_type == tokenize.COLON:
209 210 # Line ends with colon - indent
210 211 return last_indent + 4
211 212
212 213 if last_indent:
213 214 # Examine the last line for dedent cues - statements like return or
214 215 # raise which normally end a block of code.
215 216 last_line_starts = 0
216 217 for i, tok in enumerate(tokens):
217 218 if tok.type == tokenize.NEWLINE:
218 219 last_line_starts = i + 1
219 220
220 221 last_line_tokens = tokens[last_line_starts:]
221 222 names = [t.string for t in last_line_tokens if t.type == tokenize.NAME]
222 223 if names and names[0] in {'raise', 'return', 'pass', 'break', 'continue'}:
223 224 # Find the most recent indentation less than the current level
224 225 for indent in reversed(prev_indents):
225 226 if indent < last_indent:
226 227 return indent
227 228
228 229 return last_indent
229 230
230 231
231 232 def last_blank(src):
232 233 """Determine if the input source ends in a blank.
233 234
234 235 A blank is either a newline or a line consisting of whitespace.
235 236
236 237 Parameters
237 238 ----------
238 239 src : string
239 240 A single or multiline string.
240 241 """
241 242 if not src: return False
242 243 ll = src.splitlines()[-1]
243 244 return (ll == '') or ll.isspace()
244 245
245 246
246 247 last_two_blanks_re = re.compile(r'\n\s*\n\s*$', re.MULTILINE)
247 248 last_two_blanks_re2 = re.compile(r'.+\n\s*\n\s+$', re.MULTILINE)
248 249
249 250 def last_two_blanks(src):
250 251 """Determine if the input source ends in two blanks.
251 252
252 253 A blank is either a newline or a line consisting of whitespace.
253 254
254 255 Parameters
255 256 ----------
256 257 src : string
257 258 A single or multiline string.
258 259 """
259 260 if not src: return False
260 261 # The logic here is tricky: I couldn't get a regexp to work and pass all
261 262 # the tests, so I took a different approach: split the source by lines,
262 263 # grab the last two and prepend '###\n' as a stand-in for whatever was in
263 264 # the body before the last two lines. Then, with that structure, it's
264 265 # possible to analyze with two regexps. Not the most elegant solution, but
265 266 # it works. If anyone tries to change this logic, make sure to validate
266 267 # the whole test suite first!
267 268 new_src = '\n'.join(['###\n'] + src.splitlines()[-2:])
268 269 return (bool(last_two_blanks_re.match(new_src)) or
269 270 bool(last_two_blanks_re2.match(new_src)) )
270 271
271 272
272 273 def remove_comments(src):
273 274 """Remove all comments from input source.
274 275
275 276 Note: comments are NOT recognized inside of strings!
276 277
277 278 Parameters
278 279 ----------
279 280 src : string
280 281 A single or multiline input string.
281 282
282 283 Returns
283 284 -------
284 285 String with all Python comments removed.
285 286 """
286 287
287 288 return re.sub('#.*', '', src)
288 289
289 290
290 291 def get_input_encoding():
291 292 """Return the default standard input encoding.
292 293
293 294 If sys.stdin has no encoding, 'ascii' is returned."""
294 295 # There are strange environments for which sys.stdin.encoding is None. We
295 296 # ensure that a valid encoding is returned.
296 297 encoding = getattr(sys.stdin, 'encoding', None)
297 298 if encoding is None:
298 299 encoding = 'ascii'
299 300 return encoding
300 301
301 302 #-----------------------------------------------------------------------------
302 303 # Classes and functions for normal Python syntax handling
303 304 #-----------------------------------------------------------------------------
304 305
305 306 class InputSplitter(object):
306 307 r"""An object that can accumulate lines of Python source before execution.
307 308
308 309 This object is designed to be fed python source line-by-line, using
309 310 :meth:`push`. It will return on each push whether the currently pushed
310 311 code could be executed already. In addition, it provides a method called
311 312 :meth:`push_accepts_more` that can be used to query whether more input
312 313 can be pushed into a single interactive block.
313 314
314 315 This is a simple example of how an interactive terminal-based client can use
315 316 this tool::
316 317
317 318 isp = InputSplitter()
318 319 while isp.push_accepts_more():
319 320 indent = ' '*isp.indent_spaces
320 321 prompt = '>>> ' + indent
321 322 line = indent + raw_input(prompt)
322 323 isp.push(line)
323 324 print('Input source was:\n', isp.source_reset())
324 325 """
325 326 # A cache for storing the current indentation
326 327 # The first value stores the most recently processed source input
327 328 # The second value is the number of spaces for the current indentation
328 329 # If self.source matches the first value, the second value is a valid
329 330 # current indentation. Otherwise, the cache is invalid and the indentation
330 331 # must be recalculated.
331 332 _indent_spaces_cache: Union[Tuple[None, None], Tuple[str, int]] = None, None
332 333 # String, indicating the default input encoding. It is computed by default
333 334 # at initialization time via get_input_encoding(), but it can be reset by a
334 335 # client with specific knowledge of the encoding.
335 336 encoding = ''
336 337 # String where the current full source input is stored, properly encoded.
337 338 # Reading this attribute is the normal way of querying the currently pushed
338 339 # source code, that has been properly encoded.
339 340 source: str = ""
340 341 # Code object corresponding to the current source. It is automatically
341 342 # synced to the source, so it can be queried at any time to obtain the code
342 343 # object; it will be None if the source doesn't compile to valid Python.
343 344 code: Optional[CodeType] = None
344 345
345 346 # Private attributes
346 347
347 348 # List with lines of input accumulated so far
348 349 _buffer: List[str]
349 350 # Command compiler
350 351 _compile: codeop.CommandCompiler
351 352 # Boolean indicating whether the current block is complete
352 353 _is_complete: Optional[bool] = None
353 354 # Boolean indicating whether the current block has an unrecoverable syntax error
354 355 _is_invalid: bool = False
355 356
356 357 def __init__(self) -> None:
357 358 """Create a new InputSplitter instance."""
358 359 self._buffer = []
359 360 self._compile = codeop.CommandCompiler()
360 361 self.encoding = get_input_encoding()
361 362
362 363 def reset(self):
363 364 """Reset the input buffer and associated state."""
364 365 self._buffer[:] = []
365 366 self.source = ''
366 367 self.code = None
367 368 self._is_complete = False
368 369 self._is_invalid = False
369 370
370 371 def source_reset(self):
371 372 """Return the input source and perform a full reset.
372 373 """
373 374 out = self.source
374 375 self.reset()
375 376 return out
376 377
377 378 def check_complete(self, source):
378 379 """Return whether a block of code is ready to execute, or should be continued
379 380
380 381 This is a non-stateful API, and will reset the state of this InputSplitter.
381 382
382 383 Parameters
383 384 ----------
384 385 source : string
385 386 Python input code, which can be multiline.
386 387
387 388 Returns
388 389 -------
389 390 status : str
390 391 One of 'complete', 'incomplete', or 'invalid' if source is not a
391 392 prefix of valid code.
392 393 indent_spaces : int or None
393 394 The number of spaces by which to indent the next line of code. If
394 395 status is not 'incomplete', this is None.
395 396 """
396 397 self.reset()
397 398 try:
398 399 self.push(source)
399 400 except SyntaxError:
400 401 # Transformers in IPythonInputSplitter can raise SyntaxError,
401 402 # which push() will not catch.
402 403 return 'invalid', None
403 404 else:
404 405 if self._is_invalid:
405 406 return 'invalid', None
406 407 elif self.push_accepts_more():
407 408 return 'incomplete', self.get_indent_spaces()
408 409 else:
409 410 return 'complete', None
410 411 finally:
411 412 self.reset()
412 413
413 414 def push(self, lines:str) -> bool:
414 415 """Push one or more lines of input.
415 416
416 417 This stores the given lines and returns a status code indicating
417 418 whether the code forms a complete Python block or not.
418 419
419 420 Any exceptions generated in compilation are swallowed, but if an
420 421 exception was produced, the method returns True.
421 422
422 423 Parameters
423 424 ----------
424 425 lines : string
425 426 One or more lines of Python input.
426 427
427 428 Returns
428 429 -------
429 430 is_complete : boolean
430 431 True if the current input source (the result of the current input
431 432 plus prior inputs) forms a complete Python execution block. Note that
432 433 this value is also stored as a private attribute (``_is_complete``), so it
433 434 can be queried at any time.
434 435 """
435 436 assert isinstance(lines, str)
436 437 self._store(lines)
437 438 source = self.source
438 439
439 440 # Before calling _compile(), reset the code object to None so that if an
440 441 # exception is raised in compilation, we don't mislead by having
441 442 # inconsistent code/source attributes.
442 443 self.code, self._is_complete = None, None
443 444 self._is_invalid = False
444 445
445 446 # Honor termination lines properly
446 447 if source.endswith('\\\n'):
447 448 return False
448 449
449 450 try:
450 451 with warnings.catch_warnings():
451 452 warnings.simplefilter('error', SyntaxWarning)
452 453 self.code = self._compile(source, symbol="exec")
453 454 # Invalid syntax can produce any of a number of different errors from
454 455 # inside the compiler, so we have to catch them all. Syntax errors
455 456 # immediately produce a 'ready' block, so the invalid Python can be
456 457 # sent to the kernel for evaluation with possible ipython
457 458 # special-syntax conversion.
458 459 except (SyntaxError, OverflowError, ValueError, TypeError,
459 460 MemoryError, SyntaxWarning):
460 461 self._is_complete = True
461 462 self._is_invalid = True
462 463 else:
463 464 # Compilation didn't produce any exceptions (though it may not have
464 465 # given a complete code object)
465 466 self._is_complete = self.code is not None
466 467
467 468 return self._is_complete
468 469
469 470 def push_accepts_more(self):
470 471 """Return whether a block of interactive input can accept more input.
471 472
472 473 This method is meant to be used by line-oriented frontends, who need to
473 474 guess whether a block is complete or not based solely on prior and
474 475 current input lines. The InputSplitter considers it has a complete
475 476 interactive block and will not accept more input when either:
476 477
477 478 * A SyntaxError is raised
478 479
479 480 * The code is complete and consists of a single line or a single
480 481 non-compound statement
481 482
482 483 * The code is complete and has a blank line at the end
483 484
484 485 If the current input produces a syntax error, this method immediately
485 486 returns False but does *not* raise the syntax error exception, as
486 487 typically clients will want to send invalid syntax to an execution
487 488 backend which might convert the invalid syntax into valid Python via
488 489 one of the dynamic IPython mechanisms.
489 490 """
490 491
491 492 # With incomplete input, unconditionally accept more
492 493 # A syntax error also sets _is_complete to True - see push()
493 494 if not self._is_complete:
494 495 #print("Not complete") # debug
495 496 return True
496 497
497 498 # The user can make any (complete) input execute by leaving a blank line
498 499 last_line = self.source.splitlines()[-1]
499 500 if (not last_line) or last_line.isspace():
500 501 #print("Blank line") # debug
501 502 return False
502 503
503 504 # If there's just a single line or AST node, and we're flush left, as is
504 505 # the case after a simple statement such as 'a=1', we want to execute it
505 506 # straight away.
506 507 if self.get_indent_spaces() == 0:
507 508 if len(self.source.splitlines()) <= 1:
508 509 return False
509 510
510 511 try:
511 512 code_ast = ast.parse("".join(self._buffer))
512 513 except Exception:
513 514 #print("Can't parse AST") # debug
514 515 return False
515 516 else:
516 517 if len(code_ast.body) == 1 and \
517 518 not hasattr(code_ast.body[0], 'body'):
518 519 #print("Simple statement") # debug
519 520 return False
520 521
521 522 # General fallback - accept more code
522 523 return True
523 524
524 525 def get_indent_spaces(self) -> int:
525 526 sourcefor, n = self._indent_spaces_cache
526 527 if sourcefor == self.source:
527 528 assert n is not None
528 529 return n
529 530
530 531 # self.source always has a trailing newline
531 532 n = find_next_indent(self.source[:-1])
532 533 self._indent_spaces_cache = (self.source, n)
533 534 return n
534 535
535 536 # Backwards compatibility. I think all code that used .indent_spaces was
536 537 # inside IPython, but we can leave this here until IPython 7 in case any
537 538 # other modules are using it. -TK, November 2017
538 539 indent_spaces = property(get_indent_spaces)
539 540
540 541 def _store(self, lines, buffer=None, store='source'):
541 542 """Store one or more lines of input.
542 543
543 544 If input lines are not newline-terminated, a newline is automatically
544 545 appended."""
545 546
546 547 if buffer is None:
547 548 buffer = self._buffer
548 549
549 550 if lines.endswith('\n'):
550 551 buffer.append(lines)
551 552 else:
552 553 buffer.append(lines+'\n')
553 554 setattr(self, store, self._set_source(buffer))
554 555
555 556 def _set_source(self, buffer):
556 557 return u''.join(buffer)
557 558
558 559
559 560 class IPythonInputSplitter(InputSplitter):
560 561 """An input splitter that recognizes all of IPython's special syntax."""
561 562
562 563 # String with raw, untransformed input.
563 564 source_raw = ''
564 565
565 566 # Flag to track when a transformer has stored input that it hasn't given
566 567 # back yet.
567 568 transformer_accumulating = False
568 569
569 570 # Flag to track when assemble_python_lines has stored input that it hasn't
570 571 # given back yet.
571 572 within_python_line = False
572 573
573 574 # Private attributes
574 575
575 576 # List with lines of raw input accumulated so far.
576 577 _buffer_raw: List[str]
577 578
578 579 def __init__(self, line_input_checker=True, physical_line_transforms=None,
579 580 logical_line_transforms=None, python_line_transforms=None):
580 581 super(IPythonInputSplitter, self).__init__()
581 582 self._buffer_raw = []
582 583 self._validate = True
583 584
584 585 if physical_line_transforms is not None:
585 586 self.physical_line_transforms = physical_line_transforms
586 587 else:
587 588 self.physical_line_transforms = [
588 589 leading_indent(),
589 590 classic_prompt(),
590 591 ipy_prompt(),
591 592 cellmagic(end_on_blank_line=line_input_checker),
592 593 ]
593 594
594 595 self.assemble_logical_lines = assemble_logical_lines()
595 596 if logical_line_transforms is not None:
596 597 self.logical_line_transforms = logical_line_transforms
597 598 else:
598 599 self.logical_line_transforms = [
599 600 help_end(),
600 601 escaped_commands(),
601 602 assign_from_magic(),
602 603 assign_from_system(),
603 604 ]
604 605
605 606 self.assemble_python_lines = assemble_python_lines()
606 607 if python_line_transforms is not None:
607 608 self.python_line_transforms = python_line_transforms
608 609 else:
609 610 # We don't use any of these at present
610 611 self.python_line_transforms = []
611 612
612 613 @property
613 614 def transforms(self):
614 615 "Quick access to all transformers."
615 616 return self.physical_line_transforms + \
616 617 [self.assemble_logical_lines] + self.logical_line_transforms + \
617 618 [self.assemble_python_lines] + self.python_line_transforms
618 619
619 620 @property
620 621 def transforms_in_use(self):
621 622 """Transformers, excluding logical line transformers if we're in a
622 623 Python line."""
623 624 t = self.physical_line_transforms[:]
624 625 if not self.within_python_line:
625 626 t += [self.assemble_logical_lines] + self.logical_line_transforms
626 627 return t + [self.assemble_python_lines] + self.python_line_transforms
627 628
628 629 def reset(self):
629 630 """Reset the input buffer and associated state."""
630 631 super(IPythonInputSplitter, self).reset()
631 632 self._buffer_raw[:] = []
632 633 self.source_raw = ''
633 634 self.transformer_accumulating = False
634 635 self.within_python_line = False
635 636
636 637 for t in self.transforms:
637 638 try:
638 639 t.reset()
639 640 except SyntaxError:
640 641 # Nothing that calls reset() expects to handle transformer
641 642 # errors
642 643 pass
643 644
644 645 def flush_transformers(self: Self):
645 646 def _flush(transform, outs: List[str]):
646 647 """yield transformed lines
647 648
648 649 always strings, never None
649 650
650 651 transform: the current transform
651 652 outs: an iterable of previously transformed inputs.
652 653 Each may be multiline, which will be passed
653 654 one line at a time to transform.
654 655 """
655 656 for out in outs:
656 657 for line in out.splitlines():
657 658 # push one line at a time
658 659 tmp = transform.push(line)
659 660 if tmp is not None:
660 661 yield tmp
661 662
662 663 # reset the transform
663 664 tmp = transform.reset()
664 665 if tmp is not None:
665 666 yield tmp
666 667
667 668 out: List[str] = []
668 669 for t in self.transforms_in_use:
669 670 out = _flush(t, out)
670 671
671 672 out = list(out)
672 673 if out:
673 674 self._store('\n'.join(out))
674 675
675 676 def raw_reset(self):
676 677 """Return raw input only and perform a full reset.
677 678 """
678 679 out = self.source_raw
679 680 self.reset()
680 681 return out
681 682
682 683 def source_reset(self):
683 684 try:
684 685 self.flush_transformers()
685 686 return self.source
686 687 finally:
687 688 self.reset()
688 689
689 690 def push_accepts_more(self):
690 691 if self.transformer_accumulating:
691 692 return True
692 693 else:
693 694 return super(IPythonInputSplitter, self).push_accepts_more()
694 695
695 696 def transform_cell(self, cell):
696 697 """Process and translate a cell of input.
697 698 """
698 699 self.reset()
699 700 try:
700 701 self.push(cell)
701 702 self.flush_transformers()
702 703 return self.source
703 704 finally:
704 705 self.reset()
705 706
706 707 def push(self, lines:str) -> bool:
707 708 """Push one or more lines of IPython input.
708 709
709 710 This stores the given lines and returns a status code indicating
710 711 whether the code forms a complete Python block or not, after processing
711 712 all input lines for special IPython syntax.
712 713
713 714 Any exceptions generated in compilation are swallowed, but if an
714 715 exception was produced, the method returns True.
715 716
716 717 Parameters
717 718 ----------
718 719 lines : string
719 720 One or more lines of Python input.
720 721
721 722 Returns
722 723 -------
723 724 is_complete : boolean
724 725 True if the current input source (the result of the current input
725 726 plus prior inputs) forms a complete Python execution block. Note that
726 727 this value is also stored as a private attribute (_is_complete), so it
727 728 can be queried at any time.
728 729 """
729 730 assert isinstance(lines, str)
730 731 # We must ensure all input is pure unicode
731 732 # ''.splitlines() --> [], but we need to push the empty line to transformers
732 733 lines_list = lines.splitlines()
733 734 if not lines_list:
734 735 lines_list = ['']
735 736
736 737 # Store raw source before applying any transformations to it. Note
737 738 # that this must be done *after* the reset() call that would otherwise
738 739 # flush the buffer.
739 740 self._store(lines, self._buffer_raw, 'source_raw')
740 741
741 742 transformed_lines_list = []
742 743 for line in lines_list:
743 744 transformed = self._transform_line(line)
744 745 if transformed is not None:
745 746 transformed_lines_list.append(transformed)
746 747
747 748 if transformed_lines_list:
748 749 transformed_lines = '\n'.join(transformed_lines_list)
749 750 return super(IPythonInputSplitter, self).push(transformed_lines)
750 751 else:
751 752 # Got nothing back from transformers - they must be waiting for
752 753 # more input.
753 754 return False
754 755
755 756 def _transform_line(self, line):
756 757 """Push a line of input code through the various transformers.
757 758
758 759 Returns any output from the transformers, or None if a transformer
759 760 is accumulating lines.
760 761
761 762 Sets self.transformer_accumulating as a side effect.
762 763 """
763 764 def _accumulating(dbg):
764 765 #print(dbg)
765 766 self.transformer_accumulating = True
766 767 return None
767 768
768 769 for transformer in self.physical_line_transforms:
769 770 line = transformer.push(line)
770 771 if line is None:
771 772 return _accumulating(transformer)
772 773
773 774 if not self.within_python_line:
774 775 line = self.assemble_logical_lines.push(line)
775 776 if line is None:
776 777 return _accumulating('acc logical line')
777 778
778 779 for transformer in self.logical_line_transforms:
779 780 line = transformer.push(line)
780 781 if line is None:
781 782 return _accumulating(transformer)
782 783
783 784 line = self.assemble_python_lines.push(line)
784 785 if line is None:
785 786 self.within_python_line = True
786 787 return _accumulating('acc python line')
787 788 else:
788 789 self.within_python_line = False
789 790
790 791 for transformer in self.python_line_transforms:
791 792 line = transformer.push(line)
792 793 if line is None:
793 794 return _accumulating(transformer)
794 795
795 796 #print("transformers clear") #debug
796 797 self.transformer_accumulating = False
797 798 return line
798 799
@@ -1,447 +1,448
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
7 8 import platform
8 9 import string
9 10 import sys
10 11 from textwrap import dedent
11 12
12 13 import pytest
13 14
14 15 from IPython.core import inputtransformer2 as ipt2
15 16 from IPython.core.inputtransformer2 import _find_assign_op, make_tokens_by_line
16 17
17 18 MULTILINE_MAGIC = (
18 19 """\
19 20 a = f()
20 21 %foo \\
21 22 bar
22 23 g()
23 24 """.splitlines(
24 25 keepends=True
25 26 ),
26 27 (2, 0),
27 28 """\
28 29 a = f()
29 30 get_ipython().run_line_magic('foo', ' bar')
30 31 g()
31 32 """.splitlines(
32 33 keepends=True
33 34 ),
34 35 )
35 36
36 37 INDENTED_MAGIC = (
37 38 """\
38 39 for a in range(5):
39 40 %ls
40 41 """.splitlines(
41 42 keepends=True
42 43 ),
43 44 (2, 4),
44 45 """\
45 46 for a in range(5):
46 47 get_ipython().run_line_magic('ls', '')
47 48 """.splitlines(
48 49 keepends=True
49 50 ),
50 51 )
51 52
52 53 CRLF_MAGIC = (
53 54 ["a = f()\n", "%ls\r\n", "g()\n"],
54 55 (2, 0),
55 56 ["a = f()\n", "get_ipython().run_line_magic('ls', '')\n", "g()\n"],
56 57 )
57 58
58 59 MULTILINE_MAGIC_ASSIGN = (
59 60 """\
60 61 a = f()
61 62 b = %foo \\
62 63 bar
63 64 g()
64 65 """.splitlines(
65 66 keepends=True
66 67 ),
67 68 (2, 4),
68 69 """\
69 70 a = f()
70 71 b = get_ipython().run_line_magic('foo', ' bar')
71 72 g()
72 73 """.splitlines(
73 74 keepends=True
74 75 ),
75 76 )
76 77
77 78 MULTILINE_SYSTEM_ASSIGN = ("""\
78 79 a = f()
79 80 b = !foo \\
80 81 bar
81 82 g()
82 83 """.splitlines(keepends=True), (2, 4), """\
83 84 a = f()
84 85 b = get_ipython().getoutput('foo bar')
85 86 g()
86 87 """.splitlines(keepends=True))
87 88
88 89 #####
89 90
90 91 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = (
91 92 """\
92 93 def test():
93 94 for i in range(1):
94 95 print(i)
95 96 res =! ls
96 97 """.splitlines(
97 98 keepends=True
98 99 ),
99 100 (4, 7),
100 101 """\
101 102 def test():
102 103 for i in range(1):
103 104 print(i)
104 105 res =get_ipython().getoutput(\' ls\')
105 106 """.splitlines(
106 107 keepends=True
107 108 ),
108 109 )
109 110
110 111 ######
111 112
112 113 AUTOCALL_QUOTE = ([",f 1 2 3\n"], (1, 0), ['f("1", "2", "3")\n'])
113 114
114 115 AUTOCALL_QUOTE2 = ([";f 1 2 3\n"], (1, 0), ['f("1 2 3")\n'])
115 116
116 117 AUTOCALL_PAREN = (["/f 1 2 3\n"], (1, 0), ["f(1, 2, 3)\n"])
117 118
118 119 SIMPLE_HELP = (["foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', 'foo')\n"])
119 120
120 121 DETAILED_HELP = (
121 122 ["foo??\n"],
122 123 (1, 0),
123 124 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"],
124 125 )
125 126
126 127 MAGIC_HELP = (["%foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', '%foo')\n"])
127 128
128 129 HELP_IN_EXPR = (
129 130 ["a = b + c?\n"],
130 131 (1, 0),
131 132 ["get_ipython().run_line_magic('pinfo', 'c')\n"],
132 133 )
133 134
134 135 HELP_CONTINUED_LINE = (
135 136 """\
136 137 a = \\
137 138 zip?
138 139 """.splitlines(
139 140 keepends=True
140 141 ),
141 142 (1, 0),
142 143 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
143 144 )
144 145
145 146 HELP_MULTILINE = (
146 147 """\
147 148 (a,
148 149 b) = zip?
149 150 """.splitlines(
150 151 keepends=True
151 152 ),
152 153 (1, 0),
153 154 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
154 155 )
155 156
156 157 HELP_UNICODE = (
157 158 ["Ο€.foo?\n"],
158 159 (1, 0),
159 160 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"],
160 161 )
161 162
162 163
163 164 def null_cleanup_transformer(lines):
164 165 """
165 166 A cleanup transform that returns an empty list.
166 167 """
167 168 return []
168 169
169 170
170 171 def test_check_make_token_by_line_never_ends_empty():
171 172 """
172 173 Check that not sequence of single or double characters ends up leading to en empty list of tokens
173 174 """
174 175 from string import printable
175 176
176 177 for c in printable:
177 178 assert make_tokens_by_line(c)[-1] != []
178 179 for k in printable:
179 180 assert make_tokens_by_line(c + k)[-1] != []
180 181
181 182
182 183 def check_find(transformer, case, match=True):
183 184 sample, expected_start, _ = case
184 185 tbl = make_tokens_by_line(sample)
185 186 res = transformer.find(tbl)
186 187 if match:
187 188 # start_line is stored 0-indexed, expected values are 1-indexed
188 189 assert (res.start_line + 1, res.start_col) == expected_start
189 190 return res
190 191 else:
191 192 assert res is None
192 193
193 194
194 195 def check_transform(transformer_cls, case):
195 196 lines, start, expected = case
196 197 transformer = transformer_cls(start)
197 198 assert transformer.transform(lines) == expected
198 199
199 200
200 201 def test_continued_line():
201 202 lines = MULTILINE_MAGIC_ASSIGN[0]
202 203 assert ipt2.find_end_of_continued_line(lines, 1) == 2
203 204
204 205 assert ipt2.assemble_continued_line(lines, (1, 5), 2) == "foo bar"
205 206
206 207
207 208 def test_find_assign_magic():
208 209 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
209 210 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
210 211 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
211 212
212 213
213 214 def test_transform_assign_magic():
214 215 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
215 216
216 217
217 218 def test_find_assign_system():
218 219 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
219 220 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
220 221 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
221 222 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
222 223 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
223 224
224 225
225 226 def test_transform_assign_system():
226 227 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
227 228 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
228 229
229 230
230 231 def test_find_magic_escape():
231 232 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
232 233 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
233 234 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
234 235
235 236
236 237 def test_transform_magic_escape():
237 238 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
238 239 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
239 240 check_transform(ipt2.EscapedCommand, CRLF_MAGIC)
240 241
241 242
242 243 def test_find_autocalls():
243 244 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
244 245 print("Testing %r" % case[0])
245 246 check_find(ipt2.EscapedCommand, case)
246 247
247 248
248 249 def test_transform_autocall():
249 250 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
250 251 print("Testing %r" % case[0])
251 252 check_transform(ipt2.EscapedCommand, case)
252 253
253 254
254 255 def test_find_help():
255 256 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
256 257 check_find(ipt2.HelpEnd, case)
257 258
258 259 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
259 260 assert tf.q_line == 1
260 261 assert tf.q_col == 3
261 262
262 263 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
263 264 assert tf.q_line == 1
264 265 assert tf.q_col == 8
265 266
266 267 # ? in a comment does not trigger help
267 268 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
268 269 # Nor in a string
269 270 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
270 271
271 272
272 273 def test_transform_help():
273 274 tf = ipt2.HelpEnd((1, 0), (1, 9))
274 275 assert tf.transform(HELP_IN_EXPR[0]) == HELP_IN_EXPR[2]
275 276
276 277 tf = ipt2.HelpEnd((1, 0), (2, 3))
277 278 assert tf.transform(HELP_CONTINUED_LINE[0]) == HELP_CONTINUED_LINE[2]
278 279
279 280 tf = ipt2.HelpEnd((1, 0), (2, 8))
280 281 assert tf.transform(HELP_MULTILINE[0]) == HELP_MULTILINE[2]
281 282
282 283 tf = ipt2.HelpEnd((1, 0), (1, 0))
283 284 assert tf.transform(HELP_UNICODE[0]) == HELP_UNICODE[2]
284 285
285 286
286 287 def test_find_assign_op_dedent():
287 288 """
288 289 be careful that empty token like dedent are not counted as parens
289 290 """
290 291
291 292 class Tk:
292 293 def __init__(self, s):
293 294 self.string = s
294 295
295 296 assert _find_assign_op([Tk(s) for s in ("", "a", "=", "b")]) == 2
296 297 assert (
297 298 _find_assign_op([Tk(s) for s in ("", "(", "a", "=", "b", ")", "=", "5")]) == 6
298 299 )
299 300
300 301
301 302 extra_closing_paren_param = (
302 303 pytest.param("(\n))", "invalid", None)
303 304 if sys.version_info >= (3, 12)
304 305 else pytest.param("(\n))", "incomplete", 0)
305 306 )
306 307 examples = [
307 308 pytest.param("a = 1", "complete", None),
308 309 pytest.param("for a in range(5):", "incomplete", 4),
309 310 pytest.param("for a in range(5):\n if a > 0:", "incomplete", 8),
310 311 pytest.param("raise = 2", "invalid", None),
311 312 pytest.param("a = [1,\n2,", "incomplete", 0),
312 313 extra_closing_paren_param,
313 314 pytest.param("\\\r\n", "incomplete", 0),
314 315 pytest.param("a = '''\n hi", "incomplete", 3),
315 316 pytest.param("def a():\n x=1\n global x", "invalid", None),
316 317 pytest.param(
317 318 "a \\ ",
318 319 "invalid",
319 320 None,
320 321 marks=pytest.mark.xfail(
321 322 reason="Bug in python 3.9.8 – bpo 45738",
322 323 condition=sys.version_info in [(3, 11, 0, "alpha", 2)],
323 324 raises=SystemError,
324 325 strict=True,
325 326 ),
326 327 ), # Nothing allowed after backslash,
327 328 pytest.param("1\\\n+2", "complete", None),
328 329 ]
329 330
330 331
331 332 @pytest.mark.parametrize("code, expected, number", examples)
332 333 def test_check_complete_param(code, expected, number):
333 334 cc = ipt2.TransformerManager().check_complete
334 335 assert cc(code) == (expected, number)
335 336
336 337
337 338 @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
338 339 @pytest.mark.xfail(
339 340 reason="Bug in python 3.9.8 – bpo 45738",
340 341 condition=sys.version_info in [(3, 11, 0, "alpha", 2)],
341 342 raises=SystemError,
342 343 strict=True,
343 344 )
344 345 def test_check_complete():
345 346 cc = ipt2.TransformerManager().check_complete
346 347
347 348 example = dedent(
348 349 """
349 350 if True:
350 351 a=1"""
351 352 )
352 353
353 354 assert cc(example) == ("incomplete", 4)
354 355 assert cc(example + "\n") == ("complete", None)
355 356 assert cc(example + "\n ") == ("complete", None)
356 357
357 358 # no need to loop on all the letters/numbers.
358 359 short = "12abAB" + string.printable[62:]
359 360 for c in short:
360 361 # test does not raise:
361 362 cc(c)
362 363 for k in short:
363 364 cc(c + k)
364 365
365 366 assert cc("def f():\n x=0\n \\\n ") == ("incomplete", 2)
366 367
367 368
368 369 @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
369 370 @pytest.mark.parametrize(
370 371 "value, expected",
371 372 [
372 373 ('''def foo():\n """''', ("incomplete", 4)),
373 374 ("""async with example:\n pass""", ("incomplete", 4)),
374 375 ("""async with example:\n pass\n """, ("complete", None)),
375 376 ],
376 377 )
377 378 def test_check_complete_II(value, expected):
378 379 """
379 380 Test that multiple line strings are properly handled.
380 381
381 382 Separate test function for convenience
382 383
383 384 """
384 385 cc = ipt2.TransformerManager().check_complete
385 386 assert cc(value) == expected
386 387
387 388
388 389 @pytest.mark.parametrize(
389 390 "value, expected",
390 391 [
391 392 (")", ("invalid", None)),
392 393 ("]", ("invalid", None)),
393 394 ("}", ("invalid", None)),
394 395 (")(", ("invalid", None)),
395 396 ("][", ("invalid", None)),
396 397 ("}{", ("invalid", None)),
397 398 ("]()(", ("invalid", None)),
398 399 ("())(", ("invalid", None)),
399 400 (")[](", ("invalid", None)),
400 401 ("()](", ("invalid", None)),
401 402 ],
402 403 )
403 404 def test_check_complete_invalidates_sunken_brackets(value, expected):
404 405 """
405 406 Test that a single line with more closing brackets than the opening ones is
406 407 interpreted as invalid
407 408 """
408 409 cc = ipt2.TransformerManager().check_complete
409 410 assert cc(value) == expected
410 411
411 412
412 413 def test_null_cleanup_transformer():
413 414 manager = ipt2.TransformerManager()
414 415 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
415 416 assert manager.transform_cell("") == ""
416 417
417 418
418 419 def test_side_effects_I():
419 420 count = 0
420 421
421 422 def counter(lines):
422 423 nonlocal count
423 424 count += 1
424 425 return lines
425 426
426 427 counter.has_side_effects = True
427 428
428 429 manager = ipt2.TransformerManager()
429 430 manager.cleanup_transforms.insert(0, counter)
430 431 assert manager.check_complete("a=1\n") == ("complete", None)
431 432 assert count == 0
432 433
433 434
434 435 def test_side_effects_II():
435 436 count = 0
436 437
437 438 def counter(lines):
438 439 nonlocal count
439 440 count += 1
440 441 return lines
441 442
442 443 counter.has_side_effects = True
443 444
444 445 manager = ipt2.TransformerManager()
445 446 manager.line_transforms.insert(0, counter)
446 447 assert manager.check_complete("b=1\n") == ("complete", None)
447 448 assert count == 0
@@ -1,200 +1,202
1 1 import errno
2 2 import os
3 3 import shutil
4 4 import tempfile
5 5 import warnings
6 6 from unittest.mock import patch
7 7
8 8 from tempfile import TemporaryDirectory
9 9 from testpath import assert_isdir, assert_isfile, modified_env
10 10
11 11 from IPython import paths
12 12 from IPython.testing.decorators import skip_win32
13 13
14 14 TMP_TEST_DIR = os.path.realpath(tempfile.mkdtemp())
15 15 HOME_TEST_DIR = os.path.join(TMP_TEST_DIR, "home_test_dir")
16 16 XDG_TEST_DIR = os.path.join(HOME_TEST_DIR, "xdg_test_dir")
17 17 XDG_CACHE_DIR = os.path.join(HOME_TEST_DIR, "xdg_cache_dir")
18 18 IP_TEST_DIR = os.path.join(HOME_TEST_DIR,'.ipython')
19 19
20 20 def setup_module():
21 21 """Setup testenvironment for the module:
22 22
23 23 - Adds dummy home dir tree
24 24 """
25 25 # Do not mask exceptions here. In particular, catching WindowsError is a
26 26 # problem because that exception is only defined on Windows...
27 27 os.makedirs(IP_TEST_DIR)
28 28 os.makedirs(os.path.join(XDG_TEST_DIR, 'ipython'))
29 29 os.makedirs(os.path.join(XDG_CACHE_DIR, 'ipython'))
30 30
31 31
32 32 def teardown_module():
33 33 """Teardown testenvironment for the module:
34 34
35 35 - Remove dummy home dir tree
36 36 """
37 37 # Note: we remove the parent test dir, which is the root of all test
38 38 # subdirs we may have created. Use shutil instead of os.removedirs, so
39 39 # that non-empty directories are all recursively removed.
40 40 shutil.rmtree(TMP_TEST_DIR)
41 41
42 42 def patch_get_home_dir(dirpath):
43 43 return patch.object(paths, 'get_home_dir', return_value=dirpath)
44 44
45 45
46 46 def test_get_ipython_dir_1():
47 47 """test_get_ipython_dir_1, Testcase to see if we can call get_ipython_dir without Exceptions."""
48 48 env_ipdir = os.path.join("someplace", ".ipython")
49 49 with patch.object(paths, '_writable_dir', return_value=True), \
50 50 modified_env({'IPYTHONDIR': env_ipdir}):
51 51 ipdir = paths.get_ipython_dir()
52 52
53 53 assert ipdir == env_ipdir
54 54
55 55 def test_get_ipython_dir_2():
56 56 """test_get_ipython_dir_2, Testcase to see if we can call get_ipython_dir without Exceptions."""
57 57 with patch_get_home_dir('someplace'), \
58 58 patch.object(paths, 'get_xdg_dir', return_value=None), \
59 59 patch.object(paths, '_writable_dir', return_value=True), \
60 60 patch('os.name', "posix"), \
61 61 modified_env({'IPYTHON_DIR': None,
62 62 'IPYTHONDIR': None,
63 63 'XDG_CONFIG_HOME': None
64 64 }):
65 65 ipdir = paths.get_ipython_dir()
66 66
67 67 assert ipdir == os.path.join("someplace", ".ipython")
68 68
69 69 def test_get_ipython_dir_3():
70 70 """test_get_ipython_dir_3, use XDG if defined and exists, and .ipython doesn't exist."""
71 71 tmphome = TemporaryDirectory()
72 72 try:
73 73 with patch_get_home_dir(tmphome.name), \
74 74 patch('os.name', 'posix'), \
75 75 modified_env({
76 76 'IPYTHON_DIR': None,
77 77 'IPYTHONDIR': None,
78 78 'XDG_CONFIG_HOME': XDG_TEST_DIR,
79 79 }), warnings.catch_warnings(record=True) as w:
80 80 ipdir = paths.get_ipython_dir()
81 81
82 82 assert ipdir == os.path.join(tmphome.name, XDG_TEST_DIR, "ipython")
83 83 assert len(w) == 0
84 84 finally:
85 85 tmphome.cleanup()
86 86
87 87 def test_get_ipython_dir_4():
88 88 """test_get_ipython_dir_4, warn if XDG and home both exist."""
89 89 with patch_get_home_dir(HOME_TEST_DIR), \
90 90 patch('os.name', 'posix'):
91 91 try:
92 92 os.mkdir(os.path.join(XDG_TEST_DIR, 'ipython'))
93 93 except OSError as e:
94 94 if e.errno != errno.EEXIST:
95 95 raise
96 96
97 97
98 98 with modified_env({
99 99 'IPYTHON_DIR': None,
100 100 'IPYTHONDIR': None,
101 101 'XDG_CONFIG_HOME': XDG_TEST_DIR,
102 102 }), warnings.catch_warnings(record=True) as w:
103 103 ipdir = paths.get_ipython_dir()
104 104
105 105 assert len(w) == 1
106 106 assert "Ignoring" in str(w[0])
107 107
108 108
109 109 def test_get_ipython_dir_5():
110 110 """test_get_ipython_dir_5, use .ipython if exists and XDG defined, but doesn't exist."""
111 111 with patch_get_home_dir(HOME_TEST_DIR), \
112 112 patch('os.name', 'posix'):
113 113 try:
114 114 os.rmdir(os.path.join(XDG_TEST_DIR, 'ipython'))
115 115 except OSError as e:
116 116 if e.errno != errno.ENOENT:
117 117 raise
118 118
119 119 with modified_env({
120 120 'IPYTHON_DIR': None,
121 121 'IPYTHONDIR': None,
122 122 'XDG_CONFIG_HOME': XDG_TEST_DIR,
123 123 }):
124 124 ipdir = paths.get_ipython_dir()
125 125
126 126 assert ipdir == IP_TEST_DIR
127 127
128 128 def test_get_ipython_dir_6():
129 129 """test_get_ipython_dir_6, use home over XDG if defined and neither exist."""
130 130 xdg = os.path.join(HOME_TEST_DIR, 'somexdg')
131 131 os.mkdir(xdg)
132 132 shutil.rmtree(os.path.join(HOME_TEST_DIR, '.ipython'))
133 133 print(paths._writable_dir)
134 134 with patch_get_home_dir(HOME_TEST_DIR), \
135 135 patch.object(paths, 'get_xdg_dir', return_value=xdg), \
136 136 patch('os.name', 'posix'), \
137 137 modified_env({
138 138 'IPYTHON_DIR': None,
139 139 'IPYTHONDIR': None,
140 140 'XDG_CONFIG_HOME': None,
141 141 }), warnings.catch_warnings(record=True) as w:
142 142 ipdir = paths.get_ipython_dir()
143 143
144 144 assert ipdir == os.path.join(HOME_TEST_DIR, ".ipython")
145 145 assert len(w) == 0
146 146
147 147 def test_get_ipython_dir_7():
148 148 """test_get_ipython_dir_7, test home directory expansion on IPYTHONDIR"""
149 149 home_dir = os.path.normpath(os.path.expanduser('~'))
150 150 with modified_env({'IPYTHONDIR': os.path.join('~', 'somewhere')}), \
151 151 patch.object(paths, '_writable_dir', return_value=True):
152 152 ipdir = paths.get_ipython_dir()
153 153 assert ipdir == os.path.join(home_dir, "somewhere")
154 154
155 155
156 156 @skip_win32
157 157 def test_get_ipython_dir_8():
158 158 """test_get_ipython_dir_8, test / home directory"""
159 159 if not os.access("/", os.W_OK):
160 160 # test only when HOME directory actually writable
161 161 return
162 162
163 with patch.object(paths, "_writable_dir", lambda path: bool(path)), patch.object(
164 paths, "get_xdg_dir", return_value=None
165 ), modified_env(
166 {
167 "IPYTHON_DIR": None,
168 "IPYTHONDIR": None,
169 "HOME": "/",
170 }
163 with (
164 patch.object(paths, "_writable_dir", lambda path: bool(path)),
165 patch.object(paths, "get_xdg_dir", return_value=None),
166 modified_env(
167 {
168 "IPYTHON_DIR": None,
169 "IPYTHONDIR": None,
170 "HOME": "/",
171 }
172 ),
171 173 ):
172 174 assert paths.get_ipython_dir() == "/.ipython"
173 175
174 176
175 177 def test_get_ipython_cache_dir():
176 178 with modified_env({'HOME': HOME_TEST_DIR}):
177 179 if os.name == "posix":
178 180 # test default
179 181 os.makedirs(os.path.join(HOME_TEST_DIR, ".cache"))
180 182 with modified_env({'XDG_CACHE_HOME': None}):
181 183 ipdir = paths.get_ipython_cache_dir()
182 184 assert os.path.join(HOME_TEST_DIR, ".cache", "ipython") == ipdir
183 185 assert_isdir(ipdir)
184 186
185 187 # test env override
186 188 with modified_env({"XDG_CACHE_HOME": XDG_CACHE_DIR}):
187 189 ipdir = paths.get_ipython_cache_dir()
188 190 assert_isdir(ipdir)
189 191 assert ipdir == os.path.join(XDG_CACHE_DIR, "ipython")
190 192 else:
191 193 assert paths.get_ipython_cache_dir() == paths.get_ipython_dir()
192 194
193 195 def test_get_ipython_package_dir():
194 196 ipdir = paths.get_ipython_package_dir()
195 197 assert_isdir(ipdir)
196 198
197 199
198 200 def test_get_ipython_module_path():
199 201 ipapp_path = paths.get_ipython_module_path('IPython.terminal.ipapp')
200 202 assert_isfile(ipapp_path)
@@ -1,422 +1,423
1 1 """
2 2 This module contains factory functions that attempt
3 3 to return Qt submodules from the various python Qt bindings.
4 4
5 5 It also protects against double-importing Qt with different
6 6 bindings, which is unstable and likely to crash
7 7
8 8 This is used primarily by qt and qt_for_kernel, and shouldn't
9 9 be accessed directly from the outside
10 10 """
11
11 12 import importlib.abc
12 13 import sys
13 14 import os
14 15 import types
15 16 from functools import partial, lru_cache
16 17 import operator
17 18
18 19 # ### Available APIs.
19 20 # Qt6
20 21 QT_API_PYQT6 = "pyqt6"
21 22 QT_API_PYSIDE6 = "pyside6"
22 23
23 24 # Qt5
24 25 QT_API_PYQT5 = 'pyqt5'
25 26 QT_API_PYSIDE2 = 'pyside2'
26 27
27 28 # Qt4
28 29 # NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side.
29 30 QT_API_PYQT = "pyqt" # Force version 2
30 31 QT_API_PYQTv1 = "pyqtv1" # Force version 2
31 32 QT_API_PYSIDE = "pyside"
32 33
33 34 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
34 35
35 36 api_to_module = {
36 37 # Qt6
37 38 QT_API_PYQT6: "PyQt6",
38 39 QT_API_PYSIDE6: "PySide6",
39 40 # Qt5
40 41 QT_API_PYQT5: "PyQt5",
41 42 QT_API_PYSIDE2: "PySide2",
42 43 # Qt4
43 44 QT_API_PYSIDE: "PySide",
44 45 QT_API_PYQT: "PyQt4",
45 46 QT_API_PYQTv1: "PyQt4",
46 47 # default
47 48 QT_API_PYQT_DEFAULT: "PyQt6",
48 49 }
49 50
50 51
51 52 class ImportDenier(importlib.abc.MetaPathFinder):
52 53 """Import Hook that will guard against bad Qt imports
53 54 once IPython commits to a specific binding
54 55 """
55 56
56 57 def __init__(self):
57 58 self.__forbidden = set()
58 59
59 60 def forbid(self, module_name):
60 61 sys.modules.pop(module_name, None)
61 62 self.__forbidden.add(module_name)
62 63
63 64 def find_spec(self, fullname, path, target=None):
64 65 if path:
65 66 return
66 67 if fullname in self.__forbidden:
67 68 raise ImportError(
68 69 """
69 70 Importing %s disabled by IPython, which has
70 71 already imported an Incompatible QT Binding: %s
71 72 """
72 73 % (fullname, loaded_api())
73 74 )
74 75
75 76
76 77 ID = ImportDenier()
77 78 sys.meta_path.insert(0, ID)
78 79
79 80
80 81 def commit_api(api):
81 82 """Commit to a particular API, and trigger ImportErrors on subsequent
82 83 dangerous imports"""
83 84 modules = set(api_to_module.values())
84 85
85 86 modules.remove(api_to_module[api])
86 87 for mod in modules:
87 88 ID.forbid(mod)
88 89
89 90
90 91 def loaded_api():
91 92 """Return which API is loaded, if any
92 93
93 94 If this returns anything besides None,
94 95 importing any other Qt binding is unsafe.
95 96
96 97 Returns
97 98 -------
98 99 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
99 100 """
100 101 if sys.modules.get("PyQt6.QtCore"):
101 102 return QT_API_PYQT6
102 103 elif sys.modules.get("PySide6.QtCore"):
103 104 return QT_API_PYSIDE6
104 105 elif sys.modules.get("PyQt5.QtCore"):
105 106 return QT_API_PYQT5
106 107 elif sys.modules.get("PySide2.QtCore"):
107 108 return QT_API_PYSIDE2
108 109 elif sys.modules.get("PyQt4.QtCore"):
109 110 if qtapi_version() == 2:
110 111 return QT_API_PYQT
111 112 else:
112 113 return QT_API_PYQTv1
113 114 elif sys.modules.get("PySide.QtCore"):
114 115 return QT_API_PYSIDE
115 116
116 117 return None
117 118
118 119
119 120 def has_binding(api):
120 121 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
121 122
122 123 Parameters
123 124 ----------
124 125 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
125 126 Which module to check for
126 127
127 128 Returns
128 129 -------
129 130 True if the relevant module appears to be importable
130 131 """
131 132 module_name = api_to_module[api]
132 133 from importlib.util import find_spec
133 134
134 135 required = ['QtCore', 'QtGui', 'QtSvg']
135 136 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
136 137 # QT5 requires QtWidgets too
137 138 required.append('QtWidgets')
138 139
139 140 for submod in required:
140 141 try:
141 142 spec = find_spec('%s.%s' % (module_name, submod))
142 143 except ImportError:
143 144 # Package (e.g. PyQt5) not found
144 145 return False
145 146 else:
146 147 if spec is None:
147 148 # Submodule (e.g. PyQt5.QtCore) not found
148 149 return False
149 150
150 151 if api == QT_API_PYSIDE:
151 152 # We can also safely check PySide version
152 153 import PySide
153 154
154 155 return PySide.__version_info__ >= (1, 0, 3)
155 156
156 157 return True
157 158
158 159
159 160 def qtapi_version():
160 161 """Return which QString API has been set, if any
161 162
162 163 Returns
163 164 -------
164 165 The QString API version (1 or 2), or None if not set
165 166 """
166 167 try:
167 168 import sip
168 169 except ImportError:
169 170 # as of PyQt5 5.11, sip is no longer available as a top-level
170 171 # module and needs to be imported from the PyQt5 namespace
171 172 try:
172 173 from PyQt5 import sip
173 174 except ImportError:
174 175 return
175 176 try:
176 177 return sip.getapi('QString')
177 178 except ValueError:
178 179 return
179 180
180 181
181 182 def can_import(api):
182 183 """Safely query whether an API is importable, without importing it"""
183 184 if not has_binding(api):
184 185 return False
185 186
186 187 current = loaded_api()
187 188 if api == QT_API_PYQT_DEFAULT:
188 189 return current in [QT_API_PYQT6, None]
189 190 else:
190 191 return current in [api, None]
191 192
192 193
193 194 def import_pyqt4(version=2):
194 195 """
195 196 Import PyQt4
196 197
197 198 Parameters
198 199 ----------
199 200 version : 1, 2, or None
200 201 Which QString/QVariant API to use. Set to None to use the system
201 202 default
202 203 ImportErrors raised within this function are non-recoverable
203 204 """
204 205 # The new-style string API (version=2) automatically
205 206 # converts QStrings to Unicode Python strings. Also, automatically unpacks
206 207 # QVariants to their underlying objects.
207 208 import sip
208 209
209 210 if version is not None:
210 211 sip.setapi('QString', version)
211 212 sip.setapi('QVariant', version)
212 213
213 214 from PyQt4 import QtGui, QtCore, QtSvg
214 215
215 216 if QtCore.PYQT_VERSION < 0x040700:
216 217 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
217 218 QtCore.PYQT_VERSION_STR)
218 219
219 220 # Alias PyQt-specific functions for PySide compatibility.
220 221 QtCore.Signal = QtCore.pyqtSignal
221 222 QtCore.Slot = QtCore.pyqtSlot
222 223
223 224 # query for the API version (in case version == None)
224 225 version = sip.getapi('QString')
225 226 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
226 227 return QtCore, QtGui, QtSvg, api
227 228
228 229
229 230 def import_pyqt5():
230 231 """
231 232 Import PyQt5
232 233
233 234 ImportErrors raised within this function are non-recoverable
234 235 """
235 236
236 237 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
237 238
238 239 # Alias PyQt-specific functions for PySide compatibility.
239 240 QtCore.Signal = QtCore.pyqtSignal
240 241 QtCore.Slot = QtCore.pyqtSlot
241 242
242 243 # Join QtGui and QtWidgets for Qt4 compatibility.
243 244 QtGuiCompat = types.ModuleType('QtGuiCompat')
244 245 QtGuiCompat.__dict__.update(QtGui.__dict__)
245 246 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
246 247
247 248 api = QT_API_PYQT5
248 249 return QtCore, QtGuiCompat, QtSvg, api
249 250
250 251
251 252 def import_pyqt6():
252 253 """
253 254 Import PyQt6
254 255
255 256 ImportErrors raised within this function are non-recoverable
256 257 """
257 258
258 259 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
259 260
260 261 # Alias PyQt-specific functions for PySide compatibility.
261 262 QtCore.Signal = QtCore.pyqtSignal
262 263 QtCore.Slot = QtCore.pyqtSlot
263 264
264 265 # Join QtGui and QtWidgets for Qt4 compatibility.
265 266 QtGuiCompat = types.ModuleType("QtGuiCompat")
266 267 QtGuiCompat.__dict__.update(QtGui.__dict__)
267 268 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
268 269
269 270 api = QT_API_PYQT6
270 271 return QtCore, QtGuiCompat, QtSvg, api
271 272
272 273
273 274 def import_pyside():
274 275 """
275 276 Import PySide
276 277
277 278 ImportErrors raised within this function are non-recoverable
278 279 """
279 280 from PySide import QtGui, QtCore, QtSvg
280 281 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
281 282
282 283 def import_pyside2():
283 284 """
284 285 Import PySide2
285 286
286 287 ImportErrors raised within this function are non-recoverable
287 288 """
288 289 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
289 290
290 291 # Join QtGui and QtWidgets for Qt4 compatibility.
291 292 QtGuiCompat = types.ModuleType('QtGuiCompat')
292 293 QtGuiCompat.__dict__.update(QtGui.__dict__)
293 294 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
294 295 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
295 296
296 297 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
297 298
298 299
299 300 def import_pyside6():
300 301 """
301 302 Import PySide6
302 303
303 304 ImportErrors raised within this function are non-recoverable
304 305 """
305 306
306 307 def get_attrs(module):
307 308 return {
308 309 name: getattr(module, name)
309 310 for name in dir(module)
310 311 if not name.startswith("_")
311 312 }
312 313
313 314 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
314 315
315 316 # Join QtGui and QtWidgets for Qt4 compatibility.
316 317 QtGuiCompat = types.ModuleType("QtGuiCompat")
317 318 QtGuiCompat.__dict__.update(QtGui.__dict__)
318 319 if QtCore.__version_info__ < (6, 7):
319 320 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
320 321 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
321 322 else:
322 323 QtGuiCompat.__dict__.update(get_attrs(QtWidgets))
323 324 QtGuiCompat.__dict__.update(get_attrs(QtPrintSupport))
324 325
325 326 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
326 327
327 328
328 329 def load_qt(api_options):
329 330 """
330 331 Attempt to import Qt, given a preference list
331 332 of permissible bindings
332 333
333 334 It is safe to call this function multiple times.
334 335
335 336 Parameters
336 337 ----------
337 338 api_options : List of strings
338 339 The order of APIs to try. Valid items are 'pyside', 'pyside2',
339 340 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
340 341
341 342 Returns
342 343 -------
343 344 A tuple of QtCore, QtGui, QtSvg, QT_API
344 345 The first three are the Qt modules. The last is the
345 346 string indicating which module was loaded.
346 347
347 348 Raises
348 349 ------
349 350 ImportError, if it isn't possible to import any requested
350 351 bindings (either because they aren't installed, or because
351 352 an incompatible library has already been installed)
352 353 """
353 354 loaders = {
354 355 # Qt6
355 356 QT_API_PYQT6: import_pyqt6,
356 357 QT_API_PYSIDE6: import_pyside6,
357 358 # Qt5
358 359 QT_API_PYQT5: import_pyqt5,
359 360 QT_API_PYSIDE2: import_pyside2,
360 361 # Qt4
361 362 QT_API_PYSIDE: import_pyside,
362 363 QT_API_PYQT: import_pyqt4,
363 364 QT_API_PYQTv1: partial(import_pyqt4, version=1),
364 365 # default
365 366 QT_API_PYQT_DEFAULT: import_pyqt6,
366 367 }
367 368
368 369 for api in api_options:
369 370
370 371 if api not in loaders:
371 372 raise RuntimeError(
372 373 "Invalid Qt API %r, valid values are: %s" %
373 374 (api, ", ".join(["%r" % k for k in loaders.keys()])))
374 375
375 376 if not can_import(api):
376 377 continue
377 378
378 379 #cannot safely recover from an ImportError during this
379 380 result = loaders[api]()
380 381 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
381 382 commit_api(api)
382 383 return result
383 384 else:
384 385 # Clear the environment variable since it doesn't work.
385 386 if "QT_API" in os.environ:
386 387 del os.environ["QT_API"]
387 388
388 389 raise ImportError(
389 390 """
390 391 Could not load requested Qt binding. Please ensure that
391 392 PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or
392 393 PySide6 is available, and only one is imported per session.
393 394
394 395 Currently-imported Qt library: %r
395 396 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
396 397 PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
397 398 PySide2 installed: %s
398 399 PySide6 installed: %s
399 400 Tried to load: %r
400 401 """
401 402 % (
402 403 loaded_api(),
403 404 has_binding(QT_API_PYQT5),
404 405 has_binding(QT_API_PYQT6),
405 406 has_binding(QT_API_PYSIDE2),
406 407 has_binding(QT_API_PYSIDE6),
407 408 api_options,
408 409 )
409 410 )
410 411
411 412
412 413 def enum_factory(QT_API, QtCore):
413 414 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
414 415
415 416 @lru_cache(None)
416 417 def _enum(name):
417 418 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
418 419 return operator.attrgetter(
419 420 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
420 421 )(sys.modules[QtCore.__package__])
421 422
422 423 return _enum
@@ -1,101 +1,102
1 1 """ Utilities for accessing the platform's clipboard.
2 2 """
3
3 4 import os
4 5 import subprocess
5 6
6 7 from IPython.core.error import TryNext
7 8 import IPython.utils.py3compat as py3compat
8 9
9 10
10 11 class ClipboardEmpty(ValueError):
11 12 pass
12 13
13 14
14 15 def win32_clipboard_get():
15 16 """ Get the current clipboard's text on Windows.
16 17
17 18 Requires Mark Hammond's pywin32 extensions.
18 19 """
19 20 try:
20 21 import win32clipboard
21 22 except ImportError as e:
22 23 raise TryNext("Getting text from the clipboard requires the pywin32 "
23 24 "extensions: http://sourceforge.net/projects/pywin32/") from e
24 25 win32clipboard.OpenClipboard()
25 26 try:
26 27 text = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
27 28 except (TypeError, win32clipboard.error):
28 29 try:
29 30 text = win32clipboard.GetClipboardData(win32clipboard.CF_TEXT)
30 31 text = py3compat.cast_unicode(text, py3compat.DEFAULT_ENCODING)
31 32 except (TypeError, win32clipboard.error) as e:
32 33 raise ClipboardEmpty from e
33 34 finally:
34 35 win32clipboard.CloseClipboard()
35 36 return text
36 37
37 38
38 39 def osx_clipboard_get() -> str:
39 40 """ Get the clipboard's text on OS X.
40 41 """
41 42 p = subprocess.Popen(['pbpaste', '-Prefer', 'ascii'],
42 43 stdout=subprocess.PIPE)
43 44 bytes_, stderr = p.communicate()
44 45 # Text comes in with old Mac \r line endings. Change them to \n.
45 46 bytes_ = bytes_.replace(b'\r', b'\n')
46 47 text = py3compat.decode(bytes_)
47 48 return text
48 49
49 50
50 51 def tkinter_clipboard_get():
51 52 """ Get the clipboard's text using Tkinter.
52 53
53 54 This is the default on systems that are not Windows or OS X. It may
54 55 interfere with other UI toolkits and should be replaced with an
55 56 implementation that uses that toolkit.
56 57 """
57 58 try:
58 59 from tkinter import Tk, TclError
59 60 except ImportError as e:
60 61 raise TryNext("Getting text from the clipboard on this platform requires tkinter.") from e
61 62
62 63 root = Tk()
63 64 root.withdraw()
64 65 try:
65 66 text = root.clipboard_get()
66 67 except TclError as e:
67 68 raise ClipboardEmpty from e
68 69 finally:
69 70 root.destroy()
70 71 text = py3compat.cast_unicode(text, py3compat.DEFAULT_ENCODING)
71 72 return text
72 73
73 74
74 75 def wayland_clipboard_get():
75 76 """Get the clipboard's text under Wayland using wl-paste command.
76 77
77 78 This requires Wayland and wl-clipboard installed and running.
78 79 """
79 80 if os.environ.get("XDG_SESSION_TYPE") != "wayland":
80 81 raise TryNext("wayland is not detected")
81 82
82 83 try:
83 84 with subprocess.Popen(["wl-paste"], stdout=subprocess.PIPE) as p:
84 85 raw, err = p.communicate()
85 86 if p.wait():
86 87 raise TryNext(err)
87 88 except FileNotFoundError as e:
88 89 raise TryNext(
89 90 "Getting text from the clipboard under Wayland requires the wl-clipboard "
90 91 "extension: https://github.com/bugaevc/wl-clipboard"
91 92 ) from e
92 93
93 94 if not raw:
94 95 raise ClipboardEmpty
95 96
96 97 try:
97 98 text = py3compat.decode(raw)
98 99 except UnicodeDecodeError as e:
99 100 raise ClipboardEmpty from e
100 101
101 102 return text
@@ -1,155 +1,155
1 1 """
2 2 Handlers for IPythonDirective's @doctest pseudo-decorator.
3 3
4 4 The Sphinx extension that provides support for embedded IPython code provides
5 5 a pseudo-decorator @doctest, which treats the input/output block as a
6 6 doctest, raising a RuntimeError during doc generation if the actual output
7 7 (after running the input) does not match the expected output.
8 8
9 9 An example usage is:
10 10
11 11 .. code-block:: rst
12 12
13 13 .. ipython::
14 14
15 15 In [1]: x = 1
16 16
17 17 @doctest
18 18 In [2]: x + 2
19 19 Out[3]: 3
20 20
21 21 One can also provide arguments to the decorator. The first argument should be
22 22 the name of a custom handler. The specification of any other arguments is
23 23 determined by the handler. For example,
24 24
25 25 .. code-block:: rst
26 26
27 27 .. ipython::
28 28
29 29 @doctest float
30 30 In [154]: 0.1 + 0.2
31 31 Out[154]: 0.3
32 32
33 33 allows the actual output ``0.30000000000000004`` to match the expected output
34 34 due to a comparison with `np.allclose`.
35 35
36 36 This module contains handlers for the @doctest pseudo-decorator. Handlers
37 37 should have the following function signature::
38 38
39 39 handler(sphinx_shell, args, input_lines, found, submitted)
40 40
41 41 where `sphinx_shell` is the embedded Sphinx shell, `args` contains the list
42 42 of arguments that follow: '@doctest handler_name', `input_lines` contains
43 43 a list of the lines relevant to the current doctest, `found` is a string
44 44 containing the output from the IPython shell, and `submitted` is a string
45 45 containing the expected output from the IPython shell.
46 46
47 47 Handlers must be registered in the `doctests` dict at the end of this module.
48 48
49 49 """
50 50
51 51 def str_to_array(s):
52 52 """
53 53 Simplistic converter of strings from repr to float NumPy arrays.
54 54
55 55 If the repr representation has ellipsis in it, then this will fail.
56 56
57 57 Parameters
58 58 ----------
59 59 s : str
60 60 The repr version of a NumPy array.
61 61
62 62 Examples
63 63 --------
64 64 >>> s = "array([ 0.3, inf, nan])"
65 65 >>> a = str_to_array(s)
66 66
67 67 """
68 68 import numpy as np
69 69
70 70 # Need to make sure eval() knows about inf and nan.
71 71 # This also assumes default printoptions for NumPy.
72 72 from numpy import inf, nan
73 73
74 74 if s.startswith(u'array'):
75 75 # Remove array( and )
76 76 s = s[6:-1]
77 77
78 78 if s.startswith(u'['):
79 79 a = np.array(eval(s), dtype=float)
80 80 else:
81 81 # Assume its a regular float. Force 1D so we can index into it.
82 82 a = np.atleast_1d(float(s))
83 83 return a
84 84
85 85 def float_doctest(sphinx_shell, args, input_lines, found, submitted):
86 86 """
87 87 Doctest which allow the submitted output to vary slightly from the input.
88 88
89 89 Here is how it might appear in an rst file:
90 90
91 91 .. code-block:: rst
92 92
93 93 .. ipython::
94 94
95 95 @doctest float
96 96 In [1]: 0.1 + 0.2
97 97 Out[1]: 0.3
98 98
99 99 """
100 100 import numpy as np
101 101
102 102 if len(args) == 2:
103 103 rtol = 1e-05
104 104 atol = 1e-08
105 105 else:
106 106 # Both must be specified if any are specified.
107 107 try:
108 108 rtol = float(args[2])
109 109 atol = float(args[3])
110 except IndexError as e:
110 except IndexError:
111 111 e = ("Both `rtol` and `atol` must be specified "
112 112 "if either are specified: {0}".format(args))
113 113 raise IndexError(e) from e
114 114
115 115 try:
116 116 submitted = str_to_array(submitted)
117 117 found = str_to_array(found)
118 118 except:
119 119 # For example, if the array is huge and there are ellipsis in it.
120 120 error = True
121 121 else:
122 122 found_isnan = np.isnan(found)
123 123 submitted_isnan = np.isnan(submitted)
124 124 error = not np.allclose(found_isnan, submitted_isnan)
125 125 error |= not np.allclose(found[~found_isnan],
126 126 submitted[~submitted_isnan],
127 127 rtol=rtol, atol=atol)
128 128
129 129 TAB = ' ' * 4
130 130 directive = sphinx_shell.directive
131 131 if directive is None:
132 132 source = 'Unavailable'
133 133 content = 'Unavailable'
134 134 else:
135 135 source = directive.state.document.current_source
136 136 # Add tabs and make into a single string.
137 137 content = '\n'.join([TAB + line for line in directive.content])
138 138
139 139 if error:
140 140
141 141 e = ('doctest float comparison failure\n\n'
142 142 'Document source: {0}\n\n'
143 143 'Raw content: \n{1}\n\n'
144 144 'On input line(s):\n{TAB}{2}\n\n'
145 145 'we found output:\n{TAB}{3}\n\n'
146 146 'instead of the expected:\n{TAB}{4}\n\n')
147 147 e = e.format(source, content, '\n'.join(input_lines), repr(found),
148 148 repr(submitted), TAB=TAB)
149 149 raise RuntimeError(e)
150 150
151 151 # dict of allowable doctest handlers. The key represents the first argument
152 152 # that must be given to @doctest in order to activate the handler.
153 153 doctests = {
154 154 'float': float_doctest,
155 155 }
@@ -1,1276 +1,1278
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 Sphinx directive to support embedded IPython code.
4 4
5 5 IPython provides an extension for `Sphinx <http://www.sphinx-doc.org/>`_ to
6 6 highlight and run code.
7 7
8 8 This directive allows pasting of entire interactive IPython sessions, prompts
9 9 and all, and their code will actually get re-executed at doc build time, with
10 10 all prompts renumbered sequentially. It also allows you to input code as a pure
11 11 python input by giving the argument python to the directive. The output looks
12 12 like an interactive ipython section.
13 13
14 14 Here is an example of how the IPython directive can
15 15 **run** python code, at build time.
16 16
17 17 .. ipython::
18 18
19 19 In [1]: 1+1
20 20
21 21 In [1]: import datetime
22 22 ...: datetime.date.fromisoformat('2022-02-22')
23 23
24 24 It supports IPython construct that plain
25 25 Python does not understand (like magics):
26 26
27 27 .. ipython::
28 28
29 29 In [0]: import time
30 30
31 31 In [0]: %pdoc time.sleep
32 32
33 33 This will also support top-level async when using IPython 7.0+
34 34
35 35 .. ipython::
36 36
37 37 In [2]: import asyncio
38 38 ...: print('before')
39 39 ...: await asyncio.sleep(1)
40 40 ...: print('after')
41 41
42 42
43 43 The namespace will persist across multiple code chucks, Let's define a variable:
44 44
45 45 .. ipython::
46 46
47 47 In [0]: who = "World"
48 48
49 49 And now say hello:
50 50
51 51 .. ipython::
52 52
53 53 In [0]: print('Hello,', who)
54 54
55 55 If the current section raises an exception, you can add the ``:okexcept:`` flag
56 56 to the current block, otherwise the build will fail.
57 57
58 58 .. ipython::
59 59 :okexcept:
60 60
61 61 In [1]: 1/0
62 62
63 63 IPython Sphinx directive module
64 64 ===============================
65 65
66 66 To enable this directive, simply list it in your Sphinx ``conf.py`` file
67 67 (making sure the directory where you placed it is visible to sphinx, as is
68 68 needed for all Sphinx directives). For example, to enable syntax highlighting
69 69 and the IPython directive::
70 70
71 71 extensions = ['IPython.sphinxext.ipython_console_highlighting',
72 72 'IPython.sphinxext.ipython_directive']
73 73
74 74 The IPython directive outputs code-blocks with the language 'ipython'. So
75 75 if you do not have the syntax highlighting extension enabled as well, then
76 76 all rendered code-blocks will be uncolored. By default this directive assumes
77 77 that your prompts are unchanged IPython ones, but this can be customized.
78 78 The configurable options that can be placed in conf.py are:
79 79
80 80 ipython_savefig_dir:
81 81 The directory in which to save the figures. This is relative to the
82 82 Sphinx source directory. The default is `html_static_path`.
83 83 ipython_rgxin:
84 84 The compiled regular expression to denote the start of IPython input
85 85 lines. The default is ``re.compile('In \\[(\\d+)\\]:\\s?(.*)\\s*')``. You
86 86 shouldn't need to change this.
87 87 ipython_warning_is_error: [default to True]
88 88 Fail the build if something unexpected happen, for example if a block raise
89 89 an exception but does not have the `:okexcept:` flag. The exact behavior of
90 90 what is considered strict, may change between the sphinx directive version.
91 91 ipython_rgxout:
92 92 The compiled regular expression to denote the start of IPython output
93 93 lines. The default is ``re.compile('Out\\[(\\d+)\\]:\\s?(.*)\\s*')``. You
94 94 shouldn't need to change this.
95 95 ipython_promptin:
96 96 The string to represent the IPython input prompt in the generated ReST.
97 97 The default is ``'In [%d]:'``. This expects that the line numbers are used
98 98 in the prompt.
99 99 ipython_promptout:
100 100 The string to represent the IPython prompt in the generated ReST. The
101 101 default is ``'Out [%d]:'``. This expects that the line numbers are used
102 102 in the prompt.
103 103 ipython_mplbackend:
104 104 The string which specifies if the embedded Sphinx shell should import
105 105 Matplotlib and set the backend. The value specifies a backend that is
106 106 passed to `matplotlib.use()` before any lines in `ipython_execlines` are
107 107 executed. If not specified in conf.py, then the default value of 'agg' is
108 108 used. To use the IPython directive without matplotlib as a dependency, set
109 109 the value to `None`. It may end up that matplotlib is still imported
110 110 if the user specifies so in `ipython_execlines` or makes use of the
111 111 @savefig pseudo decorator.
112 112 ipython_execlines:
113 113 A list of strings to be exec'd in the embedded Sphinx shell. Typical
114 114 usage is to make certain packages always available. Set this to an empty
115 115 list if you wish to have no imports always available. If specified in
116 116 ``conf.py`` as `None`, then it has the effect of making no imports available.
117 117 If omitted from conf.py altogether, then the default value of
118 118 ['import numpy as np', 'import matplotlib.pyplot as plt'] is used.
119 119 ipython_holdcount
120 120 When the @suppress pseudo-decorator is used, the execution count can be
121 121 incremented or not. The default behavior is to hold the execution count,
122 122 corresponding to a value of `True`. Set this to `False` to increment
123 123 the execution count after each suppressed command.
124 124
125 125 As an example, to use the IPython directive when `matplotlib` is not available,
126 126 one sets the backend to `None`::
127 127
128 128 ipython_mplbackend = None
129 129
130 130 An example usage of the directive is:
131 131
132 132 .. code-block:: rst
133 133
134 134 .. ipython::
135 135
136 136 In [1]: x = 1
137 137
138 138 In [2]: y = x**2
139 139
140 140 In [3]: print(y)
141 141
142 142 See http://matplotlib.org/sampledoc/ipython_directive.html for additional
143 143 documentation.
144 144
145 145 Pseudo-Decorators
146 146 =================
147 147
148 148 Note: Only one decorator is supported per input. If more than one decorator
149 149 is specified, then only the last one is used.
150 150
151 151 In addition to the Pseudo-Decorators/options described at the above link,
152 152 several enhancements have been made. The directive will emit a message to the
153 153 console at build-time if code-execution resulted in an exception or warning.
154 154 You can suppress these on a per-block basis by specifying the :okexcept:
155 155 or :okwarning: options:
156 156
157 157 .. code-block:: rst
158 158
159 159 .. ipython::
160 160 :okexcept:
161 161 :okwarning:
162 162
163 163 In [1]: 1/0
164 164 In [2]: # raise warning.
165 165
166 166 To Do
167 167 =====
168 168
169 169 - Turn the ad-hoc test() function into a real test suite.
170 170 - Break up ipython-specific functionality from matplotlib stuff into better
171 171 separated code.
172 172
173 173 """
174 174
175 175 # Authors
176 176 # =======
177 #
177 #
178 178 # - John D Hunter: original author.
179 179 # - Fernando Perez: refactoring, documentation, cleanups, port to 0.11.
180 180 # - VΓ‘clavΕ milauer <eudoxos-AT-arcig.cz>: Prompt generalizations.
181 181 # - Skipper Seabold, refactoring, cleanups, pure python addition
182 182
183 183 #-----------------------------------------------------------------------------
184 184 # Imports
185 185 #-----------------------------------------------------------------------------
186 186
187 187 # Stdlib
188 188 import atexit
189 189 import errno
190 190 import os
191 191 import pathlib
192 192 import re
193 193 import sys
194 194 import tempfile
195 195 import ast
196 196 import warnings
197 197 import shutil
198 198 from io import StringIO
199 from typing import Any, Dict, Set
199 200
200 201 # Third-party
201 202 from docutils.parsers.rst import directives
202 203 from docutils.parsers.rst import Directive
203 204 from sphinx.util import logging
204 205
205 206 # Our own
206 207 from traitlets.config import Config
207 208 from IPython import InteractiveShell
208 209 from IPython.core.profiledir import ProfileDir
209 210
210 211 use_matplotlib = False
211 212 try:
212 213 import matplotlib
213 214 use_matplotlib = True
214 215 except Exception:
215 216 pass
216 217
217 218 #-----------------------------------------------------------------------------
218 219 # Globals
219 220 #-----------------------------------------------------------------------------
220 221 # for tokenizing blocks
221 222 COMMENT, INPUT, OUTPUT = range(3)
222 223
223 224 PSEUDO_DECORATORS = ["suppress", "verbatim", "savefig", "doctest"]
224 225
225 226 #-----------------------------------------------------------------------------
226 227 # Functions and class declarations
227 228 #-----------------------------------------------------------------------------
228 229
229 230 def block_parser(part, rgxin, rgxout, fmtin, fmtout):
230 231 """
231 232 part is a string of ipython text, comprised of at most one
232 233 input, one output, comments, and blank lines. The block parser
233 234 parses the text into a list of::
234 235
235 236 blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
236 237
237 238 where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
238 239 data is, depending on the type of token::
239 240
240 241 COMMENT : the comment string
241 242
242 243 INPUT: the (DECORATOR, INPUT_LINE, REST) where
243 244 DECORATOR: the input decorator (or None)
244 245 INPUT_LINE: the input as string (possibly multi-line)
245 246 REST : any stdout generated by the input line (not OUTPUT)
246 247
247 248 OUTPUT: the output string, possibly multi-line
248 249
249 250 """
250 251 block = []
251 252 lines = part.split('\n')
252 253 N = len(lines)
253 254 i = 0
254 255 decorator = None
255 256 while 1:
256 257
257 258 if i==N:
258 259 # nothing left to parse -- the last line
259 260 break
260 261
261 262 line = lines[i]
262 263 i += 1
263 264 line_stripped = line.strip()
264 265 if line_stripped.startswith('#'):
265 266 block.append((COMMENT, line))
266 267 continue
267 268
268 269 if any(
269 270 line_stripped.startswith("@" + pseudo_decorator)
270 271 for pseudo_decorator in PSEUDO_DECORATORS
271 272 ):
272 273 if decorator:
273 274 raise RuntimeError(
274 275 "Applying multiple pseudo-decorators on one line is not supported"
275 276 )
276 277 else:
277 278 decorator = line_stripped
278 279 continue
279 280
280 281 # does this look like an input line?
281 282 matchin = rgxin.match(line)
282 283 if matchin:
283 284 lineno, inputline = int(matchin.group(1)), matchin.group(2)
284 285
285 286 # the ....: continuation string
286 287 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
287 288 Nc = len(continuation)
288 289 # input lines can continue on for more than one line, if
289 290 # we have a '\' line continuation char or a function call
290 291 # echo line 'print'. The input line can only be
291 292 # terminated by the end of the block or an output line, so
292 293 # we parse out the rest of the input line if it is
293 294 # multiline as well as any echo text
294 295
295 296 rest = []
296 297 while i<N:
297 298
298 299 # look ahead; if the next line is blank, or a comment, or
299 300 # an output line, we're done
300 301
301 302 nextline = lines[i]
302 303 matchout = rgxout.match(nextline)
303 304 # print("nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation)))
304 305 if matchout or nextline.startswith('#'):
305 306 break
306 307 elif nextline.startswith(continuation):
307 308 # The default ipython_rgx* treat the space following the colon as optional.
308 309 # However, If the space is there we must consume it or code
309 310 # employing the cython_magic extension will fail to execute.
310 311 #
311 312 # This works with the default ipython_rgx* patterns,
312 313 # If you modify them, YMMV.
313 314 nextline = nextline[Nc:]
314 315 if nextline and nextline[0] == ' ':
315 316 nextline = nextline[1:]
316 317
317 318 inputline += '\n' + nextline
318 319 else:
319 320 rest.append(nextline)
320 321 i+= 1
321 322
322 323 block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
323 324 continue
324 325
325 326 # if it looks like an output line grab all the text to the end
326 327 # of the block
327 328 matchout = rgxout.match(line)
328 329 if matchout:
329 330 lineno, output = int(matchout.group(1)), matchout.group(2)
330 331 if i<N-1:
331 332 output = '\n'.join([output] + lines[i:])
332 333
333 334 block.append((OUTPUT, output))
334 335 break
335 336
336 337 return block
337 338
338 339
339 340 class EmbeddedSphinxShell(object):
340 341 """An embedded IPython instance to run inside Sphinx"""
341 342
342 343 def __init__(self, exec_lines=None):
343 344
344 345 self.cout = StringIO()
345 346
346 347 if exec_lines is None:
347 348 exec_lines = []
348 349
349 350 # Create config object for IPython
350 351 config = Config()
351 352 config.HistoryManager.hist_file = ':memory:'
352 353 config.InteractiveShell.autocall = False
353 354 config.InteractiveShell.autoindent = False
354 355 config.InteractiveShell.colors = 'NoColor'
355 356
356 357 # create a profile so instance history isn't saved
357 358 tmp_profile_dir = tempfile.mkdtemp(prefix='profile_')
358 359 profname = 'auto_profile_sphinx_build'
359 360 pdir = os.path.join(tmp_profile_dir,profname)
360 361 profile = ProfileDir.create_profile_dir(pdir)
361 362
362 363 # Create and initialize global ipython, but don't start its mainloop.
363 364 # This will persist across different EmbeddedSphinxShell instances.
364 365 IP = InteractiveShell.instance(config=config, profile_dir=profile)
365 366 atexit.register(self.cleanup)
366 367
367 368 # Store a few parts of IPython we'll need.
368 369 self.IP = IP
369 370 self.user_ns = self.IP.user_ns
370 371 self.user_global_ns = self.IP.user_global_ns
371 372
372 373 self.input = ''
373 374 self.output = ''
374 375 self.tmp_profile_dir = tmp_profile_dir
375 376
376 377 self.is_verbatim = False
377 378 self.is_doctest = False
378 379 self.is_suppress = False
379 380
380 381 # Optionally, provide more detailed information to shell.
381 382 # this is assigned by the SetUp method of IPythonDirective
382 383 # to point at itself.
383 384 #
384 385 # So, you can access handy things at self.directive.state
385 386 self.directive = None
386 387
387 388 # on the first call to the savefig decorator, we'll import
388 389 # pyplot as plt so we can make a call to the plt.gcf().savefig
389 390 self._pyplot_imported = False
390 391
391 392 # Prepopulate the namespace.
392 393 for line in exec_lines:
393 394 self.process_input_line(line, store_history=False)
394 395
395 396 def cleanup(self):
396 397 shutil.rmtree(self.tmp_profile_dir, ignore_errors=True)
397 398
398 399 def clear_cout(self):
399 400 self.cout.seek(0)
400 401 self.cout.truncate(0)
401 402
402 403 def process_input_line(self, line, store_history):
403 404 return self.process_input_lines([line], store_history=store_history)
404 405
405 406 def process_input_lines(self, lines, store_history=True):
406 407 """process the input, capturing stdout"""
407 408 stdout = sys.stdout
408 409 source_raw = '\n'.join(lines)
409 410 try:
410 411 sys.stdout = self.cout
411 412 self.IP.run_cell(source_raw, store_history=store_history)
412 413 finally:
413 414 sys.stdout = stdout
414 415
415 416 def process_image(self, decorator):
416 417 """
417 418 # build out an image directive like
418 419 # .. image:: somefile.png
419 420 # :width 4in
420 421 #
421 422 # from an input like
422 423 # savefig somefile.png width=4in
423 424 """
424 425 savefig_dir = self.savefig_dir
425 426 source_dir = self.source_dir
426 427 saveargs = decorator.split(' ')
427 428 filename = saveargs[1]
428 # insert relative path to image file in source
429 # insert relative path to image file in source
429 430 # as absolute path for Sphinx
430 431 # sphinx expects a posix path, even on Windows
431 432 path = pathlib.Path(savefig_dir, filename)
432 433 outfile = '/' + path.relative_to(source_dir).as_posix()
433 434
434 435 imagerows = ['.. image:: %s' % outfile]
435 436
436 437 for kwarg in saveargs[2:]:
437 438 arg, val = kwarg.split('=')
438 439 arg = arg.strip()
439 440 val = val.strip()
440 441 imagerows.append(' :%s: %s'%(arg, val))
441 442
442 443 image_file = os.path.basename(outfile) # only return file name
443 444 image_directive = '\n'.join(imagerows)
444 445 return image_file, image_directive
445 446
446 447 # Callbacks for each type of token
447 448 def process_input(self, data, input_prompt, lineno):
448 449 """
449 450 Process data block for INPUT token.
450 451
451 452 """
452 453 decorator, input, rest = data
453 454 image_file = None
454 455 image_directive = None
455 456
456 457 is_verbatim = decorator=='@verbatim' or self.is_verbatim
457 458 is_doctest = (decorator is not None and \
458 459 decorator.startswith('@doctest')) or self.is_doctest
459 460 is_suppress = decorator=='@suppress' or self.is_suppress
460 461 is_okexcept = decorator=='@okexcept' or self.is_okexcept
461 462 is_okwarning = decorator=='@okwarning' or self.is_okwarning
462 463 is_savefig = decorator is not None and \
463 464 decorator.startswith('@savefig')
464 465
465 466 input_lines = input.split('\n')
466 467 if len(input_lines) > 1:
467 468 if input_lines[-1] != "":
468 469 input_lines.append('') # make sure there's a blank line
469 470 # so splitter buffer gets reset
470 471
471 472 continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
472 473
473 474 if is_savefig:
474 475 image_file, image_directive = self.process_image(decorator)
475 476
476 477 ret = []
477 478 is_semicolon = False
478 479
479 480 # Hold the execution count, if requested to do so.
480 481 if is_suppress and self.hold_count:
481 482 store_history = False
482 483 else:
483 484 store_history = True
484 485
485 486 # Note: catch_warnings is not thread safe
486 487 with warnings.catch_warnings(record=True) as ws:
487 488 if input_lines[0].endswith(';'):
488 489 is_semicolon = True
489 490 #for i, line in enumerate(input_lines):
490 491
491 492 # process the first input line
492 493 if is_verbatim:
493 494 self.process_input_lines([''])
494 495 self.IP.execution_count += 1 # increment it anyway
495 496 else:
496 497 # only submit the line in non-verbatim mode
497 498 self.process_input_lines(input_lines, store_history=store_history)
498 499
499 500 if not is_suppress:
500 501 for i, line in enumerate(input_lines):
501 502 if i == 0:
502 503 formatted_line = '%s %s'%(input_prompt, line)
503 504 else:
504 505 formatted_line = '%s %s'%(continuation, line)
505 506 ret.append(formatted_line)
506 507
507 508 if not is_suppress and len(rest.strip()) and is_verbatim:
508 509 # The "rest" is the standard output of the input. This needs to be
509 510 # added when in verbatim mode. If there is no "rest", then we don't
510 511 # add it, as the new line will be added by the processed output.
511 512 ret.append(rest)
512 513
513 514 # Fetch the processed output. (This is not the submitted output.)
514 515 self.cout.seek(0)
515 516 processed_output = self.cout.read()
516 517 if not is_suppress and not is_semicolon:
517 518 #
518 519 # In IPythonDirective.run, the elements of `ret` are eventually
519 520 # combined such that '' entries correspond to newlines. So if
520 521 # `processed_output` is equal to '', then the adding it to `ret`
521 522 # ensures that there is a blank line between consecutive inputs
522 523 # that have no outputs, as in:
523 524 #
524 525 # In [1]: x = 4
525 526 #
526 527 # In [2]: x = 5
527 528 #
528 529 # When there is processed output, it has a '\n' at the tail end. So
529 530 # adding the output to `ret` will provide the necessary spacing
530 531 # between consecutive input/output blocks, as in:
531 532 #
532 533 # In [1]: x
533 534 # Out[1]: 5
534 535 #
535 536 # In [2]: x
536 537 # Out[2]: 5
537 538 #
538 539 # When there is stdout from the input, it also has a '\n' at the
539 540 # tail end, and so this ensures proper spacing as well. E.g.:
540 541 #
541 542 # In [1]: print(x)
542 543 # 5
543 544 #
544 545 # In [2]: x = 5
545 546 #
546 547 # When in verbatim mode, `processed_output` is empty (because
547 548 # nothing was passed to IP. Sometimes the submitted code block has
548 549 # an Out[] portion and sometimes it does not. When it does not, we
549 550 # need to ensure proper spacing, so we have to add '' to `ret`.
550 551 # However, if there is an Out[] in the submitted code, then we do
551 552 # not want to add a newline as `process_output` has stuff to add.
552 553 # The difficulty is that `process_input` doesn't know if
553 554 # `process_output` will be called---so it doesn't know if there is
554 555 # Out[] in the code block. The requires that we include a hack in
555 556 # `process_block`. See the comments there.
556 557 #
557 558 ret.append(processed_output)
558 559 elif is_semicolon:
559 560 # Make sure there is a newline after the semicolon.
560 561 ret.append('')
561 562
562 563 # context information
563 564 filename = "Unknown"
564 565 lineno = 0
565 566 if self.directive.state:
566 567 filename = self.directive.state.document.current_source
567 568 lineno = self.directive.state.document.current_line
568 569
569 570 # Use sphinx logger for warnings
570 571 logger = logging.getLogger(__name__)
571 572
572 573 # output any exceptions raised during execution to stdout
573 574 # unless :okexcept: has been specified.
574 575 if not is_okexcept and (
575 576 ("Traceback" in processed_output) or ("SyntaxError" in processed_output)
576 577 ):
577 578 s = "\n>>>" + ("-" * 73) + "\n"
578 579 s += "Exception in %s at block ending on line %s\n" % (filename, lineno)
579 580 s += "Specify :okexcept: as an option in the ipython:: block to suppress this message\n"
580 581 s += processed_output + "\n"
581 582 s += "<<<" + ("-" * 73)
582 583 logger.warning(s)
583 584 if self.warning_is_error:
584 585 raise RuntimeError(
585 586 "Unexpected exception in `{}` line {}".format(filename, lineno)
586 587 )
587 588
588 589 # output any warning raised during execution to stdout
589 590 # unless :okwarning: has been specified.
590 591 if not is_okwarning:
591 592 for w in ws:
592 593 s = "\n>>>" + ("-" * 73) + "\n"
593 594 s += "Warning in %s at block ending on line %s\n" % (filename, lineno)
594 595 s += "Specify :okwarning: as an option in the ipython:: block to suppress this message\n"
595 596 s += ("-" * 76) + "\n"
596 597 s += warnings.formatwarning(
597 598 w.message, w.category, w.filename, w.lineno, w.line
598 599 )
599 600 s += "<<<" + ("-" * 73)
600 601 logger.warning(s)
601 602 if self.warning_is_error:
602 603 raise RuntimeError(
603 604 "Unexpected warning in `{}` line {}".format(filename, lineno)
604 605 )
605 606
606 607 self.clear_cout()
607 608 return (ret, input_lines, processed_output,
608 609 is_doctest, decorator, image_file, image_directive)
609 610
610 611
611 612 def process_output(self, data, output_prompt, input_lines, output,
612 613 is_doctest, decorator, image_file):
613 614 """
614 615 Process data block for OUTPUT token.
615 616
616 617 """
617 618 # Recall: `data` is the submitted output, and `output` is the processed
618 619 # output from `input_lines`.
619 620
620 621 TAB = ' ' * 4
621 622
622 623 if is_doctest and output is not None:
623 624
624 625 found = output # This is the processed output
625 626 found = found.strip()
626 627 submitted = data.strip()
627 628
628 629 if self.directive is None:
629 630 source = 'Unavailable'
630 631 content = 'Unavailable'
631 632 else:
632 633 source = self.directive.state.document.current_source
633 634 content = self.directive.content
634 635 # Add tabs and join into a single string.
635 636 content = '\n'.join([TAB + line for line in content])
636 637
637 638 # Make sure the output contains the output prompt.
638 639 ind = found.find(output_prompt)
639 640 if ind < 0:
640 641 e = ('output does not contain output prompt\n\n'
641 642 'Document source: {0}\n\n'
642 643 'Raw content: \n{1}\n\n'
643 644 'Input line(s):\n{TAB}{2}\n\n'
644 645 'Output line(s):\n{TAB}{3}\n\n')
645 646 e = e.format(source, content, '\n'.join(input_lines),
646 647 repr(found), TAB=TAB)
647 648 raise RuntimeError(e)
648 649 found = found[len(output_prompt):].strip()
649 650
650 651 # Handle the actual doctest comparison.
651 652 if decorator.strip() == '@doctest':
652 653 # Standard doctest
653 654 if found != submitted:
654 655 e = ('doctest failure\n\n'
655 656 'Document source: {0}\n\n'
656 657 'Raw content: \n{1}\n\n'
657 658 'On input line(s):\n{TAB}{2}\n\n'
658 659 'we found output:\n{TAB}{3}\n\n'
659 660 'instead of the expected:\n{TAB}{4}\n\n')
660 661 e = e.format(source, content, '\n'.join(input_lines),
661 662 repr(found), repr(submitted), TAB=TAB)
662 663 raise RuntimeError(e)
663 664 else:
664 665 self.custom_doctest(decorator, input_lines, found, submitted)
665 666
666 667 # When in verbatim mode, this holds additional submitted output
667 668 # to be written in the final Sphinx output.
668 669 # https://github.com/ipython/ipython/issues/5776
669 670 out_data = []
670 671
671 672 is_verbatim = decorator=='@verbatim' or self.is_verbatim
672 673 if is_verbatim and data.strip():
673 674 # Note that `ret` in `process_block` has '' as its last element if
674 675 # the code block was in verbatim mode. So if there is no submitted
675 676 # output, then we will have proper spacing only if we do not add
676 677 # an additional '' to `out_data`. This is why we condition on
677 678 # `and data.strip()`.
678 679
679 680 # The submitted output has no output prompt. If we want the
680 681 # prompt and the code to appear, we need to join them now
681 682 # instead of adding them separately---as this would create an
682 683 # undesired newline. How we do this ultimately depends on the
683 684 # format of the output regex. I'll do what works for the default
684 685 # prompt for now, and we might have to adjust if it doesn't work
685 686 # in other cases. Finally, the submitted output does not have
686 687 # a trailing newline, so we must add it manually.
687 688 out_data.append("{0} {1}\n".format(output_prompt, data))
688 689
689 690 return out_data
690 691
691 692 def process_comment(self, data):
692 693 """Process data fPblock for COMMENT token."""
693 694 if not self.is_suppress:
694 695 return [data]
695 696
696 697 def save_image(self, image_file):
697 698 """
698 699 Saves the image file to disk.
699 700 """
700 701 self.ensure_pyplot()
701 702 command = 'plt.gcf().savefig("%s")'%image_file
702 703 # print('SAVEFIG', command) # dbg
703 704 self.process_input_line('bookmark ipy_thisdir', store_history=False)
704 705 self.process_input_line('cd -b ipy_savedir', store_history=False)
705 706 self.process_input_line(command, store_history=False)
706 707 self.process_input_line('cd -b ipy_thisdir', store_history=False)
707 708 self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
708 709 self.clear_cout()
709 710
710 711 def process_block(self, block):
711 712 """
712 713 process block from the block_parser and return a list of processed lines
713 714 """
714 715 ret = []
715 716 output = None
716 717 input_lines = None
717 718 lineno = self.IP.execution_count
718 719
719 720 input_prompt = self.promptin % lineno
720 721 output_prompt = self.promptout % lineno
721 722 image_file = None
722 723 image_directive = None
723 724
724 725 found_input = False
725 726 for token, data in block:
726 727 if token == COMMENT:
727 728 out_data = self.process_comment(data)
728 729 elif token == INPUT:
729 730 found_input = True
730 731 (out_data, input_lines, output, is_doctest,
731 732 decorator, image_file, image_directive) = \
732 733 self.process_input(data, input_prompt, lineno)
733 734 elif token == OUTPUT:
734 735 if not found_input:
735 736
736 737 TAB = ' ' * 4
737 738 linenumber = 0
738 739 source = 'Unavailable'
739 740 content = 'Unavailable'
740 741 if self.directive:
741 742 linenumber = self.directive.state.document.current_line
742 743 source = self.directive.state.document.current_source
743 744 content = self.directive.content
744 745 # Add tabs and join into a single string.
745 746 content = '\n'.join([TAB + line for line in content])
746 747
747 748 e = ('\n\nInvalid block: Block contains an output prompt '
748 749 'without an input prompt.\n\n'
749 750 'Document source: {0}\n\n'
750 751 'Content begins at line {1}: \n\n{2}\n\n'
751 752 'Problematic block within content: \n\n{TAB}{3}\n\n')
752 753 e = e.format(source, linenumber, content, block, TAB=TAB)
753 754
754 755 # Write, rather than include in exception, since Sphinx
755 756 # will truncate tracebacks.
756 757 sys.stdout.write(e)
757 758 raise RuntimeError('An invalid block was detected.')
758 759 out_data = \
759 760 self.process_output(data, output_prompt, input_lines,
760 761 output, is_doctest, decorator,
761 762 image_file)
762 763 if out_data:
763 764 # Then there was user submitted output in verbatim mode.
764 765 # We need to remove the last element of `ret` that was
765 766 # added in `process_input`, as it is '' and would introduce
766 767 # an undesirable newline.
767 768 assert(ret[-1] == '')
768 769 del ret[-1]
769 770
770 771 if out_data:
771 772 ret.extend(out_data)
772 773
773 774 # save the image files
774 775 if image_file is not None:
775 776 self.save_image(image_file)
776 777
777 778 return ret, image_directive
778 779
779 780 def ensure_pyplot(self):
780 781 """
781 782 Ensures that pyplot has been imported into the embedded IPython shell.
782 783
783 784 Also, makes sure to set the backend appropriately if not set already.
784 785
785 786 """
786 787 # We are here if the @figure pseudo decorator was used. Thus, it's
787 788 # possible that we could be here even if python_mplbackend were set to
788 789 # `None`. That's also strange and perhaps worthy of raising an
789 790 # exception, but for now, we just set the backend to 'agg'.
790 791
791 792 if not self._pyplot_imported:
792 793 if 'matplotlib.backends' not in sys.modules:
793 794 # Then ipython_matplotlib was set to None but there was a
794 795 # call to the @figure decorator (and ipython_execlines did
795 796 # not set a backend).
796 797 #raise Exception("No backend was set, but @figure was used!")
797 798 import matplotlib
798 799 matplotlib.use('agg')
799 800
800 801 # Always import pyplot into embedded shell.
801 802 self.process_input_line('import matplotlib.pyplot as plt',
802 803 store_history=False)
803 804 self._pyplot_imported = True
804 805
805 806 def process_pure_python(self, content):
806 807 """
807 808 content is a list of strings. it is unedited directive content
808 809
809 810 This runs it line by line in the InteractiveShell, prepends
810 811 prompts as needed capturing stderr and stdout, then returns
811 812 the content as a list as if it were ipython code
812 813 """
813 814 output = []
814 815 savefig = False # keep up with this to clear figure
815 816 multiline = False # to handle line continuation
816 817 multiline_start = None
817 818 fmtin = self.promptin
818 819
819 820 ct = 0
820 821
821 822 for lineno, line in enumerate(content):
822 823
823 824 line_stripped = line.strip()
824 825 if not len(line):
825 826 output.append(line)
826 827 continue
827 828
828 829 # handle pseudo-decorators, whilst ensuring real python decorators are treated as input
829 830 if any(
830 831 line_stripped.startswith("@" + pseudo_decorator)
831 832 for pseudo_decorator in PSEUDO_DECORATORS
832 833 ):
833 834 output.extend([line])
834 835 if 'savefig' in line:
835 836 savefig = True # and need to clear figure
836 837 continue
837 838
838 839 # handle comments
839 840 if line_stripped.startswith('#'):
840 841 output.extend([line])
841 842 continue
842 843
843 844 # deal with lines checking for multiline
844 845 continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
845 846 if not multiline:
846 847 modified = u"%s %s" % (fmtin % ct, line_stripped)
847 848 output.append(modified)
848 849 ct += 1
849 850 try:
850 851 ast.parse(line_stripped)
851 852 output.append(u'')
852 853 except Exception: # on a multiline
853 854 multiline = True
854 855 multiline_start = lineno
855 856 else: # still on a multiline
856 857 modified = u'%s %s' % (continuation, line)
857 858 output.append(modified)
858 859
859 860 # if the next line is indented, it should be part of multiline
860 861 if len(content) > lineno + 1:
861 862 nextline = content[lineno + 1]
862 863 if len(nextline) - len(nextline.lstrip()) > 3:
863 864 continue
864 865 try:
865 866 mod = ast.parse(
866 867 '\n'.join(content[multiline_start:lineno+1]))
867 868 if isinstance(mod.body[0], ast.FunctionDef):
868 869 # check to see if we have the whole function
869 870 for element in mod.body[0].body:
870 871 if isinstance(element, ast.Return):
871 872 multiline = False
872 873 else:
873 874 output.append(u'')
874 875 multiline = False
875 876 except Exception:
876 877 pass
877 878
878 879 if savefig: # clear figure if plotted
879 880 self.ensure_pyplot()
880 881 self.process_input_line('plt.clf()', store_history=False)
881 882 self.clear_cout()
882 883 savefig = False
883 884
884 885 return output
885 886
886 887 def custom_doctest(self, decorator, input_lines, found, submitted):
887 888 """
888 889 Perform a specialized doctest.
889 890
890 891 """
891 892 from .custom_doctests import doctests
892 893
893 894 args = decorator.split()
894 895 doctest_type = args[1]
895 896 if doctest_type in doctests:
896 897 doctests[doctest_type](self, args, input_lines, found, submitted)
897 898 else:
898 899 e = "Invalid option to @doctest: {0}".format(doctest_type)
899 900 raise Exception(e)
900 901
901 902
902 903 class IPythonDirective(Directive):
903 904
904 has_content = True
905 required_arguments = 0
906 optional_arguments = 4 # python, suppress, verbatim, doctest
907 final_argumuent_whitespace = True
908 option_spec = { 'python': directives.unchanged,
909 'suppress' : directives.flag,
910 'verbatim' : directives.flag,
911 'doctest' : directives.flag,
912 'okexcept': directives.flag,
913 'okwarning': directives.flag
914 }
905 has_content: bool = True
906 required_arguments: int = 0
907 optional_arguments: int = 4 # python, suppress, verbatim, doctest
908 final_argumuent_whitespace: bool = True
909 option_spec: Dict[str, Any] = {
910 "python": directives.unchanged,
911 "suppress": directives.flag,
912 "verbatim": directives.flag,
913 "doctest": directives.flag,
914 "okexcept": directives.flag,
915 "okwarning": directives.flag,
916 }
915 917
916 918 shell = None
917 919
918 seen_docs = set()
920 seen_docs: Set = set()
919 921
920 922 def get_config_options(self):
921 923 # contains sphinx configuration variables
922 924 config = self.state.document.settings.env.config
923 925
924 926 # get config variables to set figure output directory
925 927 savefig_dir = config.ipython_savefig_dir
926 928 source_dir = self.state.document.settings.env.srcdir
927 929 savefig_dir = os.path.join(source_dir, savefig_dir)
928 930
929 931 # get regex and prompt stuff
930 932 rgxin = config.ipython_rgxin
931 933 rgxout = config.ipython_rgxout
932 934 warning_is_error= config.ipython_warning_is_error
933 935 promptin = config.ipython_promptin
934 936 promptout = config.ipython_promptout
935 937 mplbackend = config.ipython_mplbackend
936 938 exec_lines = config.ipython_execlines
937 939 hold_count = config.ipython_holdcount
938 940
939 941 return (savefig_dir, source_dir, rgxin, rgxout,
940 942 promptin, promptout, mplbackend, exec_lines, hold_count, warning_is_error)
941 943
942 944 def setup(self):
943 945 # Get configuration values.
944 946 (savefig_dir, source_dir, rgxin, rgxout, promptin, promptout,
945 947 mplbackend, exec_lines, hold_count, warning_is_error) = self.get_config_options()
946 948
947 949 try:
948 950 os.makedirs(savefig_dir)
949 951 except OSError as e:
950 952 if e.errno != errno.EEXIST:
951 953 raise
952 954
953 955 if self.shell is None:
954 956 # We will be here many times. However, when the
955 957 # EmbeddedSphinxShell is created, its interactive shell member
956 958 # is the same for each instance.
957 959
958 960 if mplbackend and 'matplotlib.backends' not in sys.modules and use_matplotlib:
959 961 import matplotlib
960 962 matplotlib.use(mplbackend)
961 963
962 964 # Must be called after (potentially) importing matplotlib and
963 965 # setting its backend since exec_lines might import pylab.
964 966 self.shell = EmbeddedSphinxShell(exec_lines)
965 967
966 968 # Store IPython directive to enable better error messages
967 969 self.shell.directive = self
968 970
969 971 # reset the execution count if we haven't processed this doc
970 972 #NOTE: this may be borked if there are multiple seen_doc tmp files
971 973 #check time stamp?
972 974 if not self.state.document.current_source in self.seen_docs:
973 975 self.shell.IP.history_manager.reset()
974 976 self.shell.IP.execution_count = 1
975 977 self.seen_docs.add(self.state.document.current_source)
976 978
977 979 # and attach to shell so we don't have to pass them around
978 980 self.shell.rgxin = rgxin
979 981 self.shell.rgxout = rgxout
980 982 self.shell.promptin = promptin
981 983 self.shell.promptout = promptout
982 984 self.shell.savefig_dir = savefig_dir
983 985 self.shell.source_dir = source_dir
984 986 self.shell.hold_count = hold_count
985 987 self.shell.warning_is_error = warning_is_error
986 988
987 989 # setup bookmark for saving figures directory
988 990 self.shell.process_input_line(
989 991 'bookmark ipy_savedir "%s"' % savefig_dir, store_history=False
990 992 )
991 993 self.shell.clear_cout()
992 994
993 995 return rgxin, rgxout, promptin, promptout
994 996
995 997 def teardown(self):
996 998 # delete last bookmark
997 999 self.shell.process_input_line('bookmark -d ipy_savedir',
998 1000 store_history=False)
999 1001 self.shell.clear_cout()
1000 1002
1001 1003 def run(self):
1002 1004 debug = False
1003 1005
1004 1006 #TODO, any reason block_parser can't be a method of embeddable shell
1005 1007 # then we wouldn't have to carry these around
1006 1008 rgxin, rgxout, promptin, promptout = self.setup()
1007 1009
1008 1010 options = self.options
1009 1011 self.shell.is_suppress = 'suppress' in options
1010 1012 self.shell.is_doctest = 'doctest' in options
1011 1013 self.shell.is_verbatim = 'verbatim' in options
1012 1014 self.shell.is_okexcept = 'okexcept' in options
1013 1015 self.shell.is_okwarning = 'okwarning' in options
1014 1016
1015 1017 # handle pure python code
1016 1018 if 'python' in self.arguments:
1017 1019 content = self.content
1018 1020 self.content = self.shell.process_pure_python(content)
1019 1021
1020 1022 # parts consists of all text within the ipython-block.
1021 1023 # Each part is an input/output block.
1022 1024 parts = '\n'.join(self.content).split('\n\n')
1023 1025
1024 1026 lines = ['.. code-block:: ipython', '']
1025 1027 figures = []
1026 1028
1027 1029 # Use sphinx logger for warnings
1028 1030 logger = logging.getLogger(__name__)
1029 1031
1030 1032 for part in parts:
1031 1033 block = block_parser(part, rgxin, rgxout, promptin, promptout)
1032 1034 if len(block):
1033 1035 rows, figure = self.shell.process_block(block)
1034 1036 for row in rows:
1035 1037 lines.extend([' {0}'.format(line)
1036 1038 for line in row.split('\n')])
1037 1039
1038 1040 if figure is not None:
1039 1041 figures.append(figure)
1040 1042 else:
1041 1043 message = 'Code input with no code at {}, line {}'\
1042 1044 .format(
1043 1045 self.state.document.current_source,
1044 1046 self.state.document.current_line)
1045 1047 if self.shell.warning_is_error:
1046 1048 raise RuntimeError(message)
1047 1049 else:
1048 1050 logger.warning(message)
1049 1051
1050 1052 for figure in figures:
1051 1053 lines.append('')
1052 1054 lines.extend(figure.split('\n'))
1053 1055 lines.append('')
1054 1056
1055 1057 if len(lines) > 2:
1056 1058 if debug:
1057 1059 print('\n'.join(lines))
1058 1060 else:
1059 1061 # This has to do with input, not output. But if we comment
1060 1062 # these lines out, then no IPython code will appear in the
1061 1063 # final output.
1062 1064 self.state_machine.insert_input(
1063 1065 lines, self.state_machine.input_lines.source(0))
1064 1066
1065 1067 # cleanup
1066 1068 self.teardown()
1067 1069
1068 1070 return []
1069 1071
1070 1072 # Enable as a proper Sphinx directive
1071 1073 def setup(app):
1072 1074 setup.app = app
1073 1075
1074 1076 app.add_directive('ipython', IPythonDirective)
1075 1077 app.add_config_value('ipython_savefig_dir', 'savefig', 'env')
1076 1078 app.add_config_value('ipython_warning_is_error', True, 'env')
1077 1079 app.add_config_value('ipython_rgxin',
1078 1080 re.compile(r'In \[(\d+)\]:\s?(.*)\s*'), 'env')
1079 1081 app.add_config_value('ipython_rgxout',
1080 1082 re.compile(r'Out\[(\d+)\]:\s?(.*)\s*'), 'env')
1081 1083 app.add_config_value('ipython_promptin', 'In [%d]:', 'env')
1082 1084 app.add_config_value('ipython_promptout', 'Out[%d]:', 'env')
1083 1085
1084 1086 # We could just let matplotlib pick whatever is specified as the default
1085 1087 # backend in the matplotlibrc file, but this would cause issues if the
1086 1088 # backend didn't work in headless environments. For this reason, 'agg'
1087 1089 # is a good default backend choice.
1088 1090 app.add_config_value('ipython_mplbackend', 'agg', 'env')
1089 1091
1090 1092 # If the user sets this config value to `None`, then EmbeddedSphinxShell's
1091 1093 # __init__ method will treat it as [].
1092 1094 execlines = ['import numpy as np']
1093 1095 if use_matplotlib:
1094 1096 execlines.append('import matplotlib.pyplot as plt')
1095 1097 app.add_config_value('ipython_execlines', execlines, 'env')
1096 1098
1097 1099 app.add_config_value('ipython_holdcount', True, 'env')
1098 1100
1099 1101 metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
1100 1102 return metadata
1101 1103
1102 1104 # Simple smoke test, needs to be converted to a proper automatic test.
1103 1105 def test():
1104 1106
1105 1107 examples = [
1106 1108 r"""
1107 1109 In [9]: pwd
1108 1110 Out[9]: '/home/jdhunter/py4science/book'
1109 1111
1110 1112 In [10]: cd bookdata/
1111 1113 /home/jdhunter/py4science/book/bookdata
1112 1114
1113 1115 In [2]: from pylab import *
1114 1116
1115 1117 In [2]: ion()
1116 1118
1117 1119 In [3]: im = imread('stinkbug.png')
1118 1120
1119 1121 @savefig mystinkbug.png width=4in
1120 1122 In [4]: imshow(im)
1121 1123 Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
1122 1124
1123 1125 """,
1124 1126 r"""
1125 1127
1126 1128 In [1]: x = 'hello world'
1127 1129
1128 1130 # string methods can be
1129 1131 # used to alter the string
1130 1132 @doctest
1131 1133 In [2]: x.upper()
1132 1134 Out[2]: 'HELLO WORLD'
1133 1135
1134 1136 @verbatim
1135 1137 In [3]: x.st<TAB>
1136 1138 x.startswith x.strip
1137 1139 """,
1138 1140 r"""
1139 1141
1140 1142 In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
1141 1143 .....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
1142 1144
1143 1145 In [131]: print url.split('&')
1144 1146 ['http://ichart.finance.yahoo.com/table.csv?s=CROX', 'd=9', 'e=22', 'f=2009', 'g=d', 'a=1', 'b=8', 'c=2006', 'ignore=.csv']
1145 1147
1146 1148 In [60]: import urllib
1147 1149
1148 1150 """,
1149 1151 r"""\
1150 1152
1151 1153 In [133]: import numpy.random
1152 1154
1153 1155 @suppress
1154 1156 In [134]: numpy.random.seed(2358)
1155 1157
1156 1158 @doctest
1157 1159 In [135]: numpy.random.rand(10,2)
1158 1160 Out[135]:
1159 1161 array([[ 0.64524308, 0.59943846],
1160 1162 [ 0.47102322, 0.8715456 ],
1161 1163 [ 0.29370834, 0.74776844],
1162 1164 [ 0.99539577, 0.1313423 ],
1163 1165 [ 0.16250302, 0.21103583],
1164 1166 [ 0.81626524, 0.1312433 ],
1165 1167 [ 0.67338089, 0.72302393],
1166 1168 [ 0.7566368 , 0.07033696],
1167 1169 [ 0.22591016, 0.77731835],
1168 1170 [ 0.0072729 , 0.34273127]])
1169 1171
1170 1172 """,
1171 1173
1172 1174 r"""
1173 1175 In [106]: print x
1174 1176 jdh
1175 1177
1176 1178 In [109]: for i in range(10):
1177 1179 .....: print i
1178 1180 .....:
1179 1181 .....:
1180 1182 0
1181 1183 1
1182 1184 2
1183 1185 3
1184 1186 4
1185 1187 5
1186 1188 6
1187 1189 7
1188 1190 8
1189 1191 9
1190 1192 """,
1191 1193
1192 1194 r"""
1193 1195
1194 1196 In [144]: from pylab import *
1195 1197
1196 1198 In [145]: ion()
1197 1199
1198 1200 # use a semicolon to suppress the output
1199 1201 @savefig test_hist.png width=4in
1200 1202 In [151]: hist(np.random.randn(10000), 100);
1201 1203
1202 1204
1203 1205 @savefig test_plot.png width=4in
1204 1206 In [151]: plot(np.random.randn(10000), 'o');
1205 1207 """,
1206 1208
1207 1209 r"""
1208 1210 # use a semicolon to suppress the output
1209 1211 In [151]: plt.clf()
1210 1212
1211 1213 @savefig plot_simple.png width=4in
1212 1214 In [151]: plot([1,2,3])
1213 1215
1214 1216 @savefig hist_simple.png width=4in
1215 1217 In [151]: hist(np.random.randn(10000), 100);
1216 1218
1217 1219 """,
1218 1220 r"""
1219 1221 # update the current fig
1220 1222 In [151]: ylabel('number')
1221 1223
1222 1224 In [152]: title('normal distribution')
1223 1225
1224 1226
1225 1227 @savefig hist_with_text.png
1226 1228 In [153]: grid(True)
1227 1229
1228 1230 @doctest float
1229 1231 In [154]: 0.1 + 0.2
1230 1232 Out[154]: 0.3
1231 1233
1232 1234 @doctest float
1233 1235 In [155]: np.arange(16).reshape(4,4)
1234 1236 Out[155]:
1235 1237 array([[ 0, 1, 2, 3],
1236 1238 [ 4, 5, 6, 7],
1237 1239 [ 8, 9, 10, 11],
1238 1240 [12, 13, 14, 15]])
1239 1241
1240 1242 In [1]: x = np.arange(16, dtype=float).reshape(4,4)
1241 1243
1242 1244 In [2]: x[0,0] = np.inf
1243 1245
1244 1246 In [3]: x[0,1] = np.nan
1245 1247
1246 1248 @doctest float
1247 1249 In [4]: x
1248 1250 Out[4]:
1249 1251 array([[ inf, nan, 2., 3.],
1250 1252 [ 4., 5., 6., 7.],
1251 1253 [ 8., 9., 10., 11.],
1252 1254 [ 12., 13., 14., 15.]])
1253 1255
1254 1256
1255 1257 """,
1256 1258 ]
1257 1259 # skip local-file depending first example:
1258 1260 examples = examples[1:]
1259 1261
1260 1262 #ipython_directive.DEBUG = True # dbg
1261 1263 #options = dict(suppress=True) # dbg
1262 1264 options = {}
1263 1265 for example in examples:
1264 1266 content = example.split('\n')
1265 1267 IPythonDirective('debug', arguments=None, options=options,
1266 1268 content=content, lineno=0,
1267 1269 content_offset=None, block_text=None,
1268 1270 state=None, state_machine=None,
1269 1271 )
1270 1272
1271 1273 # Run test suite as a script
1272 1274 if __name__=='__main__':
1273 1275 if not os.path.isdir('_static'):
1274 1276 os.mkdir('_static')
1275 1277 test()
1276 1278 print('All OK? Check figures in _static/')
@@ -1,104 +1,105
1 1 """
2 2 Utilities function for keybinding with prompt toolkit.
3 3
4 4 This will be bound to specific key press and filter modes,
5 5 like whether we are in edit mode, and whether the completer is open.
6 6 """
7
7 8 import re
8 9 from prompt_toolkit.key_binding import KeyPressEvent
9 10
10 11
11 12 def parenthesis(event: KeyPressEvent):
12 13 """Auto-close parenthesis"""
13 14 event.current_buffer.insert_text("()")
14 15 event.current_buffer.cursor_left()
15 16
16 17
17 18 def brackets(event: KeyPressEvent):
18 19 """Auto-close brackets"""
19 20 event.current_buffer.insert_text("[]")
20 21 event.current_buffer.cursor_left()
21 22
22 23
23 24 def braces(event: KeyPressEvent):
24 25 """Auto-close braces"""
25 26 event.current_buffer.insert_text("{}")
26 27 event.current_buffer.cursor_left()
27 28
28 29
29 30 def double_quote(event: KeyPressEvent):
30 31 """Auto-close double quotes"""
31 32 event.current_buffer.insert_text('""')
32 33 event.current_buffer.cursor_left()
33 34
34 35
35 36 def single_quote(event: KeyPressEvent):
36 37 """Auto-close single quotes"""
37 38 event.current_buffer.insert_text("''")
38 39 event.current_buffer.cursor_left()
39 40
40 41
41 42 def docstring_double_quotes(event: KeyPressEvent):
42 43 """Auto-close docstring (double quotes)"""
43 44 event.current_buffer.insert_text('""""')
44 45 event.current_buffer.cursor_left(3)
45 46
46 47
47 48 def docstring_single_quotes(event: KeyPressEvent):
48 49 """Auto-close docstring (single quotes)"""
49 50 event.current_buffer.insert_text("''''")
50 51 event.current_buffer.cursor_left(3)
51 52
52 53
53 54 def raw_string_parenthesis(event: KeyPressEvent):
54 55 """Auto-close parenthesis in raw strings"""
55 56 matches = re.match(
56 57 r".*(r|R)[\"'](-*)",
57 58 event.current_buffer.document.current_line_before_cursor,
58 59 )
59 60 dashes = matches.group(2) if matches else ""
60 61 event.current_buffer.insert_text("()" + dashes)
61 62 event.current_buffer.cursor_left(len(dashes) + 1)
62 63
63 64
64 65 def raw_string_bracket(event: KeyPressEvent):
65 66 """Auto-close bracker in raw strings"""
66 67 matches = re.match(
67 68 r".*(r|R)[\"'](-*)",
68 69 event.current_buffer.document.current_line_before_cursor,
69 70 )
70 71 dashes = matches.group(2) if matches else ""
71 72 event.current_buffer.insert_text("[]" + dashes)
72 73 event.current_buffer.cursor_left(len(dashes) + 1)
73 74
74 75
75 76 def raw_string_braces(event: KeyPressEvent):
76 77 """Auto-close braces in raw strings"""
77 78 matches = re.match(
78 79 r".*(r|R)[\"'](-*)",
79 80 event.current_buffer.document.current_line_before_cursor,
80 81 )
81 82 dashes = matches.group(2) if matches else ""
82 83 event.current_buffer.insert_text("{}" + dashes)
83 84 event.current_buffer.cursor_left(len(dashes) + 1)
84 85
85 86
86 87 def skip_over(event: KeyPressEvent):
87 88 """Skip over automatically added parenthesis/quote.
88 89
89 90 (rather than adding another parenthesis/quote)"""
90 91 event.current_buffer.cursor_right()
91 92
92 93
93 94 def delete_pair(event: KeyPressEvent):
94 95 """Delete auto-closed parenthesis"""
95 96 event.current_buffer.delete()
96 97 event.current_buffer.delete_before_cursor()
97 98
98 99
99 100 auto_match_parens = {"(": parenthesis, "[": brackets, "{": braces}
100 101 auto_match_parens_raw_string = {
101 102 "(": raw_string_parenthesis,
102 103 "[": raw_string_bracket,
103 104 "{": raw_string_braces,
104 105 }
General Comments 0
You need to be logged in to leave comments. Login now