From 0a90b2db712ae8ed65f544ce28db361b11e92118 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Sun, 16 Apr 2017 20:30:52 -0400 Subject: [PATCH] Don't let PyInstanceMethod hide itself Python 3's `PyInstanceMethod_Type` hides itself via its `tp_descr_get`, which prevents aliasing methods via `cls.attr("m2") = cls.attr("m1")`: instead the `tp_descr_get` returns a plain function, when called on a class, or a `PyMethod`, when called on an instance. Override that behaviour for pybind11 types with a special bypass for `PyInstanceMethod_Types`. --- include/pybind11/class_support.h | 21 +++++++++++++++++++++ include/pybind11/common.h | 4 ++++ include/pybind11/pybind11.h | 6 ++---- tests/test_methods_and_attributes.cpp | 7 +++++-- tests/test_methods_and_attributes.py | 18 ++++++++++++++++++ 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/include/pybind11/class_support.h b/include/pybind11/class_support.h index b293037bb..0f16c4acf 100644 --- a/include/pybind11/class_support.h +++ b/include/pybind11/class_support.h @@ -149,6 +149,24 @@ extern "C" inline int pybind11_meta_setattro(PyObject* obj, PyObject* name, PyOb } } +#if PY_MAJOR_VERSION >= 3 +/** Python 3's PyInstanceMethod_Type hides itself via its tp_descr_get, which prevents aliasing + * methods via cls.attr("m2") = cls.attr("m1"): instead the tp_descr_get returns a plain function, + * when called on a class, or a PyMethod, when called on an instance. Override that behaviour here + * to do a special case bypass for PyInstanceMethod_Types. + */ +extern "C" inline PyObject *pybind11_meta_getattro(PyObject *obj, PyObject *name) { + PyObject *descr = _PyType_Lookup((PyTypeObject *) obj, name); + if (descr && PyInstanceMethod_Check(descr)) { + Py_INCREF(descr); + return descr; + } + else { + return PyType_Type.tp_getattro(obj, name); + } +} +#endif + /** This metaclass is assigned by default to all pybind11 types and is required in order for static properties to function correctly. Users may override this using `py::metaclass`. Return value: New reference. */ @@ -176,6 +194,9 @@ inline PyTypeObject* make_default_metaclass() { type->tp_new = pybind11_meta_new; type->tp_setattro = pybind11_meta_setattro; +#if PY_MAJOR_VERSION >= 3 + type->tp_getattro = pybind11_meta_getattro; +#endif if (PyType_Ready(type) < 0) pybind11_fail("make_default_metaclass(): failure in PyType_Ready()!"); diff --git a/include/pybind11/common.h b/include/pybind11/common.h index eb6941002..d9d9d5111 100644 --- a/include/pybind11/common.h +++ b/include/pybind11/common.h @@ -135,6 +135,8 @@ #if PY_MAJOR_VERSION >= 3 /// Compatibility macros for various Python versions #define PYBIND11_INSTANCE_METHOD_NEW(ptr, class_) PyInstanceMethod_New(ptr) +#define PYBIND11_INSTANCE_METHOD_CHECK PyInstanceMethod_Check +#define PYBIND11_INSTANCE_METHOD_GET_FUNCTION PyInstanceMethod_GET_FUNCTION #define PYBIND11_BYTES_CHECK PyBytes_Check #define PYBIND11_BYTES_FROM_STRING PyBytes_FromString #define PYBIND11_BYTES_FROM_STRING_AND_SIZE PyBytes_FromStringAndSize @@ -153,6 +155,8 @@ extern "C" PYBIND11_EXPORT PyObject *PyInit_##name() #else #define PYBIND11_INSTANCE_METHOD_NEW(ptr, class_) PyMethod_New(ptr, nullptr, class_) +#define PYBIND11_INSTANCE_METHOD_CHECK PyMethod_Check +#define PYBIND11_INSTANCE_METHOD_GET_FUNCTION PyMethod_GET_FUNCTION #define PYBIND11_BYTES_CHECK PyString_Check #define PYBIND11_BYTES_FROM_STRING PyString_FromString #define PYBIND11_BYTES_FROM_STRING_AND_SIZE PyString_FromStringAndSize diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 7f4c5175a..03a709b26 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -268,10 +268,8 @@ protected: rec->is_constructor = !strcmp(rec->name, "__init__") || !strcmp(rec->name, "__setstate__"); rec->nargs = (std::uint16_t) args; -#if PY_MAJOR_VERSION < 3 - if (rec->sibling && PyMethod_Check(rec->sibling.ptr())) - rec->sibling = PyMethod_GET_FUNCTION(rec->sibling.ptr()); -#endif + if (rec->sibling && PYBIND11_INSTANCE_METHOD_CHECK(rec->sibling.ptr())) + rec->sibling = PYBIND11_INSTANCE_METHOD_GET_FUNCTION(rec->sibling.ptr()); detail::function_record *chain = nullptr, *chain_start = rec; if (rec->sibling) { diff --git a/tests/test_methods_and_attributes.cpp b/tests/test_methods_and_attributes.cpp index e11bdf222..dc98feeba 100644 --- a/tests/test_methods_and_attributes.cpp +++ b/tests/test_methods_and_attributes.cpp @@ -163,8 +163,8 @@ public: class NotRegistered {}; test_initializer methods_and_attributes([](py::module &m) { - py::class_(m, "ExampleMandA") - .def(py::init<>()) + py::class_ emna(m, "ExampleMandA"); + emna.def(py::init<>()) .def(py::init()) .def(py::init()) .def("add1", &ExampleMandA::add1) @@ -222,6 +222,9 @@ test_initializer methods_and_attributes([](py::module &m) { .def("__str__", &ExampleMandA::toString) .def_readwrite("value", &ExampleMandA::value); + // Issue #443: can't call copied methods in Python 3 + emna.attr("add2b") = emna.attr("add2"); + py::class_(m, "TestProperties") .def(py::init<>()) .def_readonly("def_readonly", &TestProperties::value) diff --git a/tests/test_methods_and_attributes.py b/tests/test_methods_and_attributes.py index 0eaef9ce7..8139decf0 100644 --- a/tests/test_methods_and_attributes.py +++ b/tests/test_methods_and_attributes.py @@ -80,6 +80,24 @@ def test_properties(): assert instance.def_property == 3 +def test_copy_method(): + """Issue #443: calling copied methods fails in Python 3""" + from pybind11_tests import ExampleMandA + + ExampleMandA.add2c = ExampleMandA.add2 + ExampleMandA.add2d = ExampleMandA.add2b + a = ExampleMandA(123) + assert a.value == 123 + a.add2(ExampleMandA(-100)) + assert a.value == 23 + a.add2b(ExampleMandA(20)) + assert a.value == 43 + a.add2c(ExampleMandA(6)) + assert a.value == 49 + a.add2d(ExampleMandA(-7)) + assert a.value == 42 + + def test_static_properties(): from pybind11_tests import TestProperties as Type