Skip to content

Commit d834320

Browse files
committed
Promote deprecation warnings to errors
1 parent af15e1d commit d834320

File tree

3 files changed

+213
-84
lines changed

3 files changed

+213
-84
lines changed

Lib/test/test_ast/test_ast.py

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -458,14 +458,13 @@ def test_field_attr_writable(self):
458458
self.assertEqual(x._fields, 666)
459459

460460
def test_classattrs(self):
461-
with self.assertWarns(DeprecationWarning):
461+
msg = "Constant.__init__ missing 1 required positional argument: 'value'"
462+
with self.assertRaisesRegex(TypeError, re.escape(msg)):
462463
x = ast.Constant()
463-
self.assertEqual(x._fields, ('value', 'kind'))
464-
465-
with self.assertRaises(AttributeError):
466-
x.value
467464

468465
x = ast.Constant(42)
466+
self.assertEqual(x._fields, ('value', 'kind'))
467+
469468
self.assertEqual(x.value, 42)
470469

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

488-
# Arbitrary keyword arguments are supported (but deprecated)
489-
with self.assertWarns(DeprecationWarning):
490-
self.assertEqual(ast.Constant(1, foo='bar').foo, 'bar')
487+
# Arbitrary keyword arguments are not supported
488+
msg = "Constant.__init__ got an unexpected keyword argument 'foo'"
489+
with self.assertRaisesRegex(TypeError, re.escape(msg)):
490+
ast.Constant(1, foo='bar')
491491

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

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

540536
n1 = ast.Constant(1)
541537
n3 = ast.Constant(3)
542538
addop = ast.Add()
543539
x = ast.BinOp(n1, addop, n3)
540+
self.assertEqual(x._fields, ('left', 'op', 'right'))
544541
self.assertEqual(x.left, n1)
545542
self.assertEqual(x.op, addop)
546543
self.assertEqual(x.right, n3)
547544

545+
# Random attribute allowed too
546+
x.foobarbaz = 5
547+
self.assertEqual(x.foobarbaz, 5)
548+
548549
x = ast.BinOp(1, 2, 3)
549550
self.assertEqual(x.left, 1)
550551
self.assertEqual(x.op, 2)
@@ -568,10 +569,9 @@ def test_nodeclasses(self):
568569
self.assertEqual(x.right, 3)
569570
self.assertEqual(x.lineno, 0)
570571

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

