Skip to content

[Python] Avoid hack in setting __reduce__ attribute of CPPInstance #19222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 1, 2025
Merged
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
17 changes: 17 additions & 0 deletions bindings/pyroot/cppyy/CPyCppyy/src/CPPInstance.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,21 @@ static PySequenceMethods op_as_sequence = {
0, // sq_inplace_repeat
};

std::function<PyObject *(PyObject *)> &CPPInstance::ReduceMethod() {
static std::function<PyObject *(PyObject *)> reducer;
return reducer;
}

PyObject *op_reduce(PyObject *self, PyObject * /*args*/)
{
auto &reducer = CPPInstance::ReduceMethod();
if (!reducer) {
PyErr_SetString(PyExc_NotImplementedError, "");
return nullptr;
}
return reducer(self);
}


//----------------------------------------------------------------------------
static PyMethodDef op_methods[] = {
Expand All @@ -409,6 +424,8 @@ static PyMethodDef op_methods[] = {
(char*)"dispatch to selected overload"},
{(char*)"__smartptr__", (PyCFunction)op_get_smartptr, METH_NOARGS,
(char*)"get associated smart pointer, if any"},
{(char*)"__reduce__", (PyCFunction)op_reduce, METH_NOARGS,
(char*)"reduce method for serialization"},
{(char*)"__reshape__", (PyCFunction)op_reshape, METH_O,
(char*)"cast pointer to 1D array type"},
{(char*)nullptr, nullptr, 0, nullptr}
Expand Down
6 changes: 6 additions & 0 deletions bindings/pyroot/cppyy/CPyCppyy/src/CPPInstance.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "CallContext.h" // for Parameter

// Standard
#include <functional>
#include <utility>
#include <vector>

Expand Down Expand Up @@ -83,6 +84,11 @@ class CPPInstance {
void CastToArray(Py_ssize_t sz);
Py_ssize_t ArrayLength();

// implementation of the __reduce__ method: doesn't wrap any function by
// default but can be re-assigned by libraries that add C++ object
// serialization support, like ROOT
static std::function<PyObject *(PyObject *)> &ReduceMethod();

private:
void CreateExtension();
void* GetExtendedObject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@
# For the list of contributors see $ROOTSYS/README/CREDITS. #
################################################################################


def pythonize_cppinstance():
import cppyy
from ROOT.libROOTPythonizations import AddCPPInstancePickling

klass = cppyy._backend.CPPInstance
AddCPPInstancePickling()

AddCPPInstancePickling(klass)

# Instant pythonization (executed at `import ROOT` time), no need of a
# decorator. CPPInstance is the base for cppyy instance proxies and thus needs
Expand Down
26 changes: 3 additions & 23 deletions bindings/pyroot/pythonizations/src/CPPInstancePyz.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ PyObject *PyROOT::CPPInstanceExpand(PyObject * /*self*/, PyObject *args)
/// Turn the object proxy instance into a character stream and return for
/// pickle, together with the callable object that can restore the stream
/// into the object proxy instance.
PyObject *op_reduce(PyObject *self, PyObject * /*args*/)
PyObject *op_reduce(PyObject *self)
{
// keep a borrowed reference around to the callable function for expanding;
// because it is borrowed, it means that there can be no pickling during the
Expand Down Expand Up @@ -120,28 +120,8 @@ PyObject *op_reduce(PyObject *self, PyObject * /*args*/)
///
/// The C++ function op_reduce defined above is wrapped in a Python method
/// so that it can be injected in CPPInstance
PyObject *PyROOT::AddCPPInstancePickling(PyObject * /*self*/, PyObject *args)
PyObject *PyROOT::AddCPPInstancePickling(PyObject * /*self*/, PyObject * /*args*/)
{
PyObject *pyclass = PyTuple_GetItem(args, 0);

const char *attr = "__reduce__";

PyMethodDef *pdef = new PyMethodDef();
pdef->ml_name = attr;
pdef->ml_meth = (PyCFunction)op_reduce;
pdef->ml_flags = METH_NOARGS;
pdef->ml_doc = nullptr;

PyObject *func = PyCFunction_New(pdef, nullptr);
PyObject *method = CustomInstanceMethod_New(func, nullptr, pyclass);

// here PyObject_GenericSetAttr is used because CPPInstance does not allow
// attribute assignment using PyObject_SetAttr
// for more info refer to:
// https://bitbucket.org/wlav/cppyy/issues/110/user-defined-classes-in-c-dont-seem-to-be
PyObject_GenericSetAttr(pyclass, PyUnicode_FromString(attr), method);
Py_DECREF(method);
Py_DECREF(func);

CPPInstance::ReduceMethod() = op_reduce;
Py_RETURN_NONE;
}
2 changes: 1 addition & 1 deletion bindings/pyroot/pythonizations/src/PyROOTModule.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ PyObject *PyObjRefCounterAsStdAny(PyObject * /*self*/, PyObject *args)

// Methods offered by the interface
static PyMethodDef gPyROOTMethods[] = {
{(char *)"AddCPPInstancePickling", (PyCFunction)PyROOT::AddCPPInstancePickling, METH_VARARGS,
{(char *)"AddCPPInstancePickling", (PyCFunction)PyROOT::AddCPPInstancePickling, METH_NOARGS,
(char *)"Add a custom pickling mechanism for Cppyy Python proxy objects"},
{(char *)"GetBranchAttr", (PyCFunction)PyROOT::GetBranchAttr, METH_VARARGS,
(char *)"Allow to access branches as tree attributes"},
Expand Down
Loading