gh-105858: Improve AST node constructors (#105880)

Demonstration:

>>> ast.FunctionDef.__annotations__
{'name': <class 'str'>, 'args': <class 'ast.arguments'>, 'body': list[ast.stmt], 'decorator_list': list[ast.expr], 'returns': ast.expr | None, 'type_comment': str | None, 'type_params': list[ast.type_param]}
>>> ast.FunctionDef()
<stdin>:1: DeprecationWarning: FunctionDef.__init__ missing 1 required positional argument: 'name'. This will become an error in Python 3.15.
<stdin>:1: DeprecationWarning: FunctionDef.__init__ missing 1 required positional argument: 'args'. This will become an error in Python 3.15.
<ast.FunctionDef object at 0x101959460>
>>> node = ast.FunctionDef(name="foo", args=ast.arguments())
>>> node.decorator_list
[]
>>> ast.FunctionDef(whatever="you want", name="x", args=ast.arguments())
<stdin>:1: DeprecationWarning: FunctionDef.__init__ got an unexpected keyword argument 'whatever'. Support for arbitrary keyword arguments is deprecated and will be removed in Python 3.15.
<ast.FunctionDef object at 0x1019581f0>
This commit is contained in:
Jelle Zijlstra 2024-02-27 18:13:03 -08:00 committed by GitHub
parent 5a1559d949
commit ed4dfd8825
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 4675 additions and 49 deletions

View file

@ -525,17 +525,38 @@ class AST_Tests(unittest.TestCase):
if name == 'Index':
continue
if self._is_ast_node(name, item):
x = item()
x = self._construct_ast_class(item)
if isinstance(x, ast.AST):
self.assertIs(type(x._fields), tuple)
def _construct_ast_class(self, cls):
kwargs = {}
for name, typ in cls.__annotations__.items():
if typ is str:
kwargs[name] = 'capybara'
elif typ is int:
kwargs[name] = 42
elif typ is object:
kwargs[name] = b'capybara'
elif isinstance(typ, type) and issubclass(typ, ast.AST):
kwargs[name] = self._construct_ast_class(typ)
return cls(**kwargs)
def test_arguments(self):
x = ast.arguments()
self.assertEqual(x._fields, ('posonlyargs', 'args', 'vararg', 'kwonlyargs',
'kw_defaults', 'kwarg', 'defaults'))
self.assertEqual(x.__annotations__, {
'posonlyargs': list[ast.arg],
'args': list[ast.arg],
'vararg': ast.arg | None,
'kwonlyargs': list[ast.arg],
'kw_defaults': list[ast.expr],
'kwarg': ast.arg | None,
'defaults': list[ast.expr],
})
with self.assertRaises(AttributeError):
x.args
self.assertEqual(x.args, [])
self.assertIsNone(x.vararg)
x = ast.arguments(*range(1, 8))
@ -551,7 +572,7 @@ class AST_Tests(unittest.TestCase):
self.assertEqual(x._fields, 666)
def test_field_attr_writable(self):
x = ast.Constant()
x = ast.Constant(1)
# We can assign to _fields
x._fields = 666
self.assertEqual(x._fields, 666)
@ -611,15 +632,22 @@ class AST_Tests(unittest.TestCase):
self.assertEqual([str(w.message) for w in wlog], [
'ast.Num is deprecated and will be removed in Python 3.14; use ast.Constant instead',
"Constant.__init__ missing 1 required positional argument: 'value'. This will become "
'an error in Python 3.15.',
'Attribute n is deprecated and will be removed in Python 3.14; use value instead',
'ast.Num is deprecated and will be removed in Python 3.14; use ast.Constant instead',
'Attribute n is deprecated and will be removed in Python 3.14; use value instead',
'ast.Num is deprecated and will be removed in Python 3.14; use ast.Constant instead',
"Constant.__init__ missing 1 required positional argument: 'value'. This will become "
'an error in Python 3.15.',
'ast.Num is deprecated and will be removed in Python 3.14; use ast.Constant instead',
'Attribute n is deprecated and will be removed in Python 3.14; use value instead',
'ast.Num is deprecated and will be removed in Python 3.14; use ast.Constant instead',
'ast.Num is deprecated and will be removed in Python 3.14; use ast.Constant instead',
'ast.Num is deprecated and will be removed in Python 3.14; use ast.Constant instead',
"Constant.__init__ got an unexpected keyword argument 'foo'. Support for "
'arbitrary keyword arguments is deprecated and will be removed in Python '
'3.15.',
'ast.Num is deprecated and will be removed in Python 3.14; use ast.Constant instead',
'Attribute n is deprecated and will be removed in Python 3.14; use value instead',
'ast.Num is deprecated and will be removed in Python 3.14; use ast.Constant instead',
@ -636,7 +664,8 @@ class AST_Tests(unittest.TestCase):
])
def test_classattrs(self):
x = ast.Constant()
with self.assertWarns(DeprecationWarning):
x = ast.Constant()
self.assertEqual(x._fields, ('value', 'kind'))
with self.assertRaises(AttributeError):
@ -651,7 +680,7 @@ class AST_Tests(unittest.TestCase):
with self.assertRaises(AttributeError):
x.foobar
x = ast.Constant(lineno=2)
x = ast.Constant(lineno=2, value=3)
self.assertEqual(x.lineno, 2)
x = ast.Constant(42, lineno=0)
@ -662,8 +691,9 @@ class AST_Tests(unittest.TestCase):
self.assertRaises(TypeError, ast.Constant, 1, None, 2)
self.assertRaises(TypeError, ast.Constant, 1, None, 2, lineno=0)
# Arbitrary keyword arguments are supported
self.assertEqual(ast.Constant(1, foo='bar').foo, 'bar')
# Arbitrary keyword arguments are supported (but deprecated)
with self.assertWarns(DeprecationWarning):
self.assertEqual(ast.Constant(1, foo='bar').foo, 'bar')
with self.assertRaisesRegex(TypeError, "Constant got multiple values for argument 'value'"):
ast.Constant(1, value=2)
@ -815,11 +845,11 @@ class AST_Tests(unittest.TestCase):
assertBytesDeprecated(self.assertNotIsInstance, Constant('42'), Bytes)
assertNameConstantDeprecated(self.assertNotIsInstance, Constant(42), NameConstant)
assertEllipsisDeprecated(self.assertNotIsInstance, Constant(42), Ellipsis)
assertNumDeprecated(self.assertNotIsInstance, Constant(), Num)
assertStrDeprecated(self.assertNotIsInstance, Constant(), Str)
assertBytesDeprecated(self.assertNotIsInstance, Constant(), Bytes)
assertNameConstantDeprecated(self.assertNotIsInstance, Constant(), NameConstant)
assertEllipsisDeprecated(self.assertNotIsInstance, Constant(), Ellipsis)
assertNumDeprecated(self.assertNotIsInstance, Constant(None), Num)
assertStrDeprecated(self.assertNotIsInstance, Constant(None), Str)
assertBytesDeprecated(self.assertNotIsInstance, Constant(None), Bytes)
assertNameConstantDeprecated(self.assertNotIsInstance, Constant(1), NameConstant)
assertEllipsisDeprecated(self.assertNotIsInstance, Constant(None), Ellipsis)
class S(str): pass
with assertStrDeprecated():
@ -888,8 +918,9 @@ class AST_Tests(unittest.TestCase):
self.assertEqual(x.body, body)
def test_nodeclasses(self):
# Zero arguments constructor explicitly allowed
x = ast.BinOp()
# Zero arguments constructor explicitly allowed (but deprecated)
with self.assertWarns(DeprecationWarning):
x = ast.BinOp()
self.assertEqual(x._fields, ('left', 'op', 'right'))
# Random attribute allowed too
@ -927,8 +958,9 @@ class AST_Tests(unittest.TestCase):
self.assertEqual(x.right, 3)
self.assertEqual(x.lineno, 0)
# Random kwargs also allowed
x = ast.BinOp(1, 2, 3, foobarbaz=42)
# Random kwargs also allowed (but deprecated)
with self.assertWarns(DeprecationWarning):
x = ast.BinOp(1, 2, 3, foobarbaz=42)
self.assertEqual(x.foobarbaz, 42)
def test_no_fields(self):
@ -941,8 +973,9 @@ class AST_Tests(unittest.TestCase):
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
for ast in (compile(i, "?", "exec", 0x400) for i in exec_tests):
ast2 = pickle.loads(pickle.dumps(ast, protocol))
self.assertEqual(to_tuple(ast2), to_tuple(ast))
with self.subTest(ast=ast, protocol=protocol):
ast2 = pickle.loads(pickle.dumps(ast, protocol))
self.assertEqual(to_tuple(ast2), to_tuple(ast))
def test_invalid_sum(self):
pos = dict(lineno=2, col_offset=3)
@ -1310,8 +1343,9 @@ Module(
'lineno=1, col_offset=4, end_lineno=1, end_col_offset=5), lineno=1, '
'col_offset=0, end_lineno=1, end_col_offset=5))'
)
src = ast.Call(col_offset=1, lineno=1, end_lineno=1, end_col_offset=1)
new = ast.copy_location(src, ast.Call(col_offset=None, lineno=None))
func = ast.Name('spam', ast.Load())
src = ast.Call(col_offset=1, lineno=1, end_lineno=1, end_col_offset=1, func=func)
new = ast.copy_location(src, ast.Call(col_offset=None, lineno=None, func=func))
self.assertIsNone(new.end_lineno)
self.assertIsNone(new.end_col_offset)
self.assertEqual(new.lineno, 1)
@ -1570,15 +1604,15 @@ Module(
self.assertIn('sleep', ns)
def test_recursion_direct(self):
e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0)
e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0, operand=ast.Constant(1))
e.operand = e
with self.assertRaises(RecursionError):
with support.infinite_recursion():
compile(ast.Expression(e), "<test>", "eval")
def test_recursion_indirect(self):
e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0)
f = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0)
e = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0, operand=ast.Constant(1))
f = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0, operand=ast.Constant(1))
e.operand = f
f.operand = e
with self.assertRaises(RecursionError):
@ -2866,6 +2900,23 @@ class NodeTransformerTests(ASTTestMixin, BaseNodeVisitorCases, unittest.TestCase
self.assertASTTransformation(PrintToLog, code, expected)
class ASTConstructorTests(unittest.TestCase):
"""Test the autogenerated constructors for AST nodes."""
def test_FunctionDef(self):
args = ast.arguments()
self.assertEqual(args.args, [])
self.assertEqual(args.posonlyargs, [])
with self.assertWarnsRegex(DeprecationWarning,
r"FunctionDef\.__init__ missing 1 required positional argument: 'name'"):
node = ast.FunctionDef(args=args)
self.assertFalse(hasattr(node, "name"))
self.assertEqual(node.decorator_list, [])
node = ast.FunctionDef(name='foo', args=args)
self.assertEqual(node.name, 'foo')
self.assertEqual(node.decorator_list, [])
@support.cpython_only
class ModuleStateTests(unittest.TestCase):
# bpo-41194, bpo-41261, bpo-41631: The _ast module uses a global state.