Skip to content

Commit fb26eac

Browse files
committed
Adds automatic casting on assignment of non-pyobject types
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. Fixes #547 (the original post).
1 parent 8fadade commit fb26eac

File tree

9 files changed

+101
-16
lines changed

9 files changed

+101
-16
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+
auto world = py::cast("World");
268+
m.attr("what") = world;
267269
return m.ptr();
268270
}
269271

include/pybind11/cast.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,24 @@ template <typename T> T object::cast() && { return pybind11::cast<T>(std::move(*
11181118
template <> inline void object::cast() const & { return; }
11191119
template <> inline void object::cast() && { return; }
11201120

1121+
// Implicit casting via accessor assignment:
1122+
template <typename Policy>
1123+
template <typename T, detail::enable_if_t<detail::is_implicitly_castable<T>::value, int>>
1124+
void detail::accessor<Policy>::operator=(const T &value) & {
1125+
operator=(handle(pybind11::cast(value)));
1126+
}
1127+
template <typename Policy>
1128+
template <typename T, detail::enable_if_t<detail::is_implicitly_castable<T>::value, int>>
1129+
void detail::accessor<Policy>::operator=(const T &value) && {
1130+
std::move(*this).operator=(handle(pybind11::cast(value)));
1131+
}
1132+
1133+
// Implicit casting via list append:
1134+
template <typename T, detail::enable_if_t<detail::is_implicitly_castable<T>::value, int>>
1135+
void list::append(const T &value) const {
1136+
append(pybind11::cast(value));
1137+
}
1138+
11211139
NAMESPACE_BEGIN(detail)
11221140

11231141
struct overload_unused {}; // Placeholder type for the unneeded (and dead code) static variable in the OVERLOAD_INT macro

include/pybind11/pytypes.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ inline handle get_function(handle value) {
224224
return value;
225225
}
226226

227+
template <typename T> using is_implicitly_castable = bool_constant<
228+
!is_pyobject<intrinsic_t<T>>::value && !std::is_same<T, PyObject*>::value>;
229+
227230
template <typename Policy>
228231
class accessor : public object_api<accessor<Policy>> {
229232
using key_type = typename Policy::key_type;
@@ -238,6 +241,11 @@ class accessor : public object_api<accessor<Policy>> {
238241
void operator=(handle value) && { Policy::set(obj, key, value); }
239242
void operator=(handle value) & { get_cache() = reinterpret_borrow<object>(value); }
240243

244+
template <typename T, enable_if_t<is_implicitly_castable<T>::value, int> = 0>
245+
void operator=(const T &value) &;
246+
template <typename T, enable_if_t<is_implicitly_castable<T>::value, int> = 0>
247+
void operator=(const T &value) &&;
248+
241249
template <typename T = Policy>
242250
PYBIND11_DEPRECATED("Use of obj.attr(...) as bool is deprecated in favor of pybind11::hasattr(obj, ...)")
243251
explicit operator enable_if_t<std::is_same<T, accessor_policies::str_attr>::value ||
@@ -774,6 +782,8 @@ class list : public object {
774782
size_t size() const { return (size_t) PyList_Size(m_ptr); }
775783
detail::list_accessor operator[](size_t index) const { return {*this, index}; }
776784
void append(handle h) const { PyList_Append(m_ptr, h.ptr()); }
785+
template <typename T, detail::enable_if_t<detail::is_implicitly_castable<T>::value, int> = 0>
786+
void append(const T &value) const;
777787
};
778788

779789
class args : public tuple { PYBIND11_OBJECT_DEFAULT(args, tuple, PyTuple_Check) };

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: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class ExamplePythonTypes {
5959
/* Create, manipulate, and return a Python list */
6060
py::list get_list() {
6161
py::list list;
62-
list.append(py::str("value"));
62+
list.append("value");
6363
py::print("Entry at position 0:", list[0]);
6464
list[0] = py::str("overwritten");
6565
return list;
@@ -269,7 +269,7 @@ test_initializer python_types([](py::module &m) {
269269
d["missing_attr_chain"] = "raised"_s;
270270
}
271271

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

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

280280
m.def("test_tuple_accessor", [](py::tuple existing_t) {
281281
try {
282-
existing_t[0] = py::cast(1);
282+
existing_t[0] = 1;
283283
} catch (const py::error_already_set &) {
284284
// --> Python system error
285285
// Only new tuples (refcount == 1) are mutable
286286
auto new_t = py::tuple(3);
287287
for (size_t i = 0; i < new_t.size(); ++i) {
288-
new_t[i] = py::cast(i);
288+
new_t[i] = i;
289289
}
290290
return new_t;
291291
}
@@ -294,15 +294,15 @@ test_initializer python_types([](py::module &m) {
294294

295295
m.def("test_accessor_assignment", []() {
296296
auto l = py::list(1);
297-
l[0] = py::cast(0);
297+
l[0] = 0;
298298

299299
auto d = py::dict();
300300
d["get"] = l[0];
301301
auto var = l[0];
302302
d["deferred_get"] = var;
303-
l[0] = py::cast(1);
303+
l[0] = 1;
304304
d["set"] = l[0];
305-
var = py::cast(99); // this assignment should not overwrite l[0]
305+
var = 99; // this assignment should not overwrite l[0]
306306
d["deferred_set"] = l[0];
307307
d["var"] = var;
308308

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

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

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

tests/test_python_types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,16 @@ def test_move_out_container():
386386
c = MoveOutContainer()
387387
moved_out_list = c.move_list
388388
assert [x.value for x in moved_out_list] == [0, 1, 2]
389+
390+
391+
def test_implicit_casting():
392+
"""Tests implicit casting when assigning or appending to dicts and lists."""
393+
from pybind11_tests import get_implicit_casting
394+
395+
z = get_implicit_casting()
396+
assert z['d'] == {
397+
'char*_i1': 'abc', 'char*_i2': 'abc', 'char*_e': 'abc', 'char*_p': 'abc',
398+
'str_i1': 'str', 'str_i2': 'str1', 'str_e': 'str2', 'str_p': 'str3',
399+
'int_i1': 42, 'int_i2': 42, 'int_e': 43, 'int_p': 44
400+
}
401+
assert z['l'] == [3, 6, 9, 12, 15]

0 commit comments

Comments
 (0)