Show More
This diff has been collapsed as it changes many lines, (638 lines changed) Show them Hide them | |||||
@@ -0,0 +1,638 b'' | |||||
|
1 | """Input transformer machinery to support IPython special syntax. | |||
|
2 | ||||
|
3 | This includes the machinery to recognise and transform ``%magic`` commands, | |||
|
4 | ``!system`` commands, ``help?`` querying, prompt stripping, and so forth. | |||
|
5 | ||||
|
6 | Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were | |||
|
7 | deprecated in 7.0. | |||
|
8 | """ | |||
|
9 | ||||
|
10 | # Copyright (c) IPython Development Team. | |||
|
11 | # Distributed under the terms of the Modified BSD License. | |||
|
12 | ||||
|
13 | from codeop import compile_command | |||
|
14 | import re | |||
|
15 | import tokenize | |||
|
16 | from typing import List, Tuple | |||
|
17 | import warnings | |||
|
18 | ||||
|
19 | _indent_re = re.compile(r'^[ \t]+') | |||
|
20 | ||||
|
21 | def leading_indent(lines): | |||
|
22 | """Remove leading indentation. | |||
|
23 | ||||
|
24 | If the first line starts with a spaces or tabs, the same whitespace will be | |||
|
25 | removed from each following line in the cell. | |||
|
26 | """ | |||
|
27 | m = _indent_re.match(lines[0]) | |||
|
28 | if not m: | |||
|
29 | return lines | |||
|
30 | space = m.group(0) | |||
|
31 | n = len(space) | |||
|
32 | return [l[n:] if l.startswith(space) else l | |||
|
33 | for l in lines] | |||
|
34 | ||||
|
35 | class PromptStripper: | |||
|
36 | """Remove matching input prompts from a block of input. | |||
|
37 | ||||
|
38 | Parameters | |||
|
39 | ---------- | |||
|
40 | prompt_re : regular expression | |||
|
41 | A regular expression matching any input prompt (including continuation, | |||
|
42 | e.g. ``...``) | |||
|
43 | initial_re : regular expression, optional | |||
|
44 | A regular expression matching only the initial prompt, but not continuation. | |||
|
45 | If no initial expression is given, prompt_re will be used everywhere. | |||
|
46 | Used mainly for plain Python prompts (``>>>``), where the continuation prompt | |||
|
47 | ``...`` is a valid Python expression in Python 3, so shouldn't be stripped. | |||
|
48 | ||||
|
49 | If initial_re and prompt_re differ, | |||
|
50 | only initial_re will be tested against the first line. | |||
|
51 | If any prompt is found on the first two lines, | |||
|
52 | prompts will be stripped from the rest of the block. | |||
|
53 | """ | |||
|
54 | def __init__(self, prompt_re, initial_re=None): | |||
|
55 | self.prompt_re = prompt_re | |||
|
56 | self.initial_re = initial_re or prompt_re | |||
|
57 | ||||
|
58 | def _strip(self, lines): | |||
|
59 | return [self.prompt_re.sub('', l, count=1) for l in lines] | |||
|
60 | ||||
|
61 | def __call__(self, lines): | |||
|
62 | if self.initial_re.match(lines[0]) or \ | |||
|
63 | (len(lines) > 1 and self.prompt_re.match(lines[1])): | |||
|
64 | return self._strip(lines) | |||
|
65 | return lines | |||
|
66 | ||||
|
67 | classic_prompt = PromptStripper( | |||
|
68 | prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'), | |||
|
69 | initial_re=re.compile(r'^>>>( |$)') | |||
|
70 | ) | |||
|
71 | ||||
|
72 | ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)')) | |||
|
73 | ||||
|
74 | def cell_magic(lines): | |||
|
75 | if not lines[0].startswith('%%'): | |||
|
76 | return lines | |||
|
77 | if re.match('%%\w+\?', lines[0]): | |||
|
78 | # This case will be handled by help_end | |||
|
79 | return lines | |||
|
80 | magic_name, _, first_line = lines[0][2:-1].partition(' ') | |||
|
81 | body = ''.join(lines[1:]) | |||
|
82 | return ['get_ipython().run_cell_magic(%r, %r, %r)\n' | |||
|
83 | % (magic_name, first_line, body)] | |||
|
84 | ||||
|
85 | ||||
|
86 | def _find_assign_op(token_line): | |||
|
87 | """Get the index of the first assignment in the line ('=' not inside brackets) | |||
|
88 | ||||
|
89 | Note: We don't try to support multiple special assignment (a = b = %foo) | |||
|
90 | """ | |||
|
91 | paren_level = 0 | |||
|
92 | for i, ti in enumerate(token_line): | |||
|
93 | s = ti.string | |||
|
94 | if s == '=' and paren_level == 0: | |||
|
95 | return i | |||
|
96 | if s in '([{': | |||
|
97 | paren_level += 1 | |||
|
98 | elif s in ')]}': | |||
|
99 | if paren_level > 0: | |||
|
100 | paren_level -= 1 | |||
|
101 | ||||
|
102 | def find_end_of_continued_line(lines, start_line: int): | |||
|
103 | """Find the last line of a line explicitly extended using backslashes. | |||
|
104 | ||||
|
105 | Uses 0-indexed line numbers. | |||
|
106 | """ | |||
|
107 | end_line = start_line | |||
|
108 | while lines[end_line].endswith('\\\n'): | |||
|
109 | end_line += 1 | |||
|
110 | if end_line >= len(lines): | |||
|
111 | break | |||
|
112 | return end_line | |||
|
113 | ||||
|
114 | def assemble_continued_line(lines, start: Tuple[int, int], end_line: int): | |||
|
115 | """Assemble a single line from multiple continued line pieces | |||
|
116 | ||||
|
117 | Continued lines are lines ending in ``\``, and the line following the last | |||
|
118 | ``\`` in the block. | |||
|
119 | ||||
|
120 | For example, this code continues over multiple lines:: | |||
|
121 | ||||
|
122 | if (assign_ix is not None) \ | |||
|
123 | and (len(line) >= assign_ix + 2) \ | |||
|
124 | and (line[assign_ix+1].string == '%') \ | |||
|
125 | and (line[assign_ix+2].type == tokenize.NAME): | |||
|
126 | ||||
|
127 | This statement contains four continued line pieces. | |||
|
128 | Assembling these pieces into a single line would give:: | |||
|
129 | ||||
|
130 | if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[... | |||
|
131 | ||||
|
132 | This uses 0-indexed line numbers. *start* is (lineno, colno). | |||
|
133 | ||||
|
134 | Used to allow ``%magic`` and ``!system`` commands to be continued over | |||
|
135 | multiple lines. | |||
|
136 | """ | |||
|
137 | parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1] | |||
|
138 | return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline | |||
|
139 | + [parts[-1][:-1]]) # Strip newline from last line | |||
|
140 | ||||
|
141 | class TokenTransformBase: | |||
|
142 | """Base class for transformations which examine tokens. | |||
|
143 | ||||
|
144 | Special syntax should not be transformed when it occurs inside strings or | |||
|
145 | comments. This is hard to reliably avoid with regexes. The solution is to | |||
|
146 | tokenise the code as Python, and recognise the special syntax in the tokens. | |||
|
147 | ||||
|
148 | IPython's special syntax is not valid Python syntax, so tokenising may go | |||
|
149 | wrong after the special syntax starts. These classes therefore find and | |||
|
150 | transform *one* instance of special syntax at a time into regular Python | |||
|
151 | syntax. After each transformation, tokens are regenerated to find the next | |||
|
152 | piece of special syntax. | |||
|
153 | ||||
|
154 | Subclasses need to implement one class method (find) | |||
|
155 | and one regular method (transform). | |||
|
156 | ||||
|
157 | The priority attribute can select which transformation to apply if multiple | |||
|
158 | transformers match in the same place. Lower numbers have higher priority. | |||
|
159 | This allows "%magic?" to be turned into a help call rather than a magic call. | |||
|
160 | """ | |||
|
161 | # Lower numbers -> higher priority (for matches in the same location) | |||
|
162 | priority = 10 | |||
|
163 | ||||
|
164 | def sortby(self): | |||
|
165 | return self.start_line, self.start_col, self.priority | |||
|
166 | ||||
|
167 | def __init__(self, start): | |||
|
168 | self.start_line = start[0] - 1 # Shift from 1-index to 0-index | |||
|
169 | self.start_col = start[1] | |||
|
170 | ||||
|
171 | @classmethod | |||
|
172 | def find(cls, tokens_by_line): | |||
|
173 | """Find one instance of special syntax in the provided tokens. | |||
|
174 | ||||
|
175 | Tokens are grouped into logical lines for convenience, | |||
|
176 | so it is easy to e.g. look at the first token of each line. | |||
|
177 | *tokens_by_line* is a list of lists of tokenize.TokenInfo objects. | |||
|
178 | ||||
|
179 | This should return an instance of its class, pointing to the start | |||
|
180 | position it has found, or None if it found no match. | |||
|
181 | """ | |||
|
182 | raise NotImplementedError | |||
|
183 | ||||
|
184 | def transform(self, lines: List[str]): | |||
|
185 | """Transform one instance of special syntax found by ``find()`` | |||
|
186 | ||||
|
187 | Takes a list of strings representing physical lines, | |||
|
188 | returns a similar list of transformed lines. | |||
|
189 | """ | |||
|
190 | raise NotImplementedError | |||
|
191 | ||||
|
192 | class MagicAssign(TokenTransformBase): | |||
|
193 | """Transformer for assignments from magics (a = %foo)""" | |||
|
194 | @classmethod | |||
|
195 | def find(cls, tokens_by_line): | |||
|
196 | """Find the first magic assignment (a = %foo) in the cell. | |||
|
197 | """ | |||
|
198 | for line in tokens_by_line: | |||
|
199 | assign_ix = _find_assign_op(line) | |||
|
200 | if (assign_ix is not None) \ | |||
|
201 | and (len(line) >= assign_ix + 2) \ | |||
|
202 | and (line[assign_ix+1].string == '%') \ | |||
|
203 | and (line[assign_ix+2].type == tokenize.NAME): | |||
|
204 | return cls(line[assign_ix+1].start) | |||
|
205 | ||||
|
206 | def transform(self, lines: List[str]): | |||
|
207 | """Transform a magic assignment found by the ``find()`` classmethod. | |||
|
208 | """ | |||
|
209 | start_line, start_col = self.start_line, self.start_col | |||
|
210 | lhs = lines[start_line][:start_col] | |||
|
211 | end_line = find_end_of_continued_line(lines, start_line) | |||
|
212 | rhs = assemble_continued_line(lines, (start_line, start_col), end_line) | |||
|
213 | assert rhs.startswith('%'), rhs | |||
|
214 | magic_name, _, args = rhs[1:].partition(' ') | |||
|
215 | ||||
|
216 | lines_before = lines[:start_line] | |||
|
217 | call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args) | |||
|
218 | new_line = lhs + call + '\n' | |||
|
219 | lines_after = lines[end_line+1:] | |||
|
220 | ||||
|
221 | return lines_before + [new_line] + lines_after | |||
|
222 | ||||
|
223 | ||||
|
224 | class SystemAssign(TokenTransformBase): | |||
|
225 | """Transformer for assignments from system commands (a = !foo)""" | |||
|
226 | @classmethod | |||
|
227 | def find(cls, tokens_by_line): | |||
|
228 | """Find the first system assignment (a = !foo) in the cell. | |||
|
229 | """ | |||
|
230 | for line in tokens_by_line: | |||
|
231 | assign_ix = _find_assign_op(line) | |||
|
232 | if (assign_ix is not None) \ | |||
|
233 | and (len(line) >= assign_ix + 2) \ | |||
|
234 | and (line[assign_ix + 1].type == tokenize.ERRORTOKEN): | |||
|
235 | ix = assign_ix + 1 | |||
|
236 | ||||
|
237 | while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN: | |||
|
238 | if line[ix].string == '!': | |||
|
239 | return cls(line[ix].start) | |||
|
240 | elif not line[ix].string.isspace(): | |||
|
241 | break | |||
|
242 | ix += 1 | |||
|
243 | ||||
|
244 | def transform(self, lines: List[str]): | |||
|
245 | """Transform a system assignment found by the ``find()`` classmethod. | |||
|
246 | """ | |||
|
247 | start_line, start_col = self.start_line, self.start_col | |||
|
248 | ||||
|
249 | lhs = lines[start_line][:start_col] | |||
|
250 | end_line = find_end_of_continued_line(lines, start_line) | |||
|
251 | rhs = assemble_continued_line(lines, (start_line, start_col), end_line) | |||
|
252 | assert rhs.startswith('!'), rhs | |||
|
253 | cmd = rhs[1:] | |||
|
254 | ||||
|
255 | lines_before = lines[:start_line] | |||
|
256 | call = "get_ipython().getoutput({!r})".format(cmd) | |||
|
257 | new_line = lhs + call + '\n' | |||
|
258 | lines_after = lines[end_line + 1:] | |||
|
259 | ||||
|
260 | return lines_before + [new_line] + lines_after | |||
|
261 | ||||
|
262 | # The escape sequences that define the syntax transformations IPython will | |||
|
263 | # apply to user input. These can NOT be just changed here: many regular | |||
|
264 | # expressions and other parts of the code may use their hardcoded values, and | |||
|
265 | # for all intents and purposes they constitute the 'IPython syntax', so they | |||
|
266 | # should be considered fixed. | |||
|
267 | ||||
|
268 | ESC_SHELL = '!' # Send line to underlying system shell | |||
|
269 | ESC_SH_CAP = '!!' # Send line to system shell and capture output | |||
|
270 | ESC_HELP = '?' # Find information about object | |||
|
271 | ESC_HELP2 = '??' # Find extra-detailed information about object | |||
|
272 | ESC_MAGIC = '%' # Call magic function | |||
|
273 | ESC_MAGIC2 = '%%' # Call cell-magic function | |||
|
274 | ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call | |||
|
275 | ESC_QUOTE2 = ';' # Quote all args as a single string, call | |||
|
276 | ESC_PAREN = '/' # Call first argument with rest of line as arguments | |||
|
277 | ||||
|
278 | ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'} | |||
|
279 | ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately | |||
|
280 | ||||
|
281 | def _make_help_call(target, esc, next_input=None): | |||
|
282 | """Prepares a pinfo(2)/psearch call from a target name and the escape | |||
|
283 | (i.e. ? or ??)""" | |||
|
284 | method = 'pinfo2' if esc == '??' \ | |||
|
285 | else 'psearch' if '*' in target \ | |||
|
286 | else 'pinfo' | |||
|
287 | arg = " ".join([method, target]) | |||
|
288 | #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args) | |||
|
289 | t_magic_name, _, t_magic_arg_s = arg.partition(' ') | |||
|
290 | t_magic_name = t_magic_name.lstrip(ESC_MAGIC) | |||
|
291 | if next_input is None: | |||
|
292 | return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s) | |||
|
293 | else: | |||
|
294 | return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \ | |||
|
295 | (next_input, t_magic_name, t_magic_arg_s) | |||
|
296 | ||||
|
297 | def _tr_help(content): | |||
|
298 | """Translate lines escaped with: ? | |||
|
299 | ||||
|
300 | A naked help line should fire the intro help screen (shell.show_usage()) | |||
|
301 | """ | |||
|
302 | if not content: | |||
|
303 | return 'get_ipython().show_usage()' | |||
|
304 | ||||
|
305 | return _make_help_call(content, '?') | |||
|
306 | ||||
|
307 | def _tr_help2(content): | |||
|
308 | """Translate lines escaped with: ?? | |||
|
309 | ||||
|
310 | A naked help line should fire the intro help screen (shell.show_usage()) | |||
|
311 | """ | |||
|
312 | if not content: | |||
|
313 | return 'get_ipython().show_usage()' | |||
|
314 | ||||
|
315 | return _make_help_call(content, '??') | |||
|
316 | ||||
|
317 | def _tr_magic(content): | |||
|
318 | "Translate lines escaped with a percent sign: %" | |||
|
319 | name, _, args = content.partition(' ') | |||
|
320 | return 'get_ipython().run_line_magic(%r, %r)' % (name, args) | |||
|
321 | ||||
|
322 | def _tr_quote(content): | |||
|
323 | "Translate lines escaped with a comma: ," | |||
|
324 | name, _, args = content.partition(' ') | |||
|
325 | return '%s("%s")' % (name, '", "'.join(args.split()) ) | |||
|
326 | ||||
|
327 | def _tr_quote2(content): | |||
|
328 | "Translate lines escaped with a semicolon: ;" | |||
|
329 | name, _, args = content.partition(' ') | |||
|
330 | return '%s("%s")' % (name, args) | |||
|
331 | ||||
|
332 | def _tr_paren(content): | |||
|
333 | "Translate lines escaped with a slash: /" | |||
|
334 | name, _, args = content.partition(' ') | |||
|
335 | return '%s(%s)' % (name, ", ".join(args.split())) | |||
|
336 | ||||
|
337 | tr = { ESC_SHELL : 'get_ipython().system({!r})'.format, | |||
|
338 | ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format, | |||
|
339 | ESC_HELP : _tr_help, | |||
|
340 | ESC_HELP2 : _tr_help2, | |||
|
341 | ESC_MAGIC : _tr_magic, | |||
|
342 | ESC_QUOTE : _tr_quote, | |||
|
343 | ESC_QUOTE2 : _tr_quote2, | |||
|
344 | ESC_PAREN : _tr_paren } | |||
|
345 | ||||
|
346 | class EscapedCommand(TokenTransformBase): | |||
|
347 | """Transformer for escaped commands like %foo, !foo, or /foo""" | |||
|
348 | @classmethod | |||
|
349 | def find(cls, tokens_by_line): | |||
|
350 | """Find the first escaped command (%foo, !foo, etc.) in the cell. | |||
|
351 | """ | |||
|
352 | for line in tokens_by_line: | |||
|
353 | ix = 0 | |||
|
354 | while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}: | |||
|
355 | ix += 1 | |||
|
356 | if line[ix].string in ESCAPE_SINGLES: | |||
|
357 | return cls(line[ix].start) | |||
|
358 | ||||
|
359 | def transform(self, lines): | |||
|
360 | """Transform an escaped line found by the ``find()`` classmethod. | |||
|
361 | """ | |||
|
362 | start_line, start_col = self.start_line, self.start_col | |||
|
363 | ||||
|
364 | indent = lines[start_line][:start_col] | |||
|
365 | end_line = find_end_of_continued_line(lines, start_line) | |||
|
366 | line = assemble_continued_line(lines, (start_line, start_col), end_line) | |||
|
367 | ||||
|
368 | if line[:2] in ESCAPE_DOUBLES: | |||
|
369 | escape, content = line[:2], line[2:] | |||
|
370 | else: | |||
|
371 | escape, content = line[:1], line[1:] | |||
|
372 | call = tr[escape](content) | |||
|
373 | ||||
|
374 | lines_before = lines[:start_line] | |||
|
375 | new_line = indent + call + '\n' | |||
|
376 | lines_after = lines[end_line + 1:] | |||
|
377 | ||||
|
378 | return lines_before + [new_line] + lines_after | |||
|
379 | ||||
|
380 | _help_end_re = re.compile(r"""(%{0,2} | |||
|
381 | [a-zA-Z_*][\w*]* # Variable name | |||
|
382 | (\.[a-zA-Z_*][\w*]*)* # .etc.etc | |||
|
383 | ) | |||
|
384 | (\?\??)$ # ? or ?? | |||
|
385 | """, | |||
|
386 | re.VERBOSE) | |||
|
387 | ||||
|
388 | class HelpEnd(TokenTransformBase): | |||
|
389 | """Transformer for help syntax: obj? and obj??""" | |||
|
390 | # This needs to be higher priority (lower number) than EscapedCommand so | |||
|
391 | # that inspecting magics (%foo?) works. | |||
|
392 | priority = 5 | |||
|
393 | ||||
|
394 | def __init__(self, start, q_locn): | |||
|
395 | super().__init__(start) | |||
|
396 | self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed | |||
|
397 | self.q_col = q_locn[1] | |||
|
398 | ||||
|
399 | @classmethod | |||
|
400 | def find(cls, tokens_by_line): | |||
|
401 | """Find the first help command (foo?) in the cell. | |||
|
402 | """ | |||
|
403 | for line in tokens_by_line: | |||
|
404 | # Last token is NEWLINE; look at last but one | |||
|
405 | if len(line) > 2 and line[-2].string == '?': | |||
|
406 | # Find the first token that's not INDENT/DEDENT | |||
|
407 | ix = 0 | |||
|
408 | while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}: | |||
|
409 | ix += 1 | |||
|
410 | return cls(line[ix].start, line[-2].start) | |||
|
411 | ||||
|
412 | def transform(self, lines): | |||
|
413 | """Transform a help command found by the ``find()`` classmethod. | |||
|
414 | """ | |||
|
415 | piece = ''.join(lines[self.start_line:self.q_line+1]) | |||
|
416 | indent, content = piece[:self.start_col], piece[self.start_col:] | |||
|
417 | lines_before = lines[:self.start_line] | |||
|
418 | lines_after = lines[self.q_line + 1:] | |||
|
419 | ||||
|
420 | m = _help_end_re.search(content) | |||
|
421 | assert m is not None, content | |||
|
422 | target = m.group(1) | |||
|
423 | esc = m.group(3) | |||
|
424 | ||||
|
425 | # If we're mid-command, put it back on the next prompt for the user. | |||
|
426 | next_input = None | |||
|
427 | if (not lines_before) and (not lines_after) \ | |||
|
428 | and content.strip() != m.group(0): | |||
|
429 | next_input = content.rstrip('?\n') | |||
|
430 | ||||
|
431 | call = _make_help_call(target, esc, next_input=next_input) | |||
|
432 | new_line = indent + call + '\n' | |||
|
433 | ||||
|
434 | return lines_before + [new_line] + lines_after | |||
|
435 | ||||
|
436 | def make_tokens_by_line(lines): | |||
|
437 | """Tokenize a series of lines and group tokens by line. | |||
|
438 | ||||
|
439 | The tokens for a multiline Python string or expression are | |||
|
440 | grouped as one line. | |||
|
441 | """ | |||
|
442 | # NL tokens are used inside multiline expressions, but also after blank | |||
|
443 | # lines or comments. This is intentional - see https://bugs.python.org/issue17061 | |||
|
444 | # We want to group the former case together but split the latter, so we | |||
|
445 | # track parentheses level, similar to the internals of tokenize. | |||
|
446 | NEWLINE, NL = tokenize.NEWLINE, tokenize.NL | |||
|
447 | tokens_by_line = [[]] | |||
|
448 | parenlev = 0 | |||
|
449 | try: | |||
|
450 | for token in tokenize.generate_tokens(iter(lines).__next__): | |||
|
451 | tokens_by_line[-1].append(token) | |||
|
452 | if (token.type == NEWLINE) \ | |||
|
453 | or ((token.type == NL) and (parenlev <= 0)): | |||
|
454 | tokens_by_line.append([]) | |||
|
455 | elif token.string in {'(', '[', '{'}: | |||
|
456 | parenlev += 1 | |||
|
457 | elif token.string in {')', ']', '}'}: | |||
|
458 | if parenlev > 0: | |||
|
459 | parenlev -= 1 | |||
|
460 | except tokenize.TokenError: | |||
|
461 | # Input ended in a multiline string or expression. That's OK for us. | |||
|
462 | pass | |||
|
463 | ||||
|
464 | return tokens_by_line | |||
|
465 | ||||
|
466 | def show_linewise_tokens(s: str): | |||
|
467 | """For investigation and debugging""" | |||
|
468 | if not s.endswith('\n'): | |||
|
469 | s += '\n' | |||
|
470 | lines = s.splitlines(keepends=True) | |||
|
471 | for line in make_tokens_by_line(lines): | |||
|
472 | print("Line -------") | |||
|
473 | for tokinfo in line: | |||
|
474 | print(" ", tokinfo) | |||
|
475 | ||||
|
476 | # Arbitrary limit to prevent getting stuck in infinite loops | |||
|
477 | TRANSFORM_LOOP_LIMIT = 500 | |||
|
478 | ||||
|
479 | class TransformerManager: | |||
|
480 | """Applies various transformations to a cell or code block. | |||
|
481 | ||||
|
482 | The key methods for external use are ``transform_cell()`` | |||
|
483 | and ``check_complete()``. | |||
|
484 | """ | |||
|
485 | def __init__(self): | |||
|
486 | self.cleanup_transforms = [ | |||
|
487 | leading_indent, | |||
|
488 | classic_prompt, | |||
|
489 | ipython_prompt, | |||
|
490 | ] | |||
|
491 | self.line_transforms = [ | |||
|
492 | cell_magic, | |||
|
493 | ] | |||
|
494 | self.token_transformers = [ | |||
|
495 | MagicAssign, | |||
|
496 | SystemAssign, | |||
|
497 | EscapedCommand, | |||
|
498 | HelpEnd, | |||
|
499 | ] | |||
|
500 | ||||
|
501 | def do_one_token_transform(self, lines): | |||
|
502 | """Find and run the transform earliest in the code. | |||
|
503 | ||||
|
504 | Returns (changed, lines). | |||
|
505 | ||||
|
506 | This method is called repeatedly until changed is False, indicating | |||
|
507 | that all available transformations are complete. | |||
|
508 | ||||
|
509 | The tokens following IPython special syntax might not be valid, so | |||
|
510 | the transformed code is retokenised every time to identify the next | |||
|
511 | piece of special syntax. Hopefully long code cells are mostly valid | |||
|
512 | Python, not using lots of IPython special syntax, so this shouldn't be | |||
|
513 | a performance issue. | |||
|
514 | """ | |||
|
515 | tokens_by_line = make_tokens_by_line(lines) | |||
|
516 | candidates = [] | |||
|
517 | for transformer_cls in self.token_transformers: | |||
|
518 | transformer = transformer_cls.find(tokens_by_line) | |||
|
519 | if transformer: | |||
|
520 | candidates.append(transformer) | |||
|
521 | ||||
|
522 | if not candidates: | |||
|
523 | # Nothing to transform | |||
|
524 | return False, lines | |||
|
525 | ||||
|
526 | transformer = min(candidates, key=TokenTransformBase.sortby) | |||
|
527 | return True, transformer.transform(lines) | |||
|
528 | ||||
|
529 | def do_token_transforms(self, lines): | |||
|
530 | for _ in range(TRANSFORM_LOOP_LIMIT): | |||
|
531 | changed, lines = self.do_one_token_transform(lines) | |||
|
532 | if not changed: | |||
|
533 | return lines | |||
|
534 | ||||
|
535 | raise RuntimeError("Input transformation still changing after " | |||
|
536 | "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT) | |||
|
537 | ||||
|
538 | def transform_cell(self, cell: str) -> str: | |||
|
539 | """Transforms a cell of input code""" | |||
|
540 | if not cell.endswith('\n'): | |||
|
541 | cell += '\n' # Ensure the cell has a trailing newline | |||
|
542 | lines = cell.splitlines(keepends=True) | |||
|
543 | for transform in self.cleanup_transforms + self.line_transforms: | |||
|
544 | #print(transform, lines) | |||
|
545 | lines = transform(lines) | |||
|
546 | ||||
|
547 | lines = self.do_token_transforms(lines) | |||
|
548 | return ''.join(lines) | |||
|
549 | ||||
|
550 | def check_complete(self, cell: str): | |||
|
551 | """Return whether a block of code is ready to execute, or should be continued | |||
|
552 | ||||
|
553 | Parameters | |||
|
554 | ---------- | |||
|
555 | source : string | |||
|
556 | Python input code, which can be multiline. | |||
|
557 | ||||
|
558 | Returns | |||
|
559 | ------- | |||
|
560 | status : str | |||
|
561 | One of 'complete', 'incomplete', or 'invalid' if source is not a | |||
|
562 | prefix of valid code. | |||
|
563 | indent_spaces : int or None | |||
|
564 | The number of spaces by which to indent the next line of code. If | |||
|
565 | status is not 'incomplete', this is None. | |||
|
566 | """ | |||
|
567 | if not cell.endswith('\n'): | |||
|
568 | cell += '\n' # Ensure the cell has a trailing newline | |||
|
569 | lines = cell.splitlines(keepends=True) | |||
|
570 | if lines[-1][:-1].endswith('\\'): | |||
|
571 | # Explicit backslash continuation | |||
|
572 | return 'incomplete', find_last_indent(lines) | |||
|
573 | ||||
|
574 | try: | |||
|
575 | for transform in self.cleanup_transforms: | |||
|
576 | lines = transform(lines) | |||
|
577 | except SyntaxError: | |||
|
578 | return 'invalid', None | |||
|
579 | ||||
|
580 | if lines[0].startswith('%%'): | |||
|
581 | # Special case for cell magics - completion marked by blank line | |||
|
582 | if lines[-1].strip(): | |||
|
583 | return 'incomplete', find_last_indent(lines) | |||
|
584 | else: | |||
|
585 | return 'complete', None | |||
|
586 | ||||
|
587 | try: | |||
|
588 | for transform in self.line_transforms: | |||
|
589 | lines = transform(lines) | |||
|
590 | lines = self.do_token_transforms(lines) | |||
|
591 | except SyntaxError: | |||
|
592 | return 'invalid', None | |||
|
593 | ||||
|
594 | tokens_by_line = make_tokens_by_line(lines) | |||
|
595 | if tokens_by_line[-1][-1].type != tokenize.ENDMARKER: | |||
|
596 | # We're in a multiline string or expression | |||
|
597 | return 'incomplete', find_last_indent(lines) | |||
|
598 | ||||
|
599 | # Find the last token on the previous line that's not NEWLINE or COMMENT | |||
|
600 | toks_last_line = tokens_by_line[-2] | |||
|
601 | ix = len(toks_last_line) - 1 | |||
|
602 | while ix >= 0 and toks_last_line[ix].type in {tokenize.NEWLINE, | |||
|
603 | tokenize.COMMENT}: | |||
|
604 | ix -= 1 | |||
|
605 | ||||
|
606 | if toks_last_line[ix].string == ':': | |||
|
607 | # The last line starts a block (e.g. 'if foo:') | |||
|
608 | ix = 0 | |||
|
609 | while toks_last_line[ix].type in {tokenize.INDENT, tokenize.DEDENT}: | |||
|
610 | ix += 1 | |||
|
611 | indent = toks_last_line[ix].start[1] | |||
|
612 | return 'incomplete', indent + 4 | |||
|
613 | ||||
|
614 | # If there's a blank line at the end, assume we're ready to execute. | |||
|
615 | if not lines[-1].strip(): | |||
|
616 | return 'complete', None | |||
|
617 | ||||
|
618 | # At this point, our checks think the code is complete (or invalid). | |||
|
619 | # We'll use codeop.compile_command to check this with the real parser. | |||
|
620 | ||||
|
621 | try: | |||
|
622 | with warnings.catch_warnings(): | |||
|
623 | warnings.simplefilter('error', SyntaxWarning) | |||
|
624 | res = compile_command(''.join(lines), symbol='exec') | |||
|
625 | except (SyntaxError, OverflowError, ValueError, TypeError, | |||
|
626 | MemoryError, SyntaxWarning): | |||
|
627 | return 'invalid', None | |||
|
628 | else: | |||
|
629 | if res is None: | |||
|
630 | return 'incomplete', find_last_indent(lines) | |||
|
631 | return 'complete', None | |||
|
632 | ||||
|
633 | ||||
|
634 | def find_last_indent(lines): | |||
|
635 | m = _indent_re.match(lines[-1]) | |||
|
636 | if not m: | |||
|
637 | return 0 | |||
|
638 | return len(m.group(0).replace('\t', ' '*4)) |
@@ -0,0 +1,195 b'' | |||||
|
1 | """Tests for the token-based transformers in IPython.core.inputtransformer2 | |||
|
2 | ||||
|
3 | Line-based transformers are the simpler ones; token-based transformers are | |||
|
4 | more complex. See test_inputtransformer2_line for tests for line-based | |||
|
5 | transformations. | |||
|
6 | """ | |||
|
7 | import nose.tools as nt | |||
|
8 | ||||
|
9 | from IPython.core import inputtransformer2 as ipt2 | |||
|
10 | from IPython.core.inputtransformer2 import make_tokens_by_line | |||
|
11 | ||||
|
12 | MULTILINE_MAGIC = ("""\ | |||
|
13 | a = f() | |||
|
14 | %foo \\ | |||
|
15 | bar | |||
|
16 | g() | |||
|
17 | """.splitlines(keepends=True), (2, 0), """\ | |||
|
18 | a = f() | |||
|
19 | get_ipython().run_line_magic('foo', ' bar') | |||
|
20 | g() | |||
|
21 | """.splitlines(keepends=True)) | |||
|
22 | ||||
|
23 | INDENTED_MAGIC = ("""\ | |||
|
24 | for a in range(5): | |||
|
25 | %ls | |||
|
26 | """.splitlines(keepends=True), (2, 4), """\ | |||
|
27 | for a in range(5): | |||
|
28 | get_ipython().run_line_magic('ls', '') | |||
|
29 | """.splitlines(keepends=True)) | |||
|
30 | ||||
|
31 | MULTILINE_MAGIC_ASSIGN = ("""\ | |||
|
32 | a = f() | |||
|
33 | b = %foo \\ | |||
|
34 | bar | |||
|
35 | g() | |||
|
36 | """.splitlines(keepends=True), (2, 4), """\ | |||
|
37 | a = f() | |||
|
38 | b = get_ipython().run_line_magic('foo', ' bar') | |||
|
39 | g() | |||
|
40 | """.splitlines(keepends=True)) | |||
|
41 | ||||
|
42 | MULTILINE_SYSTEM_ASSIGN = ("""\ | |||
|
43 | a = f() | |||
|
44 | b = !foo \\ | |||
|
45 | bar | |||
|
46 | g() | |||
|
47 | """.splitlines(keepends=True), (2, 4), """\ | |||
|
48 | a = f() | |||
|
49 | b = get_ipython().getoutput('foo bar') | |||
|
50 | g() | |||
|
51 | """.splitlines(keepends=True)) | |||
|
52 | ||||
|
53 | AUTOCALL_QUOTE = ( | |||
|
54 | [",f 1 2 3\n"], (1, 0), | |||
|
55 | ['f("1", "2", "3")\n'] | |||
|
56 | ) | |||
|
57 | ||||
|
58 | AUTOCALL_QUOTE2 = ( | |||
|
59 | [";f 1 2 3\n"], (1, 0), | |||
|
60 | ['f("1 2 3")\n'] | |||
|
61 | ) | |||
|
62 | ||||
|
63 | AUTOCALL_PAREN = ( | |||
|
64 | ["/f 1 2 3\n"], (1, 0), | |||
|
65 | ['f(1, 2, 3)\n'] | |||
|
66 | ) | |||
|
67 | ||||
|
68 | SIMPLE_HELP = ( | |||
|
69 | ["foo?\n"], (1, 0), | |||
|
70 | ["get_ipython().run_line_magic('pinfo', 'foo')\n"] | |||
|
71 | ) | |||
|
72 | ||||
|
73 | DETAILED_HELP = ( | |||
|
74 | ["foo??\n"], (1, 0), | |||
|
75 | ["get_ipython().run_line_magic('pinfo2', 'foo')\n"] | |||
|
76 | ) | |||
|
77 | ||||
|
78 | MAGIC_HELP = ( | |||
|
79 | ["%foo?\n"], (1, 0), | |||
|
80 | ["get_ipython().run_line_magic('pinfo', '%foo')\n"] | |||
|
81 | ) | |||
|
82 | ||||
|
83 | HELP_IN_EXPR = ( | |||
|
84 | ["a = b + c?\n"], (1, 0), | |||
|
85 | ["get_ipython().set_next_input('a = b + c');" | |||
|
86 | "get_ipython().run_line_magic('pinfo', 'c')\n"] | |||
|
87 | ) | |||
|
88 | ||||
|
89 | HELP_CONTINUED_LINE = ("""\ | |||
|
90 | a = \\ | |||
|
91 | zip? | |||
|
92 | """.splitlines(keepends=True), (1, 0), | |||
|
93 | [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"] | |||
|
94 | ) | |||
|
95 | ||||
|
96 | HELP_MULTILINE = ("""\ | |||
|
97 | (a, | |||
|
98 | b) = zip? | |||
|
99 | """.splitlines(keepends=True), (1, 0), | |||
|
100 | [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"] | |||
|
101 | ) | |||
|
102 | ||||
|
103 | def check_find(transformer, case, match=True): | |||
|
104 | sample, expected_start, _ = case | |||
|
105 | tbl = make_tokens_by_line(sample) | |||
|
106 | res = transformer.find(tbl) | |||
|
107 | if match: | |||
|
108 | # start_line is stored 0-indexed, expected values are 1-indexed | |||
|
109 | nt.assert_equal((res.start_line+1, res.start_col), expected_start) | |||
|
110 | return res | |||
|
111 | else: | |||
|
112 | nt.assert_is(res, None) | |||
|
113 | ||||
|
114 | def check_transform(transformer_cls, case): | |||
|
115 | lines, start, expected = case | |||
|
116 | transformer = transformer_cls(start) | |||
|
117 | nt.assert_equal(transformer.transform(lines), expected) | |||
|
118 | ||||
|
119 | def test_continued_line(): | |||
|
120 | lines = MULTILINE_MAGIC_ASSIGN[0] | |||
|
121 | nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2) | |||
|
122 | ||||
|
123 | nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar") | |||
|
124 | ||||
|
125 | def test_find_assign_magic(): | |||
|
126 | check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN) | |||
|
127 | check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False) | |||
|
128 | ||||
|
129 | def test_transform_assign_magic(): | |||
|
130 | check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN) | |||
|
131 | ||||
|
132 | def test_find_assign_system(): | |||
|
133 | check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN) | |||
|
134 | check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None)) | |||
|
135 | check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None)) | |||
|
136 | check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False) | |||
|
137 | ||||
|
138 | def test_transform_assign_system(): | |||
|
139 | check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN) | |||
|
140 | ||||
|
141 | def test_find_magic_escape(): | |||
|
142 | check_find(ipt2.EscapedCommand, MULTILINE_MAGIC) | |||
|
143 | check_find(ipt2.EscapedCommand, INDENTED_MAGIC) | |||
|
144 | check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False) | |||
|
145 | ||||
|
146 | def test_transform_magic_escape(): | |||
|
147 | check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC) | |||
|
148 | check_transform(ipt2.EscapedCommand, INDENTED_MAGIC) | |||
|
149 | ||||
|
150 | def test_find_autocalls(): | |||
|
151 | for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]: | |||
|
152 | print("Testing %r" % case[0]) | |||
|
153 | check_find(ipt2.EscapedCommand, case) | |||
|
154 | ||||
|
155 | def test_transform_autocall(): | |||
|
156 | for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]: | |||
|
157 | print("Testing %r" % case[0]) | |||
|
158 | check_transform(ipt2.EscapedCommand, case) | |||
|
159 | ||||
|
160 | def test_find_help(): | |||
|
161 | for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]: | |||
|
162 | check_find(ipt2.HelpEnd, case) | |||
|
163 | ||||
|
164 | tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE) | |||
|
165 | nt.assert_equal(tf.q_line, 1) | |||
|
166 | nt.assert_equal(tf.q_col, 3) | |||
|
167 | ||||
|
168 | tf = check_find(ipt2.HelpEnd, HELP_MULTILINE) | |||
|
169 | nt.assert_equal(tf.q_line, 1) | |||
|
170 | nt.assert_equal(tf.q_col, 8) | |||
|
171 | ||||
|
172 | # ? in a comment does not trigger help | |||
|
173 | check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False) | |||
|
174 | # Nor in a string | |||
|
175 | check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False) | |||
|
176 | ||||
|
177 | def test_transform_help(): | |||
|
178 | tf = ipt2.HelpEnd((1, 0), (1, 9)) | |||
|
179 | nt.assert_equal(tf.transform(HELP_IN_EXPR[0]), HELP_IN_EXPR[2]) | |||
|
180 | ||||
|
181 | tf = ipt2.HelpEnd((1, 0), (2, 3)) | |||
|
182 | nt.assert_equal(tf.transform(HELP_CONTINUED_LINE[0]), HELP_CONTINUED_LINE[2]) | |||
|
183 | ||||
|
184 | tf = ipt2.HelpEnd((1, 0), (2, 8)) | |||
|
185 | nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2]) | |||
|
186 | ||||
|
187 | def test_check_complete(): | |||
|
188 | cc = ipt2.TransformerManager().check_complete | |||
|
189 | nt.assert_equal(cc("a = 1"), ('complete', None)) | |||
|
190 | nt.assert_equal(cc("for a in range(5):"), ('incomplete', 4)) | |||
|
191 | nt.assert_equal(cc("raise = 2"), ('invalid', None)) | |||
|
192 | nt.assert_equal(cc("a = [1,\n2,"), ('incomplete', 0)) | |||
|
193 | nt.assert_equal(cc("a = '''\n hi"), ('incomplete', 3)) | |||
|
194 | nt.assert_equal(cc("def a():\n x=1\n global x"), ('invalid', None)) | |||
|
195 | nt.assert_equal(cc("a \\ "), ('invalid', None)) # Nothing allowed after backslash |
@@ -0,0 +1,88 b'' | |||||
|
1 | """Tests for the line-based transformers in IPython.core.inputtransformer2 | |||
|
2 | ||||
|
3 | Line-based transformers are the simpler ones; token-based transformers are | |||
|
4 | more complex. See test_inputtransformer2 for tests for token-based transformers. | |||
|
5 | """ | |||
|
6 | import nose.tools as nt | |||
|
7 | ||||
|
8 | from IPython.core import inputtransformer2 as ipt2 | |||
|
9 | ||||
|
10 | CELL_MAGIC = ("""\ | |||
|
11 | %%foo arg | |||
|
12 | body 1 | |||
|
13 | body 2 | |||
|
14 | """, """\ | |||
|
15 | get_ipython().run_cell_magic('foo', 'arg', 'body 1\\nbody 2\\n') | |||
|
16 | """) | |||
|
17 | ||||
|
18 | def test_cell_magic(): | |||
|
19 | for sample, expected in [CELL_MAGIC]: | |||
|
20 | nt.assert_equal(ipt2.cell_magic(sample.splitlines(keepends=True)), | |||
|
21 | expected.splitlines(keepends=True)) | |||
|
22 | ||||
|
23 | CLASSIC_PROMPT = ("""\ | |||
|
24 | >>> for a in range(5): | |||
|
25 | ... print(a) | |||
|
26 | """, """\ | |||
|
27 | for a in range(5): | |||
|
28 | print(a) | |||
|
29 | """) | |||
|
30 | ||||
|
31 | CLASSIC_PROMPT_L2 = ("""\ | |||
|
32 | for a in range(5): | |||
|
33 | ... print(a) | |||
|
34 | ... print(a ** 2) | |||
|
35 | """, """\ | |||
|
36 | for a in range(5): | |||
|
37 | print(a) | |||
|
38 | print(a ** 2) | |||
|
39 | """) | |||
|
40 | ||||
|
41 | def test_classic_prompt(): | |||
|
42 | for sample, expected in [CLASSIC_PROMPT, CLASSIC_PROMPT_L2]: | |||
|
43 | nt.assert_equal(ipt2.classic_prompt(sample.splitlines(keepends=True)), | |||
|
44 | expected.splitlines(keepends=True)) | |||
|
45 | ||||
|
46 | IPYTHON_PROMPT = ("""\ | |||
|
47 | In [1]: for a in range(5): | |||
|
48 | ...: print(a) | |||
|
49 | """, """\ | |||
|
50 | for a in range(5): | |||
|
51 | print(a) | |||
|
52 | """) | |||
|
53 | ||||
|
54 | IPYTHON_PROMPT_L2 = ("""\ | |||
|
55 | for a in range(5): | |||
|
56 | ...: print(a) | |||
|
57 | ...: print(a ** 2) | |||
|
58 | """, """\ | |||
|
59 | for a in range(5): | |||
|
60 | print(a) | |||
|
61 | print(a ** 2) | |||
|
62 | """) | |||
|
63 | ||||
|
64 | def test_ipython_prompt(): | |||
|
65 | for sample, expected in [IPYTHON_PROMPT, IPYTHON_PROMPT_L2]: | |||
|
66 | nt.assert_equal(ipt2.ipython_prompt(sample.splitlines(keepends=True)), | |||
|
67 | expected.splitlines(keepends=True)) | |||
|
68 | ||||
|
69 | INDENT_SPACES = ("""\ | |||
|
70 | if True: | |||
|
71 | a = 3 | |||
|
72 | """, """\ | |||
|
73 | if True: | |||
|
74 | a = 3 | |||
|
75 | """) | |||
|
76 | ||||
|
77 | INDENT_TABS = ("""\ | |||
|
78 | \tif True: | |||
|
79 | \t\tb = 4 | |||
|
80 | """, """\ | |||
|
81 | if True: | |||
|
82 | \tb = 4 | |||
|
83 | """) | |||
|
84 | ||||
|
85 | def test_leading_indent(): | |||
|
86 | for sample, expected in [INDENT_SPACES, INDENT_TABS]: | |||
|
87 | nt.assert_equal(ipt2.leading_indent(sample.splitlines(keepends=True)), | |||
|
88 | expected.splitlines(keepends=True)) |
@@ -0,0 +1,3 b'' | |||||
|
1 | * The API for transforming input before it is parsed as Python code has been | |||
|
2 | completely redesigned, and any custom input transformations will need to be | |||
|
3 | rewritten. See :doc:`/config/inputtransforms` for details of the new API. |
@@ -131,7 +131,7 b' from types import SimpleNamespace' | |||||
131 |
|
131 | |||
132 | from traitlets.config.configurable import Configurable |
|
132 | from traitlets.config.configurable import Configurable | |
133 | from IPython.core.error import TryNext |
|
133 | from IPython.core.error import TryNext | |
134 |
from IPython.core.input |
|
134 | from IPython.core.inputtransformer2 import ESC_MAGIC | |
135 | from IPython.core.latex_symbols import latex_symbols, reverse_latex_symbol |
|
135 | from IPython.core.latex_symbols import latex_symbols, reverse_latex_symbol | |
136 | from IPython.core.oinspect import InspectColors |
|
136 | from IPython.core.oinspect import InspectColors | |
137 | from IPython.utils import generics |
|
137 | from IPython.utils import generics |
@@ -1,4 +1,6 b'' | |||||
1 | """Input handling and transformation machinery. |
|
1 | """DEPRECATED: Input handling and transformation machinery. | |
|
2 | ||||
|
3 | This module was deprecated in IPython 7.0, in favour of inputtransformer2. | |||
2 |
|
4 | |||
3 | The first class in this module, :class:`InputSplitter`, is designed to tell when |
|
5 | The first class in this module, :class:`InputSplitter`, is designed to tell when | |
4 | input from a line-oriented frontend is complete and should be executed, and when |
|
6 | input from a line-oriented frontend is complete and should be executed, and when | |
@@ -14,6 +16,11 b' and stores the results.' | |||||
14 | For more details, see the class docstrings below. |
|
16 | For more details, see the class docstrings below. | |
15 | """ |
|
17 | """ | |
16 |
|
18 | |||
|
19 | from warnings import warn | |||
|
20 | ||||
|
21 | warn('IPython.core.inputsplitter is deprecated since IPython 7 in favor of `IPython.core.inputtransformer2`', | |||
|
22 | DeprecationWarning) | |||
|
23 | ||||
17 | # Copyright (c) IPython Development Team. |
|
24 | # Copyright (c) IPython Development Team. | |
18 | # Distributed under the terms of the Modified BSD License. |
|
25 | # Distributed under the terms of the Modified BSD License. | |
19 | import ast |
|
26 | import ast |
@@ -1,4 +1,6 b'' | |||||
1 | """Input transformer classes to support IPython special syntax. |
|
1 | """DEPRECATED: Input transformer classes to support IPython special syntax. | |
|
2 | ||||
|
3 | This module was deprecated in IPython 7.0, in favour of inputtransformer2. | |||
2 |
|
4 | |||
3 | This includes the machinery to recognise and transform ``%magic`` commands, |
|
5 | This includes the machinery to recognise and transform ``%magic`` commands, | |
4 | ``!system`` commands, ``help?`` querying, prompt stripping, and so forth. |
|
6 | ``!system`` commands, ``help?`` querying, prompt stripping, and so forth. | |
@@ -6,11 +8,11 b' This includes the machinery to recognise and transform ``%magic`` commands,' | |||||
6 | import abc |
|
8 | import abc | |
7 | import functools |
|
9 | import functools | |
8 | import re |
|
10 | import re | |
|
11 | import tokenize | |||
|
12 | from tokenize import generate_tokens, untokenize, TokenError | |||
9 | from io import StringIO |
|
13 | from io import StringIO | |
10 |
|
14 | |||
11 | from IPython.core.splitinput import LineInfo |
|
15 | from IPython.core.splitinput import LineInfo | |
12 | from IPython.utils import tokenize2 |
|
|||
13 | from IPython.utils.tokenize2 import generate_tokens, untokenize, TokenError |
|
|||
14 |
|
16 | |||
15 | #----------------------------------------------------------------------------- |
|
17 | #----------------------------------------------------------------------------- | |
16 | # Globals |
|
18 | # Globals | |
@@ -138,10 +140,10 b' class TokenInputTransformer(InputTransformer):' | |||||
138 | for intok in self.tokenizer: |
|
140 | for intok in self.tokenizer: | |
139 | tokens.append(intok) |
|
141 | tokens.append(intok) | |
140 | t = intok[0] |
|
142 | t = intok[0] | |
141 |
if t == tokenize |
|
143 | if t == tokenize.NEWLINE or (stop_at_NL and t == tokenize.NL): | |
142 | # 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 | |
143 | break |
|
145 | break | |
144 |
elif t == tokenize |
|
146 | elif t == tokenize.ERRORTOKEN: | |
145 | stop_at_NL = True |
|
147 | stop_at_NL = True | |
146 | except TokenError: |
|
148 | except TokenError: | |
147 | # Multi-line statement - stop and try again with the next line |
|
149 | # Multi-line statement - stop and try again with the next line | |
@@ -317,7 +319,7 b' def has_comment(src):' | |||||
317 | comment : bool |
|
319 | comment : bool | |
318 | True if source has a comment. |
|
320 | True if source has a comment. | |
319 | """ |
|
321 | """ | |
320 |
return (tokenize |
|
322 | return (tokenize.COMMENT in _line_tokens(src)) | |
321 |
|
323 | |||
322 | def ends_in_comment_or_string(src): |
|
324 | def ends_in_comment_or_string(src): | |
323 | """Indicates whether or not an input line ends in a comment or within |
|
325 | """Indicates whether or not an input line ends in a comment or within | |
@@ -334,7 +336,7 b' def ends_in_comment_or_string(src):' | |||||
334 | True if source ends in a comment or multiline string. |
|
336 | True if source ends in a comment or multiline string. | |
335 | """ |
|
337 | """ | |
336 | toktypes = _line_tokens(src) |
|
338 | toktypes = _line_tokens(src) | |
337 |
return (tokenize |
|
339 | return (tokenize.COMMENT in toktypes) or (_MULTILINE_STRING in toktypes) | |
338 |
|
340 | |||
339 |
|
341 | |||
340 | @StatelessInputTransformer.wrap |
|
342 | @StatelessInputTransformer.wrap |
@@ -48,7 +48,7 b' from IPython.core.error import InputRejected, UsageError' | |||||
48 | from IPython.core.extensions import ExtensionManager |
|
48 | from IPython.core.extensions import ExtensionManager | |
49 | from IPython.core.formatters import DisplayFormatter |
|
49 | from IPython.core.formatters import DisplayFormatter | |
50 | from IPython.core.history import HistoryManager |
|
50 | from IPython.core.history import HistoryManager | |
51 |
from IPython.core.input |
|
51 | from IPython.core.inputtransformer2 import ESC_MAGIC, ESC_MAGIC2 | |
52 | from IPython.core.logger import Logger |
|
52 | from IPython.core.logger import Logger | |
53 | from IPython.core.macro import Macro |
|
53 | from IPython.core.macro import Macro | |
54 | from IPython.core.payload import PayloadManager |
|
54 | from IPython.core.payload import PayloadManager | |
@@ -333,15 +333,31 b' class InteractiveShell(SingletonConfigurable):' | |||||
333 | filename = Unicode("<ipython console>") |
|
333 | filename = Unicode("<ipython console>") | |
334 | ipython_dir= Unicode('').tag(config=True) # Set to get_ipython_dir() in __init__ |
|
334 | ipython_dir= Unicode('').tag(config=True) # Set to get_ipython_dir() in __init__ | |
335 |
|
335 | |||
336 | # Input splitter, to transform input line by line and detect when a block |
|
336 | # Used to transform cells before running them, and check whether code is complete | |
337 | # is ready to be executed. |
|
337 | input_transformer_manager = Instance('IPython.core.inputtransformer2.TransformerManager', | |
338 | input_splitter = Instance('IPython.core.inputsplitter.IPythonInputSplitter', |
|
338 | ()) | |
339 | (), {'line_input_checker': True}) |
|
|||
340 |
|
339 | |||
341 | # This InputSplitter instance is used to transform completed cells before |
|
340 | @property | |
342 | # running them. It allows cell magics to contain blank lines. |
|
341 | def input_transformers_cleanup(self): | |
343 | input_transformer_manager = Instance('IPython.core.inputsplitter.IPythonInputSplitter', |
|
342 | return self.input_transformer_manager.cleanup_transforms | |
344 | (), {'line_input_checker': False}) |
|
343 | ||
|
344 | input_transformers_post = List([], | |||
|
345 | help="A list of string input transformers, to be applied after IPython's " | |||
|
346 | "own input transformations." | |||
|
347 | ) | |||
|
348 | ||||
|
349 | @property | |||
|
350 | def input_splitter(self): | |||
|
351 | """Make this available for backward compatibility (pre-7.0 release) with existing code. | |||
|
352 | ||||
|
353 | For example, ipykernel ipykernel currently uses | |||
|
354 | `shell.input_splitter.check_complete` | |||
|
355 | """ | |||
|
356 | from warnings import warn | |||
|
357 | warn("`input_splitter` is deprecated since IPython 7.0, prefer `input_transformer_manager`.", | |||
|
358 | DeprecationWarning, stacklevel=2 | |||
|
359 | ) | |||
|
360 | return self.input_transformer_manager | |||
345 |
|
361 | |||
346 | logstart = Bool(False, help= |
|
362 | logstart = Bool(False, help= | |
347 | """ |
|
363 | """ | |
@@ -2656,6 +2672,7 b' class InteractiveShell(SingletonConfigurable):' | |||||
2656 | ------- |
|
2672 | ------- | |
2657 | result : :class:`ExecutionResult` |
|
2673 | result : :class:`ExecutionResult` | |
2658 | """ |
|
2674 | """ | |
|
2675 | result = None | |||
2659 | try: |
|
2676 | try: | |
2660 | result = self._run_cell( |
|
2677 | result = self._run_cell( | |
2661 | raw_cell, store_history, silent, shell_futures) |
|
2678 | raw_cell, store_history, silent, shell_futures) | |
@@ -2713,21 +2730,10 b' class InteractiveShell(SingletonConfigurable):' | |||||
2713 | preprocessing_exc_tuple = None |
|
2730 | preprocessing_exc_tuple = None | |
2714 | try: |
|
2731 | try: | |
2715 | # Static input transformations |
|
2732 | # Static input transformations | |
2716 |
cell = self. |
|
2733 | cell = self.transform_cell(raw_cell) | |
2717 |
except |
|
2734 | except Exception: | |
2718 | preprocessing_exc_tuple = sys.exc_info() |
|
2735 | preprocessing_exc_tuple = sys.exc_info() | |
2719 | cell = raw_cell # cell has to exist so it can be stored/logged |
|
2736 | cell = raw_cell # cell has to exist so it can be stored/logged | |
2720 | else: |
|
|||
2721 | if len(cell.splitlines()) == 1: |
|
|||
2722 | # Dynamic transformations - only applied for single line commands |
|
|||
2723 | with self.builtin_trap: |
|
|||
2724 | try: |
|
|||
2725 | # use prefilter_lines to handle trailing newlines |
|
|||
2726 | # restore trailing newline for ast.parse |
|
|||
2727 | cell = self.prefilter_manager.prefilter_lines(cell) + '\n' |
|
|||
2728 | except Exception: |
|
|||
2729 | # don't allow prefilter errors to crash IPython |
|
|||
2730 | preprocessing_exc_tuple = sys.exc_info() |
|
|||
2731 |
|
2737 | |||
2732 | # Store raw and processed history |
|
2738 | # Store raw and processed history | |
2733 | if store_history: |
|
2739 | if store_history: | |
@@ -2798,6 +2804,36 b' class InteractiveShell(SingletonConfigurable):' | |||||
2798 | self.execution_count += 1 |
|
2804 | self.execution_count += 1 | |
2799 |
|
2805 | |||
2800 | return result |
|
2806 | return result | |
|
2807 | ||||
|
2808 | def transform_cell(self, raw_cell): | |||
|
2809 | """Transform an input cell before parsing it. | |||
|
2810 | ||||
|
2811 | Static transformations, implemented in IPython.core.inputtransformer2, | |||
|
2812 | deal with things like ``%magic`` and ``!system`` commands. | |||
|
2813 | These run on all input. | |||
|
2814 | Dynamic transformations, for things like unescaped magics and the exit | |||
|
2815 | autocall, depend on the state of the interpreter. | |||
|
2816 | These only apply to single line inputs. | |||
|
2817 | ||||
|
2818 | These string-based transformations are followed by AST transformations; | |||
|
2819 | see :meth:`transform_ast`. | |||
|
2820 | """ | |||
|
2821 | # Static input transformations | |||
|
2822 | cell = self.input_transformer_manager.transform_cell(raw_cell) | |||
|
2823 | ||||
|
2824 | if len(cell.splitlines()) == 1: | |||
|
2825 | # Dynamic transformations - only applied for single line commands | |||
|
2826 | with self.builtin_trap: | |||
|
2827 | # use prefilter_lines to handle trailing newlines | |||
|
2828 | # restore trailing newline for ast.parse | |||
|
2829 | cell = self.prefilter_manager.prefilter_lines(cell) + '\n' | |||
|
2830 | ||||
|
2831 | lines = cell.splitlines(keepends=True) | |||
|
2832 | for transform in self.input_transformers_post: | |||
|
2833 | lines = transform(lines) | |||
|
2834 | cell = ''.join(lines) | |||
|
2835 | ||||
|
2836 | return cell | |||
2801 |
|
2837 | |||
2802 | def transform_ast(self, node): |
|
2838 | def transform_ast(self, node): | |
2803 | """Apply the AST transformations from self.ast_transformers |
|
2839 | """Apply the AST transformations from self.ast_transformers | |
@@ -2999,7 +3035,7 b' class InteractiveShell(SingletonConfigurable):' | |||||
2999 | When status is 'incomplete', this is some whitespace to insert on |
|
3035 | When status is 'incomplete', this is some whitespace to insert on | |
3000 | the next line of the prompt. |
|
3036 | the next line of the prompt. | |
3001 | """ |
|
3037 | """ | |
3002 |
status, nspaces = self.input_ |
|
3038 | status, nspaces = self.input_transformer_manager.check_complete(code) | |
3003 | return status, ' ' * (nspaces or 0) |
|
3039 | return status, ' ' * (nspaces or 0) | |
3004 |
|
3040 | |||
3005 | #------------------------------------------------------------------------- |
|
3041 | #------------------------------------------------------------------------- |
@@ -19,7 +19,7 b' from getopt import getopt, GetoptError' | |||||
19 | from traitlets.config.configurable import Configurable |
|
19 | from traitlets.config.configurable import Configurable | |
20 | from IPython.core import oinspect |
|
20 | from IPython.core import oinspect | |
21 | from IPython.core.error import UsageError |
|
21 | from IPython.core.error import UsageError | |
22 |
from IPython.core.input |
|
22 | from IPython.core.inputtransformer2 import ESC_MAGIC, ESC_MAGIC2 | |
23 | from decorator import decorator |
|
23 | from decorator import decorator | |
24 | from IPython.utils.ipstruct import Struct |
|
24 | from IPython.utils.ipstruct import Struct | |
25 | from IPython.utils.process import arg_split |
|
25 | from IPython.utils.process import arg_split |
@@ -297,7 +297,7 b' python-profiler package from non-free.""")' | |||||
297 | list_all=True, posix=False) |
|
297 | list_all=True, posix=False) | |
298 | if cell is not None: |
|
298 | if cell is not None: | |
299 | arg_str += '\n' + cell |
|
299 | arg_str += '\n' + cell | |
300 |
arg_str = self.shell. |
|
300 | arg_str = self.shell.transform_cell(arg_str) | |
301 | return self._run_with_profiler(arg_str, opts, self.shell.user_ns) |
|
301 | return self._run_with_profiler(arg_str, opts, self.shell.user_ns) | |
302 |
|
302 | |||
303 | def _run_with_profiler(self, code, opts, namespace): |
|
303 | def _run_with_profiler(self, code, opts, namespace): | |
@@ -1033,7 +1033,7 b' python-profiler package from non-free.""")' | |||||
1033 | # this code has tight coupling to the inner workings of timeit.Timer, |
|
1033 | # this code has tight coupling to the inner workings of timeit.Timer, | |
1034 | # but is there a better way to achieve that the code stmt has access |
|
1034 | # but is there a better way to achieve that the code stmt has access | |
1035 | # to the shell namespace? |
|
1035 | # to the shell namespace? | |
1036 |
transform = self.shell. |
|
1036 | transform = self.shell.transform_cell | |
1037 |
|
1037 | |||
1038 | if cell is None: |
|
1038 | if cell is None: | |
1039 | # called as line magic |
|
1039 | # called as line magic | |
@@ -1191,9 +1191,9 b' python-profiler package from non-free.""")' | |||||
1191 | raise UsageError("Can't use statement directly after '%%time'!") |
|
1191 | raise UsageError("Can't use statement directly after '%%time'!") | |
1192 |
|
1192 | |||
1193 | if cell: |
|
1193 | if cell: | |
1194 |
expr = self.shell. |
|
1194 | expr = self.shell.transform_cell(cell) | |
1195 | else: |
|
1195 | else: | |
1196 |
expr = self.shell. |
|
1196 | expr = self.shell.transform_cell(line) | |
1197 |
|
1197 | |||
1198 | # Minimum time above which parse time will be reported |
|
1198 | # Minimum time above which parse time will be reported | |
1199 | tp_min = 0.1 |
|
1199 | tp_min = 0.1 |
@@ -14,7 +14,7 b' import re' | |||||
14 |
|
14 | |||
15 | from IPython.core.autocall import IPyAutocall |
|
15 | from IPython.core.autocall import IPyAutocall | |
16 | from traitlets.config.configurable import Configurable |
|
16 | from traitlets.config.configurable import Configurable | |
17 |
from IPython.core.input |
|
17 | from IPython.core.inputtransformer2 import ( | |
18 | ESC_MAGIC, |
|
18 | ESC_MAGIC, | |
19 | ESC_QUOTE, |
|
19 | ESC_QUOTE, | |
20 | ESC_QUOTE2, |
|
20 | ESC_QUOTE2, |
@@ -833,28 +833,22 b' def test_user_expression():' | |||||
833 | class TestSyntaxErrorTransformer(unittest.TestCase): |
|
833 | class TestSyntaxErrorTransformer(unittest.TestCase): | |
834 | """Check that SyntaxError raised by an input transformer is handled by run_cell()""" |
|
834 | """Check that SyntaxError raised by an input transformer is handled by run_cell()""" | |
835 |
|
835 | |||
836 | class SyntaxErrorTransformer(InputTransformer): |
|
836 | @staticmethod | |
837 |
|
837 | def transformer(lines): | ||
838 | def push(self, line): |
|
838 | for line in lines: | |
839 | pos = line.find('syntaxerror') |
|
839 | pos = line.find('syntaxerror') | |
840 | if pos >= 0: |
|
840 | if pos >= 0: | |
841 | e = SyntaxError('input contains "syntaxerror"') |
|
841 | e = SyntaxError('input contains "syntaxerror"') | |
842 | e.text = line |
|
842 | e.text = line | |
843 | e.offset = pos + 1 |
|
843 | e.offset = pos + 1 | |
844 | raise e |
|
844 | raise e | |
845 |
|
|
845 | return lines | |
846 |
|
||||
847 | def reset(self): |
|
|||
848 | pass |
|
|||
849 |
|
846 | |||
850 | def setUp(self): |
|
847 | def setUp(self): | |
851 | self.transformer = TestSyntaxErrorTransformer.SyntaxErrorTransformer() |
|
848 | ip.input_transformers_post.append(self.transformer) | |
852 | ip.input_splitter.python_line_transforms.append(self.transformer) |
|
|||
853 | ip.input_transformer_manager.python_line_transforms.append(self.transformer) |
|
|||
854 |
|
849 | |||
855 | def tearDown(self): |
|
850 | def tearDown(self): | |
856 |
ip.input_ |
|
851 | ip.input_transformers_post.remove(self.transformer) | |
857 | ip.input_transformer_manager.python_line_transforms.remove(self.transformer) |
|
|||
858 |
|
852 | |||
859 | def test_syntaxerror_input_transformer(self): |
|
853 | def test_syntaxerror_input_transformer(self): | |
860 | with tt.AssertPrints('1234'): |
|
854 | with tt.AssertPrints('1234'): |
@@ -691,8 +691,8 b' class CellMagicTestCase(TestCase):' | |||||
691 | out = _ip.run_cell_magic(magic, 'a', 'b') |
|
691 | out = _ip.run_cell_magic(magic, 'a', 'b') | |
692 | nt.assert_equal(out, ('a','b')) |
|
692 | nt.assert_equal(out, ('a','b')) | |
693 | # Via run_cell, it goes into the user's namespace via displayhook |
|
693 | # Via run_cell, it goes into the user's namespace via displayhook | |
694 | _ip.run_cell('%%' + magic +' c\nd') |
|
694 | _ip.run_cell('%%' + magic +' c\nd\n') | |
695 | nt.assert_equal(_ip.user_ns['_'], ('c','d')) |
|
695 | nt.assert_equal(_ip.user_ns['_'], ('c','d\n')) | |
696 |
|
696 | |||
697 | def test_cell_magic_func_deco(self): |
|
697 | def test_cell_magic_func_deco(self): | |
698 | "Cell magic using simple decorator" |
|
698 | "Cell magic using simple decorator" |
@@ -52,5 +52,9 b' class TestFileToRun(tt.TempFileMixin, unittest.TestCase):' | |||||
52 | self.mktmp(src) |
|
52 | self.mktmp(src) | |
53 |
|
53 | |||
54 | out, err = tt.ipexec(self.fname, options=['-i'], |
|
54 | out, err = tt.ipexec(self.fname, options=['-i'], | |
55 | commands=['"__file__" in globals()', 'exit()']) |
|
55 | commands=['"__file__" in globals()', 'print(123)', 'exit()']) | |
56 | self.assertIn("False", out) |
|
56 | if 'False' not in out: | |
|
57 | print("Subprocess stderr:") | |||
|
58 | print(err) | |||
|
59 | print('-----') | |||
|
60 | raise AssertionError("'False' not found in %r" % out) |
@@ -292,6 +292,7 b' class EmbeddedSphinxShell(object):' | |||||
292 | self.user_ns = self.IP.user_ns |
|
292 | self.user_ns = self.IP.user_ns | |
293 | self.user_global_ns = self.IP.user_global_ns |
|
293 | self.user_global_ns = self.IP.user_global_ns | |
294 |
|
294 | |||
|
295 | self.lines_waiting = [] | |||
295 | self.input = '' |
|
296 | self.input = '' | |
296 | self.output = '' |
|
297 | self.output = '' | |
297 | self.tmp_profile_dir = tmp_profile_dir |
|
298 | self.tmp_profile_dir = tmp_profile_dir | |
@@ -326,13 +327,12 b' class EmbeddedSphinxShell(object):' | |||||
326 | """process the input, capturing stdout""" |
|
327 | """process the input, capturing stdout""" | |
327 |
|
328 | |||
328 | stdout = sys.stdout |
|
329 | stdout = sys.stdout | |
329 | splitter = self.IP.input_splitter |
|
|||
330 | try: |
|
330 | try: | |
331 | sys.stdout = self.cout |
|
331 | sys.stdout = self.cout | |
332 |
s |
|
332 | self.lines_waiting.append(line) | |
333 | more = splitter.push_accepts_more() |
|
333 | if self.IP.check_complete()[0] != 'incomplete': | |
334 | if not more: |
|
334 | source_raw = ''.join(self.lines_waiting) | |
335 | source_raw = splitter.raw_reset() |
|
335 | self.lines_waiting = [] | |
336 | self.IP.run_cell(source_raw, store_history=store_history) |
|
336 | self.IP.run_cell(source_raw, store_history=store_history) | |
337 | finally: |
|
337 | finally: | |
338 | sys.stdout = stdout |
|
338 | sys.stdout = stdout |
@@ -226,16 +226,14 b' class TerminalInteractiveShell(InteractiveShell):' | |||||
226 | def init_prompt_toolkit_cli(self): |
|
226 | def init_prompt_toolkit_cli(self): | |
227 | if self.simple_prompt: |
|
227 | if self.simple_prompt: | |
228 | # Fall back to plain non-interactive output for tests. |
|
228 | # Fall back to plain non-interactive output for tests. | |
229 |
# This is very limited |
|
229 | # This is very limited. | |
230 | def prompt(): |
|
230 | def prompt(): | |
231 | isp = self.input_splitter |
|
|||
232 | prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens()) |
|
231 | prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens()) | |
|
232 | lines = [input(prompt_text)] | |||
233 | prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens()) |
|
233 | prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens()) | |
234 | while isp.push_accepts_more(): |
|
234 | while self.check_complete('\n'.join(lines))[0] == 'incomplete': | |
235 |
line |
|
235 | lines.append( input(prompt_continuation) ) | |
236 |
|
|
236 | return '\n'.join(lines) | |
237 | prompt_text = prompt_continuation |
|
|||
238 | return isp.source_reset() |
|
|||
239 | self.prompt_for_code = prompt |
|
237 | self.prompt_for_code = prompt | |
240 | return |
|
238 | return | |
241 |
|
239 |
@@ -9,7 +9,6 b' import os' | |||||
9 | import sys |
|
9 | import sys | |
10 |
|
10 | |||
11 | from IPython.core.error import TryNext, UsageError |
|
11 | from IPython.core.error import TryNext, UsageError | |
12 | from IPython.core.inputsplitter import IPythonInputSplitter |
|
|||
13 | from IPython.core.magic import Magics, magics_class, line_magic |
|
12 | from IPython.core.magic import Magics, magics_class, line_magic | |
14 | from IPython.lib.clipboard import ClipboardEmpty |
|
13 | from IPython.lib.clipboard import ClipboardEmpty | |
15 | from IPython.utils.text import SList, strip_email_quotes |
|
14 | from IPython.utils.text import SList, strip_email_quotes | |
@@ -40,7 +39,6 b' def get_pasted_lines(sentinel, l_input=py3compat.input, quiet=False):' | |||||
40 | class TerminalMagics(Magics): |
|
39 | class TerminalMagics(Magics): | |
41 | def __init__(self, shell): |
|
40 | def __init__(self, shell): | |
42 | super(TerminalMagics, self).__init__(shell) |
|
41 | super(TerminalMagics, self).__init__(shell) | |
43 | self.input_splitter = IPythonInputSplitter() |
|
|||
44 |
|
42 | |||
45 | def store_or_execute(self, block, name): |
|
43 | def store_or_execute(self, block, name): | |
46 | """ Execute a block, or store it in a variable, per the user's request. |
|
44 | """ Execute a block, or store it in a variable, per the user's request. |
@@ -68,9 +68,8 b' def create_ipython_shortcuts(shell):' | |||||
68 | & insert_mode |
|
68 | & insert_mode | |
69 | & cursor_in_leading_ws |
|
69 | & cursor_in_leading_ws | |
70 | ))(indent_buffer) |
|
70 | ))(indent_buffer) | |
71 |
|
71 | kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode) | ||
72 | kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER) |
|
72 | )(newline_autoindent_outer(shell.input_transformer_manager)) | |
73 | & emacs_insert_mode))(newline_autoindent_outer(shell.input_splitter)) |
|
|||
74 |
|
73 | |||
75 | kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor) |
|
74 | kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor) | |
76 |
|
75 | |||
@@ -107,13 +106,13 b' def newline_or_execute_outer(shell):' | |||||
107 | check_text = d.text |
|
106 | check_text = d.text | |
108 | else: |
|
107 | else: | |
109 | check_text = d.text[:d.cursor_position] |
|
108 | check_text = d.text[:d.cursor_position] | |
110 |
status, indent = shell. |
|
109 | status, indent = shell.check_complete(check_text) | |
111 |
|
110 | |||
112 | if not (d.on_last_line or |
|
111 | if not (d.on_last_line or | |
113 | d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() |
|
112 | d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() | |
114 | ): |
|
113 | ): | |
115 | if shell.autoindent: |
|
114 | if shell.autoindent: | |
116 |
b.insert_text('\n' + |
|
115 | b.insert_text('\n' + indent) | |
117 | else: |
|
116 | else: | |
118 | b.insert_text('\n') |
|
117 | b.insert_text('\n') | |
119 | return |
|
118 | return | |
@@ -122,7 +121,7 b' def newline_or_execute_outer(shell):' | |||||
122 | b.validate_and_handle() |
|
121 | b.validate_and_handle() | |
123 | else: |
|
122 | else: | |
124 | if shell.autoindent: |
|
123 | if shell.autoindent: | |
125 |
b.insert_text('\n' + |
|
124 | b.insert_text('\n' + indent) | |
126 | else: |
|
125 | else: | |
127 | b.insert_text('\n') |
|
126 | b.insert_text('\n') | |
128 | return newline_or_execute |
|
127 | return newline_or_execute |
@@ -96,9 +96,7 b' class InteractiveShellTestCase(unittest.TestCase):' | |||||
96 | @mock_input |
|
96 | @mock_input | |
97 | def test_inputtransformer_syntaxerror(self): |
|
97 | def test_inputtransformer_syntaxerror(self): | |
98 | ip = get_ipython() |
|
98 | ip = get_ipython() | |
99 |
transformer |
|
99 | ip.input_transformers_post.append(syntax_error_transformer) | |
100 | ip.input_splitter.python_line_transforms.append(transformer) |
|
|||
101 | ip.input_transformer_manager.python_line_transforms.append(transformer) |
|
|||
102 |
|
100 | |||
103 | try: |
|
101 | try: | |
104 | #raise Exception |
|
102 | #raise Exception | |
@@ -112,8 +110,7 b' class InteractiveShellTestCase(unittest.TestCase):' | |||||
112 | yield u'print(4*4)' |
|
110 | yield u'print(4*4)' | |
113 |
|
111 | |||
114 | finally: |
|
112 | finally: | |
115 |
ip.input_ |
|
113 | ip.input_transformers_post.remove(syntax_error_transformer) | |
116 | ip.input_transformer_manager.python_line_transforms.remove(transformer) |
|
|||
117 |
|
114 | |||
118 | def test_plain_text_only(self): |
|
115 | def test_plain_text_only(self): | |
119 | ip = get_ipython() |
|
116 | ip = get_ipython() | |
@@ -146,20 +143,17 b' class InteractiveShellTestCase(unittest.TestCase):' | |||||
146 | self.assertEqual(data, {'text/plain': repr(obj)}) |
|
143 | self.assertEqual(data, {'text/plain': repr(obj)}) | |
147 | assert captured.stdout == '' |
|
144 | assert captured.stdout == '' | |
148 |
|
145 | |||
149 |
|
146 | def syntax_error_transformer(lines): | ||
150 |
|
147 | """Transformer that throws SyntaxError if 'syntaxerror' is in the code.""" | ||
151 | class SyntaxErrorTransformer(InputTransformer): |
|
148 | for line in lines: | |
152 | def push(self, line): |
|
|||
153 | pos = line.find('syntaxerror') |
|
149 | pos = line.find('syntaxerror') | |
154 | if pos >= 0: |
|
150 | if pos >= 0: | |
155 | e = SyntaxError('input contains "syntaxerror"') |
|
151 | e = SyntaxError('input contains "syntaxerror"') | |
156 | e.text = line |
|
152 | e.text = line | |
157 | e.offset = pos + 1 |
|
153 | e.offset = pos + 1 | |
158 | raise e |
|
154 | raise e | |
159 |
|
|
155 | return lines | |
160 |
|
156 | |||
161 | def reset(self): |
|
|||
162 | pass |
|
|||
163 |
|
157 | |||
164 | class TerminalMagicsTestCase(unittest.TestCase): |
|
158 | class TerminalMagicsTestCase(unittest.TestCase): | |
165 | def test_paste_magics_blankline(self): |
|
159 | def test_paste_magics_blankline(self): |
@@ -7,7 +7,7 b' from collections import namedtuple' | |||||
7 | from io import StringIO |
|
7 | from io import StringIO | |
8 | from keyword import iskeyword |
|
8 | from keyword import iskeyword | |
9 |
|
9 | |||
10 |
|
|
10 | import tokenize | |
11 |
|
11 | |||
12 |
|
12 | |||
13 | Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line']) |
|
13 | Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line']) | |
@@ -15,9 +15,9 b" Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line'])" | |||||
15 | def generate_tokens(readline): |
|
15 | def generate_tokens(readline): | |
16 | """wrap generate_tokens to catch EOF errors""" |
|
16 | """wrap generate_tokens to catch EOF errors""" | |
17 | try: |
|
17 | try: | |
18 |
for token in tokenize |
|
18 | for token in tokenize.generate_tokens(readline): | |
19 | yield token |
|
19 | yield token | |
20 |
except tokenize |
|
20 | except tokenize.TokenError: | |
21 | # catch EOF error |
|
21 | # catch EOF error | |
22 | return |
|
22 | return | |
23 |
|
23 | |||
@@ -99,12 +99,12 b' def token_at_cursor(cell, cursor_pos=0):' | |||||
99 | # don't consume it |
|
99 | # don't consume it | |
100 | break |
|
100 | break | |
101 |
|
101 | |||
102 |
if tok.token == tokenize |
|
102 | if tok.token == tokenize.NAME and not iskeyword(tok.text): | |
103 |
if names and tokens and tokens[-1].token == tokenize |
|
103 | if names and tokens and tokens[-1].token == tokenize.OP and tokens[-1].text == '.': | |
104 | names[-1] = "%s.%s" % (names[-1], tok.text) |
|
104 | names[-1] = "%s.%s" % (names[-1], tok.text) | |
105 | else: |
|
105 | else: | |
106 | names.append(tok.text) |
|
106 | names.append(tok.text) | |
107 |
elif tok.token == tokenize |
|
107 | elif tok.token == tokenize.OP: | |
108 | if tok.text == '=' and names: |
|
108 | if tok.text == '=' and names: | |
109 | # don't inspect the lhs of an assignment |
|
109 | # don't inspect the lhs of an assignment | |
110 | names.pop(-1) |
|
110 | names.pop(-1) |
@@ -45,7 +45,7 b' class _DummyTerminal(object):' | |||||
45 | """Used as a buffer to get prompt_toolkit bindings |
|
45 | """Used as a buffer to get prompt_toolkit bindings | |
46 | """ |
|
46 | """ | |
47 | handle_return = None |
|
47 | handle_return = None | |
48 |
input_ |
|
48 | input_transformer_manager = None | |
49 | display_completions = None |
|
49 | display_completions = None | |
50 |
|
50 | |||
51 | ipy_bindings = create_ipython_shortcuts(_DummyTerminal()).bindings |
|
51 | ipy_bindings = create_ipython_shortcuts(_DummyTerminal()).bindings |
@@ -282,8 +282,8 b' IPython configuration::' | |||||
282 | else: # insert a newline with auto-indentation... |
|
282 | else: # insert a newline with auto-indentation... | |
283 |
|
283 | |||
284 | if document.line_count > 1: text = text[:document.cursor_position] |
|
284 | if document.line_count > 1: text = text[:document.cursor_position] | |
285 |
indent = shell. |
|
285 | indent = shell.check_complete(text)[1] | |
286 |
buffer.insert_text('\n' + |
|
286 | buffer.insert_text('\n' + indent) | |
287 |
|
287 | |||
288 | # if you just wanted a plain newline without any indentation, you |
|
288 | # if you just wanted a plain newline without any indentation, you | |
289 | # could use `buffer.insert_text('\n')` instead of the lines above |
|
289 | # could use `buffer.insert_text('\n')` instead of the lines above |
@@ -15,163 +15,52 b' String based transformations' | |||||
15 |
|
15 | |||
16 | .. currentmodule:: IPython.core.inputtransforms |
|
16 | .. currentmodule:: IPython.core.inputtransforms | |
17 |
|
17 | |||
18 |
When the user enters |
|
18 | When the user enters code, it is first processed as a string. By the | |
19 | end of this stage, it must be valid Python syntax. |
|
19 | end of this stage, it must be valid Python syntax. | |
20 |
|
20 | |||
21 | These transformers all subclass :class:`IPython.core.inputtransformer.InputTransformer`, |
|
21 | .. versionchanged:: 7.0 | |
22 | and are used by :class:`IPython.core.inputsplitter.IPythonInputSplitter`. |
|
|||
23 |
|
||||
24 | These transformers act in three groups, stored separately as lists of instances |
|
|||
25 | in attributes of :class:`~IPython.core.inputsplitter.IPythonInputSplitter`: |
|
|||
26 |
|
||||
27 | * ``physical_line_transforms`` act on the lines as the user enters them. For |
|
|||
28 | example, these strip Python prompts from examples pasted in. |
|
|||
29 | * ``logical_line_transforms`` act on lines as connected by explicit line |
|
|||
30 | continuations, i.e. ``\`` at the end of physical lines. They are skipped |
|
|||
31 | inside multiline Python statements. This is the point where IPython recognises |
|
|||
32 | ``%magic`` commands, for instance. |
|
|||
33 | * ``python_line_transforms`` act on blocks containing complete Python statements. |
|
|||
34 | Multi-line strings, lists and function calls are reassembled before being |
|
|||
35 | passed to these, but note that function and class *definitions* are still a |
|
|||
36 | series of separate statements. IPython does not use any of these by default. |
|
|||
37 |
|
||||
38 | An InteractiveShell instance actually has two |
|
|||
39 | :class:`~IPython.core.inputsplitter.IPythonInputSplitter` instances, as the |
|
|||
40 | attributes :attr:`~IPython.core.interactiveshell.InteractiveShell.input_splitter`, |
|
|||
41 | to tell when a block of input is complete, and |
|
|||
42 | :attr:`~IPython.core.interactiveshell.InteractiveShell.input_transformer_manager`, |
|
|||
43 | to transform complete cells. If you add a transformer, you should make sure that |
|
|||
44 | it gets added to both, e.g.:: |
|
|||
45 |
|
||||
46 | ip.input_splitter.logical_line_transforms.append(my_transformer()) |
|
|||
47 | ip.input_transformer_manager.logical_line_transforms.append(my_transformer()) |
|
|||
48 |
|
22 | |||
49 | These transformers may raise :exc:`SyntaxError` if the input code is invalid, but |
|
23 | The API for string and token-based transformations has been completely | |
50 | in most cases it is clearer to pass unrecognised code through unmodified and let |
|
24 | redesigned. Any third party code extending input transformation will need to | |
51 | Python's own parser decide whether it is valid. |
|
25 | be rewritten. The new API is, hopefully, simpler. | |
52 |
|
||||
53 | .. versionchanged:: 2.0 |
|
|||
54 |
|
||||
55 | Added the option to raise :exc:`SyntaxError`. |
|
|||
56 |
|
||||
57 | Stateless transformations |
|
|||
58 | ------------------------- |
|
|||
59 |
|
||||
60 | The simplest kind of transformations work one line at a time. Write a function |
|
|||
61 | which takes a line and returns a line, and decorate it with |
|
|||
62 | :meth:`StatelessInputTransformer.wrap`:: |
|
|||
63 |
|
||||
64 | @StatelessInputTransformer.wrap |
|
|||
65 | def my_special_commands(line): |
|
|||
66 | if line.startswith("¬"): |
|
|||
67 | return "specialcommand(" + repr(line) + ")" |
|
|||
68 | return line |
|
|||
69 |
|
||||
70 | The decorator returns a factory function which will produce instances of |
|
|||
71 | :class:`~IPython.core.inputtransformer.StatelessInputTransformer` using your |
|
|||
72 | function. |
|
|||
73 |
|
||||
74 | Transforming a full block |
|
|||
75 | ------------------------- |
|
|||
76 |
|
||||
77 | .. warning:: |
|
|||
78 |
|
26 | |||
79 | Transforming a full block at once will break the automatic detection of |
|
27 | String based transformations are functions which accept a list of strings: | |
80 | whether a block of code is complete in interfaces relying on this |
|
28 | each string is a single line of the input cell, including its line ending. | |
81 | functionality, such as terminal IPython. You will need to use a |
|
29 | The transformation function should return output in the same structure. | |
82 | shortcut to force-execute your cells. |
|
|||
83 |
|
30 | |||
84 | Transforming a full block of python code is possible by implementing a |
|
31 | These transformations are in two groups, accessible as attributes of | |
85 | :class:`~IPython.core.inputtransformer.Inputtransformer` and overwriting the |
|
32 | the :class:`~IPython.core.interactiveshell.InteractiveShell` instance. | |
86 | ``push`` and ``reset`` methods. The reset method should send the full block of |
|
33 | Each group is a list of transformation functions. | |
87 | transformed text. As an example a transformer the reversed the lines from last |
|
|||
88 | to first. |
|
|||
89 |
|
34 | |||
90 | from IPython.core.inputtransformer import InputTransformer |
|
35 | * ``input_transformers_cleanup`` run first on input, to do things like stripping | |
|
36 | prompts and leading indents from copied code. It may not be possible at this | |||
|
37 | stage to parse the input as valid Python code. | |||
|
38 | * Then IPython runs its own transformations to handle its special syntax, like | |||
|
39 | ``%magics`` and ``!system`` commands. This part does not expose extension | |||
|
40 | points. | |||
|
41 | * ``input_transformers_post`` run as the last step, to do things like converting | |||
|
42 | float literals into decimal objects. These may attempt to parse the input as | |||
|
43 | Python code. | |||
91 |
|
44 | |||
92 | class ReverseLineTransformer(InputTransformer): |
|
45 | These transformers may raise :exc:`SyntaxError` if the input code is invalid, but | |
93 |
|
46 | in most cases it is clearer to pass unrecognised code through unmodified and let | ||
94 | def __init__(self): |
|
47 | Python's own parser decide whether it is valid. | |
95 | self.acc = [] |
|
|||
96 |
|
||||
97 | def push(self, line): |
|
|||
98 | self.acc.append(line) |
|
|||
99 | return None |
|
|||
100 |
|
||||
101 | def reset(self): |
|
|||
102 | ret = '\n'.join(self.acc[::-1]) |
|
|||
103 | self.acc = [] |
|
|||
104 | return ret |
|
|||
105 |
|
||||
106 |
|
||||
107 | Coroutine transformers |
|
|||
108 | ---------------------- |
|
|||
109 |
|
||||
110 | More advanced transformers can be written as coroutines. The coroutine will be |
|
|||
111 | sent each line in turn, followed by ``None`` to reset it. It can yield lines, or |
|
|||
112 | ``None`` if it is accumulating text to yield at a later point. When reset, it |
|
|||
113 | should give up any code it has accumulated. |
|
|||
114 |
|
||||
115 | You may use :meth:`CoroutineInputTransformer.wrap` to simplify the creation of |
|
|||
116 | such a transformer. |
|
|||
117 |
|
48 | |||
118 | Here is a simple :class:`CoroutineInputTransformer` that can be thought of |
|
49 | For example, imagine we want to obfuscate our code by reversing each line, so | |
119 | being the identity:: |
|
50 | we'd write ``)5(f =+ a`` instead of ``a += f(5)``. Here's how we could swap it | |
|
51 | back the right way before IPython tries to run it:: | |||
120 |
|
52 | |||
121 | from IPython.core.inputtransformer import CoroutineInputTransformer |
|
53 | def reverse_line_chars(lines): | |
|
54 | new_lines = [] | |||
|
55 | for line in lines: | |||
|
56 | chars = line[:-1] # the newline needs to stay at the end | |||
|
57 | new_lines.append(chars[::-1] + '\n') | |||
|
58 | return new_lines | |||
122 |
|
|
59 | ||
123 | @CoroutineInputTransformer.wrap |
|
60 | To start using this:: | |
124 | def noop(): |
|
|||
125 | line = '' |
|
|||
126 | while True: |
|
|||
127 | line = (yield line) |
|
|||
128 |
|
61 | |||
129 | ip = get_ipython() |
|
62 | ip = get_ipython() | |
130 |
|
63 | ip.input_transformers_cleanup.append(reverse_line_chars) | ||
131 | ip.input_splitter.logical_line_transforms.append(noop()) |
|
|||
132 | ip.input_transformer_manager.logical_line_transforms.append(noop()) |
|
|||
133 |
|
||||
134 | This code in IPython strips a constant amount of leading indentation from each |
|
|||
135 | line in a cell:: |
|
|||
136 |
|
||||
137 | from IPython.core.inputtransformer import CoroutineInputTransformer |
|
|||
138 |
|
||||
139 | @CoroutineInputTransformer.wrap |
|
|||
140 | def leading_indent(): |
|
|||
141 | """Remove leading indentation. |
|
|||
142 |
|
||||
143 | If the first line starts with a spaces or tabs, the same whitespace will be |
|
|||
144 | removed from each following line until it is reset. |
|
|||
145 | """ |
|
|||
146 | space_re = re.compile(r'^[ \t]+') |
|
|||
147 | line = '' |
|
|||
148 | while True: |
|
|||
149 | line = (yield line) |
|
|||
150 |
|
||||
151 | if line is None: |
|
|||
152 | continue |
|
|||
153 |
|
||||
154 | m = space_re.match(line) |
|
|||
155 | if m: |
|
|||
156 | space = m.group(0) |
|
|||
157 | while line is not None: |
|
|||
158 | if line.startswith(space): |
|
|||
159 | line = line[len(space):] |
|
|||
160 | line = (yield line) |
|
|||
161 | else: |
|
|||
162 | # No leading spaces - wait for reset |
|
|||
163 | while line is not None: |
|
|||
164 | line = (yield line) |
|
|||
165 |
|
||||
166 |
|
||||
167 | Token-based transformers |
|
|||
168 | ------------------------ |
|
|||
169 |
|
||||
170 | There is an experimental framework that takes care of tokenizing and |
|
|||
171 | untokenizing lines of code. Define a function that accepts a list of tokens, and |
|
|||
172 | returns an iterable of output tokens, and decorate it with |
|
|||
173 | :meth:`TokenInputTransformer.wrap`. These should only be used in |
|
|||
174 | ``python_line_transforms``. |
|
|||
175 |
|
|
64 | ||
176 | AST transformations |
|
65 | AST transformations | |
177 | =================== |
|
66 | =================== |
General Comments 0
You need to be logged in to leave comments.
Login now