diff --git a/Include/internal/pycore_call.h b/Include/internal/pycore_call.h index a9db8860e91c06c..017c9576c96ad69 100644 --- a/Include/internal/pycore_call.h +++ b/Include/internal/pycore_call.h @@ -32,7 +32,8 @@ PyAPI_FUNC(PyObject*) _Py_CheckFunctionResult( PyObject *result, const char *where); -extern PyObject* _PyObject_Call_Prepend( +// Export for '_pickle' shared extension. +PyAPI_FUNC(PyObject*) _PyObject_Call_Prepend( PyThreadState *tstate, PyObject *callable, PyObject *obj, diff --git a/Lib/test/picklecommon.py b/Lib/test/picklecommon.py index 5dd56c4fbf9ec81..2f61cf15ee224a2 100644 --- a/Lib/test/picklecommon.py +++ b/Lib/test/picklecommon.py @@ -290,6 +290,42 @@ class MyIntWithNew2(MyIntWithNew): __new__ = int.__new__ +# For test_newobj_metaclass_lookup +class LookupLoggingMeta(type): + # Name of the last class whose __new__ was looked up through the + # metaclass. + last_new_lookup = None + + def __getattribute__(cls, name): + if name == '__new__': + LookupLoggingMeta.last_new_lookup = cls.__name__ + return super().__getattribute__(name) + +class NewInheriting(metaclass=LookupLoggingMeta): + # __new__ is inherited from object, so tp_new is a C slot function. + def __init__(self, value=None): + self.value = value + +class NewDefining(metaclass=LookupLoggingMeta): + # __new__ is defined in Python, so tp_new is slot_tp_new, which + # performs its own __new__ lookup through the metaclass. + def __new__(cls, *args): + return super().__new__(cls) + + def __init__(self, value=None): + self.value = value + +class NewInheritingEx(dict, metaclass=LookupLoggingMeta): + # Like NewInheriting, but pickled with NEWOBJ_EX (protocol 4+): + # non-empty kwargs force the NEWOBJ_EX opcode, and dict's C tp_new + # accepts them while __new__ stays inherited. + def __init__(self, value=None): + self.value = value + + def __getnewargs_ex__(self): + return (), {'ignored': True} + + # For test_newobj_list_slots class SlotList(MyList): __slots__ = ["foo"] diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index c53262e358b48ea..877176b6750a9b2 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -3599,6 +3599,22 @@ def test_newobj_overridden_new(self): self.assertEqual(int(y), 1) self.assertEqual(y.foo, 42) + def test_newobj_metaclass_lookup(self): + # gh-105250: NEWOBJ and NEWOBJ_EX look __new__ up on the class, + # so a custom metaclass __getattribute__ hook observes the lookup. + for cls, min_proto in ((NewInheriting, 2), + (NewDefining, 2), + (NewInheritingEx, 4)): + for proto in protocols[min_proto:]: + with self.subTest(cls=cls, proto=proto): + s = self.dumps(cls(42), proto) + LookupLoggingMeta.last_new_lookup = None + y = self.loads(s) + self.assertIs(type(y), cls) + self.assertEqual(y.value, 42) + self.assertEqual(LookupLoggingMeta.last_new_lookup, + cls.__name__) + def test_newobj_not_class(self): # Issue 24552 if self.py_version < (3, 4): diff --git a/Misc/NEWS.d/next/Library/2026-08-17-11-35-48.gh-issue-105250.wNaXOE.rst b/Misc/NEWS.d/next/Library/2026-08-17-11-35-48.gh-issue-105250.wNaXOE.rst new file mode 100644 index 000000000000000..032ff26bc58172e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-17-11-35-48.gh-issue-105250.wNaXOE.rst @@ -0,0 +1,5 @@ +Fix a difference in behavior between the C and pure Python implementations of +:mod:`pickle` when unpickling with the ``NEWOBJ`` and ``NEWOBJ_EX`` opcodes: +the C implementation now performs an attribute lookup of ``cls.__new__`` when +the class has a custom metaclass, so that a metaclass ``__getattribute__`` +hook observes the lookup as documented. Patched by Chaewon Kim diff --git a/Modules/_pickle.c b/Modules/_pickle.c index 90200a4379319d9..566419d070de477 100644 --- a/Modules/_pickle.c +++ b/Modules/_pickle.c @@ -10,6 +10,7 @@ #include "Python.h" #include "pycore_bytesobject.h" // _PyBytesWriter +#include "pycore_call.h" // _PyObject_Call_Prepend() #include "pycore_ceval.h" // _Py_EnterRecursiveCall() #include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION() #include "pycore_dict.h" // _PyDict_SetItem_Take2() @@ -6328,7 +6329,25 @@ load_newobj(PickleState *state, UnpicklerObject *self, int use_kwargs) goto error; } - obj = ((PyTypeObject *)cls)->tp_new((PyTypeObject *)cls, args, kwargs); + if (Py_TYPE(cls) == &PyType_Type) { + /* Fast path: with the default metaclass, an attribute lookup of + cls.__new__ is not observable, so tp_new can be called + directly. */ + obj = ((PyTypeObject *)cls)->tp_new((PyTypeObject *)cls, args, + kwargs); + } + else { + /* Look __new__ up on the class so that a custom metaclass + __getattribute__ observes the lookup, as in the Python + implementation. */ + PyObject *func = PyObject_GetAttr(cls, &_Py_ID(__new__)); + if (func == NULL) { + goto error; + } + PyThreadState *tstate = _PyThreadState_GET(); + obj = _PyObject_Call_Prepend(tstate, func, cls, args, kwargs); + Py_DECREF(func); + } if (obj == NULL) { goto error; }