##// END OF EJS Templates
Allow to mark transformers as having side effects...
Matthias Bussonnier -
Show More
@@ -1,724 +1,726 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 If initial_re and prompt_re differ,
64 If initial_re and prompt_re differ,
65 only initial_re will be tested against the first line.
65 only initial_re will be tested against the first line.
66 If any prompt is found on the first two lines,
66 If any prompt is found on the first two lines,
67 prompts will be stripped from the rest of the block.
67 prompts will be stripped from the rest of the block.
68 """
68 """
69 def __init__(self, prompt_re, initial_re=None):
69 def __init__(self, prompt_re, initial_re=None):
70 self.prompt_re = prompt_re
70 self.prompt_re = prompt_re
71 self.initial_re = initial_re or prompt_re
71 self.initial_re = initial_re or prompt_re
72
72
73 def _strip(self, lines):
73 def _strip(self, lines):
74 return [self.prompt_re.sub('', l, count=1) for l in lines]
74 return [self.prompt_re.sub('', l, count=1) for l in lines]
75
75
76 def __call__(self, lines):
76 def __call__(self, lines):
77 if not lines:
77 if not lines:
78 return lines
78 return lines
79 if self.initial_re.match(lines[0]) or \
79 if self.initial_re.match(lines[0]) or \
80 (len(lines) > 1 and self.prompt_re.match(lines[1])):
80 (len(lines) > 1 and self.prompt_re.match(lines[1])):
81 return self._strip(lines)
81 return self._strip(lines)
82 return lines
82 return lines
83
83
84 classic_prompt = PromptStripper(
84 classic_prompt = PromptStripper(
85 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
85 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
86 initial_re=re.compile(r'^>>>( |$)')
86 initial_re=re.compile(r'^>>>( |$)')
87 )
87 )
88
88
89 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
89 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
90
90
91 def cell_magic(lines):
91 def cell_magic(lines):
92 if not lines or not lines[0].startswith('%%'):
92 if not lines or not lines[0].startswith('%%'):
93 return lines
93 return lines
94 if re.match(r'%%\w+\?', lines[0]):
94 if re.match(r'%%\w+\?', lines[0]):
95 # This case will be handled by help_end
95 # This case will be handled by help_end
96 return lines
96 return lines
97 magic_name, _, first_line = lines[0][2:-1].partition(' ')
97 magic_name, _, first_line = lines[0][2:-1].partition(' ')
98 body = ''.join(lines[1:])
98 body = ''.join(lines[1:])
99 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
99 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
100 % (magic_name, first_line, body)]
100 % (magic_name, first_line, body)]
101
101
102
102
103 def _find_assign_op(token_line) -> Optional[int]:
103 def _find_assign_op(token_line) -> Optional[int]:
104 """Get the index of the first assignment in the line ('=' not inside brackets)
104 """Get the index of the first assignment in the line ('=' not inside brackets)
105
105
106 Note: We don't try to support multiple special assignment (a = b = %foo)
106 Note: We don't try to support multiple special assignment (a = b = %foo)
107 """
107 """
108 paren_level = 0
108 paren_level = 0
109 for i, ti in enumerate(token_line):
109 for i, ti in enumerate(token_line):
110 s = ti.string
110 s = ti.string
111 if s == '=' and paren_level == 0:
111 if s == '=' and paren_level == 0:
112 return i
112 return i
113 if s in {'(','[','{'}:
113 if s in {'(','[','{'}:
114 paren_level += 1
114 paren_level += 1
115 elif s in {')', ']', '}'}:
115 elif s in {')', ']', '}'}:
116 if paren_level > 0:
116 if paren_level > 0:
117 paren_level -= 1
117 paren_level -= 1
118 return None
118 return None
119
119
120 def find_end_of_continued_line(lines, start_line: int):
120 def find_end_of_continued_line(lines, start_line: int):
121 """Find the last line of a line explicitly extended using backslashes.
121 """Find the last line of a line explicitly extended using backslashes.
122
122
123 Uses 0-indexed line numbers.
123 Uses 0-indexed line numbers.
124 """
124 """
125 end_line = start_line
125 end_line = start_line
126 while lines[end_line].endswith('\\\n'):
126 while lines[end_line].endswith('\\\n'):
127 end_line += 1
127 end_line += 1
128 if end_line >= len(lines):
128 if end_line >= len(lines):
129 break
129 break
130 return end_line
130 return end_line
131
131
132 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
132 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
133 r"""Assemble a single line from multiple continued line pieces
133 r"""Assemble a single line from multiple continued line pieces
134
134
135 Continued lines are lines ending in ``\``, and the line following the last
135 Continued lines are lines ending in ``\``, and the line following the last
136 ``\`` in the block.
136 ``\`` in the block.
137
137
138 For example, this code continues over multiple lines::
138 For example, this code continues over multiple lines::
139
139
140 if (assign_ix is not None) \
140 if (assign_ix is not None) \
141 and (len(line) >= assign_ix + 2) \
141 and (len(line) >= assign_ix + 2) \
142 and (line[assign_ix+1].string == '%') \
142 and (line[assign_ix+1].string == '%') \
143 and (line[assign_ix+2].type == tokenize.NAME):
143 and (line[assign_ix+2].type == tokenize.NAME):
144
144
145 This statement contains four continued line pieces.
145 This statement contains four continued line pieces.
146 Assembling these pieces into a single line would give::
146 Assembling these pieces into a single line would give::
147
147
148 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
148 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
149
149
150 This uses 0-indexed line numbers. *start* is (lineno, colno).
150 This uses 0-indexed line numbers. *start* is (lineno, colno).
151
151
152 Used to allow ``%magic`` and ``!system`` commands to be continued over
152 Used to allow ``%magic`` and ``!system`` commands to be continued over
153 multiple lines.
153 multiple lines.
154 """
154 """
155 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
155 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
156 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
156 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
157 + [parts[-1][:-1]]) # Strip newline from last line
157 + [parts[-1][:-1]]) # Strip newline from last line
158
158
159 class TokenTransformBase:
159 class TokenTransformBase:
160 """Base class for transformations which examine tokens.
160 """Base class for transformations which examine tokens.
161
161
162 Special syntax should not be transformed when it occurs inside strings or
162 Special syntax should not be transformed when it occurs inside strings or
163 comments. This is hard to reliably avoid with regexes. The solution is to
163 comments. This is hard to reliably avoid with regexes. The solution is to
164 tokenise the code as Python, and recognise the special syntax in the tokens.
164 tokenise the code as Python, and recognise the special syntax in the tokens.
165
165
166 IPython's special syntax is not valid Python syntax, so tokenising may go
166 IPython's special syntax is not valid Python syntax, so tokenising may go
167 wrong after the special syntax starts. These classes therefore find and
167 wrong after the special syntax starts. These classes therefore find and
168 transform *one* instance of special syntax at a time into regular Python
168 transform *one* instance of special syntax at a time into regular Python
169 syntax. After each transformation, tokens are regenerated to find the next
169 syntax. After each transformation, tokens are regenerated to find the next
170 piece of special syntax.
170 piece of special syntax.
171
171
172 Subclasses need to implement one class method (find)
172 Subclasses need to implement one class method (find)
173 and one regular method (transform).
173 and one regular method (transform).
174
174
175 The priority attribute can select which transformation to apply if multiple
175 The priority attribute can select which transformation to apply if multiple
176 transformers match in the same place. Lower numbers have higher priority.
176 transformers match in the same place. Lower numbers have higher priority.
177 This allows "%magic?" to be turned into a help call rather than a magic call.
177 This allows "%magic?" to be turned into a help call rather than a magic call.
178 """
178 """
179 # Lower numbers -> higher priority (for matches in the same location)
179 # Lower numbers -> higher priority (for matches in the same location)
180 priority = 10
180 priority = 10
181
181
182 def sortby(self):
182 def sortby(self):
183 return self.start_line, self.start_col, self.priority
183 return self.start_line, self.start_col, self.priority
184
184
185 def __init__(self, start):
185 def __init__(self, start):
186 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
186 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
187 self.start_col = start[1]
187 self.start_col = start[1]
188
188
189 @classmethod
189 @classmethod
190 def find(cls, tokens_by_line):
190 def find(cls, tokens_by_line):
191 """Find one instance of special syntax in the provided tokens.
191 """Find one instance of special syntax in the provided tokens.
192
192
193 Tokens are grouped into logical lines for convenience,
193 Tokens are grouped into logical lines for convenience,
194 so it is easy to e.g. look at the first token of each line.
194 so it is easy to e.g. look at the first token of each line.
195 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
195 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
196
196
197 This should return an instance of its class, pointing to the start
197 This should return an instance of its class, pointing to the start
198 position it has found, or None if it found no match.
198 position it has found, or None if it found no match.
199 """
199 """
200 raise NotImplementedError
200 raise NotImplementedError
201
201
202 def transform(self, lines: List[str]):
202 def transform(self, lines: List[str]):
203 """Transform one instance of special syntax found by ``find()``
203 """Transform one instance of special syntax found by ``find()``
204
204
205 Takes a list of strings representing physical lines,
205 Takes a list of strings representing physical lines,
206 returns a similar list of transformed lines.
206 returns a similar list of transformed lines.
207 """
207 """
208 raise NotImplementedError
208 raise NotImplementedError
209
209
210 class MagicAssign(TokenTransformBase):
210 class MagicAssign(TokenTransformBase):
211 """Transformer for assignments from magics (a = %foo)"""
211 """Transformer for assignments from magics (a = %foo)"""
212 @classmethod
212 @classmethod
213 def find(cls, tokens_by_line):
213 def find(cls, tokens_by_line):
214 """Find the first magic assignment (a = %foo) in the cell.
214 """Find the first magic assignment (a = %foo) in the cell.
215 """
215 """
216 for line in tokens_by_line:
216 for line in tokens_by_line:
217 assign_ix = _find_assign_op(line)
217 assign_ix = _find_assign_op(line)
218 if (assign_ix is not None) \
218 if (assign_ix is not None) \
219 and (len(line) >= assign_ix + 2) \
219 and (len(line) >= assign_ix + 2) \
220 and (line[assign_ix+1].string == '%') \
220 and (line[assign_ix+1].string == '%') \
221 and (line[assign_ix+2].type == tokenize.NAME):
221 and (line[assign_ix+2].type == tokenize.NAME):
222 return cls(line[assign_ix+1].start)
222 return cls(line[assign_ix+1].start)
223
223
224 def transform(self, lines: List[str]):
224 def transform(self, lines: List[str]):
225 """Transform a magic assignment found by the ``find()`` classmethod.
225 """Transform a magic assignment found by the ``find()`` classmethod.
226 """
226 """
227 start_line, start_col = self.start_line, self.start_col
227 start_line, start_col = self.start_line, self.start_col
228 lhs = lines[start_line][:start_col]
228 lhs = lines[start_line][:start_col]
229 end_line = find_end_of_continued_line(lines, start_line)
229 end_line = find_end_of_continued_line(lines, start_line)
230 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
230 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
231 assert rhs.startswith('%'), rhs
231 assert rhs.startswith('%'), rhs
232 magic_name, _, args = rhs[1:].partition(' ')
232 magic_name, _, args = rhs[1:].partition(' ')
233
233
234 lines_before = lines[:start_line]
234 lines_before = lines[:start_line]
235 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
235 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
236 new_line = lhs + call + '\n'
236 new_line = lhs + call + '\n'
237 lines_after = lines[end_line+1:]
237 lines_after = lines[end_line+1:]
238
238
239 return lines_before + [new_line] + lines_after
239 return lines_before + [new_line] + lines_after
240
240
241
241
242 class SystemAssign(TokenTransformBase):
242 class SystemAssign(TokenTransformBase):
243 """Transformer for assignments from system commands (a = !foo)"""
243 """Transformer for assignments from system commands (a = !foo)"""
244 @classmethod
244 @classmethod
245 def find(cls, tokens_by_line):
245 def find(cls, tokens_by_line):
246 """Find the first system assignment (a = !foo) in the cell.
246 """Find the first system assignment (a = !foo) in the cell.
247 """
247 """
248 for line in tokens_by_line:
248 for line in tokens_by_line:
249 assign_ix = _find_assign_op(line)
249 assign_ix = _find_assign_op(line)
250 if (assign_ix is not None) \
250 if (assign_ix is not None) \
251 and not line[assign_ix].line.strip().startswith('=') \
251 and not line[assign_ix].line.strip().startswith('=') \
252 and (len(line) >= assign_ix + 2) \
252 and (len(line) >= assign_ix + 2) \
253 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
253 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
254 ix = assign_ix + 1
254 ix = assign_ix + 1
255
255
256 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
256 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
257 if line[ix].string == '!':
257 if line[ix].string == '!':
258 return cls(line[ix].start)
258 return cls(line[ix].start)
259 elif not line[ix].string.isspace():
259 elif not line[ix].string.isspace():
260 break
260 break
261 ix += 1
261 ix += 1
262
262
263 def transform(self, lines: List[str]):
263 def transform(self, lines: List[str]):
264 """Transform a system assignment found by the ``find()`` classmethod.
264 """Transform a system assignment found by the ``find()`` classmethod.
265 """
265 """
266 start_line, start_col = self.start_line, self.start_col
266 start_line, start_col = self.start_line, self.start_col
267
267
268 lhs = lines[start_line][:start_col]
268 lhs = lines[start_line][:start_col]
269 end_line = find_end_of_continued_line(lines, start_line)
269 end_line = find_end_of_continued_line(lines, start_line)
270 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
270 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
271 assert rhs.startswith('!'), rhs
271 assert rhs.startswith('!'), rhs
272 cmd = rhs[1:]
272 cmd = rhs[1:]
273
273
274 lines_before = lines[:start_line]
274 lines_before = lines[:start_line]
275 call = "get_ipython().getoutput({!r})".format(cmd)
275 call = "get_ipython().getoutput({!r})".format(cmd)
276 new_line = lhs + call + '\n'
276 new_line = lhs + call + '\n'
277 lines_after = lines[end_line + 1:]
277 lines_after = lines[end_line + 1:]
278
278
279 return lines_before + [new_line] + lines_after
279 return lines_before + [new_line] + lines_after
280
280
281 # The escape sequences that define the syntax transformations IPython will
281 # The escape sequences that define the syntax transformations IPython will
282 # apply to user input. These can NOT be just changed here: many regular
282 # apply to user input. These can NOT be just changed here: many regular
283 # expressions and other parts of the code may use their hardcoded values, and
283 # expressions and other parts of the code may use their hardcoded values, and
284 # for all intents and purposes they constitute the 'IPython syntax', so they
284 # for all intents and purposes they constitute the 'IPython syntax', so they
285 # should be considered fixed.
285 # should be considered fixed.
286
286
287 ESC_SHELL = '!' # Send line to underlying system shell
287 ESC_SHELL = '!' # Send line to underlying system shell
288 ESC_SH_CAP = '!!' # Send line to system shell and capture output
288 ESC_SH_CAP = '!!' # Send line to system shell and capture output
289 ESC_HELP = '?' # Find information about object
289 ESC_HELP = '?' # Find information about object
290 ESC_HELP2 = '??' # Find extra-detailed information about object
290 ESC_HELP2 = '??' # Find extra-detailed information about object
291 ESC_MAGIC = '%' # Call magic function
291 ESC_MAGIC = '%' # Call magic function
292 ESC_MAGIC2 = '%%' # Call cell-magic function
292 ESC_MAGIC2 = '%%' # Call cell-magic function
293 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
293 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
294 ESC_QUOTE2 = ';' # Quote all args as a single string, call
294 ESC_QUOTE2 = ';' # Quote all args as a single string, call
295 ESC_PAREN = '/' # Call first argument with rest of line as arguments
295 ESC_PAREN = '/' # Call first argument with rest of line as arguments
296
296
297 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
297 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
298 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
298 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
299
299
300 def _make_help_call(target, esc, next_input=None):
300 def _make_help_call(target, esc, next_input=None):
301 """Prepares a pinfo(2)/psearch call from a target name and the escape
301 """Prepares a pinfo(2)/psearch call from a target name and the escape
302 (i.e. ? or ??)"""
302 (i.e. ? or ??)"""
303 method = 'pinfo2' if esc == '??' \
303 method = 'pinfo2' if esc == '??' \
304 else 'psearch' if '*' in target \
304 else 'psearch' if '*' in target \
305 else 'pinfo'
305 else 'pinfo'
306 arg = " ".join([method, target])
306 arg = " ".join([method, target])
307 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
307 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
308 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
308 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
309 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
309 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
310 if next_input is None:
310 if next_input is None:
311 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
311 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
312 else:
312 else:
313 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
313 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
314 (next_input, t_magic_name, t_magic_arg_s)
314 (next_input, t_magic_name, t_magic_arg_s)
315
315
316 def _tr_help(content):
316 def _tr_help(content):
317 """Translate lines escaped with: ?
317 """Translate lines escaped with: ?
318
318
319 A naked help line should fire the intro help screen (shell.show_usage())
319 A naked help line should fire the intro help screen (shell.show_usage())
320 """
320 """
321 if not content:
321 if not content:
322 return 'get_ipython().show_usage()'
322 return 'get_ipython().show_usage()'
323
323
324 return _make_help_call(content, '?')
324 return _make_help_call(content, '?')
325
325
326 def _tr_help2(content):
326 def _tr_help2(content):
327 """Translate lines escaped with: ??
327 """Translate lines escaped with: ??
328
328
329 A naked help line should fire the intro help screen (shell.show_usage())
329 A naked help line should fire the intro help screen (shell.show_usage())
330 """
330 """
331 if not content:
331 if not content:
332 return 'get_ipython().show_usage()'
332 return 'get_ipython().show_usage()'
333
333
334 return _make_help_call(content, '??')
334 return _make_help_call(content, '??')
335
335
336 def _tr_magic(content):
336 def _tr_magic(content):
337 "Translate lines escaped with a percent sign: %"
337 "Translate lines escaped with a percent sign: %"
338 name, _, args = content.partition(' ')
338 name, _, args = content.partition(' ')
339 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
339 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
340
340
341 def _tr_quote(content):
341 def _tr_quote(content):
342 "Translate lines escaped with a comma: ,"
342 "Translate lines escaped with a comma: ,"
343 name, _, args = content.partition(' ')
343 name, _, args = content.partition(' ')
344 return '%s("%s")' % (name, '", "'.join(args.split()) )
344 return '%s("%s")' % (name, '", "'.join(args.split()) )
345
345
346 def _tr_quote2(content):
346 def _tr_quote2(content):
347 "Translate lines escaped with a semicolon: ;"
347 "Translate lines escaped with a semicolon: ;"
348 name, _, args = content.partition(' ')
348 name, _, args = content.partition(' ')
349 return '%s("%s")' % (name, args)
349 return '%s("%s")' % (name, args)
350
350
351 def _tr_paren(content):
351 def _tr_paren(content):
352 "Translate lines escaped with a slash: /"
352 "Translate lines escaped with a slash: /"
353 name, _, args = content.partition(' ')
353 name, _, args = content.partition(' ')
354 return '%s(%s)' % (name, ", ".join(args.split()))
354 return '%s(%s)' % (name, ", ".join(args.split()))
355
355
356 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
356 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
357 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
357 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
358 ESC_HELP : _tr_help,
358 ESC_HELP : _tr_help,
359 ESC_HELP2 : _tr_help2,
359 ESC_HELP2 : _tr_help2,
360 ESC_MAGIC : _tr_magic,
360 ESC_MAGIC : _tr_magic,
361 ESC_QUOTE : _tr_quote,
361 ESC_QUOTE : _tr_quote,
362 ESC_QUOTE2 : _tr_quote2,
362 ESC_QUOTE2 : _tr_quote2,
363 ESC_PAREN : _tr_paren }
363 ESC_PAREN : _tr_paren }
364
364
365 class EscapedCommand(TokenTransformBase):
365 class EscapedCommand(TokenTransformBase):
366 """Transformer for escaped commands like %foo, !foo, or /foo"""
366 """Transformer for escaped commands like %foo, !foo, or /foo"""
367 @classmethod
367 @classmethod
368 def find(cls, tokens_by_line):
368 def find(cls, tokens_by_line):
369 """Find the first escaped command (%foo, !foo, etc.) in the cell.
369 """Find the first escaped command (%foo, !foo, etc.) in the cell.
370 """
370 """
371 for line in tokens_by_line:
371 for line in tokens_by_line:
372 if not line:
372 if not line:
373 continue
373 continue
374 ix = 0
374 ix = 0
375 ll = len(line)
375 ll = len(line)
376 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
376 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
377 ix += 1
377 ix += 1
378 if ix >= ll:
378 if ix >= ll:
379 continue
379 continue
380 if line[ix].string in ESCAPE_SINGLES:
380 if line[ix].string in ESCAPE_SINGLES:
381 return cls(line[ix].start)
381 return cls(line[ix].start)
382
382
383 def transform(self, lines):
383 def transform(self, lines):
384 """Transform an escaped line found by the ``find()`` classmethod.
384 """Transform an escaped line found by the ``find()`` classmethod.
385 """
385 """
386 start_line, start_col = self.start_line, self.start_col
386 start_line, start_col = self.start_line, self.start_col
387
387
388 indent = lines[start_line][:start_col]
388 indent = lines[start_line][:start_col]
389 end_line = find_end_of_continued_line(lines, start_line)
389 end_line = find_end_of_continued_line(lines, start_line)
390 line = assemble_continued_line(lines, (start_line, start_col), end_line)
390 line = assemble_continued_line(lines, (start_line, start_col), end_line)
391
391
392 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
392 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
393 escape, content = line[:2], line[2:]
393 escape, content = line[:2], line[2:]
394 else:
394 else:
395 escape, content = line[:1], line[1:]
395 escape, content = line[:1], line[1:]
396
396
397 if escape in tr:
397 if escape in tr:
398 call = tr[escape](content)
398 call = tr[escape](content)
399 else:
399 else:
400 call = ''
400 call = ''
401
401
402 lines_before = lines[:start_line]
402 lines_before = lines[:start_line]
403 new_line = indent + call + '\n'
403 new_line = indent + call + '\n'
404 lines_after = lines[end_line + 1:]
404 lines_after = lines[end_line + 1:]
405
405
406 return lines_before + [new_line] + lines_after
406 return lines_before + [new_line] + lines_after
407
407
408 _help_end_re = re.compile(r"""(%{0,2}
408 _help_end_re = re.compile(r"""(%{0,2}
409 (?!\d)[\w*]+ # Variable name
409 (?!\d)[\w*]+ # Variable name
410 (\.(?!\d)[\w*]+)* # .etc.etc
410 (\.(?!\d)[\w*]+)* # .etc.etc
411 )
411 )
412 (\?\??)$ # ? or ??
412 (\?\??)$ # ? or ??
413 """,
413 """,
414 re.VERBOSE)
414 re.VERBOSE)
415
415
416 class HelpEnd(TokenTransformBase):
416 class HelpEnd(TokenTransformBase):
417 """Transformer for help syntax: obj? and obj??"""
417 """Transformer for help syntax: obj? and obj??"""
418 # This needs to be higher priority (lower number) than EscapedCommand so
418 # This needs to be higher priority (lower number) than EscapedCommand so
419 # that inspecting magics (%foo?) works.
419 # that inspecting magics (%foo?) works.
420 priority = 5
420 priority = 5
421
421
422 def __init__(self, start, q_locn):
422 def __init__(self, start, q_locn):
423 super().__init__(start)
423 super().__init__(start)
424 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
424 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
425 self.q_col = q_locn[1]
425 self.q_col = q_locn[1]
426
426
427 @classmethod
427 @classmethod
428 def find(cls, tokens_by_line):
428 def find(cls, tokens_by_line):
429 """Find the first help command (foo?) in the cell.
429 """Find the first help command (foo?) in the cell.
430 """
430 """
431 for line in tokens_by_line:
431 for line in tokens_by_line:
432 # Last token is NEWLINE; look at last but one
432 # Last token is NEWLINE; look at last but one
433 if len(line) > 2 and line[-2].string == '?':
433 if len(line) > 2 and line[-2].string == '?':
434 # Find the first token that's not INDENT/DEDENT
434 # Find the first token that's not INDENT/DEDENT
435 ix = 0
435 ix = 0
436 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
436 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
437 ix += 1
437 ix += 1
438 return cls(line[ix].start, line[-2].start)
438 return cls(line[ix].start, line[-2].start)
439
439
440 def transform(self, lines):
440 def transform(self, lines):
441 """Transform a help command found by the ``find()`` classmethod.
441 """Transform a help command found by the ``find()`` classmethod.
442 """
442 """
443 piece = ''.join(lines[self.start_line:self.q_line+1])
443 piece = ''.join(lines[self.start_line:self.q_line+1])
444 indent, content = piece[:self.start_col], piece[self.start_col:]
444 indent, content = piece[:self.start_col], piece[self.start_col:]
445 lines_before = lines[:self.start_line]
445 lines_before = lines[:self.start_line]
446 lines_after = lines[self.q_line + 1:]
446 lines_after = lines[self.q_line + 1:]
447
447
448 m = _help_end_re.search(content)
448 m = _help_end_re.search(content)
449 if not m:
449 if not m:
450 raise SyntaxError(content)
450 raise SyntaxError(content)
451 assert m is not None, content
451 assert m is not None, content
452 target = m.group(1)
452 target = m.group(1)
453 esc = m.group(3)
453 esc = m.group(3)
454
454
455 # If we're mid-command, put it back on the next prompt for the user.
455 # If we're mid-command, put it back on the next prompt for the user.
456 next_input = None
456 next_input = None
457 if (not lines_before) and (not lines_after) \
457 if (not lines_before) and (not lines_after) \
458 and content.strip() != m.group(0):
458 and content.strip() != m.group(0):
459 next_input = content.rstrip('?\n')
459 next_input = content.rstrip('?\n')
460
460
461 call = _make_help_call(target, esc, next_input=next_input)
461 call = _make_help_call(target, esc, next_input=next_input)
462 new_line = indent + call + '\n'
462 new_line = indent + call + '\n'
463
463
464 return lines_before + [new_line] + lines_after
464 return lines_before + [new_line] + lines_after
465
465
466 def make_tokens_by_line(lines:List[str]):
466 def make_tokens_by_line(lines:List[str]):
467 """Tokenize a series of lines and group tokens by line.
467 """Tokenize a series of lines and group tokens by line.
468
468
469 The tokens for a multiline Python string or expression are grouped as one
469 The tokens for a multiline Python string or expression are grouped as one
470 line. All lines except the last lines should keep their line ending ('\\n',
470 line. All lines except the last lines should keep their line ending ('\\n',
471 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
471 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
472 for example when passing block of text to this function.
472 for example when passing block of text to this function.
473
473
474 """
474 """
475 # NL tokens are used inside multiline expressions, but also after blank
475 # NL tokens are used inside multiline expressions, but also after blank
476 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
476 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
477 # We want to group the former case together but split the latter, so we
477 # We want to group the former case together but split the latter, so we
478 # track parentheses level, similar to the internals of tokenize.
478 # track parentheses level, similar to the internals of tokenize.
479
479
480 # reexported from token on 3.7+
480 # reexported from token on 3.7+
481 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore
481 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore
482 tokens_by_line:List[List[Any]] = [[]]
482 tokens_by_line:List[List[Any]] = [[]]
483 if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')):
483 if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')):
484 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")
484 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")
485 parenlev = 0
485 parenlev = 0
486 try:
486 try:
487 for token in tokenize.generate_tokens(iter(lines).__next__):
487 for token in tokenize.generate_tokens(iter(lines).__next__):
488 tokens_by_line[-1].append(token)
488 tokens_by_line[-1].append(token)
489 if (token.type == NEWLINE) \
489 if (token.type == NEWLINE) \
490 or ((token.type == NL) and (parenlev <= 0)):
490 or ((token.type == NL) and (parenlev <= 0)):
491 tokens_by_line.append([])
491 tokens_by_line.append([])
492 elif token.string in {'(', '[', '{'}:
492 elif token.string in {'(', '[', '{'}:
493 parenlev += 1
493 parenlev += 1
494 elif token.string in {')', ']', '}'}:
494 elif token.string in {')', ']', '}'}:
495 if parenlev > 0:
495 if parenlev > 0:
496 parenlev -= 1
496 parenlev -= 1
497 except tokenize.TokenError:
497 except tokenize.TokenError:
498 # Input ended in a multiline string or expression. That's OK for us.
498 # Input ended in a multiline string or expression. That's OK for us.
499 pass
499 pass
500
500
501
501
502 if not tokens_by_line[-1]:
502 if not tokens_by_line[-1]:
503 tokens_by_line.pop()
503 tokens_by_line.pop()
504
504
505
505
506 return tokens_by_line
506 return tokens_by_line
507
507
508 def show_linewise_tokens(s: str):
508 def show_linewise_tokens(s: str):
509 """For investigation and debugging"""
509 """For investigation and debugging"""
510 if not s.endswith('\n'):
510 if not s.endswith('\n'):
511 s += '\n'
511 s += '\n'
512 lines = s.splitlines(keepends=True)
512 lines = s.splitlines(keepends=True)
513 for line in make_tokens_by_line(lines):
513 for line in make_tokens_by_line(lines):
514 print("Line -------")
514 print("Line -------")
515 for tokinfo in line:
515 for tokinfo in line:
516 print(" ", tokinfo)
516 print(" ", tokinfo)
517
517
518 # Arbitrary limit to prevent getting stuck in infinite loops
518 # Arbitrary limit to prevent getting stuck in infinite loops
519 TRANSFORM_LOOP_LIMIT = 500
519 TRANSFORM_LOOP_LIMIT = 500
520
520
521 class TransformerManager:
521 class TransformerManager:
522 """Applies various transformations to a cell or code block.
522 """Applies various transformations to a cell or code block.
523
523
524 The key methods for external use are ``transform_cell()``
524 The key methods for external use are ``transform_cell()``
525 and ``check_complete()``.
525 and ``check_complete()``.
526 """
526 """
527 def __init__(self):
527 def __init__(self):
528 self.cleanup_transforms = [
528 self.cleanup_transforms = [
529 leading_empty_lines,
529 leading_empty_lines,
530 leading_indent,
530 leading_indent,
531 classic_prompt,
531 classic_prompt,
532 ipython_prompt,
532 ipython_prompt,
533 ]
533 ]
534 self.line_transforms = [
534 self.line_transforms = [
535 cell_magic,
535 cell_magic,
536 ]
536 ]
537 self.token_transformers = [
537 self.token_transformers = [
538 MagicAssign,
538 MagicAssign,
539 SystemAssign,
539 SystemAssign,
540 EscapedCommand,
540 EscapedCommand,
541 HelpEnd,
541 HelpEnd,
542 ]
542 ]
543
543
544 def do_one_token_transform(self, lines):
544 def do_one_token_transform(self, lines):
545 """Find and run the transform earliest in the code.
545 """Find and run the transform earliest in the code.
546
546
547 Returns (changed, lines).
547 Returns (changed, lines).
548
548
549 This method is called repeatedly until changed is False, indicating
549 This method is called repeatedly until changed is False, indicating
550 that all available transformations are complete.
550 that all available transformations are complete.
551
551
552 The tokens following IPython special syntax might not be valid, so
552 The tokens following IPython special syntax might not be valid, so
553 the transformed code is retokenised every time to identify the next
553 the transformed code is retokenised every time to identify the next
554 piece of special syntax. Hopefully long code cells are mostly valid
554 piece of special syntax. Hopefully long code cells are mostly valid
555 Python, not using lots of IPython special syntax, so this shouldn't be
555 Python, not using lots of IPython special syntax, so this shouldn't be
556 a performance issue.
556 a performance issue.
557 """
557 """
558 tokens_by_line = make_tokens_by_line(lines)
558 tokens_by_line = make_tokens_by_line(lines)
559 candidates = []
559 candidates = []
560 for transformer_cls in self.token_transformers:
560 for transformer_cls in self.token_transformers:
561 transformer = transformer_cls.find(tokens_by_line)
561 transformer = transformer_cls.find(tokens_by_line)
562 if transformer:
562 if transformer:
563 candidates.append(transformer)
563 candidates.append(transformer)
564
564
565 if not candidates:
565 if not candidates:
566 # Nothing to transform
566 # Nothing to transform
567 return False, lines
567 return False, lines
568 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
568 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
569 for transformer in ordered_transformers:
569 for transformer in ordered_transformers:
570 try:
570 try:
571 return True, transformer.transform(lines)
571 return True, transformer.transform(lines)
572 except SyntaxError:
572 except SyntaxError:
573 pass
573 pass
574 return False, lines
574 return False, lines
575
575
576 def do_token_transforms(self, lines):
576 def do_token_transforms(self, lines):
577 for _ in range(TRANSFORM_LOOP_LIMIT):
577 for _ in range(TRANSFORM_LOOP_LIMIT):
578 changed, lines = self.do_one_token_transform(lines)
578 changed, lines = self.do_one_token_transform(lines)
579 if not changed:
579 if not changed:
580 return lines
580 return lines
581
581
582 raise RuntimeError("Input transformation still changing after "
582 raise RuntimeError("Input transformation still changing after "
583 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
583 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
584
584
585 def transform_cell(self, cell: str) -> str:
585 def transform_cell(self, cell: str) -> str:
586 """Transforms a cell of input code"""
586 """Transforms a cell of input code"""
587 if not cell.endswith('\n'):
587 if not cell.endswith('\n'):
588 cell += '\n' # Ensure the cell has a trailing newline
588 cell += '\n' # Ensure the cell has a trailing newline
589 lines = cell.splitlines(keepends=True)
589 lines = cell.splitlines(keepends=True)
590 for transform in self.cleanup_transforms + self.line_transforms:
590 for transform in self.cleanup_transforms + self.line_transforms:
591 lines = transform(lines)
591 lines = transform(lines)
592
592
593 lines = self.do_token_transforms(lines)
593 lines = self.do_token_transforms(lines)
594 return ''.join(lines)
594 return ''.join(lines)
595
595
596 def check_complete(self, cell: str):
596 def check_complete(self, cell: str):
597 """Return whether a block of code is ready to execute, or should be continued
597 """Return whether a block of code is ready to execute, or should be continued
598
598
599 Parameters
599 Parameters
600 ----------
600 ----------
601 source : string
601 source : string
602 Python input code, which can be multiline.
602 Python input code, which can be multiline.
603
603
604 Returns
604 Returns
605 -------
605 -------
606 status : str
606 status : str
607 One of 'complete', 'incomplete', or 'invalid' if source is not a
607 One of 'complete', 'incomplete', or 'invalid' if source is not a
608 prefix of valid code.
608 prefix of valid code.
609 indent_spaces : int or None
609 indent_spaces : int or None
610 The number of spaces by which to indent the next line of code. If
610 The number of spaces by which to indent the next line of code. If
611 status is not 'incomplete', this is None.
611 status is not 'incomplete', this is None.
612 """
612 """
613 # Remember if the lines ends in a new line.
613 # Remember if the lines ends in a new line.
614 ends_with_newline = False
614 ends_with_newline = False
615 for character in reversed(cell):
615 for character in reversed(cell):
616 if character == '\n':
616 if character == '\n':
617 ends_with_newline = True
617 ends_with_newline = True
618 break
618 break
619 elif character.strip():
619 elif character.strip():
620 break
620 break
621 else:
621 else:
622 continue
622 continue
623
623
624 if not ends_with_newline:
624 if not ends_with_newline:
625 # Append an newline for consistent tokenization
625 # Append an newline for consistent tokenization
626 # See https://bugs.python.org/issue33899
626 # See https://bugs.python.org/issue33899
627 cell += '\n'
627 cell += '\n'
628
628
629 lines = cell.splitlines(keepends=True)
629 lines = cell.splitlines(keepends=True)
630
630
631 if not lines:
631 if not lines:
632 return 'complete', None
632 return 'complete', None
633
633
634 if lines[-1].endswith('\\'):
634 if lines[-1].endswith('\\'):
635 # Explicit backslash continuation
635 # Explicit backslash continuation
636 return 'incomplete', find_last_indent(lines)
636 return 'incomplete', find_last_indent(lines)
637
637
638 try:
638 try:
639 for transform in self.cleanup_transforms:
639 for transform in self.cleanup_transforms:
640 lines = transform(lines)
640 if not getattr(transform, 'has_side_effects', False):
641 lines = transform(lines)
641 except SyntaxError:
642 except SyntaxError:
642 return 'invalid', None
643 return 'invalid', None
643
644
644 if lines[0].startswith('%%'):
645 if lines[0].startswith('%%'):
645 # Special case for cell magics - completion marked by blank line
646 # Special case for cell magics - completion marked by blank line
646 if lines[-1].strip():
647 if lines[-1].strip():
647 return 'incomplete', find_last_indent(lines)
648 return 'incomplete', find_last_indent(lines)
648 else:
649 else:
649 return 'complete', None
650 return 'complete', None
650
651
651 try:
652 try:
652 for transform in self.line_transforms:
653 for transform in self.line_transforms:
653 lines = transform(lines)
654 if not getattr(transform, 'has_side_effects', False):
655 lines = transform(lines)
654 lines = self.do_token_transforms(lines)
656 lines = self.do_token_transforms(lines)
655 except SyntaxError:
657 except SyntaxError:
656 return 'invalid', None
658 return 'invalid', None
657
659
658 tokens_by_line = make_tokens_by_line(lines)
660 tokens_by_line = make_tokens_by_line(lines)
659
661
660 if not tokens_by_line:
662 if not tokens_by_line:
661 return 'incomplete', find_last_indent(lines)
663 return 'incomplete', find_last_indent(lines)
662
664
663 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
665 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
664 # We're in a multiline string or expression
666 # We're in a multiline string or expression
665 return 'incomplete', find_last_indent(lines)
667 return 'incomplete', find_last_indent(lines)
666
668
667 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER} # type: ignore
669 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER} # type: ignore
668
670
669 # Pop the last line which only contains DEDENTs and ENDMARKER
671 # Pop the last line which only contains DEDENTs and ENDMARKER
670 last_token_line = None
672 last_token_line = None
671 if {t.type for t in tokens_by_line[-1]} in [
673 if {t.type for t in tokens_by_line[-1]} in [
672 {tokenize.DEDENT, tokenize.ENDMARKER},
674 {tokenize.DEDENT, tokenize.ENDMARKER},
673 {tokenize.ENDMARKER}
675 {tokenize.ENDMARKER}
674 ] and len(tokens_by_line) > 1:
676 ] and len(tokens_by_line) > 1:
675 last_token_line = tokens_by_line.pop()
677 last_token_line = tokens_by_line.pop()
676
678
677 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
679 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
678 tokens_by_line[-1].pop()
680 tokens_by_line[-1].pop()
679
681
680 if not tokens_by_line[-1]:
682 if not tokens_by_line[-1]:
681 return 'incomplete', find_last_indent(lines)
683 return 'incomplete', find_last_indent(lines)
682
684
683 if tokens_by_line[-1][-1].string == ':':
685 if tokens_by_line[-1][-1].string == ':':
684 # The last line starts a block (e.g. 'if foo:')
686 # The last line starts a block (e.g. 'if foo:')
685 ix = 0
687 ix = 0
686 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
688 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
687 ix += 1
689 ix += 1
688
690
689 indent = tokens_by_line[-1][ix].start[1]
691 indent = tokens_by_line[-1][ix].start[1]
690 return 'incomplete', indent + 4
692 return 'incomplete', indent + 4
691
693
692 if tokens_by_line[-1][0].line.endswith('\\'):
694 if tokens_by_line[-1][0].line.endswith('\\'):
693 return 'incomplete', None
695 return 'incomplete', None
694
696
695 # At this point, our checks think the code is complete (or invalid).
697 # At this point, our checks think the code is complete (or invalid).
696 # We'll use codeop.compile_command to check this with the real parser
698 # We'll use codeop.compile_command to check this with the real parser
697 try:
699 try:
698 with warnings.catch_warnings():
700 with warnings.catch_warnings():
699 warnings.simplefilter('error', SyntaxWarning)
701 warnings.simplefilter('error', SyntaxWarning)
700 res = compile_command(''.join(lines), symbol='exec')
702 res = compile_command(''.join(lines), symbol='exec')
701 except (SyntaxError, OverflowError, ValueError, TypeError,
703 except (SyntaxError, OverflowError, ValueError, TypeError,
702 MemoryError, SyntaxWarning):
704 MemoryError, SyntaxWarning):
703 return 'invalid', None
705 return 'invalid', None
704 else:
706 else:
705 if res is None:
707 if res is None:
706 return 'incomplete', find_last_indent(lines)
708 return 'incomplete', find_last_indent(lines)
707
709
708 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
710 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
709 if ends_with_newline:
711 if ends_with_newline:
710 return 'complete', None
712 return 'complete', None
711 return 'incomplete', find_last_indent(lines)
713 return 'incomplete', find_last_indent(lines)
712
714
713 # If there's a blank line at the end, assume we're ready to execute
715 # If there's a blank line at the end, assume we're ready to execute
714 if not lines[-1].strip():
716 if not lines[-1].strip():
715 return 'complete', None
717 return 'complete', None
716
718
717 return 'complete', None
719 return 'complete', None
718
720
719
721
720 def find_last_indent(lines):
722 def find_last_indent(lines):
721 m = _indent_re.match(lines[-1])
723 m = _indent_re.match(lines[-1])
722 if not m:
724 if not m:
723 return 0
725 return 0
724 return len(m.group(0).replace('\t', ' '*4))
726 return len(m.group(0).replace('\t', ' '*4))
@@ -1,292 +1,326 b''
1 """Tests for the token-based transformers in IPython.core.inputtransformer2
1 """Tests for the token-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_line for tests for line-based
4 more complex. See test_inputtransformer2_line for tests for line-based
5 transformations.
5 transformations.
6 """
6 """
7 import nose.tools as nt
7 import nose.tools as nt
8 import string
8 import string
9
9
10 from IPython.core import inputtransformer2 as ipt2
10 from IPython.core import inputtransformer2 as ipt2
11 from IPython.core.inputtransformer2 import make_tokens_by_line, _find_assign_op
11 from IPython.core.inputtransformer2 import make_tokens_by_line, _find_assign_op
12
12
13 from textwrap import dedent
13 from textwrap import dedent
14
14
15 MULTILINE_MAGIC = ("""\
15 MULTILINE_MAGIC = ("""\
16 a = f()
16 a = f()
17 %foo \\
17 %foo \\
18 bar
18 bar
19 g()
19 g()
20 """.splitlines(keepends=True), (2, 0), """\
20 """.splitlines(keepends=True), (2, 0), """\
21 a = f()
21 a = f()
22 get_ipython().run_line_magic('foo', ' bar')
22 get_ipython().run_line_magic('foo', ' bar')
23 g()
23 g()
24 """.splitlines(keepends=True))
24 """.splitlines(keepends=True))
25
25
26 INDENTED_MAGIC = ("""\
26 INDENTED_MAGIC = ("""\
27 for a in range(5):
27 for a in range(5):
28 %ls
28 %ls
29 """.splitlines(keepends=True), (2, 4), """\
29 """.splitlines(keepends=True), (2, 4), """\
30 for a in range(5):
30 for a in range(5):
31 get_ipython().run_line_magic('ls', '')
31 get_ipython().run_line_magic('ls', '')
32 """.splitlines(keepends=True))
32 """.splitlines(keepends=True))
33
33
34 MULTILINE_MAGIC_ASSIGN = ("""\
34 MULTILINE_MAGIC_ASSIGN = ("""\
35 a = f()
35 a = f()
36 b = %foo \\
36 b = %foo \\
37 bar
37 bar
38 g()
38 g()
39 """.splitlines(keepends=True), (2, 4), """\
39 """.splitlines(keepends=True), (2, 4), """\
40 a = f()
40 a = f()
41 b = get_ipython().run_line_magic('foo', ' bar')
41 b = get_ipython().run_line_magic('foo', ' bar')
42 g()
42 g()
43 """.splitlines(keepends=True))
43 """.splitlines(keepends=True))
44
44
45 MULTILINE_SYSTEM_ASSIGN = ("""\
45 MULTILINE_SYSTEM_ASSIGN = ("""\
46 a = f()
46 a = f()
47 b = !foo \\
47 b = !foo \\
48 bar
48 bar
49 g()
49 g()
50 """.splitlines(keepends=True), (2, 4), """\
50 """.splitlines(keepends=True), (2, 4), """\
51 a = f()
51 a = f()
52 b = get_ipython().getoutput('foo bar')
52 b = get_ipython().getoutput('foo bar')
53 g()
53 g()
54 """.splitlines(keepends=True))
54 """.splitlines(keepends=True))
55
55
56 #####
56 #####
57
57
58 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = ("""\
58 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = ("""\
59 def test():
59 def test():
60 for i in range(1):
60 for i in range(1):
61 print(i)
61 print(i)
62 res =! ls
62 res =! ls
63 """.splitlines(keepends=True), (4, 7), '''\
63 """.splitlines(keepends=True), (4, 7), '''\
64 def test():
64 def test():
65 for i in range(1):
65 for i in range(1):
66 print(i)
66 print(i)
67 res =get_ipython().getoutput(\' ls\')
67 res =get_ipython().getoutput(\' ls\')
68 '''.splitlines(keepends=True))
68 '''.splitlines(keepends=True))
69
69
70 ######
70 ######
71
71
72 AUTOCALL_QUOTE = (
72 AUTOCALL_QUOTE = (
73 [",f 1 2 3\n"], (1, 0),
73 [",f 1 2 3\n"], (1, 0),
74 ['f("1", "2", "3")\n']
74 ['f("1", "2", "3")\n']
75 )
75 )
76
76
77 AUTOCALL_QUOTE2 = (
77 AUTOCALL_QUOTE2 = (
78 [";f 1 2 3\n"], (1, 0),
78 [";f 1 2 3\n"], (1, 0),
79 ['f("1 2 3")\n']
79 ['f("1 2 3")\n']
80 )
80 )
81
81
82 AUTOCALL_PAREN = (
82 AUTOCALL_PAREN = (
83 ["/f 1 2 3\n"], (1, 0),
83 ["/f 1 2 3\n"], (1, 0),
84 ['f(1, 2, 3)\n']
84 ['f(1, 2, 3)\n']
85 )
85 )
86
86
87 SIMPLE_HELP = (
87 SIMPLE_HELP = (
88 ["foo?\n"], (1, 0),
88 ["foo?\n"], (1, 0),
89 ["get_ipython().run_line_magic('pinfo', 'foo')\n"]
89 ["get_ipython().run_line_magic('pinfo', 'foo')\n"]
90 )
90 )
91
91
92 DETAILED_HELP = (
92 DETAILED_HELP = (
93 ["foo??\n"], (1, 0),
93 ["foo??\n"], (1, 0),
94 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"]
94 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"]
95 )
95 )
96
96
97 MAGIC_HELP = (
97 MAGIC_HELP = (
98 ["%foo?\n"], (1, 0),
98 ["%foo?\n"], (1, 0),
99 ["get_ipython().run_line_magic('pinfo', '%foo')\n"]
99 ["get_ipython().run_line_magic('pinfo', '%foo')\n"]
100 )
100 )
101
101
102 HELP_IN_EXPR = (
102 HELP_IN_EXPR = (
103 ["a = b + c?\n"], (1, 0),
103 ["a = b + c?\n"], (1, 0),
104 ["get_ipython().set_next_input('a = b + c');"
104 ["get_ipython().set_next_input('a = b + c');"
105 "get_ipython().run_line_magic('pinfo', 'c')\n"]
105 "get_ipython().run_line_magic('pinfo', 'c')\n"]
106 )
106 )
107
107
108 HELP_CONTINUED_LINE = ("""\
108 HELP_CONTINUED_LINE = ("""\
109 a = \\
109 a = \\
110 zip?
110 zip?
111 """.splitlines(keepends=True), (1, 0),
111 """.splitlines(keepends=True), (1, 0),
112 [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
112 [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
113 )
113 )
114
114
115 HELP_MULTILINE = ("""\
115 HELP_MULTILINE = ("""\
116 (a,
116 (a,
117 b) = zip?
117 b) = zip?
118 """.splitlines(keepends=True), (1, 0),
118 """.splitlines(keepends=True), (1, 0),
119 [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
119 [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
120 )
120 )
121
121
122 HELP_UNICODE = (
122 HELP_UNICODE = (
123 ["Ο€.foo?\n"], (1, 0),
123 ["Ο€.foo?\n"], (1, 0),
124 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"]
124 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"]
125 )
125 )
126
126
127
127
128 def null_cleanup_transformer(lines):
128 def null_cleanup_transformer(lines):
129 """
129 """
130 A cleanup transform that returns an empty list.
130 A cleanup transform that returns an empty list.
131 """
131 """
132 return []
132 return []
133
133
134 def check_make_token_by_line_never_ends_empty():
134 def check_make_token_by_line_never_ends_empty():
135 """
135 """
136 Check that not sequence of single or double characters ends up leading to en empty list of tokens
136 Check that not sequence of single or double characters ends up leading to en empty list of tokens
137 """
137 """
138 from string import printable
138 from string import printable
139 for c in printable:
139 for c in printable:
140 nt.assert_not_equal(make_tokens_by_line(c)[-1], [])
140 nt.assert_not_equal(make_tokens_by_line(c)[-1], [])
141 for k in printable:
141 for k in printable:
142 nt.assert_not_equal(make_tokens_by_line(c+k)[-1], [])
142 nt.assert_not_equal(make_tokens_by_line(c+k)[-1], [])
143
143
144 def check_find(transformer, case, match=True):
144 def check_find(transformer, case, match=True):
145 sample, expected_start, _ = case
145 sample, expected_start, _ = case
146 tbl = make_tokens_by_line(sample)
146 tbl = make_tokens_by_line(sample)
147 res = transformer.find(tbl)
147 res = transformer.find(tbl)
148 if match:
148 if match:
149 # start_line is stored 0-indexed, expected values are 1-indexed
149 # start_line is stored 0-indexed, expected values are 1-indexed
150 nt.assert_equal((res.start_line+1, res.start_col), expected_start)
150 nt.assert_equal((res.start_line+1, res.start_col), expected_start)
151 return res
151 return res
152 else:
152 else:
153 nt.assert_is(res, None)
153 nt.assert_is(res, None)
154
154
155 def check_transform(transformer_cls, case):
155 def check_transform(transformer_cls, case):
156 lines, start, expected = case
156 lines, start, expected = case
157 transformer = transformer_cls(start)
157 transformer = transformer_cls(start)
158 nt.assert_equal(transformer.transform(lines), expected)
158 nt.assert_equal(transformer.transform(lines), expected)
159
159
160 def test_continued_line():
160 def test_continued_line():
161 lines = MULTILINE_MAGIC_ASSIGN[0]
161 lines = MULTILINE_MAGIC_ASSIGN[0]
162 nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2)
162 nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2)
163
163
164 nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar")
164 nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar")
165
165
166 def test_find_assign_magic():
166 def test_find_assign_magic():
167 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
167 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
168 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
168 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
169 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
169 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
170
170
171 def test_transform_assign_magic():
171 def test_transform_assign_magic():
172 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
172 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
173
173
174 def test_find_assign_system():
174 def test_find_assign_system():
175 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
175 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
176 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
176 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
177 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
177 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
178 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
178 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
179 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
179 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
180
180
181 def test_transform_assign_system():
181 def test_transform_assign_system():
182 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
182 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
183 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
183 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
184
184
185 def test_find_magic_escape():
185 def test_find_magic_escape():
186 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
186 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
187 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
187 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
188 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
188 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
189
189
190 def test_transform_magic_escape():
190 def test_transform_magic_escape():
191 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
191 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
192 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
192 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
193
193
194 def test_find_autocalls():
194 def test_find_autocalls():
195 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
195 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
196 print("Testing %r" % case[0])
196 print("Testing %r" % case[0])
197 check_find(ipt2.EscapedCommand, case)
197 check_find(ipt2.EscapedCommand, case)
198
198
199 def test_transform_autocall():
199 def test_transform_autocall():
200 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
200 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
201 print("Testing %r" % case[0])
201 print("Testing %r" % case[0])
202 check_transform(ipt2.EscapedCommand, case)
202 check_transform(ipt2.EscapedCommand, case)
203
203
204 def test_find_help():
204 def test_find_help():
205 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
205 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
206 check_find(ipt2.HelpEnd, case)
206 check_find(ipt2.HelpEnd, case)
207
207
208 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
208 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
209 nt.assert_equal(tf.q_line, 1)
209 nt.assert_equal(tf.q_line, 1)
210 nt.assert_equal(tf.q_col, 3)
210 nt.assert_equal(tf.q_col, 3)
211
211
212 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
212 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
213 nt.assert_equal(tf.q_line, 1)
213 nt.assert_equal(tf.q_line, 1)
214 nt.assert_equal(tf.q_col, 8)
214 nt.assert_equal(tf.q_col, 8)
215
215
216 # ? in a comment does not trigger help
216 # ? in a comment does not trigger help
217 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
217 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
218 # Nor in a string
218 # Nor in a string
219 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
219 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
220
220
221 def test_transform_help():
221 def test_transform_help():
222 tf = ipt2.HelpEnd((1, 0), (1, 9))
222 tf = ipt2.HelpEnd((1, 0), (1, 9))
223 nt.assert_equal(tf.transform(HELP_IN_EXPR[0]), HELP_IN_EXPR[2])
223 nt.assert_equal(tf.transform(HELP_IN_EXPR[0]), HELP_IN_EXPR[2])
224
224
225 tf = ipt2.HelpEnd((1, 0), (2, 3))
225 tf = ipt2.HelpEnd((1, 0), (2, 3))
226 nt.assert_equal(tf.transform(HELP_CONTINUED_LINE[0]), HELP_CONTINUED_LINE[2])
226 nt.assert_equal(tf.transform(HELP_CONTINUED_LINE[0]), HELP_CONTINUED_LINE[2])
227
227
228 tf = ipt2.HelpEnd((1, 0), (2, 8))
228 tf = ipt2.HelpEnd((1, 0), (2, 8))
229 nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2])
229 nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2])
230
230
231 tf = ipt2.HelpEnd((1, 0), (1, 0))
231 tf = ipt2.HelpEnd((1, 0), (1, 0))
232 nt.assert_equal(tf.transform(HELP_UNICODE[0]), HELP_UNICODE[2])
232 nt.assert_equal(tf.transform(HELP_UNICODE[0]), HELP_UNICODE[2])
233
233
234 def test_find_assign_op_dedent():
234 def test_find_assign_op_dedent():
235 """
235 """
236 be careful that empty token like dedent are not counted as parens
236 be careful that empty token like dedent are not counted as parens
237 """
237 """
238 class Tk:
238 class Tk:
239 def __init__(self, s):
239 def __init__(self, s):
240 self.string = s
240 self.string = s
241
241
242 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','a','=','b')]), 2)
242 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','a','=','b')]), 2)
243 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','(', 'a','=','b', ')', '=' ,'5')]), 6)
243 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','(', 'a','=','b', ')', '=' ,'5')]), 6)
244
244
245 def test_check_complete():
245 def test_check_complete():
246 cc = ipt2.TransformerManager().check_complete
246 cc = ipt2.TransformerManager().check_complete
247 nt.assert_equal(cc("a = 1"), ('complete', None))
247 nt.assert_equal(cc("a = 1"), ('complete', None))
248 nt.assert_equal(cc("for a in range(5):"), ('incomplete', 4))
248 nt.assert_equal(cc("for a in range(5):"), ('incomplete', 4))
249 nt.assert_equal(cc("for a in range(5):\n if a > 0:"), ('incomplete', 8))
249 nt.assert_equal(cc("for a in range(5):\n if a > 0:"), ('incomplete', 8))
250 nt.assert_equal(cc("raise = 2"), ('invalid', None))
250 nt.assert_equal(cc("raise = 2"), ('invalid', None))
251 nt.assert_equal(cc("a = [1,\n2,"), ('incomplete', 0))
251 nt.assert_equal(cc("a = [1,\n2,"), ('incomplete', 0))
252 nt.assert_equal(cc(")"), ('incomplete', 0))
252 nt.assert_equal(cc(")"), ('incomplete', 0))
253 nt.assert_equal(cc("\\\r\n"), ('incomplete', 0))
253 nt.assert_equal(cc("\\\r\n"), ('incomplete', 0))
254 nt.assert_equal(cc("a = '''\n hi"), ('incomplete', 3))
254 nt.assert_equal(cc("a = '''\n hi"), ('incomplete', 3))
255 nt.assert_equal(cc("def a():\n x=1\n global x"), ('invalid', None))
255 nt.assert_equal(cc("def a():\n x=1\n global x"), ('invalid', None))
256 nt.assert_equal(cc("a \\ "), ('invalid', None)) # Nothing allowed after backslash
256 nt.assert_equal(cc("a \\ "), ('invalid', None)) # Nothing allowed after backslash
257 nt.assert_equal(cc("1\\\n+2"), ('complete', None))
257 nt.assert_equal(cc("1\\\n+2"), ('complete', None))
258 nt.assert_equal(cc("exit"), ('complete', None))
258 nt.assert_equal(cc("exit"), ('complete', None))
259
259
260 example = dedent("""
260 example = dedent("""
261 if True:
261 if True:
262 a=1""" )
262 a=1""" )
263
263
264 nt.assert_equal(cc(example), ('incomplete', 4))
264 nt.assert_equal(cc(example), ('incomplete', 4))
265 nt.assert_equal(cc(example+'\n'), ('complete', None))
265 nt.assert_equal(cc(example+'\n'), ('complete', None))
266 nt.assert_equal(cc(example+'\n '), ('complete', None))
266 nt.assert_equal(cc(example+'\n '), ('complete', None))
267
267
268 # no need to loop on all the letters/numbers.
268 # no need to loop on all the letters/numbers.
269 short = '12abAB'+string.printable[62:]
269 short = '12abAB'+string.printable[62:]
270 for c in short:
270 for c in short:
271 # test does not raise:
271 # test does not raise:
272 cc(c)
272 cc(c)
273 for k in short:
273 for k in short:
274 cc(c+k)
274 cc(c+k)
275
275
276 nt.assert_equal(cc("def f():\n x=0\n \\\n "), ('incomplete', 2))
276 nt.assert_equal(cc("def f():\n x=0\n \\\n "), ('incomplete', 2))
277
277
278 def test_check_complete_II():
278 def test_check_complete_II():
279 """
279 """
280 Test that multiple line strings are properly handled.
280 Test that multiple line strings are properly handled.
281
281
282 Separate test function for convenience
282 Separate test function for convenience
283
283
284 """
284 """
285 cc = ipt2.TransformerManager().check_complete
285 cc = ipt2.TransformerManager().check_complete
286 nt.assert_equal(cc('''def foo():\n """'''), ('incomplete', 4))
286 nt.assert_equal(cc('''def foo():\n """'''), ('incomplete', 4))
287
287
288
288
289 def test_null_cleanup_transformer():
289 def test_null_cleanup_transformer():
290 manager = ipt2.TransformerManager()
290 manager = ipt2.TransformerManager()
291 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
291 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
292 nt.assert_is(manager.transform_cell(""), "")
292 assert manager.transform_cell("") == ""
293
294
295
296
297 def test_side_effects_I():
298 count = 0
299 def counter(lines):
300 nonlocal count
301 count += 1
302 return lines
303
304 counter.has_side_effects = True
305
306 manager = ipt2.TransformerManager()
307 manager.cleanup_transforms.insert(0, counter)
308 assert manager.check_complete("a=1\n") == ('complete', None)
309 assert count == 0
310
311
312
313
314 def test_side_effects_II():
315 count = 0
316 def counter(lines):
317 nonlocal count
318 count += 1
319 return lines
320
321 counter.has_side_effects = True
322
323 manager = ipt2.TransformerManager()
324 manager.line_transforms.insert(0, counter)
325 assert manager.check_complete("b=1\n") == ('complete', None)
326 assert count == 0
@@ -1,1018 +1,1042 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Tests for the key interactiveshell module.
2 """Tests for the key interactiveshell module.
3
3
4 Historically the main classes in interactiveshell have been under-tested. This
4 Historically the main classes in interactiveshell have been under-tested. This
5 module should grow as many single-method tests as possible to trap many of the
5 module should grow as many single-method tests as possible to trap many of the
6 recurring bugs we seem to encounter with high-level interaction.
6 recurring bugs we seem to encounter with high-level interaction.
7 """
7 """
8
8
9 # Copyright (c) IPython Development Team.
9 # Copyright (c) IPython Development Team.
10 # Distributed under the terms of the Modified BSD License.
10 # Distributed under the terms of the Modified BSD License.
11
11
12 import asyncio
12 import asyncio
13 import ast
13 import ast
14 import os
14 import os
15 import signal
15 import signal
16 import shutil
16 import shutil
17 import sys
17 import sys
18 import tempfile
18 import tempfile
19 import unittest
19 import unittest
20 from unittest import mock
20 from unittest import mock
21
21
22 from os.path import join
22 from os.path import join
23
23
24 import nose.tools as nt
24 import nose.tools as nt
25
25
26 from IPython.core.error import InputRejected
26 from IPython.core.error import InputRejected
27 from IPython.core.inputtransformer import InputTransformer
27 from IPython.core.inputtransformer import InputTransformer
28 from IPython.core import interactiveshell
28 from IPython.core import interactiveshell
29 from IPython.testing.decorators import (
29 from IPython.testing.decorators import (
30 skipif, skip_win32, onlyif_unicode_paths, onlyif_cmds_exist,
30 skipif, skip_win32, onlyif_unicode_paths, onlyif_cmds_exist,
31 )
31 )
32 from IPython.testing import tools as tt
32 from IPython.testing import tools as tt
33 from IPython.utils.process import find_cmd
33 from IPython.utils.process import find_cmd
34
34
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36 # Globals
36 # Globals
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38 # This is used by every single test, no point repeating it ad nauseam
38 # This is used by every single test, no point repeating it ad nauseam
39
39
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41 # Tests
41 # Tests
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43
43
44 class DerivedInterrupt(KeyboardInterrupt):
44 class DerivedInterrupt(KeyboardInterrupt):
45 pass
45 pass
46
46
47 class InteractiveShellTestCase(unittest.TestCase):
47 class InteractiveShellTestCase(unittest.TestCase):
48 def test_naked_string_cells(self):
48 def test_naked_string_cells(self):
49 """Test that cells with only naked strings are fully executed"""
49 """Test that cells with only naked strings are fully executed"""
50 # First, single-line inputs
50 # First, single-line inputs
51 ip.run_cell('"a"\n')
51 ip.run_cell('"a"\n')
52 self.assertEqual(ip.user_ns['_'], 'a')
52 self.assertEqual(ip.user_ns['_'], 'a')
53 # And also multi-line cells
53 # And also multi-line cells
54 ip.run_cell('"""a\nb"""\n')
54 ip.run_cell('"""a\nb"""\n')
55 self.assertEqual(ip.user_ns['_'], 'a\nb')
55 self.assertEqual(ip.user_ns['_'], 'a\nb')
56
56
57 def test_run_empty_cell(self):
57 def test_run_empty_cell(self):
58 """Just make sure we don't get a horrible error with a blank
58 """Just make sure we don't get a horrible error with a blank
59 cell of input. Yes, I did overlook that."""
59 cell of input. Yes, I did overlook that."""
60 old_xc = ip.execution_count
60 old_xc = ip.execution_count
61 res = ip.run_cell('')
61 res = ip.run_cell('')
62 self.assertEqual(ip.execution_count, old_xc)
62 self.assertEqual(ip.execution_count, old_xc)
63 self.assertEqual(res.execution_count, None)
63 self.assertEqual(res.execution_count, None)
64
64
65 def test_run_cell_multiline(self):
65 def test_run_cell_multiline(self):
66 """Multi-block, multi-line cells must execute correctly.
66 """Multi-block, multi-line cells must execute correctly.
67 """
67 """
68 src = '\n'.join(["x=1",
68 src = '\n'.join(["x=1",
69 "y=2",
69 "y=2",
70 "if 1:",
70 "if 1:",
71 " x += 1",
71 " x += 1",
72 " y += 1",])
72 " y += 1",])
73 res = ip.run_cell(src)
73 res = ip.run_cell(src)
74 self.assertEqual(ip.user_ns['x'], 2)
74 self.assertEqual(ip.user_ns['x'], 2)
75 self.assertEqual(ip.user_ns['y'], 3)
75 self.assertEqual(ip.user_ns['y'], 3)
76 self.assertEqual(res.success, True)
76 self.assertEqual(res.success, True)
77 self.assertEqual(res.result, None)
77 self.assertEqual(res.result, None)
78
78
79 def test_multiline_string_cells(self):
79 def test_multiline_string_cells(self):
80 "Code sprinkled with multiline strings should execute (GH-306)"
80 "Code sprinkled with multiline strings should execute (GH-306)"
81 ip.run_cell('tmp=0')
81 ip.run_cell('tmp=0')
82 self.assertEqual(ip.user_ns['tmp'], 0)
82 self.assertEqual(ip.user_ns['tmp'], 0)
83 res = ip.run_cell('tmp=1;"""a\nb"""\n')
83 res = ip.run_cell('tmp=1;"""a\nb"""\n')
84 self.assertEqual(ip.user_ns['tmp'], 1)
84 self.assertEqual(ip.user_ns['tmp'], 1)
85 self.assertEqual(res.success, True)
85 self.assertEqual(res.success, True)
86 self.assertEqual(res.result, "a\nb")
86 self.assertEqual(res.result, "a\nb")
87
87
88 def test_dont_cache_with_semicolon(self):
88 def test_dont_cache_with_semicolon(self):
89 "Ending a line with semicolon should not cache the returned object (GH-307)"
89 "Ending a line with semicolon should not cache the returned object (GH-307)"
90 oldlen = len(ip.user_ns['Out'])
90 oldlen = len(ip.user_ns['Out'])
91 for cell in ['1;', '1;1;']:
91 for cell in ['1;', '1;1;']:
92 res = ip.run_cell(cell, store_history=True)
92 res = ip.run_cell(cell, store_history=True)
93 newlen = len(ip.user_ns['Out'])
93 newlen = len(ip.user_ns['Out'])
94 self.assertEqual(oldlen, newlen)
94 self.assertEqual(oldlen, newlen)
95 self.assertIsNone(res.result)
95 self.assertIsNone(res.result)
96 i = 0
96 i = 0
97 #also test the default caching behavior
97 #also test the default caching behavior
98 for cell in ['1', '1;1']:
98 for cell in ['1', '1;1']:
99 ip.run_cell(cell, store_history=True)
99 ip.run_cell(cell, store_history=True)
100 newlen = len(ip.user_ns['Out'])
100 newlen = len(ip.user_ns['Out'])
101 i += 1
101 i += 1
102 self.assertEqual(oldlen+i, newlen)
102 self.assertEqual(oldlen+i, newlen)
103
103
104 def test_syntax_error(self):
104 def test_syntax_error(self):
105 res = ip.run_cell("raise = 3")
105 res = ip.run_cell("raise = 3")
106 self.assertIsInstance(res.error_before_exec, SyntaxError)
106 self.assertIsInstance(res.error_before_exec, SyntaxError)
107
107
108 def test_In_variable(self):
108 def test_In_variable(self):
109 "Verify that In variable grows with user input (GH-284)"
109 "Verify that In variable grows with user input (GH-284)"
110 oldlen = len(ip.user_ns['In'])
110 oldlen = len(ip.user_ns['In'])
111 ip.run_cell('1;', store_history=True)
111 ip.run_cell('1;', store_history=True)
112 newlen = len(ip.user_ns['In'])
112 newlen = len(ip.user_ns['In'])
113 self.assertEqual(oldlen+1, newlen)
113 self.assertEqual(oldlen+1, newlen)
114 self.assertEqual(ip.user_ns['In'][-1],'1;')
114 self.assertEqual(ip.user_ns['In'][-1],'1;')
115
115
116 def test_magic_names_in_string(self):
116 def test_magic_names_in_string(self):
117 ip.run_cell('a = """\n%exit\n"""')
117 ip.run_cell('a = """\n%exit\n"""')
118 self.assertEqual(ip.user_ns['a'], '\n%exit\n')
118 self.assertEqual(ip.user_ns['a'], '\n%exit\n')
119
119
120 def test_trailing_newline(self):
120 def test_trailing_newline(self):
121 """test that running !(command) does not raise a SyntaxError"""
121 """test that running !(command) does not raise a SyntaxError"""
122 ip.run_cell('!(true)\n', False)
122 ip.run_cell('!(true)\n', False)
123 ip.run_cell('!(true)\n\n\n', False)
123 ip.run_cell('!(true)\n\n\n', False)
124
124
125 def test_gh_597(self):
125 def test_gh_597(self):
126 """Pretty-printing lists of objects with non-ascii reprs may cause
126 """Pretty-printing lists of objects with non-ascii reprs may cause
127 problems."""
127 problems."""
128 class Spam(object):
128 class Spam(object):
129 def __repr__(self):
129 def __repr__(self):
130 return "\xe9"*50
130 return "\xe9"*50
131 import IPython.core.formatters
131 import IPython.core.formatters
132 f = IPython.core.formatters.PlainTextFormatter()
132 f = IPython.core.formatters.PlainTextFormatter()
133 f([Spam(),Spam()])
133 f([Spam(),Spam()])
134
134
135
135
136 def test_future_flags(self):
136 def test_future_flags(self):
137 """Check that future flags are used for parsing code (gh-777)"""
137 """Check that future flags are used for parsing code (gh-777)"""
138 ip.run_cell('from __future__ import barry_as_FLUFL')
138 ip.run_cell('from __future__ import barry_as_FLUFL')
139 try:
139 try:
140 ip.run_cell('prfunc_return_val = 1 <> 2')
140 ip.run_cell('prfunc_return_val = 1 <> 2')
141 assert 'prfunc_return_val' in ip.user_ns
141 assert 'prfunc_return_val' in ip.user_ns
142 finally:
142 finally:
143 # Reset compiler flags so we don't mess up other tests.
143 # Reset compiler flags so we don't mess up other tests.
144 ip.compile.reset_compiler_flags()
144 ip.compile.reset_compiler_flags()
145
145
146 def test_can_pickle(self):
146 def test_can_pickle(self):
147 "Can we pickle objects defined interactively (GH-29)"
147 "Can we pickle objects defined interactively (GH-29)"
148 ip = get_ipython()
148 ip = get_ipython()
149 ip.reset()
149 ip.reset()
150 ip.run_cell(("class Mylist(list):\n"
150 ip.run_cell(("class Mylist(list):\n"
151 " def __init__(self,x=[]):\n"
151 " def __init__(self,x=[]):\n"
152 " list.__init__(self,x)"))
152 " list.__init__(self,x)"))
153 ip.run_cell("w=Mylist([1,2,3])")
153 ip.run_cell("w=Mylist([1,2,3])")
154
154
155 from pickle import dumps
155 from pickle import dumps
156
156
157 # We need to swap in our main module - this is only necessary
157 # We need to swap in our main module - this is only necessary
158 # inside the test framework, because IPython puts the interactive module
158 # inside the test framework, because IPython puts the interactive module
159 # in place (but the test framework undoes this).
159 # in place (but the test framework undoes this).
160 _main = sys.modules['__main__']
160 _main = sys.modules['__main__']
161 sys.modules['__main__'] = ip.user_module
161 sys.modules['__main__'] = ip.user_module
162 try:
162 try:
163 res = dumps(ip.user_ns["w"])
163 res = dumps(ip.user_ns["w"])
164 finally:
164 finally:
165 sys.modules['__main__'] = _main
165 sys.modules['__main__'] = _main
166 self.assertTrue(isinstance(res, bytes))
166 self.assertTrue(isinstance(res, bytes))
167
167
168 def test_global_ns(self):
168 def test_global_ns(self):
169 "Code in functions must be able to access variables outside them."
169 "Code in functions must be able to access variables outside them."
170 ip = get_ipython()
170 ip = get_ipython()
171 ip.run_cell("a = 10")
171 ip.run_cell("a = 10")
172 ip.run_cell(("def f(x):\n"
172 ip.run_cell(("def f(x):\n"
173 " return x + a"))
173 " return x + a"))
174 ip.run_cell("b = f(12)")
174 ip.run_cell("b = f(12)")
175 self.assertEqual(ip.user_ns["b"], 22)
175 self.assertEqual(ip.user_ns["b"], 22)
176
176
177 def test_bad_custom_tb(self):
177 def test_bad_custom_tb(self):
178 """Check that InteractiveShell is protected from bad custom exception handlers"""
178 """Check that InteractiveShell is protected from bad custom exception handlers"""
179 ip.set_custom_exc((IOError,), lambda etype,value,tb: 1/0)
179 ip.set_custom_exc((IOError,), lambda etype,value,tb: 1/0)
180 self.assertEqual(ip.custom_exceptions, (IOError,))
180 self.assertEqual(ip.custom_exceptions, (IOError,))
181 with tt.AssertPrints("Custom TB Handler failed", channel='stderr'):
181 with tt.AssertPrints("Custom TB Handler failed", channel='stderr'):
182 ip.run_cell(u'raise IOError("foo")')
182 ip.run_cell(u'raise IOError("foo")')
183 self.assertEqual(ip.custom_exceptions, ())
183 self.assertEqual(ip.custom_exceptions, ())
184
184
185 def test_bad_custom_tb_return(self):
185 def test_bad_custom_tb_return(self):
186 """Check that InteractiveShell is protected from bad return types in custom exception handlers"""
186 """Check that InteractiveShell is protected from bad return types in custom exception handlers"""
187 ip.set_custom_exc((NameError,),lambda etype,value,tb, tb_offset=None: 1)
187 ip.set_custom_exc((NameError,),lambda etype,value,tb, tb_offset=None: 1)
188 self.assertEqual(ip.custom_exceptions, (NameError,))
188 self.assertEqual(ip.custom_exceptions, (NameError,))
189 with tt.AssertPrints("Custom TB Handler failed", channel='stderr'):
189 with tt.AssertPrints("Custom TB Handler failed", channel='stderr'):
190 ip.run_cell(u'a=abracadabra')
190 ip.run_cell(u'a=abracadabra')
191 self.assertEqual(ip.custom_exceptions, ())
191 self.assertEqual(ip.custom_exceptions, ())
192
192
193 def test_drop_by_id(self):
193 def test_drop_by_id(self):
194 myvars = {"a":object(), "b":object(), "c": object()}
194 myvars = {"a":object(), "b":object(), "c": object()}
195 ip.push(myvars, interactive=False)
195 ip.push(myvars, interactive=False)
196 for name in myvars:
196 for name in myvars:
197 assert name in ip.user_ns, name
197 assert name in ip.user_ns, name
198 assert name in ip.user_ns_hidden, name
198 assert name in ip.user_ns_hidden, name
199 ip.user_ns['b'] = 12
199 ip.user_ns['b'] = 12
200 ip.drop_by_id(myvars)
200 ip.drop_by_id(myvars)
201 for name in ["a", "c"]:
201 for name in ["a", "c"]:
202 assert name not in ip.user_ns, name
202 assert name not in ip.user_ns, name
203 assert name not in ip.user_ns_hidden, name
203 assert name not in ip.user_ns_hidden, name
204 assert ip.user_ns['b'] == 12
204 assert ip.user_ns['b'] == 12
205 ip.reset()
205 ip.reset()
206
206
207 def test_var_expand(self):
207 def test_var_expand(self):
208 ip.user_ns['f'] = u'Ca\xf1o'
208 ip.user_ns['f'] = u'Ca\xf1o'
209 self.assertEqual(ip.var_expand(u'echo $f'), u'echo Ca\xf1o')
209 self.assertEqual(ip.var_expand(u'echo $f'), u'echo Ca\xf1o')
210 self.assertEqual(ip.var_expand(u'echo {f}'), u'echo Ca\xf1o')
210 self.assertEqual(ip.var_expand(u'echo {f}'), u'echo Ca\xf1o')
211 self.assertEqual(ip.var_expand(u'echo {f[:-1]}'), u'echo Ca\xf1')
211 self.assertEqual(ip.var_expand(u'echo {f[:-1]}'), u'echo Ca\xf1')
212 self.assertEqual(ip.var_expand(u'echo {1*2}'), u'echo 2')
212 self.assertEqual(ip.var_expand(u'echo {1*2}'), u'echo 2')
213
213
214 self.assertEqual(ip.var_expand(u"grep x | awk '{print $1}'"), u"grep x | awk '{print $1}'")
214 self.assertEqual(ip.var_expand(u"grep x | awk '{print $1}'"), u"grep x | awk '{print $1}'")
215
215
216 ip.user_ns['f'] = b'Ca\xc3\xb1o'
216 ip.user_ns['f'] = b'Ca\xc3\xb1o'
217 # This should not raise any exception:
217 # This should not raise any exception:
218 ip.var_expand(u'echo $f')
218 ip.var_expand(u'echo $f')
219
219
220 def test_var_expand_local(self):
220 def test_var_expand_local(self):
221 """Test local variable expansion in !system and %magic calls"""
221 """Test local variable expansion in !system and %magic calls"""
222 # !system
222 # !system
223 ip.run_cell('def test():\n'
223 ip.run_cell('def test():\n'
224 ' lvar = "ttt"\n'
224 ' lvar = "ttt"\n'
225 ' ret = !echo {lvar}\n'
225 ' ret = !echo {lvar}\n'
226 ' return ret[0]\n')
226 ' return ret[0]\n')
227 res = ip.user_ns['test']()
227 res = ip.user_ns['test']()
228 nt.assert_in('ttt', res)
228 nt.assert_in('ttt', res)
229
229
230 # %magic
230 # %magic
231 ip.run_cell('def makemacro():\n'
231 ip.run_cell('def makemacro():\n'
232 ' macroname = "macro_var_expand_locals"\n'
232 ' macroname = "macro_var_expand_locals"\n'
233 ' %macro {macroname} codestr\n')
233 ' %macro {macroname} codestr\n')
234 ip.user_ns['codestr'] = "str(12)"
234 ip.user_ns['codestr'] = "str(12)"
235 ip.run_cell('makemacro()')
235 ip.run_cell('makemacro()')
236 nt.assert_in('macro_var_expand_locals', ip.user_ns)
236 nt.assert_in('macro_var_expand_locals', ip.user_ns)
237
237
238 def test_var_expand_self(self):
238 def test_var_expand_self(self):
239 """Test variable expansion with the name 'self', which was failing.
239 """Test variable expansion with the name 'self', which was failing.
240
240
241 See https://github.com/ipython/ipython/issues/1878#issuecomment-7698218
241 See https://github.com/ipython/ipython/issues/1878#issuecomment-7698218
242 """
242 """
243 ip.run_cell('class cTest:\n'
243 ip.run_cell('class cTest:\n'
244 ' classvar="see me"\n'
244 ' classvar="see me"\n'
245 ' def test(self):\n'
245 ' def test(self):\n'
246 ' res = !echo Variable: {self.classvar}\n'
246 ' res = !echo Variable: {self.classvar}\n'
247 ' return res[0]\n')
247 ' return res[0]\n')
248 nt.assert_in('see me', ip.user_ns['cTest']().test())
248 nt.assert_in('see me', ip.user_ns['cTest']().test())
249
249
250 def test_bad_var_expand(self):
250 def test_bad_var_expand(self):
251 """var_expand on invalid formats shouldn't raise"""
251 """var_expand on invalid formats shouldn't raise"""
252 # SyntaxError
252 # SyntaxError
253 self.assertEqual(ip.var_expand(u"{'a':5}"), u"{'a':5}")
253 self.assertEqual(ip.var_expand(u"{'a':5}"), u"{'a':5}")
254 # NameError
254 # NameError
255 self.assertEqual(ip.var_expand(u"{asdf}"), u"{asdf}")
255 self.assertEqual(ip.var_expand(u"{asdf}"), u"{asdf}")
256 # ZeroDivisionError
256 # ZeroDivisionError
257 self.assertEqual(ip.var_expand(u"{1/0}"), u"{1/0}")
257 self.assertEqual(ip.var_expand(u"{1/0}"), u"{1/0}")
258
258
259 def test_silent_postexec(self):
259 def test_silent_postexec(self):
260 """run_cell(silent=True) doesn't invoke pre/post_run_cell callbacks"""
260 """run_cell(silent=True) doesn't invoke pre/post_run_cell callbacks"""
261 pre_explicit = mock.Mock()
261 pre_explicit = mock.Mock()
262 pre_always = mock.Mock()
262 pre_always = mock.Mock()
263 post_explicit = mock.Mock()
263 post_explicit = mock.Mock()
264 post_always = mock.Mock()
264 post_always = mock.Mock()
265 all_mocks = [pre_explicit, pre_always, post_explicit, post_always]
265 all_mocks = [pre_explicit, pre_always, post_explicit, post_always]
266
266
267 ip.events.register('pre_run_cell', pre_explicit)
267 ip.events.register('pre_run_cell', pre_explicit)
268 ip.events.register('pre_execute', pre_always)
268 ip.events.register('pre_execute', pre_always)
269 ip.events.register('post_run_cell', post_explicit)
269 ip.events.register('post_run_cell', post_explicit)
270 ip.events.register('post_execute', post_always)
270 ip.events.register('post_execute', post_always)
271
271
272 try:
272 try:
273 ip.run_cell("1", silent=True)
273 ip.run_cell("1", silent=True)
274 assert pre_always.called
274 assert pre_always.called
275 assert not pre_explicit.called
275 assert not pre_explicit.called
276 assert post_always.called
276 assert post_always.called
277 assert not post_explicit.called
277 assert not post_explicit.called
278 # double-check that non-silent exec did what we expected
278 # double-check that non-silent exec did what we expected
279 # silent to avoid
279 # silent to avoid
280 ip.run_cell("1")
280 ip.run_cell("1")
281 assert pre_explicit.called
281 assert pre_explicit.called
282 assert post_explicit.called
282 assert post_explicit.called
283 info, = pre_explicit.call_args[0]
283 info, = pre_explicit.call_args[0]
284 result, = post_explicit.call_args[0]
284 result, = post_explicit.call_args[0]
285 self.assertEqual(info, result.info)
285 self.assertEqual(info, result.info)
286 # check that post hooks are always called
286 # check that post hooks are always called
287 [m.reset_mock() for m in all_mocks]
287 [m.reset_mock() for m in all_mocks]
288 ip.run_cell("syntax error")
288 ip.run_cell("syntax error")
289 assert pre_always.called
289 assert pre_always.called
290 assert pre_explicit.called
290 assert pre_explicit.called
291 assert post_always.called
291 assert post_always.called
292 assert post_explicit.called
292 assert post_explicit.called
293 info, = pre_explicit.call_args[0]
293 info, = pre_explicit.call_args[0]
294 result, = post_explicit.call_args[0]
294 result, = post_explicit.call_args[0]
295 self.assertEqual(info, result.info)
295 self.assertEqual(info, result.info)
296 finally:
296 finally:
297 # remove post-exec
297 # remove post-exec
298 ip.events.unregister('pre_run_cell', pre_explicit)
298 ip.events.unregister('pre_run_cell', pre_explicit)
299 ip.events.unregister('pre_execute', pre_always)
299 ip.events.unregister('pre_execute', pre_always)
300 ip.events.unregister('post_run_cell', post_explicit)
300 ip.events.unregister('post_run_cell', post_explicit)
301 ip.events.unregister('post_execute', post_always)
301 ip.events.unregister('post_execute', post_always)
302
302
303 def test_silent_noadvance(self):
303 def test_silent_noadvance(self):
304 """run_cell(silent=True) doesn't advance execution_count"""
304 """run_cell(silent=True) doesn't advance execution_count"""
305 ec = ip.execution_count
305 ec = ip.execution_count
306 # silent should force store_history=False
306 # silent should force store_history=False
307 ip.run_cell("1", store_history=True, silent=True)
307 ip.run_cell("1", store_history=True, silent=True)
308
308
309 self.assertEqual(ec, ip.execution_count)
309 self.assertEqual(ec, ip.execution_count)
310 # double-check that non-silent exec did what we expected
310 # double-check that non-silent exec did what we expected
311 # silent to avoid
311 # silent to avoid
312 ip.run_cell("1", store_history=True)
312 ip.run_cell("1", store_history=True)
313 self.assertEqual(ec+1, ip.execution_count)
313 self.assertEqual(ec+1, ip.execution_count)
314
314
315 def test_silent_nodisplayhook(self):
315 def test_silent_nodisplayhook(self):
316 """run_cell(silent=True) doesn't trigger displayhook"""
316 """run_cell(silent=True) doesn't trigger displayhook"""
317 d = dict(called=False)
317 d = dict(called=False)
318
318
319 trap = ip.display_trap
319 trap = ip.display_trap
320 save_hook = trap.hook
320 save_hook = trap.hook
321
321
322 def failing_hook(*args, **kwargs):
322 def failing_hook(*args, **kwargs):
323 d['called'] = True
323 d['called'] = True
324
324
325 try:
325 try:
326 trap.hook = failing_hook
326 trap.hook = failing_hook
327 res = ip.run_cell("1", silent=True)
327 res = ip.run_cell("1", silent=True)
328 self.assertFalse(d['called'])
328 self.assertFalse(d['called'])
329 self.assertIsNone(res.result)
329 self.assertIsNone(res.result)
330 # double-check that non-silent exec did what we expected
330 # double-check that non-silent exec did what we expected
331 # silent to avoid
331 # silent to avoid
332 ip.run_cell("1")
332 ip.run_cell("1")
333 self.assertTrue(d['called'])
333 self.assertTrue(d['called'])
334 finally:
334 finally:
335 trap.hook = save_hook
335 trap.hook = save_hook
336
336
337 def test_ofind_line_magic(self):
337 def test_ofind_line_magic(self):
338 from IPython.core.magic import register_line_magic
338 from IPython.core.magic import register_line_magic
339
339
340 @register_line_magic
340 @register_line_magic
341 def lmagic(line):
341 def lmagic(line):
342 "A line magic"
342 "A line magic"
343
343
344 # Get info on line magic
344 # Get info on line magic
345 lfind = ip._ofind('lmagic')
345 lfind = ip._ofind('lmagic')
346 info = dict(found=True, isalias=False, ismagic=True,
346 info = dict(found=True, isalias=False, ismagic=True,
347 namespace = 'IPython internal', obj= lmagic.__wrapped__,
347 namespace = 'IPython internal', obj= lmagic.__wrapped__,
348 parent = None)
348 parent = None)
349 nt.assert_equal(lfind, info)
349 nt.assert_equal(lfind, info)
350
350
351 def test_ofind_cell_magic(self):
351 def test_ofind_cell_magic(self):
352 from IPython.core.magic import register_cell_magic
352 from IPython.core.magic import register_cell_magic
353
353
354 @register_cell_magic
354 @register_cell_magic
355 def cmagic(line, cell):
355 def cmagic(line, cell):
356 "A cell magic"
356 "A cell magic"
357
357
358 # Get info on cell magic
358 # Get info on cell magic
359 find = ip._ofind('cmagic')
359 find = ip._ofind('cmagic')
360 info = dict(found=True, isalias=False, ismagic=True,
360 info = dict(found=True, isalias=False, ismagic=True,
361 namespace = 'IPython internal', obj= cmagic.__wrapped__,
361 namespace = 'IPython internal', obj= cmagic.__wrapped__,
362 parent = None)
362 parent = None)
363 nt.assert_equal(find, info)
363 nt.assert_equal(find, info)
364
364
365 def test_ofind_property_with_error(self):
365 def test_ofind_property_with_error(self):
366 class A(object):
366 class A(object):
367 @property
367 @property
368 def foo(self):
368 def foo(self):
369 raise NotImplementedError()
369 raise NotImplementedError()
370 a = A()
370 a = A()
371
371
372 found = ip._ofind('a.foo', [('locals', locals())])
372 found = ip._ofind('a.foo', [('locals', locals())])
373 info = dict(found=True, isalias=False, ismagic=False,
373 info = dict(found=True, isalias=False, ismagic=False,
374 namespace='locals', obj=A.foo, parent=a)
374 namespace='locals', obj=A.foo, parent=a)
375 nt.assert_equal(found, info)
375 nt.assert_equal(found, info)
376
376
377 def test_ofind_multiple_attribute_lookups(self):
377 def test_ofind_multiple_attribute_lookups(self):
378 class A(object):
378 class A(object):
379 @property
379 @property
380 def foo(self):
380 def foo(self):
381 raise NotImplementedError()
381 raise NotImplementedError()
382
382
383 a = A()
383 a = A()
384 a.a = A()
384 a.a = A()
385 a.a.a = A()
385 a.a.a = A()
386
386
387 found = ip._ofind('a.a.a.foo', [('locals', locals())])
387 found = ip._ofind('a.a.a.foo', [('locals', locals())])
388 info = dict(found=True, isalias=False, ismagic=False,
388 info = dict(found=True, isalias=False, ismagic=False,
389 namespace='locals', obj=A.foo, parent=a.a.a)
389 namespace='locals', obj=A.foo, parent=a.a.a)
390 nt.assert_equal(found, info)
390 nt.assert_equal(found, info)
391
391
392 def test_ofind_slotted_attributes(self):
392 def test_ofind_slotted_attributes(self):
393 class A(object):
393 class A(object):
394 __slots__ = ['foo']
394 __slots__ = ['foo']
395 def __init__(self):
395 def __init__(self):
396 self.foo = 'bar'
396 self.foo = 'bar'
397
397
398 a = A()
398 a = A()
399 found = ip._ofind('a.foo', [('locals', locals())])
399 found = ip._ofind('a.foo', [('locals', locals())])
400 info = dict(found=True, isalias=False, ismagic=False,
400 info = dict(found=True, isalias=False, ismagic=False,
401 namespace='locals', obj=a.foo, parent=a)
401 namespace='locals', obj=a.foo, parent=a)
402 nt.assert_equal(found, info)
402 nt.assert_equal(found, info)
403
403
404 found = ip._ofind('a.bar', [('locals', locals())])
404 found = ip._ofind('a.bar', [('locals', locals())])
405 info = dict(found=False, isalias=False, ismagic=False,
405 info = dict(found=False, isalias=False, ismagic=False,
406 namespace=None, obj=None, parent=a)
406 namespace=None, obj=None, parent=a)
407 nt.assert_equal(found, info)
407 nt.assert_equal(found, info)
408
408
409 def test_ofind_prefers_property_to_instance_level_attribute(self):
409 def test_ofind_prefers_property_to_instance_level_attribute(self):
410 class A(object):
410 class A(object):
411 @property
411 @property
412 def foo(self):
412 def foo(self):
413 return 'bar'
413 return 'bar'
414 a = A()
414 a = A()
415 a.__dict__['foo'] = 'baz'
415 a.__dict__['foo'] = 'baz'
416 nt.assert_equal(a.foo, 'bar')
416 nt.assert_equal(a.foo, 'bar')
417 found = ip._ofind('a.foo', [('locals', locals())])
417 found = ip._ofind('a.foo', [('locals', locals())])
418 nt.assert_is(found['obj'], A.foo)
418 nt.assert_is(found['obj'], A.foo)
419
419
420 def test_custom_syntaxerror_exception(self):
420 def test_custom_syntaxerror_exception(self):
421 called = []
421 called = []
422 def my_handler(shell, etype, value, tb, tb_offset=None):
422 def my_handler(shell, etype, value, tb, tb_offset=None):
423 called.append(etype)
423 called.append(etype)
424 shell.showtraceback((etype, value, tb), tb_offset=tb_offset)
424 shell.showtraceback((etype, value, tb), tb_offset=tb_offset)
425
425
426 ip.set_custom_exc((SyntaxError,), my_handler)
426 ip.set_custom_exc((SyntaxError,), my_handler)
427 try:
427 try:
428 ip.run_cell("1f")
428 ip.run_cell("1f")
429 # Check that this was called, and only once.
429 # Check that this was called, and only once.
430 self.assertEqual(called, [SyntaxError])
430 self.assertEqual(called, [SyntaxError])
431 finally:
431 finally:
432 # Reset the custom exception hook
432 # Reset the custom exception hook
433 ip.set_custom_exc((), None)
433 ip.set_custom_exc((), None)
434
434
435 def test_custom_exception(self):
435 def test_custom_exception(self):
436 called = []
436 called = []
437 def my_handler(shell, etype, value, tb, tb_offset=None):
437 def my_handler(shell, etype, value, tb, tb_offset=None):
438 called.append(etype)
438 called.append(etype)
439 shell.showtraceback((etype, value, tb), tb_offset=tb_offset)
439 shell.showtraceback((etype, value, tb), tb_offset=tb_offset)
440
440
441 ip.set_custom_exc((ValueError,), my_handler)
441 ip.set_custom_exc((ValueError,), my_handler)
442 try:
442 try:
443 res = ip.run_cell("raise ValueError('test')")
443 res = ip.run_cell("raise ValueError('test')")
444 # Check that this was called, and only once.
444 # Check that this was called, and only once.
445 self.assertEqual(called, [ValueError])
445 self.assertEqual(called, [ValueError])
446 # Check that the error is on the result object
446 # Check that the error is on the result object
447 self.assertIsInstance(res.error_in_exec, ValueError)
447 self.assertIsInstance(res.error_in_exec, ValueError)
448 finally:
448 finally:
449 # Reset the custom exception hook
449 # Reset the custom exception hook
450 ip.set_custom_exc((), None)
450 ip.set_custom_exc((), None)
451
451
452 def test_mktempfile(self):
452 def test_mktempfile(self):
453 filename = ip.mktempfile()
453 filename = ip.mktempfile()
454 # Check that we can open the file again on Windows
454 # Check that we can open the file again on Windows
455 with open(filename, 'w') as f:
455 with open(filename, 'w') as f:
456 f.write('abc')
456 f.write('abc')
457
457
458 filename = ip.mktempfile(data='blah')
458 filename = ip.mktempfile(data='blah')
459 with open(filename, 'r') as f:
459 with open(filename, 'r') as f:
460 self.assertEqual(f.read(), 'blah')
460 self.assertEqual(f.read(), 'blah')
461
461
462 def test_new_main_mod(self):
462 def test_new_main_mod(self):
463 # Smoketest to check that this accepts a unicode module name
463 # Smoketest to check that this accepts a unicode module name
464 name = u'jiefmw'
464 name = u'jiefmw'
465 mod = ip.new_main_mod(u'%s.py' % name, name)
465 mod = ip.new_main_mod(u'%s.py' % name, name)
466 self.assertEqual(mod.__name__, name)
466 self.assertEqual(mod.__name__, name)
467
467
468 def test_get_exception_only(self):
468 def test_get_exception_only(self):
469 try:
469 try:
470 raise KeyboardInterrupt
470 raise KeyboardInterrupt
471 except KeyboardInterrupt:
471 except KeyboardInterrupt:
472 msg = ip.get_exception_only()
472 msg = ip.get_exception_only()
473 self.assertEqual(msg, 'KeyboardInterrupt\n')
473 self.assertEqual(msg, 'KeyboardInterrupt\n')
474
474
475 try:
475 try:
476 raise DerivedInterrupt("foo")
476 raise DerivedInterrupt("foo")
477 except KeyboardInterrupt:
477 except KeyboardInterrupt:
478 msg = ip.get_exception_only()
478 msg = ip.get_exception_only()
479 self.assertEqual(msg, 'IPython.core.tests.test_interactiveshell.DerivedInterrupt: foo\n')
479 self.assertEqual(msg, 'IPython.core.tests.test_interactiveshell.DerivedInterrupt: foo\n')
480
480
481 def test_inspect_text(self):
481 def test_inspect_text(self):
482 ip.run_cell('a = 5')
482 ip.run_cell('a = 5')
483 text = ip.object_inspect_text('a')
483 text = ip.object_inspect_text('a')
484 self.assertIsInstance(text, str)
484 self.assertIsInstance(text, str)
485
485
486 def test_last_execution_result(self):
486 def test_last_execution_result(self):
487 """ Check that last execution result gets set correctly (GH-10702) """
487 """ Check that last execution result gets set correctly (GH-10702) """
488 result = ip.run_cell('a = 5; a')
488 result = ip.run_cell('a = 5; a')
489 self.assertTrue(ip.last_execution_succeeded)
489 self.assertTrue(ip.last_execution_succeeded)
490 self.assertEqual(ip.last_execution_result.result, 5)
490 self.assertEqual(ip.last_execution_result.result, 5)
491
491
492 result = ip.run_cell('a = x_invalid_id_x')
492 result = ip.run_cell('a = x_invalid_id_x')
493 self.assertFalse(ip.last_execution_succeeded)
493 self.assertFalse(ip.last_execution_succeeded)
494 self.assertFalse(ip.last_execution_result.success)
494 self.assertFalse(ip.last_execution_result.success)
495 self.assertIsInstance(ip.last_execution_result.error_in_exec, NameError)
495 self.assertIsInstance(ip.last_execution_result.error_in_exec, NameError)
496
496
497 def test_reset_aliasing(self):
497 def test_reset_aliasing(self):
498 """ Check that standard posix aliases work after %reset. """
498 """ Check that standard posix aliases work after %reset. """
499 if os.name != 'posix':
499 if os.name != 'posix':
500 return
500 return
501
501
502 ip.reset()
502 ip.reset()
503 for cmd in ('clear', 'more', 'less', 'man'):
503 for cmd in ('clear', 'more', 'less', 'man'):
504 res = ip.run_cell('%' + cmd)
504 res = ip.run_cell('%' + cmd)
505 self.assertEqual(res.success, True)
505 self.assertEqual(res.success, True)
506
506
507
507
508 class TestSafeExecfileNonAsciiPath(unittest.TestCase):
508 class TestSafeExecfileNonAsciiPath(unittest.TestCase):
509
509
510 @onlyif_unicode_paths
510 @onlyif_unicode_paths
511 def setUp(self):
511 def setUp(self):
512 self.BASETESTDIR = tempfile.mkdtemp()
512 self.BASETESTDIR = tempfile.mkdtemp()
513 self.TESTDIR = join(self.BASETESTDIR, u"Γ₯Àâ")
513 self.TESTDIR = join(self.BASETESTDIR, u"Γ₯Àâ")
514 os.mkdir(self.TESTDIR)
514 os.mkdir(self.TESTDIR)
515 with open(join(self.TESTDIR, u"Γ₯Àâtestscript.py"), "w") as sfile:
515 with open(join(self.TESTDIR, u"Γ₯Àâtestscript.py"), "w") as sfile:
516 sfile.write("pass\n")
516 sfile.write("pass\n")
517 self.oldpath = os.getcwd()
517 self.oldpath = os.getcwd()
518 os.chdir(self.TESTDIR)
518 os.chdir(self.TESTDIR)
519 self.fname = u"Γ₯Àâtestscript.py"
519 self.fname = u"Γ₯Àâtestscript.py"
520
520
521 def tearDown(self):
521 def tearDown(self):
522 os.chdir(self.oldpath)
522 os.chdir(self.oldpath)
523 shutil.rmtree(self.BASETESTDIR)
523 shutil.rmtree(self.BASETESTDIR)
524
524
525 @onlyif_unicode_paths
525 @onlyif_unicode_paths
526 def test_1(self):
526 def test_1(self):
527 """Test safe_execfile with non-ascii path
527 """Test safe_execfile with non-ascii path
528 """
528 """
529 ip.safe_execfile(self.fname, {}, raise_exceptions=True)
529 ip.safe_execfile(self.fname, {}, raise_exceptions=True)
530
530
531 class ExitCodeChecks(tt.TempFileMixin):
531 class ExitCodeChecks(tt.TempFileMixin):
532
532
533 def setUp(self):
533 def setUp(self):
534 self.system = ip.system_raw
534 self.system = ip.system_raw
535
535
536 def test_exit_code_ok(self):
536 def test_exit_code_ok(self):
537 self.system('exit 0')
537 self.system('exit 0')
538 self.assertEqual(ip.user_ns['_exit_code'], 0)
538 self.assertEqual(ip.user_ns['_exit_code'], 0)
539
539
540 def test_exit_code_error(self):
540 def test_exit_code_error(self):
541 self.system('exit 1')
541 self.system('exit 1')
542 self.assertEqual(ip.user_ns['_exit_code'], 1)
542 self.assertEqual(ip.user_ns['_exit_code'], 1)
543
543
544 @skipif(not hasattr(signal, 'SIGALRM'))
544 @skipif(not hasattr(signal, 'SIGALRM'))
545 def test_exit_code_signal(self):
545 def test_exit_code_signal(self):
546 self.mktmp("import signal, time\n"
546 self.mktmp("import signal, time\n"
547 "signal.setitimer(signal.ITIMER_REAL, 0.1)\n"
547 "signal.setitimer(signal.ITIMER_REAL, 0.1)\n"
548 "time.sleep(1)\n")
548 "time.sleep(1)\n")
549 self.system("%s %s" % (sys.executable, self.fname))
549 self.system("%s %s" % (sys.executable, self.fname))
550 self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGALRM)
550 self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGALRM)
551
551
552 @onlyif_cmds_exist("csh")
552 @onlyif_cmds_exist("csh")
553 def test_exit_code_signal_csh(self):
553 def test_exit_code_signal_csh(self):
554 SHELL = os.environ.get('SHELL', None)
554 SHELL = os.environ.get('SHELL', None)
555 os.environ['SHELL'] = find_cmd("csh")
555 os.environ['SHELL'] = find_cmd("csh")
556 try:
556 try:
557 self.test_exit_code_signal()
557 self.test_exit_code_signal()
558 finally:
558 finally:
559 if SHELL is not None:
559 if SHELL is not None:
560 os.environ['SHELL'] = SHELL
560 os.environ['SHELL'] = SHELL
561 else:
561 else:
562 del os.environ['SHELL']
562 del os.environ['SHELL']
563
563
564
564
565 class TestSystemRaw(ExitCodeChecks):
565 class TestSystemRaw(ExitCodeChecks):
566
566
567 def setUp(self):
567 def setUp(self):
568 super().setUp()
568 super().setUp()
569 self.system = ip.system_raw
569 self.system = ip.system_raw
570
570
571 @onlyif_unicode_paths
571 @onlyif_unicode_paths
572 def test_1(self):
572 def test_1(self):
573 """Test system_raw with non-ascii cmd
573 """Test system_raw with non-ascii cmd
574 """
574 """
575 cmd = u'''python -c "'Γ₯Àâ'" '''
575 cmd = u'''python -c "'Γ₯Àâ'" '''
576 ip.system_raw(cmd)
576 ip.system_raw(cmd)
577
577
578 @mock.patch('subprocess.call', side_effect=KeyboardInterrupt)
578 @mock.patch('subprocess.call', side_effect=KeyboardInterrupt)
579 @mock.patch('os.system', side_effect=KeyboardInterrupt)
579 @mock.patch('os.system', side_effect=KeyboardInterrupt)
580 def test_control_c(self, *mocks):
580 def test_control_c(self, *mocks):
581 try:
581 try:
582 self.system("sleep 1 # wont happen")
582 self.system("sleep 1 # wont happen")
583 except KeyboardInterrupt:
583 except KeyboardInterrupt:
584 self.fail("system call should intercept "
584 self.fail("system call should intercept "
585 "keyboard interrupt from subprocess.call")
585 "keyboard interrupt from subprocess.call")
586 self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGINT)
586 self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGINT)
587
587
588 # TODO: Exit codes are currently ignored on Windows.
588 # TODO: Exit codes are currently ignored on Windows.
589 class TestSystemPipedExitCode(ExitCodeChecks):
589 class TestSystemPipedExitCode(ExitCodeChecks):
590
590
591 def setUp(self):
591 def setUp(self):
592 super().setUp()
592 super().setUp()
593 self.system = ip.system_piped
593 self.system = ip.system_piped
594
594
595 @skip_win32
595 @skip_win32
596 def test_exit_code_ok(self):
596 def test_exit_code_ok(self):
597 ExitCodeChecks.test_exit_code_ok(self)
597 ExitCodeChecks.test_exit_code_ok(self)
598
598
599 @skip_win32
599 @skip_win32
600 def test_exit_code_error(self):
600 def test_exit_code_error(self):
601 ExitCodeChecks.test_exit_code_error(self)
601 ExitCodeChecks.test_exit_code_error(self)
602
602
603 @skip_win32
603 @skip_win32
604 def test_exit_code_signal(self):
604 def test_exit_code_signal(self):
605 ExitCodeChecks.test_exit_code_signal(self)
605 ExitCodeChecks.test_exit_code_signal(self)
606
606
607 class TestModules(tt.TempFileMixin):
607 class TestModules(tt.TempFileMixin):
608 def test_extraneous_loads(self):
608 def test_extraneous_loads(self):
609 """Test we're not loading modules on startup that we shouldn't.
609 """Test we're not loading modules on startup that we shouldn't.
610 """
610 """
611 self.mktmp("import sys\n"
611 self.mktmp("import sys\n"
612 "print('numpy' in sys.modules)\n"
612 "print('numpy' in sys.modules)\n"
613 "print('ipyparallel' in sys.modules)\n"
613 "print('ipyparallel' in sys.modules)\n"
614 "print('ipykernel' in sys.modules)\n"
614 "print('ipykernel' in sys.modules)\n"
615 )
615 )
616 out = "False\nFalse\nFalse\n"
616 out = "False\nFalse\nFalse\n"
617 tt.ipexec_validate(self.fname, out)
617 tt.ipexec_validate(self.fname, out)
618
618
619 class Negator(ast.NodeTransformer):
619 class Negator(ast.NodeTransformer):
620 """Negates all number literals in an AST."""
620 """Negates all number literals in an AST."""
621
621
622 # for python 3.7 and earlier
622 # for python 3.7 and earlier
623 def visit_Num(self, node):
623 def visit_Num(self, node):
624 node.n = -node.n
624 node.n = -node.n
625 return node
625 return node
626
626
627 # for python 3.8+
627 # for python 3.8+
628 def visit_Constant(self, node):
628 def visit_Constant(self, node):
629 if isinstance(node.value, int):
629 if isinstance(node.value, int):
630 return self.visit_Num(node)
630 return self.visit_Num(node)
631 return node
631 return node
632
632
633 class TestAstTransform(unittest.TestCase):
633 class TestAstTransform(unittest.TestCase):
634 def setUp(self):
634 def setUp(self):
635 self.negator = Negator()
635 self.negator = Negator()
636 ip.ast_transformers.append(self.negator)
636 ip.ast_transformers.append(self.negator)
637
637
638 def tearDown(self):
638 def tearDown(self):
639 ip.ast_transformers.remove(self.negator)
639 ip.ast_transformers.remove(self.negator)
640
640
641 def test_run_cell(self):
641 def test_run_cell(self):
642 with tt.AssertPrints('-34'):
642 with tt.AssertPrints('-34'):
643 ip.run_cell('print (12 + 22)')
643 ip.run_cell('print (12 + 22)')
644
644
645 # A named reference to a number shouldn't be transformed.
645 # A named reference to a number shouldn't be transformed.
646 ip.user_ns['n'] = 55
646 ip.user_ns['n'] = 55
647 with tt.AssertNotPrints('-55'):
647 with tt.AssertNotPrints('-55'):
648 ip.run_cell('print (n)')
648 ip.run_cell('print (n)')
649
649
650 def test_timeit(self):
650 def test_timeit(self):
651 called = set()
651 called = set()
652 def f(x):
652 def f(x):
653 called.add(x)
653 called.add(x)
654 ip.push({'f':f})
654 ip.push({'f':f})
655
655
656 with tt.AssertPrints("std. dev. of"):
656 with tt.AssertPrints("std. dev. of"):
657 ip.run_line_magic("timeit", "-n1 f(1)")
657 ip.run_line_magic("timeit", "-n1 f(1)")
658 self.assertEqual(called, {-1})
658 self.assertEqual(called, {-1})
659 called.clear()
659 called.clear()
660
660
661 with tt.AssertPrints("std. dev. of"):
661 with tt.AssertPrints("std. dev. of"):
662 ip.run_cell_magic("timeit", "-n1 f(2)", "f(3)")
662 ip.run_cell_magic("timeit", "-n1 f(2)", "f(3)")
663 self.assertEqual(called, {-2, -3})
663 self.assertEqual(called, {-2, -3})
664
664
665 def test_time(self):
665 def test_time(self):
666 called = []
666 called = []
667 def f(x):
667 def f(x):
668 called.append(x)
668 called.append(x)
669 ip.push({'f':f})
669 ip.push({'f':f})
670
670
671 # Test with an expression
671 # Test with an expression
672 with tt.AssertPrints("Wall time: "):
672 with tt.AssertPrints("Wall time: "):
673 ip.run_line_magic("time", "f(5+9)")
673 ip.run_line_magic("time", "f(5+9)")
674 self.assertEqual(called, [-14])
674 self.assertEqual(called, [-14])
675 called[:] = []
675 called[:] = []
676
676
677 # Test with a statement (different code path)
677 # Test with a statement (different code path)
678 with tt.AssertPrints("Wall time: "):
678 with tt.AssertPrints("Wall time: "):
679 ip.run_line_magic("time", "a = f(-3 + -2)")
679 ip.run_line_magic("time", "a = f(-3 + -2)")
680 self.assertEqual(called, [5])
680 self.assertEqual(called, [5])
681
681
682 def test_macro(self):
682 def test_macro(self):
683 ip.push({'a':10})
683 ip.push({'a':10})
684 # The AST transformation makes this do a+=-1
684 # The AST transformation makes this do a+=-1
685 ip.define_macro("amacro", "a+=1\nprint(a)")
685 ip.define_macro("amacro", "a+=1\nprint(a)")
686
686
687 with tt.AssertPrints("9"):
687 with tt.AssertPrints("9"):
688 ip.run_cell("amacro")
688 ip.run_cell("amacro")
689 with tt.AssertPrints("8"):
689 with tt.AssertPrints("8"):
690 ip.run_cell("amacro")
690 ip.run_cell("amacro")
691
691
692 class TestMiscTransform(unittest.TestCase):
693
694
695 def test_transform_only_once(self):
696 cleanup = 0
697 line_t = 0
698 def count_cleanup(lines):
699 nonlocal cleanup
700 cleanup += 1
701 return lines
702
703 def count_line_t(lines):
704 nonlocal line_t
705 line_t += 1
706 return lines
707
708 ip.input_transformer_manager.cleanup_transforms.append(count_cleanup)
709 ip.input_transformer_manager.line_transforms.append(count_line_t)
710
711 ip.run_cell('1')
712
713 assert cleanup == 1
714 assert line_t == 1
715
692 class IntegerWrapper(ast.NodeTransformer):
716 class IntegerWrapper(ast.NodeTransformer):
693 """Wraps all integers in a call to Integer()"""
717 """Wraps all integers in a call to Integer()"""
694
718
695 # for Python 3.7 and earlier
719 # for Python 3.7 and earlier
696
720
697 # for Python 3.7 and earlier
721 # for Python 3.7 and earlier
698 def visit_Num(self, node):
722 def visit_Num(self, node):
699 if isinstance(node.n, int):
723 if isinstance(node.n, int):
700 return ast.Call(func=ast.Name(id='Integer', ctx=ast.Load()),
724 return ast.Call(func=ast.Name(id='Integer', ctx=ast.Load()),
701 args=[node], keywords=[])
725 args=[node], keywords=[])
702 return node
726 return node
703
727
704 # For Python 3.8+
728 # For Python 3.8+
705 def visit_Constant(self, node):
729 def visit_Constant(self, node):
706 if isinstance(node.value, int):
730 if isinstance(node.value, int):
707 return self.visit_Num(node)
731 return self.visit_Num(node)
708 return node
732 return node
709
733
710
734
711 class TestAstTransform2(unittest.TestCase):
735 class TestAstTransform2(unittest.TestCase):
712 def setUp(self):
736 def setUp(self):
713 self.intwrapper = IntegerWrapper()
737 self.intwrapper = IntegerWrapper()
714 ip.ast_transformers.append(self.intwrapper)
738 ip.ast_transformers.append(self.intwrapper)
715
739
716 self.calls = []
740 self.calls = []
717 def Integer(*args):
741 def Integer(*args):
718 self.calls.append(args)
742 self.calls.append(args)
719 return args
743 return args
720 ip.push({"Integer": Integer})
744 ip.push({"Integer": Integer})
721
745
722 def tearDown(self):
746 def tearDown(self):
723 ip.ast_transformers.remove(self.intwrapper)
747 ip.ast_transformers.remove(self.intwrapper)
724 del ip.user_ns['Integer']
748 del ip.user_ns['Integer']
725
749
726 def test_run_cell(self):
750 def test_run_cell(self):
727 ip.run_cell("n = 2")
751 ip.run_cell("n = 2")
728 self.assertEqual(self.calls, [(2,)])
752 self.assertEqual(self.calls, [(2,)])
729
753
730 # This shouldn't throw an error
754 # This shouldn't throw an error
731 ip.run_cell("o = 2.0")
755 ip.run_cell("o = 2.0")
732 self.assertEqual(ip.user_ns['o'], 2.0)
756 self.assertEqual(ip.user_ns['o'], 2.0)
733
757
734 def test_timeit(self):
758 def test_timeit(self):
735 called = set()
759 called = set()
736 def f(x):
760 def f(x):
737 called.add(x)
761 called.add(x)
738 ip.push({'f':f})
762 ip.push({'f':f})
739
763
740 with tt.AssertPrints("std. dev. of"):
764 with tt.AssertPrints("std. dev. of"):
741 ip.run_line_magic("timeit", "-n1 f(1)")
765 ip.run_line_magic("timeit", "-n1 f(1)")
742 self.assertEqual(called, {(1,)})
766 self.assertEqual(called, {(1,)})
743 called.clear()
767 called.clear()
744
768
745 with tt.AssertPrints("std. dev. of"):
769 with tt.AssertPrints("std. dev. of"):
746 ip.run_cell_magic("timeit", "-n1 f(2)", "f(3)")
770 ip.run_cell_magic("timeit", "-n1 f(2)", "f(3)")
747 self.assertEqual(called, {(2,), (3,)})
771 self.assertEqual(called, {(2,), (3,)})
748
772
749 class ErrorTransformer(ast.NodeTransformer):
773 class ErrorTransformer(ast.NodeTransformer):
750 """Throws an error when it sees a number."""
774 """Throws an error when it sees a number."""
751
775
752 # for Python 3.7 and earlier
776 # for Python 3.7 and earlier
753 def visit_Num(self, node):
777 def visit_Num(self, node):
754 raise ValueError("test")
778 raise ValueError("test")
755
779
756 # for Python 3.8+
780 # for Python 3.8+
757 def visit_Constant(self, node):
781 def visit_Constant(self, node):
758 if isinstance(node.value, int):
782 if isinstance(node.value, int):
759 return self.visit_Num(node)
783 return self.visit_Num(node)
760 return node
784 return node
761
785
762
786
763 class TestAstTransformError(unittest.TestCase):
787 class TestAstTransformError(unittest.TestCase):
764 def test_unregistering(self):
788 def test_unregistering(self):
765 err_transformer = ErrorTransformer()
789 err_transformer = ErrorTransformer()
766 ip.ast_transformers.append(err_transformer)
790 ip.ast_transformers.append(err_transformer)
767
791
768 with self.assertWarnsRegex(UserWarning, "It will be unregistered"):
792 with self.assertWarnsRegex(UserWarning, "It will be unregistered"):
769 ip.run_cell("1 + 2")
793 ip.run_cell("1 + 2")
770
794
771 # This should have been removed.
795 # This should have been removed.
772 nt.assert_not_in(err_transformer, ip.ast_transformers)
796 nt.assert_not_in(err_transformer, ip.ast_transformers)
773
797
774
798
775 class StringRejector(ast.NodeTransformer):
799 class StringRejector(ast.NodeTransformer):
776 """Throws an InputRejected when it sees a string literal.
800 """Throws an InputRejected when it sees a string literal.
777
801
778 Used to verify that NodeTransformers can signal that a piece of code should
802 Used to verify that NodeTransformers can signal that a piece of code should
779 not be executed by throwing an InputRejected.
803 not be executed by throwing an InputRejected.
780 """
804 """
781
805
782 #for python 3.7 and earlier
806 #for python 3.7 and earlier
783 def visit_Str(self, node):
807 def visit_Str(self, node):
784 raise InputRejected("test")
808 raise InputRejected("test")
785
809
786 # 3.8 only
810 # 3.8 only
787 def visit_Constant(self, node):
811 def visit_Constant(self, node):
788 if isinstance(node.value, str):
812 if isinstance(node.value, str):
789 raise InputRejected("test")
813 raise InputRejected("test")
790 return node
814 return node
791
815
792
816
793 class TestAstTransformInputRejection(unittest.TestCase):
817 class TestAstTransformInputRejection(unittest.TestCase):
794
818
795 def setUp(self):
819 def setUp(self):
796 self.transformer = StringRejector()
820 self.transformer = StringRejector()
797 ip.ast_transformers.append(self.transformer)
821 ip.ast_transformers.append(self.transformer)
798
822
799 def tearDown(self):
823 def tearDown(self):
800 ip.ast_transformers.remove(self.transformer)
824 ip.ast_transformers.remove(self.transformer)
801
825
802 def test_input_rejection(self):
826 def test_input_rejection(self):
803 """Check that NodeTransformers can reject input."""
827 """Check that NodeTransformers can reject input."""
804
828
805 expect_exception_tb = tt.AssertPrints("InputRejected: test")
829 expect_exception_tb = tt.AssertPrints("InputRejected: test")
806 expect_no_cell_output = tt.AssertNotPrints("'unsafe'", suppress=False)
830 expect_no_cell_output = tt.AssertNotPrints("'unsafe'", suppress=False)
807
831
808 # Run the same check twice to verify that the transformer is not
832 # Run the same check twice to verify that the transformer is not
809 # disabled after raising.
833 # disabled after raising.
810 with expect_exception_tb, expect_no_cell_output:
834 with expect_exception_tb, expect_no_cell_output:
811 ip.run_cell("'unsafe'")
835 ip.run_cell("'unsafe'")
812
836
813 with expect_exception_tb, expect_no_cell_output:
837 with expect_exception_tb, expect_no_cell_output:
814 res = ip.run_cell("'unsafe'")
838 res = ip.run_cell("'unsafe'")
815
839
816 self.assertIsInstance(res.error_before_exec, InputRejected)
840 self.assertIsInstance(res.error_before_exec, InputRejected)
817
841
818 def test__IPYTHON__():
842 def test__IPYTHON__():
819 # This shouldn't raise a NameError, that's all
843 # This shouldn't raise a NameError, that's all
820 __IPYTHON__
844 __IPYTHON__
821
845
822
846
823 class DummyRepr(object):
847 class DummyRepr(object):
824 def __repr__(self):
848 def __repr__(self):
825 return "DummyRepr"
849 return "DummyRepr"
826
850
827 def _repr_html_(self):
851 def _repr_html_(self):
828 return "<b>dummy</b>"
852 return "<b>dummy</b>"
829
853
830 def _repr_javascript_(self):
854 def _repr_javascript_(self):
831 return "console.log('hi');", {'key': 'value'}
855 return "console.log('hi');", {'key': 'value'}
832
856
833
857
834 def test_user_variables():
858 def test_user_variables():
835 # enable all formatters
859 # enable all formatters
836 ip.display_formatter.active_types = ip.display_formatter.format_types
860 ip.display_formatter.active_types = ip.display_formatter.format_types
837
861
838 ip.user_ns['dummy'] = d = DummyRepr()
862 ip.user_ns['dummy'] = d = DummyRepr()
839 keys = {'dummy', 'doesnotexist'}
863 keys = {'dummy', 'doesnotexist'}
840 r = ip.user_expressions({ key:key for key in keys})
864 r = ip.user_expressions({ key:key for key in keys})
841
865
842 nt.assert_equal(keys, set(r.keys()))
866 nt.assert_equal(keys, set(r.keys()))
843 dummy = r['dummy']
867 dummy = r['dummy']
844 nt.assert_equal({'status', 'data', 'metadata'}, set(dummy.keys()))
868 nt.assert_equal({'status', 'data', 'metadata'}, set(dummy.keys()))
845 nt.assert_equal(dummy['status'], 'ok')
869 nt.assert_equal(dummy['status'], 'ok')
846 data = dummy['data']
870 data = dummy['data']
847 metadata = dummy['metadata']
871 metadata = dummy['metadata']
848 nt.assert_equal(data.get('text/html'), d._repr_html_())
872 nt.assert_equal(data.get('text/html'), d._repr_html_())
849 js, jsmd = d._repr_javascript_()
873 js, jsmd = d._repr_javascript_()
850 nt.assert_equal(data.get('application/javascript'), js)
874 nt.assert_equal(data.get('application/javascript'), js)
851 nt.assert_equal(metadata.get('application/javascript'), jsmd)
875 nt.assert_equal(metadata.get('application/javascript'), jsmd)
852
876
853 dne = r['doesnotexist']
877 dne = r['doesnotexist']
854 nt.assert_equal(dne['status'], 'error')
878 nt.assert_equal(dne['status'], 'error')
855 nt.assert_equal(dne['ename'], 'NameError')
879 nt.assert_equal(dne['ename'], 'NameError')
856
880
857 # back to text only
881 # back to text only
858 ip.display_formatter.active_types = ['text/plain']
882 ip.display_formatter.active_types = ['text/plain']
859
883
860 def test_user_expression():
884 def test_user_expression():
861 # enable all formatters
885 # enable all formatters
862 ip.display_formatter.active_types = ip.display_formatter.format_types
886 ip.display_formatter.active_types = ip.display_formatter.format_types
863 query = {
887 query = {
864 'a' : '1 + 2',
888 'a' : '1 + 2',
865 'b' : '1/0',
889 'b' : '1/0',
866 }
890 }
867 r = ip.user_expressions(query)
891 r = ip.user_expressions(query)
868 import pprint
892 import pprint
869 pprint.pprint(r)
893 pprint.pprint(r)
870 nt.assert_equal(set(r.keys()), set(query.keys()))
894 nt.assert_equal(set(r.keys()), set(query.keys()))
871 a = r['a']
895 a = r['a']
872 nt.assert_equal({'status', 'data', 'metadata'}, set(a.keys()))
896 nt.assert_equal({'status', 'data', 'metadata'}, set(a.keys()))
873 nt.assert_equal(a['status'], 'ok')
897 nt.assert_equal(a['status'], 'ok')
874 data = a['data']
898 data = a['data']
875 metadata = a['metadata']
899 metadata = a['metadata']
876 nt.assert_equal(data.get('text/plain'), '3')
900 nt.assert_equal(data.get('text/plain'), '3')
877
901
878 b = r['b']
902 b = r['b']
879 nt.assert_equal(b['status'], 'error')
903 nt.assert_equal(b['status'], 'error')
880 nt.assert_equal(b['ename'], 'ZeroDivisionError')
904 nt.assert_equal(b['ename'], 'ZeroDivisionError')
881
905
882 # back to text only
906 # back to text only
883 ip.display_formatter.active_types = ['text/plain']
907 ip.display_formatter.active_types = ['text/plain']
884
908
885
909
886 class TestSyntaxErrorTransformer(unittest.TestCase):
910 class TestSyntaxErrorTransformer(unittest.TestCase):
887 """Check that SyntaxError raised by an input transformer is handled by run_cell()"""
911 """Check that SyntaxError raised by an input transformer is handled by run_cell()"""
888
912
889 @staticmethod
913 @staticmethod
890 def transformer(lines):
914 def transformer(lines):
891 for line in lines:
915 for line in lines:
892 pos = line.find('syntaxerror')
916 pos = line.find('syntaxerror')
893 if pos >= 0:
917 if pos >= 0:
894 e = SyntaxError('input contains "syntaxerror"')
918 e = SyntaxError('input contains "syntaxerror"')
895 e.text = line
919 e.text = line
896 e.offset = pos + 1
920 e.offset = pos + 1
897 raise e
921 raise e
898 return lines
922 return lines
899
923
900 def setUp(self):
924 def setUp(self):
901 ip.input_transformers_post.append(self.transformer)
925 ip.input_transformers_post.append(self.transformer)
902
926
903 def tearDown(self):
927 def tearDown(self):
904 ip.input_transformers_post.remove(self.transformer)
928 ip.input_transformers_post.remove(self.transformer)
905
929
906 def test_syntaxerror_input_transformer(self):
930 def test_syntaxerror_input_transformer(self):
907 with tt.AssertPrints('1234'):
931 with tt.AssertPrints('1234'):
908 ip.run_cell('1234')
932 ip.run_cell('1234')
909 with tt.AssertPrints('SyntaxError: invalid syntax'):
933 with tt.AssertPrints('SyntaxError: invalid syntax'):
910 ip.run_cell('1 2 3') # plain python syntax error
934 ip.run_cell('1 2 3') # plain python syntax error
911 with tt.AssertPrints('SyntaxError: input contains "syntaxerror"'):
935 with tt.AssertPrints('SyntaxError: input contains "syntaxerror"'):
912 ip.run_cell('2345 # syntaxerror') # input transformer syntax error
936 ip.run_cell('2345 # syntaxerror') # input transformer syntax error
913 with tt.AssertPrints('3456'):
937 with tt.AssertPrints('3456'):
914 ip.run_cell('3456')
938 ip.run_cell('3456')
915
939
916
940
917 class TestWarningSuppression(unittest.TestCase):
941 class TestWarningSuppression(unittest.TestCase):
918 def test_warning_suppression(self):
942 def test_warning_suppression(self):
919 ip.run_cell("import warnings")
943 ip.run_cell("import warnings")
920 try:
944 try:
921 with self.assertWarnsRegex(UserWarning, "asdf"):
945 with self.assertWarnsRegex(UserWarning, "asdf"):
922 ip.run_cell("warnings.warn('asdf')")
946 ip.run_cell("warnings.warn('asdf')")
923 # Here's the real test -- if we run that again, we should get the
947 # Here's the real test -- if we run that again, we should get the
924 # warning again. Traditionally, each warning was only issued once per
948 # warning again. Traditionally, each warning was only issued once per
925 # IPython session (approximately), even if the user typed in new and
949 # IPython session (approximately), even if the user typed in new and
926 # different code that should have also triggered the warning, leading
950 # different code that should have also triggered the warning, leading
927 # to much confusion.
951 # to much confusion.
928 with self.assertWarnsRegex(UserWarning, "asdf"):
952 with self.assertWarnsRegex(UserWarning, "asdf"):
929 ip.run_cell("warnings.warn('asdf')")
953 ip.run_cell("warnings.warn('asdf')")
930 finally:
954 finally:
931 ip.run_cell("del warnings")
955 ip.run_cell("del warnings")
932
956
933
957
934 def test_deprecation_warning(self):
958 def test_deprecation_warning(self):
935 ip.run_cell("""
959 ip.run_cell("""
936 import warnings
960 import warnings
937 def wrn():
961 def wrn():
938 warnings.warn(
962 warnings.warn(
939 "I AM A WARNING",
963 "I AM A WARNING",
940 DeprecationWarning
964 DeprecationWarning
941 )
965 )
942 """)
966 """)
943 try:
967 try:
944 with self.assertWarnsRegex(DeprecationWarning, "I AM A WARNING"):
968 with self.assertWarnsRegex(DeprecationWarning, "I AM A WARNING"):
945 ip.run_cell("wrn()")
969 ip.run_cell("wrn()")
946 finally:
970 finally:
947 ip.run_cell("del warnings")
971 ip.run_cell("del warnings")
948 ip.run_cell("del wrn")
972 ip.run_cell("del wrn")
949
973
950
974
951 class TestImportNoDeprecate(tt.TempFileMixin):
975 class TestImportNoDeprecate(tt.TempFileMixin):
952
976
953 def setUp(self):
977 def setUp(self):
954 """Make a valid python temp file."""
978 """Make a valid python temp file."""
955 self.mktmp("""
979 self.mktmp("""
956 import warnings
980 import warnings
957 def wrn():
981 def wrn():
958 warnings.warn(
982 warnings.warn(
959 "I AM A WARNING",
983 "I AM A WARNING",
960 DeprecationWarning
984 DeprecationWarning
961 )
985 )
962 """)
986 """)
963 super().setUp()
987 super().setUp()
964
988
965 def test_no_dep(self):
989 def test_no_dep(self):
966 """
990 """
967 No deprecation warning should be raised from imported functions
991 No deprecation warning should be raised from imported functions
968 """
992 """
969 ip.run_cell("from {} import wrn".format(self.fname))
993 ip.run_cell("from {} import wrn".format(self.fname))
970
994
971 with tt.AssertNotPrints("I AM A WARNING"):
995 with tt.AssertNotPrints("I AM A WARNING"):
972 ip.run_cell("wrn()")
996 ip.run_cell("wrn()")
973 ip.run_cell("del wrn")
997 ip.run_cell("del wrn")
974
998
975
999
976 def test_custom_exc_count():
1000 def test_custom_exc_count():
977 hook = mock.Mock(return_value=None)
1001 hook = mock.Mock(return_value=None)
978 ip.set_custom_exc((SyntaxError,), hook)
1002 ip.set_custom_exc((SyntaxError,), hook)
979 before = ip.execution_count
1003 before = ip.execution_count
980 ip.run_cell("def foo()", store_history=True)
1004 ip.run_cell("def foo()", store_history=True)
981 # restore default excepthook
1005 # restore default excepthook
982 ip.set_custom_exc((), None)
1006 ip.set_custom_exc((), None)
983 nt.assert_equal(hook.call_count, 1)
1007 nt.assert_equal(hook.call_count, 1)
984 nt.assert_equal(ip.execution_count, before + 1)
1008 nt.assert_equal(ip.execution_count, before + 1)
985
1009
986
1010
987 def test_run_cell_async():
1011 def test_run_cell_async():
988 loop = asyncio.get_event_loop()
1012 loop = asyncio.get_event_loop()
989 ip.run_cell("import asyncio")
1013 ip.run_cell("import asyncio")
990 coro = ip.run_cell_async("await asyncio.sleep(0.01)\n5")
1014 coro = ip.run_cell_async("await asyncio.sleep(0.01)\n5")
991 assert asyncio.iscoroutine(coro)
1015 assert asyncio.iscoroutine(coro)
992 result = loop.run_until_complete(coro)
1016 result = loop.run_until_complete(coro)
993 assert isinstance(result, interactiveshell.ExecutionResult)
1017 assert isinstance(result, interactiveshell.ExecutionResult)
994 assert result.result == 5
1018 assert result.result == 5
995
1019
996
1020
997 def test_should_run_async():
1021 def test_should_run_async():
998 assert not ip.should_run_async("a = 5")
1022 assert not ip.should_run_async("a = 5")
999 assert ip.should_run_async("await x")
1023 assert ip.should_run_async("await x")
1000 assert ip.should_run_async("import asyncio; await asyncio.sleep(1)")
1024 assert ip.should_run_async("import asyncio; await asyncio.sleep(1)")
1001
1025
1002
1026
1003 def test_set_custom_completer():
1027 def test_set_custom_completer():
1004 num_completers = len(ip.Completer.matchers)
1028 num_completers = len(ip.Completer.matchers)
1005
1029
1006 def foo(*args, **kwargs):
1030 def foo(*args, **kwargs):
1007 return "I'm a completer!"
1031 return "I'm a completer!"
1008
1032
1009 ip.set_custom_completer(foo, 0)
1033 ip.set_custom_completer(foo, 0)
1010
1034
1011 # check that we've really added a new completer
1035 # check that we've really added a new completer
1012 assert len(ip.Completer.matchers) == num_completers + 1
1036 assert len(ip.Completer.matchers) == num_completers + 1
1013
1037
1014 # check that the first completer is the function we defined
1038 # check that the first completer is the function we defined
1015 assert ip.Completer.matchers[0]() == "I'm a completer!"
1039 assert ip.Completer.matchers[0]() == "I'm a completer!"
1016
1040
1017 # clean up
1041 # clean up
1018 ip.Completer.custom_matchers.pop()
1042 ip.Completer.custom_matchers.pop()
General Comments 0
You need to be logged in to leave comments. Login now