|
@@
-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)
|