Skip to content

Commit 92bf869

Browse files
bpo-43413: Fix handling keyword arguments in subclasses of some buitin classes (GH-26456)
* Constructors of subclasses of some buitin classes (e.g. tuple, list, frozenset) no longer accept arbitrary keyword arguments. * Subclass of set can now define a __new__() method with additional keyword parameters without overriding also __init__().
1 parent 5277ffe commit 92bf869

26 files changed

+285
-67
lines changed

Lib/test/seq_tests.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ def __getitem__(self, i):
145145
self.assertEqual(self.type2test(LyingTuple((2,))), self.type2test((1,)))
146146
self.assertEqual(self.type2test(LyingList([2])), self.type2test([1]))
147147

148+
with self.assertRaises(TypeError):
149+
self.type2test(unsupported_arg=[])
150+
148151
def test_truth(self):
149152
self.assertFalse(self.type2test())
150153
self.assertTrue(self.type2test([42]))

Lib/test/test_float.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,33 @@ def test_keyword_args(self):
245245
with self.assertRaisesRegex(TypeError, 'keyword argument'):
246246
float(x='3.14')
247247

248+
def test_keywords_in_subclass(self):
249+
class subclass(float):
250+
pass
251+
u = subclass(2.5)
252+
self.assertIs(type(u), subclass)
253+
self.assertEqual(float(u), 2.5)
254+
with self.assertRaises(TypeError):
255+
subclass(x=0)
256+
257+
class subclass_with_init(float):
258+
def __init__(self, arg, newarg=None):
259+
self.newarg = newarg
260+
u = subclass_with_init(2.5, newarg=3)
261+
self.assertIs(type(u), subclass_with_init)
262+
self.assertEqual(float(u), 2.5)
263+
self.assertEqual(u.newarg, 3)
264+
265+
class subclass_with_new(float):
266+
def __new__(cls, arg, newarg=None):
267+
self = super().__new__(cls, arg)
268+
self.newarg = newarg
269+
return self
270+
u = subclass_with_new(2.5, newarg=3)
271+
self.assertIs(type(u), subclass_with_new)
272+
self.assertEqual(float(u), 2.5)
273+
self.assertEqual(u.newarg, 3)
274+
248275
def test_is_integer(self):
249276
self.assertFalse((1.1).is_integer())
250277
self.assertTrue((1.).is_integer())

Lib/test/test_itertools.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2229,16 +2229,57 @@ def __eq__(self, other):
22292229
class SubclassWithKwargsTest(unittest.TestCase):
22302230
def test_keywords_in_subclass(self):
22312231
# count is not subclassable...
2232-
for cls in (repeat, zip, filter, filterfalse, chain, map,
2233-
starmap, islice, takewhile, dropwhile, cycle, compress):
2234-
class Subclass(cls):
2235-
def __init__(self, newarg=None, *args):
2236-
cls.__init__(self, *args)
2237-
try:
2238-
Subclass(newarg=1)
2239-
except TypeError as err:
2240-
# we expect type errors because of wrong argument count
2241-
self.assertNotIn("keyword arguments", err.args[0])
2232+
testcases = [
2233+
(repeat, (1, 2), [1, 1]),
2234+
(zip, ([1, 2], 'ab'), [(1, 'a'), (2, 'b')]),
2235+
(filter, (None, [0, 1]), [1]),
2236+
(filterfalse, (None, [0, 1]), [0]),
2237+
(chain, ([1, 2], [3, 4]), [1, 2, 3]),
2238+
(map, (str, [1, 2]), ['1', '2']),
2239+
(starmap, (operator.pow, ((2, 3), (3, 2))), [8, 9]),
2240+
(islice, ([1, 2, 3, 4], 1, 3), [2, 3]),
2241+
(takewhile, (isEven, [2, 3, 4]), [2]),
2242+
(dropwhile, (isEven, [2, 3, 4]), [3, 4]),
2243+
(cycle, ([1, 2],), [1, 2, 1]),
2244+
(compress, ('ABC', [1, 0, 1]), ['A', 'C']),
2245+
]
2246+
for cls, args, result in testcases:
2247+
with self.subTest(cls):
2248+
class subclass(cls):
2249+
pass
2250+
u = subclass(*args)
2251+
self.assertIs(type(u), subclass)
2252+
self.assertEqual(list(islice(u, 0, 3)), result)
2253+
with self.assertRaises(TypeError):
2254+
subclass(*args, newarg=3)
2255+
2256+
for cls, args, result in testcases:
2257+
# Constructors of repeat, zip, compress accept keyword arguments.
2258+
# Their subclasses need overriding __new__ to support new
2259+
# keyword arguments.
2260+
if cls in [repeat, zip, compress]:
2261+
continue
2262+
with self.subTest(cls):
2263+
class subclass_with_init(cls):
2264+
def __init__(self, *args, newarg=None):
2265+
self.newarg = newarg
2266+
u = subclass_with_init(*args, newarg=3)
2267+
self.assertIs(type(u), subclass_with_init)
2268+
self.assertEqual(list(islice(u, 0, 3)), result)
2269+
self.assertEqual(u.newarg, 3)
2270+
2271+
for cls, args, result in testcases:
2272+
with self.subTest(cls):
2273+
class subclass_with_new(cls):
2274+
def __new__(cls, *args, newarg=None):
2275+
self = super().__new__(cls, *args)
2276+
self.newarg = newarg
2277+
return self
2278+
u = subclass_with_new(*args, newarg=3)
2279+
self.assertIs(type(u), subclass_with_new)
2280+
self.assertEqual(list(islice(u, 0, 3)), result)
2281+
self.assertEqual(u.newarg, 3)
2282+
22422283

