diff --git a/include/boost/python/object/pickle_support.hpp b/include/boost/python/object/pickle_support.hpp new file mode 100644 index 00000000..730ae639 --- /dev/null +++ b/include/boost/python/object/pickle_support.hpp @@ -0,0 +1,96 @@ +// (C) Copyright R.W. Grosse-Kunstleve 2002. +// Permission to copy, use, modify, sell and distribute this software +// is granted provided this copyright notice appears in all copies. This +// software is provided "as is" without express or implied warranty, and +// with no claim as to its suitability for any purpose. +#ifndef BOOST_PYTHON_OBJECT_PICKLE_SUPPORT_RWGK20020603_HPP +#define BOOST_PYTHON_OBJECT_PICKLE_SUPPORT_RWGK20020603_HPP + +#include +#include + +namespace boost { namespace python { + +handle<> make_instance_reduce_function(); + +namespace error_messages { + + template + struct missing_pickle_support_function_or_incorrect_signature {}; + +} + +class pickle_support_base +{ + private: + struct dummy_return_type_ {}; + + public: + template + static + void + register_( + Class_& cl, + tuple (*getinitargs_fn)(Tgetinitargs), + dummy_return_type_* (*getstate_fn)(), + dummy_return_type_* (*setstate_fn)(), + bool) + { + cl.enable_pickle_support(false); + cl.def("__getinitargs__", getinitargs_fn); + } + + template + static + void + register_( + Class_& cl, + dummy_return_type_* (*getinitargs_fn)(), + tuple (*getstate_fn)(Tgetstate), + void (*setstate_fn)(Tsetstate, object), + bool getstate_manages_dict) + { + cl.enable_pickle_support(getstate_manages_dict); + cl.def("__getstate__", getstate_fn); + cl.def("__setstate__", setstate_fn); + } + + template + static + void + register_( + Class_& cl, + tuple (*getinitargs_fn)(Tgetinitargs), + tuple (*getstate_fn)(Tgetstate), + void (*setstate_fn)(Tsetstate, object), + bool getstate_manages_dict) + { + cl.enable_pickle_support(getstate_manages_dict); + cl.def("__getinitargs__", getinitargs_fn); + cl.def("__getstate__", getstate_fn); + cl.def("__setstate__", setstate_fn); + } + + template + static + void + register_( + Class_&, + ...) + { + typedef typename + error_messages::missing_pickle_support_function_or_incorrect_signature< + Class_>::error_type error_type; + } + + static dummy_return_type_* getinitargs() { return 0; } + static dummy_return_type_* getstate() { return 0; } + static dummy_return_type_* setstate() { return 0; } + + static bool getstate_manages_dict() { return false; } +}; + +}} // namespace boost::python + +#endif // BOOST_PYTHON_OBJECT_PICKLE_SUPPORT_RWGK20020603_HPP diff --git a/src/object/pickle_support.cpp b/src/object/pickle_support.cpp new file mode 100644 index 00000000..3b14c8ea --- /dev/null +++ b/src/object/pickle_support.cpp @@ -0,0 +1,63 @@ +// (C) Copyright R.W. Grosse-Kunstleve 2002. +// Permission to copy, use, modify, sell and distribute this software +// is granted provided this copyright notice appears in all copies. This +// software is provided "as is" without express or implied warranty, and +// with no claim as to its suitability for any purpose. + +#include +#include +#include +#include +#include +#include + +namespace boost { namespace python { + +namespace { + + tuple instance_reduce(object instance_obj) + { + list result; + object instance_class(instance_obj.attr("__class__")); + result.append(instance_class); + object none; + object getinitargs = getattr(instance_obj, "__getinitargs__", none); + tuple initargs; + if (getinitargs.ptr() != none.ptr()) { + initargs = tuple(getinitargs()); + } + result.append(initargs); + object getstate = getattr(instance_obj, "__getstate__", none); + object instance_dict = getattr(instance_obj, "__dict__", none); + long len_instance_dict = 0; + if (instance_dict.ptr() != none.ptr()) { + len_instance_dict = len(instance_dict); + } + if (getstate.ptr() != none.ptr()) { + if (len_instance_dict > 0) { + object getstate_manages_dict = getattr( + instance_obj, "__getstate_manages_dict__", none); + if (getstate_manages_dict.ptr() == none.ptr()) { + PyErr_SetString(PyExc_RuntimeError, + "Incomplete pickle support" + " (__getstate_manages_dict__ not set)"); + throw_error_already_set(); + } + } + result.append(getstate()); + } + else if (len_instance_dict > 0) { + result.append(instance_dict); + } + return tuple(result); + } + +} // namespace + +handle<> make_instance_reduce_function() +{ + static handle<> result(make_function(&instance_reduce)); + return result; +} + +}} // namespace boost::python diff --git a/test/pickle1.cpp b/test/pickle1.cpp new file mode 100644 index 00000000..b8b0fa92 --- /dev/null +++ b/test/pickle1.cpp @@ -0,0 +1,60 @@ +// Example by Ralf W. Grosse-Kunstleve + +/* + This example shows how to make an Extension Class "pickleable". + + The world class below can be fully restored by passing the + appropriate argument to the constructor. Therefore it is sufficient + to define the pickle interface method __getinitargs__. + + For more information refer to boost/libs/python/doc/pickle.html. + */ + +#include +#include +#include +#include + +#include + +namespace { + + // A friendly class. + class world + { + private: + std::string country; + public: + world(const std::string& country) { + this->country = country; + } + std::string greet() const { return "Hello from " + country + "!"; } + std::string get_country() const { return country; } + }; + + struct world_pickle_support : boost::python::pickle_support_base + { + static + boost::python::tuple + getinitargs(const world& w) + { + using namespace boost::python; + list result; + result.append(object(w.get_country())); + return tuple(result); + } + }; + +} + +BOOST_PYTHON_MODULE_INIT(pickle1_ext) +{ + using namespace boost::python; + module("pickle1_ext") + .add(class_("world") + .def_init(args()) + .def("greet", &world::greet) + .pickle_support(world_pickle_support()) + ) + ; +} diff --git a/test/pickle1.py b/test/pickle1.py new file mode 100644 index 00000000..1aeabc70 --- /dev/null +++ b/test/pickle1.py @@ -0,0 +1,30 @@ +r'''>>> import pickle1_ext + >>> import pickle + >>> pickle1_ext.world.__module__ + 'pickle1_ext' + >>> pickle1_ext.world.__safe_for_unpickling__ + 1 + >>> pickle1_ext.world.__name__ + 'world' + >>> pickle1_ext.world('Hello').__reduce__() + (, ('Hello',)) + >>> wd = pickle1_ext.world('California') + >>> pstr = pickle.dumps(wd) + >>> wl = pickle.loads(pstr) + >>> print wd.greet() + Hello from California! + >>> print wl.greet() + Hello from California! +''' + +def run(args = None): + if args is not None: + import sys + sys.argv = args + import doctest, pickle1 + return doctest.testmod(pickle1) + +if __name__ == '__main__': + import sys + sys.exit(run()[0]) + diff --git a/test/pickle2.cpp b/test/pickle2.cpp new file mode 100644 index 00000000..fed1066b --- /dev/null +++ b/test/pickle2.cpp @@ -0,0 +1,101 @@ +// Example by Ralf W. Grosse-Kunstleve + +/* + This example shows how to make an Extension Class "pickleable". + + The world class below contains member data (secret_number) that + cannot be restored by any of the constructors. Therefore it is + necessary to provide the __getstate__/__setstate__ pair of pickle + interface methods. + + For simplicity, the __dict__ is not included in the result of + __getstate__. This is not generally recommended, but a valid + approach if it is anticipated that the object's __dict__ will + always be empty. Note that safety guards are provided to catch + the cases where this assumption is not true. + + pickle3.cpp shows how to include the object's __dict__ in the + result of __getstate__. + + For more information refer to boost/libs/python/doc/pickle.html. + */ + +#include + +#include +#include +#include +#include +#include +#include + +namespace { // Avoid cluttering the global namespace. + + // A friendly class. + class world + { + public: + world(const std::string& country) : secret_number(0) { + this->country = country; + } + std::string greet() const { return "Hello from " + country + "!"; } + std::string get_country() const { return country; } + void set_secret_number(int number) { secret_number = number; } + int get_secret_number() const { return secret_number; } + private: + std::string country; + int secret_number; + }; + + struct world_pickle_support : boost::python::pickle_support_base + { + static + boost::python::tuple + getinitargs(const world& w) + { + using namespace boost::python; + list result; + result.append(object(w.get_country())); + return tuple(result); + } + + static + boost::python::tuple + getstate(const world& w) + { + using namespace boost::python; + list result; + result.append(object(w.get_secret_number())); + return tuple(result); + } + + static + void + setstate(world& w, boost::python::object state) + { + using namespace boost::python; + extract state_proxy(state); + if (!state_proxy.check() || len(state_proxy()) != 1) { + PyErr_SetString(PyExc_ValueError, + "Unexpected argument in call to __setstate__."); + throw_error_already_set(); + } + long number = extract(state_proxy()[0])(); + if (number != 42) w.set_secret_number(number); + } + }; + +} + +BOOST_PYTHON_MODULE_INIT(pickle2_ext) +{ + boost::python::module("pickle2_ext") + .add(boost::python::class_("world") + .def_init(boost::python::args()) + .def("greet", &world::greet) + .def("get_secret_number", &world::get_secret_number) + .def("set_secret_number", &world::set_secret_number) + .pickle_support(world_pickle_support()) + ) + ; +} diff --git a/test/pickle2.py b/test/pickle2.py new file mode 100644 index 00000000..7d31e48b --- /dev/null +++ b/test/pickle2.py @@ -0,0 +1,44 @@ +r'''>>> import pickle2_ext + >>> import pickle + >>> pickle2_ext.world.__module__ + 'pickle2_ext' + >>> pickle2_ext.world.__safe_for_unpickling__ + 1 + >>> pickle2_ext.world.__name__ + 'world' + >>> pickle2_ext.world('Hello').__reduce__() + (, ('Hello',), (0,)) + >>> for number in (24, 42): + ... wd = pickle2_ext.world('California') + ... wd.set_secret_number(number) + ... pstr = pickle.dumps(wd) + ... wl = pickle.loads(pstr) + ... print wd.greet(), wd.get_secret_number() + ... print wl.greet(), wl.get_secret_number() + Hello from California! 24 + Hello from California! 24 + Hello from California! 42 + Hello from California! 0 + +# Now show that the __dict__ is not taken care of. + >>> wd = pickle2_ext.world('California') + >>> wd.x = 1 + >>> wd.__dict__ + {'x': 1} + >>> try: pstr = pickle.dumps(wd) + ... except RuntimeError, err: print err[0] + ... + Incomplete pickle support (__getstate_manages_dict__ not set) +''' + +def run(args = None): + if args is not None: + import sys + sys.argv = args + import doctest, pickle2 + return doctest.testmod(pickle2) + +if __name__ == '__main__': + import sys + sys.exit(run()[0]) + diff --git a/test/pickle3.cpp b/test/pickle3.cpp new file mode 100644 index 00000000..617143d3 --- /dev/null +++ b/test/pickle3.cpp @@ -0,0 +1,107 @@ +// Example by Ralf W. Grosse-Kunstleve + +/* + This example shows how to make an Extension Class "pickleable". + + The world class below contains member data (secret_number) that + cannot be restored by any of the constructors. Therefore it is + necessary to provide the __getstate__/__setstate__ pair of pickle + interface methods. + + The object's __dict__ is included in the result of __getstate__. + This requires more code (compare with pickle2.cpp), but is + unavoidable if the object's __dict__ is not always empty. + + For more information refer to boost/libs/python/doc/pickle.html. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { // Avoid cluttering the global namespace. + + // A friendly class. + class world + { + public: + world(const std::string& country) : secret_number(0) { + this->country = country; + } + std::string greet() const { return "Hello from " + country + "!"; } + std::string get_country() const { return country; } + void set_secret_number(int number) { secret_number = number; } + int get_secret_number() const { return secret_number; } + private: + std::string country; + int secret_number; + }; + + struct world_pickle_support : boost::python::pickle_support_base + { + static + boost::python::tuple + getinitargs(const world& w) + { + using namespace boost::python; + list result; + result.append(object(w.get_country())); + return tuple(result); + } + + static + boost::python::tuple + getstate(boost::python::object w_obj) + { + using namespace boost::python; + world const& w = extract(w_obj)(); + list result; + // store the object's __dict__ + result.append(w_obj.attr("__dict__")); + // store the internal state of the C++ object + result.append(object(w.get_secret_number())); + return tuple(result); + } + + static + void + setstate(boost::python::object w_obj, boost::python::object state) + { + using namespace boost::python; + world& w = extract(w_obj)(); + extract state_proxy(state); + if (!state_proxy.check() || len(state_proxy()) != 2) { + PyErr_SetString(PyExc_ValueError, + "Unexpected argument in call to __setstate__."); + throw_error_already_set(); + } + // restore the object's __dict__ + w_obj.attr("__dict__").attr("update")(object(state_proxy()[0])); + // restore the internal state of the C++ object + long number = extract(state_proxy()[1])(); + if (number != 42) w.set_secret_number(number); + } + + static bool getstate_manages_dict() { return true; } + }; + +} + +BOOST_PYTHON_MODULE_INIT(pickle3_ext) +{ + boost::python::module("pickle3_ext") + .add(boost::python::class_("world") + .def_init(boost::python::args()) + .def("greet", &world::greet) + .def("get_secret_number", &world::get_secret_number) + .def("set_secret_number", &world::set_secret_number) + .pickle_support(world_pickle_support()) + ) + ; +} diff --git a/test/pickle3.py b/test/pickle3.py new file mode 100644 index 00000000..8cce8dfb --- /dev/null +++ b/test/pickle3.py @@ -0,0 +1,38 @@ +r'''>>> import pickle3_ext + >>> import pickle + >>> pickle3_ext.world.__module__ + 'pickle3_ext' + >>> pickle3_ext.world.__safe_for_unpickling__ + 1 + >>> pickle3_ext.world.__getstate_manages_dict__ + 1 + >>> pickle3_ext.world.__name__ + 'world' + >>> pickle3_ext.world('Hello').__reduce__() + (, ('Hello',), ({}, 0)) + >>> for number in (24, 42): + ... wd = pickle3_ext.world('California') + ... wd.set_secret_number(number) + ... wd.x = 2 * number + ... wd.y = 'y' * number + ... wd.z = 3. * number + ... pstr = pickle.dumps(wd) + ... wl = pickle.loads(pstr) + ... print wd.greet(), wd.get_secret_number(), wd.x, wd.y, wd.z + ... print wl.greet(), wl.get_secret_number(), wl.x, wl.y, wl.z + Hello from California! 24 48 yyyyyyyyyyyyyyyyyyyyyyyy 72.0 + Hello from California! 24 48 yyyyyyyyyyyyyyyyyyyyyyyy 72.0 + Hello from California! 42 84 yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy 126.0 + Hello from California! 0 84 yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy 126.0 +''' + +def run(args = None): + if args is not None: + import sys + sys.argv = args + import doctest, pickle3 + return doctest.testmod(pickle3) + +if __name__ == '__main__': + import sys + sys.exit(run()[0])