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

Skip to content

Commit e042219

Browse files
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.
1 parent 040571f commit e042219

File tree

6 files changed

+94
-33
lines changed

6 files changed

+94
-33
lines changed

Doc/library/ast.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Node classes
6161

6262
.. attribute:: _fields
6363

64-
Each concrete class has an attribute :attr:`_fields` which gives the names
64+
Each concrete class has an attribute :attr:`!_fields` which gives the names
6565
of all child nodes.
6666

6767
Each instance of a concrete class has one attribute for each child node,
@@ -74,6 +74,18 @@ Node classes
7474
as Python lists. All possible attributes must be present and have valid
7575
values when compiling an AST with :func:`compile`.
7676

77+
.. attribute:: _field_types
78+
79+
The :attr:`!_field_types` attribute on each concrete class is a dictionary
80+
mapping field names (as also listed in :attr:`_fields`) to their types.
81+
82+
.. doctest::
83+
84+
>>> ast.TypeVar._field_types
85+
{'name': <class 'str'>, 'bound': ast.expr | None, 'default_value': ast.expr | None}
86+
87+
.. versionadded:: 3.13
88+
7789
.. attribute:: lineno
7890
col_offset
7991
end_lineno

Doc/whatsnew/3.13.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,12 @@ ast
384384
argument that does not map to a field on the AST node is now deprecated,
385385
and will raise an exception in Python 3.15.
386386

387+
These changes do not apply to user-defined subclasses of :class:`ast.AST`,
388+
unless the class opts in to the new behavior by setting the attribute
389+
:attr:`ast.AST._field_types`.
390+
391+
(Contributed by Jelle Zijlstra in :gh:`105858` and :gh:`117486`.)
392+
387393
* :func:`ast.parse` now accepts an optional argument *optimize*
388394
which is passed on to the :func:`compile` built-in. This makes it
389395
possible to obtain an optimized AST.

Lib/test/test_ast.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3036,25 +3036,25 @@ def test_FunctionDef(self):
30363036
self.assertEqual(node.name, 'foo')
30373037
self.assertEqual(node.decorator_list, [])
30383038

3039-
def test_custom_subclass(self):
3039+
def test_custom_subclass_with_no_fields(self):
30403040
class NoInit(ast.AST):
30413041
pass
30423042

30433043
obj = NoInit()
30443044
self.assertIsInstance(obj, NoInit)
30453045
self.assertEqual(obj.__dict__, {})
30463046

3047+
def test_fields_but_no_field_types(self):
30473048
class Fields(ast.AST):
30483049
_fields = ('a',)
30493050

3050-
with self.assertWarnsRegex(DeprecationWarning,
3051-
r"Fields provides _fields but not _field_types."):
3052-
obj = Fields()
3051+
obj = Fields()
30533052
with self.assertRaises(AttributeError):
30543053
obj.a
30553054
obj = Fields(a=1)
30563055
self.assertEqual(obj.a, 1)
30573056

3057+
def test_fields_and_types(self):
30583058
class FieldsAndTypes(ast.AST):
30593059
_fields = ('a',)
30603060
_field_types = {'a': int | None}
@@ -3065,6 +3065,7 @@ class FieldsAndTypes(ast.AST):
30653065
obj = FieldsAndTypes(a=1)
30663066
self.assertEqual(obj.a, 1)
30673067

3068+
def test_fields_and_types_no_default(self):
30683069
class FieldsAndTypesNoDefault(ast.AST):
30693070
_fields = ('a',)
30703071
_field_types = {'a': int}
@@ -3077,6 +3078,38 @@ class FieldsAndTypesNoDefault(ast.AST):
30773078
obj = FieldsAndTypesNoDefault(a=1)
30783079
self.assertEqual(obj.a, 1)
30793080

3081+
def test_incomplete_field_types(self):
3082+
class MoreFieldsThanTypes(ast.AST):
3083+
_fields = ('a', 'b')
3084+
_field_types = {'a': int | None}
3085+
a: int | None = None
3086+
b: int | None = None
3087+
3088+
with self.assertWarnsRegex(
3089+
DeprecationWarning,
3090+
r"Field 'b' is missing from MoreFieldsThanTypes\._field_types"
3091+
):
3092+
obj = MoreFieldsThanTypes()
3093+
self.assertIs(obj.a, None)
3094+
self.assertIs(obj.b, None)
3095+
3096+
obj = MoreFieldsThanTypes(a=1, b=2)
3097+
self.assertEqual(obj.a, 1)
3098+
self.assertEqual(obj.b, 2)
3099+
3100+
def test_complete_field_types(self):
3101+
class _AllFieldTypes(ast.AST):
3102+
_fields = ('a', 'b')
3103+
_field_types = {'a': int | None, 'b': list[str]}
3104+
# This must be set explicitly
3105+
a: int | None = None
3106+
# This will add an implicit empty list default
3107+
b: list[str]
3108+
3109+
obj = _AllFieldTypes()
3110+
self.assertIs(obj.a, None)
3111+
self.assertEqual(obj.b, [])
3112+
30803113

30813114
@support.cpython_only
30823115
class ModuleStateTests(unittest.TestCase):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Improve the behavior of user-defined subclasses of :class:`ast.AST`. Such
2+
classes will now require no changes in the usual case to conform with the
3+
behavior changes of the :mod:`ast` module in Python 3.13. Patch by Jelle
4+
Zijlstra.

Parser/asdl_c.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -979,14 +979,9 @@ def visitModule(self, mod):
979979
goto cleanup;
980980
}
981981
if (field_types == NULL) {
982-
if (PyErr_WarnFormat(
983-
PyExc_DeprecationWarning, 1,
984-
"%.400s provides _fields but not _field_types. "
985-
"This will become an error in Python 3.15.",
986-
Py_TYPE(self)->tp_name
987-
) < 0) {
988-
res = -1;
989-
}
982+
// Probably a user-defined subclass of AST that lacks _field_types.
983+
// This will continue to work as it did before 3.13; i.e., attributes
984+
// that are not passed in simply do not exist on the instance.
990985
goto cleanup;
991986
}
992987
remaining_list = PySequence_List(remaining_fields);
@@ -997,12 +992,21 @@ def visitModule(self, mod):
997992
PyObject *name = PyList_GET_ITEM(remaining_list, i);
998993
PyObject *type = PyDict_GetItemWithError(field_types, name);
999994
if (!type) {
1000-
if (!PyErr_Occurred()) {
1001-
PyErr_SetObject(PyExc_KeyError, name);
995+
if (PyErr_Occurred()) {
996+
goto set_remaining_cleanup;
997+
}
998+
else {
999+
if (PyErr_WarnFormat(
1000+
PyExc_DeprecationWarning, 1,
1001+
"Field '%U' is missing from %.400s._field_types. "
1002+
"This will become an error in Python 3.15.",
1003+
name, Py_TYPE(self)->tp_name
1004+
) < 0) {
1005+
goto set_remaining_cleanup;
1006+
}
10021007
}
1003-
goto set_remaining_cleanup;
10041008
}
1005-
if (_PyUnion_Check(type)) {
1009+
else if (_PyUnion_Check(type)) {
10061010
// optional field
10071011
// do nothing, we'll have set a None default on the class
10081012
}
@@ -1026,8 +1030,7 @@ def visitModule(self, mod):
10261030
"This will become an error in Python 3.15.",
10271031
Py_TYPE(self)->tp_name, name
10281032
) < 0) {
1029-
res = -1;
1030-
goto cleanup;
1033+
goto set_remaining_cleanup;
10311034
}
10321035
}
10331036
}

Python/Python-ast.c

Lines changed: 17 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)