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