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).
This commit is contained in:
Jason Rhinelander 2016-12-12 17:42:52 -05:00 committed by Wenzel Jakob
parent 7c9ef7b553
commit 3f1ff3f4d1
9 changed files with 110 additions and 26 deletions

View File

@ -254,16 +254,18 @@ The shorthand notation is also available for default arguments:
Exporting variables Exporting variables
=================== ===================
To expose a value from C++, use the ``attr`` function to register it in a module To expose a value from C++, use the ``attr`` function to register it in a
as shown below. Built-in types and general objects (more on that later) can be 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``. converted using the function ``py::cast``.
.. code-block:: cpp .. code-block:: cpp
PYBIND11_PLUGIN(example) { PYBIND11_PLUGIN(example) {
py::module m("example", "pybind11 example plugin"); py::module m("example", "pybind11 example plugin");
m.attr("the_answer") = py::cast(42); m.attr("the_answer") = 42;
m.attr("what") = py::cast("World"); py::object world = py::cast("World");
m.attr("what") = world;
return m.ptr(); return m.ptr();
} }

View File

@ -1120,6 +1120,10 @@ template <> inline void object::cast() && { return; }
NAMESPACE_BEGIN(detail) NAMESPACE_BEGIN(detail)
// Declared in pytypes.h:
template <typename T, enable_if_t<!is_pyobject<T>::value, int>>
object object_or_cast(T &&o) { return pybind11::cast(std::forward<T>(o)); }
struct overload_unused {}; // Placeholder type for the unneeded (and dead code) static variable in the OVERLOAD_INT macro struct overload_unused {}; // Placeholder type for the unneeded (and dead code) static variable in the OVERLOAD_INT macro
template <typename ret_type> using overload_caster_t = conditional_t< template <typename ret_type> using overload_caster_t = conditional_t<
cast_is_temporary_value_reference<ret_type>::value, make_caster<ret_type>, overload_unused>; cast_is_temporary_value_reference<ret_type>::value, make_caster<ret_type>, overload_unused>;

View File

@ -43,7 +43,7 @@ using tuple_accessor = accessor<accessor_policies::tuple_item>;
/// Tag and check to identify a class which implements the Python object API /// Tag and check to identify a class which implements the Python object API
class pyobject_tag { }; class pyobject_tag { };
template <typename T> using is_pyobject = std::is_base_of<pyobject_tag, T>; template <typename T> using is_pyobject = std::is_base_of<pyobject_tag, typename std::remove_reference<T>::type>;
/// Mixin which adds common functions to handle, object and various accessors. /// Mixin which adds common functions to handle, object and various accessors.
/// The only requirement for `Derived` is to implement `PyObject *Derived::ptr() const`. /// 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<handle> { class handle : public detail::object_api<handle> {
public: public:
handle() = default; 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() const { return m_ptr; }
PyObject *&ptr() { return m_ptr; } PyObject *&ptr() { return m_ptr; }
@ -224,6 +224,18 @@ inline handle get_function(handle value) {
return 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 <typename T, enable_if_t<is_pyobject<T>::value, int> = 0>
auto object_or_cast(T &&o) -> decltype(std::forward<T>(o)) { return std::forward<T>(o); }
// The following casting version is implemented in cast.h:
template <typename T, enable_if_t<!is_pyobject<T>::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 <typename Policy> template <typename Policy>
class accessor : public object_api<accessor<Policy>> { class accessor : public object_api<accessor<Policy>> {
using key_type = typename Policy::key_type; using key_type = typename Policy::key_type;
@ -231,12 +243,17 @@ class accessor : public object_api<accessor<Policy>> {
public: public:
accessor(handle obj, key_type key) : obj(obj), key(std::move(key)) { } 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) && { std::move(*this).operator=(handle(a)); }
void operator=(const accessor &a) & { 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)); } template <typename T> void operator=(T &&value) && {
void operator=(handle value) && { Policy::set(obj, key, value); } Policy::set(obj, key, object_or_cast(std::forward<T>(value)));
void operator=(handle value) & { get_cache() = reinterpret_borrow<object>(value); } }
template <typename T> void operator=(T &&value) & {
get_cache() = reinterpret_borrow<object>(object_or_cast(std::forward<T>(value)));
}
template <typename T = Policy> template <typename T = Policy>
PYBIND11_DEPRECATED("Use of obj.attr(...) as bool is deprecated in favor of pybind11::hasattr(obj, ...)") 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); } size_t size() const { return (size_t) PyList_Size(m_ptr); }
detail::list_accessor operator[](size_t index) const { return {*this, index}; } detail::list_accessor operator[](size_t index) const { return {*this, index}; }
void append(handle h) const { PyList_Append(m_ptr, h.ptr()); } template <typename T> void append(T &&val) const {
PyList_Append(m_ptr, detail::object_or_cast(std::forward<T>(val)).ptr());
}
}; };
class args : public tuple { PYBIND11_OBJECT_DEFAULT(args, tuple, PyTuple_Check) }; 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!"); if (!m_ptr) pybind11_fail("Could not allocate set object!");
} }
size_t size() const { return (size_t) PySet_Size(m_ptr); } 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 <typename T> bool add(T &&val) const {
return PySet_Add(m_ptr, detail::object_or_cast(std::forward<T>(val)).ptr()) == 0;
}
void clear() const { PySet_Clear(m_ptr); } void clear() const { PySet_Clear(m_ptr); }
}; };

View File

@ -39,7 +39,7 @@ PYBIND11_PLUGIN(pybind11_tests) {
for (const auto &initializer : initializers()) for (const auto &initializer : initializers())
initializer(m); 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(); return m.ptr();
} }

View File

