##// END OF EJS Templates
Add & improve docstrings following @willingc's review
Thomas Kluyver -
Show More
@@ -2,6 +2,9 b''
2 2
3 3 This includes the machinery to recognise and transform ``%magic`` commands,
4 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 10 # Copyright (c) IPython Development Team.
@@ -19,7 +22,7 b' def leading_indent(lines):'
19 22 """Remove leading indentation.
20 23
21 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 27 m = _indent_re.match(lines[0])
25 28 if not m:
@@ -35,11 +38,12 b' class PromptStripper:'
35 38 Parameters
36 39 ----------
37 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 43 initial_re : regular expression, optional
40 44 A regular expression matching only the initial prompt, but not continuation.
41 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 47 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
44 48
45 49 If initial_re and prompt_re differ,
@@ -78,11 +82,12 b' def cell_magic(lines):'
78 82 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
79 83 % (magic_name, first_line, body)]
80 84
81 # -----
82 85
83 86 def _find_assign_op(token_line):
84 # 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)
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 """
86 91 paren_level = 0
87 92 for i, ti in enumerate(token_line):
88 93 s = ti.string
@@ -107,15 +112,48 b' def find_end_of_continued_line(lines, start_line: int):'
107 112 return end_line
108 113
109 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):
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).
111 133
112 Uses 0-indexed line numbers. *start* is (lineno, colno).
134 Used to allow ``%magic`` and ``!system`` commands to be continued over
135 multiple lines.
113 136 """
114 137 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
115 138 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
116 139 + [parts[-1][:-1]]) # Strip newline from last line
117 140
118 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 157 # Lower numbers -> higher priority (for matches in the same location)
120 158 priority = 10
121 159
@@ -126,15 +164,32 b' class TokenTransformBase:'
126 164 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
127 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 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 186 raise NotImplementedError
131 187
132 188 class MagicAssign(TokenTransformBase):
189 """Transformer for assignments from magics (a = %foo)"""
133 190 @classmethod
134 191 def find(cls, tokens_by_line):
135 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 194 for line in tokens_by_line:
140 195 assign_ix = _find_assign_op(line)
@@ -145,7 +200,7 b' class MagicAssign(TokenTransformBase):'
145 200 return cls(line[assign_ix+1].start)
146 201
147 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 205 start_line, start_col = self.start_line, self.start_col
151 206 lhs = lines[start_line][:start_col]
@@ -163,11 +218,10 b' class MagicAssign(TokenTransformBase):'
163 218
164 219
165 220 class SystemAssign(TokenTransformBase):
221 """Transformer for assignments from system commands (a = !foo)"""
166 222 @classmethod
167 223 def find(cls, tokens_by_line):
168 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 226 for line in tokens_by_line:
173 227 assign_ix = _find_assign_op(line)
@@ -184,7 +238,7 b' class SystemAssign(TokenTransformBase):'
184 238 ix += 1
185 239
186 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 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 291 (next_input, t_magic_name, t_magic_arg_s)
238 292
239 293 def _tr_help(content):
240 "Translate lines escaped with: ?"
241 # A naked help line should just fire the intro help screen
294 """Translate lines escaped with: ?
295
296 A naked help line should fire the intro help screen (shell.show_usage())
297 """
242 298 if not content:
243 299 return 'get_ipython().show_usage()'
244 300
245 301 return _make_help_call(content, '?')
246 302
247 303 def _tr_help2(content):
248 "Translate lines escaped with: ??"
249 # A naked help line should just fire the intro help screen
304 """Translate lines escaped with: ??
305
306 A naked help line should fire the intro help screen (shell.show_usage())
307 """
250 308 if not content:
251 309 return 'get_ipython().show_usage()'
252 310
253 311 return _make_help_call(content, '??')
254 312
255 313 def _tr_magic(content):
256 "Translate lines escaped with: %"
314 "Translate lines escaped with a percent sign: %"
257 315 name, _, args = content.partition(' ')
258 316 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
259 317
260 318 def _tr_quote(content):
261 "Translate lines escaped with: ,"
319 "Translate lines escaped with a comma: ,"
262 320 name, _, args = content.partition(' ')
263 321 return '%s("%s")' % (name, '", "'.join(args.split()) )
264 322
265 323 def _tr_quote2(content):
266 "Translate lines escaped with: ;"
324 "Translate lines escaped with a semicolon: ;"
267 325 name, _, args = content.partition(' ')
268 326 return '%s("%s")' % (name, args)
269 327
270 328 def _tr_paren(content):
271 "Translate lines escaped with: /"
329 "Translate lines escaped with a slash: /"
272 330 name, _, args = content.partition(' ')
273 331 return '%s(%s)' % (name, ", ".join(args.split()))
274 332
@@ -282,11 +340,10 b" tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,"
282 340 ESC_PAREN : _tr_paren }
283 341
284 342 class EscapedCommand(TokenTransformBase):
343 """Transformer for escaped commands like %foo, !foo, or /foo"""
285 344 @classmethod
286 345 def find(cls, tokens_by_line):
287 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 348 for line in tokens_by_line:
292 349 ix = 0
@@ -296,6 +353,8 b' class EscapedCommand(TokenTransformBase):'
296 353 return cls(line[ix].start)
297 354
298 355 def transform(self, lines):
356 """Transform an escaped line found by the ``find()`` classmethod.
357 """
299 358 start_line, start_col = self.start_line, self.start_col
300 359
301 360 indent = lines[start_line][:start_col]
@@ -323,6 +382,7 b' _help_end_re = re.compile(r"""(%{0,2}'
323 382 re.VERBOSE)
324 383
325 384 class HelpEnd(TokenTransformBase):
385 """Transformer for help syntax: obj? and obj??"""
326 386 # This needs to be higher priority (lower number) than EscapedCommand so
327 387 # that inspecting magics (%foo?) works.
328 388 priority = 5
@@ -334,6 +394,8 b' class HelpEnd(TokenTransformBase):'
334 394
335 395 @classmethod
336 396 def find(cls, tokens_by_line):
397 """Find the first help command (foo?) in the cell.
398 """
337 399 for line in tokens_by_line:
338 400 # Last token is NEWLINE; look at last but one
339 401 if len(line) > 2 and line[-2].string == '?':
@@ -344,6 +406,8 b' class HelpEnd(TokenTransformBase):'
344 406 return cls(line[ix].start, line[-2].start)
345 407
346 408 def transform(self, lines):
409 """Transform a help command found by the ``find()`` classmethod.
410 """
347 411 piece = ''.join(lines[self.start_line:self.q_line+1])
348 412 indent, content = piece[:self.start_col], piece[self.start_col:]
349 413 lines_before = lines[:self.start_line]
@@ -396,7 +460,7 b' def make_tokens_by_line(lines):'
396 460 return tokens_by_line
397 461
398 462 def show_linewise_tokens(s: str):
399 """For investigation"""
463 """For investigation and debugging"""
400 464 if not s.endswith('\n'):
401 465 s += '\n'
402 466 lines = s.splitlines(keepends=True)
@@ -409,6 +473,11 b' def show_linewise_tokens(s: str):'
409 473 TRANSFORM_LOOP_LIMIT = 500
410 474
411 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 481 def __init__(self):
413 482 self.cleanup_transforms = [
414 483 leading_indent,
@@ -462,7 +531,8 b' class TransformerManager:'
462 531 raise RuntimeError("Input transformation still changing after "
463 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 536 if not cell.endswith('\n'):
467 537 cell += '\n' # Ensure the cell has a trailing newline
468 538 lines = cell.splitlines(keepends=True)
@@ -2807,6 +2807,18 b' class InteractiveShell(SingletonConfigurable):'
2807 2807 return result
2808 2808
2809 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 2822 # Static input transformations
2811 2823 cell = self.input_transformer_manager.transform_cell(raw_cell)
2812 2824
@@ -1,9 +1,9 b''
1 1 """Tests for the token-based transformers in IPython.core.inputtransformer2
2 2
3 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 7 import nose.tools as nt
8 8
9 9 from IPython.core import inputtransformer2 as ipt2
@@ -1,7 +1,7 b''
1 1 """Tests for the line-based transformers in IPython.core.inputtransformer2
2 2
3 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 6 import nose.tools as nt
7 7
General Comments 0
You need to be logged in to leave comments. Login now