Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d2f506a

Browse files
brianschubertpicnixzJelleZijlstra
authored
gh-137600: Promote ast node constructor deprecation warnings to errors (#137601)
Co-authored-by: Bénédikt Tran <[email protected]> Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent 29a92ab commit d2f506a

6 files changed

Lines changed: 255 additions & 466 deletions

File tree

Doc/library/ast.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ The abstract grammar is currently defined as follows:
3535
:language: asdl
3636

3737

38+
.. _ast_nodes:
39+
3840
Node classes
3941
------------
4042

@@ -164,8 +166,7 @@ Node classes
164166
Previous versions of Python allowed the creation of AST nodes that were missing
165167
required fields. Similarly, AST node constructors allowed arbitrary keyword
166168
arguments that were set as attributes of the AST node, even if they did not
167-
match any of the fields of the AST node. This behavior is deprecated and will
168-
be removed in Python 3.15.
169+
match any of the fields of the AST node. These cases now raise a :exc:`TypeError`.
169170

170171
.. note::
171172
The descriptions of the specific node classes displayed here

Doc/whatsnew/3.15.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,16 @@ This was made possible by a refactoring of JIT data structures.
16201620
Removed
16211621
========
16221622

1623+
ast
1624+
---
1625+
1626+
* The constructors of :ref:`AST nodes <ast_nodes>` now raise a :exc:`TypeError`
1627+
when a required argument is omitted or when a keyword argument that does not
1628+
map to a field on the AST node is passed. These cases had previously raised a
1629+
:exc:`DeprecationWarning` since Python 3.13.
1630+
(Contributed by Brian Schubert and Jelle Zijlstra in :gh:`137600` and :gh:`105858`.)
1631+
1632+
16231633
collections.abc
16241634
---------------
16251635

Lib/test/test_ast/test_ast.py

Lines changed: 46 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,17 @@ def test_field_attr_existence(self):
418418
if isinstance(x, ast.AST):
419419
self.assertIs(type(x._fields), tuple)
420420

421+
def test_dynamic_attr(self):
422+
for name, item in ast.__dict__.items():
423+
# constructor has a different signature
424+
if name == 'Index':
425+
continue
426+
if self._is_ast_node(name, item):
427+
x = self._construct_ast_class(item)
428+
# Custom attribute assignment is allowed
429+
x.foo = 5
430+
self.assertEqual(x.foo, 5)
431+
421432
def _construct_ast_class(self, cls):
422433
kwargs = {}
423434
for name, typ in cls.__annotations__.items():
@@ -459,14 +470,9 @@ def test_field_attr_writable(self):
459470
self.assertEqual(x._fields, 666)
460471

461472
def test_classattrs(self):
462-
with self.assertWarns(DeprecationWarning):
463-
x = ast.Constant()
473+
x = ast.Constant(42)
464474
self.assertEqual(x._fields, ('value', 'kind'))
465475

466-
with self.assertRaises(AttributeError):
467-
x.value
468-
469-
x = ast.Constant(42)
470476
self.assertEqual(x.value, 42)
471477

472478
with self.assertRaises(AttributeError):
@@ -486,11 +492,8 @@ def test_classattrs(self):
486492
self.assertRaises(TypeError, ast.Constant, 1, None, 2)
487493
self.assertRaises(TypeError, ast.Constant, 1, None, 2, lineno=0)
488494

489-
# Arbitrary keyword arguments are supported (but deprecated)
490-
with self.assertWarns(DeprecationWarning):
491-
self.assertEqual(ast.Constant(1, foo='bar').foo, 'bar')
492-
493-
with self.assertRaisesRegex(TypeError, "Constant got multiple values for argument 'value'"):
495+
msg = "ast.Constant got multiple values for argument 'value'"
496+
with self.assertRaisesRegex(TypeError, re.escape(msg)):
494497
ast.Constant(1, value=2)
495498

496499
self.assertEqual(ast.Constant(42).value, 42)
@@ -529,23 +532,24 @@ def test_module(self):
529532
self.assertEqual(x.body, body)
530533

531534
def test_nodeclasses(self):
532-
# Zero arguments constructor explicitly allowed (but deprecated)
533-
with self.assertWarns(DeprecationWarning):
534-
x = ast.BinOp()
535-
self.assertEqual(x._fields, ('left', 'op', 'right'))
536-
537-
# Random attribute allowed too
538-
x.foobarbaz = 5
539-
self.assertEqual(x.foobarbaz, 5)
535+
# Zero arguments constructor is not allowed
536+
msg = "ast.BinOp.__init__ missing 3 required positional arguments: 'left', 'op', and 'right'"
537+
self.assertRaisesRegex(TypeError, re.escape(msg), ast.BinOp)
540538

541539
n1 = ast.Constant(1)
542540
n3 = ast.Constant(3)
543541
addop = ast.Add()
544542
x = ast.BinOp(n1, addop, n3)
543+
self.assertEqual(x._fields, ('left', 'op', 'right'))
545544
self.assertEqual(x.left, n1)
546545
self.assertEqual(x.op, addop)
547546
self.assertEqual(x.right, n3)
548547

548+
# Arbitrary attributes are allowed
549+
x.foobarbaz = 5
550+
self.assertEqual(x.foobarbaz, 5)
551+
self.assertEqual(x._fields, ('left', 'op', 'right'))
552+
549553
x = ast.BinOp(1, 2, 3)
550554
self.assertEqual(x.left, 1)
551555
self.assertEqual(x.op, 2)
@@ -569,10 +573,10 @@ def test_nodeclasses(self):
569573
self.assertEqual(x.right, 3)
570574
self.assertEqual(x.lineno, 0)
571575

572-
# Random kwargs also allowed (but deprecated)
573-
with self.assertWarns(DeprecationWarning):
574-
x = ast.BinOp(1, 2, 3, foobarbaz=42)
575-
self.assertEqual(x.foobarbaz, 42)
576+
# Arbitrary keyword arguments are not allowed
577+
msg = "ast.BinOp.__init__ got an unexpected keyword argument 'foobarbaz'"
578+
with self.assertRaisesRegex(TypeError, re.escape(msg)):
579+
ast.BinOp(1, 2, 3, foobarbaz=42)
576580

577581
def test_no_fields(self):
578582
# this used to fail because Sub._fields was None
@@ -1377,14 +1381,14 @@ def test_replace_ignore_known_custom_instance_fields(self):
13771381
self.assertRaises(AttributeError, getattr, repl, 'extra')
13781382

13791383
def test_replace_reject_missing_field(self):
1380-
# case: warn if deleted field is not replaced
1384+
# case: raise if deleted field is not replaced
13811385
node = ast.parse('x').body[0].value
13821386
context = node.ctx
13831387
del node.id
13841388

13851389
self.assertRaises(AttributeError, getattr, node, 'id')
13861390
self.assertIs(node.ctx, context)
1387-
msg = "Name.__replace__ missing 1 keyword argument: 'id'."
1391+
msg = "ast.Name.__init__ missing 1 required positional argument: 'id'"
13881392
with self.assertRaisesRegex(TypeError, re.escape(msg)):
13891393
copy.replace(node)
13901394
# assert that there is no side-effect
@@ -1421,7 +1425,7 @@ def test_replace_reject_known_custom_instance_fields_commits(self):
14211425

14221426
# explicit rejection of known instance fields
14231427
self.assertHasAttr(node, 'extra')
1424-
msg = "Name.__replace__ got an unexpected keyword argument 'extra'."
1428+
msg = "ast.Name.__init__ got an unexpected keyword argument 'extra'"
14251429
with self.assertRaisesRegex(TypeError, re.escape(msg)):
14261430
copy.replace(node, extra=1)
14271431
# assert that there is no side-effect
@@ -1435,7 +1439,7 @@ def test_replace_reject_unknown_instance_fields(self):
14351439

14361440
# explicit rejection of unknown extra fields
14371441
self.assertRaises(AttributeError, getattr, node, 'unknown')
1438-
msg = "Name.__replace__ got an unexpected keyword argument 'unknown'."
1442+
msg = "ast.Name.__init__ got an unexpected keyword argument 'unknown'"
14391443
with self.assertRaisesRegex(TypeError, re.escape(msg)):
14401444
copy.replace(node, unknown=1)
14411445
# assert that there is no side-effect
@@ -3200,11 +3204,10 @@ def test_FunctionDef(self):
32003204
args = ast.arguments()
32013205
self.assertEqual(args.args, [])
32023206
self.assertEqual(args.posonlyargs, [])
3203-
with self.assertWarnsRegex(DeprecationWarning,
3204-
r"FunctionDef\.__init__ missing 1 required positional argument: 'name'"):
3205-
node = ast.FunctionDef(args=args)
3206-
self.assertNotHasAttr(node, "name")
3207-
self.assertEqual(node.decorator_list, [])
3207+
msg = "ast.FunctionDef.__init__ missing 1 required positional argument: 'name'"
3208+
with self.assertRaisesRegex(TypeError, re.escape(msg)):
3209+
ast.FunctionDef(args=args)
3210+
32083211
node = ast.FunctionDef(name='foo', args=args)
32093212
self.assertEqual(node.name, 'foo')
32103213
self.assertEqual(node.decorator_list, [])
@@ -3222,9 +3225,8 @@ def test_expr_context(self):
32223225
self.assertEqual(name3.id, "x")
32233226
self.assertIsInstance(name3.ctx, ast.Del)
32243227

3225-
with self.assertWarnsRegex(DeprecationWarning,
3226-
r"Name\.__init__ missing 1 required positional argument: 'id'"):
3227-
name3 = ast.Name()
3228+
msg = "ast.Name.__init__ missing 1 required positional argument: 'id'"
3229+
self.assertRaisesRegex(TypeError, re.escape(msg), ast.Name)
32283230

32293231
def test_custom_subclass_with_no_fields(self):
32303232
class NoInit(ast.AST):
@@ -3263,20 +3265,18 @@ class MyAttrs(ast.AST):
32633265
self.assertEqual(obj.a, 1)
32643266
self.assertEqual(obj.b, 2)
32653267

3266-
with self.assertWarnsRegex(DeprecationWarning,
3267-
r"MyAttrs.__init__ got an unexpected keyword argument 'c'."):
3268-
obj = MyAttrs(c=3)
3268+
msg = "MyAttrs.__init__ got an unexpected keyword argument 'c'"
3269+
with self.assertRaisesRegex(TypeError, re.escape(msg)):
3270+
MyAttrs(c=3)
32693271

32703272
def test_fields_and_types_no_default(self):
32713273
class FieldsAndTypesNoDefault(ast.AST):
32723274
_fields = ('a',)
32733275
_field_types = {'a': int}
32743276

3275-
with self.assertWarnsRegex(DeprecationWarning,
3276-
r"FieldsAndTypesNoDefault\.__init__ missing 1 required positional argument: 'a'\."):
3277-
obj = FieldsAndTypesNoDefault()
3278-
with self.assertRaises(AttributeError):
3279-
obj.a
3277+
msg = "FieldsAndTypesNoDefault.__init__ missing 1 required positional argument: 'a'"
3278+
self.assertRaisesRegex(TypeError, re.escape(msg), FieldsAndTypesNoDefault)
3279+
32803280
obj = FieldsAndTypesNoDefault(a=1)
32813281
self.assertEqual(obj.a, 1)
32823282

@@ -3287,13 +3287,8 @@ class MoreFieldsThanTypes(ast.AST):
32873287
a: int | None = None
32883288
b: int | None = None
32893289

3290-
with self.assertWarnsRegex(
3291-
DeprecationWarning,
3292-
r"Field 'b' is missing from MoreFieldsThanTypes\._field_types"
3293-
):
3294-
obj = MoreFieldsThanTypes()
3295-
self.assertIs(obj.a, None)
3296-
self.assertIs(obj.b, None)
3290+
msg = r"Field 'b' is missing from .*\.MoreFieldsThanTypes\._field_types"
3291+
self.assertRaisesRegex(TypeError, msg, MoreFieldsThanTypes)
32973292

32983293
obj = MoreFieldsThanTypes(a=1, b=2)
32993294
self.assertEqual(obj.a, 1)
@@ -3305,8 +3300,7 @@ class BadFields(ast.AST):
33053300
_field_types = {'a': int}
33063301

33073302
# This should not crash
3308-
with self.assertWarnsRegex(DeprecationWarning, r"Field b'\\xff\\xff.*' .*"):
3309-
obj = BadFields()
3303+
self.assertRaisesRegex(TypeError, r"Field b'\\xff\\xff.*' .*", BadFields)
33103304

33113305
def test_complete_field_types(self):
33123306
class _AllFieldTypes(ast.AST):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`ast`: The constructors of AST nodes now raise a :exc:`TypeError` when
2+
a required argument is omitted or when a keyword argument that does not map to
3+
a field on the AST node is passed. These cases had previously raised a
4+
:exc:`DeprecationWarning` since Python 3.13. Patch by Brian Schubert.

0 commit comments

Comments
 (0)