Skip to content

Commit e195b33

Browse files
Support [g]ast.get_source_segment
Based on py3.8 implementation
1 parent 04cbcc3 commit e195b33

File tree

2 files changed

+86
-0
lines changed

2 files changed

+86
-0
lines changed

gast/gast.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,77 @@ def _fix(node, lineno, col_offset, end_lineno, end_col_offset):
461461
return node
462462

463463

464+
def _splitlines_no_ff(source):
465+
"""Split a string into lines ignoring form feed and other chars.
466+
467+
This mimics how the Python parser splits source code.
468+
"""
469+
idx = 0
470+
lines = []
471+
next_line = ''
472+
while idx < len(source):
473+
c = source[idx]
474+
next_line += c
475+
idx += 1
476+
# Keep \r\n together
477+
if c == '\r' and idx < len(source) and source[idx] == '\n':
478+
next_line += '\n'
479+
idx += 1
480+
if c in '\r\n':
481+
lines.append(next_line)
482+
next_line = ''
483+
484+
if next_line:
485+
lines.append(next_line)
486+
return lines
487+
488+
489+
def _pad_whitespace(source):
490+
"""Replace all chars except '\f\t' in a line with spaces."""
491+
result = ''
492+
for c in source:
493+
if c in '\f\t':
494+
result += c
495+
else:
496+
result += ' '
497+
return result
498+
499+
500+
def get_source_segment(source, node, *, padded=False):
501+
"""Get source code segment of the *source* that generated *node*.
502+
503+
If some location information (`lineno`, `end_lineno`, `col_offset`,
504+
or `end_col_offset`) is missing, return None.
505+
506+
If *padded* is `True`, the first line of a multi-line statement will
507+
be padded with spaces to match its original position.
508+
"""
509+
try:
510+
lineno = node.lineno - 1
511+
end_lineno = node.end_lineno - 1
512+
col_offset = node.col_offset
513+
end_col_offset = node.end_col_offset
514+
except AttributeError:
515+
return None
516+
517+
lines = _splitlines_no_ff(source)
518+
if end_lineno == lineno:
519+
return lines[lineno].encode()[col_offset:end_col_offset].decode()
520+
521+
if padded:
522+
padding = _pad_whitespace(lines[lineno].encode()[:col_offset].decode())
523+
else:
524+
padding = ''
525+
526+
first = padding + lines[lineno].encode()[col_offset:].decode()
527+
last = lines[end_lineno].encode()[:end_col_offset].decode()
528+
lines = lines[lineno+1:end_lineno]
529+
530+
lines.insert(0, first)
531+
lines.append(last)
532+
return ''.join(lines)
533+
534+
464535
def increment_lineno(node, n=1):
465536
"""
466537
Increment the line number and end line number of each node in the tree

tests/test_api.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ def test_increment_lineno(self):
7474
gast.increment_lineno(tree)
7575
self.assertEqual(tree.lineno, 2)
7676

77+
def test_get_source_segment(self):
78+
code = 'x + 1'
79+
tree = gast.parse(code)
80+
source = gast.get_source_segment(code, tree.body[0].value.left)
81+
self.assertEqual(source, 'x')
82+
83+
def test_get_source_segment_padded(self):
84+
code = 'if 1:\n if 2:\n 3'
85+
tree = gast.parse(code)
86+
if_tree = tree.body[0].body[0]
87+
source_nopadding = gast.get_source_segment(code, if_tree, padded=False)
88+
self.assertEqual(source_nopadding, 'if 2:\n 3')
89+
source_padding = gast.get_source_segment(code, if_tree, padded=True)
90+
self.assertEqual(source_padding, ' if 2:\n 3')
91+
7792
def test_get_docstring_function(self):
7893
code = 'def foo(): "foo"'
7994
tree = gast.parse(code)

0 commit comments

Comments
 (0)