Skip to content

Commit 3f1ff3f

Browse files
jagermanwjakob
authored andcommitted
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).
1 parent 7c9ef7b commit 3f1ff3f

File tree

9 files changed

+110
-26
lines changed

9 files changed

+110
-26
lines changed

docs/basics.rst

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,16 +254,18 @@ The shorthand notation is also available for default arguments:
254254
Exporting variables
255255
===================
256256

257-
To expose a value from C++, use the ``attr`` function to register it in a module
258-
as shown below. Built-in types and general objects (more on that later) can be
257+
To expose a value from C++, use the ``attr`` function to register it in a
258+
module as shown below. Built-in types and general objects (more on that later)
259+
are automatically converted when assigned as attributes, and can be explicitly
259260
converted using the function ``py::cast``.
260261

261262
.. code-block:: cpp
262263
263264
PYBIND11_PLUGIN(example) {
264265
py::module m("example", "pybind11 example plugin");
265-
m.attr("the_answer") = py::cast(42);
266-
m.attr("what") = py::cast("World");
266+
m.attr("the_answer") = 42;
267+
py::object world = py::cast("World");
268+
m.attr("what") = world;
267269
return m.ptr();
268270
}
269271

include/pybind11/cast.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,10 @@ template <> inline void object::cast() && { return; }
11201120

11211121
NAMESPACE_BEGIN(detail)
11221122

1123+
// Declared in pytypes.h:
1124+
template <typename T, enable_if_t<!is_pyobject<T>::value, int>>
1125+
object object_or_cast(T &&o) { return pybind11::cast(std::forward<T>(o)); }
1126+
11231127
struct overload_unused {}; // Placeholder type for the unneeded (and dead code) static variable in the OVERLOAD_INT macro
11241128
template <typename ret_type> using overload_caster_t = conditional_t<
11251129
cast_is_temporary_value_reference<ret_type>::value, make_caster<ret_type>, overload_unused>;

include/pybind11/pytypes.h

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ using tuple_accessor = accessor<accessor_policies::tuple_item>;
4343

4444
/// Tag and check to identify a class which implements the Python object API
4545
class pyobject_tag { };
46-
template <typename T> using is_pyobject = std::is_base_of<pyobject_tag, T>;
46+
template <typename T> using is_pyobject = std::is_base_of<pyobject_tag, typename std::remove_reference<T>::type>;
4747

4848
/// Mixin which adds common functions to handle, object and various accessors.
4949
/// The only requirement for `Derived` is to implement `PyObject *Derived::ptr() const`.
@@ -81,7 +81,7 @@ NAMESPACE_END(detail)
8181
class handle : public detail::object_api<handle> {
8282
public:
8383
handle() = default;
84-
handle(PyObject *ptr) : m_ptr(ptr) { }
84+
handle(PyObject *ptr) : m_ptr(ptr) { } // Allow implicit conversion from PyObject*
8585

8686
PyObject *ptr() const { return m_ptr; }
8787
PyObject *&ptr() { return m_ptr; }
@@ -224,19 +224,36 @@ inline handle get_function(handle value) {
224224
return value;
225225
}
226226

227+
// Helper aliases/functions to support implicit casting of values given to python accessors/methods.
228+
// When given a pyobject, this simply returns the pyobject as-is; for other C++ type, the value goes
229+
// through pybind11::cast(obj) to convert it to an `object`.
230+
template <typename T, enable_if_t<is_pyobject<T>::value, int> = 0>
231+
auto object_or_cast(T &&o) -> decltype(std::forward<T>(o)) { return std::forward<T>(o); }
232+
// The following casting version is implemented in cast.h:
233+
template <typename T, enable_if_t<!is_pyobject<T>::value, int> = 0>
234+
object object_or_cast(T &&o);
235+
// Match a PyObject*, which we want to convert directly to handle via its converting constructor
236+
inline handle object_or_cast(PyObject *ptr) { return ptr; }
237+
238+
227239
template <typename Policy>
228240
class accessor : public object_api<accessor<Policy>> {
229241
using key_type = typename Policy::key_type;
230242

231243
public:
232244
accessor(handle obj, key_type key) : obj(obj), key(std::move(key)) { }
233245

246+
// accessor overload required to override default assignment operator (templates are not allowed
247+
// to replace default compiler-generated assignments).
234248
void operator=(const accessor &a) && { std::move(*this).operator=(handle(a)); }
235249
void operator=(const accessor &a) & { operator=(handle(a)); }
236-
void operator=(const object &o) && { std::move(*this).operator=(handle(o)); }
237-
void operator=(const object &o) & { operator=(handle(o)); }
238-
void operator=(handle value) && { Policy::set(obj, key, value); }
239-
void operator=(handle value) & { get_cache() = reinterpret_borrow<object>(value); }
250+
251+
template <typename T> void operator=(T &&value) && {
252+
Policy::set(obj, key, object_or_cast(std::forward<T>(value)));
253+
}
254+
template <typename T> void operator=(T &&value) & {
255+
get_cache() = reinterpret_borrow<object>(object_or_cast(std::forward<T>(value)));
256+
}
240257

241258
template <typename T = Policy>
242259
PYBIND11_DEPRECATED("Use of obj.attr(...) as bool is deprecated in favor of pybind11::hasattr(obj, ...)")
@@ -773,7 +790,9 @@ class list : public object {
773790
}
774791
size_t size() const { return (size_t) PyList_Size(m_ptr); }
775792
detail::list_accessor operator[](size_t index) const { return {*this, index}; }
776-
void append(handle h) const { PyList_Append(m_ptr, h.ptr()); }
793+
template <typename T> void append(T &&val) const {
794+
PyList_Append(m_ptr, detail::object_or_cast(std::forward<T>(val)).ptr());
795+
}
777796
};
778797

779798
class args : public tuple { PYBIND11_OBJECT_DEFAULT(args, tuple, PyTuple_Check) };
@@ -786,7 +805,9 @@ class set : public object {
786805
if (!m_ptr) pybind11_fail("Could not allocate set object!");
787806
}
788807
size_t size() const { return (size_t) PySet_Size(m_ptr); }
789-
bool add(const object &object) const { return PySet_Add(m_ptr, object.ptr()) == 0; }
808+
template <typename T> bool add(T &&val) const {
809+
return PySet_Add(m_ptr, detail::object_or_cast(std::forward<T>(val)).ptr()) == 0;
810+
}
790811
void clear() const { PySet_Clear(m_ptr); }
791812
};
792813

