[C++-sig] Proposal to improve Python exception handling in Boost.Python
David Abrahams
dave at boostpro.com
Mon Jun 23 22:47:34 CEST 2008
Frank Benkstein wrote:
> Hi,
>
> David Abrahams wrote:
>> on Thu Jun 19 2008, Frank Benkstein <benkstein-AT-langer-emv.de> wrote:
>>> <code>
>>> using namespace boost::python;
>>>
>>> try {
>>> object value = some_dict["key"];
>>> ...
>>> } catch (exceptions::key_error &e) {
>>> BOOST_ASSERT(e.args() == make_tuple("key"));
>>> // Do actual exception handling
>>> ...
>>> } catch (exceptions::base_exception &e) {
>>> std::cerr << e.traceback << std::flush;
>>> std::cerr << e << e.flush;
>>> }
>>> </code>
>>>
>>> Those exceptions could be automatically translated back to the original
>>> Python exceptions and the traceback restored (using PyErr_Restore).
>> Also good... but what is restoring the traceback for? Doesn't the
>> traceback that's already there work?
>
> I forgot to mention the main API difference between error_already_set
> and this new approach: error_already_set is just a signal that the
> Python error indicator (exception type, value and the traceback) is set.
> In this approach I would fetch those three objects and clear the
> indicator. The reason for this is that if you wrap
> Py_Initialize/Py_Finalize in a RAII-Style class an exception might cause
> Py_Finalize to be called. Py_Finalize will terminate the application
> with a fatal error (calling abort()) if the error indicator is set.
I don't know if you're aware of this, but Boost.Python is currently
incompatible with the use of Py_Finalize
But regardless of that, I don't understand the relationship of what you
wrote just above to clearing the Python error indicator.
> Also if you get another Python exception while already handling one you
> might want to be able to decide which one to rethrow. If you don't save
> the original traceback it is lost when the second exception is raised.
I see.
>>> If you (the Boost.Python developers) agree to this proposal I would
>>> like to submit patches implementing the mentioned functionality
>>> through the following steps:
>>>
>>> 1. Convert all sites throwing error_already_set or calling
>>> throw_error_already_set to calls to handle_python_exception.
>>> handle_python_exception would just call throw_error_already_set
>>> itself.
>> We probably ought to package up this little gem, which is a little
>> bigger than throw_error_already_set, into a single function in the
>> library:
>>
>> if (PyErr_Occurred())
>> throw_error_already_set();
>
> I don't understead, what you mean. My plan was to ensure that
> error_already_set is only thrown through a single function that we have
> control over. This function can later decide what to do: throw
> error_already_set or handle the exception in a Boost.Python specific
> way.
All I was saying was that that single function probably ought to include
the PyErr_Occurred check
>>> 2. Provide a registry for functions to be called when a specific Python
>>> exception is encountered.
>>> These functions get called with three
>>> boost::python::object arguments, the exception type, value and the
>>> traceback object when an exception is detected by
>>> handle_python_exception. As a fallback if no matching function is
>>> found error_already_set would still be thrown.
>>> 3. Provide a new namespace boost::python::exceptions with C++ exceptions
>>> that correspond to the Python standard exceptions[1] including
>>> inheritance.
>> Good... but boost::python::exceptions::Exception should be derived from
>> error_already_set for backward compatibility. Or maybe just
>>
>> typedef exceptions::Exception error_already_set;
>
> To reiterate:
>
> - If you catch error_already_set the error indicator is set, it is not
> safe to call Py_Finalize().
> - If you catch exceptions::Exception the error indicator is not set, it
> is safe to call Py_Finalize().
Okay... well, I really dislike the idea of having both things in the
library at once, and I was never really thrilled with error_already_set.
We should think about how to replace it with your thing. My guess is
that if the library starts throwing something not derived from
error_already_set, regardless of whether it leaves the Python error
there, it's going to break more code than if we changed the meaning of
error_already_set or "lied" by deriving your thing from it. There will
be transition issues, so we should discuss deprecation, compile-time
checks, runtime checks, etc.
>>> Each class should have a static boost::python::object
>>> member "mapped_type" wrapping the Python type object.
>> How about "python_type?"
>
> I don't care that much about the name. mapped_type was just to make it
> clear that this is the member that is used to find the C++ exception
> class belonging to a Python exception class. Also it is just the base
> type because the actual exception can be a subclass.
>
> I imagine the following procedure:
>
> 1. Register all your C++-mapped-exception classes in a sorted map or
> list somewhere in Boost.Python. When looking at the "mapped_type"
> member It is ensured that the subclasses come before their bases.
> 2. Whenever a Python exception is detected by Boost.Python (through a
> call to handle_python_exception) go through the map or list and throw
> the first C++ exception that matches.
>
> mapped_type would act as some kind of key in the map when storing the
> C++ exception. The actual Python exception type would act as the key
> when retrieving the C++ class.
>
>>> And instance members "type", "value" and "traceback" so it can be
>>> set or restored as the Python error indicator using PyErr_Restore
>>> or PyErr_SetObject if the traceback is None.
>> Do you really need to store the type in each instance? Oh, maybe you
>> do; it could end up being a python exception derived from one of the
>> standard exceptions. Then what's "mapped_type" (a.k.a. "python_type")
>> for?
Inquiring minds wanna know.
>>> 4. Register all exceptions from 3. with the mechanism from 2. Remove
>>> the fallback throwing error_already_set from handle_python_exception
>>> because base_exception would match all unknown exceptions.
>> Why do we want "base_exception," and why have the fallback if you're
>> just going to remove it?
>
> The four steps were meant to be incremental improvements (patches) so
> that Boost.Python stays functional between each step. I though that
> this would make reviewing easier.
I think I'd prefer a single analysis and proposal of what should be
done. We can talk about how to get there after I understand where we're
going.
--
Dave Abrahams
BoostPro Computing
http://www.boostpro.com
More information about the Cplusplus-sig
mailing list