22432284
@support.cpython_only
22442285
class SizeofTest(unittest.TestCase):

Lib/test/test_list.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,34 @@ def test_keyword_args(self):
4646
with self.assertRaisesRegex(TypeError, 'keyword argument'):
4747
list(sequence=[])
4848

49+
def test_keywords_in_subclass(self):
50+
class subclass(list):
51+
pass
52+
u = subclass([1, 2])
53+
self.assertIs(type(u), subclass)
54+
self.assertEqual(list(u), [1, 2])
55+
with self.assertRaises(TypeError):
56+
subclass(sequence=())
57+
58+
class subclass_with_init(list):
59+
def __init__(self, seq, newarg=None):
60+
super().__init__(seq)
61+
self.newarg = newarg
62+
u = subclass_with_init([1, 2], newarg=3)
63+
self.assertIs(type(u), subclass_with_init)
64+
self.assertEqual(list(u), [1, 2])
65+
self.assertEqual(u.newarg, 3)
66+
67+
class subclass_with_new(list):
68+
def __new__(cls, seq, newarg=None):
69+
self = super().__new__(cls, seq)
70+
self.newarg = newarg
71+
return self
72+
u = subclass_with_new([1, 2], newarg=3)
73+
self.assertIs(type(u), subclass_with_new)
74+
self.assertEqual(list(u), [1, 2])
75+
self.assertEqual(u.newarg, 3)
76+
4977
def test_truth(self):
5078
super().test_truth()
5179
self.assertTrue(not [])

Lib/test/test_set.py

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -644,15 +644,34 @@ class TestSetSubclass(TestSet):
644644
thetype = SetSubclass
645645
basetype = set
646646

647-
class SetSubclassWithKeywordArgs(set):
648-
def __init__(self, iterable=[], newarg=None):
649-
set.__init__(self, iterable)
650-
651-
class TestSetSubclassWithKeywordArgs(TestSet):
652-
653647
def test_keywords_in_subclass(self):
654-
'SF bug #1486663 -- this used to erroneously raise a TypeError'
655-
SetSubclassWithKeywordArgs(newarg=1)
648+
class subclass(set):
649+
pass
650+
u = subclass([1, 2])
651+
self.assertIs(type(u), subclass)
652+
self.assertEqual(set(u), {1, 2})
653+
with self.assertRaises(TypeError):
654+
subclass(sequence=())
655+
656+
class subclass_with_init(set):
657+
def __init__(self, arg, newarg=None):
658+
super().__init__(arg)
659+
self.newarg = newarg
660+
u = subclass_with_init([1, 2], newarg=3)
661+
self.assertIs(type(u), subclass_with_init)
662+
self.assertEqual(set(u), {1, 2})
663+
self.assertEqual(u.newarg, 3)
664+
665+
class subclass_with_new(set):
666+
def __new__(cls, arg, newarg=None):
667+
self = super().__new__(cls, arg)
668+
self.newarg = newarg
669+
return self
670+
u = subclass_with_new([1, 2], newarg=3)
671+
self.assertIs(type(u), subclass_with_new)
672+
self.assertEqual(set(u), {1, 2})
673+
self.assertEqual(u.newarg, 3)
674+
656675

