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