Skip to content

Commit 9efd8a1

Browse files
authored
Elide to-python conversion of setter return values (#30027)
* Snapshot of pybind/pybind11#4621 applied to pywrapcc * Bug fix: obvious in hindsight. I thought the original version was incrementing the reference count for None, but no. Discovered via many failing tests in the wild (10s of thousands). It is very tricky to construct a meaningful unit test for this bug specifically. It's unlikely to come back, because 10s of thousands of tests will fail again.
1 parent 77a15e1 commit 9efd8a1

File tree

4 files changed

+70
-5
lines changed

4 files changed

+70
-5
lines changed

include/pybind11/attr.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ struct is_method {
2626
explicit is_method(const handle &c) : class_(c) {}
2727
};
2828

29+
/// Annotation for setters
30+
struct is_setter {};
31+
2932
/// Annotation for operators
3033
struct is_operator {};
3134

@@ -190,8 +193,8 @@ struct argument_record {
190193
struct function_record {
191194
function_record()
192195
: is_constructor(false), is_new_style_constructor(false), is_stateless(false),
193-
is_operator(false), is_method(false), has_args(false), has_kwargs(false),
194-
prepend(false) {}
196+
is_operator(false), is_method(false), is_setter(false), has_args(false),
197+
has_kwargs(false), prepend(false) {}
195198

196199
/// Function name
197200
char *name = nullptr; /* why no C++ strings? They generate heavier code.. */
@@ -232,6 +235,9 @@ struct function_record {
232235
/// True if this is a method
233236
bool is_method : 1;
234237

238+
/// True if this is a setter
239+
bool is_setter : 1;
240+
235241
/// True if the function has a '*args' argument
236242
bool has_args : 1;
237243

@@ -433,6 +439,12 @@ struct process_attribute<is_method> : process_attribute_default<is_method> {
433439
}
434440
};
435441

442+
/// Process an attribute which indicates that this function is a setter
443+
template <>
444+
struct process_attribute<is_setter> : process_attribute_default<is_setter> {
445+
static void init(const is_setter &, function_record *r) { r->is_setter = true; }
446+
};
447+
436448
/// Process an attribute which indicates the parent scope of a method
437449
template <>
438450
struct process_attribute<scope> : process_attribute_default<scope> {

include/pybind11/pybind11.h

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class cpp_function : public function {
8686
cpp_function() = default;
8787
// NOLINTNEXTLINE(google-explicit-constructor)
8888
cpp_function(std::nullptr_t) {}
89+
cpp_function(std::nullptr_t, const is_setter &) {}
8990

9091
/// Construct a cpp_function from a vanilla function pointer
9192
template <typename Return, typename... Args, typename... Extra>
@@ -246,8 +247,16 @@ class cpp_function : public function {
246247
using Guard = extract_guard_t<Extra...>;
247248

248249
/* Perform the function call */
249-
handle result = cast_out::cast(
250-
std::move(args_converter).template call<Return, Guard>(cap->f), rvpp, call.parent);
250+
handle result;
251+
if (call.func.is_setter) {
252+
(void) std::move(args_converter).template call<Return, Guard>(cap->f);
253+
result = none().release();
254+
} else {
255+
result = cast_out::cast(
256+
std::move(args_converter).template call<Return, Guard>(cap->f),
257+
rvpp,
258+
call.parent);
259+
}
251260

252261
/* Invoke call policy post-call hook */
253262
process_attributes<Extra...>::postcall(call, result);
@@ -1982,7 +1991,8 @@ class class_ : public detail::generic_type {
19821991
template <typename Getter, typename Setter, typename... Extra>
19831992
class_ &
19841993
def_property(const char *name, const Getter &fget, const Setter &fset, const Extra &...extra) {
1985-
return def_property(name, fget, cpp_function(method_adaptor<type>(fset)), extra...);
1994+
return def_property(
1995+
name, fget, cpp_function(method_adaptor<type>(fset), is_setter()), extra...);
19861996
}
19871997
template <typename Getter, typename... Extra>
19881998
class_ &def_property(const char *name,

tests/test_methods_and_attributes.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,38 @@ struct RValueRefParam {
177177
std::size_t func4(std::string &&s) const & { return s.size(); }
178178
};
179179

180+
namespace pybind11_tests {
181+
namespace exercise_is_setter {
182+
183+
struct FieldBase {
184+
int int_value() const { return int_value_; }
185+
186+
FieldBase &SetIntValue(int int_value) {
187+
int_value_ = int_value;
188+
return *this;
189+
}
190+
191+
private:
192+
int int_value_ = -99;
193+
};
194+
195+
struct Field : FieldBase {};
196+
197+
void add_bindings(py::module &m) {
198+
py::module sm = m.def_submodule("exercise_is_setter");
199+
// NOTE: FieldBase is not wrapped, therefore ...
200+
py::class_<Field>(sm, "Field")
201+
.def(py::init<>())
202+
.def_property(
203+
"int_value",
204+
&Field::int_value,
205+
&Field::SetIntValue // ... the `FieldBase &` return value here cannot be converted.
206+
);
207+
}
208+
209+
} // namespace exercise_is_setter
210+
} // namespace pybind11_tests
211+
180212
TEST_SUBMODULE(methods_and_attributes, m) {
181213
// test_methods_and_attributes
182214
py::class_<ExampleMandA> emna(m, "ExampleMandA");
@@ -460,4 +492,6 @@ TEST_SUBMODULE(methods_and_attributes, m) {
460492
.def("func2", &RValueRefParam::func2)
461493
.def("func3", &RValueRefParam::func3)
462494
.def("func4", &RValueRefParam::func4);
495+
496+
pybind11_tests::exercise_is_setter::add_bindings(m);
463497
}

tests/test_methods_and_attributes.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,12 @@ def test_rvalue_ref_param():
524524
assert r.func2("1234") == 4
525525
assert r.func3("12345") == 5
526526
assert r.func4("123456") == 6
527+
528+
529+
def test_is_setter():
530+
fld = m.exercise_is_setter.Field()
531+
assert fld.int_value == -99
532+
setter_return = fld.int_value = 100
533+
assert isinstance(setter_return, int)
534+
assert setter_return == 100
535+
assert fld.int_value == 100

0 commit comments

Comments
 (0)