2016-06-14 22 views
11

W celu odsłonięcia C++ do Python wyjątek w sposób, który rzeczywiście działa, trzeba napisać coś takiego:Boost.Python dodać do istniejącego powiązania PyObject (dla obsługi wyjątków)

std::string scope = py::extract<std::string>(py::scope().attr("__name__")); 
std::string full_name = scope + "." + name; 
PyObject* exc_type = PyErr_NewException(&full_name[0], PyExc_RuntimeError, 0); 
// ... 

Ale to nie robi” Wydaje się, że interraktuje z czymkolwiek innym w Boost.Python. Jeśli chcę wystawiać:

struct Error { int code; }; 

mógłbym napisać:

py::class_<Error>("Error", py::no_init) 
    .def_readonly("code", &Error::code) 
; 

Jak mogę połączyć wiążący dla Error z tworzeniem wyjątek PyErr_NewException klasę? Zasadniczo, chcę throw Error{42} i mieć, że praca w oczywisty sposób od Python: mogę złapać przez Error lub RuntimeError i mieć tę pracę, a ja mogę złapać przez AssertionError (lub podobny) i mieć, że ani złapać Error ani rzucać SystemError .

+1

Lekko off topic ale sugestia, [ 'Cython' zdaje sobie z tym poradzić ładnie] (http://docs.cython.org/src/userguide/wrapping_CPlusPlus.html#exceptions) . Może mógłbyś owinąć większość kodu 'C++' 'boost-python' i poradzić sobie z tak trudnymi przypadkami używając bardziej elastycznych narzędzi takich jak' Cython'? –

Odpowiedz

5

Typ Pythona utworzony za pomocą class_ ma niezgodny układ z typami Python exceptions. Próba utworzenia typu zawierającego oba w swojej hierarchii zakończy się niepowodzeniem z TypeError. Jako Pythonową wyjątkiem klauzuli wykonuje kontrolę typu, jedną z opcji jest, aby utworzyć pythonowy wyjątek typu że:

  • pochodzi od typu (typów) żądanego pythonowy wyjątek
  • proxy z osadzonym zastrzeżeniem obiektu, który jest instancją typu odsłoniętej przez Boost.Python

podejście to wymaga kilku etapów:

  • utworzyć Python typ wyjątku, pochodzących z wyjątkami Pythona
  • modyfikacji __delattr__, __getattr__ i __setattr metody pytona wyjątek, zdefiniowane przez użytkownika, tak że proxy osadzonym zastrzeżeniem obiektu
  • plaster inicjatora pytona wyjątek, zdefiniowane przez użytkownika, aby osadzić obiektu, na podstawie których to będzie proxy

czysty implementacja Pythona podejścia byłby następujący:

def as_exception(base): 
    ''' Decorator that will return a type derived from `base` and proxy to the 
     decorated class. 

    ''' 
    def make_exception_type(wrapped_cls): 
     # Generic proxying to subject. 
     def del_subject_attr(self, name): 
      return delattr(self._subject, name) 

     def get_subject_attr(self, name): 
      return getattr(self._subject, name) 

     def set_subject_attr(self, name, value): 
      return setattr(self._subject, name, value) 

     # Create new type that derives from base and proxies to subject. 
     exception_type = type(wrapped_cls.__name__, (base,), { 
      '__delattr__': del_subject_attr, 
      '__getattr__': get_subject_attr, 
      '__setattr__': set_subject_attr, 
     }) 

     # Monkey-patch the initializer now that it has been created. 
     original_init = exception_type.__init__ 

     def init(self, *args, **kwargs): 
      original_init(self, *args, **kwargs) 
      self.__dict__['_subject'] = wrapped_cls(*args, **kwargs) 
     exception_type.__init__ = init 

     return exception_type 
    return make_exception_type 


@as_exception(RuntimeError) 
class Error: 
    def __init__(self, code): 
     self.code = code 

assert(issubclass(Error, RuntimeError)) 
try: 
    raise Error(42) 
except RuntimeError as e: 
    assert(e.code == 42) 
except: 
    assert(False) 

to samo ogólne podejście może być używany przez Boost.Python, eliminując potrzebę zapisu odpowiednika class_ dla wyjątków. Jednakże, istnieją dodatkowe kroki i:

  • zarejestrować tłumacza z boost::python::register_exception_translator() które budowę wyjątek Pythona zdefiniowany przez użytkownika gdy instancja obiektu C++ wyrzuca
  • typu obiekt może mieć odsłonięte inicjatora do Pythona. Dlatego przy tworzeniu wystąpienia wyjątku w Pythonie należy spróbować zainicjować obiekt za pomocą __init__. Z drugiej strony, podczas tworzenia wystąpienia wyjątku w C++, należy użyć konwersji na python, aby uniknąć __init__.
  • Można chcieć zarejestrować się w konwerterach Pythona, aby umożliwić przekazanie instancji typu wyjątku z Python do C++, konwertując ją do instancji owiniętego obiektu.

Poniżej jest pełny przykład demonstrating metody opisanej powyżej:

#include <boost/python.hpp> 

namespace exception { 
namespace detail { 

/// @brief Return a Boost.Python object given a borrowed object. 
template <typename T> 
boost::python::object borrowed_object(T* object) 
{ 
    namespace python = boost::python; 
    python::handle<T> handle(python::borrowed(object)); 
    return python::object(handle); 
} 

/// @brief Return a tuple of Boost.Python objects given borrowed objects. 
boost::python::tuple borrowed_objects(
    std::initializer_list<PyObject*> objects) 
{ 
    namespace python = boost::python; 
    python::list objects_; 

    for(auto&& object: objects) 
    { 
    objects_.append(borrowed_object(object)); 
    } 

    return python::tuple(objects_); 
} 

/// @brief Get the class object for a wrapped type that has been exposed 
///  through Boost.Python. 
template <typename T> 
boost::python::object get_instance_class() 
{ 
    namespace python = boost::python; 
    python::type_info type = python::type_id<T>(); 
    const python::converter::registration* registration = 
    python::converter::registry::query(type); 

    // If the class is not registered, return None. 
    if (!registration) return python::object(); 

    return detail::borrowed_object(registration->get_class_object()); 
} 

} // namespace detail 
namespace proxy { 

/// @brief Get the subject object from a proxy. 
boost::python::object get_subject(boost::python::object proxy) 
{ 
    return proxy.attr("__dict__")["_obj"]; 
} 

/// @brief Check if the subject has a subject. 
bool has_subject(boost::python::object proxy) 
{ 
    return boost::python::extract<bool>(
    proxy.attr("__dict__").attr("__contains__")("_obj")); 
} 

/// @brief Set the subject object on a proxy object. 
boost::python::object set_subject(
    boost::python::object proxy, 
    boost::python::object subject) 
{ 
    return proxy.attr("__dict__")["_obj"] = subject; 
} 

/// @brief proxy's __delattr__ that delegates to the subject. 
void del_subject_attr(
    boost::python::object proxy, 
    boost::python::str name) 
{ 
    delattr(get_subject(proxy), name); 
}; 

/// @brief proxy's __getattr__ that delegates to the subject. 
boost::python::object get_subject_attr(
    boost::python::object proxy, 
    boost::python::str name) 
{ 
    return getattr(get_subject(proxy), name); 
}; 

/// @brief proxy's __setattr__ that delegates to the subject. 
void set_subject_attr(
    boost::python::object proxy, 
    boost::python::str name, 
    boost::python::object value) 
{ 
    setattr(get_subject(proxy), name, value); 
}; 

boost::python::dict proxy_attrs() 
{ 
    // By proxying to Boost.Python exposed object, one does not have to 
    // reimplement the entire Boost.Python class_ API for exceptions. 

    // Generic proxying. 
    boost::python::dict attrs; 
    attrs["__detattr__"] = &del_subject_attr; 
    attrs["__getattr__"] = &get_subject_attr; 
    attrs["__setattr__"] = &set_subject_attr; 
    return attrs; 
} 

} // namespace proxy 

/// @brief Registers from-Python converter for an exception type. 
template <typename Subject> 
struct from_python_converter 
{ 
    from_python_converter() 
    { 
    boost::python::converter::registry::push_back(
     &convertible, 
     &construct, 
     boost::python::type_id<Subject>() 
    ); 
    } 

    static void* convertible(PyObject* object) 
    { 
    namespace python = boost::python; 
    python::object subject = proxy::get_subject(
     detail::borrowed_object(object) 
    ); 

    // Locate registration based on the C++ type. 
    python::object subject_instance_class = 
     detail::get_instance_class<Subject>(); 
    if (!subject_instance_class) return nullptr; 

    bool is_instance = (1 == PyObject_IsInstance(
     subject.ptr(), 
     subject_instance_class.ptr() 
    )); 
    return is_instance 
     ? object 
     : nullptr; 
    } 

    static void construct(
    PyObject* object, 
    boost::python::converter::rvalue_from_python_stage1_data* data) 
    { 
    // Object is a borrowed reference, so create a handle indicting it is 
    // borrowed for proper reference counting. 
    namespace python = boost::python; 
    python::object proxy = detail::borrowed_object(object); 

    // Obtain a handle to the memory block that the converter has allocated 
    // for the C++ type. 
    using storage_type = 
     python::converter::rvalue_from_python_storage<Subject>; 
    void* storage = reinterpret_cast<storage_type*>(data)->storage.bytes; 

    // Copy construct the subject into the converter storage block. 
    python::object subject = proxy::get_subject(proxy); 
    new (storage) Subject(python::extract<const Subject&>(subject)()); 

    // Indicate the object has been constructed into the storage. 
    data->convertible = storage; 
    } 

}; 

/// @brief Expose an exception type in the current scope, that embeds and 
//   proxies to the Wrapped type. 
template <typename Wrapped> 
class exception: 
    boost::python::object 
{ 
public: 

    /// @brief Expose a RuntimeError exception type with the provided name. 
    exception(const char* name) : exception(name, {}) {} 

    /// @brief Expose an expcetion with the provided name, deriving from the 
    ///  borrowed base type. 
    exception(
    const char* name, 
    PyObject* borrowed_base 
) : exception(name, {borrowed_base}) {} 

    /// @brief Expose an expcetion with the provided name, deriving from the 
    ///  multiple borrowed base type. 
    exception(
    const char* name, 
    std::initializer_list<PyObject*> borrowed_bases 
) : exception(name, detail::borrowed_objects(borrowed_bases)) {} 

    /// @brief Expose an expcetion with the provided name, deriving from tuple 
    ///  of bases. 
    exception(
    const char* name, 
    boost::python::tuple bases) 
    { 
    // Default to deriving from Python's RuntimeError. 
    if (!bases) 
    { 
     bases = make_tuple(detail::borrowed_object(PyExc_RuntimeError)); 
    } 

    register_exception_type(name, bases); 
    patch_initializer(); 
    register_translator(); 
    } 

public: 

    exception& enable_from_python() 
    { 
    from_python_converter<Wrapped>{}; 
    return *this; 
    } 

private: 

    /// @brief Handle to this class object. 
    boost::python::object this_class_object() { return *this; } 

    /// @brief Create the Python exception type and install it into this object. 
    void register_exception_type(
    std::string name, 
    boost::python::tuple bases) 
    { 
    // Copy the instance class' name and scope. 
    namespace python = boost::python; 
    auto scoped_name = python::scope().attr("__name__") + "." + name; 

    // Docstring handling. 
    auto docstring = detail::get_instance_class<Wrapped>().attr("__doc__"); 

    // Create exception dervied from the desired exception types, but with 
    // the same name as the Boost.Python class. This is required because 
    // Python exception types and Boost.Python classes have incompatiable 
    // layouts. 
    // >> type_name = type(fullname, (bases,), {proxying attrs}) 
    python::handle<> handle(PyErr_NewExceptionWithDoc(
     python::extract<char*>(scoped_name)(), 
     docstring ? python::extract<char*>(docstring)() : nullptr, 
     bases.ptr(), 
     proxy::proxy_attrs().ptr() 
    )); 

    // Assign the exception type to this object. 
    python::object::operator=(python::object{handle}); 

    // Insert this object into current scope. 
    setattr(python::scope(), name, this_class_object()); 
    } 

    /// @brief Patch the initializer to install the delegate object. 
    void patch_initializer() 
    { 
    namespace python = boost::python; 
    auto original_init = getattr(this_class_object(), "__init__"); 

    // Use raw function so that *args and **kwargs can transparently be 
    // passed to the initializers. 
    this_class_object().attr("__init__") = python::raw_function(
     [original_init](
     python::tuple args, // self + *args 
     python::dict kwargs) // **kwargs 
     { 
     original_init(*args, **kwargs); 
     // If the subject does not exists, then create it. 
     auto self = args[0]; 
     if (!proxy::has_subject(self)) 
     { 
      proxy::set_subject(self, detail::get_instance_class<Wrapped>()(
      *args[python::slice(1, python::_)], // args[1:] 
      **kwargs 
     )); 
     } 

     return python::object{}; // None 
     }); 
    } 

    // @brief Register translator within the Boost.Python exception handling 
    //  chaining. This allows for an instance of the wrapped type to be 
    //  converted to an instance of this exception. 
    void register_translator() 
    { 
    namespace python = boost::python; 
    auto exception_type = this_class_object(); 
    python::register_exception_translator<Wrapped>(
     [exception_type](const Wrapped& proxied_object) 
     { 
     // Create the exception object. If a subject is not installed before 
     // the initialization of the instance, then a subject will attempt to 
     // be installed. As the subject may not be constructible from Python, 
     // manually inject a subject after construction, but before 
     // initialization. 
     python::object exception_object = exception_type.attr("__new__")(
      exception_type 
     ); 

     proxy::set_subject(exception_object, python::object(proxied_object)); 

     // Initialize the object. 
     exception_type.attr("__init__")(exception_object); 

     // Set the exception. 
     PyErr_SetObject(exception_type.ptr(), exception_object.ptr()); 
     }); 
    } 
}; 

// @brief Visitor that will turn the visited class into an exception, 
/// enabling exception translation. 
class export_as_exception 
    : public boost::python::def_visitor<export_as_exception> 
{ 
public: 

    /// @brief Expose a RuntimeError exception type. 
    export_as_exception() : export_as_exception({}) {} 

    /// @brief Expose an expcetion type deriving from the borrowed base type. 
    export_as_exception(PyObject* borrowed_base) 
    : export_as_exception({borrowed_base}) {} 

    /// @brief Expose an expcetion type deriving from multiple borrowed 
    ///  base types. 
    export_as_exception(std::initializer_list<PyObject*> borrowed_bases) 
    : export_as_exception(detail::borrowed_objects(borrowed_bases)) {} 

    /// @brief Expose an expcetion type deriving from multiple bases. 
    export_as_exception(boost::python::tuple bases) : bases_(bases) {} 

private: 

    friend class boost::python::def_visitor_access; 

    template <typename Wrapped, typename ...Args> 
    void visit(boost::python::class_<Wrapped, Args...> instance_class) const 
    { 
    exception<Wrapped>{ 
     boost::python::extract<const char*>(instance_class.attr("__name__"))(), 
     bases_ 
    }; 
    } 

private: 
    boost::python::tuple bases_; 
}; 

} // namespace exception 

struct foo { int code; }; 

struct spam 
{ 
    spam(int code): code(code) {} 
    int code; 
}; 

BOOST_PYTHON_MODULE(example) 
{ 
    namespace python = boost::python; 

    // Expose `foo` as `example.FooError`. 
    python::class_<foo>("FooError", python::no_init) 
    .def_readonly("code", &foo::code) 
    // Redefine the exposed `example.FooError` class as an exception. 
    .def(exception::export_as_exception(PyExc_RuntimeError)); 
    ; 

    // Expose `spam` as `example.Spam`. 
    python::class_<spam>("Spam", python::init<int>()) 
    .def_readwrite("code", &spam::code) 
    ; 

    // Also expose `spam` as `example.SpamError`. 
    exception::exception<spam>("SpamError", {PyExc_IOError, PyExc_SystemError}) 
    .enable_from_python() 
    ; 

    // Verify from-python. 
    python::def("test_foo", +[](int x){ throw foo{x}; }); 
    // Verify to-Python and from-Python. 
    python::def("test_spam", +[](const spam& error) { throw error; }); 
} 

W powyższym przykładzie, typu C++ foo narażony jest example.FooError, następnie example.FooError zostanie na nowo na typ wyjątku, że pochodzi od RuntimeError i serwerów proxy do oryginalnego example.FooError. Ponadto typ C++ spam jest wyeksponowany jako example.Spam, a zdefiniowany jest wyjątek typu example.SpamError, który wywodzi się od IOError i SystemError, a także serwerów proxy do example.Spam. Model example.SpamError można również przekształcić w typ C++ spam.

Interactive Wykorzystanie:

>>> import example 
>>> try: 
...  example.test_foo(100) 
... except example.FooError as e: 
...  assert(isinstance(e, RuntimeError)) 
...  assert(e.code == 100) 
... except: 
...  assert(False) 
... 
>>> try: 
...  example.test_foo(101) 
... except RuntimeError as e: 
...  assert(isinstance(e, example.FooError)) 
...  assert(e.code == 101) 
... except: 
...  assert(False) 
... 
... spam_error = example.SpamError(102) 
... assert(isinstance(spam_error, IOError)) 
... assert(isinstance(spam_error, SystemError)) 
>>> try: 
...  example.test_spam(spam_error) 
... except IOError as e: 
...  assert(e.code == 102) 
... except: 
...  assert(False) 
... 
+0

To jest naprawdę niesamowite. Więc zasadniczo, mając hierarchię wyjątków C++, ponieważ nie ma sposobu, aby wyrazić to w Pythonie poprzez Boost? – Barry

Powiązane problemy