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