Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,16 @@ hashlib
Removed
=======

ast
---

* The constructors of node types in the :mod:`ast` module now raise a
:exc:`TypeError` when a required argument is omitted or when a
keyword-argument that does not map to a field on the AST node is passed.
These cases had previously raised a :exc:`DeprecationWarning` since Python 3.13.
(Contributed by Brian Schubert and Jelle Zijlstra in :gh:`137600` and :gh:`105858`.)


ctypes
------

Expand Down
67 changes: 30 additions & 37 deletions Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,14 +458,13 @@ def test_field_attr_writable(self):
self.assertEqual(x._fields, 666)

def test_classattrs(self):
with self.assertWarns(DeprecationWarning):
msg = "Constant.__init__ missing 1 required positional argument: 'value'"
with self.assertRaisesRegex(TypeError, re.escape(msg)):
x = ast.Constant()
self.assertEqual(x._fields, ('value', 'kind'))

with self.assertRaises(AttributeError):
x.value

x = ast.Constant(42)
self.assertEqual(x._fields, ('value', 'kind'))

self.assertEqual(x.value, 42)

with self.assertRaises(AttributeError):
Expand All @@ -485,9 +484,10 @@ def test_classattrs(self):
self.assertRaises(TypeError, ast.Constant, 1, None, 2)
self.assertRaises(TypeError, ast.Constant, 1, None, 2, lineno=0)

# Arbitrary keyword arguments are supported (but deprecated)
with self.assertWarns(DeprecationWarning):
self.assertEqual(ast.Constant(1, foo='bar').foo, 'bar')
# Arbitrary keyword arguments are not supported
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, this is now testing something unrelated to what this function is about.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intent here was to be sure we were ignoring kwargs.

msg = "Constant.__init__ got an unexpected keyword argument 'foo'"
with self.assertRaisesRegex(TypeError, re.escape(msg)):
ast.Constant(1, foo='bar')

with self.assertRaisesRegex(TypeError, "Constant got multiple values for argument 'value'"):
ast.Constant(1, value=2)
Expand Down Expand Up @@ -528,23 +528,24 @@ def test_module(self):
self.assertEqual(x.body, body)

def test_nodeclasses(self):
# Zero arguments constructor explicitly allowed (but deprecated)
with self.assertWarns(DeprecationWarning):
# Zero arguments constructor is not allowed
msg = "missing 3 required positional arguments: 'left', 'op', and 'right'"
with self.assertRaisesRegex(TypeError, re.escape(msg)):
x = ast.BinOp()
self.assertEqual(x._fields, ('left', 'op', 'right'))

# Random attribute allowed too
x.foobarbaz = 5
self.assertEqual(x.foobarbaz, 5)

n1 = ast.Constant(1)
n3 = ast.Constant(3)
addop = ast.Add()
x = ast.BinOp(n1, addop, n3)
self.assertEqual(x._fields, ('left', 'op', 'right'))
self.assertEqual(x.left, n1)
self.assertEqual(x.op, addop)
self.assertEqual(x.right, n3)

# Random attribute allowed too
x.foobarbaz = 5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we already cover this, but maybe we should instead:

  • Find a way to construct all nodes "correctly" or have at least one instance of each nodes.
  • For all these instances, check that we can set random attributes and delete them properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, added a test in the style of the existing test_field_attr_existence test.

self.assertEqual(x.foobarbaz, 5)

x = ast.BinOp(1, 2, 3)
self.assertEqual(x.left, 1)
self.assertEqual(x.op, 2)
Expand All @@ -568,10 +569,9 @@ def test_nodeclasses(self):
self.assertEqual(x.right, 3)
self.assertEqual(x.lineno, 0)

# Random kwargs also allowed (but deprecated)
with self.assertWarns(DeprecationWarning):
# Random kwargs are not allowed
with self.assertRaisesRegex(TypeError, "unexpected keyword argument 'foobarbaz'"):
x = ast.BinOp(1, 2, 3, foobarbaz=42)
self.assertEqual(x.foobarbaz, 42)

def test_no_fields(self):
# this used to fail because Sub._fields was None
Expand Down Expand Up @@ -1412,7 +1412,7 @@ def test_replace_reject_missing_field(self):

