gh-117486: Improve behavior for user-defined AST subclasses (#118212)

Now, such classes will no longer require changes in Python 3.13 in the normal case.
The test suite for robotframework passes with no DeprecationWarnings under this PR.

I also added a new DeprecationWarning for the case where `_field_types` exists
but is incomplete, since that seems likely to indicate a user mistake.
This commit is contained in:
Jelle Zijlstra 2024-05-06 15:57:27 -07:00 committed by GitHub
parent 040571f258
commit e0422198fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 94 additions and 33 deletions

View File

@ -61,7 +61,7 @@ Node classes
.. attribute:: _fields .. attribute:: _fields
Each concrete class has an attribute :attr:`_fields` which gives the names Each concrete class has an attribute :attr:`!_fields` which gives the names
of all child nodes. of all child nodes.
Each instance of a concrete class has one attribute for each child node, Each instance of a concrete class has one attribute for each child node,
@ -74,6 +74,18 @@ Node classes
as Python lists. All possible attributes must be present and have valid as Python lists. All possible attributes must be present and have valid
values when compiling an AST with :func:`compile`. values when compiling an AST with :func:`compile`.
.. attribute:: _field_types
The :attr:`!_field_types` attribute on each concrete class is a dictionary
mapping field names (as also listed in :attr:`_fields`) to their types.
.. doctest::
>>> ast.TypeVar._field_types
{'name': <class 'str'>, 'bound': ast.expr | None, 'default_value': ast.expr | None}
.. versionadded:: 3.13
.. attribute:: lineno .. attribute:: lineno
col_offset col_offset
end_lineno end_lineno

View File

@ -384,6 +384,12 @@ ast
argument that does not map to a field on the AST node is now deprecated, argument that does not map to a field on the AST node is now deprecated,
and will raise an exception in Python 3.15. and will raise an exception in Python 3.15.
These changes do not apply to user-defined subclasses of :class:`ast.AST`,
unless the class opts in to the new behavior by setting the attribute
:attr:`ast.AST._field_types`.
(Contributed by Jelle Zijlstra in :gh:`105858` and :gh:`117486`.)
* :func:`ast.parse` now accepts an optional argument *optimize* * :func:`ast.parse` now accepts an optional argument *optimize*
which is passed on to the :func:`compile` built-in. This makes it which is passed on to the :func:`compile` built-in. This makes it
possible to obtain an optimized AST. possible to obtain an optimized AST.

View File

@ -3036,7 +3036,7 @@ class ASTConstructorTests(unittest.TestCase):
self.assertEqual(node.name, 'foo') self.assertEqual(node.name, 'foo')
self.assertEqual(node.decorator_list, []) self.assertEqual(node.decorator_list, [])
def test_custom_subclass(self): def test_custom_subclass_with_no_fields(self):
class NoInit(ast.AST): class NoInit(ast.AST):
pass pass
@ -3044,17 +3044,17 @@ class ASTConstructorTests(unittest.TestCase):
self.assertIsInstance(obj, NoInit) self.assertIsInstance(obj, NoInit)
self.assertEqual(obj.__dict__, {}) self.assertEqual(obj.__dict__, {})
def test_fields_but_no_field_types(self):
class Fields(ast.AST): class Fields(ast.AST):
_fields = ('a',) _fields = ('a',)
with self.assertWarnsRegex(DeprecationWarning, obj = Fields()
r"Fields provides _fields but not _field_types."):
obj = Fields()
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
obj.a obj.a
obj = Fields(a=1) obj = Fields(a=1)
self.assertEqual(obj.a, 1) self.assertEqual(obj.a, 1)
def test_fields_and_types(self):
class FieldsAndTypes(ast.AST): class FieldsAndTypes(ast.AST):
_fields = ('a',) _fields = ('a',)
_field_types = {'a': int | None} _field_types = {'a': int | None}
@ -3065,6 +3065,7 @@ class ASTConstructorTests(unittest.TestCase):
obj = FieldsAndTypes(a=1) obj = FieldsAndTypes(a=1)
self.assertEqual(obj.a, 1) self.assertEqual(obj.a, 1)
def test_fields_and_types_no_default(self):
class FieldsAndTypesNoDefault(ast.AST): class FieldsAndTypesNoDefault(ast.AST):
_fields = ('a',) _fields = ('a',)
_field_types = {'a': int} _field_types = {'a': int}
@ -3077,6 +3078,38 @@ class ASTConstructorTests(unittest.TestCase):
obj = FieldsAndTypesNoDefault(a=1) obj = FieldsAndTypesNoDefault(a=1)
self.assertEqual(obj.a, 1) self.assertEqual(obj.a, 1)
def test_incomplete_field_types(self):
class MoreFieldsThanTypes(ast.AST):
_fields = ('a', 'b')
_field_types = {'a': int | None}
a: int | None = None
b: int | None = None
with self.assertWarnsRegex(
DeprecationWarning,
r"Field 'b' is missing from MoreFieldsThanTypes\._field_types"
):
obj = MoreFieldsThanTypes()
self.assertIs(obj.a, None)
self.assertIs(obj.b, None)
obj = MoreFieldsThanTypes(a=1, b=2)
self.assertEqual(obj.a, 1)
self.assertEqual(obj.b, 2)
def test_complete_field_types(self):
class _AllFieldTypes(ast.AST):
_fields = ('a', 'b')
_field_types = {'a': int | None, 'b': list[str]}
# This must be set explicitly
a: int | None = None
# This will add an implicit empty list default
b: list[str]
obj = _AllFieldTypes()
self.assertIs(obj.a, None)
self.assertEqual(obj.b, [])
@support.cpython_only @support.cpython_only
class ModuleStateTests(unittest.TestCase): class ModuleStateTests(unittest.TestCase):

View File

@ -0,0 +1,4 @@
Improve the behavior of user-defined subclasses of :class:`ast.AST`. Such
classes will now require no changes in the usual case to conform with the
behavior changes of the :mod:`ast` module in Python 3.13. Patch by Jelle
Zijlstra.

View File

@ -979,14 +979,9 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw)
goto cleanup; goto cleanup;
} }
if (field_types == NULL) { if (field_types == NULL) {
if (PyErr_WarnFormat( // Probably a user-defined subclass of AST that lacks _field_types.
PyExc_DeprecationWarning, 1, // This will continue to work as it did before 3.13; i.e., attributes
"%.400s provides _fields but not _field_types. " // that are not passed in simply do not exist on the instance.
"This will become an error in Python 3.15.",
Py_TYPE(self)->tp_name
) < 0) {
res = -1;
}
goto cleanup; goto cleanup;
} }
remaining_list = PySequence_List(remaining_fields); remaining_list = PySequence_List(remaining_fields);
@ -997,12 +992,21 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw)
PyObject *name = PyList_GET_ITEM(remaining_list, i); PyObject *name = PyList_GET_ITEM(remaining_list, i);
PyObject *type = PyDict_GetItemWithError(field_types, name); PyObject *type = PyDict_GetItemWithError(field_types, name);
if (!type) { if (!type) {
if (!PyErr_Occurred()) { if (PyErr_Occurred()) {
PyErr_SetObject(PyExc_KeyError, name); goto set_remaining_cleanup;
}
else {
if (PyErr_WarnFormat(
PyExc_DeprecationWarning, 1,
"Field '%U' is missing from %.400s._field_types. "
"This will become an error in Python 3.15.",
name, Py_TYPE(self)->tp_name
) < 0) {
goto set_remaining_cleanup;
}
} }
goto set_remaining_cleanup;
} }
if (_PyUnion_Check(type)) { else if (_PyUnion_Check(type)) {
// optional field // optional field
// do nothing, we'll have set a None default on the class // do nothing, we'll have set a None default on the class
} }
@ -1026,8 +1030,7 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw)
"This will become an error in Python 3.15.", "This will become an error in Python 3.15.",
Py_TYPE(self)->tp_name, name Py_TYPE(self)->tp_name, name
) < 0) { ) < 0) {
res = -1; goto set_remaining_cleanup;
goto cleanup;
} }
} }
} }

