Skip to content

Commit 10399f7

Browse files
committed
pythongh-105250: Fix NEWOBJ handling of custom metaclasses in C pickle
The NEWOBJ and NEWOBJ_EX opcodes are documented to call cls.__new__(cls, *args), but the C implementation called tp_new directly, so a metaclass __getattribute__ hook was skipped unless the class happened to define __new__ in Python. Perform a real attribute lookup of cls.__new__ when the class has a custom metaclass, matching the pure Python implementation. The default-metaclass case keeps calling tp_new directly, where the lookup is not observable.
1 parent 7a845ce commit 10399f7

4 files changed

Lines changed: 84 additions & 1 deletion

File tree

‎Lib/test/picklecommon.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,40 @@ class MyIntWithNew2(MyIntWithNew):
290290
__new__ = int.__new__
291291

292292

293+
# For test_newobj_metaclass_lookup
294+
metaclass_new_lookups = []
295+
296+
class LookupLoggingMeta(type):
297+
def __getattribute__(cls, name):
298+
if name == '__new__':
299+
metaclass_new_lookups.append(name)
300+
return super().__getattribute__(name)
301+
302+
class NewInheriting(metaclass=LookupLoggingMeta):
303+
# __new__ is inherited from object, so tp_new is a C slot function.
304+
def __init__(self, value=None):
305+
self.value = value
306+
307+
class NewDefining(metaclass=LookupLoggingMeta):
308+
# __new__ is defined in Python, so tp_new is slot_tp_new, which
309+
# performs its own __new__ lookup through the metaclass.
310+
def __new__(cls, *args):
311+
return super().__new__(cls)
312+
313+
def __init__(self, value=None):
314+
self.value = value
315+
316+
class NewInheritingEx(dict, metaclass=LookupLoggingMeta):
317+
# Like NewInheriting, but pickled with NEWOBJ_EX (protocol 4+):
318+
# non-empty kwargs force the NEWOBJ_EX opcode, and dict's C tp_new
319+
# accepts them while __new__ stays inherited.
320+
def __init__(self, value=None):
321+
self.value = value
322+
323+
def __getnewargs_ex__(self):
324+
return (), {'ignored': True}
325+
326+
293327
# For test_newobj_list_slots
294328
class SlotList(MyList):
295329
__slots__ = ["foo"]

‎Lib/test/pickletester.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3599,6 +3599,21 @@ def test_newobj_overridden_new(self):
35993599
self.assertEqual(int(y), 1)
36003600
self.assertEqual(y.foo, 42)
36013601

3602+
def test_newobj_metaclass_lookup(self):
3603+
# gh-105250: NEWOBJ and NEWOBJ_EX look __new__ up on the class,
3604+
# so a custom metaclass __getattribute__ hook observes the lookup.
3605+
for cls, min_proto in ((NewInheriting, 2),
3606+
(NewDefining, 2),
3607+
(NewInheritingEx, 4)):
3608+
for proto in protocols[min_proto:]:
3609+
with self.subTest(cls=cls, proto=proto):
3610+
s = self.dumps(cls(42), proto)
3611+
metaclass_new_lookups.clear()
3612+
y = self.loads(s)
3613+
self.assertIs(type(y), cls)
3614+
self.assertEqual(y.value, 42)
3615+
self.assertIn('__new__', metaclass_new_lookups)
3616+
36023617
def test_newobj_not_class(self):
36033618
# Issue 24552
36043619
if self.py_version < (3, 4):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a difference in behavior between the C and pure Python implementations of
2+
:mod:`pickle` when unpickling with the ``NEWOBJ`` and ``NEWOBJ_EX`` opcodes:
3+
the C implementation now performs an attribute lookup of ``cls.__new__`` when
4+
the class has a custom metaclass, so that a metaclass ``__getattribute__``
5+
hook observes the lookup as documented.

‎Modules/_pickle.c‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6328,7 +6328,36 @@ load_newobj(PickleState *state, UnpicklerObject *self, int use_kwargs)
63286328
goto error;
63296329
}
63306330

6331-
obj = ((PyTypeObject *)cls)->tp_new((PyTypeObject *)cls, args, kwargs);
6331+
if (Py_TYPE(cls) == &PyType_Type) {
6332+
/* Fast path: with the default metaclass, an attribute lookup of
6333+
cls.__new__ is not observable, so tp_new can be called
6334+
directly. */
6335+
obj = ((PyTypeObject *)cls)->tp_new((PyTypeObject *)cls, args,
6336+
kwargs);
6337+
}
6338+
else {
6339+
/* Look __new__ up on the class so that a custom metaclass
6340+
__getattribute__ observes the lookup, as in the Python
6341+
implementation. */
6342+
PyObject *func = PyObject_GetAttr(cls, &_Py_ID(__new__));
6343+
if (func == NULL) {
6344+
goto error;
6345+
}
6346+
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
6347+
PyObject *newargs = PyTuple_New(nargs + 1);
6348+
if (newargs == NULL) {
6349+
Py_DECREF(func);
6350+
goto error;
6351+
}
6352+
PyTuple_SET_ITEM(newargs, 0, Py_NewRef(cls));
6353+
for (Py_ssize_t i = 0; i < nargs; i++) {
6354+
PyTuple_SET_ITEM(newargs, i + 1,
6355+
Py_NewRef(PyTuple_GET_ITEM(args, i)));
6356+
}
6357+
obj = PyObject_Call(func, newargs, kwargs);
6358+
Py_DECREF(newargs);
6359+
Py_DECREF(func);
6360+
}
63326361
if (obj == NULL) {
63336362
goto error;
63346363
}

0 commit comments

Comments
 (0)