Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions Doc/library/ast.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,26 @@
end_lineno
end_col_offset

Instances of :class:`ast.expr` and :class:`ast.stmt` subclasses have
:attr:`lineno`, :attr:`col_offset`, :attr:`end_lineno`, and
:attr:`end_col_offset` attributes. The :attr:`lineno` and :attr:`end_lineno`
Instances of :class:`ast.stmt`, :class:`ast.expr`,

Check warning on line 93 in Doc/library/ast.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:class reference target not found: ast.type_param [ref.class]

Check warning on line 93 in Doc/library/ast.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:class reference target not found: ast.pattern [ref.class]

Check warning on line 93 in Doc/library/ast.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:class reference target not found: ast.excepthandler [ref.class]

Check warning on line 93 in Doc/library/ast.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:class reference target not found: ast.expr [ref.class]

Check warning on line 93 in Doc/library/ast.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:class reference target not found: ast.stmt [ref.class]
:class:`ast.excepthandler`, :class:`ast.arg`, :class:`ast.keyword`,
:class:`ast.alias`, :class:`ast.pattern` and :class:`ast.type_param`
subclasses have the :attr:`lineno`, :attr:`col_offset`,
:attr:`end_lineno`, and :attr:`end_col_offset` attributes. These are the
classes carrying an ``attributes`` clause in the abstract grammar
:ref:`above <abstract-grammar>`; no other class has them.
The :attr:`lineno` and :attr:`end_lineno`
are the first and last line numbers of source text span (1-indexed so the
first line is line 1) and the :attr:`col_offset` and :attr:`end_col_offset`
are the corresponding UTF-8 byte offsets of the first and last tokens that
generated the node. The UTF-8 offset is recorded because the parser uses
UTF-8 internally.

Note that the end positions are not required by the compiler and are
therefore optional. The end offset is *after* the last symbol, for example
The end positions are optional on the classes that already existed when
they were added in Python 3.8, so that code written before then can still
build those nodes; a class added afterwards requires them. The abstract
grammar spells this out, declaring the two fields ``int?`` in the first
case and ``int`` in the second. The end offset is *after* the last
symbol, for example
one can get the source segment of a one-line expression node using
``source_line[node.col_offset : node.end_col_offset]``.

Expand Down
28 changes: 28 additions & 0 deletions Lib/test/test_asdl_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,34 @@ def test_attributes(self):
self.assertEqual(repr(stmt.attributes[2]), 'Field(int, end_lineno, quantifiers=[OPTIONAL])')
self.assertEqual(repr(stmt.attributes[3]), 'Field(int, end_col_offset, quantifiers=[OPTIONAL])')

# Types that already existed when end positions were added in 3.8 keep
# them optional, so that code written before 3.8 can still build their
# nodes. A type introduced afterwards has no such callers and requires
# them: pattern was given required end positions when it was added
# (gh-88058) and type_param was corrected to match (gh-106145).
OPTIONAL_END_POSITIONS = frozenset({
'stmt', 'expr', 'excepthandler', 'arg', 'keyword', 'alias',
})

def test_end_positions_are_required_for_new_types(self):
for name, type_ in self.types.items():
attributes = getattr(type_, 'attributes', None)
if not attributes:
continue
ends = [f for f in attributes
if f.name in ('end_lineno', 'end_col_offset')]
with self.subTest(type=name):
self.assertEqual(len(ends), 2)
optional = [f.opt for f in ends]
if name in self.OPTIONAL_END_POSITIONS:
self.assertEqual(optional, [True, True])
else:
self.assertEqual(
optional, [False, False],
f'{name} postdates the 3.8 addition of end positions, '
f'so end_lineno and end_col_offset should be declared '
f'"int" rather than "int?"')

def test_constructor_fields(self):
ehandler = self.types['excepthandler']
self.assertEqual(len(ehandler.types), 1)
Expand Down
Loading