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