31
Python/Python-ast.c generated
View File

@ -5178,14 +5178,9 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw)
goto cleanup; goto cleanup;
} }
if (field_types == NULL) { if (field_types == NULL) {
if (PyErr_WarnFormat( // Probably a user-defined subclass of AST that lacks _field_types.
PyExc_DeprecationWarning, 1, // This will continue to work as it did before 3.13; i.e., attributes
"%.400s provides _fields but not _field_types. " // that are not passed in simply do not exist on the instance.
"This will become an error in Python 3.15.",
Py_TYPE(self)->tp_name
) < 0) {
res = -1;
}
goto cleanup; goto cleanup;
} }
remaining_list = PySequence_List(remaining_fields); remaining_list = PySequence_List(remaining_fields);
@ -5196,12 +5191,21 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw)
PyObject *name = PyList_GET_ITEM(remaining_list, i); PyObject *name = PyList_GET_ITEM(remaining_list, i);
PyObject *type = PyDict_GetItemWithError(field_types, name); PyObject *type = PyDict_GetItemWithError(field_types, name);
if (!type) { if (!type) {
if (!PyErr_Occurred()) { if (PyErr_Occurred()) {
PyErr_SetObject(PyExc_KeyError, name); goto set_remaining_cleanup;
}
else {
if (PyErr_WarnFormat(
PyExc_DeprecationWarning, 1,
"Field '%U' is missing from %.400s._field_types. "
"This will become an error in Python 3.15.",
name, Py_TYPE(self)->tp_name
) < 0) {
goto set_remaining_cleanup;
}
} }
goto set_remaining_cleanup;
} }
if (_PyUnion_Check(type)) { else if (_PyUnion_Check(type)) {
// optional field // optional field
// do nothing, we'll have set a None default on the class // do nothing, we'll have set a None default on the class
} }
@ -5225,8 +5229,7 @@ ast_type_init(PyObject *self, PyObject *args, PyObject *kw)
"This will become an error in Python 3.15.", "This will become an error in Python 3.15.",
Py_TYPE(self)->tp_name, name Py_TYPE(self)->tp_name, name
) < 0) { ) < 0) {
res = -1; goto set_remaining_cleanup;
goto cleanup;
} }
} }
} }