576576
def test_no_fields(self):
577577
# this used to fail because Sub._fields was None
@@ -3209,11 +3209,10 @@ def test_FunctionDef(self):
32093209
args = ast.arguments()
32103210
self.assertEqual(args.args, [])
32113211
self.assertEqual(args.posonlyargs, [])
3212-
with self.assertWarnsRegex(DeprecationWarning,
3212+
with self.assertRaisesRegex(TypeError,
32133213
r"FunctionDef\.__init__ missing 1 required positional argument: 'name'"):
32143214
node = ast.FunctionDef(args=args)
3215-
self.assertNotHasAttr(node, "name")
3216-
self.assertEqual(node.decorator_list, [])
3215+
32173216
node = ast.FunctionDef(name='foo', args=args)
32183217
self.assertEqual(node.name, 'foo')
32193218
self.assertEqual(node.decorator_list, [])
@@ -3231,7 +3230,7 @@ def test_expr_context(self):
32313230
self.assertEqual(name3.id, "x")
32323231
self.assertIsInstance(name3.ctx, ast.Del)
32333232

3234-
with self.assertWarnsRegex(DeprecationWarning,
3233+
with self.assertRaisesRegex(TypeError,
32353234
r"Name\.__init__ missing 1 required positional argument: 'id'"):
32363235
name3 = ast.Name()
32373236

@@ -3272,20 +3271,19 @@ class MyAttrs(ast.AST):
32723271
self.assertEqual(obj.a, 1)
32733272
self.assertEqual(obj.b, 2)
32743273

3275-
with self.assertWarnsRegex(DeprecationWarning,
3276-
r"MyAttrs.__init__ got an unexpected keyword argument 'c'."):
3274+
with self.assertRaisesRegex(TypeError,
3275+
r"MyAttrs.__init__ got an unexpected keyword argument 'c'"):
32773276
obj = MyAttrs(c=3)
32783277

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

3284-
with self.assertWarnsRegex(DeprecationWarning,
3285-
r"FieldsAndTypesNoDefault\.__init__ missing 1 required positional argument: 'a'\."):
3283+
with self.assertRaisesRegex(TypeError,
3284+
r"FieldsAndTypesNoDefault\.__init__ missing 1 required positional argument: 'a'"):
32863285
obj = FieldsAndTypesNoDefault()
3287-
with self.assertRaises(AttributeError):
3288-
obj.a
3286+
32893287
obj = FieldsAndTypesNoDefault(a=1)
32903288
self.assertEqual(obj.a, 1)
32913289

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

3299-
with self.assertWarnsRegex(
3300-
DeprecationWarning,
3301-
r"Field 'b' is missing from MoreFieldsThanTypes\._field_types"
3302-
):
3297+
with self.assertRaisesRegex(TypeError, "Field 'b' is missing"):
33033298
obj = MoreFieldsThanTypes()
3304-
self.assertIs(obj.a, None)
3305-
self.assertIs(obj.b, None)
33063299

33073300
obj = MoreFieldsThanTypes(a=1, b=2)
33083301
self.assertEqual(obj.a, 1)

Parser/asdl_c.py

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,69 @@ def visitModule(self, mod):
873873
return 0;
874874
}
875875
876+
/*
877+
* Format the names in the set 'missing' into a natural language list,
878+
* sorted in the order in which they appear in 'fields'.
879+
*
880+
* Similar to format_missing from 'Python/ceval.c'.
881+
*
882+
* Parameters
883+
884+
* missing Set of missing field names to render.
885+
* fields Sequence of AST node field names (self._fields).
886+
*/
887+
static PyObject *
888+
format_missing(PyObject *missing, PyObject *fields)
889+
{
890+
Py_ssize_t num_fields, num_total, num_left;
891+
num_fields = PySequence_Size(fields);
892+
if (num_fields == -1) {
893+
return NULL;
894+
}
895+
num_total = num_left = PySet_GET_SIZE(missing);
896+
PyObject *name_str = PyUnicode_FromString("");
897+
// Iterate all AST node fields in order so that the missing positional
898+
// arguments are rendered in the order in which __init__ expects them.
899+
for (Py_ssize_t i = 0; i < num_fields; i++) {
900+
PyObject *name = PySequence_GetItem(fields, i);
901+
if (!name) {
902+
Py_DECREF(name_str);
903+
return NULL;
904+
}
905+
int contains = PySet_Contains(missing, name);
906+
if (contains == -1) {
907+
Py_DECREF(name_str);
908+
Py_DECREF(name);
909+
return NULL;
910+
}
911+
else if (contains == 1) {
912+
const char* fmt = NULL;
913+
if (num_left == 1) {
914+
fmt = "'%U'";
915+
}
916+
else if (num_total == 2) {
917+
fmt = "'%U' and ";
918+
}
919+
else if (num_left == 2) {
920+
fmt = "'%U', and ";
921+
}
922+
else {
923+
fmt = "'%U', ";
924+
}
925+
num_left--;
926+
PyObject *tmp = PyUnicode_FromFormat(fmt, name);
927+
if (!tmp) {
928+
Py_DECREF(name_str);
929+
Py_DECREF(name);
930+
return NULL;
931+
}
932+
name_str = PyUnicode_Concat(name_str, tmp);
933+
}
934+
Py_DECREF(name);
935+
}
936+
return name_str;
937+
}
938+
876939
static int
877940
ast_type_init(PyObject *self, PyObject *args, PyObject *kw)
878941
{
@@ -963,16 +1026,11 @@ def visitModule(self, mod):
9631026
goto cleanup;
9641027
}
9651028
else if (contains == 0) {
966-
if (PyErr_WarnFormat(
967-
PyExc_DeprecationWarning, 1,
968-
"%.400s.__init__ got an unexpected keyword argument '%U'. "
969-
"Support for arbitrary keyword arguments is deprecated "
970-
"and will be removed in Python 3.15.",
971-
Py_TYPE(self)->tp_name, key
972-
) < 0) {
973-
res = -1;
974-
goto cleanup;
975-
}
1029+
PyErr_Format(PyExc_TypeError,
1030+
"%T.__init__ got an unexpected keyword "
1031+
"argument '%U'", self, key);
1032+
res = -1;
1033+
goto cleanup;
9761034
}
9771035
}
9781036
res = PyObject_SetAttr(self, key, value);
@@ -982,7 +1040,7 @@ def visitModule(self, mod):
9821040
}
9831041
}
9841042
Py_ssize_t size = PySet_Size(remaining_fields);
985-
PyObject *field_types = NULL, *remaining_list = NULL;
1043+
PyObject *field_types = NULL, *remaining_list = NULL, *missing_names = NULL;
9861044
if (size > 0) {
9871045
if (PyObject_GetOptionalAttr((PyObject*)Py_TYPE(self), &_Py_ID(_field_types),
9881046
&field_types) < 0) {
@@ -999,6 +1057,10 @@ def visitModule(self, mod):
9991057
if (!remaining_list) {
10001058
goto set_remaining_cleanup;
10011059
}
1060+
missing_names = PySet_New(NULL);
1061+
if (!missing_names) {
1062+
goto set_remaining_cleanup;
1063+
}
10021064
for (Py_ssize_t i = 0; i < size; i++) {
10031065
PyObject *name = PyList_GET_ITEM(remaining_list, i);
10041066
PyObject *type = PyDict_GetItemWithError(field_types, name);
@@ -1007,14 +1069,10 @@ def visitModule(self, mod):
10071069
goto set_remaining_cleanup;
10081070
}
10091071
else {
1010-
if (PyErr_WarnFormat(
1011-
PyExc_DeprecationWarning, 1,
1012-
"Field '%U' is missing from %.400s._field_types. "
1013-
"This will become an error in Python 3.15.",
1014-
name, Py_TYPE(self)->tp_name
1015-
) < 0) {
1016-
goto set_remaining_cleanup;
1017-
}
1072+
PyErr_Format(PyExc_TypeError,
1073+
"Field '%U' is missing from %T._field_types",
1074+
name, self);
1075+
goto set_remaining_cleanup;
10181076
}
10191077
}
10201078
else if (_PyUnion_Check(type)) {
@@ -1042,16 +1100,25 @@ def visitModule(self, mod):
10421100
}
10431101
else {
10441102
// simple field (e.g., identifier)
1045-
if (PyErr_WarnFormat(
1046-
PyExc_DeprecationWarning, 1,
1047-
"%.400s.__init__ missing 1 required positional argument: '%U'. "
1048-
"This will become an error in Python 3.15.",
1049-
Py_TYPE(self)->tp_name, name
1050-
) < 0) {
1103+
res = PySet_Add(missing_names, name);
1104+
if (res < 0) {
10511105
goto set_remaining_cleanup;
10521106
}
10531107
}
10541108
}
1109+
Py_ssize_t num_missing = PySet_GET_SIZE(missing_names);
1110+
if (num_missing > 0) {
1111+
PyObject* name_str = format_missing(missing_names, fields);
1112+
if (!name_str) {
1113+
goto set_remaining_cleanup;
1114+
}
1115+
PyErr_Format(PyExc_TypeError,
1116+
"%T.__init__ missing %d required positional argument%s: %U",
1117+
self, num_missing, num_missing == 1 ? "" : "s", name_str);
1118+
Py_DECREF(name_str);
1119+
goto set_remaining_cleanup;
1120+
}
1121+
Py_DECREF(missing_names);
10551122
Py_DECREF(remaining_list);
10561123
Py_DECREF(field_types);
10571124
}
@@ -1061,6 +1128,7 @@ def visitModule(self, mod):
10611128
Py_XDECREF(remaining_fields);
10621129
return res;
10631130
set_remaining_cleanup:
1131+
Py_XDECREF(missing_names);
10641132
Py_XDECREF(remaining_list);
10651133
Py_XDECREF(field_types);
10661134
res = -1;

0 commit comments

Comments
 (0)