657676
class TestFrozenSet(TestJointOps, unittest.TestCase):
658677
thetype = frozenset
@@ -734,6 +753,33 @@ class TestFrozenSetSubclass(TestFrozenSet):
734753
thetype = FrozenSetSubclass
735754
basetype = frozenset
736755

756+
def test_keywords_in_subclass(self):
757+
class subclass(frozenset):
758+
pass
759+
u = subclass([1, 2])
760+
self.assertIs(type(u), subclass)
761+
self.assertEqual(set(u), {1, 2})
762+
with self.assertRaises(TypeError):
763+
subclass(sequence=())
764+
765+
class subclass_with_init(frozenset):
766+
def __init__(self, arg, newarg=None):
767+
self.newarg = newarg
768+
u = subclass_with_init([1, 2], newarg=3)
769+
self.assertIs(type(u), subclass_with_init)
770+
self.assertEqual(set(u), {1, 2})
771+
self.assertEqual(u.newarg, 3)
772+
773+
class subclass_with_new(frozenset):
774+
def __new__(cls, arg, newarg=None):
775+
self = super().__new__(cls, arg)
776+
self.newarg = newarg
777+
return self
778+
u = subclass_with_new([1, 2], newarg=3)
779+
self.assertIs(type(u), subclass_with_new)
780+
self.assertEqual(set(u), {1, 2})
781+
self.assertEqual(u.newarg, 3)
782+
737783
def test_constructor_identity(self):
738784
s = self.thetype(range(3))
739785
t = self.thetype(s)

Lib/test/test_tuple.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,33 @@ def test_keyword_args(self):
4242
with self.assertRaisesRegex(TypeError, 'keyword argument'):
4343
tuple(sequence=())
4444

45+
def test_keywords_in_subclass(self):
46+
class subclass(tuple):
47+
pass
48+
u = subclass([1, 2])
49+
self.assertIs(type(u), subclass)
50+
self.assertEqual(list(u), [1, 2])
51+
with self.assertRaises(TypeError):
52+
subclass(sequence=())
53+
54+
class subclass_with_init(tuple):
55+
def __init__(self, arg, newarg=None):
56+
self.newarg = newarg
57+
u = subclass_with_init([1, 2], newarg=3)
58+
self.assertIs(type(u), subclass_with_init)
59+
self.assertEqual(list(u), [1, 2])
60+
self.assertEqual(u.newarg, 3)
61+
62+
class subclass_with_new(tuple):
63+
def __new__(cls, arg, newarg=None):
64+
self = super().__new__(cls, arg)
65+
self.newarg = newarg
66+
return self
67+
u = subclass_with_new([1, 2], newarg=3)
68+
self.assertIs(type(u), subclass_with_new)
69+
self.assertEqual(list(u), [1, 2])
70+
self.assertEqual(u.newarg, 3)
71+
4572
def test_truth(self):
4673
super().test_truth()
4774
self.assertTrue(not ())
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Constructors of subclasses of some buitin classes (e.g. :class:`tuple`,
2+
:class:`list`, :class:`frozenset`) no longer accept arbitrary keyword
3+
arguments. Subclass of :class:`set` can now define a ``__new__()`` method
4+
with additional keyword parameters without overriding also ``__init__()``.

Modules/_io/clinic/bufferedio.c.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,8 @@ _io_BufferedRWPair___init__(PyObject *self, PyObject *args, PyObject *kwargs)
553553
PyObject *writer;
554554
Py_ssize_t buffer_size = DEFAULT_BUFFER_SIZE;
555555