tests/pybind11_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ PYBIND11_PLUGIN(pybind11_tests) {
3939
for (const auto &initializer : initializers())
4040
initializer(m);
4141

42-
if (!py::hasattr(m, "have_eigen")) m.attr("have_eigen") = py::cast(false);
42+
if (!py::hasattr(m, "have_eigen")) m.attr("have_eigen") = false;
4343

4444
return m.ptr();
4545
}

tests/test_eigen.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ test_initializer eigen([](py::module &m) {
4040
typedef Eigen::SparseMatrix<float, Eigen::RowMajor> SparseMatrixR;
4141
typedef Eigen::SparseMatrix<float> SparseMatrixC;
4242

43-
m.attr("have_eigen") = py::cast(true);
43+
m.attr("have_eigen") = true;
4444

4545
// Non-symmetric matrix with zero elements
4646
Eigen::MatrixXf mat(5, 6);

tests/test_exceptions.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ void throws_logic_error() {
8888

8989
struct PythonCallInDestructor {
9090
PythonCallInDestructor(const py::dict &d) : d(d) {}
91-
~PythonCallInDestructor() { d["good"] = py::cast(true); }
91+
~PythonCallInDestructor() { d["good"] = true; }
9292

9393
py::dict d;
9494
};

tests/test_issues.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,11 @@ void init_issues(py::module &m) {
381381
.def_static("make", &MyDerived::make)
382382
.def_static("make2", &MyDerived::make);
383383

384+
py::dict d;
385+
std::string bar = "bar";
386+
d["str"] = bar;
387+
d["num"] = 3.7;
388+
384389
/// Issue #528: templated constructor
385390
m2.def("tpl_constr_vector", [](std::vector<TplConstrClass> &) {});
386391
m2.def("tpl_constr_map", [](std::unordered_map<TplConstrClass, TplConstrClass> &) {});

tests/test_python_types.cpp

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ class ExamplePythonTypes {
3737
py::set get_set() {
3838
py::set set;
3939
set.add(py::str("key1"));
40-
set.add(py::str("key2"));
40+
set.add("key2");
41+
set.add(std::string("key3"));
4142
return set;
4243
}
4344

@@ -59,7 +60,7 @@ class ExamplePythonTypes {
5960
/* Create, manipulate, and return a Python list */
6061
py::list get_list() {
6162
py::list list;
62-
list.append(py::str("value"));
63+
list.append("value");
6364
py::print("Entry at position 0:", list[0]);
6465
list[0] = py::str("overwritten");
6566
return list;
@@ -269,7 +270,7 @@ test_initializer python_types([](py::module &m) {
269270
d["missing_attr_chain"] = "raised"_s;
270271
}
271272

272-
d["is_none"] = py::cast(o.attr("basic_attr").is_none());
273+
d["is_none"] = o.attr("basic_attr").is_none();
273274

274275
d["operator()"] = o.attr("func")(1);
275276
d["operator*"] = o.attr("func")(*o.attr("begin_end"));
@@ -279,13 +280,13 @@ test_initializer python_types([](py::module &m) {
279280

280281
m.def("test_tuple_accessor", [](py::tuple existing_t) {
281282
try {
282-
existing_t[0] = py::cast(1);
283+
existing_t[0] = 1;
283284
} catch (const py::error_already_set &) {
284285
// --> Python system error
285286
// Only new tuples (refcount == 1) are mutable
286287
auto new_t = py::tuple(3);
287288
for (size_t i = 0; i < new_t.size(); ++i) {
288-
new_t[i] = py::cast(i);
289+
new_t[i] = i;
289290
}
290291
return new_t;
291292
}
@@ -294,15 +295,15 @@ test_initializer python_types([](py::module &m) {
294295

295296
m.def("test_accessor_assignment", []() {
296297
auto l = py::list(1);
297-
l[0] = py::cast(0);
298+
l[0] = 0;
298299

299300
auto d = py::dict();
300301
d["get"] = l[0];
301302
auto var = l[0];
302303
d["deferred_get"] = var;
303-
l[0] = py::cast(1);
304+
l[0] = 1;
304305
d["set"] = l[0];
305-
var = py::cast(99); // this assignment should not overwrite l[0]
306+
var = 99; // this assignment should not overwrite l[0]
306307
d["deferred_set"] = l[0];
307308
d["var"] = var;
308309

@@ -338,8 +339,8 @@ test_initializer python_types([](py::module &m) {
338339
}, py::arg_v("x", std::experimental::nullopt, "None"));
339340
#endif
340341

341-
m.attr("has_optional") = py::cast(has_optional);
342-
m.attr("has_exp_optional") = py::cast(has_exp_optional);
342+
m.attr("has_optional") = has_optional;
343+
m.attr("has_exp_optional") = has_exp_optional;
343344

344345
m.def("test_default_constructors", []() {
345346
return py::dict(
@@ -389,4 +390,41 @@ test_initializer python_types([](py::module &m) {
389390
py::class_<MoveOutContainer>(m, "MoveOutContainer")
390391
.def(py::init<>())
391392
.def_property_readonly("move_list", &MoveOutContainer::move_list);
393+
394+
m.def("get_implicit_casting", []() {
395+
py::dict d;
396+
d["char*_i1"] = "abc";
397+
const char *c2 = "abc";
398+
d["char*_i2"] = c2;
399+
d["char*_e"] = py::cast(c2);
400+
d["char*_p"] = py::str(c2);
401+
402+
d["int_i1"] = 42;
403+
int i = 42;
404+
d["int_i2"] = i;
405+
i++;
406+
d["int_e"] = py::cast(i);
407+
i++;
408+
d["int_p"] = py::int_(i);
409+
410+
d["str_i1"] = std::string("str");
411+
std::string s2("str1");
412+
d["str_i2"] = s2;
413+
s2[3] = '2';
414+
d["str_e"] = py::cast(s2);
415+
s2[3] = '3';
416+
d["str_p"] = py::str(s2);
417+
418+
py::list l(2);
419+
l[0] = 3;
420+
l[1] = py::cast(6);
421+
l.append(9);
422+
l.append(py::cast(12));
423+
l.append(py::int_(15));
424+
425+
return py::dict(
426+
"d"_a=d,
427+
"l"_a=l
428+
);
429+
});
392430
});

tests/test_python_types.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ def test_instance(capture):
3838
"""
3939
with capture:
4040
set_result = instance.get_set()
41-
set_result.add('key3')
41+
set_result.add('key4')
4242
instance.print_set(set_result)
4343
assert capture.unordered == """
4444
key: key1
4545
key: key2
4646
key: key3
47+
key: key4
4748
"""
4849
with capture:
4950
set_result = instance.get_set2()
@@ -386,3 +387,16 @@ def test_move_out_container():
386387
c = MoveOutContainer()
387388
moved_out_list = c.move_list
388389
assert [x.value for x in moved_out_list] == [0, 1, 2]
390+
391+
392+
def test_implicit_casting():
393+
"""Tests implicit casting when assigning or appending to dicts and lists."""
394+
from pybind11_tests import get_implicit_casting
395+
396+
z = get_implicit_casting()
397+
assert z['d'] == {
398+
'char*_i1': 'abc', 'char*_i2': 'abc', 'char*_e': 'abc', 'char*_p': 'abc',
399+
'str_i1': 'str', 'str_i2': 'str1', 'str_e': 'str2', 'str_p': 'str3',
400+
'int_i1': 42, 'int_i2': 42, 'int_e': 43, 'int_p': 44
401+
}
402+
assert z['l'] == [3, 6, 9, 12, 15]

0 commit comments

Comments
 (0)