Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Include/internal/pycore_call.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
36 changes: 36 additions & 0 deletions Lib/test/picklecommon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to move look up count as class varaible
Something like:

class LookupLoggingMeta(type):
     new_lookup_count = 0
    def __getattribute__(cls, name):

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f22a1a5 — moved the count to a class variable on the metaclass.

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"]
Expand Down
16 changes: 16 additions & 0 deletions Lib/test/pickletester.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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
21 changes: 20 additions & 1 deletion Modules/_pickle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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;
}
Expand Down
Loading