self.assertRaises(AttributeError, getattr, node, 'id')
self.assertIs(node.ctx, context)
msg = "Name.__replace__ missing 1 keyword argument: 'id'."
msg = "ast.Name.__init__ missing 1 required positional argument: 'id'"
with self.assertRaisesRegex(TypeError, re.escape(msg)):
copy.replace(node)
# assert that there is no side-effect
Expand Down Expand Up @@ -1449,7 +1449,7 @@ def test_replace_reject_known_custom_instance_fields_commits(self):

# explicit rejection of known instance fields
self.assertHasAttr(node, 'extra')
msg = "Name.__replace__ got an unexpected keyword argument 'extra'."
msg = "ast.Name.__init__ got an unexpected keyword argument 'extra'"
with self.assertRaisesRegex(TypeError, re.escape(msg)):
copy.replace(node, extra=1)
# assert that there is no side-effect
Expand All @@ -1463,7 +1463,7 @@ def test_replace_reject_unknown_instance_fields(self):

# explicit rejection of unknown extra fields
self.assertRaises(AttributeError, getattr, node, 'unknown')
msg = "Name.__replace__ got an unexpected keyword argument 'unknown'."
msg = "ast.Name.__init__ got an unexpected keyword argument 'unknown'"
with self.assertRaisesRegex(TypeError, re.escape(msg)):
copy.replace(node, unknown=1)
# assert that there is no side-effect
Expand Down Expand Up @@ -3209,11 +3209,10 @@ def test_FunctionDef(self):
args = ast.arguments()
self.assertEqual(args.args, [])
self.assertEqual(args.posonlyargs, [])
with self.assertWarnsRegex(DeprecationWarning,
with self.assertRaisesRegex(TypeError,
r"FunctionDef\.__init__ missing 1 required positional argument: 'name'"):
node = ast.FunctionDef(args=args)
self.assertNotHasAttr(node, "name")
self.assertEqual(node.decorator_list, [])

node = ast.FunctionDef(name='foo', args=args)
self.assertEqual(node.name, 'foo')
self.assertEqual(node.decorator_list, [])
Expand All @@ -3231,7 +3230,7 @@ def test_expr_context(self):
self.assertEqual(name3.id, "x")
self.assertIsInstance(name3.ctx, ast.Del)

with self.assertWarnsRegex(DeprecationWarning,
with self.assertRaisesRegex(TypeError,
r"Name\.__init__ missing 1 required positional argument: 'id'"):
name3 = ast.Name()

Expand Down Expand Up @@ -3272,20 +3271,19 @@ class MyAttrs(ast.AST):
self.assertEqual(obj.a, 1)
self.assertEqual(obj.b, 2)

with self.assertWarnsRegex(DeprecationWarning,
r"MyAttrs.__init__ got an unexpected keyword argument 'c'."):
with self.assertRaisesRegex(TypeError,
r"MyAttrs.__init__ got an unexpected keyword argument 'c'"):
obj = MyAttrs(c=3)

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

with self.assertWarnsRegex(DeprecationWarning,
r"FieldsAndTypesNoDefault\.__init__ missing 1 required positional argument: 'a'\."):
with self.assertRaisesRegex(TypeError,
r"FieldsAndTypesNoDefault\.__init__ missing 1 required positional argument: 'a'"):
obj = FieldsAndTypesNoDefault()
with self.assertRaises(AttributeError):
obj.a

obj = FieldsAndTypesNoDefault(a=1)
self.assertEqual(obj.a, 1)

Expand All @@ -3296,13 +3294,8 @@ class MoreFieldsThanTypes(ast.AST):
a: int | None = None
b: int | None = None

with self.assertWarnsRegex(
DeprecationWarning,
r"Field 'b' is missing from MoreFieldsThanTypes\._field_types"
):
with self.assertRaisesRegex(TypeError, "Field 'b' is missing"):
obj = MoreFieldsThanTypes()
self.assertIs(obj.a, None)
self.assertIs(obj.b, None)

obj = MoreFieldsThanTypes(a=1, b=2)
self.assertEqual(obj.a, 1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The constructors of node types in the :mod:`ast` module now raise a
:exc:`TypeError` when a required argument is omitted or when a
keyword-argument that does not map to a field on the AST node is passed.
These cases had previously raised a :exc:`DeprecationWarning` since Python
3.13. Patch by Brian Schubert.
Loading
Loading