@ -40,7 +40,7 @@ test_initializer eigen([](py::module &m) {
typedef Eigen::SparseMatrix<float, Eigen::RowMajor> SparseMatrixR; typedef Eigen::SparseMatrix<float, Eigen::RowMajor> SparseMatrixR;
typedef Eigen::SparseMatrix<float> SparseMatrixC; typedef Eigen::SparseMatrix<float> SparseMatrixC;
m.attr("have_eigen") = py::cast(true); m.attr("have_eigen") = true;
// Non-symmetric matrix with zero elements // Non-symmetric matrix with zero elements
Eigen::MatrixXf mat(5, 6); Eigen::MatrixXf mat(5, 6);

View File

@ -88,7 +88,7 @@ void throws_logic_error() {
struct PythonCallInDestructor { struct PythonCallInDestructor {
PythonCallInDestructor(const py::dict &d) : d(d) {} PythonCallInDestructor(const py::dict &d) : d(d) {}
~PythonCallInDestructor() { d["good"] = py::cast(true); } ~PythonCallInDestructor() { d["good"] = true; }
py::dict d; py::dict d;
}; };

View File

@ -381,6 +381,11 @@ void init_issues(py::module &m) {
.def_static("make", &MyDerived::make) .def_static("make", &MyDerived::make)
.def_static("make2", &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 /// Issue #528: templated constructor
m2.def("tpl_constr_vector", [](std::vector<TplConstrClass> &) {}); m2.def("tpl_constr_vector", [](std::vector<TplConstrClass> &) {});
m2.def("tpl_constr_map", [](std::unordered_map<TplConstrClass, TplConstrClass> &) {}); m2.def("tpl_constr_map", [](std::unordered_map<TplConstrClass, TplConstrClass> &) {});

View File

@ -37,7 +37,8 @@ public:
py::set get_set() { py::set get_set() {
py::set set; py::set set;
set.add(py::str("key1")); set.add(py::str("key1"));
set.add(py::str("key2")); set.add("key2");
set.add(std::string("key3"));
return set; return set;
} }
@ -59,7 +60,7 @@ public:
/* Create, manipulate, and return a Python list */ /* Create, manipulate, and return a Python list */
py::list get_list() { py::list get_list() {
py::list list; py::list list;
list.append(py::str("value")); list.append("value");
py::print("Entry at position 0:", list[0]); py::print("Entry at position 0:", list[0]);
list[0] = py::str("overwritten"); list[0] = py::str("overwritten");
return list; return list;
@ -269,7 +270,7 @@ test_initializer python_types([](py::module &m) {
d["missing_attr_chain"] = "raised"_s; 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")(1);
d["operator*"] = o.attr("func")(*o.attr("begin_end")); 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) { m.def("test_tuple_accessor", [](py::tuple existing_t) {
try { try {
existing_t[0] = py::cast(1); existing_t[0] = 1;
} catch (const py::error_already_set &) { } catch (const py::error_already_set &) {
// --> Python system error // --> Python system error
// Only new tuples (refcount == 1) are mutable // Only new tuples (refcount == 1) are mutable
auto new_t = py::tuple(3); auto new_t = py::tuple(3);
for (size_t i = 0; i < new_t.size(); ++i) { for (size_t i = 0; i < new_t.size(); ++i) {
new_t[i] = py::cast(i); new_t[i] = i;
} }
return new_t; return new_t;
} }
@ -294,15 +295,15 @@ test_initializer python_types([](py::module &m) {
m.def("test_accessor_assignment", []() { m.def("test_accessor_assignment", []() {
auto l = py::list(1); auto l = py::list(1);
l[0] = py::cast(0); l[0] = 0;
auto d = py::dict(); auto d = py::dict();
d["get"] = l[0]; d["get"] = l[0];
auto var = l[0]; auto var = l[0];
d["deferred_get"] = var; d["deferred_get"] = var;
l[0] = py::cast(1); l[0] = 1;
d["set"] = l[0]; 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["deferred_set"] = l[0];
d["var"] = var; d["var"] = var;
@ -338,8 +339,8 @@ test_initializer python_types([](py::module &m) {
}, py::arg_v("x", std::experimental::nullopt, "None")); }, py::arg_v("x", std::experimental::nullopt, "None"));
#endif #endif
m.attr("has_optional") = py::cast(has_optional); m.attr("has_optional") = has_optional;
m.attr("has_exp_optional") = py::cast(has_exp_optional); m.attr("has_exp_optional") = has_exp_optional;
m.def("test_default_constructors", []() { m.def("test_default_constructors", []() {
return py::dict( return py::dict(
@ -389,4 +390,41 @@ test_initializer python_types([](py::module &m) {
py::class_<MoveOutContainer>(m, "MoveOutContainer") py::class_<MoveOutContainer>(m, "MoveOutContainer")
.def(py::init<>()) .def(py::init<>())
.def_property_readonly("move_list", &MoveOutContainer::move_list); .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
);
});
}); });

View File

@ -38,12 +38,13 @@ def test_instance(capture):
""" """
with capture: with capture:
set_result = instance.get_set() set_result = instance.get_set()
set_result.add('key3') set_result.add('key4')
instance.print_set(set_result) instance.print_set(set_result)
assert capture.unordered == """ assert capture.unordered == """
key: key1 key: key1
key: key2 key: key2
key: key3 key: key3
key: key4
""" """
with capture: with capture:
set_result = instance.get_set2() set_result = instance.get_set2()
@ -386,3 +387,16 @@ def test_move_out_container():
c = MoveOutContainer() c = MoveOutContainer()
moved_out_list = c.move_list moved_out_list = c.move_list
assert [x.value for x in moved_out_list] == [0, 1, 2] 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]