mirror of https://github.com/python/cpython
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:
parent
040571f258
commit
e0422198fb
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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.
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue