2016-06-14 29 views
11

Al fine di esporre un'eccezione C++ per Python in un modo che funziona davvero, devi scrivere qualcosa di simile:Boost.Python aggiungere associazioni per PyObject esistente (per la gestione delle eccezioni)

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); 
// ... 

Ma questo doesn' Sembra che si interrompa con qualsiasi altra cosa in Boost.Python. Se voglio esporre:

struct Error { int code; }; 

potrei scrivere:

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

Come posso combinare la classe vincolante per Error con la creazione un'eccezione per PyErr_NewException? Fondamentalmente, voglio fare throw Error{42} e farlo funzionare in modo ovvio da Python: posso catturare per Error o RuntimeError e avere quel lavoro, e posso prendere per AssertionError (o simile) e non prendere né lo Error né lanciare un SystemError .

+1

Leggermente fuori tema suggerimento, ma, [ 'Cython' sembra gestire questo bene] (http://docs.cython.org/src/userguide/wrapping_CPlusPlus.html#exceptions) . Forse potresti racchiudere la maggior parte del codice 'C++' con 'boost-python' e gestire casi così complicati con strumenti più flessibili come' Cython'? –

risposta

5

Il tipo Python creato con class_ ha un layout incompatibile con i tipi Python exceptions. Il tentativo di creare un tipo contenente entrambi nella sua gerarchia fallirà con un TypeError. Come il python tranne clausola eseguirà la verifica del tipo, una possibilità è quella di creare digitare un'eccezione Python che:

  • deriva dal tipo di eccezione Python desiderato (s)
  • proxy per un oggetto soggetto incorporato che è un'istanza di un tipo esposto attraverso Boost.Python

Questo approccio richiede pochi passi:

  • creare un pitone tipo di eccezione, derivanti da eccezioni Python
  • modificare l'eccezione Python definito dall'utente __delattr__, __getattr__ e __setattr metodi in modo da proxy per un oggetto soggetto incorporato
  • rattoppare inizializzatore l'eccezione Python definita dall'utente per incorporare un oggetto soggetto a cui lo farà delega

Un'implementazione Python puro dell'approccio sarebbe la seguente:

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) 

lo stesso approccio generale può essere utilizzato da Boost.Python, ovviando alla necessità di scrivere l'equivalente di class_ per le eccezioni. Tuttavia, ci sono passi e considerazioni aggiuntive:

  • registrare un traduttore con boost::python::register_exception_translator() che costruirà l'eccezione Python definito dall'utente quando un'istanza di C++ oggetto viene generata
  • il tipo di soggetto non può avere un inizializzatore esposta a Python. Quindi, quando si crea un'istanza dell'eccezione in Python, si dovrebbe tentare di inizializzare l'oggetto con __init__. D'altra parte, quando si crea un'istanza dell'eccezione in C++, si dovrebbe usare una conversione to-python per evitare __init__.
  • Si potrebbe desiderare di registrare da convertitori Python per consentire un'istanza del tipo di eccezione da passare da Python a C++, convertendola in un'istanza dell'oggetto spostato.

Ecco un esempio completo demonstrating l'approccio sopra descritto:

#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; }); 
} 

Nell'esempio precedente, il tipo C++ foo è esposto come example.FooError, quindi example.FooError ottiene ridefinito a un tipo di eccezione che deriva da RuntimeError e proxy all'originale example.FooError. Inoltre, il tipo C++ spam è esposto come example.Spam e viene definito un tipo di eccezione example.SpamError che deriva da IOError e SystemError e proxy a example.Spam. Lo example.SpamError è anche convertibile in tipo C++ spam.

utilizzo interattivo:

>>> 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

Questo è abbastanza sorprendente. Quindi, fondamentalmente, avere una gerarchia di eccezioni C++ è fuori dal momento che non c'è davvero modo di esprimerlo in Python tramite Boost? – Barry