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