From 8c0fcf94629f72d8e52575f151f1604d63799129 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 26 Jun 2026 02:30:22 +0100 Subject: [PATCH 01/10] Fix wrapping of explicitly listed free functions --- cppwg/info/free_function_info.py | 13 ++++++ cppwg/parsers/package_info_parser.py | 2 +- tests/test_package_info_parser.py | 59 ++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/test_package_info_parser.py diff --git a/cppwg/info/free_function_info.py b/cppwg/info/free_function_info.py index b89cb41..9a5b21f 100644 --- a/cppwg/info/free_function_info.py +++ b/cppwg/info/free_function_info.py @@ -1,5 +1,6 @@ """Free function information structure.""" +import logging from typing import Any, Dict, Optional from cppwg.info.cpp_entity_info import CppEntityInfo @@ -25,4 +26,16 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 The source namespace """ ff_decls = source_ns.free_functions(self.name, allow_empty=True) + + if not ff_decls: + # The function's header was not parsed. For explicitly listed free + # functions, the header is only included when source_file or + # source_file_path is set in the config. + logger = logging.getLogger() + logger.error( + f"Could not find free function {self.name}. Set source_file or " + "source_file_path in the config so that its header is included." + ) + raise RuntimeError(f"Could not find free function: {self.name}") + self.decls = [ff_decls[0]] diff --git a/cppwg/parsers/package_info_parser.py b/cppwg/parsers/package_info_parser.py index 0fa71ac..9b67092 100644 --- a/cppwg/parsers/package_info_parser.py +++ b/cppwg/parsers/package_info_parser.py @@ -212,7 +212,7 @@ def parse(self) -> PackageInfo: # Create the CppFreeFunctionInfo object from the free function config dict free_function_info = CppFreeFunctionInfo( - free_function_config["name"], free_function_config + raw_free_function_info["name"], free_function_config ) # Add the free function to the module diff --git a/tests/test_package_info_parser.py b/tests/test_package_info_parser.py new file mode 100644 index 0000000..76a3a5b --- /dev/null +++ b/tests/test_package_info_parser.py @@ -0,0 +1,59 @@ +"""Unit tests for cppwg.parsers.package_info_parser.""" + +import os +import textwrap + +from cppwg.parsers.package_info_parser import PackageInfoParser + + +def _write_config(tmp_path, body): + """Write a package info yaml file and return its path.""" + config_path = os.path.join(tmp_path, "package_info.yaml") + with open(config_path, "w") as config_file: + config_file.write(textwrap.dedent(body)) + return config_path + + +def test_parses_explicit_free_function_list(tmp_path): + """An explicit free_functions list is parsed without error. + + Regression test: the parser previously raised KeyError('name') for any + explicitly listed free function, so the explicit free_functions path always + crashed before reaching the C++ source. + """ + config_path = _write_config( + tmp_path, + """ + name: testpkg + modules: + - name: mymod + free_functions: + - name: my_func + """, + ) + + package_info = PackageInfoParser(config_path, str(tmp_path)).parse() + + module_info = package_info.module_collection[0] + assert module_info.use_all_free_functions is False + assert [ff.name for ff in module_info.free_function_collection] == ["my_func"] + + +def test_parses_all_free_functions_option(tmp_path): + """The CPPWG_ALL free_functions option sets use_all_free_functions.""" + config_path = _write_config( + tmp_path, + """ + name: testpkg + modules: + - name: mymod + free_functions: CPPWG_ALL + """, + ) + + package_info = PackageInfoParser(config_path, str(tmp_path)).parse() + + module_info = package_info.module_collection[0] + assert module_info.use_all_free_functions is True + # Discovery happens later from the parsed source, so none are added yet. + assert module_info.free_function_collection == [] From 898b9d272098058455536a1318efd46a971ab029 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 26 Jun 2026 02:33:33 +0100 Subject: [PATCH 02/10] #83 Generate pybind11 exception translators from config --- README.md | 5 ++ cppwg/info/package_info.py | 105 +++++++++++++++++++++- cppwg/parsers/package_info_parser.py | 1 + cppwg/writers/header_collection_writer.py | 12 +++ cppwg/writers/module_writer.py | 45 ++++++++++ 5 files changed, 167 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0473380..78d8120 100644 --- a/README.md +++ b/README.md @@ -165,5 +165,10 @@ r = Rectangle(4, 5) - To pass extra flags to the castxml clang frontend (e.g. to silence a diagnostic), use `--castxml_cflags`. Values starting with `-` must use `=`, e.g. `--castxml_cflags="-Wno-deprecated"`. +- To stop C++ exceptions from crashing the Python interpreter, list their class + names under `exceptions` in the config. cppwg generates a pybind11 exception + translator for each. By default the message is read with `what()`; set + `message_method` on an entry to use a different accessor (e.g. `GetMessage`). + See `examples/shapes/wrapper/package_info.yaml` for an example. - See the [pybind11 documentation](https://pybind11.readthedocs.io/) for help on pybind11 wrapper code. diff --git a/cppwg/info/package_info.py b/cppwg/info/package_info.py index 0ca426d..33c0a12 100644 --- a/cppwg/info/package_info.py +++ b/cppwg/info/package_info.py @@ -4,7 +4,9 @@ import logging import os from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple + +from pygccxml import declarations from cppwg.info.base_info import BaseInfo from cppwg.utils.constants import CPPWG_EXT @@ -18,6 +20,11 @@ class PackageInfo(BaseInfo): ---------- common_include_file : bool Use a common include file for all source files + exceptions : List[Union[str, Dict[str, str]]] + C++ exception classes to translate into Python exceptions. Each entry is + a class name, or a dict with a `name` and an optional `message_method` + (the accessor for the message, defaulting to "what"). A pybind11 + exception translator is generated automatically for each. exclude_default_args : bool Exclude default arguments from method wrappers. name : str @@ -25,6 +32,9 @@ class PackageInfo(BaseInfo): source_hpp_patterns : List[str] A list of source file patterns to include + exception_info : List[Dict[str, str]] + Resolved exception translation data (cpp_type, message_expr, + source_file), populated from `exceptions` after parsing the source. module_collection : List[ModuleInfo] A list of module info objects associated with this package source_hpp_files : List[str] @@ -47,9 +57,11 @@ def __init__( super().__init__(name, package_config) self.common_include_file: bool = False + self.exceptions: List[str] = [] self.exclude_default_args: bool = False self.source_hpp_patterns: List[str] = ["*.hpp"] + self.exception_info: List[Dict[str, str]] = [] self.module_collection: List["ModuleInfo"] = [] # noqa: F821 self.source_hpp_files: List[str] = [] @@ -57,6 +69,7 @@ def __init__( self.common_include_file = package_config.get( "common_include_file", self.common_include_file ) + self.exceptions = package_config.get("exceptions", self.exceptions) self.exclude_default_args = package_config.get( "exclude_default_args", self.exclude_default_args ) @@ -152,3 +165,93 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 """ for module_info in self.module_collection: module_info.update_from_ns(source_ns) + + self.resolve_exceptions(source_ns) + + @staticmethod + def parse_exception_entry(entry: Any) -> Tuple[str, str]: + """ + Return the (class name, message method) for an exceptions config entry. + + An entry may be a bare class name string, or a dict with a `name` and an + optional `message_method` (defaulting to "what"). + + Parameters + ---------- + entry : Any + A single entry from the `exceptions` config list. + + Returns + ------- + Tuple[str, str] + The exception class name and the message accessor method name. + """ + if isinstance(entry, dict): + return entry["name"], entry.get("message_method", "what") + return entry, "what" + + @property + def exception_names(self) -> List[str]: + """Return the names of the configured exception classes.""" + return [self.parse_exception_entry(entry)[0] for entry in self.exceptions] + + def resolve_exceptions(self, source_ns: "namespace_t") -> None: # noqa: F821 + """ + Resolve exception config entries into translation data. + + For each entry in `exceptions`, look up the class in the source + namespace, work out how to extract its message (calling the configured + message_method, defaulting to what(), and adding .c_str() unless it + already returns a pointer), and find which header declares it. The + result is used to generate a pybind11 exception translator per module. + + Parameters + ---------- + source_ns : pygccxml.declarations.namespace_t + The source namespace + """ + logger = logging.getLogger() + + self.exception_info = [] + for entry in self.exceptions: + name, message_method = self.parse_exception_entry(entry) + + class_decls = source_ns.classes( + lambda decl: decl.name == name, allow_empty=True # noqa: B023 + ) + + if not class_decls: + logger.error(f"Could not find exception class {name}.") + raise RuntimeError(f"Could not find exception class: {name}") + + class_decl = class_decls[0] + + # PyErr_SetString needs a const char*. Add .c_str() unless the + # message method already returns a pointer (e.g. what()). + method_decls = class_decl.member_functions( + message_method, allow_empty=True + ) + if method_decls: + returns_pointer = declarations.is_pointer(method_decls[0].return_type) + elif message_method == "what": + # std::exception::what() is inherited and returns const char* + returns_pointer = True + else: + logger.error( + f"Could not find method {message_method} on exception {name}." + ) + raise RuntimeError( + f"Could not find method {message_method} on exception: {name}" + ) + + message_expr = f"e.{message_method}()" + if not returns_pointer: + message_expr += ".c_str()" + + self.exception_info.append( + { + "cpp_type": name, + "message_expr": message_expr, + "source_file": os.path.basename(class_decl.location.file_name), + } + ) diff --git a/cppwg/parsers/package_info_parser.py b/cppwg/parsers/package_info_parser.py index 9b67092..543cf47 100644 --- a/cppwg/parsers/package_info_parser.py +++ b/cppwg/parsers/package_info_parser.py @@ -78,6 +78,7 @@ def parse(self) -> PackageInfo: package_config: Dict[str, Any] = { "name": "cppwg_package", "common_include_file": True, + "exceptions": [], "exclude_default_args": False, "source_hpp_patterns": ["*.hpp"], } diff --git a/cppwg/writers/header_collection_writer.py b/cppwg/writers/header_collection_writer.py index 50efea5..865f02e 100644 --- a/cppwg/writers/header_collection_writer.py +++ b/cppwg/writers/header_collection_writer.py @@ -6,6 +6,7 @@ from cppwg.info.class_info import CppClassInfo from cppwg.info.free_function_info import CppFreeFunctionInfo from cppwg.info.package_info import PackageInfo +from cppwg.utils import utils from cppwg.utils.utils import write_file_if_changed @@ -118,6 +119,17 @@ def write(self) -> None: self.hpp_collection += f'#include "{filename}"\n' seen_files.add(filename) + # Include headers that declare the configured exception classes so + # they are parsed and can be introspected for the translator. + for exception_name in self.package_info.exception_names: + for filepath in self.package_info.source_hpp_files: + if utils.find_classes_in_source_file(filepath, exception_name): + filename = os.path.basename(filepath) + if filename not in seen_files: + self.hpp_collection += f'#include "{filename}"\n' + seen_files.add(filename) + break + # Add the template instantiations e.g. `template class Foo<2,2>;` # and typdefs e.g. `typedef Foo<2,2> Foo_2_2;` template_instantiations = "" diff --git a/cppwg/writers/module_writer.py b/cppwg/writers/module_writer.py index c817217..d0c7254 100644 --- a/cppwg/writers/module_writer.py +++ b/cppwg/writers/module_writer.py @@ -58,6 +58,36 @@ def __init__( for decl, cpp_name in zip(class_info.decls, class_info.cpp_names): self.classes[decl] = cpp_name + def generate_exception_translator(self) -> str: + """ + Generate a pybind11 exception translator for the package's exceptions. + + Produces a `py::register_exception_translator` call with a catch clause + for each configured exception class, mapping it to a Python + RuntimeError. Returns an empty string if no exceptions are configured. + + Returns + ------- + str + The exception translator code, indented for the module body. + """ + exception_info = self.module_info.package_info.exception_info + if not exception_info: + return "" + + code = " py::register_exception_translator([](std::exception_ptr p) {\n" + code += " try {\n" + code += " if (p) std::rethrow_exception(p);\n" + for exception in exception_info: + code += f" }} catch (const {exception['cpp_type']}& e) {{\n" + code += ( + " PyErr_SetString(PyExc_RuntimeError, " + f"{exception['message_expr']});\n" + ) + code += " }\n" + code += " });\n\n" + return code + def write_module_wrapper(self) -> None: """ Generate the contents of the main cpp file for the module. @@ -92,6 +122,17 @@ def write_module_wrapper(self) -> None: if self.module_info.package_info.common_include_file: cpp_string += f'#include "{CPPWG_HEADER_COLLECTION_FILENAME}"\n' + else: + # Include the headers that declare any exception classes so the + # generated exception translator can reference them. When a common + # include file is used these are already available via the header + # collection. + seen = set() + for exception in self.module_info.package_info.exception_info: + source_file = exception["source_file"] + if source_file not in seen: + seen.add(source_file) + cpp_string += f'#include "{source_file}"\n' # Add outputs from running custom generator code if self.module_info.custom_generator_instance: @@ -119,6 +160,10 @@ def write_module_wrapper(self) -> None: cpp_string += f"\nPYBIND11_MODULE({full_module_name}, m)\n" cpp_string += "{\n" + # Register a pybind11 exception translator for the configured exception + # classes so that C++ exceptions surface as Python exceptions + cpp_string += self.generate_exception_translator() + # Add free functions for free_function_info in self.module_info.free_function_collection: function_writer = CppFreeFunctionWrapperWriter( From 8cb2bd6ab661851a6b6c2f7c97d9715907918944 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 26 Jun 2026 02:35:13 +0100 Subject: [PATCH 03/10] #83 Add exception translation to the shapes example --- .../src/cpp/math_funcs/ThrowingFunction.hpp | 38 +++++++++++++++++++ .../shapes/src/py/tests/test_functions.py | 8 ++++ .../_pyshapes_geometry.main.cppwg.cpp | 8 ++++ .../_pyshapes_math_funcs.main.cppwg.cpp | 9 +++++ examples/shapes/wrapper/package_info.yaml | 8 ++++ .../_pyshapes_primitives.main.cppwg.cpp | 8 ++++ .../wrapper_header_collection.cppwg.hpp | 1 + 7 files changed, 80 insertions(+) create mode 100644 examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp diff --git a/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp b/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp new file mode 100644 index 0000000..804c9e2 --- /dev/null +++ b/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp @@ -0,0 +1,38 @@ +#ifndef _THROWING_FUNCTION_HPP +#define _THROWING_FUNCTION_HPP + +#include + +/** + * A simple exception type that does NOT derive from std::exception. + * + * pybind11 cannot translate this automatically, so without a registered + * exception translator it would terminate the Python interpreter. It is used + * to exercise the package's exception_translation_code option. + */ +class ShapeException +{ +public: + explicit ShapeException(const std::string& rMessage) : mMessage(rMessage) + { + } + + std::string GetMessage() const + { + return mMessage; + } + +private: + std::string mMessage; +}; + +/** + * Throw a ShapeException. Used to test that C++ exceptions surface as Python + * exceptions rather than crashing the interpreter. + */ +inline void throw_exception() +{ + throw ShapeException("C++ exception thrown"); +} + +#endif // _THROWING_FUNCTION_HPP diff --git a/examples/shapes/src/py/tests/test_functions.py b/examples/shapes/src/py/tests/test_functions.py index 47db051..b3a99ac 100644 --- a/examples/shapes/src/py/tests/test_functions.py +++ b/examples/shapes/src/py/tests/test_functions.py @@ -10,6 +10,14 @@ def testAdd(self): c = math_funcs.add(4, 5) self.assertTrue(c == a + b) + def testExceptionTranslation(self): + # The C++ function throws a ShapeException (which does not derive from + # std::exception). The registered exception translator should surface it + # as a Python RuntimeError rather than crashing the interpreter. + with self.assertRaises(RuntimeError) as context: + math_funcs.throw_exception() + self.assertEqual(str(context.exception), "C++ exception thrown") + if __name__ == "__main__": unittest.main() diff --git a/examples/shapes/wrapper/geometry/_pyshapes_geometry.main.cppwg.cpp b/examples/shapes/wrapper/geometry/_pyshapes_geometry.main.cppwg.cpp index 6d7d31b..c032fa0 100644 --- a/examples/shapes/wrapper/geometry/_pyshapes_geometry.main.cppwg.cpp +++ b/examples/shapes/wrapper/geometry/_pyshapes_geometry.main.cppwg.cpp @@ -10,6 +10,14 @@ namespace py = pybind11; PYBIND11_MODULE(_pyshapes_geometry, m) { + py::register_exception_translator([](std::exception_ptr p) { + try { + if (p) std::rethrow_exception(p); + } catch (const ShapeException& e) { + PyErr_SetString(PyExc_RuntimeError, e.GetMessage().c_str()); + } + }); + register_Point_2_class(m); register_Point_3_class(m); } diff --git a/examples/shapes/wrapper/math_funcs/_pyshapes_math_funcs.main.cppwg.cpp b/examples/shapes/wrapper/math_funcs/_pyshapes_math_funcs.main.cppwg.cpp index b9367a1..9cb5ccf 100644 --- a/examples/shapes/wrapper/math_funcs/_pyshapes_math_funcs.main.cppwg.cpp +++ b/examples/shapes/wrapper/math_funcs/_pyshapes_math_funcs.main.cppwg.cpp @@ -8,5 +8,14 @@ namespace py = pybind11; PYBIND11_MODULE(_pyshapes_math_funcs, m) { + py::register_exception_translator([](std::exception_ptr p) { + try { + if (p) std::rethrow_exception(p); + } catch (const ShapeException& e) { + PyErr_SetString(PyExc_RuntimeError, e.GetMessage().c_str()); + } + }); + m.def("add", &add, " ", py::arg("i") = 1.0, py::arg("j") = 2.0); + m.def("throw_exception", &throw_exception, " "); } diff --git a/examples/shapes/wrapper/package_info.yaml b/examples/shapes/wrapper/package_info.yaml index 9a6c7e1..f0c0d27 100644 --- a/examples/shapes/wrapper/package_info.yaml +++ b/examples/shapes/wrapper/package_info.yaml @@ -20,6 +20,14 @@ source_includes: # Exclude default arguments from wrapped methods. exclude_default_args: False +# C++ exception classes to translate into Python exceptions. cppwg generates a +# pybind11 exception translator for each, so C++ exceptions surface as Python +# exceptions instead of terminating the interpreter. message_method is the +# accessor used for the message text; it defaults to "what". +exceptions: + - name: ShapeException + message_method: GetMessage + # Signature/replacement settings for explicit template instantiations. template_substitutions: - signature: diff --git a/examples/shapes/wrapper/primitives/_pyshapes_primitives.main.cppwg.cpp b/examples/shapes/wrapper/primitives/_pyshapes_primitives.main.cppwg.cpp index f76918f..b83622b 100644 --- a/examples/shapes/wrapper/primitives/_pyshapes_primitives.main.cppwg.cpp +++ b/examples/shapes/wrapper/primitives/_pyshapes_primitives.main.cppwg.cpp @@ -12,6 +12,14 @@ namespace py = pybind11; PYBIND11_MODULE(_pyshapes_primitives, m) { + py::register_exception_translator([](std::exception_ptr p) { + try { + if (p) std::rethrow_exception(p); + } catch (const ShapeException& e) { + PyErr_SetString(PyExc_RuntimeError, e.GetMessage().c_str()); + } + }); + register_Shape_2_class(m); register_Shape_3_class(m); register_Rectangle_class(m); diff --git a/examples/shapes/wrapper/wrapper_header_collection.cppwg.hpp b/examples/shapes/wrapper/wrapper_header_collection.cppwg.hpp index b4694d9..50a1f26 100644 --- a/examples/shapes/wrapper/wrapper_header_collection.cppwg.hpp +++ b/examples/shapes/wrapper/wrapper_header_collection.cppwg.hpp @@ -11,6 +11,7 @@ #include "Shape.hpp" #include "SimpleMathFunctions.hpp" #include "Square.hpp" +#include "ThrowingFunction.hpp" #include "Triangle.hpp" // Instantiate Template Classes From 2133ad1e365e9a38790b376da6f455f8352ccbc3 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 26 Jun 2026 02:36:08 +0100 Subject: [PATCH 04/10] #83 Add exception translation to the cells example --- examples/cells/dynamic/config.yaml | 8 ++++ .../dynamic/wrappers/all/PetscUtils.cppwg.cpp | 3 ++ .../wrappers/all/_pycells_all.main.cppwg.cpp | 9 ++++ .../wrapper_header_collection.cppwg.hpp | 1 + examples/cells/src/cpp/utils/PetscUtils.cpp | 7 +++ examples/cells/src/cpp/utils/PetscUtils.hpp | 3 ++ .../src/cpp/utils/SimulationException.hpp | 45 +++++++++++++++++++ examples/cells/tests/test_cells.py | 14 ++++++ 8 files changed, 90 insertions(+) create mode 100644 examples/cells/src/cpp/utils/SimulationException.hpp diff --git a/examples/cells/dynamic/config.yaml b/examples/cells/dynamic/config.yaml index 044db50..2051658 100644 --- a/examples/cells/dynamic/config.yaml +++ b/examples/cells/dynamic/config.yaml @@ -11,6 +11,14 @@ source_includes: exclude_default_args: False +# C++ exception classes to translate into Python exceptions. cppwg generates a +# pybind11 exception translator for each, so C++ exceptions surface as Python +# exceptions instead of terminating the interpreter. message_method is the +# accessor used for the message text; it defaults to "what". +exceptions: + - name: SimulationException + message_method: GetMessage + template_substitutions: - signature: replacement: [[2], [3]] diff --git a/examples/cells/dynamic/wrappers/all/PetscUtils.cppwg.cpp b/examples/cells/dynamic/wrappers/all/PetscUtils.cppwg.cpp index c7de3f9..1c50220 100644 --- a/examples/cells/dynamic/wrappers/all/PetscUtils.cppwg.cpp +++ b/examples/cells/dynamic/wrappers/all/PetscUtils.cppwg.cpp @@ -31,5 +31,8 @@ void register_PetscUtils_class(py::module &m) .def_static("CreateVec", (::Vec(*)(int)) &PetscUtils::CreateVec, " ", py::arg("size"), py::return_value_policy::reference) + .def_static("ThrowException", + (void(*)()) &PetscUtils::ThrowException, + " ") ; } diff --git a/examples/cells/dynamic/wrappers/all/_pycells_all.main.cppwg.cpp b/examples/cells/dynamic/wrappers/all/_pycells_all.main.cppwg.cpp index c504ec2..636fdec 100644 --- a/examples/cells/dynamic/wrappers/all/_pycells_all.main.cppwg.cpp +++ b/examples/cells/dynamic/wrappers/all/_pycells_all.main.cppwg.cpp @@ -1,6 +1,7 @@ // This file is auto-generated by cppwg; manual changes will be overwritten. #include +#include "SimulationException.hpp" #include "Cell.cppwg.hpp" #include "Node_2.cppwg.hpp" #include "Node_3.cppwg.hpp" @@ -18,6 +19,14 @@ namespace py = pybind11; PYBIND11_MODULE(_pycells_all, m) { + py::register_exception_translator([](std::exception_ptr p) { + try { + if (p) std::rethrow_exception(p); + } catch (const SimulationException& e) { + PyErr_SetString(PyExc_RuntimeError, e.GetMessage().c_str()); + } + }); + register_Cell_class(m); register_Node_2_class(m); register_Node_3_class(m); diff --git a/examples/cells/dynamic/wrappers/wrapper_header_collection.cppwg.hpp b/examples/cells/dynamic/wrappers/wrapper_header_collection.cppwg.hpp index 1db0a7d..c0d2515 100644 --- a/examples/cells/dynamic/wrappers/wrapper_header_collection.cppwg.hpp +++ b/examples/cells/dynamic/wrappers/wrapper_header_collection.cppwg.hpp @@ -11,6 +11,7 @@ #include "PetscUtils.hpp" #include "PottsMesh.hpp" #include "Scene.hpp" +#include "SimulationException.hpp" // Instantiate Template Classes template class AbstractMesh<2, 2>; diff --git a/examples/cells/src/cpp/utils/PetscUtils.cpp b/examples/cells/src/cpp/utils/PetscUtils.cpp index eb16b08..ae2cd69 100644 --- a/examples/cells/src/cpp/utils/PetscUtils.cpp +++ b/examples/cells/src/cpp/utils/PetscUtils.cpp @@ -8,6 +8,8 @@ #include +#include "SimulationException.hpp" + void PetscUtils::Initialise() { if (!PetscUtils::IsInitialised()) @@ -64,3 +66,8 @@ Vec PetscUtils::CreateVec(int size) VecSetFromOptions(v); return v; } + +void PetscUtils::ThrowException() +{ + throw SimulationException("C++ exception thrown", __FILE__, __LINE__); +} diff --git a/examples/cells/src/cpp/utils/PetscUtils.hpp b/examples/cells/src/cpp/utils/PetscUtils.hpp index d6e89bb..e349953 100644 --- a/examples/cells/src/cpp/utils/PetscUtils.hpp +++ b/examples/cells/src/cpp/utils/PetscUtils.hpp @@ -19,6 +19,9 @@ class PetscUtils static int GetRank(); static Vec CreateVec(int size); + + /** Throw a SimulationException, to test exception translation. */ + static void ThrowException(); }; #endif // PETSCUTILS_HPP_ diff --git a/examples/cells/src/cpp/utils/SimulationException.hpp b/examples/cells/src/cpp/utils/SimulationException.hpp new file mode 100644 index 0000000..dd81e32 --- /dev/null +++ b/examples/cells/src/cpp/utils/SimulationException.hpp @@ -0,0 +1,45 @@ +#ifndef SIMULATIONEXCEPTION_HPP_ +#define SIMULATIONEXCEPTION_HPP_ + +#include +#include +#include + +/** + * A simple exception type that derives from std::runtime_error and exposes + * its message via GetMessage(). + * + * A pybind11 exception translator (see dynamic/config.yaml) can use + * GetMessage() to raise a helpful Python error instead of crashing. + */ +class SimulationException : public std::runtime_error +{ +public: + SimulationException(const std::string& rMessage, + const std::string& rFilename, + unsigned lineNumber) + : std::runtime_error(rMessage), mShortMessage(rMessage) + { + std::stringstream message; + message << rFilename << ":" << lineNumber << ": " << rMessage; + mMessage = message.str(); + } + + /** @return the full message, including file and line number. */ + std::string GetMessage() const + { + return mMessage; + } + + /** @return just the text of the message. */ + std::string GetShortMessage() const + { + return mShortMessage; + } + +private: + std::string mMessage; /**< Full message, including file and line number. */ + std::string mShortMessage; /**< Just the text of the message. */ +}; + +#endif // SIMULATIONEXCEPTION_HPP_ diff --git a/examples/cells/tests/test_cells.py b/examples/cells/tests/test_cells.py index 1e2e727..28cb47b 100644 --- a/examples/cells/tests/test_cells.py +++ b/examples/cells/tests/test_cells.py @@ -23,6 +23,20 @@ def testUblasCaster(self): node.Translate([1, 1]) self.assertEqual(list(node.GetLocation()), [1, 1]) + def testExceptionTranslation(self): + # ThrowException raises a C++ SimulationException (which, like Chaste's + # Exception, derives from std::runtime_error and exposes GetMessage()). + # The registered exception translator should surface it as a Python + # RuntimeError rather than crashing the interpreter. + with self.assertRaises(RuntimeError) as context: + PetscUtils.ThrowException() + message = str(context.exception) + self.assertIn("C++ exception thrown", message) + # The translator uses GetMessage(), which prepends the file and line, + # so the message differs from the default what() text. This confirms the + # custom translator (not just pybind11's default) handled the exception. + self.assertIn("PetscUtils.cpp:", message) + if __name__ == "__main__": unittest.main() From 5350ccde07d329da57472613cebaaf21397433ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 01:52:00 +0000 Subject: [PATCH 05/10] Fix free_function error message, exceptions type annotation, ThrowingFunction comment --- cppwg/info/free_function_info.py | 8 ++++---- cppwg/info/package_info.py | 4 ++-- examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cppwg/info/free_function_info.py b/cppwg/info/free_function_info.py index 9a5b21f..479ba19 100644 --- a/cppwg/info/free_function_info.py +++ b/cppwg/info/free_function_info.py @@ -29,12 +29,12 @@ def update_from_ns(self, source_ns: "namespace_t") -> None: # noqa: F821 if not ff_decls: # The function's header was not parsed. For explicitly listed free - # functions, the header is only included when source_file or - # source_file_path is set in the config. + # functions, the header is only included when source_file_path is + # set in the config. logger = logging.getLogger() logger.error( - f"Could not find free function {self.name}. Set source_file or " - "source_file_path in the config so that its header is included." + f"Could not find free function {self.name}. Set source_file_path " + "in the config so that its header is included." ) raise RuntimeError(f"Could not find free function: {self.name}") diff --git a/cppwg/info/package_info.py b/cppwg/info/package_info.py index 33c0a12..5b2e078 100644 --- a/cppwg/info/package_info.py +++ b/cppwg/info/package_info.py @@ -4,7 +4,7 @@ import logging import os from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union from pygccxml import declarations @@ -57,7 +57,7 @@ def __init__( super().__init__(name, package_config) self.common_include_file: bool = False - self.exceptions: List[str] = [] + self.exceptions: List[Union[str, Dict[str, str]]] = [] self.exclude_default_args: bool = False self.source_hpp_patterns: List[str] = ["*.hpp"] diff --git a/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp b/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp index 804c9e2..fc56681 100644 --- a/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp +++ b/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp @@ -8,7 +8,7 @@ * * pybind11 cannot translate this automatically, so without a registered * exception translator it would terminate the Python interpreter. It is used - * to exercise the package's exception_translation_code option. + * to exercise the package's exceptions option. */ class ShapeException { From d7781bf7977efd8fd54637e77a4555b9571ba8c1 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 26 Jun 2026 14:26:38 +0100 Subject: [PATCH 06/10] #83 Test that unconfigured exceptions surface as errors --- .../src/cpp/math_funcs/ThrowingFunction.hpp | 25 +++++++++++++++---- .../shapes/src/py/tests/test_functions.py | 8 ++++++ .../_pyshapes_math_funcs.main.cppwg.cpp | 1 + 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp b/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp index fc56681..52ac1ae 100644 --- a/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp +++ b/examples/shapes/src/cpp/math_funcs/ThrowingFunction.hpp @@ -4,11 +4,9 @@ #include /** - * A simple exception type that does NOT derive from std::exception. - * - * pybind11 cannot translate this automatically, so without a registered - * exception translator it would terminate the Python interpreter. It is used - * to exercise the package's exceptions option. + * A simple exception type that does not derive from std::exception. It is + * listed under the package's exceptions option, so cppwg generates a translator + * that surfaces its GetMessage() text as a Python error. */ class ShapeException { @@ -35,4 +33,21 @@ inline void throw_exception() throw ShapeException("C++ exception thrown"); } +/** + * An exception type that is not listed in the package's exceptions option, so + * no translator is generated for it. + */ +class UnwrappedException +{ +}; + +/** + * Throw an UnwrappedException. Used to test that exceptions without a + * configured translator are not silently ignored. + */ +inline void throw_unwrapped_exception() +{ + throw UnwrappedException(); +} + #endif // _THROWING_FUNCTION_HPP diff --git a/examples/shapes/src/py/tests/test_functions.py b/examples/shapes/src/py/tests/test_functions.py index b3a99ac..f146243 100644 --- a/examples/shapes/src/py/tests/test_functions.py +++ b/examples/shapes/src/py/tests/test_functions.py @@ -18,6 +18,14 @@ def testExceptionTranslation(self): math_funcs.throw_exception() self.assertEqual(str(context.exception), "C++ exception thrown") + def testUnwrappedException(self): + # throw_unwrapped_exception raises a C++ type that is not listed under + # `exceptions`, so cppwg generates no translator for it. pybind11 still + # surfaces it as a generic RuntimeError rather than letting it escape. + with self.assertRaises(RuntimeError) as context: + math_funcs.throw_unwrapped_exception() + self.assertEqual(str(context.exception), "Caught an unknown exception!") + if __name__ == "__main__": unittest.main() diff --git a/examples/shapes/wrapper/math_funcs/_pyshapes_math_funcs.main.cppwg.cpp b/examples/shapes/wrapper/math_funcs/_pyshapes_math_funcs.main.cppwg.cpp index 9cb5ccf..6d5c0df 100644 --- a/examples/shapes/wrapper/math_funcs/_pyshapes_math_funcs.main.cppwg.cpp +++ b/examples/shapes/wrapper/math_funcs/_pyshapes_math_funcs.main.cppwg.cpp @@ -18,4 +18,5 @@ PYBIND11_MODULE(_pyshapes_math_funcs, m) m.def("add", &add, " ", py::arg("i") = 1.0, py::arg("j") = 2.0); m.def("throw_exception", &throw_exception, " "); + m.def("throw_unwrapped_exception", &throw_unwrapped_exception, " "); } From e235b6eae7c826d4c95f0e4fbf0e772cfea13ed0 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 26 Jun 2026 14:30:07 +0100 Subject: [PATCH 07/10] #83 Add PETSc error handling example and move generic throw to Scene --- README.md | 5 +++ .../dynamic/wrappers/all/PetscUtils.cppwg.cpp | 4 +- .../dynamic/wrappers/all/Scene_2.cppwg.cpp | 3 ++ .../dynamic/wrappers/all/Scene_3.cppwg.cpp | 3 ++ examples/cells/src/cpp/utils/PetscUtils.cpp | 37 +++++++++++++++++-- examples/cells/src/cpp/utils/PetscUtils.hpp | 4 +- .../cells/src/cpp/visualization/Scene.cpp | 8 ++++ .../cells/src/cpp/visualization/Scene.hpp | 3 ++ examples/cells/tests/test_cells.py | 21 ++++++++--- 9 files changed, 74 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 78d8120..03feaee 100644 --- a/README.md +++ b/README.md @@ -170,5 +170,10 @@ r = Rectangle(4, 5) translator for each. By default the message is read with `what()`; set `message_method` on an entry to use a different accessor (e.g. `GetMessage`). See `examples/shapes/wrapper/package_info.yaml` for an example. +- This only applies to thrown C++ exceptions. Errors from C dependencies that + use return codes (e.g. a PETSc `PetscErrorCode`) are not caught unless the + wrapped C++ code converts them into a C++ exception first (for PETSc, via + `PetscCallThrow()` in a C++-exception build, or by checking the code and + throwing). See `PetscUtils::ThrowPetscError` in the cells example. - See the [pybind11 documentation](https://pybind11.readthedocs.io/) for help on pybind11 wrapper code. diff --git a/examples/cells/dynamic/wrappers/all/PetscUtils.cppwg.cpp b/examples/cells/dynamic/wrappers/all/PetscUtils.cppwg.cpp index 1c50220..5f0e9bf 100644 --- a/examples/cells/dynamic/wrappers/all/PetscUtils.cppwg.cpp +++ b/examples/cells/dynamic/wrappers/all/PetscUtils.cppwg.cpp @@ -31,8 +31,8 @@ void register_PetscUtils_class(py::module &m) .def_static("CreateVec", (::Vec(*)(int)) &PetscUtils::CreateVec, " ", py::arg("size"), py::return_value_policy::reference) - .def_static("ThrowException", - (void(*)()) &PetscUtils::ThrowException, + .def_static("ThrowPetscError", + (void(*)()) &PetscUtils::ThrowPetscError, " ") ; } diff --git a/examples/cells/dynamic/wrappers/all/Scene_2.cppwg.cpp b/examples/cells/dynamic/wrappers/all/Scene_2.cppwg.cpp index bccdd79..d03a68f 100644 --- a/examples/cells/dynamic/wrappers/all/Scene_2.cppwg.cpp +++ b/examples/cells/dynamic/wrappers/all/Scene_2.cppwg.cpp @@ -19,5 +19,8 @@ void register_Scene_2_class(py::module &m) .def("GetRenderer", (::vtkSmartPointer(Scene_2::*)()) &Scene_2::GetRenderer, " ") + .def_static("ThrowException", + (void(*)()) &Scene_2::ThrowException, + " ") ; } diff --git a/examples/cells/dynamic/wrappers/all/Scene_3.cppwg.cpp b/examples/cells/dynamic/wrappers/all/Scene_3.cppwg.cpp index 04b2d41..6a8c433 100644 --- a/examples/cells/dynamic/wrappers/all/Scene_3.cppwg.cpp +++ b/examples/cells/dynamic/wrappers/all/Scene_3.cppwg.cpp @@ -19,5 +19,8 @@ void register_Scene_3_class(py::module &m) .def("GetRenderer", (::vtkSmartPointer(Scene_3::*)()) &Scene_3::GetRenderer, " ") + .def_static("ThrowException", + (void(*)()) &Scene_3::ThrowException, + " ") ; } diff --git a/examples/cells/src/cpp/utils/PetscUtils.cpp b/examples/cells/src/cpp/utils/PetscUtils.cpp index ae2cd69..c7de6be 100644 --- a/examples/cells/src/cpp/utils/PetscUtils.cpp +++ b/examples/cells/src/cpp/utils/PetscUtils.cpp @@ -6,10 +6,10 @@ #include #include +#include +#include #include -#include "SimulationException.hpp" - void PetscUtils::Initialise() { if (!PetscUtils::IsInitialised()) @@ -67,7 +67,36 @@ Vec PetscUtils::CreateVec(int size) return v; } -void PetscUtils::ThrowException() +void PetscUtils::ThrowPetscError() { - throw SimulationException("C++ exception thrown", __FILE__, __LINE__); + if (!PetscUtils::IsInitialised()) + { + PetscUtils::Initialise(); + } + + Vec v; + VecCreate(PETSC_COMM_WORLD, &v); + + // PETSc reports errors with C return codes, not C++ exceptions, so the + // wrapper must turn an error code into a C++ exception for it to reach + // Python. +#ifdef PETSC_CLANGUAGE_CXX + // When PETSc is built with C++ as its base language, PetscCallThrow() + // throws a C++ exception on a non-zero error code. + PetscCallThrow(VecSetType(v, "no_such_vec_type")); +#else + // Otherwise (the common case, including the standard PETSc packages), + // PetscCallThrow() is unavailable, so we replicate it: detect the non-zero + // PetscErrorCode and throw a std::runtime_error. The return error handler + // keeps PETSc from printing a traceback or aborting. + PetscPushErrorHandler(PetscReturnErrorHandler, nullptr); + PetscErrorCode ierr = VecSetType(v, "no_such_vec_type"); + PetscPopErrorHandler(); + + if (ierr) + { + throw std::runtime_error( + "PETSc returned error code " + std::to_string(ierr)); + } +#endif } diff --git a/examples/cells/src/cpp/utils/PetscUtils.hpp b/examples/cells/src/cpp/utils/PetscUtils.hpp index e349953..31aa4e5 100644 --- a/examples/cells/src/cpp/utils/PetscUtils.hpp +++ b/examples/cells/src/cpp/utils/PetscUtils.hpp @@ -20,8 +20,8 @@ class PetscUtils static Vec CreateVec(int size); - /** Throw a SimulationException, to test exception translation. */ - static void ThrowException(); + /** Throw a C++ exception on a bad PETSc error code (see .cpp). */ + static void ThrowPetscError(); }; #endif // PETSCUTILS_HPP_ diff --git a/examples/cells/src/cpp/visualization/Scene.cpp b/examples/cells/src/cpp/visualization/Scene.cpp index 97a29aa..95bd3f7 100644 --- a/examples/cells/src/cpp/visualization/Scene.cpp +++ b/examples/cells/src/cpp/visualization/Scene.cpp @@ -5,6 +5,8 @@ #include #include +#include "SimulationException.hpp" + template Scene::Scene() : mpRenderer(vtkSmartPointer::New()), @@ -26,5 +28,11 @@ vtkSmartPointer Scene::GetRenderer() return mpRenderer; } +template +void Scene::ThrowException() +{ + throw SimulationException("C++ exception thrown", __FILE__, __LINE__); +} + template class Scene<2>; template class Scene<3>; diff --git a/examples/cells/src/cpp/visualization/Scene.hpp b/examples/cells/src/cpp/visualization/Scene.hpp index 40560b2..3fdf34f 100644 --- a/examples/cells/src/cpp/visualization/Scene.hpp +++ b/examples/cells/src/cpp/visualization/Scene.hpp @@ -22,6 +22,9 @@ class Scene virtual ~Scene(); vtkSmartPointer GetRenderer(); + + /** Throw a SimulationException, to test exception translation. */ + static void ThrowException(); }; #endif // SCENE_HPP_ diff --git a/examples/cells/tests/test_cells.py b/examples/cells/tests/test_cells.py index 28cb47b..e1c2821 100644 --- a/examples/cells/tests/test_cells.py +++ b/examples/cells/tests/test_cells.py @@ -24,18 +24,27 @@ def testUblasCaster(self): self.assertEqual(list(node.GetLocation()), [1, 1]) def testExceptionTranslation(self): - # ThrowException raises a C++ SimulationException (which, like Chaste's - # Exception, derives from std::runtime_error and exposes GetMessage()). - # The registered exception translator should surface it as a Python - # RuntimeError rather than crashing the interpreter. + # Scene.ThrowException raises a C++ SimulationException (which, like + # Chaste's Exception, derives from std::runtime_error and exposes + # GetMessage()). The registered exception translator should surface it as + # a Python RuntimeError rather than crashing the interpreter. with self.assertRaises(RuntimeError) as context: - PetscUtils.ThrowException() + Scene[2].ThrowException() message = str(context.exception) self.assertIn("C++ exception thrown", message) # The translator uses GetMessage(), which prepends the file and line, # so the message differs from the default what() text. This confirms the # custom translator (not just pybind11's default) handled the exception. - self.assertIn("PetscUtils.cpp:", message) + self.assertIn("Scene.cpp:", message) + + def testPetscError(self): + # PETSc is a C library that reports errors with return codes. The + # wrapper detects a bad PetscErrorCode and throws a std::runtime_error + # (as PetscCallThrow would), which pybind11 surfaces as a Python + # RuntimeError instead of letting the C error pass silently. + with self.assertRaises(RuntimeError) as context: + PetscUtils.ThrowPetscError() + self.assertIn("PETSc returned error code", str(context.exception)) if __name__ == "__main__": From 4f2f8b9660d1ef69278aeac4a1d8cd5768280bd2 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 26 Jun 2026 14:31:14 +0100 Subject: [PATCH 08/10] #83 Remove unused example code --- cppwg/info/package_info.py | 4 +--- examples/cells/src/cpp/utils/SimulationException.hpp | 9 +-------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/cppwg/info/package_info.py b/cppwg/info/package_info.py index 5b2e078..32ee931 100644 --- a/cppwg/info/package_info.py +++ b/cppwg/info/package_info.py @@ -228,9 +228,7 @@ def resolve_exceptions(self, source_ns: "namespace_t") -> None: # noqa: F821 # PyErr_SetString needs a const char*. Add .c_str() unless the # message method already returns a pointer (e.g. what()). - method_decls = class_decl.member_functions( - message_method, allow_empty=True - ) + method_decls = class_decl.member_functions(message_method, allow_empty=True) if method_decls: returns_pointer = declarations.is_pointer(method_decls[0].return_type) elif message_method == "what": diff --git a/examples/cells/src/cpp/utils/SimulationException.hpp b/examples/cells/src/cpp/utils/SimulationException.hpp index dd81e32..6c800b8 100644 --- a/examples/cells/src/cpp/utils/SimulationException.hpp +++ b/examples/cells/src/cpp/utils/SimulationException.hpp @@ -18,7 +18,7 @@ class SimulationException : public std::runtime_error SimulationException(const std::string& rMessage, const std::string& rFilename, unsigned lineNumber) - : std::runtime_error(rMessage), mShortMessage(rMessage) + : std::runtime_error(rMessage) { std::stringstream message; message << rFilename << ":" << lineNumber << ": " << rMessage; @@ -31,15 +31,8 @@ class SimulationException : public std::runtime_error return mMessage; } - /** @return just the text of the message. */ - std::string GetShortMessage() const - { - return mShortMessage; - } - private: std::string mMessage; /**< Full message, including file and line number. */ - std::string mShortMessage; /**< Just the text of the message. */ }; #endif // SIMULATIONEXCEPTION_HPP_ From 32d2032e94dfd706af7f383f357d6d2a565fc0b5 Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 26 Jun 2026 14:48:07 +0100 Subject: [PATCH 09/10] #83 Destroy PETSc Vec Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- examples/cells/src/cpp/utils/PetscUtils.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/cells/src/cpp/utils/PetscUtils.cpp b/examples/cells/src/cpp/utils/PetscUtils.cpp index c7de6be..0a83aa3 100644 --- a/examples/cells/src/cpp/utils/PetscUtils.cpp +++ b/examples/cells/src/cpp/utils/PetscUtils.cpp @@ -75,23 +75,29 @@ void PetscUtils::ThrowPetscError() } Vec v; - VecCreate(PETSC_COMM_WORLD, &v); + PetscErrorCode ierr = VecCreate(PETSC_COMM_WORLD, &v); + if (ierr) + { + throw std::runtime_error( + "VecCreate failed with PETSc error code " + std::to_string(ierr)); + } // PETSc reports errors with C return codes, not C++ exceptions, so the // wrapper must turn an error code into a C++ exception for it to reach // Python. #ifdef PETSC_CLANGUAGE_CXX - // When PETSc is built with C++ as its base language, PetscCallThrow() - // throws a C++ exception on a non-zero error code. - PetscCallThrow(VecSetType(v, "no_such_vec_type")); + ierr = VecSetType(v, "no_such_vec_type"); + VecDestroy(&v); + PetscCallThrow(ierr); #else // Otherwise (the common case, including the standard PETSc packages), // PetscCallThrow() is unavailable, so we replicate it: detect the non-zero // PetscErrorCode and throw a std::runtime_error. The return error handler // keeps PETSc from printing a traceback or aborting. PetscPushErrorHandler(PetscReturnErrorHandler, nullptr); - PetscErrorCode ierr = VecSetType(v, "no_such_vec_type"); + ierr = VecSetType(v, "no_such_vec_type"); PetscPopErrorHandler(); + VecDestroy(&v); if (ierr) { From 5db265d0695d8a05c192ad3bb4f949dd2d2e1b3c Mon Sep 17 00:00:00 2001 From: Kwabena Amponsah Date: Fri, 26 Jun 2026 15:10:43 +0100 Subject: [PATCH 10/10] #83 Validate exception class message_method --- cppwg/info/package_info.py | 24 ++++++++++-------------- cppwg/utils/utils.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/cppwg/info/package_info.py b/cppwg/info/package_info.py index 32ee931..96b2754 100644 --- a/cppwg/info/package_info.py +++ b/cppwg/info/package_info.py @@ -9,6 +9,7 @@ from pygccxml import declarations from cppwg.info.base_info import BaseInfo +from cppwg.utils import utils from cppwg.utils.constants import CPPWG_EXT @@ -226,24 +227,19 @@ def resolve_exceptions(self, source_ns: "namespace_t") -> None: # noqa: F821 class_decl = class_decls[0] - # PyErr_SetString needs a const char*. Add .c_str() unless the - # message method already returns a pointer (e.g. what()). - method_decls = class_decl.member_functions(message_method, allow_empty=True) - if method_decls: - returns_pointer = declarations.is_pointer(method_decls[0].return_type) - elif message_method == "what": - # std::exception::what() is inherited and returns const char* - returns_pointer = True - else: - logger.error( - f"Could not find method {message_method} on exception {name}." - ) + # Check the class (and its base classes, e.g. for an inherited + # what()) actually declares the configured message method. + method_decl = utils.find_member_function(class_decl, message_method) + if method_decl is None: + logger.error(f"Exception class {name} has no method {message_method}.") raise RuntimeError( - f"Could not find method {message_method} on exception: {name}" + f"Exception class {name} has no method: {message_method}" ) + # PyErr_SetString needs a const char*. Add .c_str() unless the + # message method already returns a pointer (e.g. what()). message_expr = f"e.{message_method}()" - if not returns_pointer: + if not declarations.is_pointer(method_decl.return_type): message_expr += ".c_str()" self.exception_info.append( diff --git a/cppwg/utils/utils.py b/cppwg/utils/utils.py index a5b0e13..156074a 100644 --- a/cppwg/utils/utils.py +++ b/cppwg/utils/utils.py @@ -4,7 +4,7 @@ import os import re from numbers import Number -from typing import Any, List, Tuple +from typing import Any, List, Optional, Tuple from cppwg.utils.constants import CPPWG_ALL_STRING, CPPWG_TRUE_STRINGS @@ -165,6 +165,41 @@ def find_classes_in_source_file( return classes +def find_member_function( + class_decl: "class_t", method_name: str # noqa: F821 +) -> Optional["member_function_t"]: # noqa: F821 + """ + Find a member function on a class or any of its base classes. + + Searches the class itself and then its base classes (e.g. to find a what() + method inherited from std::exception). + + Parameters + ---------- + class_decl : pygccxml.declarations.class_t + The class to search. + method_name : str + The name of the member function to find. + + Returns + ------- + Optional[pygccxml.declarations.member_function_t] + The member function declaration, or None if not found. + """ + method_decls = class_decl.member_functions(method_name, allow_empty=True) + if method_decls: + return method_decls[0] + + for hierarchy_info in class_decl.recursive_bases: + method_decls = hierarchy_info.related_class.member_functions( + method_name, allow_empty=True + ) + if method_decls: + return method_decls[0] + + return None + + def read_source_file( source_file_path: str, strip_comments: bool = True,