556-
if (Py_IS_TYPE(self, &PyBufferedRWPair_Type) &&
556+
if ((Py_IS_TYPE(self, &PyBufferedRWPair_Type) ||
557+
Py_TYPE(self)->tp_new == PyBufferedRWPair_Type.tp_new) &&
557558
!_PyArg_NoKeywords("BufferedRWPair", kwargs)) {
558559
goto exit;
559560
}
@@ -637,4 +638,4 @@ _io_BufferedRandom___init__(PyObject *self, PyObject *args, PyObject *kwargs)
637638
exit:
638639
return return_value;
639640
}
640-
/*[clinic end generated code: output=98ccf7610c0e82ba input=a9049054013a1b77]*/
641+
/*[clinic end generated code: output=79138a088729b5ee input=a9049054013a1b77]*/

Modules/_randommodule.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,8 +523,9 @@ random_init(RandomObject *self, PyObject *args, PyObject *kwds)
523523
PyObject *arg = NULL;
524524
_randomstate *state = _randomstate_type(Py_TYPE(self));
525525

526-
if (Py_IS_TYPE(self, (PyTypeObject *)state->Random_Type) &&
527-
!_PyArg_NoKeywords("Random()", kwds)) {
526+
if ((Py_IS_TYPE(self, (PyTypeObject *)state->Random_Type) ||
527+
Py_TYPE(self)->tp_init == ((PyTypeObject*)state->Random_Type)->tp_init) &&
528+
!_PyArg_NoKeywords("Random", kwds)) {
528529
return -1;
529530
}
530531

Modules/_sqlite/clinic/cursor.c.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ pysqlite_cursor_init(PyObject *self, PyObject *args, PyObject *kwargs)
1212
int return_value = -1;
1313
pysqlite_Connection *connection;
1414

15-
if (Py_IS_TYPE(self, clinic_state()->CursorType) &&
15+
if ((Py_IS_TYPE(self, clinic_state()->CursorType) ||
16+
Py_TYPE(self)->tp_new == clinic_state()->CursorType->tp_new) &&
1617
!_PyArg_NoKeywords("Cursor", kwargs)) {
1718
goto exit;
1819
}
@@ -299,4 +300,4 @@ pysqlite_cursor_close(pysqlite_Cursor *self, PyTypeObject *cls, PyObject *const
299300
exit:
300301
return return_value;
301302
}
302-
/*[clinic end generated code: output=ace31a7481aa3f41 input=a9049054013a1b77]*/
303+
/*[clinic end generated code: output=3b5328c1619b7626 input=a9049054013a1b77]*/

Modules/_sqlite/clinic/row.c.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ pysqlite_row_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
1313
pysqlite_Cursor *cursor;
1414
PyObject *data;
1515

16-
if ((type == clinic_state()->RowType) &&
16+
if ((type == clinic_state()->RowType ||
17+
type->tp_init == clinic_state()->RowType->tp_init) &&
1718
!_PyArg_NoKeywords("Row", kwargs)) {
1819
goto exit;
1920
}
@@ -53,4 +54,4 @@ pysqlite_row_keys(pysqlite_Row *self, PyObject *Py_UNUSED(ignored))
5354
{
5455
return pysqlite_row_keys_impl(self);
5556
}
56-
/*[clinic end generated code: output=0382771b4fc85f36 input=a9049054013a1b77]*/
57+
/*[clinic end generated code: output=9d54919dbb4ba5f1 input=a9049054013a1b77]*/

Modules/arraymodule.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2617,7 +2617,9 @@ array_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
26172617
PyObject *initial = NULL, *it = NULL;
26182618
const struct arraydescr *descr;
26192619

2620-
if (type == state->ArrayType && !_PyArg_NoKeywords("array.array", kwds))
2620+
if ((type == state->ArrayType ||
2621+
type->tp_init == state->ArrayType->tp_init) &&
2622+
!_PyArg_NoKeywords("array.array", kwds))
26212623
return NULL;
26222624

26232625
if (!PyArg_ParseTuple(args, "C|O:array", &c, &initial))

Modules/clinic/_collectionsmodule.c.h

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

Modules/clinic/_queuemodule.c.h

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

0 commit comments

Comments
 (0)