Skip to content

Commit 80c9ee6

Browse files
authored
Special handling of @__setstate__ to support alternative pickle mechanisms. (#30094)
* Use `py::metaclass((PyObject *) &PyType_Type)` and test compatibility with `metaclass=abc.ABCMeta` * `@__setstate__` support (first stab) * Add `test_abc_meta_incompatibility()`, `test_abc_meta_compatibility()` to test_methods_and_attributes.py * Add `exercise_getinitargs_getstate_setstate` to test_pickling * Improve `"@__setstate__"` related code in pybind11.h for readability. * Resolve MSVC errors: declaration of 'state' hides class member * Change `@__setstate__` to `__setstate__[non-constructor]` based on feedback by @rainwoodman. Also revise comments.
1 parent 82d7824 commit 80c9ee6

File tree

4 files changed

+101
-4
lines changed

4 files changed

+101
-4
lines changed

include/pybind11/pybind11.h

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,11 @@ class cpp_function : public function {
394394
// it alive.
395395
auto *rec = unique_rec.get();
396396

397+
// Note the "__setstate__" manipulation below.
398+
rec->is_constructor = rec->name != nullptr
399+
&& (std::strcmp(rec->name, "__init__") == 0
400+
|| std::strcmp(rec->name, "__setstate__") == 0);
401+
397402
// Keep track of strdup'ed strings, and clean them up as long as the function's capsule
398403
// has not taken ownership yet (when `unique_rec.release()` is called).
399404
// Note: This cannot easily be fixed by a `unique_ptr` with custom deleter, because the
@@ -403,7 +408,14 @@ class cpp_function : public function {
403408
strdup_guard guarded_strdup;
404409

405410
/* Create copies of all referenced C-style strings */
406-
rec->name = guarded_strdup(rec->name ? rec->name : "");
411+
if (rec->name == nullptr) {
412+
rec->name = guarded_strdup("");
413+
} else if (std::strcmp(rec->name, "__setstate__[non-constructor]") == 0) {
414+
// See google/pywrapcc#30094 for background.
415+
rec->name = guarded_strdup("__setstate__");
416+
} else {
417+
rec->name = guarded_strdup(rec->name);
418+
}
407419
if (rec->doc) {
408420
rec->doc = guarded_strdup(rec->doc);
409421
}
@@ -418,9 +430,6 @@ class cpp_function : public function {
418430
}
419431
}
420432

421-
rec->is_constructor = (std::strcmp(rec->name, "__init__") == 0)
422-
|| (std::strcmp(rec->name, "__setstate__") == 0);
423-
424433
#if defined(PYBIND11_DETAILED_ERROR_MESSAGES) && !defined(PYBIND11_DISABLE_NEW_STYLE_INIT_WARNING)
425434
if (rec->is_constructor && !rec->is_new_style_constructor) {
426435
const auto class_name

tests/test_methods_and_attributes.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import abc
12
import sys
23

34
import pytest
@@ -227,6 +228,22 @@ def test_metaclass_override():
227228
assert isinstance(m.MetaclassOverride.__dict__["readonly"], int)
228229

229230

231+
def test_abc_meta_incompatibility(): # Mostly to clearly expose the behavior.
232+
with pytest.raises(TypeError) as exc_info:
233+
234+
class ExampleMandAABC(m.ExampleMandA, metaclass=abc.ABCMeta):
235+
pass
236+
237+
assert "metaclass conflict" in str(exc_info.value)
238+
239+
240+
def test_abc_meta_compatibility():
241+
class MetaclassOverrideABC(m.MetaclassOverride, metaclass=abc.ABCMeta):
242+
pass
243+
244+
assert type(MetaclassOverrideABC).__name__ == "ABCMeta"
245+
246+
230247
def test_no_mixed_overloads():
231248
from pybind11_tests import detailed_error_messages_enabled
232249

tests/test_pickling.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@
88
BSD-style license that can be found in the LICENSE file.
99
*/
1010

11+
#include <pybind11/stl.h>
12+
1113
#include "pybind11_tests.h"
1214

1315
#include <memory>
1416
#include <stdexcept>
1517
#include <utility>
18+
#include <vector>
1619

1720
namespace exercise_trampoline {
1821

@@ -60,6 +63,53 @@ void wrap(py::module m) {
6063

6164
} // namespace exercise_trampoline
6265

66+
namespace exercise_getinitargs_getstate_setstate {
67+
68+
// Exercise `__setstate__[non-constructor]` (see google/pywrapcc#30094), which
69+
// was added to support a pickle protocol as established with Boost.Python
70+
// (in 2002):
71+
// https://www.boost.org/doc/libs/1_31_0/libs/python/doc/v2/pickle.html
72+
// This mechanism was also adopted for PyCLIF:
73+
// https://github.com/google/clif/blob/7d388e1de7db5beeb3d7429c18a2776d8188f44f/clif/testing/python/pickle_compatibility.clif
74+
// The code below exercises the pickle protocol intentionally exactly as used
75+
// in PyCLIF, to ensure that pybind11 stays fully compatible with existing
76+
// client code relying on the long-established protocol. (It is impractical to
77+
// change all such client code.)
78+
79+
class StoreTwoWithState {
80+
public:
81+
StoreTwoWithState(int v0, int v1) : values_{v0, v1}, state_{"blank"} {}
82+
const std::vector<int> &GetInitArgs() const { return values_; }
83+
const std::string &GetState() const { return state_; }
84+
void SetState(const std::string &state) { state_ = state; }
85+
86+
private:
87+
std::vector<int> values_;
88+
std::string state_;
89+
};
90+
91+
void wrap(py::module m) {
92+
py::class_<StoreTwoWithState>(std::move(m), "StoreTwoWithState")
93+
.def(py::init<int, int>())
94+
.def("__getinitargs__",
95+
[](const StoreTwoWithState &self) { return py::tuple(py::cast(self.GetInitArgs())); })
96+
.def("__getstate__", &StoreTwoWithState::GetState)
97+
.def(
98+
"__reduce_ex__",
99+
[](py::handle self, int /*protocol*/) {
100+
return py::make_tuple(self.attr("__class__"),
101+
self.attr("__getinitargs__")(),
102+
self.attr("__getstate__")());
103+
},
104+
py::arg("protocol") = -1)
105+
.def(
106+
"__setstate__[non-constructor]", // See google/pywrapcc#30094 for background.
107+
[](StoreTwoWithState *self, const std::string &state) { self->SetState(state); },
108+
py::arg("state"));
109+
}
110+
111+
} // namespace exercise_getinitargs_getstate_setstate
112+
63113
TEST_SUBMODULE(pickling, m) {
64114
m.def("simple_callable", []() { return 20220426; });
65115

@@ -191,4 +241,5 @@ TEST_SUBMODULE(pickling, m) {
191241
#endif
192242

193243
exercise_trampoline::wrap(m);
244+
exercise_getinitargs_getstate_setstate::wrap(m);
194245
}

tests/test_pickling.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
import pickle
23
import re
34

@@ -91,3 +92,22 @@ def test_roundtrip_simple_cpp_derived():
9192
# Issue #3062: pickleable base C++ classes can incur object slicing
9293
# if derived typeid is not registered with pybind11
9394
assert not m.check_dynamic_cast_SimpleCppDerived(p2)
95+
96+
97+
#
98+
# exercise_getinitargs_getstate_setstate
99+
#
100+
def test_StoreTwoWithState():
101+
obj = m.StoreTwoWithState(-38, 27)
102+
assert obj.__getinitargs__() == (-38, 27)
103+
assert obj.__getstate__() == "blank"
104+
obj.__setstate__("blue")
105+
reduced = obj.__reduce_ex__()
106+
assert reduced == (m.StoreTwoWithState, (-38, 27), "blue")
107+
cpy = copy.deepcopy(obj)
108+
assert cpy.__reduce_ex__() == reduced
109+
assert pickle.HIGHEST_PROTOCOL > 0 # To be sure the loop below makes sense.
110+
for protocol in range(-1, pickle.HIGHEST_PROTOCOL + 1):
111+
serialized = pickle.dumps(obj, protocol)
112+
restored = pickle.loads(serialized)
113+
assert restored.__reduce_ex__() == reduced

0 commit comments

Comments
 (0)