From 3f1ff3f4d1f3fb0298d5bc05cad22ed621d72a5d Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 12 Dec 2016 17:42:52 -0500 Subject: [PATCH] Adds automatic casting on assignment of non-pyobject types (#551) This adds automatic casting when assigning to python types like dict, list, and attributes. Instead of: dict["key"] = py::cast(val); m.attr("foo") = py::cast(true); list.append(py::cast(42)); you can now simply write: dict["key"] = val; m.attr("foo") = true; list.append(42); Casts needing extra parameters (e.g. for a non-default rvp) still require the py::cast() call. set::add() is also supported. All usage is channeled through a SFINAE implementation which either just returns or casts. Combined non-converting handle and autocasting template methods via a helper method that either just returns (handle) or casts (C++ type). --- docs/basics.rst | 10 ++++--- include/pybind11/cast.h | 4 +++ include/pybind11/pytypes.h | 37 ++++++++++++++++++----- tests/pybind11_tests.cpp | 2 +- tests/test_eigen.cpp | 2 +- tests/test_exceptions.cpp | 2 +- tests/test_issues.cpp | 5 ++++ tests/test_python_types.cpp | 58 ++++++++++++++++++++++++++++++------- tests/test_python_types.py | 16 +++++++++- 9 files changed, 110 insertions(+), 26 deletions(-) diff --git a/docs/basics.rst b/docs/basics.rst index 45272b7ed..c1dd47412 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -254,16 +254,18 @@ The shorthand notation is also available for default arguments: Exporting variables =================== -To expose a value from C++, use the ``attr`` function to register it in a module -as shown below. Built-in types and general objects (more on that later) can be +To expose a value from C++, use the ``attr`` function to register it in a +module as shown below. Built-in types and general objects (more on that later) +are automatically converted when assigned as attributes, and can be explicitly converted using the function ``py::cast``. .. code-block:: cpp PYBIND11_PLUGIN(example) { py::module m("example", "pybind11 example plugin"); - m.attr("the_answer") = py::cast(42); - m.attr("what") = py::cast("World"); + m.attr("the_answer") = 42; + py::object world = py::cast("World"); + m.attr("what") = world; return m.ptr(); } diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 6cdc4825d..535516b37 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1120,6 +1120,10 @@ template <> inline void object::cast() && { return; } NAMESPACE_BEGIN(detail) +// Declared in pytypes.h: +template ::value, int>> +object object_or_cast(T &&o) { return pybind11::cast(std::forward(o)); } + struct overload_unused {}; // Placeholder type for the unneeded (and dead code) static variable in the OVERLOAD_INT macro template using overload_caster_t = conditional_t< cast_is_temporary_value_reference::value, make_caster, overload_unused>; diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index d078d58e0..2b49ecfc9 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -43,7 +43,7 @@ using tuple_accessor = accessor; /// Tag and check to identify a class which implements the Python object API class pyobject_tag { }; -template using is_pyobject = std::is_base_of; +template using is_pyobject = std::is_base_of::type>; /// Mixin which adds common functions to handle, object and various accessors. /// The only requirement for `Derived` is to implement `PyObject *Derived::ptr() const`. @@ -81,7 +81,7 @@ NAMESPACE_END(detail) class handle : public detail::object_api { public: handle() = default; - handle(PyObject *ptr) : m_ptr(ptr) { } + handle(PyObject *ptr) : m_ptr(ptr) { } // Allow implicit conversion from PyObject* PyObject *ptr() const { return m_ptr; } PyObject *&ptr() { return m_ptr; } @@ -224,6 +224,18 @@ inline handle get_function(handle value) { return value; } +// Helper aliases/functions to support implicit casting of values given to python accessors/methods. +// When given a pyobject, this simply returns the pyobject as-is; for other C++ type, the value goes +// through pybind11::cast(obj) to convert it to an `object`. +template ::value, int> = 0> +auto object_or_cast(T &&o) -> decltype(std::forward(o)) { return std::forward(o); } +// The following casting version is implemented in cast.h: +template ::value, int> = 0> +object object_or_cast(T &&o); +// Match a PyObject*, which we want to convert directly to handle via its converting constructor +inline handle object_or_cast(PyObject *ptr) { return ptr; } + + template class accessor : public object_api> { using key_type = typename Policy::key_type; @@ -231,12 +243,17 @@ class accessor : public object_api> { public: accessor(handle obj, key_type key) : obj(obj), key(std::move(key)) { } + // accessor overload required to override default assignment operator (templates are not allowed + // to replace default compiler-generated assignments). void operator=(const accessor &a) && { std::move(*this).operator=(handle(a)); } void operator=(const accessor &a) & { operator=(handle(a)); } - void operator=(const object &o) && { std::move(*this).operator=(handle(o)); } - void operator=(const object &o) & { operator=(handle(o)); } - void operator=(handle value) && { Policy::set(obj, key, value); } - void operator=(handle value) & { get_cache() = reinterpret_borrow(value); } + + template void operator=(T &&value) && { + Policy::set(obj, key, object_or_cast(std::forward(value))); + } + template void operator=(T &&value) & { + get_cache() = reinterpret_borrow(object_or_cast(std::forward(value))); + } template PYBIND11_DEPRECATED("Use of obj.attr(...) as bool is deprecated in favor of pybind11::hasattr(obj, ...)") @@ -773,7 +790,9 @@ public: } size_t size() const { return (size_t) PyList_Size(m_ptr); } detail::list_accessor operator[](size_t index) const { return {*this, index}; } - void append(handle h) const { PyList_Append(m_ptr, h.ptr()); } + template void append(T &&val) const { + PyList_Append(m_ptr, detail::object_or_cast(std::forward(val)).ptr()); + } }; class args : public tuple { PYBIND11_OBJECT_DEFAULT(args, tuple, PyTuple_Check) }; @@ -786,7 +805,9 @@ public: if (!m_ptr) pybind11_fail("Could not allocate set object!"); } size_t size() const { return (size_t) PySet_Size(m_ptr); } - bool add(const object &object) const { return PySet_Add(m_ptr, object.ptr()) == 0; } + template bool add(T &&val) const { + return PySet_Add(m_ptr, detail::object_or_cast(std::forward(val)).ptr()) == 0; + } void clear() const { PySet_Clear(m_ptr); } }; diff --git a/tests/pybind11_tests.cpp b/tests/pybind11_tests.cpp index 35981a0a6..9c593eee1 100644 --- a/tests/pybind11_tests.cpp +++ b/tests/pybind11_tests.cpp @@ -39,7 +39,7 @@ PYBIND11_PLUGIN(pybind11_tests) { for (const auto &initializer : initializers()) initializer(m); - if (!py::hasattr(m, "have_eigen")) m.attr("have_eigen") = py::cast(false); + if (!py::hasattr(m, "have_eigen")) m.attr("have_eigen") = false; return m.ptr(); } diff --git a/tests/test_eigen.cpp b/tests/test_eigen.cpp index a9cb9f21c..588cdceb3 100644 --- a/tests/test_eigen.cpp +++ b/tests/test_eigen.cpp @@ -40,7 +40,7 @@ test_initializer eigen([](py::module &m) { typedef Eigen::SparseMatrix SparseMatrixR; typedef Eigen::SparseMatrix SparseMatrixC; - m.attr("have_eigen") = py::cast(true); + m.attr("have_eigen") = true; // Non-symmetric matrix with zero elements Eigen::MatrixXf mat(5, 6); diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index ca2afa642..706b500f2 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -88,7 +88,7 @@ void throws_logic_error() { struct PythonCallInDestructor { PythonCallInDestructor(const py::dict &d) : d(d) {} - ~PythonCallInDestructor() { d["good"] = py::cast(true); } + ~PythonCallInDestructor() { d["good"] = true; } py::dict d; }; diff --git a/tests/test_issues.cpp b/tests/test_issues.cpp index 378da5220..4c59a1b12 100644 --- a/tests/test_issues.cpp +++ b/tests/test_issues.cpp @@ -381,6 +381,11 @@ void init_issues(py::module &m) { .def_static("make", &MyDerived::make) .def_static("make2", &MyDerived::make); + py::dict d; + std::string bar = "bar"; + d["str"] = bar; + d["num"] = 3.7; + /// Issue #528: templated constructor m2.def("tpl_constr_vector", [](std::vector &) {}); m2.def("tpl_constr_map", [](std::unordered_map &) {}); diff --git a/tests/test_python_types.cpp b/tests/test_python_types.cpp index 68e07adb5..33c655b52 100644 --- a/tests/test_python_types.cpp +++ b/tests/test_python_types.cpp @@ -37,7 +37,8 @@ public: py::set get_set() { py::set set; set.add(py::str("key1")); - set.add(py::str("key2")); + set.add("key2"); + set.add(std::string("key3")); return set; } @@ -59,7 +60,7 @@ public: /* Create, manipulate, and return a Python list */ py::list get_list() { py::list list; - list.append(py::str("value")); + list.append("value"); py::print("Entry at position 0:", list[0]); list[0] = py::str("overwritten"); return list; @@ -269,7 +270,7 @@ test_initializer python_types([](py::module &m) { d["missing_attr_chain"] = "raised"_s; } - d["is_none"] = py::cast(o.attr("basic_attr").is_none()); + d["is_none"] = o.attr("basic_attr").is_none(); d["operator()"] = o.attr("func")(1); d["operator*"] = o.attr("func")(*o.attr("begin_end")); @@ -279,13 +280,13 @@ test_initializer python_types([](py::module &m) { m.def("test_tuple_accessor", [](py::tuple existing_t) { try { - existing_t[0] = py::cast(1); + existing_t[0] = 1; } catch (const py::error_already_set &) { // --> Python system error // Only new tuples (refcount == 1) are mutable auto new_t = py::tuple(3); for (size_t i = 0; i < new_t.size(); ++i) { - new_t[i] = py::cast(i); + new_t[i] = i; } return new_t; } @@ -294,15 +295,15 @@ test_initializer python_types([](py::module &m) { m.def("test_accessor_assignment", []() { auto l = py::list(1); - l[0] = py::cast(0); + l[0] = 0; auto d = py::dict(); d["get"] = l[0]; auto var = l[0]; d["deferred_get"] = var; - l[0] = py::cast(1); + l[0] = 1; d["set"] = l[0]; - var = py::cast(99); // this assignment should not overwrite l[0] + var = 99; // this assignment should not overwrite l[0] d["deferred_set"] = l[0]; d["var"] = var; @@ -338,8 +339,8 @@ test_initializer python_types([](py::module &m) { }, py::arg_v("x", std::experimental::nullopt, "None")); #endif - m.attr("has_optional") = py::cast(has_optional); - m.attr("has_exp_optional") = py::cast(has_exp_optional); + m.attr("has_optional") = has_optional; + m.attr("has_exp_optional") = has_exp_optional; m.def("test_default_constructors", []() { return py::dict( @@ -389,4 +390,41 @@ test_initializer python_types([](py::module &m) { py::class_(m, "MoveOutContainer") .def(py::init<>()) .def_property_readonly("move_list", &MoveOutContainer::move_list); + + m.def("get_implicit_casting", []() { + py::dict d; + d["char*_i1"] = "abc"; + const char *c2 = "abc"; + d["char*_i2"] = c2; + d["char*_e"] = py::cast(c2); + d["char*_p"] = py::str(c2); + + d["int_i1"] = 42; + int i = 42; + d["int_i2"] = i; + i++; + d["int_e"] = py::cast(i); + i++; + d["int_p"] = py::int_(i); + + d["str_i1"] = std::string("str"); + std::string s2("str1"); + d["str_i2"] = s2; + s2[3] = '2'; + d["str_e"] = py::cast(s2); + s2[3] = '3'; + d["str_p"] = py::str(s2); + + py::list l(2); + l[0] = 3; + l[1] = py::cast(6); + l.append(9); + l.append(py::cast(12)); + l.append(py::int_(15)); + + return py::dict( + "d"_a=d, + "l"_a=l + ); + }); }); diff --git a/tests/test_python_types.py b/tests/test_python_types.py index 50a707ef5..b90b44c05 100644 --- a/tests/test_python_types.py +++ b/tests/test_python_types.py @@ -38,12 +38,13 @@ def test_instance(capture): """ with capture: set_result = instance.get_set() - set_result.add('key3') + set_result.add('key4') instance.print_set(set_result) assert capture.unordered == """ key: key1 key: key2 key: key3 + key: key4 """ with capture: set_result = instance.get_set2() @@ -386,3 +387,16 @@ def test_move_out_container(): c = MoveOutContainer() moved_out_list = c.move_list assert [x.value for x in moved_out_list] == [0, 1, 2] + + +def test_implicit_casting(): + """Tests implicit casting when assigning or appending to dicts and lists.""" + from pybind11_tests import get_implicit_casting + + z = get_implicit_casting() + assert z['d'] == { + 'char*_i1': 'abc', 'char*_i2': 'abc', 'char*_e': 'abc', 'char*_p': 'abc', + 'str_i1': 'str', 'str_i2': 'str1', 'str_e': 'str2', 'str_p': 'str3', + 'int_i1': 42, 'int_i2': 42, 'int_e': 43, 'int_p': 44 + } + assert z['l'] == [3, 6, 9, 12, 15]