##// END OF EJS Templates
Start integrating new input transformation machinery into InteractiveShell
Thomas Kluyver -
Show More

The requested changes are too big and content was truncated. Show full diff

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