##// 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 77 def _find_assign_op(token_line):
84 78 # Find the first assignment in the line ('=' not inside brackets)
85 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 108 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
115 109 + [parts[-1][:-1]]) # Strip newline from last line
116 110
117 class MagicAssign:
118 @staticmethod
119 def find(tokens_by_line):
111 class TokenTransformBase:
112 # Lower numbers -> higher priority (for matches in the same location)
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 128 """Find the first magic assignment (a = %foo) in the cell.
121 129
122 130 Returns (line, column) of the % if found, or None. *line* is 1-indexed.
@@ -127,15 +135,12 b' class MagicAssign:'
127 135 and (len(line) >= assign_ix + 2) \
128 136 and (line[assign_ix+1].string == '%') \
129 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
133 def transform(lines: List[str], start: Tuple[int, int]):
140 def transform(self, lines: List[str]):
134 141 """Transform a magic assignment found by find
135 142 """
136 start_line = start[0] - 1 # Shift from 1-index to 0-index
137 start_col = start[1]
138
143 start_line, start_col = self.start_line, self.start_col
139 144 lhs = lines[start_line][:start_col]
140 145 end_line = find_end_of_continued_line(lines, start_line)
141 146 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
@@ -150,9 +155,9 b' class MagicAssign:'
150 155 return lines_before + [new_line] + lines_after
151 156
152 157
153 class SystemAssign:
154 @staticmethod
155 def find(tokens_by_line):
158 class SystemAssign(TokenTransformBase):
159 @classmethod
160 def find(cls, tokens_by_line):
156 161 """Find the first system assignment (a = !foo) in the cell.
157 162
158 163 Returns (line, column) of the ! if found, or None. *line* is 1-indexed.
@@ -166,17 +171,15 b' class SystemAssign:'
166 171
167 172 while ix < len(line) and line[ix].type == tokenize2.ERRORTOKEN:
168 173 if line[ix].string == '!':
169 return line[ix].start
174 return cls(line[ix].start)
170 175 elif not line[ix].string.isspace():
171 176 break
172 177 ix += 1
173 178
174 @staticmethod
175 def transform(lines: List[str], start: Tuple[int, int]):
179 def transform(self, lines: List[str]):
176 180 """Transform a system assignment found by find
177 181 """
178 start_line = start[0] - 1 # Shift from 1-index to 0-index
179 start_col = start[1]
182 start_line, start_col = self.start_line, self.start_col
180 183
181 184 lhs = lines[start_line][:start_col]
182 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 274 ESC_QUOTE2 : _tr_quote2,
272 275 ESC_PAREN : _tr_paren }
273 276
274 class EscapedCommand:
275 @staticmethod
276 def find(tokens_by_line):
277 class EscapedCommand(TokenTransformBase):
278 @classmethod
279 def find(cls, tokens_by_line):
277 280 """Find the first escaped command (%foo, !foo, etc.) in the cell.
278 281
279 282 Returns (line, column) of the escape if found, or None. *line* is 1-indexed.
@@ -283,12 +286,10 b' class EscapedCommand:'
283 286 while line[ix].type in {tokenize2.INDENT, tokenize2.DEDENT}:
284 287 ix += 1
285 288 if line[ix].string in ESCAPE_SINGLES:
286 return line[ix].start
289 return cls(line[ix].start)
287 290
288 @staticmethod
289 def transform(lines, start):
290 start_line = start[0] - 1 # Shift from 1-index to 0-index
291 start_col = start[1]
291 def transform(self, lines):
292 start_line, start_col = self.start_line, self.start_col
292 293
293 294 indent = lines[start_line][:start_col]
294 295 end_line = find_end_of_continued_line(lines, start_line)
@@ -306,6 +307,57 b' class EscapedCommand:'
306 307
307 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 361 def make_tokens_by_line(lines):
310 362 tokens_by_line = [[]]
311 363 for token in generate_tokens(iter(lines).__next__):
@@ -330,6 +382,8 b' class TokenTransformers:'
330 382 self.transformers = [
331 383 MagicAssign,
332 384 SystemAssign,
385 EscapedCommand,
386 HelpEnd,
333 387 ]
334 388
335 389 def do_one_transform(self, lines):
@@ -348,17 +402,17 b' class TokenTransformers:'
348 402 """
349 403 tokens_by_line = make_tokens_by_line(lines)
350 404 candidates = []
351 for transformer in self.transformers:
352 locn = transformer.find(tokens_by_line)
353 if locn:
354 candidates.append((locn, transformer))
405 for transformer_cls in self.transformers:
406 transformer = transformer_cls.find(tokens_by_line)
407 if transformer:
408 candidates.append(transformer)
355 409
356 410 if not candidates:
357 411 # Nothing to transform
358 412 return False, lines
359 413
360 first_locn, transformer = min(candidates)
361 return True, transformer.transform(lines, first_locn)
414 transformer = min(candidates, key=TokenTransformBase.sortby)
415 return True, transformer.transform(lines)
362 416
363 417 def __call__(self, lines):
364 418 while True:
@@ -366,9 +420,6 b' class TokenTransformers:'
366 420 if not changed:
367 421 return lines
368 422
369 def assign_from_system(tokens_by_line, lines):
370 pass
371
372 423
373 424 def transform_cell(cell):
374 425 if not cell.endswith('\n'):
@@ -8,7 +8,7 b' a = f()'
8 8 %foo \\
9 9 bar
10 10 g()
11 """.splitlines(keepends=True), """\
11 """.splitlines(keepends=True), (2, 0), """\
12 12 a = f()
13 13 get_ipython().run_line_magic('foo', ' bar')
14 14 g()
@@ -17,7 +17,7 b' g()'
17 17 INDENTED_MAGIC = ("""\
18 18 for a in range(5):
19 19 %ls
20 """.splitlines(keepends=True), """\
20 """.splitlines(keepends=True), (2, 4), """\
21 21 for a in range(5):
22 22 get_ipython().run_line_magic('ls', '')
23 23 """.splitlines(keepends=True))
@@ -27,7 +27,7 b' a = f()'
27 27 b = %foo \\
28 28 bar
29 29 g()
30 """.splitlines(keepends=True), """\
30 """.splitlines(keepends=True), (2, 4), """\
31 31 a = f()
32 32 b = get_ipython().run_line_magic('foo', ' bar')
33 33 g()
@@ -38,27 +38,78 b' a = f()'
38 38 b = !foo \\
39 39 bar
40 40 g()
41 """.splitlines(keepends=True), """\
41 """.splitlines(keepends=True), (2, 4), """\
42 42 a = f()
43 43 b = get_ipython().getoutput('foo bar')
44 44 g()
45 45 """.splitlines(keepends=True))
46 46
47 47 AUTOCALL_QUOTE = (
48 [",f 1 2 3\n"],
48 [",f 1 2 3\n"], (1, 0),
49 49 ['f("1", "2", "3")\n']
50 50 )
51 51
52 52 AUTOCALL_QUOTE2 = (
53 [";f 1 2 3\n"],
53 [";f 1 2 3\n"], (1, 0),
54 54 ['f("1 2 3")\n']
55 55 )
56 56
57 57 AUTOCALL_PAREN = (
58 ["/f 1 2 3\n"],
58 ["/f 1 2 3\n"], (1, 0),
59 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 113 def test_continued_line():
63 114 lines = MULTILINE_MAGIC_ASSIGN[0]
64 115 nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2)
@@ -66,58 +117,63 b' def test_continued_line():'
66 117 nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar")
67 118
68 119 def test_find_assign_magic():
69 tbl = make_tokens_by_line(MULTILINE_MAGIC_ASSIGN[0])
70 nt.assert_equal(ipt2.MagicAssign.find(tbl), (2, 4))
71
72 tbl = make_tokens_by_line(MULTILINE_SYSTEM_ASSIGN[0]) # Nothing to find
73 nt.assert_equal(ipt2.MagicAssign.find(tbl), None)
120 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
121 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
74 122
75 123 def test_transform_assign_magic():
76 res = ipt2.MagicAssign.transform(MULTILINE_MAGIC_ASSIGN[0], (2, 4))
77 nt.assert_equal(res, MULTILINE_MAGIC_ASSIGN[1])
124 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
78 125
79 126 def test_find_assign_system():
80 tbl = make_tokens_by_line(MULTILINE_SYSTEM_ASSIGN[0])
81 nt.assert_equal(ipt2.SystemAssign.find(tbl), (2, 4))
127 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
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"])
84 nt.assert_equal(ipt2.SystemAssign.find(tbl), (1, 5))
132 def test_transform_assign_system():
133 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
85 134
86 tbl = make_tokens_by_line(["a=!ls\n"])
87 nt.assert_equal(ipt2.SystemAssign.find(tbl), (1, 2))
135 def test_find_magic_escape():
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
90 nt.assert_equal(ipt2.SystemAssign.find(tbl), None)
140 def test_transform_magic_escape():
141 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
142 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
91 143
92 def test_transform_assign_system():
93 res = ipt2.SystemAssign.transform(MULTILINE_SYSTEM_ASSIGN[0], (2, 4))
94 nt.assert_equal(res, MULTILINE_SYSTEM_ASSIGN[1])
144 def test_find_autocalls():
145 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
146 print("Testing %r" % case[0])
147 check_find(ipt2.EscapedCommand, case)
95 148
96 def test_find_magic_escape():
97 tbl = make_tokens_by_line(MULTILINE_MAGIC[0])
98 nt.assert_equal(ipt2.EscapedCommand.find(tbl), (2, 0))
149 def test_transform_autocall():
150 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
151 print("Testing %r" % case[0])
152 check_transform(ipt2.EscapedCommand, case)
99 153
100 tbl = make_tokens_by_line(INDENTED_MAGIC[0])
101 nt.assert_equal(ipt2.EscapedCommand.find(tbl), (2, 4))
154 def test_find_help():
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
104 nt.assert_equal(ipt2.EscapedCommand.find(tbl), None)
158 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
159 nt.assert_equal(tf.q_line, 1)
160 nt.assert_equal(tf.q_col, 3)
105 161
106 def test_transform_magic_escape():
107 res = ipt2.EscapedCommand.transform(MULTILINE_MAGIC[0], (2, 0))
108 nt.assert_equal(res, MULTILINE_MAGIC[1])
162 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
163 nt.assert_equal(tf.q_line, 1)
164 nt.assert_equal(tf.q_col, 8)
109 165
110 res = ipt2.EscapedCommand.transform(INDENTED_MAGIC[0], (2, 4))
111 nt.assert_equal(res, INDENTED_MAGIC[1])
166 # ? in a comment does not trigger help
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():
114 for sample, _ in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
115 print("Testing %r" % sample)
116 tbl = make_tokens_by_line(sample)
117 nt.assert_equal(ipt2.EscapedCommand.find(tbl), (1, 0))
171 def test_transform_help():
172 tf = ipt2.HelpEnd((1, 0), (1, 9))
173 nt.assert_equal(tf.transform(HELP_IN_EXPR[0]), HELP_IN_EXPR[2])
118 174
119 def test_transform_autocall():
120 for sample, expected in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
121 print("Testing %r" % sample)
122 res = ipt2.EscapedCommand.transform(sample, (1, 0))
123 nt.assert_equal(res, expected)
175 tf = ipt2.HelpEnd((1, 0), (2, 3))
176 nt.assert_equal(tf.transform(HELP_CONTINUED_LINE[0]), HELP_CONTINUED_LINE[2])
177
178 tf = ipt2.HelpEnd((1, 0), (2, 8))
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