##// END OF EJS Templates
Backport PR #13625: Remove set-next input when triggering help.
Matthias Bussonnier -
Show More
@@ -1,536 +1,534 b''
1 """DEPRECATED: Input transformer classes to support IPython special syntax.
1 """DEPRECATED: Input transformer classes to support IPython special syntax.
2
2
3 This module was deprecated in IPython 7.0, in favour of inputtransformer2.
3 This module was deprecated in IPython 7.0, in favour of inputtransformer2.
4
4
5 This includes the machinery to recognise and transform ``%magic`` commands,
5 This includes the machinery to recognise and transform ``%magic`` commands,
6 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
6 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
7 """
7 """
8 import abc
8 import abc
9 import functools
9 import functools
10 import re
10 import re
11 import tokenize
11 import tokenize
12 from tokenize import generate_tokens, untokenize, TokenError
12 from tokenize import generate_tokens, untokenize, TokenError
13 from io import StringIO
13 from io import StringIO
14
14
15 from IPython.core.splitinput import LineInfo
15 from IPython.core.splitinput import LineInfo
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Globals
18 # Globals
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 # The escape sequences that define the syntax transformations IPython will
21 # The escape sequences that define the syntax transformations IPython will
22 # apply to user input. These can NOT be just changed here: many regular
22 # apply to user input. These can NOT be just changed here: many regular
23 # expressions and other parts of the code may use their hardcoded values, and
23 # expressions and other parts of the code may use their hardcoded values, and
24 # for all intents and purposes they constitute the 'IPython syntax', so they
24 # for all intents and purposes they constitute the 'IPython syntax', so they
25 # should be considered fixed.
25 # should be considered fixed.
26
26
27 ESC_SHELL = '!' # Send line to underlying system shell
27 ESC_SHELL = '!' # Send line to underlying system shell
28 ESC_SH_CAP = '!!' # Send line to system shell and capture output
28 ESC_SH_CAP = '!!' # Send line to system shell and capture output
29 ESC_HELP = '?' # Find information about object
29 ESC_HELP = '?' # Find information about object
30 ESC_HELP2 = '??' # Find extra-detailed information about object
30 ESC_HELP2 = '??' # Find extra-detailed information about object
31 ESC_MAGIC = '%' # Call magic function
31 ESC_MAGIC = '%' # Call magic function
32 ESC_MAGIC2 = '%%' # Call cell-magic function
32 ESC_MAGIC2 = '%%' # Call cell-magic function
33 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
33 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
34 ESC_QUOTE2 = ';' # Quote all args as a single string, call
34 ESC_QUOTE2 = ';' # Quote all args as a single string, call
35 ESC_PAREN = '/' # Call first argument with rest of line as arguments
35 ESC_PAREN = '/' # Call first argument with rest of line as arguments
36
36
37 ESC_SEQUENCES = [ESC_SHELL, ESC_SH_CAP, ESC_HELP ,\
37 ESC_SEQUENCES = [ESC_SHELL, ESC_SH_CAP, ESC_HELP ,\
38 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,\
38 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,\
39 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN ]
39 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN ]
40
40
41
41
42 class InputTransformer(metaclass=abc.ABCMeta):
42 class InputTransformer(metaclass=abc.ABCMeta):
43 """Abstract base class for line-based input transformers."""
43 """Abstract base class for line-based input transformers."""
44
44
45 @abc.abstractmethod
45 @abc.abstractmethod
46 def push(self, line):
46 def push(self, line):
47 """Send a line of input to the transformer, returning the transformed
47 """Send a line of input to the transformer, returning the transformed
48 input or None if the transformer is waiting for more input.
48 input or None if the transformer is waiting for more input.
49
49
50 Must be overridden by subclasses.
50 Must be overridden by subclasses.
51
51
52 Implementations may raise ``SyntaxError`` if the input is invalid. No
52 Implementations may raise ``SyntaxError`` if the input is invalid. No
53 other exceptions may be raised.
53 other exceptions may be raised.
54 """
54 """
55 pass
55 pass
56
56
57 @abc.abstractmethod
57 @abc.abstractmethod
58 def reset(self):
58 def reset(self):
59 """Return, transformed any lines that the transformer has accumulated,
59 """Return, transformed any lines that the transformer has accumulated,
60 and reset its internal state.
60 and reset its internal state.
61
61
62 Must be overridden by subclasses.
62 Must be overridden by subclasses.
63 """
63 """
64 pass
64 pass
65
65
66 @classmethod
66 @classmethod
67 def wrap(cls, func):
67 def wrap(cls, func):
68 """Can be used by subclasses as a decorator, to return a factory that
68 """Can be used by subclasses as a decorator, to return a factory that
69 will allow instantiation with the decorated object.
69 will allow instantiation with the decorated object.
70 """
70 """
71 @functools.wraps(func)
71 @functools.wraps(func)
72 def transformer_factory(**kwargs):
72 def transformer_factory(**kwargs):
73 return cls(func, **kwargs)
73 return cls(func, **kwargs)
74
74
75 return transformer_factory
75 return transformer_factory
76
76
77 class StatelessInputTransformer(InputTransformer):
77 class StatelessInputTransformer(InputTransformer):
78 """Wrapper for a stateless input transformer implemented as a function."""
78 """Wrapper for a stateless input transformer implemented as a function."""
79 def __init__(self, func):
79 def __init__(self, func):
80 self.func = func
80 self.func = func
81
81
82 def __repr__(self):
82 def __repr__(self):
83 return "StatelessInputTransformer(func={0!r})".format(self.func)
83 return "StatelessInputTransformer(func={0!r})".format(self.func)
84
84
85 def push(self, line):
85 def push(self, line):
86 """Send a line of input to the transformer, returning the
86 """Send a line of input to the transformer, returning the
87 transformed input."""
87 transformed input."""
88 return self.func(line)
88 return self.func(line)
89
89
90 def reset(self):
90 def reset(self):
91 """No-op - exists for compatibility."""
91 """No-op - exists for compatibility."""
92 pass
92 pass
93
93
94 class CoroutineInputTransformer(InputTransformer):
94 class CoroutineInputTransformer(InputTransformer):
95 """Wrapper for an input transformer implemented as a coroutine."""
95 """Wrapper for an input transformer implemented as a coroutine."""
96 def __init__(self, coro, **kwargs):
96 def __init__(self, coro, **kwargs):
97 # Prime it
97 # Prime it
98 self.coro = coro(**kwargs)
98 self.coro = coro(**kwargs)
99 next(self.coro)
99 next(self.coro)
100
100
101 def __repr__(self):
101 def __repr__(self):
102 return "CoroutineInputTransformer(coro={0!r})".format(self.coro)
102 return "CoroutineInputTransformer(coro={0!r})".format(self.coro)
103
103
104 def push(self, line):
104 def push(self, line):
105 """Send a line of input to the transformer, returning the
105 """Send a line of input to the transformer, returning the
106 transformed input or None if the transformer is waiting for more
106 transformed input or None if the transformer is waiting for more
107 input.
107 input.
108 """
108 """
109 return self.coro.send(line)
109 return self.coro.send(line)
110
110
111 def reset(self):
111 def reset(self):
112 """Return, transformed any lines that the transformer has
112 """Return, transformed any lines that the transformer has
113 accumulated, and reset its internal state.
113 accumulated, and reset its internal state.
114 """
114 """
115 return self.coro.send(None)
115 return self.coro.send(None)
116
116
117 class TokenInputTransformer(InputTransformer):
117 class TokenInputTransformer(InputTransformer):
118 """Wrapper for a token-based input transformer.
118 """Wrapper for a token-based input transformer.
119
119
120 func should accept a list of tokens (5-tuples, see tokenize docs), and
120 func should accept a list of tokens (5-tuples, see tokenize docs), and
121 return an iterable which can be passed to tokenize.untokenize().
121 return an iterable which can be passed to tokenize.untokenize().
122 """
122 """
123 def __init__(self, func):
123 def __init__(self, func):
124 self.func = func
124 self.func = func
125 self.buf = []
125 self.buf = []
126 self.reset_tokenizer()
126 self.reset_tokenizer()
127
127
128 def reset_tokenizer(self):
128 def reset_tokenizer(self):
129 it = iter(self.buf)
129 it = iter(self.buf)
130 self.tokenizer = generate_tokens(it.__next__)
130 self.tokenizer = generate_tokens(it.__next__)
131
131
132 def push(self, line):
132 def push(self, line):
133 self.buf.append(line + '\n')
133 self.buf.append(line + '\n')
134 if all(l.isspace() for l in self.buf):
134 if all(l.isspace() for l in self.buf):
135 return self.reset()
135 return self.reset()
136
136
137 tokens = []
137 tokens = []
138 stop_at_NL = False
138 stop_at_NL = False
139 try:
139 try:
140 for intok in self.tokenizer:
140 for intok in self.tokenizer:
141 tokens.append(intok)
141 tokens.append(intok)
142 t = intok[0]
142 t = intok[0]
143 if t == tokenize.NEWLINE or (stop_at_NL and t == tokenize.NL):
143 if t == tokenize.NEWLINE or (stop_at_NL and t == tokenize.NL):
144 # Stop before we try to pull a line we don't have yet
144 # Stop before we try to pull a line we don't have yet
145 break
145 break
146 elif t == tokenize.ERRORTOKEN:
146 elif t == tokenize.ERRORTOKEN:
147 stop_at_NL = True
147 stop_at_NL = True
148 except TokenError:
148 except TokenError:
149 # Multi-line statement - stop and try again with the next line
149 # Multi-line statement - stop and try again with the next line
150 self.reset_tokenizer()
150 self.reset_tokenizer()
151 return None
151 return None
152
152
153 return self.output(tokens)
153 return self.output(tokens)
154
154
155 def output(self, tokens):
155 def output(self, tokens):
156 self.buf.clear()
156 self.buf.clear()
157 self.reset_tokenizer()
157 self.reset_tokenizer()
158 return untokenize(self.func(tokens)).rstrip('\n')
158 return untokenize(self.func(tokens)).rstrip('\n')
159
159
160 def reset(self):
160 def reset(self):
161 l = ''.join(self.buf)
161 l = ''.join(self.buf)
162 self.buf.clear()
162 self.buf.clear()
163 self.reset_tokenizer()
163 self.reset_tokenizer()
164 if l:
164 if l:
165 return l.rstrip('\n')
165 return l.rstrip('\n')
166
166
167 class assemble_python_lines(TokenInputTransformer):
167 class assemble_python_lines(TokenInputTransformer):
168 def __init__(self):
168 def __init__(self):
169 super(assemble_python_lines, self).__init__(None)
169 super(assemble_python_lines, self).__init__(None)
170
170
171 def output(self, tokens):
171 def output(self, tokens):
172 return self.reset()
172 return self.reset()
173
173
174 @CoroutineInputTransformer.wrap
174 @CoroutineInputTransformer.wrap
175 def assemble_logical_lines():
175 def assemble_logical_lines():
176 r"""Join lines following explicit line continuations (\)"""
176 r"""Join lines following explicit line continuations (\)"""
177 line = ''
177 line = ''
178 while True:
178 while True:
179 line = (yield line)
179 line = (yield line)
180 if not line or line.isspace():
180 if not line or line.isspace():
181 continue
181 continue
182
182
183 parts = []
183 parts = []
184 while line is not None:
184 while line is not None:
185 if line.endswith('\\') and (not has_comment(line)):
185 if line.endswith('\\') and (not has_comment(line)):
186 parts.append(line[:-1])
186 parts.append(line[:-1])
187 line = (yield None) # Get another line
187 line = (yield None) # Get another line
188 else:
188 else:
189 parts.append(line)
189 parts.append(line)
190 break
190 break
191
191
192 # Output
192 # Output
193 line = ''.join(parts)
193 line = ''.join(parts)
194
194
195 # Utilities
195 # Utilities
196 def _make_help_call(target, esc, lspace, next_input=None):
196 def _make_help_call(target, esc, lspace):
197 """Prepares a pinfo(2)/psearch call from a target name and the escape
197 """Prepares a pinfo(2)/psearch call from a target name and the escape
198 (i.e. ? or ??)"""
198 (i.e. ? or ??)"""
199 method = 'pinfo2' if esc == '??' \
199 method = 'pinfo2' if esc == '??' \
200 else 'psearch' if '*' in target \
200 else 'psearch' if '*' in target \
201 else 'pinfo'
201 else 'pinfo'
202 arg = " ".join([method, target])
202 arg = " ".join([method, target])
203 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
203 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
204 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
204 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
205 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
205 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
206 if next_input is None:
206 return "%sget_ipython().run_line_magic(%r, %r)" % (
207 return '%sget_ipython().run_line_magic(%r, %r)' % (lspace, t_magic_name, t_magic_arg_s)
207 lspace,
208 else:
208 t_magic_name,
209 return '%sget_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
209 t_magic_arg_s,
210 (lspace, next_input, t_magic_name, t_magic_arg_s)
210 )
211
211
212
212 # These define the transformations for the different escape characters.
213 # These define the transformations for the different escape characters.
213 def _tr_system(line_info):
214 def _tr_system(line_info):
214 "Translate lines escaped with: !"
215 "Translate lines escaped with: !"
215 cmd = line_info.line.lstrip().lstrip(ESC_SHELL)
216 cmd = line_info.line.lstrip().lstrip(ESC_SHELL)
216 return '%sget_ipython().system(%r)' % (line_info.pre, cmd)
217 return '%sget_ipython().system(%r)' % (line_info.pre, cmd)
217
218
218 def _tr_system2(line_info):
219 def _tr_system2(line_info):
219 "Translate lines escaped with: !!"
220 "Translate lines escaped with: !!"
220 cmd = line_info.line.lstrip()[2:]
221 cmd = line_info.line.lstrip()[2:]
221 return '%sget_ipython().getoutput(%r)' % (line_info.pre, cmd)
222 return '%sget_ipython().getoutput(%r)' % (line_info.pre, cmd)
222
223
223 def _tr_help(line_info):
224 def _tr_help(line_info):
224 "Translate lines escaped with: ?/??"
225 "Translate lines escaped with: ?/??"
225 # A naked help line should just fire the intro help screen
226 # A naked help line should just fire the intro help screen
226 if not line_info.line[1:]:
227 if not line_info.line[1:]:
227 return 'get_ipython().show_usage()'
228 return 'get_ipython().show_usage()'
228
229
229 return _make_help_call(line_info.ifun, line_info.esc, line_info.pre)
230 return _make_help_call(line_info.ifun, line_info.esc, line_info.pre)
230
231
231 def _tr_magic(line_info):
232 def _tr_magic(line_info):
232 "Translate lines escaped with: %"
233 "Translate lines escaped with: %"
233 tpl = '%sget_ipython().run_line_magic(%r, %r)'
234 tpl = '%sget_ipython().run_line_magic(%r, %r)'
234 if line_info.line.startswith(ESC_MAGIC2):
235 if line_info.line.startswith(ESC_MAGIC2):
235 return line_info.line
236 return line_info.line
236 cmd = ' '.join([line_info.ifun, line_info.the_rest]).strip()
237 cmd = ' '.join([line_info.ifun, line_info.the_rest]).strip()
237 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
238 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
238 t_magic_name, _, t_magic_arg_s = cmd.partition(' ')
239 t_magic_name, _, t_magic_arg_s = cmd.partition(' ')
239 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
240 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
240 return tpl % (line_info.pre, t_magic_name, t_magic_arg_s)
241 return tpl % (line_info.pre, t_magic_name, t_magic_arg_s)
241
242
242 def _tr_quote(line_info):
243 def _tr_quote(line_info):
243 "Translate lines escaped with: ,"
244 "Translate lines escaped with: ,"
244 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
245 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
245 '", "'.join(line_info.the_rest.split()) )
246 '", "'.join(line_info.the_rest.split()) )
246
247
247 def _tr_quote2(line_info):
248 def _tr_quote2(line_info):
248 "Translate lines escaped with: ;"
249 "Translate lines escaped with: ;"
249 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
250 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
250 line_info.the_rest)
251 line_info.the_rest)
251
252
252 def _tr_paren(line_info):
253 def _tr_paren(line_info):
253 "Translate lines escaped with: /"
254 "Translate lines escaped with: /"
254 return '%s%s(%s)' % (line_info.pre, line_info.ifun,
255 return '%s%s(%s)' % (line_info.pre, line_info.ifun,
255 ", ".join(line_info.the_rest.split()))
256 ", ".join(line_info.the_rest.split()))
256
257
257 tr = { ESC_SHELL : _tr_system,
258 tr = { ESC_SHELL : _tr_system,
258 ESC_SH_CAP : _tr_system2,
259 ESC_SH_CAP : _tr_system2,
259 ESC_HELP : _tr_help,
260 ESC_HELP : _tr_help,
260 ESC_HELP2 : _tr_help,
261 ESC_HELP2 : _tr_help,
261 ESC_MAGIC : _tr_magic,
262 ESC_MAGIC : _tr_magic,
262 ESC_QUOTE : _tr_quote,
263 ESC_QUOTE : _tr_quote,
263 ESC_QUOTE2 : _tr_quote2,
264 ESC_QUOTE2 : _tr_quote2,
264 ESC_PAREN : _tr_paren }
265 ESC_PAREN : _tr_paren }
265
266
266 @StatelessInputTransformer.wrap
267 @StatelessInputTransformer.wrap
267 def escaped_commands(line):
268 def escaped_commands(line):
268 """Transform escaped commands - %magic, !system, ?help + various autocalls.
269 """Transform escaped commands - %magic, !system, ?help + various autocalls.
269 """
270 """
270 if not line or line.isspace():
271 if not line or line.isspace():
271 return line
272 return line
272 lineinf = LineInfo(line)
273 lineinf = LineInfo(line)
273 if lineinf.esc not in tr:
274 if lineinf.esc not in tr:
274 return line
275 return line
275
276
276 return tr[lineinf.esc](lineinf)
277 return tr[lineinf.esc](lineinf)
277
278
278 _initial_space_re = re.compile(r'\s*')
279 _initial_space_re = re.compile(r'\s*')
279
280
280 _help_end_re = re.compile(r"""(%{0,2}
281 _help_end_re = re.compile(r"""(%{0,2}
281 (?!\d)[\w*]+ # Variable name
282 (?!\d)[\w*]+ # Variable name
282 (\.(?!\d)[\w*]+)* # .etc.etc
283 (\.(?!\d)[\w*]+)* # .etc.etc
283 )
284 )
284 (\?\??)$ # ? or ??
285 (\?\??)$ # ? or ??
285 """,
286 """,
286 re.VERBOSE)
287 re.VERBOSE)
287
288
288 # Extra pseudotokens for multiline strings and data structures
289 # Extra pseudotokens for multiline strings and data structures
289 _MULTILINE_STRING = object()
290 _MULTILINE_STRING = object()
290 _MULTILINE_STRUCTURE = object()
291 _MULTILINE_STRUCTURE = object()
291
292
292 def _line_tokens(line):
293 def _line_tokens(line):
293 """Helper for has_comment and ends_in_comment_or_string."""
294 """Helper for has_comment and ends_in_comment_or_string."""
294 readline = StringIO(line).readline
295 readline = StringIO(line).readline
295 toktypes = set()
296 toktypes = set()
296 try:
297 try:
297 for t in generate_tokens(readline):
298 for t in generate_tokens(readline):
298 toktypes.add(t[0])
299 toktypes.add(t[0])
299 except TokenError as e:
300 except TokenError as e:
300 # There are only two cases where a TokenError is raised.
301 # There are only two cases where a TokenError is raised.
301 if 'multi-line string' in e.args[0]:
302 if 'multi-line string' in e.args[0]:
302 toktypes.add(_MULTILINE_STRING)
303 toktypes.add(_MULTILINE_STRING)
303 else:
304 else:
304 toktypes.add(_MULTILINE_STRUCTURE)
305 toktypes.add(_MULTILINE_STRUCTURE)
305 return toktypes
306 return toktypes
306
307
307 def has_comment(src):
308 def has_comment(src):
308 """Indicate whether an input line has (i.e. ends in, or is) a comment.
309 """Indicate whether an input line has (i.e. ends in, or is) a comment.
309
310
310 This uses tokenize, so it can distinguish comments from # inside strings.
311 This uses tokenize, so it can distinguish comments from # inside strings.
311
312
312 Parameters
313 Parameters
313 ----------
314 ----------
314 src : string
315 src : string
315 A single line input string.
316 A single line input string.
316
317
317 Returns
318 Returns
318 -------
319 -------
319 comment : bool
320 comment : bool
320 True if source has a comment.
321 True if source has a comment.
321 """
322 """
322 return (tokenize.COMMENT in _line_tokens(src))
323 return (tokenize.COMMENT in _line_tokens(src))
323
324
324 def ends_in_comment_or_string(src):
325 def ends_in_comment_or_string(src):
325 """Indicates whether or not an input line ends in a comment or within
326 """Indicates whether or not an input line ends in a comment or within
326 a multiline string.
327 a multiline string.
327
328
328 Parameters
329 Parameters
329 ----------
330 ----------
330 src : string
331 src : string
331 A single line input string.
332 A single line input string.
332
333
333 Returns
334 Returns
334 -------
335 -------
335 comment : bool
336 comment : bool
336 True if source ends in a comment or multiline string.
337 True if source ends in a comment or multiline string.
337 """
338 """
338 toktypes = _line_tokens(src)
339 toktypes = _line_tokens(src)
339 return (tokenize.COMMENT in toktypes) or (_MULTILINE_STRING in toktypes)
340 return (tokenize.COMMENT in toktypes) or (_MULTILINE_STRING in toktypes)
340
341
341
342
342 @StatelessInputTransformer.wrap
343 @StatelessInputTransformer.wrap
343 def help_end(line):
344 def help_end(line):
344 """Translate lines with ?/?? at the end"""
345 """Translate lines with ?/?? at the end"""
345 m = _help_end_re.search(line)
346 m = _help_end_re.search(line)
346 if m is None or ends_in_comment_or_string(line):
347 if m is None or ends_in_comment_or_string(line):
347 return line
348 return line
348 target = m.group(1)
349 target = m.group(1)
349 esc = m.group(3)
350 esc = m.group(3)
350 lspace = _initial_space_re.match(line).group(0)
351 lspace = _initial_space_re.match(line).group(0)
351
352
352 # If we're mid-command, put it back on the next prompt for the user.
353 return _make_help_call(target, esc, lspace)
353 next_input = line.rstrip('?') if line.strip() != m.group(0) else None
354
355 return _make_help_call(target, esc, lspace, next_input)
356
354
357
355
358 @CoroutineInputTransformer.wrap
356 @CoroutineInputTransformer.wrap
359 def cellmagic(end_on_blank_line=False):
357 def cellmagic(end_on_blank_line=False):
360 """Captures & transforms cell magics.
358 """Captures & transforms cell magics.
361
359
362 After a cell magic is started, this stores up any lines it gets until it is
360 After a cell magic is started, this stores up any lines it gets until it is
363 reset (sent None).
361 reset (sent None).
364 """
362 """
365 tpl = 'get_ipython().run_cell_magic(%r, %r, %r)'
363 tpl = 'get_ipython().run_cell_magic(%r, %r, %r)'
366 cellmagic_help_re = re.compile(r'%%\w+\?')
364 cellmagic_help_re = re.compile(r'%%\w+\?')
367 line = ''
365 line = ''
368 while True:
366 while True:
369 line = (yield line)
367 line = (yield line)
370 # consume leading empty lines
368 # consume leading empty lines
371 while not line:
369 while not line:
372 line = (yield line)
370 line = (yield line)
373
371
374 if not line.startswith(ESC_MAGIC2):
372 if not line.startswith(ESC_MAGIC2):
375 # This isn't a cell magic, idle waiting for reset then start over
373 # This isn't a cell magic, idle waiting for reset then start over
376 while line is not None:
374 while line is not None:
377 line = (yield line)
375 line = (yield line)
378 continue
376 continue
379
377
380 if cellmagic_help_re.match(line):
378 if cellmagic_help_re.match(line):
381 # This case will be handled by help_end
379 # This case will be handled by help_end
382 continue
380 continue
383
381
384 first = line
382 first = line
385 body = []
383 body = []
386 line = (yield None)
384 line = (yield None)
387 while (line is not None) and \
385 while (line is not None) and \
388 ((line.strip() != '') or not end_on_blank_line):
386 ((line.strip() != '') or not end_on_blank_line):
389 body.append(line)
387 body.append(line)
390 line = (yield None)
388 line = (yield None)
391
389
392 # Output
390 # Output
393 magic_name, _, first = first.partition(' ')
391 magic_name, _, first = first.partition(' ')
394 magic_name = magic_name.lstrip(ESC_MAGIC2)
392 magic_name = magic_name.lstrip(ESC_MAGIC2)
395 line = tpl % (magic_name, first, u'\n'.join(body))
393 line = tpl % (magic_name, first, u'\n'.join(body))
396
394
397
395
398 def _strip_prompts(prompt_re, initial_re=None, turnoff_re=None):
396 def _strip_prompts(prompt_re, initial_re=None, turnoff_re=None):
399 """Remove matching input prompts from a block of input.
397 """Remove matching input prompts from a block of input.
400
398
401 Parameters
399 Parameters
402 ----------
400 ----------
403 prompt_re : regular expression
401 prompt_re : regular expression
404 A regular expression matching any input prompt (including continuation)
402 A regular expression matching any input prompt (including continuation)
405 initial_re : regular expression, optional
403 initial_re : regular expression, optional
406 A regular expression matching only the initial prompt, but not continuation.
404 A regular expression matching only the initial prompt, but not continuation.
407 If no initial expression is given, prompt_re will be used everywhere.
405 If no initial expression is given, prompt_re will be used everywhere.
408 Used mainly for plain Python prompts, where the continuation prompt
406 Used mainly for plain Python prompts, where the continuation prompt
409 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
407 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
410
408
411 If initial_re and prompt_re differ,
409 If initial_re and prompt_re differ,
412 only initial_re will be tested against the first line.
410 only initial_re will be tested against the first line.
413 If any prompt is found on the first two lines,
411 If any prompt is found on the first two lines,
414 prompts will be stripped from the rest of the block.
412 prompts will be stripped from the rest of the block.
415 """
413 """
416 if initial_re is None:
414 if initial_re is None:
417 initial_re = prompt_re
415 initial_re = prompt_re
418 line = ''
416 line = ''
419 while True:
417 while True:
420 line = (yield line)
418 line = (yield line)
421
419
422 # First line of cell
420 # First line of cell
423 if line is None:
421 if line is None:
424 continue
422 continue
425 out, n1 = initial_re.subn('', line, count=1)
423 out, n1 = initial_re.subn('', line, count=1)
426 if turnoff_re and not n1:
424 if turnoff_re and not n1:
427 if turnoff_re.match(line):
425 if turnoff_re.match(line):
428 # We're in e.g. a cell magic; disable this transformer for
426 # We're in e.g. a cell magic; disable this transformer for
429 # the rest of the cell.
427 # the rest of the cell.
430 while line is not None:
428 while line is not None:
431 line = (yield line)
429 line = (yield line)
432 continue
430 continue
433
431
434 line = (yield out)
432 line = (yield out)
435
433
436 if line is None:
434 if line is None:
437 continue
435 continue
438 # check for any prompt on the second line of the cell,
436 # check for any prompt on the second line of the cell,
439 # because people often copy from just after the first prompt,
437 # because people often copy from just after the first prompt,
440 # so we might not see it in the first line.
438 # so we might not see it in the first line.
441 out, n2 = prompt_re.subn('', line, count=1)
439 out, n2 = prompt_re.subn('', line, count=1)
442 line = (yield out)
440 line = (yield out)
443
441
444 if n1 or n2:
442 if n1 or n2:
445 # Found a prompt in the first two lines - check for it in
443 # Found a prompt in the first two lines - check for it in
446 # the rest of the cell as well.
444 # the rest of the cell as well.
447 while line is not None:
445 while line is not None:
448 line = (yield prompt_re.sub('', line, count=1))
446 line = (yield prompt_re.sub('', line, count=1))
449
447
450 else:
448 else:
451 # Prompts not in input - wait for reset
449 # Prompts not in input - wait for reset
452 while line is not None:
450 while line is not None:
453 line = (yield line)
451 line = (yield line)
454
452
455 @CoroutineInputTransformer.wrap
453 @CoroutineInputTransformer.wrap
456 def classic_prompt():
454 def classic_prompt():
457 """Strip the >>>/... prompts of the Python interactive shell."""
455 """Strip the >>>/... prompts of the Python interactive shell."""
458 # FIXME: non-capturing version (?:...) usable?
456 # FIXME: non-capturing version (?:...) usable?
459 prompt_re = re.compile(r'^(>>>|\.\.\.)( |$)')
457 prompt_re = re.compile(r'^(>>>|\.\.\.)( |$)')
460 initial_re = re.compile(r'^>>>( |$)')
458 initial_re = re.compile(r'^>>>( |$)')
461 # Any %magic/!system is IPython syntax, so we needn't look for >>> prompts
459 # Any %magic/!system is IPython syntax, so we needn't look for >>> prompts
462 turnoff_re = re.compile(r'^[%!]')
460 turnoff_re = re.compile(r'^[%!]')
463 return _strip_prompts(prompt_re, initial_re, turnoff_re)
461 return _strip_prompts(prompt_re, initial_re, turnoff_re)
464
462
465 @CoroutineInputTransformer.wrap
463 @CoroutineInputTransformer.wrap
466 def ipy_prompt():
464 def ipy_prompt():
467 """Strip IPython's In [1]:/...: prompts."""
465 """Strip IPython's In [1]:/...: prompts."""
468 # FIXME: non-capturing version (?:...) usable?
466 # FIXME: non-capturing version (?:...) usable?
469 prompt_re = re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)')
467 prompt_re = re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)')
470 # Disable prompt stripping inside cell magics
468 # Disable prompt stripping inside cell magics
471 turnoff_re = re.compile(r'^%%')
469 turnoff_re = re.compile(r'^%%')
472 return _strip_prompts(prompt_re, turnoff_re=turnoff_re)
470 return _strip_prompts(prompt_re, turnoff_re=turnoff_re)
473
471
474
472
475 @CoroutineInputTransformer.wrap
473 @CoroutineInputTransformer.wrap
476 def leading_indent():
474 def leading_indent():
477 """Remove leading indentation.
475 """Remove leading indentation.
478
476
479 If the first line starts with a spaces or tabs, the same whitespace will be
477 If the first line starts with a spaces or tabs, the same whitespace will be
480 removed from each following line until it is reset.
478 removed from each following line until it is reset.
481 """
479 """
482 space_re = re.compile(r'^[ \t]+')
480 space_re = re.compile(r'^[ \t]+')
483 line = ''
481 line = ''
484 while True:
482 while True:
485 line = (yield line)
483 line = (yield line)
486
484
487 if line is None:
485 if line is None:
488 continue
486 continue
489
487
490 m = space_re.match(line)
488 m = space_re.match(line)
491 if m:
489 if m:
492 space = m.group(0)
490 space = m.group(0)
493 while line is not None:
491 while line is not None:
494 if line.startswith(space):
492 if line.startswith(space):
495 line = line[len(space):]
493 line = line[len(space):]
496 line = (yield line)
494 line = (yield line)
497 else:
495 else:
498 # No leading spaces - wait for reset
496 # No leading spaces - wait for reset
499 while line is not None:
497 while line is not None:
500 line = (yield line)
498 line = (yield line)
501
499
502
500
503 _assign_pat = \
501 _assign_pat = \
504 r'''(?P<lhs>(\s*)
502 r'''(?P<lhs>(\s*)
505 ([\w\.]+) # Initial identifier
503 ([\w\.]+) # Initial identifier
506 (\s*,\s*
504 (\s*,\s*
507 \*?[\w\.]+)* # Further identifiers for unpacking
505 \*?[\w\.]+)* # Further identifiers for unpacking
508 \s*?,? # Trailing comma
506 \s*?,? # Trailing comma
509 )
507 )
510 \s*=\s*
508 \s*=\s*
511 '''
509 '''
512
510
513 assign_system_re = re.compile(r'{}!\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
511 assign_system_re = re.compile(r'{}!\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
514 assign_system_template = '%s = get_ipython().getoutput(%r)'
512 assign_system_template = '%s = get_ipython().getoutput(%r)'
515 @StatelessInputTransformer.wrap
513 @StatelessInputTransformer.wrap
516 def assign_from_system(line):
514 def assign_from_system(line):
517 """Transform assignment from system commands (e.g. files = !ls)"""
515 """Transform assignment from system commands (e.g. files = !ls)"""
518 m = assign_system_re.match(line)
516 m = assign_system_re.match(line)
519 if m is None:
517 if m is None:
520 return line
518 return line
521
519
522 return assign_system_template % m.group('lhs', 'cmd')
520 return assign_system_template % m.group('lhs', 'cmd')
523
521
524 assign_magic_re = re.compile(r'{}%\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
522 assign_magic_re = re.compile(r'{}%\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
525 assign_magic_template = '%s = get_ipython().run_line_magic(%r, %r)'
523 assign_magic_template = '%s = get_ipython().run_line_magic(%r, %r)'
526 @StatelessInputTransformer.wrap
524 @StatelessInputTransformer.wrap
527 def assign_from_magic(line):
525 def assign_from_magic(line):
528 """Transform assignment from magic commands (e.g. a = %who_ls)"""
526 """Transform assignment from magic commands (e.g. a = %who_ls)"""
529 m = assign_magic_re.match(line)
527 m = assign_magic_re.match(line)
530 if m is None:
528 if m is None:
531 return line
529 return line
532 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
530 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
533 m_lhs, m_cmd = m.group('lhs', 'cmd')
531 m_lhs, m_cmd = m.group('lhs', 'cmd')
534 t_magic_name, _, t_magic_arg_s = m_cmd.partition(' ')
532 t_magic_name, _, t_magic_arg_s = m_cmd.partition(' ')
535 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
533 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
536 return assign_magic_template % (m_lhs, t_magic_name, t_magic_arg_s)
534 return assign_magic_template % (m_lhs, t_magic_name, t_magic_arg_s)
@@ -1,752 +1,744 b''
1 """Input transformer machinery to support IPython special syntax.
1 """Input transformer machinery to support IPython special syntax.
2
2
3 This includes the machinery to recognise and transform ``%magic`` commands,
3 This includes the machinery to recognise and transform ``%magic`` commands,
4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
5
5
6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
7 deprecated in 7.0.
7 deprecated in 7.0.
8 """
8 """
9
9
10 # Copyright (c) IPython Development Team.
10 # Copyright (c) IPython Development Team.
11 # Distributed under the terms of the Modified BSD License.
11 # Distributed under the terms of the Modified BSD License.
12
12
13 import ast
13 import ast
14 import sys
14 import sys
15 from codeop import CommandCompiler, Compile
15 from codeop import CommandCompiler, Compile
16 import re
16 import re
17 import tokenize
17 import tokenize
18 from typing import List, Tuple, Union
18 from typing import List, Tuple, Union
19 import warnings
19 import warnings
20
20
21 _indent_re = re.compile(r'^[ \t]+')
21 _indent_re = re.compile(r'^[ \t]+')
22
22
23 def leading_empty_lines(lines):
23 def leading_empty_lines(lines):
24 """Remove leading empty lines
24 """Remove leading empty lines
25
25
26 If the leading lines are empty or contain only whitespace, they will be
26 If the leading lines are empty or contain only whitespace, they will be
27 removed.
27 removed.
28 """
28 """
29 if not lines:
29 if not lines:
30 return lines
30 return lines
31 for i, line in enumerate(lines):
31 for i, line in enumerate(lines):
32 if line and not line.isspace():
32 if line and not line.isspace():
33 return lines[i:]
33 return lines[i:]
34 return lines
34 return lines
35
35
36 def leading_indent(lines):
36 def leading_indent(lines):
37 """Remove leading indentation.
37 """Remove leading indentation.
38
38
39 If the first line starts with a spaces or tabs, the same whitespace will be
39 If the first line starts with a spaces or tabs, the same whitespace will be
40 removed from each following line in the cell.
40 removed from each following line in the cell.
41 """
41 """
42 if not lines:
42 if not lines:
43 return lines
43 return lines
44 m = _indent_re.match(lines[0])
44 m = _indent_re.match(lines[0])
45 if not m:
45 if not m:
46 return lines
46 return lines
47 space = m.group(0)
47 space = m.group(0)
48 n = len(space)
48 n = len(space)
49 return [l[n:] if l.startswith(space) else l
49 return [l[n:] if l.startswith(space) else l
50 for l in lines]
50 for l in lines]
51
51
52 class PromptStripper:
52 class PromptStripper:
53 """Remove matching input prompts from a block of input.
53 """Remove matching input prompts from a block of input.
54
54
55 Parameters
55 Parameters
56 ----------
56 ----------
57 prompt_re : regular expression
57 prompt_re : regular expression
58 A regular expression matching any input prompt (including continuation,
58 A regular expression matching any input prompt (including continuation,
59 e.g. ``...``)
59 e.g. ``...``)
60 initial_re : regular expression, optional
60 initial_re : regular expression, optional
61 A regular expression matching only the initial prompt, but not continuation.
61 A regular expression matching only the initial prompt, but not continuation.
62 If no initial expression is given, prompt_re will be used everywhere.
62 If no initial expression is given, prompt_re will be used everywhere.
63 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
63 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
64 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
64 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
65
65
66 Notes
66 Notes
67 -----
67 -----
68
68
69 If initial_re and prompt_re differ,
69 If initial_re and prompt_re differ,
70 only initial_re will be tested against the first line.
70 only initial_re will be tested against the first line.
71 If any prompt is found on the first two lines,
71 If any prompt is found on the first two lines,
72 prompts will be stripped from the rest of the block.
72 prompts will be stripped from the rest of the block.
73 """
73 """
74 def __init__(self, prompt_re, initial_re=None):
74 def __init__(self, prompt_re, initial_re=None):
75 self.prompt_re = prompt_re
75 self.prompt_re = prompt_re
76 self.initial_re = initial_re or prompt_re
76 self.initial_re = initial_re or prompt_re
77
77
78 def _strip(self, lines):
78 def _strip(self, lines):
79 return [self.prompt_re.sub('', l, count=1) for l in lines]
79 return [self.prompt_re.sub('', l, count=1) for l in lines]
80
80
81 def __call__(self, lines):
81 def __call__(self, lines):
82 if not lines:
82 if not lines:
83 return lines
83 return lines
84 if self.initial_re.match(lines[0]) or \
84 if self.initial_re.match(lines[0]) or \
85 (len(lines) > 1 and self.prompt_re.match(lines[1])):
85 (len(lines) > 1 and self.prompt_re.match(lines[1])):
86 return self._strip(lines)
86 return self._strip(lines)
87 return lines
87 return lines
88
88
89 classic_prompt = PromptStripper(
89 classic_prompt = PromptStripper(
90 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
90 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
91 initial_re=re.compile(r'^>>>( |$)')
91 initial_re=re.compile(r'^>>>( |$)')
92 )
92 )
93
93
94 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
94 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
95
95
96 def cell_magic(lines):
96 def cell_magic(lines):
97 if not lines or not lines[0].startswith('%%'):
97 if not lines or not lines[0].startswith('%%'):
98 return lines
98 return lines
99 if re.match(r'%%\w+\?', lines[0]):
99 if re.match(r'%%\w+\?', lines[0]):
100 # This case will be handled by help_end
100 # This case will be handled by help_end
101 return lines
101 return lines
102 magic_name, _, first_line = lines[0][2:].rstrip().partition(' ')
102 magic_name, _, first_line = lines[0][2:].rstrip().partition(' ')
103 body = ''.join(lines[1:])
103 body = ''.join(lines[1:])
104 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
104 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
105 % (magic_name, first_line, body)]
105 % (magic_name, first_line, body)]
106
106
107
107
108 def _find_assign_op(token_line) -> Union[int, None]:
108 def _find_assign_op(token_line) -> Union[int, None]:
109 """Get the index of the first assignment in the line ('=' not inside brackets)
109 """Get the index of the first assignment in the line ('=' not inside brackets)
110
110
111 Note: We don't try to support multiple special assignment (a = b = %foo)
111 Note: We don't try to support multiple special assignment (a = b = %foo)
112 """
112 """
113 paren_level = 0
113 paren_level = 0
114 for i, ti in enumerate(token_line):
114 for i, ti in enumerate(token_line):
115 s = ti.string
115 s = ti.string
116 if s == '=' and paren_level == 0:
116 if s == '=' and paren_level == 0:
117 return i
117 return i
118 if s in {'(','[','{'}:
118 if s in {'(','[','{'}:
119 paren_level += 1
119 paren_level += 1
120 elif s in {')', ']', '}'}:
120 elif s in {')', ']', '}'}:
121 if paren_level > 0:
121 if paren_level > 0:
122 paren_level -= 1
122 paren_level -= 1
123
123
124 def find_end_of_continued_line(lines, start_line: int):
124 def find_end_of_continued_line(lines, start_line: int):
125 """Find the last line of a line explicitly extended using backslashes.
125 """Find the last line of a line explicitly extended using backslashes.
126
126
127 Uses 0-indexed line numbers.
127 Uses 0-indexed line numbers.
128 """
128 """
129 end_line = start_line
129 end_line = start_line
130 while lines[end_line].endswith('\\\n'):
130 while lines[end_line].endswith('\\\n'):
131 end_line += 1
131 end_line += 1
132 if end_line >= len(lines):
132 if end_line >= len(lines):
133 break
133 break
134 return end_line
134 return end_line
135
135
136 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
136 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
137 r"""Assemble a single line from multiple continued line pieces
137 r"""Assemble a single line from multiple continued line pieces
138
138
139 Continued lines are lines ending in ``\``, and the line following the last
139 Continued lines are lines ending in ``\``, and the line following the last
140 ``\`` in the block.
140 ``\`` in the block.
141
141
142 For example, this code continues over multiple lines::
142 For example, this code continues over multiple lines::
143
143
144 if (assign_ix is not None) \
144 if (assign_ix is not None) \
145 and (len(line) >= assign_ix + 2) \
145 and (len(line) >= assign_ix + 2) \
146 and (line[assign_ix+1].string == '%') \
146 and (line[assign_ix+1].string == '%') \
147 and (line[assign_ix+2].type == tokenize.NAME):
147 and (line[assign_ix+2].type == tokenize.NAME):
148
148
149 This statement contains four continued line pieces.
149 This statement contains four continued line pieces.
150 Assembling these pieces into a single line would give::
150 Assembling these pieces into a single line would give::
151
151
152 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
152 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
153
153
154 This uses 0-indexed line numbers. *start* is (lineno, colno).
154 This uses 0-indexed line numbers. *start* is (lineno, colno).
155
155
156 Used to allow ``%magic`` and ``!system`` commands to be continued over
156 Used to allow ``%magic`` and ``!system`` commands to be continued over
157 multiple lines.
157 multiple lines.
158 """
158 """
159 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
159 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
160 return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
160 return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
161 + [parts[-1].rstrip()]) # Strip newline from last line
161 + [parts[-1].rstrip()]) # Strip newline from last line
162
162
163 class TokenTransformBase:
163 class TokenTransformBase:
164 """Base class for transformations which examine tokens.
164 """Base class for transformations which examine tokens.
165
165
166 Special syntax should not be transformed when it occurs inside strings or
166 Special syntax should not be transformed when it occurs inside strings or
167 comments. This is hard to reliably avoid with regexes. The solution is to
167 comments. This is hard to reliably avoid with regexes. The solution is to
168 tokenise the code as Python, and recognise the special syntax in the tokens.
168 tokenise the code as Python, and recognise the special syntax in the tokens.
169
169
170 IPython's special syntax is not valid Python syntax, so tokenising may go
170 IPython's special syntax is not valid Python syntax, so tokenising may go
171 wrong after the special syntax starts. These classes therefore find and
171 wrong after the special syntax starts. These classes therefore find and
172 transform *one* instance of special syntax at a time into regular Python
172 transform *one* instance of special syntax at a time into regular Python
173 syntax. After each transformation, tokens are regenerated to find the next
173 syntax. After each transformation, tokens are regenerated to find the next
174 piece of special syntax.
174 piece of special syntax.
175
175
176 Subclasses need to implement one class method (find)
176 Subclasses need to implement one class method (find)
177 and one regular method (transform).
177 and one regular method (transform).
178
178
179 The priority attribute can select which transformation to apply if multiple
179 The priority attribute can select which transformation to apply if multiple
180 transformers match in the same place. Lower numbers have higher priority.
180 transformers match in the same place. Lower numbers have higher priority.
181 This allows "%magic?" to be turned into a help call rather than a magic call.
181 This allows "%magic?" to be turned into a help call rather than a magic call.
182 """
182 """
183 # Lower numbers -> higher priority (for matches in the same location)
183 # Lower numbers -> higher priority (for matches in the same location)
184 priority = 10
184 priority = 10
185
185
186 def sortby(self):
186 def sortby(self):
187 return self.start_line, self.start_col, self.priority
187 return self.start_line, self.start_col, self.priority
188
188
189 def __init__(self, start):
189 def __init__(self, start):
190 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
190 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
191 self.start_col = start[1]
191 self.start_col = start[1]
192
192
193 @classmethod
193 @classmethod
194 def find(cls, tokens_by_line):
194 def find(cls, tokens_by_line):
195 """Find one instance of special syntax in the provided tokens.
195 """Find one instance of special syntax in the provided tokens.
196
196
197 Tokens are grouped into logical lines for convenience,
197 Tokens are grouped into logical lines for convenience,
198 so it is easy to e.g. look at the first token of each line.
198 so it is easy to e.g. look at the first token of each line.
199 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
199 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
200
200
201 This should return an instance of its class, pointing to the start
201 This should return an instance of its class, pointing to the start
202 position it has found, or None if it found no match.
202 position it has found, or None if it found no match.
203 """
203 """
204 raise NotImplementedError
204 raise NotImplementedError
205
205
206 def transform(self, lines: List[str]):
206 def transform(self, lines: List[str]):
207 """Transform one instance of special syntax found by ``find()``
207 """Transform one instance of special syntax found by ``find()``
208
208
209 Takes a list of strings representing physical lines,
209 Takes a list of strings representing physical lines,
210 returns a similar list of transformed lines.
210 returns a similar list of transformed lines.
211 """
211 """
212 raise NotImplementedError
212 raise NotImplementedError
213
213
214 class MagicAssign(TokenTransformBase):
214 class MagicAssign(TokenTransformBase):
215 """Transformer for assignments from magics (a = %foo)"""
215 """Transformer for assignments from magics (a = %foo)"""
216 @classmethod
216 @classmethod
217 def find(cls, tokens_by_line):
217 def find(cls, tokens_by_line):
218 """Find the first magic assignment (a = %foo) in the cell.
218 """Find the first magic assignment (a = %foo) in the cell.
219 """
219 """
220 for line in tokens_by_line:
220 for line in tokens_by_line:
221 assign_ix = _find_assign_op(line)
221 assign_ix = _find_assign_op(line)
222 if (assign_ix is not None) \
222 if (assign_ix is not None) \
223 and (len(line) >= assign_ix + 2) \
223 and (len(line) >= assign_ix + 2) \
224 and (line[assign_ix+1].string == '%') \
224 and (line[assign_ix+1].string == '%') \
225 and (line[assign_ix+2].type == tokenize.NAME):
225 and (line[assign_ix+2].type == tokenize.NAME):
226 return cls(line[assign_ix+1].start)
226 return cls(line[assign_ix+1].start)
227
227
228 def transform(self, lines: List[str]):
228 def transform(self, lines: List[str]):
229 """Transform a magic assignment found by the ``find()`` classmethod.
229 """Transform a magic assignment found by the ``find()`` classmethod.
230 """
230 """
231 start_line, start_col = self.start_line, self.start_col
231 start_line, start_col = self.start_line, self.start_col
232 lhs = lines[start_line][:start_col]
232 lhs = lines[start_line][:start_col]
233 end_line = find_end_of_continued_line(lines, start_line)
233 end_line = find_end_of_continued_line(lines, start_line)
234 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
234 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
235 assert rhs.startswith('%'), rhs
235 assert rhs.startswith('%'), rhs
236 magic_name, _, args = rhs[1:].partition(' ')
236 magic_name, _, args = rhs[1:].partition(' ')
237
237
238 lines_before = lines[:start_line]
238 lines_before = lines[:start_line]
239 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
239 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
240 new_line = lhs + call + '\n'
240 new_line = lhs + call + '\n'
241 lines_after = lines[end_line+1:]
241 lines_after = lines[end_line+1:]
242
242
243 return lines_before + [new_line] + lines_after
243 return lines_before + [new_line] + lines_after
244
244
245
245
246 class SystemAssign(TokenTransformBase):
246 class SystemAssign(TokenTransformBase):
247 """Transformer for assignments from system commands (a = !foo)"""
247 """Transformer for assignments from system commands (a = !foo)"""
248 @classmethod
248 @classmethod
249 def find(cls, tokens_by_line):
249 def find(cls, tokens_by_line):
250 """Find the first system assignment (a = !foo) in the cell.
250 """Find the first system assignment (a = !foo) in the cell.
251 """
251 """
252 for line in tokens_by_line:
252 for line in tokens_by_line:
253 assign_ix = _find_assign_op(line)
253 assign_ix = _find_assign_op(line)
254 if (assign_ix is not None) \
254 if (assign_ix is not None) \
255 and not line[assign_ix].line.strip().startswith('=') \
255 and not line[assign_ix].line.strip().startswith('=') \
256 and (len(line) >= assign_ix + 2) \
256 and (len(line) >= assign_ix + 2) \
257 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
257 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
258 ix = assign_ix + 1
258 ix = assign_ix + 1
259
259
260 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
260 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
261 if line[ix].string == '!':
261 if line[ix].string == '!':
262 return cls(line[ix].start)
262 return cls(line[ix].start)
263 elif not line[ix].string.isspace():
263 elif not line[ix].string.isspace():
264 break
264 break
265 ix += 1
265 ix += 1
266
266
267 def transform(self, lines: List[str]):
267 def transform(self, lines: List[str]):
268 """Transform a system assignment found by the ``find()`` classmethod.
268 """Transform a system assignment found by the ``find()`` classmethod.
269 """
269 """
270 start_line, start_col = self.start_line, self.start_col
270 start_line, start_col = self.start_line, self.start_col
271
271
272 lhs = lines[start_line][:start_col]
272 lhs = lines[start_line][:start_col]
273 end_line = find_end_of_continued_line(lines, start_line)
273 end_line = find_end_of_continued_line(lines, start_line)
274 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
274 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
275 assert rhs.startswith('!'), rhs
275 assert rhs.startswith('!'), rhs
276 cmd = rhs[1:]
276 cmd = rhs[1:]
277
277
278 lines_before = lines[:start_line]
278 lines_before = lines[:start_line]
279 call = "get_ipython().getoutput({!r})".format(cmd)
279 call = "get_ipython().getoutput({!r})".format(cmd)
280 new_line = lhs + call + '\n'
280 new_line = lhs + call + '\n'
281 lines_after = lines[end_line + 1:]
281 lines_after = lines[end_line + 1:]
282
282
283 return lines_before + [new_line] + lines_after
283 return lines_before + [new_line] + lines_after
284
284
285 # The escape sequences that define the syntax transformations IPython will
285 # The escape sequences that define the syntax transformations IPython will
286 # apply to user input. These can NOT be just changed here: many regular
286 # apply to user input. These can NOT be just changed here: many regular
287 # expressions and other parts of the code may use their hardcoded values, and
287 # expressions and other parts of the code may use their hardcoded values, and
288 # for all intents and purposes they constitute the 'IPython syntax', so they
288 # for all intents and purposes they constitute the 'IPython syntax', so they
289 # should be considered fixed.
289 # should be considered fixed.
290
290
291 ESC_SHELL = '!' # Send line to underlying system shell
291 ESC_SHELL = '!' # Send line to underlying system shell
292 ESC_SH_CAP = '!!' # Send line to system shell and capture output
292 ESC_SH_CAP = '!!' # Send line to system shell and capture output
293 ESC_HELP = '?' # Find information about object
293 ESC_HELP = '?' # Find information about object
294 ESC_HELP2 = '??' # Find extra-detailed information about object
294 ESC_HELP2 = '??' # Find extra-detailed information about object
295 ESC_MAGIC = '%' # Call magic function
295 ESC_MAGIC = '%' # Call magic function
296 ESC_MAGIC2 = '%%' # Call cell-magic function
296 ESC_MAGIC2 = '%%' # Call cell-magic function
297 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
297 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
298 ESC_QUOTE2 = ';' # Quote all args as a single string, call
298 ESC_QUOTE2 = ';' # Quote all args as a single string, call
299 ESC_PAREN = '/' # Call first argument with rest of line as arguments
299 ESC_PAREN = '/' # Call first argument with rest of line as arguments
300
300
301 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
301 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
302 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
302 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
303
303
304 def _make_help_call(target, esc, next_input=None):
304 def _make_help_call(target, esc):
305 """Prepares a pinfo(2)/psearch call from a target name and the escape
305 """Prepares a pinfo(2)/psearch call from a target name and the escape
306 (i.e. ? or ??)"""
306 (i.e. ? or ??)"""
307 method = 'pinfo2' if esc == '??' \
307 method = 'pinfo2' if esc == '??' \
308 else 'psearch' if '*' in target \
308 else 'psearch' if '*' in target \
309 else 'pinfo'
309 else 'pinfo'
310 arg = " ".join([method, target])
310 arg = " ".join([method, target])
311 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
311 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
312 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
312 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
313 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
313 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
314 if next_input is None:
314 return "get_ipython().run_line_magic(%r, %r)" % (t_magic_name, t_magic_arg_s)
315 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
315
316 else:
317 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
318 (next_input, t_magic_name, t_magic_arg_s)
319
316
320 def _tr_help(content):
317 def _tr_help(content):
321 """Translate lines escaped with: ?
318 """Translate lines escaped with: ?
322
319
323 A naked help line should fire the intro help screen (shell.show_usage())
320 A naked help line should fire the intro help screen (shell.show_usage())
324 """
321 """
325 if not content:
322 if not content:
326 return 'get_ipython().show_usage()'
323 return 'get_ipython().show_usage()'
327
324
328 return _make_help_call(content, '?')
325 return _make_help_call(content, '?')
329
326
330 def _tr_help2(content):
327 def _tr_help2(content):
331 """Translate lines escaped with: ??
328 """Translate lines escaped with: ??
332
329
333 A naked help line should fire the intro help screen (shell.show_usage())
330 A naked help line should fire the intro help screen (shell.show_usage())
334 """
331 """
335 if not content:
332 if not content:
336 return 'get_ipython().show_usage()'
333 return 'get_ipython().show_usage()'
337
334
338 return _make_help_call(content, '??')
335 return _make_help_call(content, '??')
339
336
340 def _tr_magic(content):
337 def _tr_magic(content):
341 "Translate lines escaped with a percent sign: %"
338 "Translate lines escaped with a percent sign: %"
342 name, _, args = content.partition(' ')
339 name, _, args = content.partition(' ')
343 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
340 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
344
341
345 def _tr_quote(content):
342 def _tr_quote(content):
346 "Translate lines escaped with a comma: ,"
343 "Translate lines escaped with a comma: ,"
347 name, _, args = content.partition(' ')
344 name, _, args = content.partition(' ')
348 return '%s("%s")' % (name, '", "'.join(args.split()) )
345 return '%s("%s")' % (name, '", "'.join(args.split()) )
349
346
350 def _tr_quote2(content):
347 def _tr_quote2(content):
351 "Translate lines escaped with a semicolon: ;"
348 "Translate lines escaped with a semicolon: ;"
352 name, _, args = content.partition(' ')
349 name, _, args = content.partition(' ')
353 return '%s("%s")' % (name, args)
350 return '%s("%s")' % (name, args)
354
351
355 def _tr_paren(content):
352 def _tr_paren(content):
356 "Translate lines escaped with a slash: /"
353 "Translate lines escaped with a slash: /"
357 name, _, args = content.partition(' ')
354 name, _, args = content.partition(' ')
358 return '%s(%s)' % (name, ", ".join(args.split()))
355 return '%s(%s)' % (name, ", ".join(args.split()))
359
356
360 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
357 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
361 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
358 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
362 ESC_HELP : _tr_help,
359 ESC_HELP : _tr_help,
363 ESC_HELP2 : _tr_help2,
360 ESC_HELP2 : _tr_help2,
364 ESC_MAGIC : _tr_magic,
361 ESC_MAGIC : _tr_magic,
365 ESC_QUOTE : _tr_quote,
362 ESC_QUOTE : _tr_quote,
366 ESC_QUOTE2 : _tr_quote2,
363 ESC_QUOTE2 : _tr_quote2,
367 ESC_PAREN : _tr_paren }
364 ESC_PAREN : _tr_paren }
368
365
369 class EscapedCommand(TokenTransformBase):
366 class EscapedCommand(TokenTransformBase):
370 """Transformer for escaped commands like %foo, !foo, or /foo"""
367 """Transformer for escaped commands like %foo, !foo, or /foo"""
371 @classmethod
368 @classmethod
372 def find(cls, tokens_by_line):
369 def find(cls, tokens_by_line):
373 """Find the first escaped command (%foo, !foo, etc.) in the cell.
370 """Find the first escaped command (%foo, !foo, etc.) in the cell.
374 """
371 """
375 for line in tokens_by_line:
372 for line in tokens_by_line:
376 if not line:
373 if not line:
377 continue
374 continue
378 ix = 0
375 ix = 0
379 ll = len(line)
376 ll = len(line)
380 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
377 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
381 ix += 1
378 ix += 1
382 if ix >= ll:
379 if ix >= ll:
383 continue
380 continue
384 if line[ix].string in ESCAPE_SINGLES:
381 if line[ix].string in ESCAPE_SINGLES:
385 return cls(line[ix].start)
382 return cls(line[ix].start)
386
383
387 def transform(self, lines):
384 def transform(self, lines):
388 """Transform an escaped line found by the ``find()`` classmethod.
385 """Transform an escaped line found by the ``find()`` classmethod.
389 """
386 """
390 start_line, start_col = self.start_line, self.start_col
387 start_line, start_col = self.start_line, self.start_col
391
388
392 indent = lines[start_line][:start_col]
389 indent = lines[start_line][:start_col]
393 end_line = find_end_of_continued_line(lines, start_line)
390 end_line = find_end_of_continued_line(lines, start_line)
394 line = assemble_continued_line(lines, (start_line, start_col), end_line)
391 line = assemble_continued_line(lines, (start_line, start_col), end_line)
395
392
396 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
393 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
397 escape, content = line[:2], line[2:]
394 escape, content = line[:2], line[2:]
398 else:
395 else:
399 escape, content = line[:1], line[1:]
396 escape, content = line[:1], line[1:]
400
397
401 if escape in tr:
398 if escape in tr:
402 call = tr[escape](content)
399 call = tr[escape](content)
403 else:
400 else:
404 call = ''
401 call = ''
405
402
406 lines_before = lines[:start_line]
403 lines_before = lines[:start_line]
407 new_line = indent + call + '\n'
404 new_line = indent + call + '\n'
408 lines_after = lines[end_line + 1:]
405 lines_after = lines[end_line + 1:]
409
406
410 return lines_before + [new_line] + lines_after
407 return lines_before + [new_line] + lines_after
411
408
412 _help_end_re = re.compile(r"""(%{0,2}
409 _help_end_re = re.compile(r"""(%{0,2}
413 (?!\d)[\w*]+ # Variable name
410 (?!\d)[\w*]+ # Variable name
414 (\.(?!\d)[\w*]+)* # .etc.etc
411 (\.(?!\d)[\w*]+)* # .etc.etc
415 )
412 )
416 (\?\??)$ # ? or ??
413 (\?\??)$ # ? or ??
417 """,
414 """,
418 re.VERBOSE)
415 re.VERBOSE)
419
416
420 class HelpEnd(TokenTransformBase):
417 class HelpEnd(TokenTransformBase):
421 """Transformer for help syntax: obj? and obj??"""
418 """Transformer for help syntax: obj? and obj??"""
422 # This needs to be higher priority (lower number) than EscapedCommand so
419 # This needs to be higher priority (lower number) than EscapedCommand so
423 # that inspecting magics (%foo?) works.
420 # that inspecting magics (%foo?) works.
424 priority = 5
421 priority = 5
425
422
426 def __init__(self, start, q_locn):
423 def __init__(self, start, q_locn):
427 super().__init__(start)
424 super().__init__(start)
428 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
425 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
429 self.q_col = q_locn[1]
426 self.q_col = q_locn[1]
430
427
431 @classmethod
428 @classmethod
432 def find(cls, tokens_by_line):
429 def find(cls, tokens_by_line):
433 """Find the first help command (foo?) in the cell.
430 """Find the first help command (foo?) in the cell.
434 """
431 """
435 for line in tokens_by_line:
432 for line in tokens_by_line:
436 # Last token is NEWLINE; look at last but one
433 # Last token is NEWLINE; look at last but one
437 if len(line) > 2 and line[-2].string == '?':
434 if len(line) > 2 and line[-2].string == '?':
438 # Find the first token that's not INDENT/DEDENT
435 # Find the first token that's not INDENT/DEDENT
439 ix = 0
436 ix = 0
440 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
437 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
441 ix += 1
438 ix += 1
442 return cls(line[ix].start, line[-2].start)
439 return cls(line[ix].start, line[-2].start)
443
440
444 def transform(self, lines):
441 def transform(self, lines):
445 """Transform a help command found by the ``find()`` classmethod.
442 """Transform a help command found by the ``find()`` classmethod.
446 """
443 """
447 piece = ''.join(lines[self.start_line:self.q_line+1])
444 piece = ''.join(lines[self.start_line:self.q_line+1])
448 indent, content = piece[:self.start_col], piece[self.start_col:]
445 indent, content = piece[:self.start_col], piece[self.start_col:]
449 lines_before = lines[:self.start_line]
446 lines_before = lines[:self.start_line]
450 lines_after = lines[self.q_line + 1:]
447 lines_after = lines[self.q_line + 1:]
451
448
452 m = _help_end_re.search(content)
449 m = _help_end_re.search(content)
453 if not m:
450 if not m:
454 raise SyntaxError(content)
451 raise SyntaxError(content)
455 assert m is not None, content
452 assert m is not None, content
456 target = m.group(1)
453 target = m.group(1)
457 esc = m.group(3)
454 esc = m.group(3)
458
455
459 # If we're mid-command, put it back on the next prompt for the user.
460 next_input = None
461 if (not lines_before) and (not lines_after) \
462 and content.strip() != m.group(0):
463 next_input = content.rstrip('?\n')
464
456
465 call = _make_help_call(target, esc, next_input=next_input)
457 call = _make_help_call(target, esc)
466 new_line = indent + call + '\n'
458 new_line = indent + call + '\n'
467
459
468 return lines_before + [new_line] + lines_after
460 return lines_before + [new_line] + lines_after
469
461
470 def make_tokens_by_line(lines:List[str]):
462 def make_tokens_by_line(lines:List[str]):
471 """Tokenize a series of lines and group tokens by line.
463 """Tokenize a series of lines and group tokens by line.
472
464
473 The tokens for a multiline Python string or expression are grouped as one
465 The tokens for a multiline Python string or expression are grouped as one
474 line. All lines except the last lines should keep their line ending ('\\n',
466 line. All lines except the last lines should keep their line ending ('\\n',
475 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
467 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
476 for example when passing block of text to this function.
468 for example when passing block of text to this function.
477
469
478 """
470 """
479 # NL tokens are used inside multiline expressions, but also after blank
471 # NL tokens are used inside multiline expressions, but also after blank
480 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
472 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
481 # We want to group the former case together but split the latter, so we
473 # We want to group the former case together but split the latter, so we
482 # track parentheses level, similar to the internals of tokenize.
474 # track parentheses level, similar to the internals of tokenize.
483 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL
475 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL
484 tokens_by_line = [[]]
476 tokens_by_line = [[]]
485 if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')):
477 if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')):
486 warnings.warn("`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified")
478 warnings.warn("`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified")
487 parenlev = 0
479 parenlev = 0
488 try:
480 try:
489 for token in tokenize.generate_tokens(iter(lines).__next__):
481 for token in tokenize.generate_tokens(iter(lines).__next__):
490 tokens_by_line[-1].append(token)
482 tokens_by_line[-1].append(token)
491 if (token.type == NEWLINE) \
483 if (token.type == NEWLINE) \
492 or ((token.type == NL) and (parenlev <= 0)):
484 or ((token.type == NL) and (parenlev <= 0)):
493 tokens_by_line.append([])
485 tokens_by_line.append([])
494 elif token.string in {'(', '[', '{'}:
486 elif token.string in {'(', '[', '{'}:
495 parenlev += 1
487 parenlev += 1
496 elif token.string in {')', ']', '}'}:
488 elif token.string in {')', ']', '}'}:
497 if parenlev > 0:
489 if parenlev > 0:
498 parenlev -= 1
490 parenlev -= 1
499 except tokenize.TokenError:
491 except tokenize.TokenError:
500 # Input ended in a multiline string or expression. That's OK for us.
492 # Input ended in a multiline string or expression. That's OK for us.
501 pass
493 pass
502
494
503
495
504 if not tokens_by_line[-1]:
496 if not tokens_by_line[-1]:
505 tokens_by_line.pop()
497 tokens_by_line.pop()
506
498
507
499
508 return tokens_by_line
500 return tokens_by_line
509
501
510 def show_linewise_tokens(s: str):
502 def show_linewise_tokens(s: str):
511 """For investigation and debugging"""
503 """For investigation and debugging"""
512 if not s.endswith('\n'):
504 if not s.endswith('\n'):
513 s += '\n'
505 s += '\n'
514 lines = s.splitlines(keepends=True)
506 lines = s.splitlines(keepends=True)
515 for line in make_tokens_by_line(lines):
507 for line in make_tokens_by_line(lines):
516 print("Line -------")
508 print("Line -------")
517 for tokinfo in line:
509 for tokinfo in line:
518 print(" ", tokinfo)
510 print(" ", tokinfo)
519
511
520 # Arbitrary limit to prevent getting stuck in infinite loops
512 # Arbitrary limit to prevent getting stuck in infinite loops
521 TRANSFORM_LOOP_LIMIT = 500
513 TRANSFORM_LOOP_LIMIT = 500
522
514
523 class TransformerManager:
515 class TransformerManager:
524 """Applies various transformations to a cell or code block.
516 """Applies various transformations to a cell or code block.
525
517
526 The key methods for external use are ``transform_cell()``
518 The key methods for external use are ``transform_cell()``
527 and ``check_complete()``.
519 and ``check_complete()``.
528 """
520 """
529 def __init__(self):
521 def __init__(self):
530 self.cleanup_transforms = [
522 self.cleanup_transforms = [
531 leading_empty_lines,
523 leading_empty_lines,
532 leading_indent,
524 leading_indent,
533 classic_prompt,
525 classic_prompt,
534 ipython_prompt,
526 ipython_prompt,
535 ]
527 ]
536 self.line_transforms = [
528 self.line_transforms = [
537 cell_magic,
529 cell_magic,
538 ]
530 ]
539 self.token_transformers = [
531 self.token_transformers = [
540 MagicAssign,
532 MagicAssign,
541 SystemAssign,
533 SystemAssign,
542 EscapedCommand,
534 EscapedCommand,
543 HelpEnd,
535 HelpEnd,
544 ]
536 ]
545
537
546 def do_one_token_transform(self, lines):
538 def do_one_token_transform(self, lines):
547 """Find and run the transform earliest in the code.
539 """Find and run the transform earliest in the code.
548
540
549 Returns (changed, lines).
541 Returns (changed, lines).
550
542
551 This method is called repeatedly until changed is False, indicating
543 This method is called repeatedly until changed is False, indicating
552 that all available transformations are complete.
544 that all available transformations are complete.
553
545
554 The tokens following IPython special syntax might not be valid, so
546 The tokens following IPython special syntax might not be valid, so
555 the transformed code is retokenised every time to identify the next
547 the transformed code is retokenised every time to identify the next
556 piece of special syntax. Hopefully long code cells are mostly valid
548 piece of special syntax. Hopefully long code cells are mostly valid
557 Python, not using lots of IPython special syntax, so this shouldn't be
549 Python, not using lots of IPython special syntax, so this shouldn't be
558 a performance issue.
550 a performance issue.
559 """
551 """
560 tokens_by_line = make_tokens_by_line(lines)
552 tokens_by_line = make_tokens_by_line(lines)
561 candidates = []
553 candidates = []
562 for transformer_cls in self.token_transformers:
554 for transformer_cls in self.token_transformers:
563 transformer = transformer_cls.find(tokens_by_line)
555 transformer = transformer_cls.find(tokens_by_line)
564 if transformer:
556 if transformer:
565 candidates.append(transformer)
557 candidates.append(transformer)
566
558
567 if not candidates:
559 if not candidates:
568 # Nothing to transform
560 # Nothing to transform
569 return False, lines
561 return False, lines
570 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
562 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
571 for transformer in ordered_transformers:
563 for transformer in ordered_transformers:
572 try:
564 try:
573 return True, transformer.transform(lines)
565 return True, transformer.transform(lines)
574 except SyntaxError:
566 except SyntaxError:
575 pass
567 pass
576 return False, lines
568 return False, lines
577
569
578 def do_token_transforms(self, lines):
570 def do_token_transforms(self, lines):
579 for _ in range(TRANSFORM_LOOP_LIMIT):
571 for _ in range(TRANSFORM_LOOP_LIMIT):
580 changed, lines = self.do_one_token_transform(lines)
572 changed, lines = self.do_one_token_transform(lines)
581 if not changed:
573 if not changed:
582 return lines
574 return lines
583
575
584 raise RuntimeError("Input transformation still changing after "
576 raise RuntimeError("Input transformation still changing after "
585 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
577 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
586
578
587 def transform_cell(self, cell: str) -> str:
579 def transform_cell(self, cell: str) -> str:
588 """Transforms a cell of input code"""
580 """Transforms a cell of input code"""
589 if not cell.endswith('\n'):
581 if not cell.endswith('\n'):
590 cell += '\n' # Ensure the cell has a trailing newline
582 cell += '\n' # Ensure the cell has a trailing newline
591 lines = cell.splitlines(keepends=True)
583 lines = cell.splitlines(keepends=True)
592 for transform in self.cleanup_transforms + self.line_transforms:
584 for transform in self.cleanup_transforms + self.line_transforms:
593 lines = transform(lines)
585 lines = transform(lines)
594
586
595 lines = self.do_token_transforms(lines)
587 lines = self.do_token_transforms(lines)
596 return ''.join(lines)
588 return ''.join(lines)
597
589
598 def check_complete(self, cell: str):
590 def check_complete(self, cell: str):
599 """Return whether a block of code is ready to execute, or should be continued
591 """Return whether a block of code is ready to execute, or should be continued
600
592
601 Parameters
593 Parameters
602 ----------
594 ----------
603 source : string
595 source : string
604 Python input code, which can be multiline.
596 Python input code, which can be multiline.
605
597
606 Returns
598 Returns
607 -------
599 -------
608 status : str
600 status : str
609 One of 'complete', 'incomplete', or 'invalid' if source is not a
601 One of 'complete', 'incomplete', or 'invalid' if source is not a
610 prefix of valid code.
602 prefix of valid code.
611 indent_spaces : int or None
603 indent_spaces : int or None
612 The number of spaces by which to indent the next line of code. If
604 The number of spaces by which to indent the next line of code. If
613 status is not 'incomplete', this is None.
605 status is not 'incomplete', this is None.
614 """
606 """
615 # Remember if the lines ends in a new line.
607 # Remember if the lines ends in a new line.
616 ends_with_newline = False
608 ends_with_newline = False
617 for character in reversed(cell):
609 for character in reversed(cell):
618 if character == '\n':
610 if character == '\n':
619 ends_with_newline = True
611 ends_with_newline = True
620 break
612 break
621 elif character.strip():
613 elif character.strip():
622 break
614 break
623 else:
615 else:
624 continue
616 continue
625
617
626 if not ends_with_newline:
618 if not ends_with_newline:
627 # Append an newline for consistent tokenization
619 # Append an newline for consistent tokenization
628 # See https://bugs.python.org/issue33899
620 # See https://bugs.python.org/issue33899
629 cell += '\n'
621 cell += '\n'
630
622
631 lines = cell.splitlines(keepends=True)
623 lines = cell.splitlines(keepends=True)
632
624
633 if not lines:
625 if not lines:
634 return 'complete', None
626 return 'complete', None
635
627
636 if lines[-1].endswith('\\'):
628 if lines[-1].endswith('\\'):
637 # Explicit backslash continuation
629 # Explicit backslash continuation
638 return 'incomplete', find_last_indent(lines)
630 return 'incomplete', find_last_indent(lines)
639
631
640 try:
632 try:
641 for transform in self.cleanup_transforms:
633 for transform in self.cleanup_transforms:
642 if not getattr(transform, 'has_side_effects', False):
634 if not getattr(transform, 'has_side_effects', False):
643 lines = transform(lines)
635 lines = transform(lines)
644 except SyntaxError:
636 except SyntaxError:
645 return 'invalid', None
637 return 'invalid', None
646
638
647 if lines[0].startswith('%%'):
639 if lines[0].startswith('%%'):
648 # Special case for cell magics - completion marked by blank line
640 # Special case for cell magics - completion marked by blank line
649 if lines[-1].strip():
641 if lines[-1].strip():
650 return 'incomplete', find_last_indent(lines)
642 return 'incomplete', find_last_indent(lines)
651 else:
643 else:
652 return 'complete', None
644 return 'complete', None
653
645
654 try:
646 try:
655 for transform in self.line_transforms:
647 for transform in self.line_transforms:
656 if not getattr(transform, 'has_side_effects', False):
648 if not getattr(transform, 'has_side_effects', False):
657 lines = transform(lines)
649 lines = transform(lines)
658 lines = self.do_token_transforms(lines)
650 lines = self.do_token_transforms(lines)
659 except SyntaxError:
651 except SyntaxError:
660 return 'invalid', None
652 return 'invalid', None
661
653
662 tokens_by_line = make_tokens_by_line(lines)
654 tokens_by_line = make_tokens_by_line(lines)
663
655
664 if not tokens_by_line:
656 if not tokens_by_line:
665 return 'incomplete', find_last_indent(lines)
657 return 'incomplete', find_last_indent(lines)
666
658
667 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
659 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
668 # We're in a multiline string or expression
660 # We're in a multiline string or expression
669 return 'incomplete', find_last_indent(lines)
661 return 'incomplete', find_last_indent(lines)
670
662
671 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER}
663 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER}
672
664
673 # Pop the last line which only contains DEDENTs and ENDMARKER
665 # Pop the last line which only contains DEDENTs and ENDMARKER
674 last_token_line = None
666 last_token_line = None
675 if {t.type for t in tokens_by_line[-1]} in [
667 if {t.type for t in tokens_by_line[-1]} in [
676 {tokenize.DEDENT, tokenize.ENDMARKER},
668 {tokenize.DEDENT, tokenize.ENDMARKER},
677 {tokenize.ENDMARKER}
669 {tokenize.ENDMARKER}
678 ] and len(tokens_by_line) > 1:
670 ] and len(tokens_by_line) > 1:
679 last_token_line = tokens_by_line.pop()
671 last_token_line = tokens_by_line.pop()
680
672
681 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
673 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
682 tokens_by_line[-1].pop()
674 tokens_by_line[-1].pop()
683
675
684 if not tokens_by_line[-1]:
676 if not tokens_by_line[-1]:
685 return 'incomplete', find_last_indent(lines)
677 return 'incomplete', find_last_indent(lines)
686
678
687 if tokens_by_line[-1][-1].string == ':':
679 if tokens_by_line[-1][-1].string == ':':
688 # The last line starts a block (e.g. 'if foo:')
680 # The last line starts a block (e.g. 'if foo:')
689 ix = 0
681 ix = 0
690 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
682 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
691 ix += 1
683 ix += 1
692
684
693 indent = tokens_by_line[-1][ix].start[1]
685 indent = tokens_by_line[-1][ix].start[1]
694 return 'incomplete', indent + 4
686 return 'incomplete', indent + 4
695
687
696 if tokens_by_line[-1][0].line.endswith('\\'):
688 if tokens_by_line[-1][0].line.endswith('\\'):
697 return 'incomplete', None
689 return 'incomplete', None
698
690
699 # At this point, our checks think the code is complete (or invalid).
691 # At this point, our checks think the code is complete (or invalid).
700 # We'll use codeop.compile_command to check this with the real parser
692 # We'll use codeop.compile_command to check this with the real parser
701 try:
693 try:
702 with warnings.catch_warnings():
694 with warnings.catch_warnings():
703 warnings.simplefilter('error', SyntaxWarning)
695 warnings.simplefilter('error', SyntaxWarning)
704 res = compile_command(''.join(lines), symbol='exec')
696 res = compile_command(''.join(lines), symbol='exec')
705 except (SyntaxError, OverflowError, ValueError, TypeError,
697 except (SyntaxError, OverflowError, ValueError, TypeError,
706 MemoryError, SyntaxWarning):
698 MemoryError, SyntaxWarning):
707 return 'invalid', None
699 return 'invalid', None
708 else:
700 else:
709 if res is None:
701 if res is None:
710 return 'incomplete', find_last_indent(lines)
702 return 'incomplete', find_last_indent(lines)
711
703
712 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
704 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
713 if ends_with_newline:
705 if ends_with_newline:
714 return 'complete', None
706 return 'complete', None
715 return 'incomplete', find_last_indent(lines)
707 return 'incomplete', find_last_indent(lines)
716
708
717 # If there's a blank line at the end, assume we're ready to execute
709 # If there's a blank line at the end, assume we're ready to execute
718 if not lines[-1].strip():
710 if not lines[-1].strip():
719 return 'complete', None
711 return 'complete', None
720
712
721 return 'complete', None
713 return 'complete', None
722
714
723
715
724 def find_last_indent(lines):
716 def find_last_indent(lines):
725 m = _indent_re.match(lines[-1])
717 m = _indent_re.match(lines[-1])
726 if not m:
718 if not m:
727 return 0
719 return 0
728 return len(m.group(0).replace('\t', ' '*4))
720 return len(m.group(0).replace('\t', ' '*4))
729
721
730
722
731 class MaybeAsyncCompile(Compile):
723 class MaybeAsyncCompile(Compile):
732 def __init__(self, extra_flags=0):
724 def __init__(self, extra_flags=0):
733 super().__init__()
725 super().__init__()
734 self.flags |= extra_flags
726 self.flags |= extra_flags
735
727
736
728
737 if sys.version_info < (3,8):
729 if sys.version_info < (3,8):
738 def __call__(self, *args, **kwds):
730 def __call__(self, *args, **kwds):
739 return compile(*args, **kwds)
731 return compile(*args, **kwds)
740
732
741
733
742 class MaybeAsyncCommandCompiler(CommandCompiler):
734 class MaybeAsyncCommandCompiler(CommandCompiler):
743 def __init__(self, extra_flags=0):
735 def __init__(self, extra_flags=0):
744 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags)
736 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags)
745
737
746
738
747 if (sys.version_info.major, sys.version_info.minor) >= (3, 8):
739 if (sys.version_info.major, sys.version_info.minor) >= (3, 8):
748 _extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
740 _extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
749 else:
741 else:
750 _extra_flags = ast.PyCF_ONLY_AST
742 _extra_flags = ast.PyCF_ONLY_AST
751
743
752 compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)
744 compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)
@@ -1,495 +1,499 b''
1 import tokenize
1 import tokenize
2 import nose.tools as nt
2 import nose.tools as nt
3
3
4 from IPython.testing import tools as tt
4 from IPython.testing import tools as tt
5 from IPython.utils import py3compat
5 from IPython.utils import py3compat
6 u_fmt = py3compat.u_format
6 u_fmt = py3compat.u_format
7
7
8 from IPython.core import inputtransformer as ipt
8 from IPython.core import inputtransformer as ipt
9
9
10 def transform_and_reset(transformer):
10 def transform_and_reset(transformer):
11 transformer = transformer()
11 transformer = transformer()
12 def transform(inp):
12 def transform(inp):
13 try:
13 try:
14 return transformer.push(inp)
14 return transformer.push(inp)
15 finally:
15 finally:
16 transformer.reset()
16 transformer.reset()
17
17
18 return transform
18 return transform
19
19
20 # Transformer tests
20 # Transformer tests
21 def transform_checker(tests, transformer, **kwargs):
21 def transform_checker(tests, transformer, **kwargs):
22 """Utility to loop over test inputs"""
22 """Utility to loop over test inputs"""
23 transformer = transformer(**kwargs)
23 transformer = transformer(**kwargs)
24 try:
24 try:
25 for inp, tr in tests:
25 for inp, tr in tests:
26 if inp is None:
26 if inp is None:
27 out = transformer.reset()
27 out = transformer.reset()
28 else:
28 else:
29 out = transformer.push(inp)
29 out = transformer.push(inp)
30 nt.assert_equal(out, tr)
30 nt.assert_equal(out, tr)
31 finally:
31 finally:
32 transformer.reset()
32 transformer.reset()
33
33
34 # Data for all the syntax tests in the form of lists of pairs of
34 # Data for all the syntax tests in the form of lists of pairs of
35 # raw/transformed input. We store it here as a global dict so that we can use
35 # raw/transformed input. We store it here as a global dict so that we can use
36 # it both within single-function tests and also to validate the behavior of the
36 # it both within single-function tests and also to validate the behavior of the
37 # larger objects
37 # larger objects
38
38
39 syntax = \
39 syntax = dict(
40 dict(assign_system =
40 assign_system=[
41 [(i,py3compat.u_format(o)) for i,o in \
41 (i, py3compat.u_format(o))
42 [(u'a =! ls', "a = get_ipython().getoutput('ls')"),
42 for i, o in [
43 (u'b = !ls', "b = get_ipython().getoutput('ls')"),
43 (u"a =! ls", "a = get_ipython().getoutput('ls')"),
44 (u'c= !ls', "c = get_ipython().getoutput('ls')"),
44 (u"b = !ls", "b = get_ipython().getoutput('ls')"),
45 (u'd == !ls', u'd == !ls'), # Invalid syntax, but we leave == alone.
45 (u"c= !ls", "c = get_ipython().getoutput('ls')"),
46 ('x=1', 'x=1'), # normal input is unmodified
46 (u"d == !ls", u"d == !ls"), # Invalid syntax, but we leave == alone.
47 (' ',' '), # blank lines are kept intact
47 ("x=1", "x=1"), # normal input is unmodified
48 # Tuple unpacking
48 (" ", " "), # blank lines are kept intact
49 (u"a, b = !echo 'a\\nb'", u"a, b = get_ipython().getoutput(\"echo 'a\\\\nb'\")"),
49 # Tuple unpacking
50 (u"a,= !echo 'a'", u"a, = get_ipython().getoutput(\"echo 'a'\")"),
50 (
51 (u"a, *bc = !echo 'a\\nb\\nc'", u"a, *bc = get_ipython().getoutput(\"echo 'a\\\\nb\\\\nc'\")"),
51 u"a, b = !echo 'a\\nb'",
52 # Tuple unpacking with regular Python expressions, not our syntax.
52 u"a, b = get_ipython().getoutput(\"echo 'a\\\\nb'\")",
53 (u"a, b = range(2)", u"a, b = range(2)"),
53 ),
54 (u"a, = range(1)", u"a, = range(1)"),
54 (u"a,= !echo 'a'", u"a, = get_ipython().getoutput(\"echo 'a'\")"),
55 (u"a, *bc = range(3)", u"a, *bc = range(3)"),
55 (
56 ]],
56 u"a, *bc = !echo 'a\\nb\\nc'",
57
57 u"a, *bc = get_ipython().getoutput(\"echo 'a\\\\nb\\\\nc'\")",
58 assign_magic =
58 ),
59 [(i,py3compat.u_format(o)) for i,o in \
59 # Tuple unpacking with regular Python expressions, not our syntax.
60 [(u'a =% who', "a = get_ipython().run_line_magic('who', '')"),
60 (u"a, b = range(2)", u"a, b = range(2)"),
61 (u'b = %who', "b = get_ipython().run_line_magic('who', '')"),
61 (u"a, = range(1)", u"a, = range(1)"),
62 (u'c= %ls', "c = get_ipython().run_line_magic('ls', '')"),
62 (u"a, *bc = range(3)", u"a, *bc = range(3)"),
63 (u'd == %ls', u'd == %ls'), # Invalid syntax, but we leave == alone.
63 ]
64 ('x=1', 'x=1'), # normal input is unmodified
64 ],
65 (' ',' '), # blank lines are kept intact
65 assign_magic=[
66 (u"a, b = %foo", u"a, b = get_ipython().run_line_magic('foo', '')"),
66 (i, py3compat.u_format(o))
67 ]],
67 for i, o in [
68
68 (u"a =% who", "a = get_ipython().run_line_magic('who', '')"),
69 classic_prompt =
69 (u"b = %who", "b = get_ipython().run_line_magic('who', '')"),
70 [('>>> x=1', 'x=1'),
70 (u"c= %ls", "c = get_ipython().run_line_magic('ls', '')"),
71 ('x=1', 'x=1'), # normal input is unmodified
71 (u"d == %ls", u"d == %ls"), # Invalid syntax, but we leave == alone.
72 (' ', ' '), # blank lines are kept intact
72 ("x=1", "x=1"), # normal input is unmodified
73 ],
73 (" ", " "), # blank lines are kept intact
74
74 (u"a, b = %foo", u"a, b = get_ipython().run_line_magic('foo', '')"),
75 ipy_prompt =
75 ]
76 [('In [1]: x=1', 'x=1'),
76 ],
77 ('x=1', 'x=1'), # normal input is unmodified
77 classic_prompt=[
78 (' ',' '), # blank lines are kept intact
78 (">>> x=1", "x=1"),
79 ],
79 ("x=1", "x=1"), # normal input is unmodified
80
80 (" ", " "), # blank lines are kept intact
81 # Tests for the escape transformer to leave normal code alone
81 ],
82 escaped_noesc =
82 ipy_prompt=[
83 [ (' ', ' '),
83 ("In [1]: x=1", "x=1"),
84 ('x=1', 'x=1'),
84 ("x=1", "x=1"), # normal input is unmodified
85 ],
85 (" ", " "), # blank lines are kept intact
86
86 ],
87 # System calls
87 # Tests for the escape transformer to leave normal code alone
88 escaped_shell =
88 escaped_noesc=[
89 [(i,py3compat.u_format(o)) for i,o in \
89 (" ", " "),
90 [ (u'!ls', "get_ipython().system('ls')"),
90 ("x=1", "x=1"),
91 # Double-escape shell, this means to capture the output of the
91 ],
92 # subprocess and return it
92 # System calls
93 (u'!!ls', "get_ipython().getoutput('ls')"),
93 escaped_shell=[
94 ]],
94 (i, py3compat.u_format(o))
95
95 for i, o in [
96 # Help/object info
96 (u"!ls", "get_ipython().system('ls')"),
97 escaped_help =
97 # Double-escape shell, this means to capture the output of the
98 [(i,py3compat.u_format(o)) for i,o in \
98 # subprocess and return it
99 [ (u'?', 'get_ipython().show_usage()'),
99 (u"!!ls", "get_ipython().getoutput('ls')"),
100 (u'?x1', "get_ipython().run_line_magic('pinfo', 'x1')"),
100 ]
101 (u'??x2', "get_ipython().run_line_magic('pinfo2', 'x2')"),
101 ],
102 (u'?a.*s', "get_ipython().run_line_magic('psearch', 'a.*s')"),
102 # Help/object info
103 (u'?%hist1', "get_ipython().run_line_magic('pinfo', '%hist1')"),
103 escaped_help=[
104 (u'?%%hist2', "get_ipython().run_line_magic('pinfo', '%%hist2')"),
104 (i, py3compat.u_format(o))
105 (u'?abc = qwe', "get_ipython().run_line_magic('pinfo', 'abc')"),
105 for i, o in [
106 ]],
106 (u"?", "get_ipython().show_usage()"),
107
107 (u"?x1", "get_ipython().run_line_magic('pinfo', 'x1')"),
108 end_help =
108 (u"??x2", "get_ipython().run_line_magic('pinfo2', 'x2')"),
109 [(i,py3compat.u_format(o)) for i,o in \
109 (u"?a.*s", "get_ipython().run_line_magic('psearch', 'a.*s')"),
110 [ (u'x3?', "get_ipython().run_line_magic('pinfo', 'x3')"),
110 (u"?%hist1", "get_ipython().run_line_magic('pinfo', '%hist1')"),
111 (u'x4??', "get_ipython().run_line_magic('pinfo2', 'x4')"),
111 (u"?%%hist2", "get_ipython().run_line_magic('pinfo', '%%hist2')"),
112 (u'%hist1?', "get_ipython().run_line_magic('pinfo', '%hist1')"),
112 (u"?abc = qwe", "get_ipython().run_line_magic('pinfo', 'abc')"),
113 (u'%hist2??', "get_ipython().run_line_magic('pinfo2', '%hist2')"),
113 ]
114 (u'%%hist3?', "get_ipython().run_line_magic('pinfo', '%%hist3')"),
114 ],
115 (u'%%hist4??', "get_ipython().run_line_magic('pinfo2', '%%hist4')"),
115 end_help=[
116 (u'Ο€.foo?', "get_ipython().run_line_magic('pinfo', 'Ο€.foo')"),
116 (i, py3compat.u_format(o))
117 (u'f*?', "get_ipython().run_line_magic('psearch', 'f*')"),
117 for i, o in [
118 (u'ax.*aspe*?', "get_ipython().run_line_magic('psearch', 'ax.*aspe*')"),
118 (u"x3?", "get_ipython().run_line_magic('pinfo', 'x3')"),
119 (u'a = abc?', "get_ipython().set_next_input('a = abc');"
119 (u"x4??", "get_ipython().run_line_magic('pinfo2', 'x4')"),
120 "get_ipython().run_line_magic('pinfo', 'abc')"),
120 (u"%hist1?", "get_ipython().run_line_magic('pinfo', '%hist1')"),
121 (u'a = abc.qe??', "get_ipython().set_next_input('a = abc.qe');"
121 (u"%hist2??", "get_ipython().run_line_magic('pinfo2', '%hist2')"),
122 "get_ipython().run_line_magic('pinfo2', 'abc.qe')"),
122 (u"%%hist3?", "get_ipython().run_line_magic('pinfo', '%%hist3')"),
123 (u'a = *.items?', "get_ipython().set_next_input('a = *.items');"
123 (u"%%hist4??", "get_ipython().run_line_magic('pinfo2', '%%hist4')"),
124 "get_ipython().run_line_magic('psearch', '*.items')"),
124 (u"Ο€.foo?", "get_ipython().run_line_magic('pinfo', 'Ο€.foo')"),
125 (u'plot(a?', "get_ipython().set_next_input('plot(a');"
125 (u"f*?", "get_ipython().run_line_magic('psearch', 'f*')"),
126 "get_ipython().run_line_magic('pinfo', 'a')"),
126 (u"ax.*aspe*?", "get_ipython().run_line_magic('psearch', 'ax.*aspe*')"),
127 (u'a*2 #comment?', 'a*2 #comment?'),
127 (u"a = abc?", "get_ipython().run_line_magic('pinfo', 'abc')"),
128 ]],
128 (u"a = abc.qe??", "get_ipython().run_line_magic('pinfo2', 'abc.qe')"),
129
129 (u"a = *.items?", "get_ipython().run_line_magic('psearch', '*.items')"),
130 # Explicit magic calls
130 (u"plot(a?", "get_ipython().run_line_magic('pinfo', 'a')"),
131 escaped_magic =
131 (u"a*2 #comment?", "a*2 #comment?"),
132 [(i,py3compat.u_format(o)) for i,o in \
132 ]
133 [ (u'%cd', "get_ipython().run_line_magic('cd', '')"),
133 ],
134 (u'%cd /home', "get_ipython().run_line_magic('cd', '/home')"),
134 # Explicit magic calls
135 # Backslashes need to be escaped.
135 escaped_magic=[
136 (u'%cd C:\\User', "get_ipython().run_line_magic('cd', 'C:\\\\User')"),
136 (i, py3compat.u_format(o))
137 (u' %magic', " get_ipython().run_line_magic('magic', '')"),
137 for i, o in [
138 ]],
138 (u"%cd", "get_ipython().run_line_magic('cd', '')"),
139
139 (u"%cd /home", "get_ipython().run_line_magic('cd', '/home')"),
140 # Quoting with separate arguments
140 # Backslashes need to be escaped.
141 escaped_quote =
141 (u"%cd C:\\User", "get_ipython().run_line_magic('cd', 'C:\\\\User')"),
142 [ (',f', 'f("")'),
142 (u" %magic", " get_ipython().run_line_magic('magic', '')"),
143 (',f x', 'f("x")'),
143 ]
144 (' ,f y', ' f("y")'),
144 ],
145 (',f a b', 'f("a", "b")'),
145 # Quoting with separate arguments
146 ],
146 escaped_quote=[
147
147 (",f", 'f("")'),
148 # Quoting with single argument
148 (",f x", 'f("x")'),
149 escaped_quote2 =
149 (" ,f y", ' f("y")'),
150 [ (';f', 'f("")'),
150 (",f a b", 'f("a", "b")'),
151 (';f x', 'f("x")'),
151 ],
152 (' ;f y', ' f("y")'),
152 # Quoting with single argument
153 (';f a b', 'f("a b")'),
153 escaped_quote2=[
154 ],
154 (";f", 'f("")'),
155
155 (";f x", 'f("x")'),
156 # Simply apply parens
156 (" ;f y", ' f("y")'),
157 escaped_paren =
157 (";f a b", 'f("a b")'),
158 [ ('/f', 'f()'),
158 ],
159 ('/f x', 'f(x)'),
159 # Simply apply parens
160 (' /f y', ' f(y)'),
160 escaped_paren=[
161 ('/f a b', 'f(a, b)'),
161 ("/f", "f()"),
162 ],
162 ("/f x", "f(x)"),
163
163 (" /f y", " f(y)"),
164 # Check that we transform prompts before other transforms
164 ("/f a b", "f(a, b)"),
165 mixed =
165 ],
166 [(i,py3compat.u_format(o)) for i,o in \
166 # Check that we transform prompts before other transforms
167 [ (u'In [1]: %lsmagic', "get_ipython().run_line_magic('lsmagic', '')"),
167 mixed=[
168 (u'>>> %lsmagic', "get_ipython().run_line_magic('lsmagic', '')"),
168 (i, py3compat.u_format(o))
169 (u'In [2]: !ls', "get_ipython().system('ls')"),
169 for i, o in [
170 (u'In [3]: abs?', "get_ipython().run_line_magic('pinfo', 'abs')"),
170 (u"In [1]: %lsmagic", "get_ipython().run_line_magic('lsmagic', '')"),
171 (u'In [4]: b = %who', "b = get_ipython().run_line_magic('who', '')"),
171 (u">>> %lsmagic", "get_ipython().run_line_magic('lsmagic', '')"),
172 ]],
172 (u"In [2]: !ls", "get_ipython().system('ls')"),
173 )
173 (u"In [3]: abs?", "get_ipython().run_line_magic('pinfo', 'abs')"),
174 (u"In [4]: b = %who", "b = get_ipython().run_line_magic('who', '')"),
175 ]
176 ],
177 )
174
178
175 # multiline syntax examples. Each of these should be a list of lists, with
179 # multiline syntax examples. Each of these should be a list of lists, with
176 # each entry itself having pairs of raw/transformed input. The union (with
180 # each entry itself having pairs of raw/transformed input. The union (with
177 # '\n'.join() of the transformed inputs is what the splitter should produce
181 # '\n'.join() of the transformed inputs is what the splitter should produce
178 # when fed the raw lines one at a time via push.
182 # when fed the raw lines one at a time via push.
179 syntax_ml = \
183 syntax_ml = \
180 dict(classic_prompt =
184 dict(classic_prompt =
181 [ [('>>> for i in range(10):','for i in range(10):'),
185 [ [('>>> for i in range(10):','for i in range(10):'),
182 ('... print i',' print i'),
186 ('... print i',' print i'),
183 ('... ', ''),
187 ('... ', ''),
184 ],
188 ],
185 [('>>> a="""','a="""'),
189 [('>>> a="""','a="""'),
186 ('... 123"""','123"""'),
190 ('... 123"""','123"""'),
187 ],
191 ],
188 [('a="""','a="""'),
192 [('a="""','a="""'),
189 ('... 123','123'),
193 ('... 123','123'),
190 ('... 456"""','456"""'),
194 ('... 456"""','456"""'),
191 ],
195 ],
192 [('a="""','a="""'),
196 [('a="""','a="""'),
193 ('>>> 123','123'),
197 ('>>> 123','123'),
194 ('... 456"""','456"""'),
198 ('... 456"""','456"""'),
195 ],
199 ],
196 [('a="""','a="""'),
200 [('a="""','a="""'),
197 ('123','123'),
201 ('123','123'),
198 ('... 456"""','... 456"""'),
202 ('... 456"""','... 456"""'),
199 ],
203 ],
200 [('....__class__','....__class__'),
204 [('....__class__','....__class__'),
201 ],
205 ],
202 [('a=5', 'a=5'),
206 [('a=5', 'a=5'),
203 ('...', ''),
207 ('...', ''),
204 ],
208 ],
205 [('>>> def f(x):', 'def f(x):'),
209 [('>>> def f(x):', 'def f(x):'),
206 ('...', ''),
210 ('...', ''),
207 ('... return x', ' return x'),
211 ('... return x', ' return x'),
208 ],
212 ],
209 [('board = """....', 'board = """....'),
213 [('board = """....', 'board = """....'),
210 ('....', '....'),
214 ('....', '....'),
211 ('...."""', '...."""'),
215 ('...."""', '...."""'),
212 ],
216 ],
213 ],
217 ],
214
218
215 ipy_prompt =
219 ipy_prompt =
216 [ [('In [24]: for i in range(10):','for i in range(10):'),
220 [ [('In [24]: for i in range(10):','for i in range(10):'),
217 (' ....: print i',' print i'),
221 (' ....: print i',' print i'),
218 (' ....: ', ''),
222 (' ....: ', ''),
219 ],
223 ],
220 [('In [24]: for i in range(10):','for i in range(10):'),
224 [('In [24]: for i in range(10):','for i in range(10):'),
221 # Qt console prompts expand with spaces, not dots
225 # Qt console prompts expand with spaces, not dots
222 (' ...: print i',' print i'),
226 (' ...: print i',' print i'),
223 (' ...: ', ''),
227 (' ...: ', ''),
224 ],
228 ],
225 [('In [24]: for i in range(10):','for i in range(10):'),
229 [('In [24]: for i in range(10):','for i in range(10):'),
226 # Sometimes whitespace preceding '...' has been removed
230 # Sometimes whitespace preceding '...' has been removed
227 ('...: print i',' print i'),
231 ('...: print i',' print i'),
228 ('...: ', ''),
232 ('...: ', ''),
229 ],
233 ],
230 [('In [24]: for i in range(10):','for i in range(10):'),
234 [('In [24]: for i in range(10):','for i in range(10):'),
231 # Space after last continuation prompt has been removed (issue #6674)
235 # Space after last continuation prompt has been removed (issue #6674)
232 ('...: print i',' print i'),
236 ('...: print i',' print i'),
233 ('...:', ''),
237 ('...:', ''),
234 ],
238 ],
235 [('In [2]: a="""','a="""'),
239 [('In [2]: a="""','a="""'),
236 (' ...: 123"""','123"""'),
240 (' ...: 123"""','123"""'),
237 ],
241 ],
238 [('a="""','a="""'),
242 [('a="""','a="""'),
239 (' ...: 123','123'),
243 (' ...: 123','123'),
240 (' ...: 456"""','456"""'),
244 (' ...: 456"""','456"""'),
241 ],
245 ],
242 [('a="""','a="""'),
246 [('a="""','a="""'),
243 ('In [1]: 123','123'),
247 ('In [1]: 123','123'),
244 (' ...: 456"""','456"""'),
248 (' ...: 456"""','456"""'),
245 ],
249 ],
246 [('a="""','a="""'),
250 [('a="""','a="""'),
247 ('123','123'),
251 ('123','123'),
248 (' ...: 456"""',' ...: 456"""'),
252 (' ...: 456"""',' ...: 456"""'),
249 ],
253 ],
250 ],
254 ],
251
255
252 multiline_datastructure_prompt =
256 multiline_datastructure_prompt =
253 [ [('>>> a = [1,','a = [1,'),
257 [ [('>>> a = [1,','a = [1,'),
254 ('... 2]','2]'),
258 ('... 2]','2]'),
255 ],
259 ],
256 ],
260 ],
257
261
258 multiline_datastructure =
262 multiline_datastructure =
259 [ [('b = ("%s"', None),
263 [ [('b = ("%s"', None),
260 ('# comment', None),
264 ('# comment', None),
261 ('%foo )', 'b = ("%s"\n# comment\n%foo )'),
265 ('%foo )', 'b = ("%s"\n# comment\n%foo )'),
262 ],
266 ],
263 ],
267 ],
264
268
265 multiline_string =
269 multiline_string =
266 [ [("'''foo?", None),
270 [ [("'''foo?", None),
267 ("bar'''", "'''foo?\nbar'''"),
271 ("bar'''", "'''foo?\nbar'''"),
268 ],
272 ],
269 ],
273 ],
270
274
271 leading_indent =
275 leading_indent =
272 [ [(' print "hi"','print "hi"'),
276 [ [(' print "hi"','print "hi"'),
273 ],
277 ],
274 [(' for a in range(5):','for a in range(5):'),
278 [(' for a in range(5):','for a in range(5):'),
275 (' a*2',' a*2'),
279 (' a*2',' a*2'),
276 ],
280 ],
277 [(' a="""','a="""'),
281 [(' a="""','a="""'),
278 (' 123"""','123"""'),
282 (' 123"""','123"""'),
279 ],
283 ],
280 [('a="""','a="""'),
284 [('a="""','a="""'),
281 (' 123"""',' 123"""'),
285 (' 123"""',' 123"""'),
282 ],
286 ],
283 ],
287 ],
284
288
285 cellmagic =
289 cellmagic =
286 [ [(u'%%foo a', None),
290 [ [(u'%%foo a', None),
287 (None, u_fmt("get_ipython().run_cell_magic('foo', 'a', '')")),
291 (None, u_fmt("get_ipython().run_cell_magic('foo', 'a', '')")),
288 ],
292 ],
289 [(u'%%bar 123', None),
293 [(u'%%bar 123', None),
290 (u'hello', None),
294 (u'hello', None),
291 (None , u_fmt("get_ipython().run_cell_magic('bar', '123', 'hello')")),
295 (None , u_fmt("get_ipython().run_cell_magic('bar', '123', 'hello')")),
292 ],
296 ],
293 [(u'a=5', 'a=5'),
297 [(u'a=5', 'a=5'),
294 (u'%%cellmagic', '%%cellmagic'),
298 (u'%%cellmagic', '%%cellmagic'),
295 ],
299 ],
296 ],
300 ],
297
301
298 escaped =
302 escaped =
299 [ [('%abc def \\', None),
303 [ [('%abc def \\', None),
300 ('ghi', u_fmt("get_ipython().run_line_magic('abc', 'def ghi')")),
304 ('ghi', u_fmt("get_ipython().run_line_magic('abc', 'def ghi')")),
301 ],
305 ],
302 [('%abc def \\', None),
306 [('%abc def \\', None),
303 ('ghi\\', None),
307 ('ghi\\', None),
304 (None, u_fmt("get_ipython().run_line_magic('abc', 'def ghi')")),
308 (None, u_fmt("get_ipython().run_line_magic('abc', 'def ghi')")),
305 ],
309 ],
306 ],
310 ],
307
311
308 assign_magic =
312 assign_magic =
309 [ [(u'a = %bc de \\', None),
313 [ [(u'a = %bc de \\', None),
310 (u'fg', u_fmt("a = get_ipython().run_line_magic('bc', 'de fg')")),
314 (u'fg', u_fmt("a = get_ipython().run_line_magic('bc', 'de fg')")),
311 ],
315 ],
312 [(u'a = %bc de \\', None),
316 [(u'a = %bc de \\', None),
313 (u'fg\\', None),
317 (u'fg\\', None),
314 (None, u_fmt("a = get_ipython().run_line_magic('bc', 'de fg')")),
318 (None, u_fmt("a = get_ipython().run_line_magic('bc', 'de fg')")),
315 ],
319 ],
316 ],
320 ],
317
321
318 assign_system =
322 assign_system =
319 [ [(u'a = !bc de \\', None),
323 [ [(u'a = !bc de \\', None),
320 (u'fg', u_fmt("a = get_ipython().getoutput('bc de fg')")),
324 (u'fg', u_fmt("a = get_ipython().getoutput('bc de fg')")),
321 ],
325 ],
322 [(u'a = !bc de \\', None),
326 [(u'a = !bc de \\', None),
323 (u'fg\\', None),
327 (u'fg\\', None),
324 (None, u_fmt("a = get_ipython().getoutput('bc de fg')")),
328 (None, u_fmt("a = get_ipython().getoutput('bc de fg')")),
325 ],
329 ],
326 ],
330 ],
327 )
331 )
328
332
329
333
330 def test_assign_system():
334 def test_assign_system():
331 tt.check_pairs(transform_and_reset(ipt.assign_from_system), syntax['assign_system'])
335 tt.check_pairs(transform_and_reset(ipt.assign_from_system), syntax['assign_system'])
332
336
333 def test_assign_magic():
337 def test_assign_magic():
334 tt.check_pairs(transform_and_reset(ipt.assign_from_magic), syntax['assign_magic'])
338 tt.check_pairs(transform_and_reset(ipt.assign_from_magic), syntax['assign_magic'])
335
339
336 def test_classic_prompt():
340 def test_classic_prompt():
337 tt.check_pairs(transform_and_reset(ipt.classic_prompt), syntax['classic_prompt'])
341 tt.check_pairs(transform_and_reset(ipt.classic_prompt), syntax['classic_prompt'])
338 for example in syntax_ml['classic_prompt']:
342 for example in syntax_ml['classic_prompt']:
339 transform_checker(example, ipt.classic_prompt)
343 transform_checker(example, ipt.classic_prompt)
340 for example in syntax_ml['multiline_datastructure_prompt']:
344 for example in syntax_ml['multiline_datastructure_prompt']:
341 transform_checker(example, ipt.classic_prompt)
345 transform_checker(example, ipt.classic_prompt)
342
346
343 # Check that we don't transform the second line if the first is obviously
347 # Check that we don't transform the second line if the first is obviously
344 # IPython syntax
348 # IPython syntax
345 transform_checker([
349 transform_checker([
346 (u'%foo', '%foo'),
350 (u'%foo', '%foo'),
347 (u'>>> bar', '>>> bar'),
351 (u'>>> bar', '>>> bar'),
348 ], ipt.classic_prompt)
352 ], ipt.classic_prompt)
349
353
350
354
351 def test_ipy_prompt():
355 def test_ipy_prompt():
352 tt.check_pairs(transform_and_reset(ipt.ipy_prompt), syntax['ipy_prompt'])
356 tt.check_pairs(transform_and_reset(ipt.ipy_prompt), syntax['ipy_prompt'])
353 for example in syntax_ml['ipy_prompt']:
357 for example in syntax_ml['ipy_prompt']:
354 transform_checker(example, ipt.ipy_prompt)
358 transform_checker(example, ipt.ipy_prompt)
355
359
356 # Check that we don't transform the second line if we're inside a cell magic
360 # Check that we don't transform the second line if we're inside a cell magic
357 transform_checker([
361 transform_checker([
358 (u'%%foo', '%%foo'),
362 (u'%%foo', '%%foo'),
359 (u'In [1]: bar', 'In [1]: bar'),
363 (u'In [1]: bar', 'In [1]: bar'),
360 ], ipt.ipy_prompt)
364 ], ipt.ipy_prompt)
361
365
362 def test_assemble_logical_lines():
366 def test_assemble_logical_lines():
363 tests = \
367 tests = \
364 [ [(u"a = \\", None),
368 [ [(u"a = \\", None),
365 (u"123", u"a = 123"),
369 (u"123", u"a = 123"),
366 ],
370 ],
367 [(u"a = \\", None), # Test resetting when within a multi-line string
371 [(u"a = \\", None), # Test resetting when within a multi-line string
368 (u"12 *\\", None),
372 (u"12 *\\", None),
369 (None, u"a = 12 *"),
373 (None, u"a = 12 *"),
370 ],
374 ],
371 [(u"# foo\\", u"# foo\\"), # Comments can't be continued like this
375 [(u"# foo\\", u"# foo\\"), # Comments can't be continued like this
372 ],
376 ],
373 ]
377 ]
374 for example in tests:
378 for example in tests:
375 transform_checker(example, ipt.assemble_logical_lines)
379 transform_checker(example, ipt.assemble_logical_lines)
376
380
377 def test_assemble_python_lines():
381 def test_assemble_python_lines():
378 tests = \
382 tests = \
379 [ [(u"a = '''", None),
383 [ [(u"a = '''", None),
380 (u"abc'''", u"a = '''\nabc'''"),
384 (u"abc'''", u"a = '''\nabc'''"),
381 ],
385 ],
382 [(u"a = '''", None), # Test resetting when within a multi-line string
386 [(u"a = '''", None), # Test resetting when within a multi-line string
383 (u"def", None),
387 (u"def", None),
384 (None, u"a = '''\ndef"),
388 (None, u"a = '''\ndef"),
385 ],
389 ],
386 [(u"a = [1,", None),
390 [(u"a = [1,", None),
387 (u"2]", u"a = [1,\n2]"),
391 (u"2]", u"a = [1,\n2]"),
388 ],
392 ],
389 [(u"a = [1,", None), # Test resetting when within a multi-line string
393 [(u"a = [1,", None), # Test resetting when within a multi-line string
390 (u"2,", None),
394 (u"2,", None),
391 (None, u"a = [1,\n2,"),
395 (None, u"a = [1,\n2,"),
392 ],
396 ],
393 [(u"a = '''", None), # Test line continuation within a multi-line string
397 [(u"a = '''", None), # Test line continuation within a multi-line string
394 (u"abc\\", None),
398 (u"abc\\", None),
395 (u"def", None),
399 (u"def", None),
396 (u"'''", u"a = '''\nabc\\\ndef\n'''"),
400 (u"'''", u"a = '''\nabc\\\ndef\n'''"),
397 ],
401 ],
398 ] + syntax_ml['multiline_datastructure']
402 ] + syntax_ml['multiline_datastructure']
399 for example in tests:
403 for example in tests:
400 transform_checker(example, ipt.assemble_python_lines)
404 transform_checker(example, ipt.assemble_python_lines)
401
405
402
406
403 def test_help_end():
407 def test_help_end():
404 tt.check_pairs(transform_and_reset(ipt.help_end), syntax['end_help'])
408 tt.check_pairs(transform_and_reset(ipt.help_end), syntax['end_help'])
405
409
406 def test_escaped_noesc():
410 def test_escaped_noesc():
407 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_noesc'])
411 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_noesc'])
408
412
409
413
410 def test_escaped_shell():
414 def test_escaped_shell():
411 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_shell'])
415 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_shell'])
412
416
413
417
414 def test_escaped_help():
418 def test_escaped_help():
415 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_help'])
419 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_help'])
416
420
417
421
418 def test_escaped_magic():
422 def test_escaped_magic():
419 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_magic'])
423 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_magic'])
420
424
421
425
422 def test_escaped_quote():
426 def test_escaped_quote():
423 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_quote'])
427 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_quote'])
424
428
425
429
426 def test_escaped_quote2():
430 def test_escaped_quote2():
427 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_quote2'])
431 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_quote2'])
428
432
429
433
430 def test_escaped_paren():
434 def test_escaped_paren():
431 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_paren'])
435 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_paren'])
432
436
433
437
434 def test_cellmagic():
438 def test_cellmagic():
435 for example in syntax_ml['cellmagic']:
439 for example in syntax_ml['cellmagic']:
436 transform_checker(example, ipt.cellmagic)
440 transform_checker(example, ipt.cellmagic)
437
441
438 line_example = [(u'%%bar 123', None),
442 line_example = [(u'%%bar 123', None),
439 (u'hello', None),
443 (u'hello', None),
440 (u'' , u_fmt("get_ipython().run_cell_magic('bar', '123', 'hello')")),
444 (u'' , u_fmt("get_ipython().run_cell_magic('bar', '123', 'hello')")),
441 ]
445 ]
442 transform_checker(line_example, ipt.cellmagic, end_on_blank_line=True)
446 transform_checker(line_example, ipt.cellmagic, end_on_blank_line=True)
443
447
444 def test_has_comment():
448 def test_has_comment():
445 tests = [('text', False),
449 tests = [('text', False),
446 ('text #comment', True),
450 ('text #comment', True),
447 ('text #comment\n', True),
451 ('text #comment\n', True),
448 ('#comment', True),
452 ('#comment', True),
449 ('#comment\n', True),
453 ('#comment\n', True),
450 ('a = "#string"', False),
454 ('a = "#string"', False),
451 ('a = "#string" # comment', True),
455 ('a = "#string" # comment', True),
452 ('a #comment not "string"', True),
456 ('a #comment not "string"', True),
453 ]
457 ]
454 tt.check_pairs(ipt.has_comment, tests)
458 tt.check_pairs(ipt.has_comment, tests)
455
459
456 @ipt.TokenInputTransformer.wrap
460 @ipt.TokenInputTransformer.wrap
457 def decistmt(tokens):
461 def decistmt(tokens):
458 """Substitute Decimals for floats in a string of statements.
462 """Substitute Decimals for floats in a string of statements.
459
463
460 Based on an example from the tokenize module docs.
464 Based on an example from the tokenize module docs.
461 """
465 """
462 result = []
466 result = []
463 for toknum, tokval, _, _, _ in tokens:
467 for toknum, tokval, _, _, _ in tokens:
464 if toknum == tokenize.NUMBER and '.' in tokval: # replace NUMBER tokens
468 if toknum == tokenize.NUMBER and '.' in tokval: # replace NUMBER tokens
465 for newtok in [
469 for newtok in [
466 (tokenize.NAME, 'Decimal'),
470 (tokenize.NAME, 'Decimal'),
467 (tokenize.OP, '('),
471 (tokenize.OP, '('),
468 (tokenize.STRING, repr(tokval)),
472 (tokenize.STRING, repr(tokval)),
469 (tokenize.OP, ')')
473 (tokenize.OP, ')')
470 ]:
474 ]:
471 yield newtok
475 yield newtok
472 else:
476 else:
473 yield (toknum, tokval)
477 yield (toknum, tokval)
474
478
475
479
476
480
477 def test_token_input_transformer():
481 def test_token_input_transformer():
478 tests = [(u'1.2', u_fmt(u"Decimal ('1.2')")),
482 tests = [(u'1.2', u_fmt(u"Decimal ('1.2')")),
479 (u'"1.2"', u'"1.2"'),
483 (u'"1.2"', u'"1.2"'),
480 ]
484 ]
481 tt.check_pairs(transform_and_reset(decistmt), tests)
485 tt.check_pairs(transform_and_reset(decistmt), tests)
482 ml_tests = \
486 ml_tests = \
483 [ [(u"a = 1.2; b = '''x", None),
487 [ [(u"a = 1.2; b = '''x", None),
484 (u"y'''", u_fmt(u"a =Decimal ('1.2');b ='''x\ny'''")),
488 (u"y'''", u_fmt(u"a =Decimal ('1.2');b ='''x\ny'''")),
485 ],
489 ],
486 [(u"a = [1.2,", None),
490 [(u"a = [1.2,", None),
487 (u"3]", u_fmt(u"a =[Decimal ('1.2'),\n3 ]")),
491 (u"3]", u_fmt(u"a =[Decimal ('1.2'),\n3 ]")),
488 ],
492 ],
489 [(u"a = '''foo", None), # Test resetting when within a multi-line string
493 [(u"a = '''foo", None), # Test resetting when within a multi-line string
490 (u"bar", None),
494 (u"bar", None),
491 (None, u"a = '''foo\nbar"),
495 (None, u"a = '''foo\nbar"),
492 ],
496 ],
493 ]
497 ]
494 for example in ml_tests:
498 for example in ml_tests:
495 transform_checker(example, decistmt)
499 transform_checker(example, decistmt)
@@ -1,358 +1,395 b''
1 """Tests for the token-based transformers in IPython.core.inputtransformer2
1 """Tests for the token-based transformers in IPython.core.inputtransformer2
2
2
3 Line-based transformers are the simpler ones; token-based transformers are
3 Line-based transformers are the simpler ones; token-based transformers are
4 more complex. See test_inputtransformer2_line for tests for line-based
4 more complex. See test_inputtransformer2_line for tests for line-based
5 transformations.
5 transformations.
6 """
6 """
7 import nose.tools as nt
7 import nose.tools as nt
8 import string
8 import string
9 import sys
9 import sys
10 from textwrap import dedent
10 from textwrap import dedent
11
11
12 import pytest
12 import pytest
13
13
14 from IPython.core import inputtransformer2 as ipt2
14 from IPython.core import inputtransformer2 as ipt2
15 from IPython.core.inputtransformer2 import _find_assign_op, make_tokens_by_line
15 from IPython.core.inputtransformer2 import _find_assign_op, make_tokens_by_line
16 from IPython.testing.decorators import skip
16 from IPython.testing.decorators import skip
17
17
18 MULTILINE_MAGIC = ("""\
18 MULTILINE_MAGIC = (
19 """\
19 a = f()
20 a = f()
20 %foo \\
21 %foo \\
21 bar
22 bar
22 g()
23 g()
23 """.splitlines(keepends=True), (2, 0), """\
24 """.splitlines(
25 keepends=True
26 ),
27 (2, 0),
28 """\
24 a = f()
29 a = f()
25 get_ipython().run_line_magic('foo', ' bar')
30 get_ipython().run_line_magic('foo', ' bar')
26 g()
31 g()
27 """.splitlines(keepends=True))
32 """.splitlines(
33 keepends=True
34 ),
35 )
28
36
29 INDENTED_MAGIC = ("""\
37 INDENTED_MAGIC = (
38 """\
30 for a in range(5):
39 for a in range(5):
31 %ls
40 %ls
32 """.splitlines(keepends=True), (2, 4), """\
41 """.splitlines(
42 keepends=True
43 ),
44 (2, 4),
45 """\
33 for a in range(5):
46 for a in range(5):
34 get_ipython().run_line_magic('ls', '')
47 get_ipython().run_line_magic('ls', '')
35 """.splitlines(keepends=True))
48 """.splitlines(
49 keepends=True
50 ),
51 )
36
52
37 CRLF_MAGIC = ([
53 CRLF_MAGIC = (
38 "a = f()\n",
54 ["a = f()\n", "%ls\r\n", "g()\n"],
39 "%ls\r\n",
55 (2, 0),
40 "g()\n"
56 ["a = f()\n", "get_ipython().run_line_magic('ls', '')\n", "g()\n"],
41 ], (2, 0), [
57 )
42 "a = f()\n",
58
43 "get_ipython().run_line_magic('ls', '')\n",
59 MULTILINE_MAGIC_ASSIGN = (
44 "g()\n"
60 """\
45 ])
46
47 MULTILINE_MAGIC_ASSIGN = ("""\
48 a = f()
61 a = f()
49 b = %foo \\
62 b = %foo \\
50 bar
63 bar
51 g()
64 g()
52 """.splitlines(keepends=True), (2, 4), """\
65 """.splitlines(
66 keepends=True
67 ),
68 (2, 4),
69 """\
53 a = f()
70 a = f()
54 b = get_ipython().run_line_magic('foo', ' bar')
71 b = get_ipython().run_line_magic('foo', ' bar')
55 g()
72 g()
56 """.splitlines(keepends=True))
73 """.splitlines(
74 keepends=True
75 ),
76 )
57
77
58 MULTILINE_SYSTEM_ASSIGN = ("""\
78 MULTILINE_SYSTEM_ASSIGN = ("""\
59 a = f()
79 a = f()
60 b = !foo \\
80 b = !foo \\
61 bar
81 bar
62 g()
82 g()
63 """.splitlines(keepends=True), (2, 4), """\
83 """.splitlines(keepends=True), (2, 4), """\
64 a = f()
84 a = f()
65 b = get_ipython().getoutput('foo bar')
85 b = get_ipython().getoutput('foo bar')
66 g()
86 g()
67 """.splitlines(keepends=True))
87 """.splitlines(keepends=True))
68
88
69 #####
89 #####
70
90
71 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = ("""\
91 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = ("""\
72 def test():
92 def test():
73 for i in range(1):
93 for i in range(1):
74 print(i)
94 print(i)
75 res =! ls
95 res =! ls
76 """.splitlines(keepends=True), (4, 7), '''\
96 """.splitlines(
97 keepends=True
98 ),
99 (4, 7),
100 """\
77 def test():
101 def test():
78 for i in range(1):
102 for i in range(1):
79 print(i)
103 print(i)
80 res =get_ipython().getoutput(\' ls\')
104 res =get_ipython().getoutput(\' ls\')
81 '''.splitlines(keepends=True))
105 """.splitlines(
106 keepends=True
107 ),
108 )
82
109
83 ######
110 ######
84
111
85 AUTOCALL_QUOTE = (
112 AUTOCALL_QUOTE = ([",f 1 2 3\n"], (1, 0), ['f("1", "2", "3")\n'])
86 [",f 1 2 3\n"], (1, 0),
87 ['f("1", "2", "3")\n']
88 )
89
113
90 AUTOCALL_QUOTE2 = (
114 AUTOCALL_QUOTE2 = ([";f 1 2 3\n"], (1, 0), ['f("1 2 3")\n'])
91 [";f 1 2 3\n"], (1, 0),
92 ['f("1 2 3")\n']
93 )
94
115
95 AUTOCALL_PAREN = (
116 AUTOCALL_PAREN = (["/f 1 2 3\n"], (1, 0), ["f(1, 2, 3)\n"])
96 ["/f 1 2 3\n"], (1, 0),
97 ['f(1, 2, 3)\n']
98 )
99
117
100 SIMPLE_HELP = (
118 SIMPLE_HELP = (["foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', 'foo')\n"])
101 ["foo?\n"], (1, 0),
102 ["get_ipython().run_line_magic('pinfo', 'foo')\n"]
103 )
104
119
105 DETAILED_HELP = (
120 DETAILED_HELP = (
106 ["foo??\n"], (1, 0),
121 ["foo??\n"],
107 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"]
122 (1, 0),
123 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"],
108 )
124 )
109
125
110 MAGIC_HELP = (
126 MAGIC_HELP = (["%foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', '%foo')\n"])
111 ["%foo?\n"], (1, 0),
112 ["get_ipython().run_line_magic('pinfo', '%foo')\n"]
113 )
114
127
115 HELP_IN_EXPR = (
128 HELP_IN_EXPR = (
116 ["a = b + c?\n"], (1, 0),
129 ["a = b + c?\n"],
117 ["get_ipython().set_next_input('a = b + c');"
130 (1, 0),
118 "get_ipython().run_line_magic('pinfo', 'c')\n"]
131 ["get_ipython().run_line_magic('pinfo', 'c')\n"],
119 )
132 )
120
133
121 HELP_CONTINUED_LINE = ("""\
134 HELP_CONTINUED_LINE = (
135 """\
122 a = \\
136 a = \\
123 zip?
137 zip?
124 """.splitlines(keepends=True), (1, 0),
138 """.splitlines(
125 [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
139 keepends=True
140 ),
141 (1, 0),
142 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
126 )
143 )
127
144
128 HELP_MULTILINE = ("""\
145 HELP_MULTILINE = (
146 """\
129 (a,
147 (a,
130 b) = zip?
148 b) = zip?
131 """.splitlines(keepends=True), (1, 0),
149 """.splitlines(
132 [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
150 keepends=True
151 ),
152 (1, 0),
153 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
133 )
154 )
134
155
135 HELP_UNICODE = (
156 HELP_UNICODE = (
136 ["Ο€.foo?\n"], (1, 0),
157 ["Ο€.foo?\n"],
137 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"]
158 (1, 0),
159 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"],
138 )
160 )
139
161
140
162
141 def null_cleanup_transformer(lines):
163 def null_cleanup_transformer(lines):
142 """
164 """
143 A cleanup transform that returns an empty list.
165 A cleanup transform that returns an empty list.
144 """
166 """
145 return []
167 return []
146
168
147 def check_make_token_by_line_never_ends_empty():
169 def check_make_token_by_line_never_ends_empty():
148 """
170 """
149 Check that not sequence of single or double characters ends up leading to en empty list of tokens
171 Check that not sequence of single or double characters ends up leading to en empty list of tokens
150 """
172 """
151 from string import printable
173 from string import printable
174
152 for c in printable:
175 for c in printable:
153 nt.assert_not_equal(make_tokens_by_line(c)[-1], [])
176 nt.assert_not_equal(make_tokens_by_line(c)[-1], [])
154 for k in printable:
177 for k in printable:
155 nt.assert_not_equal(make_tokens_by_line(c+k)[-1], [])
178 nt.assert_not_equal(make_tokens_by_line(c+k)[-1], [])
156
179
157 def check_find(transformer, case, match=True):
180 def check_find(transformer, case, match=True):
158 sample, expected_start, _ = case
181 sample, expected_start, _ = case
159 tbl = make_tokens_by_line(sample)
182 tbl = make_tokens_by_line(sample)
160 res = transformer.find(tbl)
183 res = transformer.find(tbl)
161 if match:
184 if match:
162 # start_line is stored 0-indexed, expected values are 1-indexed
185 # start_line is stored 0-indexed, expected values are 1-indexed
163 nt.assert_equal((res.start_line+1, res.start_col), expected_start)
186 nt.assert_equal((res.start_line+1, res.start_col), expected_start)
164 return res
187 return res
165 else:
188 else:
166 nt.assert_is(res, None)
189 nt.assert_is(res, None)
167
190
191
168 def check_transform(transformer_cls, case):
192 def check_transform(transformer_cls, case):
169 lines, start, expected = case
193 lines, start, expected = case
170 transformer = transformer_cls(start)
194 transformer = transformer_cls(start)
171 nt.assert_equal(transformer.transform(lines), expected)
195 nt.assert_equal(transformer.transform(lines), expected)
172
196
197
173 def test_continued_line():
198 def test_continued_line():
174 lines = MULTILINE_MAGIC_ASSIGN[0]
199 lines = MULTILINE_MAGIC_ASSIGN[0]
175 nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2)
200 nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2)
176
201
177 nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar")
202 nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar")
178
203
204
179 def test_find_assign_magic():
205 def test_find_assign_magic():
180 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
206 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
181 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
207 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
182 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
208 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
183
209
210
184 def test_transform_assign_magic():
211 def test_transform_assign_magic():
185 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
212 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
186
213
214
187 def test_find_assign_system():
215 def test_find_assign_system():
188 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
216 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
189 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
217 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
190 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
218 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
191 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
219 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
192 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
220 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
193
221
222
194 def test_transform_assign_system():
223 def test_transform_assign_system():
195 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
224 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
196 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
225 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
197
226
227
198 def test_find_magic_escape():
228 def test_find_magic_escape():
199 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
229 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
200 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
230 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
201 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
231 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
202
232
233
203 def test_transform_magic_escape():
234 def test_transform_magic_escape():
204 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
235 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
205 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
236 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
206 check_transform(ipt2.EscapedCommand, CRLF_MAGIC)
237 check_transform(ipt2.EscapedCommand, CRLF_MAGIC)
207
238
239
208 def test_find_autocalls():
240 def test_find_autocalls():
209 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
241 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
210 print("Testing %r" % case[0])
242 print("Testing %r" % case[0])
211 check_find(ipt2.EscapedCommand, case)
243 check_find(ipt2.EscapedCommand, case)
212
244
245
213 def test_transform_autocall():
246 def test_transform_autocall():
214 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
247 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
215 print("Testing %r" % case[0])
248 print("Testing %r" % case[0])
216 check_transform(ipt2.EscapedCommand, case)
249 check_transform(ipt2.EscapedCommand, case)
217
250
251
218 def test_find_help():
252 def test_find_help():
219 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
253 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
220 check_find(ipt2.HelpEnd, case)
254 check_find(ipt2.HelpEnd, case)
221
255
222 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
256 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
223 nt.assert_equal(tf.q_line, 1)
257 nt.assert_equal(tf.q_line, 1)
224 nt.assert_equal(tf.q_col, 3)
258 nt.assert_equal(tf.q_col, 3)
225
259
226 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
260 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
227 nt.assert_equal(tf.q_line, 1)
261 nt.assert_equal(tf.q_line, 1)
228 nt.assert_equal(tf.q_col, 8)
262 nt.assert_equal(tf.q_col, 8)
229
263
230 # ? in a comment does not trigger help
264 # ? in a comment does not trigger help
231 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
265 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
232 # Nor in a string
266 # Nor in a string
233 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
267 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
234
268
269
235 def test_transform_help():
270 def test_transform_help():
236 tf = ipt2.HelpEnd((1, 0), (1, 9))
271 tf = ipt2.HelpEnd((1, 0), (1, 9))
237 nt.assert_equal(tf.transform(HELP_IN_EXPR[0]), HELP_IN_EXPR[2])
272 nt.assert_equal(tf.transform(HELP_IN_EXPR[0]), HELP_IN_EXPR[2])
238
273
239 tf = ipt2.HelpEnd((1, 0), (2, 3))
274 tf = ipt2.HelpEnd((1, 0), (2, 3))
240 nt.assert_equal(tf.transform(HELP_CONTINUED_LINE[0]), HELP_CONTINUED_LINE[2])
275 nt.assert_equal(tf.transform(HELP_CONTINUED_LINE[0]), HELP_CONTINUED_LINE[2])
241
276
242 tf = ipt2.HelpEnd((1, 0), (2, 8))
277 tf = ipt2.HelpEnd((1, 0), (2, 8))
243 nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2])
278 nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2])
244
279
245 tf = ipt2.HelpEnd((1, 0), (1, 0))
280 tf = ipt2.HelpEnd((1, 0), (1, 0))
246 nt.assert_equal(tf.transform(HELP_UNICODE[0]), HELP_UNICODE[2])
281 nt.assert_equal(tf.transform(HELP_UNICODE[0]), HELP_UNICODE[2])
247
282
283
248 def test_find_assign_op_dedent():
284 def test_find_assign_op_dedent():
249 """
285 """
250 be careful that empty token like dedent are not counted as parens
286 be careful that empty token like dedent are not counted as parens
251 """
287 """
288
252 class Tk:
289 class Tk:
253 def __init__(self, s):
290 def __init__(self, s):
254 self.string = s
291 self.string = s
255
292
256 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','a','=','b')]), 2)
293 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','a','=','b')]), 2)
257 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','(', 'a','=','b', ')', '=' ,'5')]), 6)
294 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','(', 'a','=','b', ')', '=' ,'5')]), 6)
258
295
259 examples = [
296 examples = [
260 pytest.param("a = 1", "complete", None),
297 pytest.param("a = 1", "complete", None),
261 pytest.param("for a in range(5):", "incomplete", 4),
298 pytest.param("for a in range(5):", "incomplete", 4),
262 pytest.param("for a in range(5):\n if a > 0:", "incomplete", 8),
299 pytest.param("for a in range(5):\n if a > 0:", "incomplete", 8),
263 pytest.param("raise = 2", "invalid", None),
300 pytest.param("raise = 2", "invalid", None),
264 pytest.param("a = [1,\n2,", "incomplete", 0),
301 pytest.param("a = [1,\n2,", "incomplete", 0),
265 pytest.param("(\n))", "incomplete", 0),
302 pytest.param("(\n))", "incomplete", 0),
266 pytest.param("\\\r\n", "incomplete", 0),
303 pytest.param("\\\r\n", "incomplete", 0),
267 pytest.param("a = '''\n hi", "incomplete", 3),
304 pytest.param("a = '''\n hi", "incomplete", 3),
268 pytest.param("def a():\n x=1\n global x", "invalid", None),
305 pytest.param("def a():\n x=1\n global x", "invalid", None),
269 pytest.param(
306 pytest.param(
270 "a \\ ",
307 "a \\ ",
271 "invalid",
308 "invalid",
272 None,
309 None,
273 marks=pytest.mark.xfail(
310 marks=pytest.mark.xfail(
274 reason="Bug in python 3.9.8 – bpo 45738",
311 reason="Bug in python 3.9.8 – bpo 45738",
275 condition=sys.version_info[:3] == (3, 9, 8),
312 condition=sys.version_info[:3] == (3, 9, 8),
276 raises=SystemError,
313 raises=SystemError,
277 strict=True,
314 strict=True,
278 ),
315 ),
279 ), # Nothing allowed after backslash,
316 ), # Nothing allowed after backslash,
280 pytest.param("1\\\n+2", "complete", None),
317 pytest.param("1\\\n+2", "complete", None),
281 ]
318 ]
282
319
283
320
284 @skip('Tested on master, skip only on iptest not available on 7.x')
321 @skip('Tested on master, skip only on iptest not available on 7.x')
285 @pytest.mark.xfail(
322 @pytest.mark.xfail(
286 reason="Bug in python 3.9.8 – bpo 45738",
323 reason="Bug in python 3.9.8 – bpo 45738",
287 condition=sys.version_info[:3] == (3, 9, 8),
324 condition=sys.version_info[:3] == (3, 9, 8),
288 )
325 )
289 def test_check_complete():
326 def test_check_complete():
290 cc = ipt2.TransformerManager().check_complete
327 cc = ipt2.TransformerManager().check_complete
291
328
292 example = dedent("""
329 example = dedent(
330 """
293 if True:
331 if True:
294 a=1""" )
332 a=1"""
333 )
295
334
296 nt.assert_equal(cc(example), ('incomplete', 4))
335 nt.assert_equal(cc(example), ('incomplete', 4))
297 nt.assert_equal(cc(example+'\n'), ('complete', None))
336 nt.assert_equal(cc(example+'\n'), ('complete', None))
298 nt.assert_equal(cc(example+'\n '), ('complete', None))
337 nt.assert_equal(cc(example+'\n '), ('complete', None))
299
338
300 # no need to loop on all the letters/numbers.
339 # no need to loop on all the letters/numbers.
301 short = '12abAB'+string.printable[62:]
340 short = "12abAB" + string.printable[62:]
302 for c in short:
341 for c in short:
303 # test does not raise:
342 # test does not raise:
304 cc(c)
343 cc(c)
305 for k in short:
344 for k in short:
306 cc(c+k)
345 cc(c + k)
307
346
308 nt.assert_equal(cc("def f():\n x=0\n \\\n "), ('incomplete', 2))
347 nt.assert_equal(cc("def f():\n x=0\n \\\n "), ('incomplete', 2))
309
348
310 def test_check_complete_II():
349 def test_check_complete_II():
311 """
350 """
312 Test that multiple line strings are properly handled.
351 Test that multiple line strings are properly handled.
313
352
314 Separate test function for convenience
353 Separate test function for convenience
315
354
316 """
355 """
317 cc = ipt2.TransformerManager().check_complete
356 cc = ipt2.TransformerManager().check_complete
318 nt.assert_equal(cc('''def foo():\n """'''), ('incomplete', 4))
357 nt.assert_equal(cc('''def foo():\n """'''), ('incomplete', 4))
319
358
320
359
321 def test_null_cleanup_transformer():
360 def test_null_cleanup_transformer():
322 manager = ipt2.TransformerManager()
361 manager = ipt2.TransformerManager()
323 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
362 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
324 assert manager.transform_cell("") == ""
363 assert manager.transform_cell("") == ""
325
364
326
365
327
328
329 def test_side_effects_I():
366 def test_side_effects_I():
330 count = 0
367 count = 0
368
331 def counter(lines):
369 def counter(lines):
332 nonlocal count
370 nonlocal count
333 count += 1
371 count += 1
334 return lines
372 return lines
335
373
336 counter.has_side_effects = True
374 counter.has_side_effects = True
337
375
338 manager = ipt2.TransformerManager()
376 manager = ipt2.TransformerManager()
339 manager.cleanup_transforms.insert(0, counter)
377 manager.cleanup_transforms.insert(0, counter)
340 assert manager.check_complete("a=1\n") == ('complete', None)
378 assert manager.check_complete("a=1\n") == ("complete", None)
341 assert count == 0
379 assert count == 0
342
380
343
381
344
345
346 def test_side_effects_II():
382 def test_side_effects_II():
347 count = 0
383 count = 0
384
348 def counter(lines):
385 def counter(lines):
349 nonlocal count
386 nonlocal count
350 count += 1
387 count += 1
351 return lines
388 return lines
352
389
353 counter.has_side_effects = True
390 counter.has_side_effects = True
354
391
355 manager = ipt2.TransformerManager()
392 manager = ipt2.TransformerManager()
356 manager.line_transforms.insert(0, counter)
393 manager.line_transforms.insert(0, counter)
357 assert manager.check_complete("b=1\n") == ('complete', None)
394 assert manager.check_complete("b=1\n") == ("complete", None)
358 assert count == 0
395 assert count == 0
General Comments 0
You need to be logged in to leave comments. Login now