Skip to content

bpo-39981: Default values for AST nodes #19031

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

Closed
wants to merge 5 commits into from
Closed
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
297 changes: 163 additions & 134 deletions Doc/library/ast.rst

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ Added the *indent* option to :func:`~ast.dump` which allows it to produce a
multiline indented output.
(Contributed by Serhiy Storchaka in :issue:`37995`.)

Added the *omit_defaults* option to :func:`~ast.dump` which allows to
produce a representation without printing default values.
(Contributed by Serhiy Storchaka and Batuhan Taskaya in :issue:`36287`)

Added :func:`ast.unparse` as a function in the :mod:`ast` module that can
be used to unparse an :class:`ast.AST` object and produce a string with code
that would produce an equivalent :class:`ast.AST` object when parsed.
Expand All @@ -167,6 +171,9 @@ that would produce an equivalent :class:`ast.AST` object when parsed.
Added docstrings to AST nodes that contains the ASDL signature used to
construct that node. (Contributed by Batuhan Taskaya in :issue:`39638`.)

Added default values to AST node classes.
(Contributed by Batuhan Taskaya in :issue:`39981`)

asyncio
-------

Expand Down
20 changes: 16 additions & 4 deletions Lib/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from _ast import *
from contextlib import contextmanager, nullcontext
from enum import IntEnum, auto
from functools import lru_cache


def parse(source, filename='<unknown>', mode='exec', *,
Expand Down Expand Up @@ -102,7 +103,7 @@ def _convert(node):
return _convert(node_or_string)


def dump(node, annotate_fields=True, include_attributes=False, *, indent=None):
def dump(node, annotate_fields=True, include_attributes=False, *, indent=None, omit_defaults=True):
"""
Return a formatted dump of the tree in node. This is mainly useful for
debugging purposes. If annotate_fields is true (by default),
Expand All @@ -112,7 +113,9 @@ def dump(node, annotate_fields=True, include_attributes=False, *, indent=None):
numbers and column offsets are not dumped by default. If this is wanted,
include_attributes can be set to true. If indent is a non-negative
integer or string, then the tree will be pretty-printed with that indent
level. None (the default) selects the single line representation.
level. None (the default) selects the single line representation. If
omit_defaults is true, fields that have their default values will be
omitted.
"""
def _format(node, level=0):
if indent is not None:
Expand All @@ -127,13 +130,18 @@ def _format(node, level=0):
args = []
allsimple = True
keywords = annotate_fields
field_defaults = cls._field_defaults
for name in node._fields:
try:
value = getattr(node, name)
except AttributeError:
keywords = True
continue
if value is None and getattr(cls, name, ...) is None:
if (
omit_defaults
and name in field_defaults
and field_defaults[name] == value
):
keywords = True
continue
value, simple = _format(value, level)
Expand All @@ -148,7 +156,11 @@ def _format(node, level=0):
value = getattr(node, name)
except AttributeError:
continue
if value is None and getattr(cls, name, ...) is None:
if (
omit_defaults
and value is None
and getattr(cls, name, ...) is None
):
continue
value, simple = _format(value, level)
allsimple = allsimple and simple
Expand Down
72 changes: 41 additions & 31 deletions Lib/test/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,23 +352,42 @@ def test_field_attr_existence(self):
self.assertEqual(type(x._fields), tuple)

def test_arguments(self):
x = ast.arguments()
self.assertEqual(x._fields, ('posonlyargs', 'args', 'vararg', 'kwonlyargs',
'kw_defaults', 'kwarg', 'defaults'))
x = ast.FunctionDef()
self.assertEqual(x._fields, ('name', 'args', 'body', 'decorator_list', 'returns', 'type_comment'))
self.assertEqual(x._field_defaults, {'body': [], 'decorator_list': [], 'returns': None, 'type_comment': None})

with self.assertRaises(AttributeError):
x.args
self.assertIsNone(x.vararg)
x.name

x = ast.arguments(*range(1, 8))
self.assertIsNone(x.returns)
self.assertEqual(x.body, [])

x = ast.FunctionDef(*range(1, 4))
self.assertEqual(x.args, 2)
self.assertEqual(x.vararg, 3)
self.assertEqual(x.body, 3)
self.assertEqual(x.decorator_list, [])
self.assertIsNone(x.returns)
self.assertIsNone(x.type_comment)

x = ast.Module(body=1)
self.assertEqual(x.body, 1)
self.assertEqual(x.type_ignores, [])

x = ast.Module(body=1, type_ignores=2)
self.assertEqual(x.body, 1)
self.assertEqual(x.type_ignores, 2)

x = ast.Module(type_ignores=2)
self.assertEqual(x.body, [])
self.assertEqual(x.type_ignores, 2)

def test_field_attr_writable(self):
x = ast.Num()
# We can assign to _fields
x._fields = 666
x._field_defaults = 999
self.assertEqual(x._fields, 666)
self.assertEqual(x._field_defaults, 999)

def test_classattrs(self):
x = ast.Num()
Expand Down Expand Up @@ -666,21 +685,21 @@ def test_dump(self):
node = ast.parse('spam(eggs, "and cheese")')
self.assertEqual(ast.dump(node),
"Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load()), "
"args=[Name(id='eggs', ctx=Load()), Constant(value='and cheese')], "
"keywords=[]))], type_ignores=[])"
"args=[Name(id='eggs', ctx=Load()), Constant(value='and cheese')"
"]))])"
)
self.assertEqual(ast.dump(node, annotate_fields=False),
"Module([Expr(Call(Name('spam', Load()), [Name('eggs', Load()), "
"Constant('and cheese')], []))], [])"
"Constant('and cheese')]))])"
)
self.assertEqual(ast.dump(node, include_attributes=True),
"Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load(), "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=4), "
"args=[Name(id='eggs', ctx=Load(), lineno=1, col_offset=5, "
"end_lineno=1, end_col_offset=9), Constant(value='and cheese', "
"lineno=1, col_offset=11, end_lineno=1, end_col_offset=23)], keywords=[], "
"lineno=1, col_offset=11, end_lineno=1, end_col_offset=23)], "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=24), "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=24)], type_ignores=[])"
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=24)])"
)

