##// END OF EJS Templates
Add & improve docstrings following @willingc's review
Thomas Kluyver -
Show More
@@ -2,6 +2,9 b''
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
6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
7 deprecated in 7.0.
5 """
8 """
6
9
7 # Copyright (c) IPython Development Team.
10 # Copyright (c) IPython Development Team.
@@ -19,7 +22,7 b' def leading_indent(lines):'
19 """Remove leading indentation.
22 """Remove leading indentation.
20
23
21 If the first line starts with a spaces or tabs, the same whitespace will be
24 If the first line starts with a spaces or tabs, the same whitespace will be
22 removed from each following line.
25 removed from each following line in the cell.
23 """
26 """
24 m = _indent_re.match(lines[0])
27 m = _indent_re.match(lines[0])
25 if not m:
28 if not m:
@@ -35,11 +38,12 b' class PromptStripper:'
35 Parameters
38 Parameters
36 ----------
39 ----------
37 prompt_re : regular expression
40 prompt_re : regular expression
38 A regular expression matching any input prompt (including continuation)
41 A regular expression matching any input prompt (including continuation,
42 e.g. ``...``)
39 initial_re : regular expression, optional
43 initial_re : regular expression, optional
40 A regular expression matching only the initial prompt, but not continuation.
44 A regular expression matching only the initial prompt, but not continuation.
41 If no initial expression is given, prompt_re will be used everywhere.
45 If no initial expression is given, prompt_re will be used everywhere.
42 Used mainly for plain Python prompts, where the continuation prompt
46 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
43 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
47 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
44
48
45 If initial_re and prompt_re differ,
49 If initial_re and prompt_re differ,
@@ -78,11 +82,12 b' def cell_magic(lines):'
78 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
82 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
79 % (magic_name, first_line, body)]
83 % (magic_name, first_line, body)]
80
84
81 # -----
82
85
83 def _find_assign_op(token_line):
86 def _find_assign_op(token_line):
84 # Get the index of the first assignment in the line ('=' not inside brackets)
87 """Get the index of the first assignment in the line ('=' not inside brackets)
85 # We don't try to support multiple special assignment (a = b = %foo)
88
89 Note: We don't try to support multiple special assignment (a = b = %foo)
90 """
86 paren_level = 0
91 paren_level = 0
87 for i, ti in enumerate(token_line):
92 for i, ti in enumerate(token_line):
88 s = ti.string
93 s = ti.string
@@ -107,15 +112,48 b' def find_end_of_continued_line(lines, start_line: int):'
107 return end_line
112 return end_line
108
113
109 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
114 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
110 """Assemble pieces of a continued line into a single line.
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):
111
126
112 Uses 0-indexed line numbers. *start* is (lineno, colno).
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.
113 """
136 """
114 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
137 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
115 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
138 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
116 + [parts[-1][:-1]]) # Strip newline from last line
139 + [parts[-1][:-1]]) # Strip newline from last line
117
140
118 class TokenTransformBase:
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 """
119 # Lower numbers -> higher priority (for matches in the same location)
157 # Lower numbers -> higher priority (for matches in the same location)
120 priority = 10
158 priority = 10
121
159
@@ -126,15 +164,32 b' class TokenTransformBase:'
126 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
164 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
127 self.start_col = start[1]
165 self.start_col = start[1]
128
166
167 @classmethod
168 def find(cls, tokens_by_line):
169 """Find one instance of special syntax in the provided tokens.
170
171 Tokens are grouped into logical lines for convenience,
172 so it is easy to e.g. look at the first token of each line.
173 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
174
175 This should return an instance of its class, pointing to the start
176 position it has found, or None if it found no match.
177 """
178 raise NotImplementedError
179
129 def transform(self, lines: List[str]):
180 def transform(self, lines: List[str]):
181 """Transform one instance of special syntax found by ``find()``
182
183 Takes a list of strings representing physical lines,
184 returns a similar list of transformed lines.
185 """
130 raise NotImplementedError
186 raise NotImplementedError
131
187
132 class MagicAssign(TokenTransformBase):
188 class MagicAssign(TokenTransformBase):
189 """Transformer for assignments from magics (a = %foo)"""
133 @classmethod
190 @classmethod
134 def find(cls, tokens_by_line):
191 def find(cls, tokens_by_line):
135 """Find the first magic assignment (a = %foo) in the cell.
192 """Find the first magic assignment (a = %foo) in the cell.
136
137 Returns (line, column) of the % if found, or None. *line* is 1-indexed.
138 """
193 """
139 for line in tokens_by_line:
194 for line in tokens_by_line:
140 assign_ix = _find_assign_op(line)
195 assign_ix = _find_assign_op(line)
@@ -145,7 +200,7 b' class MagicAssign(TokenTransformBase):'
145 return cls(line[assign_ix+1].start)
200 return cls(line[assign_ix+1].start)
146
201
147 def transform(self, lines: List[str]):
202 def transform(self, lines: List[str]):
148 """Transform a magic assignment found by find
203 """Transform a magic assignment found by the ``find()`` classmethod.
149 """
204 """
150 start_line, start_col = self.start_line, self.start_col
205 start_line, start_col = self.start_line, self.start_col
151 lhs = lines[start_line][:start_col]
206 lhs = lines[start_line][:start_col]
@@ -163,11 +218,10 b' class MagicAssign(TokenTransformBase):'
163
218
164
219
165 class SystemAssign(TokenTransformBase):
220 class SystemAssign(TokenTransformBase):
221 """Transformer for assignments from system commands (a = !foo)"""
166 @classmethod
222 @classmethod
167 def find(cls, tokens_by_line):
223 def find(cls, tokens_by_line):
168 """Find the first system assignment (a = !foo) in the cell.
224 """Find the first system assignment (a = !foo) in the cell.
169
170 Returns (line, column) of the ! if found, or None. *line* is 1-indexed.
171 """
225 """
172 for line in tokens_by_line:
226 for line in tokens_by_line:
173 assign_ix = _find_assign_op(line)
227 assign_ix = _find_assign_op(line)
@@ -184,7 +238,7 b' class SystemAssign(TokenTransformBase):'
184 ix += 1
238 ix += 1
185
239
186 def transform(self, lines: List[str]):
240 def transform(self, lines: List[str]):
187 """Transform a system assignment found by find
241 """Transform a system assignment found by the ``find()`` classmethod.
188 """
242 """
189 start_line, start_col = self.start_line, self.start_col
243 start_line, start_col = self.start_line, self.start_col
190
244
@@ -237,38 +291,42 b' def _make_help_call(target, esc, next_input=None):'
237 (next_input, t_magic_name, t_magic_arg_s)
291 (next_input, t_magic_name, t_magic_arg_s)
238
292
239 def _tr_help(content):
293 def _tr_help(content):
240 "Translate lines escaped with: ?"
294 """Translate lines escaped with: ?
241 # A naked help line should just fire the intro help screen
295
296 A naked help line should fire the intro help screen (shell.show_usage())
297 """
242 if not content:
298 if not content:
243 return 'get_ipython().show_usage()'
299 return 'get_ipython().show_usage()'
244
300
245 return _make_help_call(content, '?')
301 return _make_help_call(content, '?')
246
302
247 def _tr_help2(content):
303 def _tr_help2(content):
248 "Translate lines escaped with: ??"
304 """Translate lines escaped with: ??
249 # A naked help line should just fire the intro help screen
305
306 A naked help line should fire the intro help screen (shell.show_usage())
307 """
250 if not content:
308 if not content:
251 return 'get_ipython().show_usage()'
309 return 'get_ipython().show_usage()'
252
310
253 return _make_help_call(content, '??')
311 return _make_help_call(content, '??')
254
312
255 def _tr_magic(content):
313 def _tr_magic(content):
256 "Translate lines escaped with: %"
314 "Translate lines escaped with a percent sign: %"
257 name, _, args = content.partition(' ')
315 name, _, args = content.partition(' ')
258 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
316 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
259
317
260 def _tr_quote(content):
318 def _tr_quote(content):
261 "Translate lines escaped with: ,"
319 "Translate lines escaped with a comma: ,"
262 name, _, args = content.partition(' ')
320 name, _, args = content.partition(' ')
263 return '%s("%s")' % (name, '", "'.join(args.split()) )
321 return '%s("%s")' % (name, '", "'.join(args.split()) )
264
322
265 def _tr_quote2(content):
323 def _tr_quote2(content):
266 "Translate lines escaped with: ;"
324 "Translate lines escaped with a semicolon: ;"
267 name, _, args = content.partition(' ')
325 name, _, args = content.partition(' ')
268 return '%s("%s")' % (name, args)
326 return '%s("%s")' % (name, args)
269
327
270 def _tr_paren(content):
328 def _tr_paren(content):
271 "Translate lines escaped with: /"
329 "Translate lines escaped with a slash: /"
272 name, _, args = content.partition(' ')
330 name, _, args = content.partition(' ')
273 return '%s(%s)' % (name, ", ".join(args.split()))
331 return '%s(%s)' % (name, ", ".join(args.split()))
274
332
@@ -282,11 +340,10 b" tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,"
282 ESC_PAREN : _tr_paren }
340 ESC_PAREN : _tr_paren }
283
341
284 class EscapedCommand(TokenTransformBase):
342 class EscapedCommand(TokenTransformBase):
343 """Transformer for escaped commands like %foo, !foo, or /foo"""
285 @classmethod
344 @classmethod
286 def find(cls, tokens_by_line):
345 def find(cls, tokens_by_line):
287 """Find the first escaped command (%foo, !foo, etc.) in the cell.
346 """Find the first escaped command (%foo, !foo, etc.) in the cell.
288
289 Returns (line, column) of the escape if found, or None. *line* is 1-indexed.
290 """
347 """
291 for line in tokens_by_line:
348 for line in tokens_by_line:
292 ix = 0
349 ix = 0
@@ -296,6 +353,8 b' class EscapedCommand(TokenTransformBase):'
296 return cls(line[ix].start)
353 return cls(line[ix].start)
297
354
298 def transform(self, lines):
355 def transform(self, lines):
356 """Transform an escaped line found by the ``find()`` classmethod.
357 """
299 start_line, start_col = self.start_line, self.start_col
358 start_line, start_col = self.start_line, self.start_col
300
359
301 indent = lines[start_line][:start_col]
360 indent = lines[start_line][:start_col]
@@ -323,6 +382,7 b' _help_end_re = re.compile(r"""(%{0,2}'
323 re.VERBOSE)
382 re.VERBOSE)
324
383
325 class HelpEnd(TokenTransformBase):
384 class HelpEnd(TokenTransformBase):
385 """Transformer for help syntax: obj? and obj??"""
326 # This needs to be higher priority (lower number) than EscapedCommand so
386 # This needs to be higher priority (lower number) than EscapedCommand so
327 # that inspecting magics (%foo?) works.
387 # that inspecting magics (%foo?) works.
328 priority = 5
388 priority = 5
@@ -334,6 +394,8 b' class HelpEnd(TokenTransformBase):'
334
394
335 @classmethod
395 @classmethod
336 def find(cls, tokens_by_line):
396 def find(cls, tokens_by_line):
397 """Find the first help command (foo?) in the cell.
398 """
337 for line in tokens_by_line:
399 for line in tokens_by_line:
338 # Last token is NEWLINE; look at last but one
400 # Last token is NEWLINE; look at last but one
339 if len(line) > 2 and line[-2].string == '?':
401 if len(line) > 2 and line[-2].string == '?':
@@ -344,6 +406,8 b' class HelpEnd(TokenTransformBase):'
344 return cls(line[ix].start, line[-2].start)
406 return cls(line[ix].start, line[-2].start)
345
407
346 def transform(self, lines):
408 def transform(self, lines):
409 """Transform a help command found by the ``find()`` classmethod.
410 """
347 piece = ''.join(lines[self.start_line:self.q_line+1])
411 piece = ''.join(lines[self.start_line:self.q_line+1])
348 indent, content = piece[:self.start_col], piece[self.start_col:]
412 indent, content = piece[:self.start_col], piece[self.start_col:]
349 lines_before = lines[:self.start_line]
413 lines_before = lines[:self.start_line]
@@ -396,7 +460,7 b' def make_tokens_by_line(lines):'
396 return tokens_by_line
460 return tokens_by_line
397
461
398 def show_linewise_tokens(s: str):
462 def show_linewise_tokens(s: str):
399 """For investigation"""
463 """For investigation and debugging"""
400 if not s.endswith('\n'):
464 if not s.endswith('\n'):
401 s += '\n'
465 s += '\n'
402 lines = s.splitlines(keepends=True)
466 lines = s.splitlines(keepends=True)
@@ -409,6 +473,11 b' def show_linewise_tokens(s: str):'
409 TRANSFORM_LOOP_LIMIT = 500
473 TRANSFORM_LOOP_LIMIT = 500
410
474
411 class TransformerManager:
475 class TransformerManager:
476 """Applies various transformations to a cell or code block.
477
478 The key methods for external use are ``transform_cell()``
479 and ``check_complete()``.
480 """
412 def __init__(self):
481 def __init__(self):
413 self.cleanup_transforms = [
482 self.cleanup_transforms = [
414 leading_indent,
483 leading_indent,
@@ -462,7 +531,8 b' class TransformerManager:'
462 raise RuntimeError("Input transformation still changing after "
531 raise RuntimeError("Input transformation still changing after "
463 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
532 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
464
533
465 def transform_cell(self, cell: str):
534 def transform_cell(self, cell: str) -> str:
535 """Transforms a cell of input code"""
466 if not cell.endswith('\n'):
536 if not cell.endswith('\n'):
467 cell += '\n' # Ensure the cell has a trailing newline
537 cell += '\n' # Ensure the cell has a trailing newline
468 lines = cell.splitlines(keepends=True)
538 lines = cell.splitlines(keepends=True)
@@ -2807,6 +2807,18 b' class InteractiveShell(SingletonConfigurable):'
2807 return result
2807 return result
2808
2808
2809 def transform_cell(self, raw_cell):
2809 def transform_cell(self, raw_cell):
2810 """Transform an input cell before parsing it.
2811
2812 Static transformations, implemented in IPython.core.inputtransformer2,
2813 deal with things like ``%magic`` and ``!system`` commands.
2814 These run on all input.
2815 Dynamic transformations, for things like unescaped magics and the exit
2816 autocall, depend on the state of the interpreter.
2817 These only apply to single line inputs.
2818
2819 These string-based transformations are followed by AST transformations;
2820 see :meth:`transform_ast`.
2821 """
2810 # Static input transformations
2822 # Static input transformations
2811 cell = self.input_transformer_manager.transform_cell(raw_cell)
2823 cell = self.input_transformer_manager.transform_cell(raw_cell)
2812
2824
@@ -1,9 +1,9 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.
4 more complex. See test_inputtransformer2_line for tests for line-based
5 transformations.
5 """
6 """
6
7 import nose.tools as nt
7 import nose.tools as nt
8
8
9 from IPython.core import inputtransformer2 as ipt2
9 from IPython.core import inputtransformer2 as ipt2
@@ -1,7 +1,7 b''
1 """Tests for the line-based transformers in IPython.core.inputtransformer2
1 """Tests for the line-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.
4 more complex. See test_inputtransformer2 for tests for token-based transformers.
5 """
5 """
6 import nose.tools as nt
6 import nose.tools as nt
7
7
General Comments 0
You need to be logged in to leave comments. Login now