##// END OF EJS Templates
Transformations for 'help?' syntax
Thomas Kluyver -
Show More
@@ -1,383 +1,434 b''
1 import re
1 import re
2 from typing import List, Tuple
2 from typing import List, Tuple
3 from IPython.utils import tokenize2
3 from IPython.utils import tokenize2
4 from IPython.utils.tokenutil import generate_tokens
4 from IPython.utils.tokenutil import generate_tokens
5
5
6 def leading_indent(lines):
6 def leading_indent(lines):
7 """Remove leading indentation.
7 """Remove leading indentation.
8
8
9 If the first line starts with a spaces or tabs, the same whitespace will be
9 If the first line starts with a spaces or tabs, the same whitespace will be
10 removed from each following line.
10 removed from each following line.
11 """
11 """
12 m = re.match(r'^[ \t]+', lines[0])
12 m = re.match(r'^[ \t]+', lines[0])
13 if not m:
13 if not m:
14 return lines
14 return lines
15 space = m.group(0)
15 space = m.group(0)
16 n = len(space)
16 n = len(space)
17 return [l[n:] if l.startswith(space) else l
17 return [l[n:] if l.startswith(space) else l
18 for l in lines]
18 for l in lines]
19
19
20 class PromptStripper:
20 class PromptStripper:
21 """Remove matching input prompts from a block of input.
21 """Remove matching input prompts from a block of input.
22
22
23 Parameters
23 Parameters
24 ----------
24 ----------
25 prompt_re : regular expression
25 prompt_re : regular expression
26 A regular expression matching any input prompt (including continuation)
26 A regular expression matching any input prompt (including continuation)
27 initial_re : regular expression, optional
27 initial_re : regular expression, optional
28 A regular expression matching only the initial prompt, but not continuation.
28 A regular expression matching only the initial prompt, but not continuation.
29 If no initial expression is given, prompt_re will be used everywhere.
29 If no initial expression is given, prompt_re will be used everywhere.
30 Used mainly for plain Python prompts, where the continuation prompt
30 Used mainly for plain Python prompts, where the continuation prompt
31 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
31 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
32
32
33 If initial_re and prompt_re differ,
33 If initial_re and prompt_re differ,
34 only initial_re will be tested against the first line.
34 only initial_re will be tested against the first line.
35 If any prompt is found on the first two lines,
35 If any prompt is found on the first two lines,
36 prompts will be stripped from the rest of the block.
36 prompts will be stripped from the rest of the block.
37 """
37 """
38 def __init__(self, prompt_re, initial_re=None):
38 def __init__(self, prompt_re, initial_re=None):
39 self.prompt_re = prompt_re
39 self.prompt_re = prompt_re
40 self.initial_re = initial_re or prompt_re
40 self.initial_re = initial_re or prompt_re
41
41
42 def _strip(self, lines):
42 def _strip(self, lines):
43 return [self.prompt_re.sub('', l, count=1) for l in lines]
43 return [self.prompt_re.sub('', l, count=1) for l in lines]
44
44
45 def __call__(self, lines):
45 def __call__(self, lines):
46 if self.initial_re.match(lines[0]) or \
46 if self.initial_re.match(lines[0]) or \
47 (len(lines) > 1 and self.prompt_re.match(lines[1])):
47 (len(lines) > 1 and self.prompt_re.match(lines[1])):
48 return self._strip(lines)
48 return self._strip(lines)
49 return lines
49 return lines
50
50
51 classic_prompt = PromptStripper(
51 classic_prompt = PromptStripper(
52 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
52 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
53 initial_re=re.compile(r'^>>>( |$)')
53 initial_re=re.compile(r'^>>>( |$)')
54 )
54 )
55
55
56 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
56 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
57
57
58 def cell_magic(lines):
58 def cell_magic(lines):
59 if not lines[0].startswith('%%'):
59 if not lines[0].startswith('%%'):
60 return lines
60 return lines
61 if re.match('%%\w+\?', lines[0]):
61 if re.match('%%\w+\?', lines[0]):
62 # This case will be handled by help_end
62 # This case will be handled by help_end
63 return lines
63 return lines
64 magic_name, first_line = lines[0][2:].partition(' ')
64 magic_name, first_line = lines[0][2:].partition(' ')
65 body = '\n'.join(lines[1:])
65 body = '\n'.join(lines[1:])
66 return ['get_ipython().run_cell_magic(%r, %r, %r)' % (magic_name, first_line, body)]
66 return ['get_ipython().run_cell_magic(%r, %r, %r)' % (magic_name, first_line, body)]
67
67
68 line_transforms = [
68 line_transforms = [
69 leading_indent,
69 leading_indent,
70 classic_prompt,
70 classic_prompt,
71 ipython_prompt,
71 ipython_prompt,
72 cell_magic,
72 cell_magic,
73 ]
73 ]
74
74
75 # -----
75 # -----
76
76
77 def help_end(tokens_by_line):
78 pass
79
80 def escaped_command(tokens_by_line):
81 pass
82
83 def _find_assign_op(token_line):
77 def _find_assign_op(token_line):
84 # Find the first assignment in the line ('=' not inside brackets)
78 # Find the first assignment in the line ('=' not inside brackets)
85 # We don't try to support multiple special assignment (a = b = %foo)
79 # We don't try to support multiple special assignment (a = b = %foo)
86 paren_level = 0
80 paren_level = 0
87 for i, ti in enumerate(token_line):
81 for i, ti in enumerate(token_line):
88 s = ti.string
82 s = ti.string
89 if s == '=' and paren_level == 0:
83 if s == '=' and paren_level == 0:
90 return i
84 return i
91 if s in '([{':
85 if s in '([{':
92 paren_level += 1
86 paren_level += 1
93 elif s in ')]}':
87 elif s in ')]}':
94 paren_level -= 1
88 paren_level -= 1
95
89
96 def find_end_of_continued_line(lines, start_line: int):
90 def find_end_of_continued_line(lines, start_line: int):
97 """Find the last line of a line explicitly extended using backslashes.
91 """Find the last line of a line explicitly extended using backslashes.
98
92
99 Uses 0-indexed line numbers.
93 Uses 0-indexed line numbers.
100 """
94 """
101 end_line = start_line
95 end_line = start_line
102 while lines[end_line].endswith('\\\n'):
96 while lines[end_line].endswith('\\\n'):
103 end_line += 1
97 end_line += 1
104 if end_line >= len(lines):
98 if end_line >= len(lines):
105 break
99 break
106 return end_line
100 return end_line
107
101
108 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
102 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
109 """Assemble pieces of a continued line into a single line.
103 """Assemble pieces of a continued line into a single line.
110
104
111 Uses 0-indexed line numbers. *start* is (lineno, colno).
105 Uses 0-indexed line numbers. *start* is (lineno, colno).
112 """
106 """
113 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
107 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
114 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
108 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
115 + [parts[-1][:-1]]) # Strip newline from last line
109 + [parts[-1][:-1]]) # Strip newline from last line
116
110
117 class MagicAssign:
111 class TokenTransformBase:
118 @staticmethod
112 # Lower numbers -> higher priority (for matches in the same location)
119 def find(tokens_by_line):
113 priority = 10
114
115 def sortby(self):
116 return self.start_line, self.start_col, self.priority
117
118 def __init__(self, start):
119 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
120 self.start_col = start[1]
121
122 def transform(self, lines: List[str]):
123 raise NotImplementedError
124
125 class MagicAssign(TokenTransformBase):
126 @classmethod
127 def find(cls, tokens_by_line):
120 """Find the first magic assignment (a = %foo) in the cell.
128 """Find the first magic assignment (a = %foo) in the cell.
121
129
122 Returns (line, column) of the % if found, or None. *line* is 1-indexed.
130 Returns (line, column) of the % if found, or None. *line* is 1-indexed.
123 """
131 """
124 for line in tokens_by_line:
132 for line in tokens_by_line:
125 assign_ix = _find_assign_op(line)
133 assign_ix = _find_assign_op(line)
126 if (assign_ix is not None) \
134 if (assign_ix is not None) \
127 and (len(line) >= assign_ix + 2) \
135 and (len(line) >= assign_ix + 2) \
128 and (line[assign_ix+1].string == '%') \
136 and (line[assign_ix+1].string == '%') \
129 and (line[assign_ix+2].type == tokenize2.NAME):
137 and (line[assign_ix+2].type == tokenize2.NAME):
130 return line[assign_ix+1].start
138 return cls(line[assign_ix+1].start)
131
139
132 @staticmethod
140 def transform(self, lines: List[str]):
133 def transform(lines: List[str], start: Tuple[int, int]):
134 """Transform a magic assignment found by find
141 """Transform a magic assignment found by find
135 """
142 """
136 start_line = start[0] - 1 # Shift from 1-index to 0-index
143 start_line, start_col = self.start_line, self.start_col
137 start_col = start[1]
138
139 lhs = lines[start_line][:start_col]
144 lhs = lines[start_line][:start_col]
140 end_line = find_end_of_continued_line(lines, start_line)
145 end_line = find_end_of_continued_line(lines, start_line)
141 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
146 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
142 assert rhs.startswith('%'), rhs
147 assert rhs.startswith('%'), rhs
143 magic_name, _, args = rhs[1:].partition(' ')
148 magic_name, _, args = rhs[1:].partition(' ')
144
149
145 lines_before = lines[:start_line]
150 lines_before = lines[:start_line]
146 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
151 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
147 new_line = lhs + call + '\n'
152 new_line = lhs + call + '\n'
148 lines_after = lines[end_line+1:]
153 lines_after = lines[end_line+1:]
149
154
150 return lines_before + [new_line] + lines_after
155 return lines_before + [new_line] + lines_after
151
156
152
157
153 class SystemAssign:
158 class SystemAssign(TokenTransformBase):
154 @staticmethod
159 @classmethod
155 def find(tokens_by_line):
160 def find(cls, tokens_by_line):
156 """Find the first system assignment (a = !foo) in the cell.
161 """Find the first system assignment (a = !foo) in the cell.
157
162
158 Returns (line, column) of the ! if found, or None. *line* is 1-indexed.
163 Returns (line, column) of the ! if found, or None. *line* is 1-indexed.
159 """
164 """
160 for line in tokens_by_line:
165 for line in tokens_by_line:
161 assign_ix = _find_assign_op(line)
166 assign_ix = _find_assign_op(line)
162 if (assign_ix is not None) \
167 if (assign_ix is not None) \
163 and (len(line) >= assign_ix + 2) \
168 and (len(line) >= assign_ix + 2) \
164 and (line[assign_ix + 1].type == tokenize2.ERRORTOKEN):
169 and (line[assign_ix + 1].type == tokenize2.ERRORTOKEN):
165 ix = assign_ix + 1
170 ix = assign_ix + 1
166
171
167 while ix < len(line) and line[ix].type == tokenize2.ERRORTOKEN:
172 while ix < len(line) and line[ix].type == tokenize2.ERRORTOKEN:
168 if line[ix].string == '!':
173 if line[ix].string == '!':
169 return line[ix].start
174 return cls(line[ix].start)
170 elif not line[ix].string.isspace():
175 elif not line[ix].string.isspace():
171 break
176 break
172 ix += 1
177 ix += 1
173
178
174 @staticmethod
179 def transform(self, lines: List[str]):
175 def transform(lines: List[str], start: Tuple[int, int]):
176 """Transform a system assignment found by find
180 """Transform a system assignment found by find
177 """
181 """
178 start_line = start[0] - 1 # Shift from 1-index to 0-index
182 start_line, start_col = self.start_line, self.start_col
179 start_col = start[1]
180
183
181 lhs = lines[start_line][:start_col]
184 lhs = lines[start_line][:start_col]
182 end_line = find_end_of_continued_line(lines, start_line)
185 end_line = find_end_of_continued_line(lines, start_line)
183 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
186 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
184 assert rhs.startswith('!'), rhs
187 assert rhs.startswith('!'), rhs
185 cmd = rhs[1:]
188 cmd = rhs[1:]
186
189
187 lines_before = lines[:start_line]
190 lines_before = lines[:start_line]
188 call = "get_ipython().getoutput({!r})".format(cmd)
191 call = "get_ipython().getoutput({!r})".format(cmd)
189 new_line = lhs + call + '\n'
192 new_line = lhs + call + '\n'
190 lines_after = lines[end_line + 1:]
193 lines_after = lines[end_line + 1:]
191
194
192 return lines_before + [new_line] + lines_after
195 return lines_before + [new_line] + lines_after
193
196
194 # The escape sequences that define the syntax transformations IPython will
197 # The escape sequences that define the syntax transformations IPython will
195 # apply to user input. These can NOT be just changed here: many regular
198 # apply to user input. These can NOT be just changed here: many regular
196 # expressions and other parts of the code may use their hardcoded values, and
199 # expressions and other parts of the code may use their hardcoded values, and
197 # for all intents and purposes they constitute the 'IPython syntax', so they
200 # for all intents and purposes they constitute the 'IPython syntax', so they
198 # should be considered fixed.
201 # should be considered fixed.
199
202
200 ESC_SHELL = '!' # Send line to underlying system shell
203 ESC_SHELL = '!' # Send line to underlying system shell
201 ESC_SH_CAP = '!!' # Send line to system shell and capture output
204 ESC_SH_CAP = '!!' # Send line to system shell and capture output
202 ESC_HELP = '?' # Find information about object
205 ESC_HELP = '?' # Find information about object
203 ESC_HELP2 = '??' # Find extra-detailed information about object
206 ESC_HELP2 = '??' # Find extra-detailed information about object
204 ESC_MAGIC = '%' # Call magic function
207 ESC_MAGIC = '%' # Call magic function
205 ESC_MAGIC2 = '%%' # Call cell-magic function
208 ESC_MAGIC2 = '%%' # Call cell-magic function
206 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
209 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
207 ESC_QUOTE2 = ';' # Quote all args as a single string, call
210 ESC_QUOTE2 = ';' # Quote all args as a single string, call
208 ESC_PAREN = '/' # Call first argument with rest of line as arguments
211 ESC_PAREN = '/' # Call first argument with rest of line as arguments
209
212
210 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
213 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
211 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
214 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
212
215
213 def _make_help_call(target, esc, next_input=None):
216 def _make_help_call(target, esc, next_input=None):
214 """Prepares a pinfo(2)/psearch call from a target name and the escape
217 """Prepares a pinfo(2)/psearch call from a target name and the escape
215 (i.e. ? or ??)"""
218 (i.e. ? or ??)"""
216 method = 'pinfo2' if esc == '??' \
219 method = 'pinfo2' if esc == '??' \
217 else 'psearch' if '*' in target \
220 else 'psearch' if '*' in target \
218 else 'pinfo'
221 else 'pinfo'
219 arg = " ".join([method, target])
222 arg = " ".join([method, target])
220 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
223 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
221 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
224 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
222 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
225 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
223 if next_input is None:
226 if next_input is None:
224 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
227 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
225 else:
228 else:
226 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
229 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
227 (next_input, t_magic_name, t_magic_arg_s)
230 (next_input, t_magic_name, t_magic_arg_s)
228
231
229 def _tr_help(content):
232 def _tr_help(content):
230 "Translate lines escaped with: ?"
233 "Translate lines escaped with: ?"
231 # A naked help line should just fire the intro help screen
234 # A naked help line should just fire the intro help screen
232 if not content:
235 if not content:
233 return 'get_ipython().show_usage()'
236 return 'get_ipython().show_usage()'
234
237
235 return _make_help_call(content, '?')
238 return _make_help_call(content, '?')
236
239
237 def _tr_help2(content):
240 def _tr_help2(content):
238 "Translate lines escaped with: ??"
241 "Translate lines escaped with: ??"
239 # A naked help line should just fire the intro help screen
242 # A naked help line should just fire the intro help screen
240 if not content:
243 if not content:
241 return 'get_ipython().show_usage()'
244 return 'get_ipython().show_usage()'
242
245
243 return _make_help_call(content, '??')
246 return _make_help_call(content, '??')
244
247
245 def _tr_magic(content):
248 def _tr_magic(content):
246 "Translate lines escaped with: %"
249 "Translate lines escaped with: %"
247 name, _, args = content.partition(' ')
250 name, _, args = content.partition(' ')
248 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
251 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
249
252
250 def _tr_quote(content):
253 def _tr_quote(content):
251 "Translate lines escaped with: ,"
254 "Translate lines escaped with: ,"
252 name, _, args = content.partition(' ')
255 name, _, args = content.partition(' ')
253 return '%s("%s")' % (name, '", "'.join(args.split()) )
256 return '%s("%s")' % (name, '", "'.join(args.split()) )
254
257
255 def _tr_quote2(content):
258 def _tr_quote2(content):
256 "Translate lines escaped with: ;"
259 "Translate lines escaped with: ;"
257 name, _, args = content.partition(' ')
260 name, _, args = content.partition(' ')
258 return '%s("%s")' % (name, args)
261 return '%s("%s")' % (name, args)
259
262
260 def _tr_paren(content):
263 def _tr_paren(content):
261 "Translate lines escaped with: /"
264 "Translate lines escaped with: /"
262 name, _, args = content.partition(' ')
265 name, _, args = content.partition(' ')
263 return '%s(%s)' % (name, ", ".join(args.split()))
266 return '%s(%s)' % (name, ", ".join(args.split()))
264
267
265 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
268 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
266 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
269 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
267 ESC_HELP : _tr_help,
270 ESC_HELP : _tr_help,
268 ESC_HELP2 : _tr_help2,
271 ESC_HELP2 : _tr_help2,
269 ESC_MAGIC : _tr_magic,
272 ESC_MAGIC : _tr_magic,
270 ESC_QUOTE : _tr_quote,
273 ESC_QUOTE : _tr_quote,
271 ESC_QUOTE2 : _tr_quote2,
274 ESC_QUOTE2 : _tr_quote2,
272 ESC_PAREN : _tr_paren }
275 ESC_PAREN : _tr_paren }
273
276
274 class EscapedCommand:
277 class EscapedCommand(TokenTransformBase):
275 @staticmethod
278 @classmethod
276 def find(tokens_by_line):
279 def find(cls, tokens_by_line):
277 """Find the first escaped command (%foo, !foo, etc.) in the cell.
280 """Find the first escaped command (%foo, !foo, etc.) in the cell.
278
281
279 Returns (line, column) of the escape if found, or None. *line* is 1-indexed.
282 Returns (line, column) of the escape if found, or None. *line* is 1-indexed.
280 """
283 """
281 for line in tokens_by_line:
284 for line in tokens_by_line:
282 ix = 0
285 ix = 0
283 while line[ix].type in {tokenize2.INDENT, tokenize2.DEDENT}:
286 while line[ix].type in {tokenize2.INDENT, tokenize2.DEDENT}:
284 ix += 1
287 ix += 1
285 if line[ix].string in ESCAPE_SINGLES:
288 if line[ix].string in ESCAPE_SINGLES:
286 return line[ix].start
289 return cls(line[ix].start)
287
290
288 @staticmethod
291 def transform(self, lines):
289 def transform(lines, start):
292 start_line, start_col = self.start_line, self.start_col
290 start_line = start[0] - 1 # Shift from 1-index to 0-index
291 start_col = start[1]
292
293
293 indent = lines[start_line][:start_col]
294 indent = lines[start_line][:start_col]
294 end_line = find_end_of_continued_line(lines, start_line)
295 end_line = find_end_of_continued_line(lines, start_line)
295 line = assemble_continued_line(lines, (start_line, start_col), end_line)
296 line = assemble_continued_line(lines, (start_line, start_col), end_line)
296
297
297 if line[:2] in ESCAPE_DOUBLES:
298 if line[:2] in ESCAPE_DOUBLES:
298 escape, content = line[:2], line[2:]
299 escape, content = line[:2], line[2:]
299 else:
300 else:
300 escape, content = line[:1], line[1:]
301 escape, content = line[:1], line[1:]
301 call = tr[escape](content)
302 call = tr[escape](content)
302
303
303 lines_before = lines[:start_line]
304 lines_before = lines[:start_line]
304 new_line = indent + call + '\n'
305 new_line = indent + call + '\n'
305 lines_after = lines[end_line + 1:]
306 lines_after = lines[end_line + 1:]
306
307
307 return lines_before + [new_line] + lines_after
308 return lines_before + [new_line] + lines_after
308
309
310 _help_end_re = re.compile(r"""(%{0,2}
311 [a-zA-Z_*][\w*]* # Variable name
312 (\.[a-zA-Z_*][\w*]*)* # .etc.etc
313 )
314 (\?\??)$ # ? or ??
315 """,
316 re.VERBOSE)
317
318 class HelpEnd(TokenTransformBase):
319 # This needs to be higher priority (lower number) than EscapedCommand so
320 # that inspecting magics (%foo?) works.
321 priority = 5
322
323 def __init__(self, start, q_locn):
324 super().__init__(start)
325 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
326 self.q_col = q_locn[1]
327
328 @classmethod
329 def find(cls, tokens_by_line):
330 for line in tokens_by_line:
331 # Last token is NEWLINE; look at last but one
332 if len(line) > 2 and line[-2].string == '?':
333 # Find the first token that's not INDENT/DEDENT
334 ix = 0
335 while line[ix].type in {tokenize2.INDENT, tokenize2.DEDENT}:
336 ix += 1
337 return cls(line[ix].start, line[-2].start)
338
339 def transform(self, lines):
340 piece = ''.join(lines[self.start_line:self.q_line+1])
341 indent, content = piece[:self.start_col], piece[self.start_col:]
342 lines_before = lines[:self.start_line]
343 lines_after = lines[self.q_line + 1:]
344
345 m = _help_end_re.search(content)
346 assert m is not None, content
347 target = m.group(1)
348 esc = m.group(3)
349
350 # If we're mid-command, put it back on the next prompt for the user.
351 next_input = None
352 if (not lines_before) and (not lines_after) \
353 and content.strip() != m.group(0):
354 next_input = content.rstrip('?\n')
355
356 call = _make_help_call(target, esc, next_input=next_input)
357 new_line = indent + call + '\n'
358
359 return lines_before + [new_line] + lines_after
360
309 def make_tokens_by_line(lines):
361 def make_tokens_by_line(lines):
310 tokens_by_line = [[]]
362 tokens_by_line = [[]]
311 for token in generate_tokens(iter(lines).__next__):
363 for token in generate_tokens(iter(lines).__next__):
312 tokens_by_line[-1].append(token)
364 tokens_by_line[-1].append(token)
313 if token.type == tokenize2.NEWLINE:
365 if token.type == tokenize2.NEWLINE:
314 tokens_by_line.append([])
366 tokens_by_line.append([])
315
367
316 return tokens_by_line
368 return tokens_by_line
317
369
318 def show_linewise_tokens(s: str):
370 def show_linewise_tokens(s: str):
319 """For investigation"""
371 """For investigation"""
320 if not s.endswith('\n'):
372 if not s.endswith('\n'):
321 s += '\n'
373 s += '\n'
322 lines = s.splitlines(keepends=True)
374 lines = s.splitlines(keepends=True)
323 for line in make_tokens_by_line(lines):
375 for line in make_tokens_by_line(lines):
324 print("Line -------")
376 print("Line -------")
325 for tokinfo in line:
377 for tokinfo in line:
326 print(" ", tokinfo)
378 print(" ", tokinfo)
327
379
328 class TokenTransformers:
380 class TokenTransformers:
329 def __init__(self):
381 def __init__(self):
330 self.transformers = [
382 self.transformers = [
331 MagicAssign,
383 MagicAssign,
332 SystemAssign,
384 SystemAssign,
385 EscapedCommand,
386 HelpEnd,
333 ]
387 ]
334
388
335 def do_one_transform(self, lines):
389 def do_one_transform(self, lines):
336 """Find and run the transform earliest in the code.
390 """Find and run the transform earliest in the code.
337
391
338 Returns (changed, lines).
392 Returns (changed, lines).
339
393
340 This method is called repeatedly until changed is False, indicating
394 This method is called repeatedly until changed is False, indicating
341 that all available transformations are complete.
395 that all available transformations are complete.
342
396
343 The tokens following IPython special syntax might not be valid, so
397 The tokens following IPython special syntax might not be valid, so
344 the transformed code is retokenised every time to identify the next
398 the transformed code is retokenised every time to identify the next
345 piece of special syntax. Hopefully long code cells are mostly valid
399 piece of special syntax. Hopefully long code cells are mostly valid
346 Python, not using lots of IPython special syntax, so this shouldn't be
400 Python, not using lots of IPython special syntax, so this shouldn't be
347 a performance issue.
401 a performance issue.
348 """
402 """
349 tokens_by_line = make_tokens_by_line(lines)
403 tokens_by_line = make_tokens_by_line(lines)
350 candidates = []
404 candidates = []
351 for transformer in self.transformers:
405 for transformer_cls in self.transformers:
352 locn = transformer.find(tokens_by_line)
406 transformer = transformer_cls.find(tokens_by_line)
353 if locn:
407 if transformer:
354 candidates.append((locn, transformer))
408 candidates.append(transformer)
355
409
356 if not candidates:
410 if not candidates:
357 # Nothing to transform
411 # Nothing to transform
358 return False, lines
412 return False, lines
359
413
360 first_locn, transformer = min(candidates)
414 transformer = min(candidates, key=TokenTransformBase.sortby)
361 return True, transformer.transform(lines, first_locn)
415 return True, transformer.transform(lines)
362
416
363 def __call__(self, lines):
417 def __call__(self, lines):
364 while True:
418 while True:
365 changed, lines = self.do_one_transform(lines)
419 changed, lines = self.do_one_transform(lines)
366 if not changed:
420 if not changed:
367 return lines
421 return lines
368
422
369 def assign_from_system(tokens_by_line, lines):
370 pass
371
372
423
373 def transform_cell(cell):
424 def transform_cell(cell):
374 if not cell.endswith('\n'):
425 if not cell.endswith('\n'):
375 cell += '\n' # Ensure every line has a newline
426 cell += '\n' # Ensure every line has a newline
376 lines = cell.splitlines(keepends=True)
427 lines = cell.splitlines(keepends=True)
377 for transform in line_transforms:
428 for transform in line_transforms:
378 #print(transform, lines)
429 #print(transform, lines)
379 lines = transform(lines)
430 lines = transform(lines)
380
431
381 lines = TokenTransformers()(lines)
432 lines = TokenTransformers()(lines)
382 for line in lines:
433 for line in lines:
383 print('~~', line)
434 print('~~', line)
@@ -1,123 +1,179 b''
1 import nose.tools as nt
1 import nose.tools as nt
2
2
3 from IPython.core import inputtransformer2 as ipt2
3 from IPython.core import inputtransformer2 as ipt2
4 from IPython.core.inputtransformer2 import make_tokens_by_line
4 from IPython.core.inputtransformer2 import make_tokens_by_line
5
5
6 MULTILINE_MAGIC = ("""\
6 MULTILINE_MAGIC = ("""\
7 a = f()
7 a = f()
8 %foo \\
8 %foo \\
9 bar
9 bar
10 g()
10 g()
11 """.splitlines(keepends=True), """\
11 """.splitlines(keepends=True), (2, 0), """\
12 a = f()
12 a = f()
13 get_ipython().run_line_magic('foo', ' bar')
13 get_ipython().run_line_magic('foo', ' bar')
14 g()
14 g()
15 """.splitlines(keepends=True))
15 """.splitlines(keepends=True))
16
16
17 INDENTED_MAGIC = ("""\
17 INDENTED_MAGIC = ("""\
18 for a in range(5):
18 for a in range(5):
19 %ls
19 %ls
20 """.splitlines(keepends=True), """\
20 """.splitlines(keepends=True), (2, 4), """\
21 for a in range(5):
21 for a in range(5):
22 get_ipython().run_line_magic('ls', '')
22 get_ipython().run_line_magic('ls', '')
23 """.splitlines(keepends=True))
23 """.splitlines(keepends=True))
24
24
25 MULTILINE_MAGIC_ASSIGN = ("""\
25 MULTILINE_MAGIC_ASSIGN = ("""\
26 a = f()
26 a = f()
27 b = %foo \\
27 b = %foo \\
28 bar
28 bar
29 g()
29 g()
30 """.splitlines(keepends=True), """\
30 """.splitlines(keepends=True), (2, 4), """\
31 a = f()
31 a = f()
32 b = get_ipython().run_line_magic('foo', ' bar')
32 b = get_ipython().run_line_magic('foo', ' bar')
33 g()
33 g()
34 """.splitlines(keepends=True))
34 """.splitlines(keepends=True))
35
35
36 MULTILINE_SYSTEM_ASSIGN = ("""\
36 MULTILINE_SYSTEM_ASSIGN = ("""\
37 a = f()
37 a = f()
38 b = !foo \\
38 b = !foo \\
39 bar
39 bar
40 g()
40 g()
41 """.splitlines(keepends=True), """\
41 """.splitlines(keepends=True), (2, 4), """\
42 a = f()
42 a = f()
43 b = get_ipython().getoutput('foo bar')
43 b = get_ipython().getoutput('foo bar')
44 g()
44 g()
45 """.splitlines(keepends=True))
45 """.splitlines(keepends=True))
46
46
47 AUTOCALL_QUOTE = (
47 AUTOCALL_QUOTE = (
48 [",f 1 2 3\n"],
48 [",f 1 2 3\n"], (1, 0),
49 ['f("1", "2", "3")\n']
49 ['f("1", "2", "3")\n']
50 )
50 )
51
51
52 AUTOCALL_QUOTE2 = (
52 AUTOCALL_QUOTE2 = (
53 [";f 1 2 3\n"],
53 [";f 1 2 3\n"], (1, 0),
54 ['f("1 2 3")\n']
54 ['f("1 2 3")\n']
55 )
55 )
56
56
57 AUTOCALL_PAREN = (
57 AUTOCALL_PAREN = (
58 ["/f 1 2 3\n"],
58 ["/f 1 2 3\n"], (1, 0),
59 ['f(1, 2, 3)\n']
59 ['f(1, 2, 3)\n']
60 )
60 )
61
61
62 SIMPLE_HELP = (
63 ["foo?\n"], (1, 0),
64 ["get_ipython().run_line_magic('pinfo', 'foo')\n"]
65 )
66
67 DETAILED_HELP = (
68 ["foo??\n"], (1, 0),
69 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"]
70 )
71
72 MAGIC_HELP = (
73 ["%foo?\n"], (1, 0),
74 ["get_ipython().run_line_magic('pinfo', '%foo')\n"]
75 )
76
77 HELP_IN_EXPR = (
78 ["a = b + c?\n"], (1, 0),
79 ["get_ipython().set_next_input('a = b + c');"
80 "get_ipython().run_line_magic('pinfo', 'c')\n"]
81 )
82
83 HELP_CONTINUED_LINE = ("""\
84 a = \\
85 zip?
86 """.splitlines(keepends=True), (1, 0),
87 [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
88 )
89
90 HELP_MULTILINE = ("""\
91 (a,
92 b) = zip?
93 """.splitlines(keepends=True), (1, 0),
94 [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
95 )
96
97 def check_find(transformer, case, match=True):
98 sample, expected_start, _ = case
99 tbl = make_tokens_by_line(sample)
100 res = transformer.find(tbl)
101 if match:
102 # start_line is stored 0-indexed, expected values are 1-indexed
103 nt.assert_equal((res.start_line+1, res.start_col), expected_start)
104 return res
105 else:
106 nt.assert_is(res, None)
107
108 def check_transform(transformer_cls, case):
109 lines, start, expected = case
110 transformer = transformer_cls(start)
111 nt.assert_equal(transformer.transform(lines), expected)
112
62 def test_continued_line():
113 def test_continued_line():
63 lines = MULTILINE_MAGIC_ASSIGN[0]
114 lines = MULTILINE_MAGIC_ASSIGN[0]
64 nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2)
115 nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2)
65
116
66 nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar")
117 nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar")
67
118
68 def test_find_assign_magic():
119 def test_find_assign_magic():
69 tbl = make_tokens_by_line(MULTILINE_MAGIC_ASSIGN[0])
120 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
70 nt.assert_equal(ipt2.MagicAssign.find(tbl), (2, 4))
121 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
71
72 tbl = make_tokens_by_line(MULTILINE_SYSTEM_ASSIGN[0]) # Nothing to find
73 nt.assert_equal(ipt2.MagicAssign.find(tbl), None)
74
122
75 def test_transform_assign_magic():
123 def test_transform_assign_magic():
76 res = ipt2.MagicAssign.transform(MULTILINE_MAGIC_ASSIGN[0], (2, 4))
124 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
77 nt.assert_equal(res, MULTILINE_MAGIC_ASSIGN[1])
78
125
79 def test_find_assign_system():
126 def test_find_assign_system():
80 tbl = make_tokens_by_line(MULTILINE_SYSTEM_ASSIGN[0])
127 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
81 nt.assert_equal(ipt2.SystemAssign.find(tbl), (2, 4))
128 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
129 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
130 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
82
131
83 tbl = make_tokens_by_line(["a = !ls\n"])
132 def test_transform_assign_system():
84 nt.assert_equal(ipt2.SystemAssign.find(tbl), (1, 5))
133 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
85
134
86 tbl = make_tokens_by_line(["a=!ls\n"])
135 def test_find_magic_escape():
87 nt.assert_equal(ipt2.SystemAssign.find(tbl), (1, 2))
136 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
137 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
138 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
88
139
89 tbl = make_tokens_by_line(MULTILINE_MAGIC_ASSIGN[0]) # Nothing to find
140 def test_transform_magic_escape():
90 nt.assert_equal(ipt2.SystemAssign.find(tbl), None)
141 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
142 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
91
143
92 def test_transform_assign_system():
144 def test_find_autocalls():
93 res = ipt2.SystemAssign.transform(MULTILINE_SYSTEM_ASSIGN[0], (2, 4))
145 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
94 nt.assert_equal(res, MULTILINE_SYSTEM_ASSIGN[1])
146 print("Testing %r" % case[0])
147 check_find(ipt2.EscapedCommand, case)
95
148
96 def test_find_magic_escape():
149 def test_transform_autocall():
97 tbl = make_tokens_by_line(MULTILINE_MAGIC[0])
150 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
98 nt.assert_equal(ipt2.EscapedCommand.find(tbl), (2, 0))
151 print("Testing %r" % case[0])
152 check_transform(ipt2.EscapedCommand, case)
99
153
100 tbl = make_tokens_by_line(INDENTED_MAGIC[0])
154 def test_find_help():
101 nt.assert_equal(ipt2.EscapedCommand.find(tbl), (2, 4))
155 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
156 check_find(ipt2.HelpEnd, case)
102
157
103 tbl = make_tokens_by_line(MULTILINE_MAGIC_ASSIGN[0]) # Shouldn't find a = %foo
158 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
104 nt.assert_equal(ipt2.EscapedCommand.find(tbl), None)
159 nt.assert_equal(tf.q_line, 1)
160 nt.assert_equal(tf.q_col, 3)
105
161
106 def test_transform_magic_escape():
162 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
107 res = ipt2.EscapedCommand.transform(MULTILINE_MAGIC[0], (2, 0))
163 nt.assert_equal(tf.q_line, 1)
108 nt.assert_equal(res, MULTILINE_MAGIC[1])
164 nt.assert_equal(tf.q_col, 8)
109
165
110 res = ipt2.EscapedCommand.transform(INDENTED_MAGIC[0], (2, 4))
166 # ? in a comment does not trigger help
111 nt.assert_equal(res, INDENTED_MAGIC[1])
167 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
168 # Nor in a string
169 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
112
170
113 def test_find_autocalls():
171 def test_transform_help():
114 for sample, _ in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
172 tf = ipt2.HelpEnd((1, 0), (1, 9))
115 print("Testing %r" % sample)
173 nt.assert_equal(tf.transform(HELP_IN_EXPR[0]), HELP_IN_EXPR[2])
116 tbl = make_tokens_by_line(sample)
117 nt.assert_equal(ipt2.EscapedCommand.find(tbl), (1, 0))
118
174
119 def test_transform_autocall():
175 tf = ipt2.HelpEnd((1, 0), (2, 3))
120 for sample, expected in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
176 nt.assert_equal(tf.transform(HELP_CONTINUED_LINE[0]), HELP_CONTINUED_LINE[2])
121 print("Testing %r" % sample)
177
122 res = ipt2.EscapedCommand.transform(sample, (1, 0))
178 tf = ipt2.HelpEnd((1, 0), (2, 8))
123 nt.assert_equal(res, expected)
179 nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2])
General Comments 0
You need to be logged in to leave comments. Login now