inputsplitter.py
772 lines
| 27.5 KiB
| text/x-python
|
PythonLexer
Thomas Kluyver
|
r24177 | """DEPRECATED: Input handling and transformation machinery. | ||
This module was deprecated in IPython 7.0, in favour of inputtransformer2. | ||||
Fernando Perez
|
r2628 | |||
Thomas Kluyver
|
r13890 | The first class in this module, :class:`InputSplitter`, is designed to tell when | ||
input from a line-oriented frontend is complete and should be executed, and when | ||||
the user should be prompted for another line of code instead. The name 'input | ||||
splitter' is largely for historical reasons. | ||||
Fernando Perez
|
r2628 | |||
Fernando Perez
|
r2782 | A companion, :class:`IPythonInputSplitter`, provides the same functionality but | ||
with full support for the extended IPython syntax (magics, system calls, etc). | ||||
Thomas Kluyver
|
r13890 | The code to actually do these transformations is in :mod:`IPython.core.inputtransformer`. | ||
:class:`IPythonInputSplitter` feeds the raw code to the transformers in order | ||||
and stores the results. | ||||
Fernando Perez
|
r2782 | |||
Thomas Kluyver
|
r13890 | For more details, see the class docstrings below. | ||
Fernando Perez
|
r2628 | """ | ||
Matthias Bussonnier
|
r24406 | from warnings import warn | ||
warn('IPython.core.inputsplitter is deprecated since IPython 7 in favor of `IPython.core.inputtransformer2`', | ||||
DeprecationWarning) | ||||
MinRK
|
r17813 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
Thomas Kluyver
|
r3454 | import ast | ||
Fernando Perez
|
r2628 | import codeop | ||
Thomas Kluyver
|
r23331 | import io | ||
Fernando Perez
|
r2628 | import re | ||
import sys | ||||
Thomas Kluyver
|
r23331 | import tokenize | ||
Min RK
|
r18850 | import warnings | ||
Fernando Perez
|
r2628 | |||
Thomas Kluyver
|
r10093 | from IPython.core.inputtransformer import (leading_indent, | ||
classic_prompt, | ||||
ipy_prompt, | ||||
cellmagic, | ||||
Thomas Kluyver
|
r10105 | assemble_logical_lines, | ||
Thomas Kluyver
|
r10093 | help_end, | ||
Thomas Kluyver
|
r10107 | escaped_commands, | ||
Thomas Kluyver
|
r10093 | assign_from_magic, | ||
assign_from_system, | ||||
Thomas Kluyver
|
r10106 | assemble_python_lines, | ||
Thomas Kluyver
|
r10093 | ) | ||
Thomas Kluyver
|
r11124 | # These are available in this module for backwards compatibility. | ||
Thomas Kluyver
|
r10093 | from IPython.core.inputtransformer import (ESC_SHELL, ESC_SH_CAP, ESC_HELP, | ||
ESC_HELP2, ESC_MAGIC, ESC_MAGIC2, | ||||
ESC_QUOTE, ESC_QUOTE2, ESC_PAREN, ESC_SEQUENCES) | ||||
Matthias BUSSONNIER
|
r7554 | |||
Fernando Perez
|
r2780 | #----------------------------------------------------------------------------- | ||
Fernando Perez
|
r2628 | # Utilities | ||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r2780 | # FIXME: These are general-purpose utilities that later can be moved to the | ||
# general ward. Kept here for now because we're being very strict about test | ||||
# coverage with this code, and this lets us ensure that we keep 100% coverage | ||||
# while developing. | ||||
Fernando Perez
|
r2633 | |||
Fernando Perez
|
r2628 | # compiled regexps for autoindent management | ||
David Warde-Farley
|
r3704 | dedent_re = re.compile('|'.join([ | ||
r'^\s+raise(\s.*)?$', # raise statement (+ space + other stuff, maybe) | ||||
r'^\s+raise\([^\)]*\).*$', # wacky raise with immediate open paren | ||||
r'^\s+return(\s.*)?$', # normal return (+ space + other stuff, maybe) | ||||
r'^\s+return\([^\)]*\).*$', # wacky return with immediate open paren | ||||
Aaron Meurer
|
r7824 | r'^\s+pass\s*$', # pass (optionally followed by trailing spaces) | ||
r'^\s+break\s*$', # break (optionally followed by trailing spaces) | ||||
r'^\s+continue\s*$', # continue (optionally followed by trailing spaces) | ||||
David Warde-Farley
|
r3704 | ])) | ||
Fernando Perez
|
r2628 | ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)') | ||
Fernando Perez
|
r2979 | # regexp to match pure comment lines so we don't accidentally insert 'if 1:' | ||
# before pure comments | ||||
Matthias Bussonnier
|
r24452 | comment_line_re = re.compile(r'^\s*\#') | ||
Fernando Perez
|
r2979 | |||
Fernando Perez
|
r2628 | |||
def num_ini_spaces(s): | ||||
"""Return the number of initial spaces in a string. | ||||
Note that tabs are counted as a single space. For now, we do *not* support | ||||
mixing of tabs and spaces in the user's input. | ||||
Parameters | ||||
---------- | ||||
s : string | ||||
Fernando Perez
|
r2663 | |||
Returns | ||||
------- | ||||
n : int | ||||
Fernando Perez
|
r2628 | """ | ||
ini_spaces = ini_spaces_re.match(s) | ||||
if ini_spaces: | ||||
return ini_spaces.end() | ||||
else: | ||||
return 0 | ||||
Thomas Kluyver
|
r23331 | # Fake token types for partial_tokenize: | ||
INCOMPLETE_STRING = tokenize.N_TOKENS | ||||
IN_MULTILINE_STATEMENT = tokenize.N_TOKENS + 1 | ||||
# The 2 classes below have the same API as TokenInfo, but don't try to look up | ||||
# a token type name that they won't find. | ||||
class IncompleteString: | ||||
type = exact_type = INCOMPLETE_STRING | ||||
def __init__(self, s, start, end, line): | ||||
self.s = s | ||||
self.start = start | ||||
self.end = end | ||||
self.line = line | ||||
class InMultilineStatement: | ||||
type = exact_type = IN_MULTILINE_STATEMENT | ||||
def __init__(self, pos, line): | ||||
self.s = '' | ||||
self.start = self.end = pos | ||||
self.line = line | ||||
def partial_tokens(s): | ||||
"""Iterate over tokens from a possibly-incomplete string of code. | ||||
This adds two special token types: INCOMPLETE_STRING and | ||||
IN_MULTILINE_STATEMENT. These can only occur as the last token yielded, and | ||||
represent the two main ways for code to be incomplete. | ||||
""" | ||||
readline = io.StringIO(s).readline | ||||
token = tokenize.TokenInfo(tokenize.NEWLINE, '', (1, 0), (1, 0), '') | ||||
try: | ||||
for token in tokenize.generate_tokens(readline): | ||||
yield token | ||||
except tokenize.TokenError as e: | ||||
# catch EOF error | ||||
lines = s.splitlines(keepends=True) | ||||
end = len(lines), len(lines[-1]) | ||||
if 'multi-line string' in e.args[0]: | ||||
l, c = start = token.end | ||||
s = lines[l-1][c:] + ''.join(lines[l:]) | ||||
yield IncompleteString(s, start, end, lines[-1]) | ||||
elif 'multi-line statement' in e.args[0]: | ||||
yield InMultilineStatement(end, lines[-1]) | ||||
else: | ||||
raise | ||||
def find_next_indent(code): | ||||
"""Find the number of spaces for the next line of indentation""" | ||||
tokens = list(partial_tokens(code)) | ||||
if tokens[-1].type == tokenize.ENDMARKER: | ||||
tokens.pop() | ||||
if not tokens: | ||||
return 0 | ||||
Matthias Bussonnier
|
r23344 | while (tokens[-1].type in {tokenize.DEDENT, tokenize.NEWLINE, tokenize.COMMENT}): | ||
Thomas Kluyver
|
r23331 | tokens.pop() | ||
if tokens[-1].type == INCOMPLETE_STRING: | ||||
# Inside a multiline string | ||||
return 0 | ||||
# Find the indents used before | ||||
prev_indents = [0] | ||||
def _add_indent(n): | ||||
if n != prev_indents[-1]: | ||||
prev_indents.append(n) | ||||
tokiter = iter(tokens) | ||||
for tok in tokiter: | ||||
if tok.type in {tokenize.INDENT, tokenize.DEDENT}: | ||||
_add_indent(tok.end[1]) | ||||
elif (tok.type == tokenize.NL): | ||||
try: | ||||
_add_indent(next(tokiter).start[1]) | ||||
except StopIteration: | ||||
break | ||||
last_indent = prev_indents.pop() | ||||
Thomas Kluyver
|
r23333 | # If we've just opened a multiline statement (e.g. 'a = ['), indent more | ||
Thomas Kluyver
|
r23331 | if tokens[-1].type == IN_MULTILINE_STATEMENT: | ||
if tokens[-2].exact_type in {tokenize.LPAR, tokenize.LSQB, tokenize.LBRACE}: | ||||
return last_indent + 4 | ||||
return last_indent | ||||
if tokens[-1].exact_type == tokenize.COLON: | ||||
# Line ends with colon - indent | ||||
return last_indent + 4 | ||||
if last_indent: | ||||
# Examine the last line for dedent cues - statements like return or | ||||
# raise which normally end a block of code. | ||||
last_line_starts = 0 | ||||
for i, tok in enumerate(tokens): | ||||
if tok.type == tokenize.NEWLINE: | ||||
last_line_starts = i + 1 | ||||
last_line_tokens = tokens[last_line_starts:] | ||||
names = [t.string for t in last_line_tokens if t.type == tokenize.NAME] | ||||
if names and names[0] in {'raise', 'return', 'pass', 'break', 'continue'}: | ||||
# Find the most recent indentation less than the current level | ||||
for indent in reversed(prev_indents): | ||||
if indent < last_indent: | ||||
return indent | ||||
return last_indent | ||||
Fernando Perez
|
r6978 | def last_blank(src): | ||
"""Determine if the input source ends in a blank. | ||||
A blank is either a newline or a line consisting of whitespace. | ||||
Parameters | ||||
---------- | ||||
src : string | ||||
A single or multiline string. | ||||
""" | ||||
Fernando Perez
|
r6981 | if not src: return False | ||
ll = src.splitlines()[-1] | ||||
return (ll == '') or ll.isspace() | ||||
Fernando Perez
|
r6979 | |||
Fernando Perez
|
r6981 | last_two_blanks_re = re.compile(r'\n\s*\n\s*$', re.MULTILINE) | ||
last_two_blanks_re2 = re.compile(r'.+\n\s*\n\s+$', re.MULTILINE) | ||||
Fernando Perez
|
r6979 | |||
def last_two_blanks(src): | ||||
"""Determine if the input source ends in two blanks. | ||||
A blank is either a newline or a line consisting of whitespace. | ||||
Parameters | ||||
---------- | ||||
src : string | ||||
A single or multiline string. | ||||
""" | ||||
Fernando Perez
|
r6981 | if not src: return False | ||
# The logic here is tricky: I couldn't get a regexp to work and pass all | ||||
# the tests, so I took a different approach: split the source by lines, | ||||
# grab the last two and prepend '###\n' as a stand-in for whatever was in | ||||
# the body before the last two lines. Then, with that structure, it's | ||||
# possible to analyze with two regexps. Not the most elegant solution, but | ||||
# it works. If anyone tries to change this logic, make sure to validate | ||||
# the whole test suite first! | ||||
new_src = '\n'.join(['###\n'] + src.splitlines()[-2:]) | ||||
return (bool(last_two_blanks_re.match(new_src)) or | ||||
bool(last_two_blanks_re2.match(new_src)) ) | ||||
Fernando Perez
|
r6978 | |||
Fernando Perez
|
r2628 | def remove_comments(src): | ||
"""Remove all comments from input source. | ||||
Note: comments are NOT recognized inside of strings! | ||||
Parameters | ||||
---------- | ||||
src : string | ||||
A single or multiline input string. | ||||
Returns | ||||
------- | ||||
String with all Python comments removed. | ||||
""" | ||||
return re.sub('#.*', '', src) | ||||
Aaron Meurer
|
r7823 | |||
Fernando Perez
|
r2628 | |||
def get_input_encoding(): | ||||
Fernando Perez
|
r2718 | """Return the default standard input encoding. | ||
If sys.stdin has no encoding, 'ascii' is returned.""" | ||||
epatters
|
r2674 | # There are strange environments for which sys.stdin.encoding is None. We | ||
# ensure that a valid encoding is returned. | ||||
encoding = getattr(sys.stdin, 'encoding', None) | ||||
if encoding is None: | ||||
encoding = 'ascii' | ||||
return encoding | ||||
Fernando Perez
|
r2628 | |||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r2780 | # Classes and functions for normal Python syntax handling | ||
Fernando Perez
|
r2628 | #----------------------------------------------------------------------------- | ||
Fernando Perez
|
r2663 | class InputSplitter(object): | ||
Thomas Kluyver
|
r13597 | r"""An object that can accumulate lines of Python source before execution. | ||
Fernando Perez
|
r2663 | |||
Thomas Kluyver
|
r4077 | This object is designed to be fed python source line-by-line, using | ||
Thomas Kluyver
|
r13597 | :meth:`push`. It will return on each push whether the currently pushed | ||
code could be executed already. In addition, it provides a method called | ||||
:meth:`push_accepts_more` that can be used to query whether more input | ||||
can be pushed into a single interactive block. | ||||
Fernando Perez
|
r2663 | |||
This is a simple example of how an interactive terminal-based client can use | ||||
this tool:: | ||||
isp = InputSplitter() | ||||
while isp.push_accepts_more(): | ||||
indent = ' '*isp.indent_spaces | ||||
prompt = '>>> ' + indent | ||||
line = indent + raw_input(prompt) | ||||
isp.push(line) | ||||
print 'Input source was:\n', isp.source_reset(), | ||||
""" | ||||
Thomas Kluyver
|
r24050 | # A cache for storing the current indentation | ||
# The first value stores the most recently processed source input | ||||
# The second value is the number of spaces for the current indentation | ||||
# If self.source matches the first value, the second value is a valid | ||||
# current indentation. Otherwise, the cache is invalid and the indentation | ||||
# must be recalculated. | ||||
Thomas Kluyver
|
r24048 | _indent_spaces_cache = None, None | ||
Fernando Perez
|
r2663 | # String, indicating the default input encoding. It is computed by default | ||
# at initialization time via get_input_encoding(), but it can be reset by a | ||||
# client with specific knowledge of the encoding. | ||||
Fernando Perez
|
r2628 | encoding = '' | ||
Fernando Perez
|
r2663 | # String where the current full source input is stored, properly encoded. | ||
# Reading this attribute is the normal way of querying the currently pushed | ||||
# source code, that has been properly encoded. | ||||
Fernando Perez
|
r2628 | source = '' | ||
Fernando Perez
|
r2663 | # Code object corresponding to the current source. It is automatically | ||
# synced to the source, so it can be queried at any time to obtain the code | ||||
# object; it will be None if the source doesn't compile to valid Python. | ||||
Fernando Perez
|
r2628 | code = None | ||
Aaron Meurer
|
r7823 | |||
Fernando Perez
|
r2633 | # Private attributes | ||
Aaron Meurer
|
r7823 | |||
Fernando Perez
|
r2663 | # List with lines of input accumulated so far | ||
Fernando Perez
|
r2633 | _buffer = None | ||
Fernando Perez
|
r2663 | # Command compiler | ||
_compile = None | ||||
# Boolean indicating whether the current block is complete | ||||
_is_complete = None | ||||
Thomas Kluyver
|
r17804 | # Boolean indicating whether the current block has an unrecoverable syntax error | ||
_is_invalid = False | ||||
Aaron Meurer
|
r7823 | |||
Thomas Kluyver
|
r10251 | def __init__(self): | ||
Fernando Perez
|
r2663 | """Create a new InputSplitter instance. | ||
Fernando Perez
|
r2634 | """ | ||
Fernando Perez
|
r2633 | self._buffer = [] | ||
Fernando Perez
|
r2663 | self._compile = codeop.CommandCompiler() | ||
Fernando Perez
|
r2628 | self.encoding = get_input_encoding() | ||
def reset(self): | ||||
"""Reset the input buffer and associated state.""" | ||||
Fernando Perez
|
r2633 | self._buffer[:] = [] | ||
Fernando Perez
|
r2628 | self.source = '' | ||
Fernando Perez
|
r2633 | self.code = None | ||
Fernando Perez
|
r2663 | self._is_complete = False | ||
Thomas Kluyver
|
r17804 | self._is_invalid = False | ||
Fernando Perez
|
r2628 | |||
Fernando Perez
|
r2636 | def source_reset(self): | ||
"""Return the input source and perform a full reset. | ||||
Fernando Perez
|
r2628 | """ | ||
out = self.source | ||||
Fernando Perez
|
r2636 | self.reset() | ||
Fernando Perez
|
r2628 | return out | ||
Thomas Kluyver
|
r17804 | def check_complete(self, source): | ||
Thomas Kluyver
|
r17624 | """Return whether a block of code is ready to execute, or should be continued | ||
This is a non-stateful API, and will reset the state of this InputSplitter. | ||||
Thomas Kluyver
|
r17804 | |||
Parameters | ||||
---------- | ||||
source : string | ||||
Python input code, which can be multiline. | ||||
Returns | ||||
------- | ||||
status : str | ||||
One of 'complete', 'incomplete', or 'invalid' if source is not a | ||||
prefix of valid code. | ||||
indent_spaces : int or None | ||||
The number of spaces by which to indent the next line of code. If | ||||
status is not 'incomplete', this is None. | ||||
Thomas Kluyver
|
r17624 | """ | ||
self.reset() | ||||
try: | ||||
self.push(source) | ||||
except SyntaxError: | ||||
# Transformers in IPythonInputSplitter can raise SyntaxError, | ||||
# which push() will not catch. | ||||
Thomas Kluyver
|
r17804 | return 'invalid', None | ||
else: | ||||
if self._is_invalid: | ||||
return 'invalid', None | ||||
elif self.push_accepts_more(): | ||||
Thomas Kluyver
|
r24048 | return 'incomplete', self.get_indent_spaces() | ||
Thomas Kluyver
|
r17804 | else: | ||
return 'complete', None | ||||
Thomas Kluyver
|
r17624 | finally: | ||
self.reset() | ||||
Matthias Bussonnier
|
r25343 | def push(self, lines:str) -> bool: | ||
Robert Kern
|
r3293 | """Push one or more lines of input. | ||
Fernando Perez
|
r2628 | |||
This stores the given lines and returns a status code indicating | ||||
whether the code forms a complete Python block or not. | ||||
Fernando Perez
|
r2663 | Any exceptions generated in compilation are swallowed, but if an | ||
exception was produced, the method returns True. | ||||
Fernando Perez
|
r2628 | |||
Parameters | ||||
---------- | ||||
lines : string | ||||
One or more lines of Python input. | ||||
Aaron Meurer
|
r7823 | |||
Fernando Perez
|
r2628 | Returns | ||
------- | ||||
is_complete : boolean | ||||
True if the current input source (the result of the current input | ||||
Jason Grout
|
r7643 | plus prior inputs) forms a complete Python execution block. Note that | ||
this value is also stored as a private attribute (``_is_complete``), so it | ||||
can be queried at any time. | ||||
Fernando Perez
|
r2628 | """ | ||
Matthias Bussonnier
|
r25343 | assert isinstance(lines, str) | ||
Fernando Perez
|
r2633 | self._store(lines) | ||
Fernando Perez
|
r2628 | source = self.source | ||
Fernando Perez
|
r2663 | # Before calling _compile(), reset the code object to None so that if an | ||
Fernando Perez
|
r2628 | # exception is raised in compilation, we don't mislead by having | ||
# inconsistent code/source attributes. | ||||
Fernando Perez
|
r2663 | self.code, self._is_complete = None, None | ||
Thomas Kluyver
|
r17804 | self._is_invalid = False | ||
Fernando Perez
|
r2645 | |||
Fernando Perez
|
r3013 | # Honor termination lines properly | ||
Aaron Meurer
|
r7823 | if source.endswith('\\\n'): | ||
Fernando Perez
|
r3013 | return False | ||
Fernando Perez
|
r2635 | try: | ||
Min RK
|
r18850 | with warnings.catch_warnings(): | ||
warnings.simplefilter('error', SyntaxWarning) | ||||
self.code = self._compile(source, symbol="exec") | ||||
Fernando Perez
|
r2635 | # Invalid syntax can produce any of a number of different errors from | ||
# inside the compiler, so we have to catch them all. Syntax errors | ||||
# immediately produce a 'ready' block, so the invalid Python can be | ||||
# sent to the kernel for evaluation with possible ipython | ||||
# special-syntax conversion. | ||||
Fernando Perez
|
r2645 | except (SyntaxError, OverflowError, ValueError, TypeError, | ||
Min RK
|
r18850 | MemoryError, SyntaxWarning): | ||
Fernando Perez
|
r2663 | self._is_complete = True | ||
Thomas Kluyver
|
r17804 | self._is_invalid = True | ||
Fernando Perez
|
r2635 | else: | ||
# Compilation didn't produce any exceptions (though it may not have | ||||
# given a complete code object) | ||||
Fernando Perez
|
r2663 | self._is_complete = self.code is not None | ||
Fernando Perez
|
r2635 | |||
Fernando Perez
|
r2663 | return self._is_complete | ||
Fernando Perez
|
r2628 | |||
Fernando Perez
|
r2663 | def push_accepts_more(self): | ||
"""Return whether a block of interactive input can accept more input. | ||||
Fernando Perez
|
r2628 | |||
This method is meant to be used by line-oriented frontends, who need to | ||||
guess whether a block is complete or not based solely on prior and | ||||
Fernando Perez
|
r2663 | current input lines. The InputSplitter considers it has a complete | ||
Thomas Kluyver
|
r10255 | interactive block and will not accept more input when either: | ||
* A SyntaxError is raised | ||||
Aaron Meurer
|
r7823 | |||
Thomas Kluyver
|
r10255 | * The code is complete and consists of a single line or a single | ||
non-compound statement | ||||
Fernando Perez
|
r2628 | |||
Thomas Kluyver
|
r10255 | * The code is complete and has a blank line at the end | ||
Fernando Perez
|
r2628 | |||
Fernando Perez
|
r2663 | If the current input produces a syntax error, this method immediately | ||
returns False but does *not* raise the syntax error exception, as | ||||
typically clients will want to send invalid syntax to an execution | ||||
backend which might convert the invalid syntax into valid Python via | ||||
one of the dynamic IPython mechanisms. | ||||
Fernando Perez
|
r2628 | """ | ||
Fernando Perez
|
r3004 | |||
# With incomplete input, unconditionally accept more | ||||
Thomas Kluyver
|
r10251 | # A syntax error also sets _is_complete to True - see push() | ||
Fernando Perez
|
r2663 | if not self._is_complete: | ||
Thomas Kluyver
|
r10251 | #print("Not complete") # debug | ||
Fernando Perez
|
r2628 | return True | ||
Thomas Kluyver
|
r10251 | |||
# The user can make any (complete) input execute by leaving a blank line | ||||
last_line = self.source.splitlines()[-1] | ||||
if (not last_line) or last_line.isspace(): | ||||
#print("Blank line") # debug | ||||
return False | ||||
Thomas Kluyver
|
r10255 | # If there's just a single line or AST node, and we're flush left, as is | ||
# the case after a simple statement such as 'a=1', we want to execute it | ||||
Thomas Kluyver
|
r10251 | # straight away. | ||
Thomas Kluyver
|
r24048 | if self.get_indent_spaces() == 0: | ||
Thomas Kluyver
|
r10255 | if len(self.source.splitlines()) <= 1: | ||
return False | ||||
Thomas Kluyver
|
r10251 | try: | ||
code_ast = ast.parse(u''.join(self._buffer)) | ||||
except Exception: | ||||
#print("Can't parse AST") # debug | ||||
return False | ||||
Fernando Perez
|
r3004 | else: | ||
Thomas Kluyver
|
r10251 | if len(code_ast.body) == 1 and \ | ||
not hasattr(code_ast.body[0], 'body'): | ||||
#print("Simple statement") # debug | ||||
Fernando Perez
|
r3004 | return False | ||
Thomas Kluyver
|
r10251 | # General fallback - accept more code | ||
return True | ||||
Fernando Perez
|
r2628 | |||
Thomas Kluyver
|
r24048 | def get_indent_spaces(self): | ||
sourcefor, n = self._indent_spaces_cache | ||||
if sourcefor == self.source: | ||||
return n | ||||
Thomas Kluyver
|
r23331 | # self.source always has a trailing newline | ||
Thomas Kluyver
|
r24048 | n = find_next_indent(self.source[:-1]) | ||
self._indent_spaces_cache = (self.source, n) | ||||
return n | ||||
Fernando Perez
|
r2628 | |||
Thomas Kluyver
|
r24051 | # Backwards compatibility. I think all code that used .indent_spaces was | ||
# inside IPython, but we can leave this here until IPython 7 in case any | ||||
# other modules are using it. -TK, November 2017 | ||||
indent_spaces = property(get_indent_spaces) | ||||
Fernando Perez
|
r3080 | def _store(self, lines, buffer=None, store='source'): | ||
Fernando Perez
|
r2633 | """Store one or more lines of input. | ||
If input lines are not newline-terminated, a newline is automatically | ||||
appended.""" | ||||
Aaron Meurer
|
r7823 | |||
Fernando Perez
|
r3080 | if buffer is None: | ||
buffer = self._buffer | ||||
Aaron Meurer
|
r7823 | |||
Fernando Perez
|
r2633 | if lines.endswith('\n'): | ||
Fernando Perez
|
r3080 | buffer.append(lines) | ||
Fernando Perez
|
r2633 | else: | ||
Fernando Perez
|
r3080 | buffer.append(lines+'\n') | ||
setattr(self, store, self._set_source(buffer)) | ||||
Fernando Perez
|
r2645 | |||
Fernando Perez
|
r3080 | def _set_source(self, buffer): | ||
Thomas Kluyver
|
r3455 | return u''.join(buffer) | ||
Fernando Perez
|
r2719 | |||
class IPythonInputSplitter(InputSplitter): | ||||
"""An input splitter that recognizes all of IPython's special syntax.""" | ||||
Fernando Perez
|
r3080 | # String with raw, untransformed input. | ||
source_raw = '' | ||||
Thomas Kluyver
|
r10093 | |||
# Flag to track when a transformer has stored input that it hasn't given | ||||
# back yet. | ||||
transformer_accumulating = False | ||||
Thomas Kluyver
|
r10106 | |||
# Flag to track when assemble_python_lines has stored input that it hasn't | ||||
# given back yet. | ||||
within_python_line = False | ||||
Fernando Perez
|
r6976 | |||
Fernando Perez
|
r3080 | # Private attributes | ||
Fernando Perez
|
r6978 | |||
Fernando Perez
|
r3080 | # List with lines of raw input accumulated so far. | ||
_buffer_raw = None | ||||
Thomas Kluyver
|
r10493 | def __init__(self, line_input_checker=True, physical_line_transforms=None, | ||
Thomas Kluyver
|
r10106 | logical_line_transforms=None, python_line_transforms=None): | ||
Thomas Kluyver
|
r10251 | super(IPythonInputSplitter, self).__init__() | ||
Fernando Perez
|
r3080 | self._buffer_raw = [] | ||
Fernando Perez
|
r6978 | self._validate = True | ||
Thomas Kluyver
|
r10106 | |||
Thomas Kluyver
|
r10113 | if physical_line_transforms is not None: | ||
self.physical_line_transforms = physical_line_transforms | ||||
else: | ||||
MinRK
|
r11460 | self.physical_line_transforms = [ | ||
leading_indent(), | ||||
Thomas Kluyver
|
r10113 | classic_prompt(), | ||
ipy_prompt(), | ||||
MinRK
|
r11463 | cellmagic(end_on_blank_line=line_input_checker), | ||
Thomas Kluyver
|
r10113 | ] | ||
Thomas Kluyver
|
r10106 | |||
self.assemble_logical_lines = assemble_logical_lines() | ||||
Thomas Kluyver
|
r10113 | if logical_line_transforms is not None: | ||
self.logical_line_transforms = logical_line_transforms | ||||
else: | ||||
MinRK
|
r11460 | self.logical_line_transforms = [ | ||
Thomas Kluyver
|
r10113 | help_end(), | ||
escaped_commands(), | ||||
assign_from_magic(), | ||||
assign_from_system(), | ||||
] | ||||
Thomas Kluyver
|
r10106 | |||
self.assemble_python_lines = assemble_python_lines() | ||||
Thomas Kluyver
|
r10113 | if python_line_transforms is not None: | ||
self.python_line_transforms = python_line_transforms | ||||
else: | ||||
# We don't use any of these at present | ||||
self.python_line_transforms = [] | ||||
Thomas Kluyver
|
r10106 | |||
@property | ||||
def transforms(self): | ||||
"Quick access to all transformers." | ||||
return self.physical_line_transforms + \ | ||||
[self.assemble_logical_lines] + self.logical_line_transforms + \ | ||||
[self.assemble_python_lines] + self.python_line_transforms | ||||
@property | ||||
def transforms_in_use(self): | ||||
"""Transformers, excluding logical line transformers if we're in a | ||||
Python line.""" | ||||
Thomas Kluyver
|
r10112 | t = self.physical_line_transforms[:] | ||
Thomas Kluyver
|
r10106 | if not self.within_python_line: | ||
Thomas Kluyver
|
r10112 | t += [self.assemble_logical_lines] + self.logical_line_transforms | ||
Thomas Kluyver
|
r10106 | return t + [self.assemble_python_lines] + self.python_line_transforms | ||
Aaron Meurer
|
r7823 | |||
Fernando Perez
|
r3080 | def reset(self): | ||
"""Reset the input buffer and associated state.""" | ||||
Fernando Perez
|
r6976 | super(IPythonInputSplitter, self).reset() | ||
Fernando Perez
|
r3080 | self._buffer_raw[:] = [] | ||
self.source_raw = '' | ||||
Thomas Kluyver
|
r10093 | self.transformer_accumulating = False | ||
Thomas Kluyver
|
r10112 | self.within_python_line = False | ||
Thomas Kluyver
|
r13529 | |||
Thomas Kluyver
|
r10096 | for t in self.transforms: | ||
Thomas Kluyver
|
r13529 | try: | ||
t.reset() | ||||
Thomas Kluyver
|
r13912 | except SyntaxError: | ||
# Nothing that calls reset() expects to handle transformer | ||||
# errors | ||||
pass | ||||
Thomas Kluyver
|
r10096 | |||
def flush_transformers(self): | ||||
MinRK
|
r17814 | def _flush(transform, outs): | ||
MinRK
|
r17813 | """yield transformed lines | ||
always strings, never None | ||||
transform: the current transform | ||||
MinRK
|
r17814 | outs: an iterable of previously transformed inputs. | ||
MinRK
|
r17813 | Each may be multiline, which will be passed | ||
one line at a time to transform. | ||||
""" | ||||
MinRK
|
r17814 | for out in outs: | ||
MinRK
|
r17813 | for line in out.splitlines(): | ||
# push one line at a time | ||||
tmp = transform.push(line) | ||||
if tmp is not None: | ||||
yield tmp | ||||
MinRK
|
r17814 | |||
# reset the transform | ||||
tmp = transform.reset() | ||||
if tmp is not None: | ||||
yield tmp | ||||
Thomas Kluyver
|
r10106 | |||
MinRK
|
r17813 | out = [] | ||
Thomas Kluyver
|
r10106 | for t in self.transforms_in_use: | ||
out = _flush(t, out) | ||||
MinRK
|
r17813 | out = list(out) | ||
if out: | ||||
self._store('\n'.join(out)) | ||||
Fernando Perez
|
r3080 | |||
Thomas Kluyver
|
r13912 | def raw_reset(self): | ||
"""Return raw input only and perform a full reset. | ||||
Fernando Perez
|
r3080 | """ | ||
Thomas Kluyver
|
r13912 | out = self.source_raw | ||
Fernando Perez
|
r3080 | self.reset() | ||
Thomas Kluyver
|
r13912 | return out | ||
Thomas Kluyver
|
r10096 | |||
def source_reset(self): | ||||
Thomas Kluyver
|
r13912 | try: | ||
self.flush_transformers() | ||||
return self.source | ||||
finally: | ||||
self.reset() | ||||
Fernando Perez
|
r3080 | |||
Fernando Perez
|
r6978 | def push_accepts_more(self): | ||
Thomas Kluyver
|
r10093 | if self.transformer_accumulating: | ||
return True | ||||
Fernando Perez
|
r6978 | else: | ||
return super(IPythonInputSplitter, self).push_accepts_more() | ||||
Fernando Perez
|
r7487 | def transform_cell(self, cell): | ||
"""Process and translate a cell of input. | ||||
""" | ||||
self.reset() | ||||
Thomas Kluyver
|
r13912 | try: | ||
self.push(cell) | ||||
self.flush_transformers() | ||||
return self.source | ||||
finally: | ||||
self.reset() | ||||
Fernando Perez
|
r7487 | |||
Matthias Bussonnier
|
r25343 | def push(self, lines:str) -> bool: | ||
Fernando Perez
|
r2719 | """Push one or more lines of IPython input. | ||
Fernando Perez
|
r6976 | |||
This stores the given lines and returns a status code indicating | ||||
whether the code forms a complete Python block or not, after processing | ||||
all input lines for special IPython syntax. | ||||
Any exceptions generated in compilation are swallowed, but if an | ||||
exception was produced, the method returns True. | ||||
Parameters | ||||
---------- | ||||
lines : string | ||||
One or more lines of Python input. | ||||
Returns | ||||
------- | ||||
is_complete : boolean | ||||
True if the current input source (the result of the current input | ||||
Thomas Kluyver
|
r13890 | plus prior inputs) forms a complete Python execution block. Note that | ||
this value is also stored as a private attribute (_is_complete), so it | ||||
can be queried at any time. | ||||
Fernando Perez
|
r2719 | """ | ||
Matthias Bussonnier
|
r25343 | assert isinstance(lines, str) | ||
Fernando Perez
|
r3126 | # We must ensure all input is pure unicode | ||
Thomas Kluyver
|
r10094 | # ''.splitlines() --> [], but we need to push the empty line to transformers | ||
Fernando Perez
|
r2782 | lines_list = lines.splitlines() | ||
Thomas Kluyver
|
r10094 | if not lines_list: | ||
lines_list = [''] | ||||
Fernando Perez
|
r2782 | |||
Fernando Perez
|
r3080 | # Store raw source before applying any transformations to it. Note | ||
# that this must be done *after* the reset() call that would otherwise | ||||
# flush the buffer. | ||||
self._store(lines, self._buffer_raw, 'source_raw') | ||||
Aaron Meurer
|
r7823 | |||
Thomas Kluyver
|
r24049 | transformed_lines_list = [] | ||
Thomas Kluyver
|
r10251 | for line in lines_list: | ||
Thomas Kluyver
|
r24049 | transformed = self._transform_line(line) | ||
if transformed is not None: | ||||
transformed_lines_list.append(transformed) | ||||
Thomas Kluyver
|
r10251 | |||
Thomas Kluyver
|
r24049 | if transformed_lines_list: | ||
transformed_lines = '\n'.join(transformed_lines_list) | ||||
return super(IPythonInputSplitter, self).push(transformed_lines) | ||||
else: | ||||
# Got nothing back from transformers - they must be waiting for | ||||
# more input. | ||||
return False | ||||
def _transform_line(self, line): | ||||
"""Push a line of input code through the various transformers. | ||||
Thomas Kluyver
|
r10106 | |||
Thomas Kluyver
|
r24049 | Returns any output from the transformers, or None if a transformer | ||
is accumulating lines. | ||||
Sets self.transformer_accumulating as a side effect. | ||||
""" | ||||
Thomas Kluyver
|
r10106 | def _accumulating(dbg): | ||
#print(dbg) | ||||
self.transformer_accumulating = True | ||||
Thomas Kluyver
|
r24049 | return None | ||
Thomas Kluyver
|
r10106 | for transformer in self.physical_line_transforms: | ||
line = transformer.push(line) | ||||
if line is None: | ||||
return _accumulating(transformer) | ||||
Thomas Kluyver
|
r24049 | |||
Thomas Kluyver
|
r10106 | if not self.within_python_line: | ||
Thomas Kluyver
|
r10112 | line = self.assemble_logical_lines.push(line) | ||
if line is None: | ||||
Thomas Kluyver
|
r24049 | return _accumulating('acc logical line') | ||
Thomas Kluyver
|
r10106 | for transformer in self.logical_line_transforms: | ||
line = transformer.push(line) | ||||
if line is None: | ||||
return _accumulating(transformer) | ||||
Thomas Kluyver
|
r24049 | |||
Thomas Kluyver
|
r10106 | line = self.assemble_python_lines.push(line) | ||
if line is None: | ||||
self.within_python_line = True | ||||
return _accumulating('acc python line') | ||||
else: | ||||
self.within_python_line = False | ||||
Thomas Kluyver
|
r24049 | |||
Thomas Kluyver
|
r10106 | for transformer in self.python_line_transforms: | ||
Thomas Kluyver
|
r10105 | line = transformer.push(line) | ||
if line is None: | ||||
Thomas Kluyver
|
r10106 | return _accumulating(transformer) | ||
Thomas Kluyver
|
r10093 | |||
Thomas Kluyver
|
r10106 | #print("transformers clear") #debug | ||
Thomas Kluyver
|
r10093 | self.transformer_accumulating = False | ||
Thomas Kluyver
|
r24049 | return line | ||