-
-
Notifications
You must be signed in to change notification settings - Fork 32k
bpo-33416: Add end positions to Python AST #11605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ba4ba82
3e343e3
1684c17
514d4ea
3ab2516
1d3e352
dbf9cc9
a44207b
5af33da
ce7f5ce
2171eb9
10cf4bd
ed05305
58fbfa6
f2589ff
7d5ca5e
aa62e3c
96a0ec0
c169025
553a772
5cc01e9
dce260e
9ba6604
69a6280
4af426f
e5a12c3
0275a93
f20635b
c9da8f5
f97f38a
70cc16c
48936b9
ac5b5cb
4726f17
eeea87d
027a4ca
ff361f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -115,10 +115,10 @@ def _format(node): | |
|
||
def copy_location(new_node, old_node): | ||
""" | ||
Copy source location (`lineno` and `col_offset` attributes) from | ||
*old_node* to *new_node* if possible, and return *new_node*. | ||
Copy source location (`lineno`, `col_offset`, `end_lineno`, and `end_col_offset` | ||
attributes) from *old_node* to *new_node* if possible, and return *new_node*. | ||
""" | ||
for attr in 'lineno', 'col_offset': | ||
for attr in 'lineno', 'col_offset', 'end_lineno', 'end_col_offset': | ||
if attr in old_node._attributes and attr in new_node._attributes \ | ||
and hasattr(old_node, attr): | ||
setattr(new_node, attr, getattr(old_node, attr)) | ||
|
@@ -133,31 +133,44 @@ def fix_missing_locations(node): | |
recursively where not already set, by setting them to the values of the | ||
parent node. It works recursively starting at *node*. | ||
""" | ||
def _fix(node, lineno, col_offset): | ||
def _fix(node, lineno, col_offset, end_lineno, end_col_offset): | ||
if 'lineno' in node._attributes: | ||
if not hasattr(node, 'lineno'): | ||
node.lineno = lineno | ||
else: | ||
lineno = node.lineno | ||
if 'end_lineno' in node._attributes: | ||
if not hasattr(node, 'end_lineno'): | ||
node.end_lineno = end_lineno | ||
else: | ||
end_lineno = node.end_lineno | ||
if 'col_offset' in node._attributes: | ||
if not hasattr(node, 'col_offset'): | ||
node.col_offset = col_offset | ||
else: | ||
col_offset = node.col_offset | ||
if 'end_col_offset' in node._attributes: | ||
if not hasattr(node, 'end_col_offset'): | ||
node.end_col_offset = end_col_offset | ||
else: | ||
end_col_offset = node.end_col_offset | ||
for child in iter_child_nodes(node): | ||
_fix(child, lineno, col_offset) | ||
_fix(node, 1, 0) | ||
_fix(child, lineno, col_offset, end_lineno, end_col_offset) | ||
_fix(node, 1, 0, 1, 0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole function looks a bit suspicious.Shouldn't it at least ensure that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is used to compile manually generated AST to bytecode. Although technically end positions are not needed for the compiler (only start positions are recorded in the byte code), it is probably good to have them fixed. The algorithm for fixing is quite naive (take either a position of nearest root-side node if known, otherwise use start of the file), but it never intended to be robust, it exist just to allow compilation in those case where a user doesn't care about line numbers. |
||
return node | ||
|
||
|
||
def increment_lineno(node, n=1): | ||
""" | ||
Increment the line number of each node in the tree starting at *node* by *n*. | ||
This is useful to "move code" to a different location in a file. | ||
Increment the line number and end line number of each node in the tree | ||
starting at *node* by *n*. This is useful to "move code" to a different | ||
location in a file. | ||
""" | ||
for child in walk(node): | ||
if 'lineno' in child._attributes: | ||
child.lineno = getattr(child, 'lineno', 0) + n | ||
if 'end_lineno' in child._attributes: | ||
child.end_lineno = getattr(child, 'end_lineno', 0) + n | ||
return node | ||
|
||
|
||
|
@@ -213,6 +226,77 @@ def get_docstring(node, clean=True): | |
return text | ||
|
||
|
||
def _splitlines_no_ff(source): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another option is to use |
||
"""Split a string into lines ignoring form feed and other chars. | ||
|
||
This mimics how the Python parser splits source code. | ||
""" | ||
idx = 0 | ||
lines = [] | ||
next_line = '' | ||
while idx < len(source): | ||
c = source[idx] | ||
next_line += c | ||
idx += 1 | ||
# Keep \r\n together | ||
if c == '\r' and idx < len(source) and source[idx] == '\n': | ||
next_line += '\n' | ||
idx += 1 | ||
if c in '\r\n': | ||
lines.append(next_line) | ||
next_line = '' | ||
|
||
if next_line: | ||
lines.append(next_line) | ||
return lines | ||
|
||
|
||
def _pad_whitespace(source): | ||
"""Replace all chars except '\f\t' in a line with spaces.""" | ||
result = '' | ||
for c in source: | ||
if c in '\f\t': | ||
result += c | ||
else: | ||
result += ' ' | ||
return result | ||
|
||
|
||
def get_source_segment(source, node, *, padded=False): | ||
"""Get source code segment of the *source* that generated *node*. | ||
|
||
If some location information (`lineno`, `end_lineno`, `col_offset`, | ||
or `end_col_offset`) is missing, return None. | ||
|
||
If *padded* is `True`, the first line of a multi-line statement will | ||
be padded with spaces to match its original position. | ||
""" | ||
try: | ||
lineno = node.lineno - 1 | ||
end_lineno = node.end_lineno - 1 | ||
col_offset = node.col_offset | ||
end_col_offset = node.end_col_offset | ||
except AttributeError: | ||
return None | ||
|
||
lines = _splitlines_no_ff(source) | ||
if end_lineno == lineno: | ||
return lines[lineno].encode()[col_offset:end_col_offset].decode() | ||
|
||
if padded: | ||
padding = _pad_whitespace(lines[lineno].encode()[:col_offset].decode()) | ||
else: | ||
padding = '' | ||
|
||
first = padding + lines[lineno].encode()[col_offset:].decode() | ||
last = lines[end_lineno].encode()[:end_col_offset].decode() | ||
lines = lines[lineno+1:end_lineno] | ||
|
||
lines.insert(0, first) | ||
lines.append(last) | ||
return ''.join(lines) | ||
|
||
|
||
def walk(node): | ||
""" | ||
Recursively yield all descendant nodes in the tree starting at *node* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,14 +62,16 @@ def test_product(self): | |
|
||
def test_attributes(self): | ||
stmt = self.types['stmt'] | ||
self.assertEqual(len(stmt.attributes), 2) | ||
self.assertEqual(len(stmt.attributes), 4) | ||
self.assertEqual(str(stmt.attributes[0]), 'Field(int, lineno)') | ||
self.assertEqual(str(stmt.attributes[1]), 'Field(int, col_offset)') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add tests for stmt.attributes[2] and stmt.attributes[3]? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. |
||
self.assertEqual(str(stmt.attributes[2]), 'Field(int, end_lineno, opt=True)') | ||
self.assertEqual(str(stmt.attributes[3]), 'Field(int, end_col_offset, opt=True)') | ||
|
||
def test_constructor_fields(self): | ||
ehandler = self.types['excepthandler'] | ||
self.assertEqual(len(ehandler.types), 1) | ||
self.assertEqual(len(ehandler.attributes), 2) | ||
self.assertEqual(len(ehandler.attributes), 4) | ||
|
||
cons = ehandler.types[0] | ||
self.assertIsInstance(cons, self.asdl.Constructor) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this only affect the node, or the whole tree? I've never used this so I don't know what to expect from context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one (unlike some others) is non-recursive, one can even copy from a node of a different kind.