pybind / pybind/pybind11

Do not change type of python exceptions when returning to Python

Open
#1,498 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
18k
Forks
2.3k
Avg merge
5d 17h
Merged PRs (30d)
10

Description

Issue description

If Python raises an exception, pybind11 handles that in C++ as pybind11::error_already_set and in PYBIND11_CATCH_INIT_EXCEPTIONS this is always sent to Python as an ImportError with the text of the pre-existing exception added to the ImportError.

Unfortunately, this means that the nuance of the Python exception is completely lost when calling py::import::module(). For example ModuleNotFoundError is rebranded as an ImportError and in particular ImportWarning or RuntimeWarning are forced to be ImportError. I feel that Pybind11 should behave more closely to Python.

As an example, here is what happens if warnings are turned fatal and py::import::module() imports a package that generates a warning:

Traceback (most recent call last):
  File "tests/test.py", line 3, in <module>
    import cmake_example as m
ImportError: ImportWarning: Not importing directory /Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/mpl_toolkits: missing __init__

At:
  <frozen importlib._bootstrap_external>(426): _find_module_shim
  /Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py(2111): _handle_ns
  /Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py(2194): declare_namespace
  /Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py(2633): activate
  /Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py(3102): <genexpr>

This does include the warning message but it is reported as an ImportError. The stack trace does not tell me anything about where the warning is coming from because it is truncated and also reversed from the python standard.

With this patch (ie, let python keep its exception as is):

diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h
index 3c67228..d38c279 100644
--- a/include/pybind11/detail/common.h
+++ b/include/pybind11/detail/common.h
@@ -229,7 +229,7 @@ extern "C" {
 
 #define PYBIND11_CATCH_INIT_EXCEPTIONS \
         catch (pybind11::error_already_set &e) {                               \
-            PyErr_SetString(PyExc_ImportError, e.what());                      \
+            e.restore();                                                       \
             return nullptr;                                                    \
         } catch (const std::exception &e) {                                    \
             PyErr_SetString(PyExc_ImportError, e.what());                      \

the stack trace becomes much more informative and Pythonic, and the trace is presented in the expected order and is complete:

Traceback (most recent call last):
  File "tests/test.py", line 3, in <module>
    import cmake_example as m
  File "/Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/astropy/__init__.py", line 118, in <module>
    _check_numpy()
  File "/Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/astropy/__init__.py", line 107, in _check_numpy
    requirement_met = minversion(numpy, __minimum_numpy_version__)
  File "/Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/astropy/utils/introspection.py", line 147, in minversion
    from pkg_resources import parse_version
  File "/Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py", line 3074, in <module>
    @_call_aside
  File "/Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py", line 3058, in _call_aside
    f(*args, **kwargs)
  File "/Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py", line 3102, in _initialize_master_working_set
    for dist in working_set
  File "/Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py", line 3102, in <genexpr>
    for dist in working_set
  File "/Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2633, in activate
    declare_namespace(pkg)
  File "/Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2191, in declare_namespace
    _handle_ns(packageName, path_item)
  File "/Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2111, in _handle_ns
    loader = importer.find_module(packageName)
  File "<frozen importlib._bootstrap_external>", line 426, in _find_module_shim
ImportWarning: Not importing directory /Users/timj/work/lsstsw3/miniconda/envs/lsst-scipipe/lib/python3.6/site-packages/mpl_toolkits: missing __init__

Finally, if you import a pybind11 module, that imports some python that imports some pybind11 that imports a python module that fails, you end up with ImportError: ImportError: ImportWarning as the ImportErrors keep stacking up.

Reproducible example code

This is a simple change that I made to cmake_example to demonstrate the issue. Any import that generates a warning will suffice (such as formatter in the standard distribution; although that doesn't have such a large stack trace so doesn't give the full effect). I have tested on python3.6 and 3.7.

diff --git a/src/main.cpp b/src/main.cpp
index 86ab582..ad93c6c 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -7,6 +7,8 @@ int add(int i, int j) {
 namespace py = pybind11;
 
 PYBIND11_MODULE(cmake_example, m) {
+
+    py::module::import("astropy.units");
     m.doc() = R"pbdoc(
         Pybind11 example plugin
         -----------------------
diff --git a/tests/test.py b/tests/test.py
index cea38c4..747cf0e 100644
--- a/tests/test.py
+++ b/tests/test.py
@@ -1,3 +1,5 @@
+import warnings
+warnings.simplefilter("error")
 import cmake_example as m
 
 assert m.__version__ == '0.0.1'

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in include/pybind11/detail/common.h at PYBIND11_CATCH_INIT_EXCEPTIONS, then reproduce the behavior with the cmake example in src/main.cpp and tests/test.py. Done means Python exceptions retain their original type and complete traceback when returned from a pybind11 module, including warning exceptions raised during imports.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
backend-api-design
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.