def test_dump_indent(self):
Expand All @@ -693,9 +712,7 @@ def test_dump_indent(self):
func=Name(id='spam', ctx=Load()),
args=[
Name(id='eggs', ctx=Load()),
Constant(value='and cheese')],
keywords=[]))],
type_ignores=[])""")
Constant(value='and cheese')]))])""")
self.assertEqual(ast.dump(node, annotate_fields=False, indent='\t'), """\
Module(
\t[
Expand All @@ -704,9 +721,7 @@ def test_dump_indent(self):
\t\t\t\tName('spam', Load()),
\t\t\t\t[
\t\t\t\t\tName('eggs', Load()),
\t\t\t\t\tConstant('and cheese')],
\t\t\t\t[]))],
\t[])""")
\t\t\t\t\tConstant('and cheese')]))])""")
self.assertEqual(ast.dump(node, include_attributes=True, indent=3), """\
Module(
body=[
Expand All @@ -733,16 +748,14 @@ def test_dump_indent(self):
col_offset=11,
end_lineno=1,
end_col_offset=23)],
keywords=[],
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=24),
lineno=1,
col_offset=0,
end_lineno=1,
end_col_offset=24)],
type_ignores=[])""")
end_col_offset=24)])""")

def test_dump_incomplete(self):
node = ast.Raise(lineno=3, col_offset=4)
Expand Down Expand Up @@ -791,16 +804,13 @@ def test_fix_missing_locations(self):
self.maxDiff = None
self.assertEqual(ast.dump(src, include_attributes=True),
"Module(body=[Expr(value=Call(func=Name(id='write', ctx=Load(), "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=5), "
"args=[Constant(value='spam', lineno=1, col_offset=6, end_lineno=1, "
"end_col_offset=12)], keywords=[], lineno=1, col_offset=0, end_lineno=1, "
"end_col_offset=13), lineno=1, col_offset=0, end_lineno=1, "
"end_col_offset=13), Expr(value=Call(func=Name(id='spam', ctx=Load(), "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=0), "
"args=[Constant(value='eggs', lineno=1, col_offset=0, end_lineno=1, "
"end_col_offset=0)], keywords=[], lineno=1, col_offset=0, end_lineno=1, "
"end_col_offset=0), lineno=1, col_offset=0, end_lineno=1, end_col_offset=0)], "
"type_ignores=[])"
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=5), args=["
"Constant(value='spam', lineno=1, col_offset=6, end_lineno=1, end_col_offset=12)], "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=13), lineno=1, col_offset=0, "
"end_lineno=1, end_col_offset=13), Expr(value=Call(func=Name(id='spam', ctx=Load(), "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=0), args=[Constant(value='eggs', "
"lineno=1, col_offset=0, end_lineno=1, end_col_offset=0)], lineno=1, col_offset=0, "
"end_lineno=1, end_col_offset=0), lineno=1, col_offset=0, end_lineno=1, end_col_offset=0)])"
)

def test_increment_lineno(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add default values for :mod:`ast` node classes. Patch by Batuhan Taskaya.
15 changes: 7 additions & 8 deletions Parser/asdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,15 @@ def __init__(self, type, name=None, seq=False, opt=False):
self.name = name
self.seq = seq
self.opt = opt

def __str__(self):
if self.seq:
extra = "*"
elif self.opt:
extra = "?"
if seq:
self.extra = "*"
elif opt:
self.extra = "?"
else:
extra = ""
self.extra = ""

return "{}{} {}".format(self.type, extra, self.name)
def __str__(self):
return "{}{} {}".format(self.type, self.extra, self.name)

def __repr__(self):
if self.seq:
Expand Down
Loading