##// END OF EJS Templates
Transformations for 'help?' syntax
Thomas Kluyver -
Show More
@@ -74,12 +74,6 b' line_transforms = ['
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)
@@ -114,9 +108,23 b' def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):'
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.
@@ -127,15 +135,12 b' class MagicAssign:'
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)
@@ -150,9 +155,9 b' class MagicAssign:'
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.
@@ -166,17 +171,15 b' class SystemAssign:'
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)
@@ -271,9 +274,9 b" tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,"
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.
@@ -283,12 +286,10 b' class EscapedCommand:'
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)
@@ -306,6 +307,57 b' class EscapedCommand:'
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__):
@@ -330,6 +382,8 b' class TokenTransformers:'
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):
@@ -348,17 +402,17 b' class TokenTransformers:'
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:
@@ -366,9 +420,6 b' class TokenTransformers:'
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'):
@@ -8,7 +8,7 b' 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()
@@ -17,7 +17,7 b' g()'
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))
@@ -27,7 +27,7 b' 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()
@@ -38,27 +38,78 b' 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)
@@ -66,58 +117,63 b' def test_continued_line():'
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