##// END OF EJS Templates
declare `MaybeAsyncCompile.__call__()` as a method
Artur Svistunov -
Show More
@@ -1,798 +1,799 b''
1 """Input transformer machinery to support IPython special syntax.
1 """Input transformer machinery to support IPython special syntax.
2
2
3 This includes the machinery to recognise and transform ``%magic`` commands,
3 This includes the machinery to recognise and transform ``%magic`` commands,
4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
5
5
6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
7 deprecated in 7.0.
7 deprecated in 7.0.
8 """
8 """
9
9
10 # Copyright (c) IPython Development Team.
10 # Copyright (c) IPython Development Team.
11 # Distributed under the terms of the Modified BSD License.
11 # Distributed under the terms of the Modified BSD License.
12
12
13 import ast
13 import ast
14 import sys
14 import sys
15 from codeop import CommandCompiler, Compile
15 from codeop import CommandCompiler, Compile
16 import re
16 import re
17 import tokenize
17 import tokenize
18 from typing import List, Tuple, Optional, Any
18 from typing import List, Tuple, Optional, Any
19 import warnings
19 import warnings
20
20
21 _indent_re = re.compile(r'^[ \t]+')
21 _indent_re = re.compile(r'^[ \t]+')
22
22
23 def leading_empty_lines(lines):
23 def leading_empty_lines(lines):
24 """Remove leading empty lines
24 """Remove leading empty lines
25
25
26 If the leading lines are empty or contain only whitespace, they will be
26 If the leading lines are empty or contain only whitespace, they will be
27 removed.
27 removed.
28 """
28 """
29 if not lines:
29 if not lines:
30 return lines
30 return lines
31 for i, line in enumerate(lines):
31 for i, line in enumerate(lines):
32 if line and not line.isspace():
32 if line and not line.isspace():
33 return lines[i:]
33 return lines[i:]
34 return lines
34 return lines
35
35
36 def leading_indent(lines):
36 def leading_indent(lines):
37 """Remove leading indentation.
37 """Remove leading indentation.
38
38
39 If the first line starts with a spaces or tabs, the same whitespace will be
39 If the first line starts with a spaces or tabs, the same whitespace will be
40 removed from each following line in the cell.
40 removed from each following line in the cell.
41 """
41 """
42 if not lines:
42 if not lines:
43 return lines
43 return lines
44 m = _indent_re.match(lines[0])
44 m = _indent_re.match(lines[0])
45 if not m:
45 if not m:
46 return lines
46 return lines
47 space = m.group(0)
47 space = m.group(0)
48 n = len(space)
48 n = len(space)
49 return [l[n:] if l.startswith(space) else l
49 return [l[n:] if l.startswith(space) else l
50 for l in lines]
50 for l in lines]
51
51
52 class PromptStripper:
52 class PromptStripper:
53 """Remove matching input prompts from a block of input.
53 """Remove matching input prompts from a block of input.
54
54
55 Parameters
55 Parameters
56 ----------
56 ----------
57 prompt_re : regular expression
57 prompt_re : regular expression
58 A regular expression matching any input prompt (including continuation,
58 A regular expression matching any input prompt (including continuation,
59 e.g. ``...``)
59 e.g. ``...``)
60 initial_re : regular expression, optional
60 initial_re : regular expression, optional
61 A regular expression matching only the initial prompt, but not continuation.
61 A regular expression matching only the initial prompt, but not continuation.
62 If no initial expression is given, prompt_re will be used everywhere.
62 If no initial expression is given, prompt_re will be used everywhere.
63 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
63 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
64 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
64 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
65
65
66 Notes
66 Notes
67 -----
67 -----
68
68
69 If initial_re and prompt_re differ,
69 If initial_re and prompt_re differ,
70 only initial_re will be tested against the first line.
70 only initial_re will be tested against the first line.
71 If any prompt is found on the first two lines,
71 If any prompt is found on the first two lines,
72 prompts will be stripped from the rest of the block.
72 prompts will be stripped from the rest of the block.
73 """
73 """
74 def __init__(self, prompt_re, initial_re=None):
74 def __init__(self, prompt_re, initial_re=None):
75 self.prompt_re = prompt_re
75 self.prompt_re = prompt_re
76 self.initial_re = initial_re or prompt_re
76 self.initial_re = initial_re or prompt_re
77
77
78 def _strip(self, lines):
78 def _strip(self, lines):
79 return [self.prompt_re.sub('', l, count=1) for l in lines]
79 return [self.prompt_re.sub('', l, count=1) for l in lines]
80
80
81 def __call__(self, lines):
81 def __call__(self, lines):
82 if not lines:
82 if not lines:
83 return lines
83 return lines
84 if self.initial_re.match(lines[0]) or \
84 if self.initial_re.match(lines[0]) or \
85 (len(lines) > 1 and self.prompt_re.match(lines[1])):
85 (len(lines) > 1 and self.prompt_re.match(lines[1])):
86 return self._strip(lines)
86 return self._strip(lines)
87 return lines
87 return lines
88
88
89 classic_prompt = PromptStripper(
89 classic_prompt = PromptStripper(
90 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
90 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
91 initial_re=re.compile(r'^>>>( |$)')
91 initial_re=re.compile(r'^>>>( |$)')
92 )
92 )
93
93
94 ipython_prompt = PromptStripper(
94 ipython_prompt = PromptStripper(
95 re.compile(
95 re.compile(
96 r"""
96 r"""
97 ^( # Match from the beginning of a line, either:
97 ^( # Match from the beginning of a line, either:
98
98
99 # 1. First-line prompt:
99 # 1. First-line prompt:
100 ((\[nav\]|\[ins\])?\ )? # Vi editing mode prompt, if it's there
100 ((\[nav\]|\[ins\])?\ )? # Vi editing mode prompt, if it's there
101 In\ # The 'In' of the prompt, with a space
101 In\ # The 'In' of the prompt, with a space
102 \[\d+\]: # Command index, as displayed in the prompt
102 \[\d+\]: # Command index, as displayed in the prompt
103 \ # With a mandatory trailing space
103 \ # With a mandatory trailing space
104
104
105 | # ... or ...
105 | # ... or ...
106
106
107 # 2. The three dots of the multiline prompt
107 # 2. The three dots of the multiline prompt
108 \s* # All leading whitespace characters
108 \s* # All leading whitespace characters
109 \.{3,}: # The three (or more) dots
109 \.{3,}: # The three (or more) dots
110 \ ? # With an optional trailing space
110 \ ? # With an optional trailing space
111
111
112 )
112 )
113 """,
113 """,
114 re.VERBOSE,
114 re.VERBOSE,
115 )
115 )
116 )
116 )
117
117
118
118
119 def cell_magic(lines):
119 def cell_magic(lines):
120 if not lines or not lines[0].startswith('%%'):
120 if not lines or not lines[0].startswith('%%'):
121 return lines
121 return lines
122 if re.match(r'%%\w+\?', lines[0]):
122 if re.match(r'%%\w+\?', lines[0]):
123 # This case will be handled by help_end
123 # This case will be handled by help_end
124 return lines
124 return lines
125 magic_name, _, first_line = lines[0][2:].rstrip().partition(' ')
125 magic_name, _, first_line = lines[0][2:].rstrip().partition(' ')
126 body = ''.join(lines[1:])
126 body = ''.join(lines[1:])
127 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
127 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
128 % (magic_name, first_line, body)]
128 % (magic_name, first_line, body)]
129
129
130
130
131 def _find_assign_op(token_line) -> Optional[int]:
131 def _find_assign_op(token_line) -> Optional[int]:
132 """Get the index of the first assignment in the line ('=' not inside brackets)
132 """Get the index of the first assignment in the line ('=' not inside brackets)
133
133
134 Note: We don't try to support multiple special assignment (a = b = %foo)
134 Note: We don't try to support multiple special assignment (a = b = %foo)
135 """
135 """
136 paren_level = 0
136 paren_level = 0
137 for i, ti in enumerate(token_line):
137 for i, ti in enumerate(token_line):
138 s = ti.string
138 s = ti.string
139 if s == '=' and paren_level == 0:
139 if s == '=' and paren_level == 0:
140 return i
140 return i
141 if s in {'(','[','{'}:
141 if s in {'(','[','{'}:
142 paren_level += 1
142 paren_level += 1
143 elif s in {')', ']', '}'}:
143 elif s in {')', ']', '}'}:
144 if paren_level > 0:
144 if paren_level > 0:
145 paren_level -= 1
145 paren_level -= 1
146 return None
146 return None
147
147
148 def find_end_of_continued_line(lines, start_line: int):
148 def find_end_of_continued_line(lines, start_line: int):
149 """Find the last line of a line explicitly extended using backslashes.
149 """Find the last line of a line explicitly extended using backslashes.
150
150
151 Uses 0-indexed line numbers.
151 Uses 0-indexed line numbers.
152 """
152 """
153 end_line = start_line
153 end_line = start_line
154 while lines[end_line].endswith('\\\n'):
154 while lines[end_line].endswith('\\\n'):
155 end_line += 1
155 end_line += 1
156 if end_line >= len(lines):
156 if end_line >= len(lines):
157 break
157 break
158 return end_line
158 return end_line
159
159
160 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
160 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
161 r"""Assemble a single line from multiple continued line pieces
161 r"""Assemble a single line from multiple continued line pieces
162
162
163 Continued lines are lines ending in ``\``, and the line following the last
163 Continued lines are lines ending in ``\``, and the line following the last
164 ``\`` in the block.
164 ``\`` in the block.
165
165
166 For example, this code continues over multiple lines::
166 For example, this code continues over multiple lines::
167
167
168 if (assign_ix is not None) \
168 if (assign_ix is not None) \
169 and (len(line) >= assign_ix + 2) \
169 and (len(line) >= assign_ix + 2) \
170 and (line[assign_ix+1].string == '%') \
170 and (line[assign_ix+1].string == '%') \
171 and (line[assign_ix+2].type == tokenize.NAME):
171 and (line[assign_ix+2].type == tokenize.NAME):
172
172
173 This statement contains four continued line pieces.
173 This statement contains four continued line pieces.
174 Assembling these pieces into a single line would give::
174 Assembling these pieces into a single line would give::
175
175
176 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
176 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
177
177
178 This uses 0-indexed line numbers. *start* is (lineno, colno).
178 This uses 0-indexed line numbers. *start* is (lineno, colno).
179
179
180 Used to allow ``%magic`` and ``!system`` commands to be continued over
180 Used to allow ``%magic`` and ``!system`` commands to be continued over
181 multiple lines.
181 multiple lines.
182 """
182 """
183 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
183 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
184 return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
184 return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
185 + [parts[-1].rstrip()]) # Strip newline from last line
185 + [parts[-1].rstrip()]) # Strip newline from last line
186
186
187 class TokenTransformBase:
187 class TokenTransformBase:
188 """Base class for transformations which examine tokens.
188 """Base class for transformations which examine tokens.
189
189
190 Special syntax should not be transformed when it occurs inside strings or
190 Special syntax should not be transformed when it occurs inside strings or
191 comments. This is hard to reliably avoid with regexes. The solution is to
191 comments. This is hard to reliably avoid with regexes. The solution is to
192 tokenise the code as Python, and recognise the special syntax in the tokens.
192 tokenise the code as Python, and recognise the special syntax in the tokens.
193
193
194 IPython's special syntax is not valid Python syntax, so tokenising may go
194 IPython's special syntax is not valid Python syntax, so tokenising may go
195 wrong after the special syntax starts. These classes therefore find and
195 wrong after the special syntax starts. These classes therefore find and
196 transform *one* instance of special syntax at a time into regular Python
196 transform *one* instance of special syntax at a time into regular Python
197 syntax. After each transformation, tokens are regenerated to find the next
197 syntax. After each transformation, tokens are regenerated to find the next
198 piece of special syntax.
198 piece of special syntax.
199
199
200 Subclasses need to implement one class method (find)
200 Subclasses need to implement one class method (find)
201 and one regular method (transform).
201 and one regular method (transform).
202
202
203 The priority attribute can select which transformation to apply if multiple
203 The priority attribute can select which transformation to apply if multiple
204 transformers match in the same place. Lower numbers have higher priority.
204 transformers match in the same place. Lower numbers have higher priority.
205 This allows "%magic?" to be turned into a help call rather than a magic call.
205 This allows "%magic?" to be turned into a help call rather than a magic call.
206 """
206 """
207 # Lower numbers -> higher priority (for matches in the same location)
207 # Lower numbers -> higher priority (for matches in the same location)
208 priority = 10
208 priority = 10
209
209
210 def sortby(self):
210 def sortby(self):
211 return self.start_line, self.start_col, self.priority
211 return self.start_line, self.start_col, self.priority
212
212
213 def __init__(self, start):
213 def __init__(self, start):
214 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
214 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
215 self.start_col = start[1]
215 self.start_col = start[1]
216
216
217 @classmethod
217 @classmethod
218 def find(cls, tokens_by_line):
218 def find(cls, tokens_by_line):
219 """Find one instance of special syntax in the provided tokens.
219 """Find one instance of special syntax in the provided tokens.
220
220
221 Tokens are grouped into logical lines for convenience,
221 Tokens are grouped into logical lines for convenience,
222 so it is easy to e.g. look at the first token of each line.
222 so it is easy to e.g. look at the first token of each line.
223 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
223 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
224
224
225 This should return an instance of its class, pointing to the start
225 This should return an instance of its class, pointing to the start
226 position it has found, or None if it found no match.
226 position it has found, or None if it found no match.
227 """
227 """
228 raise NotImplementedError
228 raise NotImplementedError
229
229
230 def transform(self, lines: List[str]):
230 def transform(self, lines: List[str]):
231 """Transform one instance of special syntax found by ``find()``
231 """Transform one instance of special syntax found by ``find()``
232
232
233 Takes a list of strings representing physical lines,
233 Takes a list of strings representing physical lines,
234 returns a similar list of transformed lines.
234 returns a similar list of transformed lines.
235 """
235 """
236 raise NotImplementedError
236 raise NotImplementedError
237
237
238 class MagicAssign(TokenTransformBase):
238 class MagicAssign(TokenTransformBase):
239 """Transformer for assignments from magics (a = %foo)"""
239 """Transformer for assignments from magics (a = %foo)"""
240 @classmethod
240 @classmethod
241 def find(cls, tokens_by_line):
241 def find(cls, tokens_by_line):
242 """Find the first magic assignment (a = %foo) in the cell.
242 """Find the first magic assignment (a = %foo) in the cell.
243 """
243 """
244 for line in tokens_by_line:
244 for line in tokens_by_line:
245 assign_ix = _find_assign_op(line)
245 assign_ix = _find_assign_op(line)
246 if (assign_ix is not None) \
246 if (assign_ix is not None) \
247 and (len(line) >= assign_ix + 2) \
247 and (len(line) >= assign_ix + 2) \
248 and (line[assign_ix+1].string == '%') \
248 and (line[assign_ix+1].string == '%') \
249 and (line[assign_ix+2].type == tokenize.NAME):
249 and (line[assign_ix+2].type == tokenize.NAME):
250 return cls(line[assign_ix+1].start)
250 return cls(line[assign_ix+1].start)
251
251
252 def transform(self, lines: List[str]):
252 def transform(self, lines: List[str]):
253 """Transform a magic assignment found by the ``find()`` classmethod.
253 """Transform a magic assignment found by the ``find()`` classmethod.
254 """
254 """
255 start_line, start_col = self.start_line, self.start_col
255 start_line, start_col = self.start_line, self.start_col
256 lhs = lines[start_line][:start_col]
256 lhs = lines[start_line][:start_col]
257 end_line = find_end_of_continued_line(lines, start_line)
257 end_line = find_end_of_continued_line(lines, start_line)
258 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
258 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
259 assert rhs.startswith('%'), rhs
259 assert rhs.startswith('%'), rhs
260 magic_name, _, args = rhs[1:].partition(' ')
260 magic_name, _, args = rhs[1:].partition(' ')
261
261
262 lines_before = lines[:start_line]
262 lines_before = lines[:start_line]
263 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
263 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
264 new_line = lhs + call + '\n'
264 new_line = lhs + call + '\n'
265 lines_after = lines[end_line+1:]
265 lines_after = lines[end_line+1:]
266
266
267 return lines_before + [new_line] + lines_after
267 return lines_before + [new_line] + lines_after
268
268
269
269
270 class SystemAssign(TokenTransformBase):
270 class SystemAssign(TokenTransformBase):
271 """Transformer for assignments from system commands (a = !foo)"""
271 """Transformer for assignments from system commands (a = !foo)"""
272 @classmethod
272 @classmethod
273 def find(cls, tokens_by_line):
273 def find(cls, tokens_by_line):
274 """Find the first system assignment (a = !foo) in the cell.
274 """Find the first system assignment (a = !foo) in the cell.
275 """
275 """
276 for line in tokens_by_line:
276 for line in tokens_by_line:
277 assign_ix = _find_assign_op(line)
277 assign_ix = _find_assign_op(line)
278 if (assign_ix is not None) \
278 if (assign_ix is not None) \
279 and not line[assign_ix].line.strip().startswith('=') \
279 and not line[assign_ix].line.strip().startswith('=') \
280 and (len(line) >= assign_ix + 2) \
280 and (len(line) >= assign_ix + 2) \
281 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
281 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
282 ix = assign_ix + 1
282 ix = assign_ix + 1
283
283
284 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
284 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
285 if line[ix].string == '!':
285 if line[ix].string == '!':
286 return cls(line[ix].start)
286 return cls(line[ix].start)
287 elif not line[ix].string.isspace():
287 elif not line[ix].string.isspace():
288 break
288 break
289 ix += 1
289 ix += 1
290
290
291 def transform(self, lines: List[str]):
291 def transform(self, lines: List[str]):
292 """Transform a system assignment found by the ``find()`` classmethod.
292 """Transform a system assignment found by the ``find()`` classmethod.
293 """
293 """
294 start_line, start_col = self.start_line, self.start_col
294 start_line, start_col = self.start_line, self.start_col
295
295
296 lhs = lines[start_line][:start_col]
296 lhs = lines[start_line][:start_col]
297 end_line = find_end_of_continued_line(lines, start_line)
297 end_line = find_end_of_continued_line(lines, start_line)
298 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
298 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
299 assert rhs.startswith('!'), rhs
299 assert rhs.startswith('!'), rhs
300 cmd = rhs[1:]
300 cmd = rhs[1:]
301
301
302 lines_before = lines[:start_line]
302 lines_before = lines[:start_line]
303 call = "get_ipython().getoutput({!r})".format(cmd)
303 call = "get_ipython().getoutput({!r})".format(cmd)
304 new_line = lhs + call + '\n'
304 new_line = lhs + call + '\n'
305 lines_after = lines[end_line + 1:]
305 lines_after = lines[end_line + 1:]
306
306
307 return lines_before + [new_line] + lines_after
307 return lines_before + [new_line] + lines_after
308
308
309 # The escape sequences that define the syntax transformations IPython will
309 # The escape sequences that define the syntax transformations IPython will
310 # apply to user input. These can NOT be just changed here: many regular
310 # apply to user input. These can NOT be just changed here: many regular
311 # expressions and other parts of the code may use their hardcoded values, and
311 # expressions and other parts of the code may use their hardcoded values, and
312 # for all intents and purposes they constitute the 'IPython syntax', so they
312 # for all intents and purposes they constitute the 'IPython syntax', so they
313 # should be considered fixed.
313 # should be considered fixed.
314
314
315 ESC_SHELL = '!' # Send line to underlying system shell
315 ESC_SHELL = '!' # Send line to underlying system shell
316 ESC_SH_CAP = '!!' # Send line to system shell and capture output
316 ESC_SH_CAP = '!!' # Send line to system shell and capture output
317 ESC_HELP = '?' # Find information about object
317 ESC_HELP = '?' # Find information about object
318 ESC_HELP2 = '??' # Find extra-detailed information about object
318 ESC_HELP2 = '??' # Find extra-detailed information about object
319 ESC_MAGIC = '%' # Call magic function
319 ESC_MAGIC = '%' # Call magic function
320 ESC_MAGIC2 = '%%' # Call cell-magic function
320 ESC_MAGIC2 = '%%' # Call cell-magic function
321 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
321 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
322 ESC_QUOTE2 = ';' # Quote all args as a single string, call
322 ESC_QUOTE2 = ';' # Quote all args as a single string, call
323 ESC_PAREN = '/' # Call first argument with rest of line as arguments
323 ESC_PAREN = '/' # Call first argument with rest of line as arguments
324
324
325 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
325 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
326 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
326 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
327
327
328 def _make_help_call(target, esc, next_input=None):
328 def _make_help_call(target, esc, next_input=None):
329 """Prepares a pinfo(2)/psearch call from a target name and the escape
329 """Prepares a pinfo(2)/psearch call from a target name and the escape
330 (i.e. ? or ??)"""
330 (i.e. ? or ??)"""
331 method = 'pinfo2' if esc == '??' \
331 method = 'pinfo2' if esc == '??' \
332 else 'psearch' if '*' in target \
332 else 'psearch' if '*' in target \
333 else 'pinfo'
333 else 'pinfo'
334 arg = " ".join([method, target])
334 arg = " ".join([method, target])
335 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
335 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
336 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
336 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
337 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
337 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
338 if next_input is None:
338 if next_input is None:
339 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
339 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
340 else:
340 else:
341 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
341 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
342 (next_input, t_magic_name, t_magic_arg_s)
342 (next_input, t_magic_name, t_magic_arg_s)
343
343
344 def _tr_help(content):
344 def _tr_help(content):
345 """Translate lines escaped with: ?
345 """Translate lines escaped with: ?
346
346
347 A naked help line should fire the intro help screen (shell.show_usage())
347 A naked help line should fire the intro help screen (shell.show_usage())
348 """
348 """
349 if not content:
349 if not content:
350 return 'get_ipython().show_usage()'
350 return 'get_ipython().show_usage()'
351
351
352 return _make_help_call(content, '?')
352 return _make_help_call(content, '?')
353
353
354 def _tr_help2(content):
354 def _tr_help2(content):
355 """Translate lines escaped with: ??
355 """Translate lines escaped with: ??
356
356
357 A naked help line should fire the intro help screen (shell.show_usage())
357 A naked help line should fire the intro help screen (shell.show_usage())
358 """
358 """
359 if not content:
359 if not content:
360 return 'get_ipython().show_usage()'
360 return 'get_ipython().show_usage()'
361
361
362 return _make_help_call(content, '??')
362 return _make_help_call(content, '??')
363
363
364 def _tr_magic(content):
364 def _tr_magic(content):
365 "Translate lines escaped with a percent sign: %"
365 "Translate lines escaped with a percent sign: %"
366 name, _, args = content.partition(' ')
366 name, _, args = content.partition(' ')
367 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
367 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
368
368
369 def _tr_quote(content):
369 def _tr_quote(content):
370 "Translate lines escaped with a comma: ,"
370 "Translate lines escaped with a comma: ,"
371 name, _, args = content.partition(' ')
371 name, _, args = content.partition(' ')
372 return '%s("%s")' % (name, '", "'.join(args.split()) )
372 return '%s("%s")' % (name, '", "'.join(args.split()) )
373
373
374 def _tr_quote2(content):
374 def _tr_quote2(content):
375 "Translate lines escaped with a semicolon: ;"
375 "Translate lines escaped with a semicolon: ;"
376 name, _, args = content.partition(' ')
376 name, _, args = content.partition(' ')
377 return '%s("%s")' % (name, args)
377 return '%s("%s")' % (name, args)
378
378
379 def _tr_paren(content):
379 def _tr_paren(content):
380 "Translate lines escaped with a slash: /"
380 "Translate lines escaped with a slash: /"
381 name, _, args = content.partition(' ')
381 name, _, args = content.partition(' ')
382 return '%s(%s)' % (name, ", ".join(args.split()))
382 return '%s(%s)' % (name, ", ".join(args.split()))
383
383
384 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
384 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
385 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
385 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
386 ESC_HELP : _tr_help,
386 ESC_HELP : _tr_help,
387 ESC_HELP2 : _tr_help2,
387 ESC_HELP2 : _tr_help2,
388 ESC_MAGIC : _tr_magic,
388 ESC_MAGIC : _tr_magic,
389 ESC_QUOTE : _tr_quote,
389 ESC_QUOTE : _tr_quote,
390 ESC_QUOTE2 : _tr_quote2,
390 ESC_QUOTE2 : _tr_quote2,
391 ESC_PAREN : _tr_paren }
391 ESC_PAREN : _tr_paren }
392
392
393 class EscapedCommand(TokenTransformBase):
393 class EscapedCommand(TokenTransformBase):
394 """Transformer for escaped commands like %foo, !foo, or /foo"""
394 """Transformer for escaped commands like %foo, !foo, or /foo"""
395 @classmethod
395 @classmethod
396 def find(cls, tokens_by_line):
396 def find(cls, tokens_by_line):
397 """Find the first escaped command (%foo, !foo, etc.) in the cell.
397 """Find the first escaped command (%foo, !foo, etc.) in the cell.
398 """
398 """
399 for line in tokens_by_line:
399 for line in tokens_by_line:
400 if not line:
400 if not line:
401 continue
401 continue
402 ix = 0
402 ix = 0
403 ll = len(line)
403 ll = len(line)
404 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
404 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
405 ix += 1
405 ix += 1
406 if ix >= ll:
406 if ix >= ll:
407 continue
407 continue
408 if line[ix].string in ESCAPE_SINGLES:
408 if line[ix].string in ESCAPE_SINGLES:
409 return cls(line[ix].start)
409 return cls(line[ix].start)
410
410
411 def transform(self, lines):
411 def transform(self, lines):
412 """Transform an escaped line found by the ``find()`` classmethod.
412 """Transform an escaped line found by the ``find()`` classmethod.
413 """
413 """
414 start_line, start_col = self.start_line, self.start_col
414 start_line, start_col = self.start_line, self.start_col
415
415
416 indent = lines[start_line][:start_col]
416 indent = lines[start_line][:start_col]
417 end_line = find_end_of_continued_line(lines, start_line)
417 end_line = find_end_of_continued_line(lines, start_line)
418 line = assemble_continued_line(lines, (start_line, start_col), end_line)
418 line = assemble_continued_line(lines, (start_line, start_col), end_line)
419
419
420 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
420 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
421 escape, content = line[:2], line[2:]
421 escape, content = line[:2], line[2:]
422 else:
422 else:
423 escape, content = line[:1], line[1:]
423 escape, content = line[:1], line[1:]
424
424
425 if escape in tr:
425 if escape in tr:
426 call = tr[escape](content)
426 call = tr[escape](content)
427 else:
427 else:
428 call = ''
428 call = ''
429
429
430 lines_before = lines[:start_line]
430 lines_before = lines[:start_line]
431 new_line = indent + call + '\n'
431 new_line = indent + call + '\n'
432 lines_after = lines[end_line + 1:]
432 lines_after = lines[end_line + 1:]
433
433
434 return lines_before + [new_line] + lines_after
434 return lines_before + [new_line] + lines_after
435
435
436 _help_end_re = re.compile(r"""(%{0,2}
436 _help_end_re = re.compile(r"""(%{0,2}
437 (?!\d)[\w*]+ # Variable name
437 (?!\d)[\w*]+ # Variable name
438 (\.(?!\d)[\w*]+)* # .etc.etc
438 (\.(?!\d)[\w*]+)* # .etc.etc
439 )
439 )
440 (\?\??)$ # ? or ??
440 (\?\??)$ # ? or ??
441 """,
441 """,
442 re.VERBOSE)
442 re.VERBOSE)
443
443
444 class HelpEnd(TokenTransformBase):
444 class HelpEnd(TokenTransformBase):
445 """Transformer for help syntax: obj? and obj??"""
445 """Transformer for help syntax: obj? and obj??"""
446 # This needs to be higher priority (lower number) than EscapedCommand so
446 # This needs to be higher priority (lower number) than EscapedCommand so
447 # that inspecting magics (%foo?) works.
447 # that inspecting magics (%foo?) works.
448 priority = 5
448 priority = 5
449
449
450 def __init__(self, start, q_locn):
450 def __init__(self, start, q_locn):
451 super().__init__(start)
451 super().__init__(start)
452 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
452 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
453 self.q_col = q_locn[1]
453 self.q_col = q_locn[1]
454
454
455 @classmethod
455 @classmethod
456 def find(cls, tokens_by_line):
456 def find(cls, tokens_by_line):
457 """Find the first help command (foo?) in the cell.
457 """Find the first help command (foo?) in the cell.
458 """
458 """
459 for line in tokens_by_line:
459 for line in tokens_by_line:
460 # Last token is NEWLINE; look at last but one
460 # Last token is NEWLINE; look at last but one
461 if len(line) > 2 and line[-2].string == '?':
461 if len(line) > 2 and line[-2].string == '?':
462 # Find the first token that's not INDENT/DEDENT
462 # Find the first token that's not INDENT/DEDENT
463 ix = 0
463 ix = 0
464 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
464 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
465 ix += 1
465 ix += 1
466 return cls(line[ix].start, line[-2].start)
466 return cls(line[ix].start, line[-2].start)
467
467
468 def transform(self, lines):
468 def transform(self, lines):
469 """Transform a help command found by the ``find()`` classmethod.
469 """Transform a help command found by the ``find()`` classmethod.
470 """
470 """
471 piece = ''.join(lines[self.start_line:self.q_line+1])
471 piece = ''.join(lines[self.start_line:self.q_line+1])
472 indent, content = piece[:self.start_col], piece[self.start_col:]
472 indent, content = piece[:self.start_col], piece[self.start_col:]
473 lines_before = lines[:self.start_line]
473 lines_before = lines[:self.start_line]
474 lines_after = lines[self.q_line + 1:]
474 lines_after = lines[self.q_line + 1:]
475
475
476 m = _help_end_re.search(content)
476 m = _help_end_re.search(content)
477 if not m:
477 if not m:
478 raise SyntaxError(content)
478 raise SyntaxError(content)
479 assert m is not None, content
479 assert m is not None, content
480 target = m.group(1)
480 target = m.group(1)
481 esc = m.group(3)
481 esc = m.group(3)
482
482
483 # If we're mid-command, put it back on the next prompt for the user.
483 # If we're mid-command, put it back on the next prompt for the user.
484 next_input = None
484 next_input = None
485 if (not lines_before) and (not lines_after) \
485 if (not lines_before) and (not lines_after) \
486 and content.strip() != m.group(0):
486 and content.strip() != m.group(0):
487 next_input = content.rstrip('?\n')
487 next_input = content.rstrip('?\n')
488
488
489 call = _make_help_call(target, esc, next_input=next_input)
489 call = _make_help_call(target, esc, next_input=next_input)
490 new_line = indent + call + '\n'
490 new_line = indent + call + '\n'
491
491
492 return lines_before + [new_line] + lines_after
492 return lines_before + [new_line] + lines_after
493
493
494 def make_tokens_by_line(lines:List[str]):
494 def make_tokens_by_line(lines:List[str]):
495 """Tokenize a series of lines and group tokens by line.
495 """Tokenize a series of lines and group tokens by line.
496
496
497 The tokens for a multiline Python string or expression are grouped as one
497 The tokens for a multiline Python string or expression are grouped as one
498 line. All lines except the last lines should keep their line ending ('\\n',
498 line. All lines except the last lines should keep their line ending ('\\n',
499 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
499 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
500 for example when passing block of text to this function.
500 for example when passing block of text to this function.
501
501
502 """
502 """
503 # NL tokens are used inside multiline expressions, but also after blank
503 # NL tokens are used inside multiline expressions, but also after blank
504 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
504 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
505 # We want to group the former case together but split the latter, so we
505 # We want to group the former case together but split the latter, so we
506 # track parentheses level, similar to the internals of tokenize.
506 # track parentheses level, similar to the internals of tokenize.
507
507
508 # reexported from token on 3.7+
508 # reexported from token on 3.7+
509 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore
509 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore
510 tokens_by_line:List[List[Any]] = [[]]
510 tokens_by_line:List[List[Any]] = [[]]
511 if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')):
511 if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')):
512 warnings.warn("`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified")
512 warnings.warn("`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified")
513 parenlev = 0
513 parenlev = 0
514 try:
514 try:
515 for token in tokenize.generate_tokens(iter(lines).__next__):
515 for token in tokenize.generate_tokens(iter(lines).__next__):
516 tokens_by_line[-1].append(token)
516 tokens_by_line[-1].append(token)
517 if (token.type == NEWLINE) \
517 if (token.type == NEWLINE) \
518 or ((token.type == NL) and (parenlev <= 0)):
518 or ((token.type == NL) and (parenlev <= 0)):
519 tokens_by_line.append([])
519 tokens_by_line.append([])
520 elif token.string in {'(', '[', '{'}:
520 elif token.string in {'(', '[', '{'}:
521 parenlev += 1
521 parenlev += 1
522 elif token.string in {')', ']', '}'}:
522 elif token.string in {')', ']', '}'}:
523 if parenlev > 0:
523 if parenlev > 0:
524 parenlev -= 1
524 parenlev -= 1
525 except tokenize.TokenError:
525 except tokenize.TokenError:
526 # Input ended in a multiline string or expression. That's OK for us.
526 # Input ended in a multiline string or expression. That's OK for us.
527 pass
527 pass
528
528
529
529
530 if not tokens_by_line[-1]:
530 if not tokens_by_line[-1]:
531 tokens_by_line.pop()
531 tokens_by_line.pop()
532
532
533
533
534 return tokens_by_line
534 return tokens_by_line
535
535
536
536
537 def has_sunken_brackets(tokens: List[tokenize.TokenInfo]):
537 def has_sunken_brackets(tokens: List[tokenize.TokenInfo]):
538 """Check if the depth of brackets in the list of tokens drops below 0"""
538 """Check if the depth of brackets in the list of tokens drops below 0"""
539 parenlev = 0
539 parenlev = 0
540 for token in tokens:
540 for token in tokens:
541 if token.string in {"(", "[", "{"}:
541 if token.string in {"(", "[", "{"}:
542 parenlev += 1
542 parenlev += 1
543 elif token.string in {")", "]", "}"}:
543 elif token.string in {")", "]", "}"}:
544 parenlev -= 1
544 parenlev -= 1
545 if parenlev < 0:
545 if parenlev < 0:
546 return True
546 return True
547 return False
547 return False
548
548
549
549
550 def show_linewise_tokens(s: str):
550 def show_linewise_tokens(s: str):
551 """For investigation and debugging"""
551 """For investigation and debugging"""
552 if not s.endswith('\n'):
552 if not s.endswith('\n'):
553 s += '\n'
553 s += '\n'
554 lines = s.splitlines(keepends=True)
554 lines = s.splitlines(keepends=True)
555 for line in make_tokens_by_line(lines):
555 for line in make_tokens_by_line(lines):
556 print("Line -------")
556 print("Line -------")
557 for tokinfo in line:
557 for tokinfo in line:
558 print(" ", tokinfo)
558 print(" ", tokinfo)
559
559
560 # Arbitrary limit to prevent getting stuck in infinite loops
560 # Arbitrary limit to prevent getting stuck in infinite loops
561 TRANSFORM_LOOP_LIMIT = 500
561 TRANSFORM_LOOP_LIMIT = 500
562
562
563 class TransformerManager:
563 class TransformerManager:
564 """Applies various transformations to a cell or code block.
564 """Applies various transformations to a cell or code block.
565
565
566 The key methods for external use are ``transform_cell()``
566 The key methods for external use are ``transform_cell()``
567 and ``check_complete()``.
567 and ``check_complete()``.
568 """
568 """
569 def __init__(self):
569 def __init__(self):
570 self.cleanup_transforms = [
570 self.cleanup_transforms = [
571 leading_empty_lines,
571 leading_empty_lines,
572 leading_indent,
572 leading_indent,
573 classic_prompt,
573 classic_prompt,
574 ipython_prompt,
574 ipython_prompt,
575 ]
575 ]
576 self.line_transforms = [
576 self.line_transforms = [
577 cell_magic,
577 cell_magic,
578 ]
578 ]
579 self.token_transformers = [
579 self.token_transformers = [
580 MagicAssign,
580 MagicAssign,
581 SystemAssign,
581 SystemAssign,
582 EscapedCommand,
582 EscapedCommand,
583 HelpEnd,
583 HelpEnd,
584 ]
584 ]
585
585
586 def do_one_token_transform(self, lines):
586 def do_one_token_transform(self, lines):
587 """Find and run the transform earliest in the code.
587 """Find and run the transform earliest in the code.
588
588
589 Returns (changed, lines).
589 Returns (changed, lines).
590
590
591 This method is called repeatedly until changed is False, indicating
591 This method is called repeatedly until changed is False, indicating
592 that all available transformations are complete.
592 that all available transformations are complete.
593
593
594 The tokens following IPython special syntax might not be valid, so
594 The tokens following IPython special syntax might not be valid, so
595 the transformed code is retokenised every time to identify the next
595 the transformed code is retokenised every time to identify the next
596 piece of special syntax. Hopefully long code cells are mostly valid
596 piece of special syntax. Hopefully long code cells are mostly valid
597 Python, not using lots of IPython special syntax, so this shouldn't be
597 Python, not using lots of IPython special syntax, so this shouldn't be
598 a performance issue.
598 a performance issue.
599 """
599 """
600 tokens_by_line = make_tokens_by_line(lines)
600 tokens_by_line = make_tokens_by_line(lines)
601 candidates = []
601 candidates = []
602 for transformer_cls in self.token_transformers:
602 for transformer_cls in self.token_transformers:
603 transformer = transformer_cls.find(tokens_by_line)
603 transformer = transformer_cls.find(tokens_by_line)
604 if transformer:
604 if transformer:
605 candidates.append(transformer)
605 candidates.append(transformer)
606
606
607 if not candidates:
607 if not candidates:
608 # Nothing to transform
608 # Nothing to transform
609 return False, lines
609 return False, lines
610 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
610 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
611 for transformer in ordered_transformers:
611 for transformer in ordered_transformers:
612 try:
612 try:
613 return True, transformer.transform(lines)
613 return True, transformer.transform(lines)
614 except SyntaxError:
614 except SyntaxError:
615 pass
615 pass
616 return False, lines
616 return False, lines
617
617
618 def do_token_transforms(self, lines):
618 def do_token_transforms(self, lines):
619 for _ in range(TRANSFORM_LOOP_LIMIT):
619 for _ in range(TRANSFORM_LOOP_LIMIT):
620 changed, lines = self.do_one_token_transform(lines)
620 changed, lines = self.do_one_token_transform(lines)
621 if not changed:
621 if not changed:
622 return lines
622 return lines
623
623
624 raise RuntimeError("Input transformation still changing after "
624 raise RuntimeError("Input transformation still changing after "
625 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
625 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
626
626
627 def transform_cell(self, cell: str) -> str:
627 def transform_cell(self, cell: str) -> str:
628 """Transforms a cell of input code"""
628 """Transforms a cell of input code"""
629 if not cell.endswith('\n'):
629 if not cell.endswith('\n'):
630 cell += '\n' # Ensure the cell has a trailing newline
630 cell += '\n' # Ensure the cell has a trailing newline
631 lines = cell.splitlines(keepends=True)
631 lines = cell.splitlines(keepends=True)
632 for transform in self.cleanup_transforms + self.line_transforms:
632 for transform in self.cleanup_transforms + self.line_transforms:
633 lines = transform(lines)
633 lines = transform(lines)
634
634
635 lines = self.do_token_transforms(lines)
635 lines = self.do_token_transforms(lines)
636 return ''.join(lines)
636 return ''.join(lines)
637
637
638 def check_complete(self, cell: str):
638 def check_complete(self, cell: str):
639 """Return whether a block of code is ready to execute, or should be continued
639 """Return whether a block of code is ready to execute, or should be continued
640
640
641 Parameters
641 Parameters
642 ----------
642 ----------
643 source : string
643 source : string
644 Python input code, which can be multiline.
644 Python input code, which can be multiline.
645
645
646 Returns
646 Returns
647 -------
647 -------
648 status : str
648 status : str
649 One of 'complete', 'incomplete', or 'invalid' if source is not a
649 One of 'complete', 'incomplete', or 'invalid' if source is not a
650 prefix of valid code.
650 prefix of valid code.
651 indent_spaces : int or None
651 indent_spaces : int or None
652 The number of spaces by which to indent the next line of code. If
652 The number of spaces by which to indent the next line of code. If
653 status is not 'incomplete', this is None.
653 status is not 'incomplete', this is None.
654 """
654 """
655 # Remember if the lines ends in a new line.
655 # Remember if the lines ends in a new line.
656 ends_with_newline = False
656 ends_with_newline = False
657 for character in reversed(cell):
657 for character in reversed(cell):
658 if character == '\n':
658 if character == '\n':
659 ends_with_newline = True
659 ends_with_newline = True
660 break
660 break
661 elif character.strip():
661 elif character.strip():
662 break
662 break
663 else:
663 else:
664 continue
664 continue
665
665
666 if not ends_with_newline:
666 if not ends_with_newline:
667 # Append an newline for consistent tokenization
667 # Append an newline for consistent tokenization
668 # See https://bugs.python.org/issue33899
668 # See https://bugs.python.org/issue33899
669 cell += '\n'
669 cell += '\n'
670
670
671 lines = cell.splitlines(keepends=True)
671 lines = cell.splitlines(keepends=True)
672
672
673 if not lines:
673 if not lines:
674 return 'complete', None
674 return 'complete', None
675
675
676 if lines[-1].endswith('\\'):
676 if lines[-1].endswith('\\'):
677 # Explicit backslash continuation
677 # Explicit backslash continuation
678 return 'incomplete', find_last_indent(lines)
678 return 'incomplete', find_last_indent(lines)
679
679
680 try:
680 try:
681 for transform in self.cleanup_transforms:
681 for transform in self.cleanup_transforms:
682 if not getattr(transform, 'has_side_effects', False):
682 if not getattr(transform, 'has_side_effects', False):
683 lines = transform(lines)
683 lines = transform(lines)
684 except SyntaxError:
684 except SyntaxError:
685 return 'invalid', None
685 return 'invalid', None
686
686
687 if lines[0].startswith('%%'):
687 if lines[0].startswith('%%'):
688 # Special case for cell magics - completion marked by blank line
688 # Special case for cell magics - completion marked by blank line
689 if lines[-1].strip():
689 if lines[-1].strip():
690 return 'incomplete', find_last_indent(lines)
690 return 'incomplete', find_last_indent(lines)
691 else:
691 else:
692 return 'complete', None
692 return 'complete', None
693
693
694 try:
694 try:
695 for transform in self.line_transforms:
695 for transform in self.line_transforms:
696 if not getattr(transform, 'has_side_effects', False):
696 if not getattr(transform, 'has_side_effects', False):
697 lines = transform(lines)
697 lines = transform(lines)
698 lines = self.do_token_transforms(lines)
698 lines = self.do_token_transforms(lines)
699 except SyntaxError:
699 except SyntaxError:
700 return 'invalid', None
700 return 'invalid', None
701
701
702 tokens_by_line = make_tokens_by_line(lines)
702 tokens_by_line = make_tokens_by_line(lines)
703
703
704 # Bail if we got one line and there are more closing parentheses than
704 # Bail if we got one line and there are more closing parentheses than
705 # the opening ones
705 # the opening ones
706 if (
706 if (
707 len(lines) == 1
707 len(lines) == 1
708 and tokens_by_line
708 and tokens_by_line
709 and has_sunken_brackets(tokens_by_line[0])
709 and has_sunken_brackets(tokens_by_line[0])
710 ):
710 ):
711 return "invalid", None
711 return "invalid", None
712
712
713 if not tokens_by_line:
713 if not tokens_by_line:
714 return 'incomplete', find_last_indent(lines)
714 return 'incomplete', find_last_indent(lines)
715
715
716 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
716 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
717 # We're in a multiline string or expression
717 # We're in a multiline string or expression
718 return 'incomplete', find_last_indent(lines)
718 return 'incomplete', find_last_indent(lines)
719
719
720 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER} # type: ignore
720 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER} # type: ignore
721
721
722 # Pop the last line which only contains DEDENTs and ENDMARKER
722 # Pop the last line which only contains DEDENTs and ENDMARKER
723 last_token_line = None
723 last_token_line = None
724 if {t.type for t in tokens_by_line[-1]} in [
724 if {t.type for t in tokens_by_line[-1]} in [
725 {tokenize.DEDENT, tokenize.ENDMARKER},
725 {tokenize.DEDENT, tokenize.ENDMARKER},
726 {tokenize.ENDMARKER}
726 {tokenize.ENDMARKER}
727 ] and len(tokens_by_line) > 1:
727 ] and len(tokens_by_line) > 1:
728 last_token_line = tokens_by_line.pop()
728 last_token_line = tokens_by_line.pop()
729
729
730 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
730 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
731 tokens_by_line[-1].pop()
731 tokens_by_line[-1].pop()
732
732
733 if not tokens_by_line[-1]:
733 if not tokens_by_line[-1]:
734 return 'incomplete', find_last_indent(lines)
734 return 'incomplete', find_last_indent(lines)
735
735
736 if tokens_by_line[-1][-1].string == ':':
736 if tokens_by_line[-1][-1].string == ':':
737 # The last line starts a block (e.g. 'if foo:')
737 # The last line starts a block (e.g. 'if foo:')
738 ix = 0
738 ix = 0
739 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
739 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
740 ix += 1
740 ix += 1
741
741
742 indent = tokens_by_line[-1][ix].start[1]
742 indent = tokens_by_line[-1][ix].start[1]
743 return 'incomplete', indent + 4
743 return 'incomplete', indent + 4
744
744
745 if tokens_by_line[-1][0].line.endswith('\\'):
745 if tokens_by_line[-1][0].line.endswith('\\'):
746 return 'incomplete', None
746 return 'incomplete', None
747
747
748 # At this point, our checks think the code is complete (or invalid).
748 # At this point, our checks think the code is complete (or invalid).
749 # We'll use codeop.compile_command to check this with the real parser
749 # We'll use codeop.compile_command to check this with the real parser
750 try:
750 try:
751 with warnings.catch_warnings():
751 with warnings.catch_warnings():
752 warnings.simplefilter('error', SyntaxWarning)
752 warnings.simplefilter('error', SyntaxWarning)
753 res = compile_command(''.join(lines), symbol='exec')
753 res = compile_command(''.join(lines), symbol='exec')
754 except (SyntaxError, OverflowError, ValueError, TypeError,
754 except (SyntaxError, OverflowError, ValueError, TypeError,
755 MemoryError, SyntaxWarning):
755 MemoryError, SyntaxWarning):
756 return 'invalid', None
756 return 'invalid', None
757 else:
757 else:
758 if res is None:
758 if res is None:
759 return 'incomplete', find_last_indent(lines)
759 return 'incomplete', find_last_indent(lines)
760
760
761 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
761 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
762 if ends_with_newline:
762 if ends_with_newline:
763 return 'complete', None
763 return 'complete', None
764 return 'incomplete', find_last_indent(lines)
764 return 'incomplete', find_last_indent(lines)
765
765
766 # If there's a blank line at the end, assume we're ready to execute
766 # If there's a blank line at the end, assume we're ready to execute
767 if not lines[-1].strip():
767 if not lines[-1].strip():
768 return 'complete', None
768 return 'complete', None
769
769
770 return 'complete', None
770 return 'complete', None
771
771
772
772
773 def find_last_indent(lines):
773 def find_last_indent(lines):
774 m = _indent_re.match(lines[-1])
774 m = _indent_re.match(lines[-1])
775 if not m:
775 if not m:
776 return 0
776 return 0
777 return len(m.group(0).replace('\t', ' '*4))
777 return len(m.group(0).replace('\t', ' '*4))
778
778
779
779
780 class MaybeAsyncCompile(Compile):
780 class MaybeAsyncCompile(Compile):
781 def __init__(self, extra_flags=0):
781 def __init__(self, extra_flags=0):
782 super().__init__()
782 super().__init__()
783 self.flags |= extra_flags
783 self.flags |= extra_flags
784
784
785 __call__ = compile
785 def __call__(self, *args, **kwds):
786 return compile(*args, **kwds)
786
787
787
788
788 class MaybeAsyncCommandCompiler(CommandCompiler):
789 class MaybeAsyncCommandCompiler(CommandCompiler):
789 def __init__(self, extra_flags=0):
790 def __init__(self, extra_flags=0):
790 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags)
791 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags)
791
792
792
793
793 if (sys.version_info.major, sys.version_info.minor) >= (3, 8):
794 if (sys.version_info.major, sys.version_info.minor) >= (3, 8):
794 _extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
795 _extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
795 else:
796 else:
796 _extra_flags = ast.PyCF_ONLY_AST
797 _extra_flags = ast.PyCF_ONLY_AST
797
798
798 compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)
799 compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)
General Comments 0
You need to be logged in to leave comments. Login now