feat(pybind): register exception translator for cytnx::error -> CytnxError#989
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a custom Python exception class CytnxError (subclassing RuntimeError) and registers a module-global exception translator in pybind/cytnx.cpp to map C++ cytnx::error exceptions to Python. It also adds comprehensive unit tests in pytests/binding_exception_test.py to verify this behavior. The reviewer suggested simplifying the exception registration in pybind/cytnx.cpp by using pybind11's built-in py::register_exception helper instead of manually defining a static exception object and translator.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| static py::exception<cytnx::error> cytnx_error_exc(m, "CytnxError", PyExc_RuntimeError); | ||
| cytnx_error_exc.attr("__doc__") = | ||
| "Raised when a cytnx C++ operation fails (via cytnx_error_msg)."; | ||
| py::register_exception_translator([](std::exception_ptr p) { | ||
| try { | ||
| if (p) std::rethrow_exception(p); | ||
| } catch (const cytnx::error &e) { | ||
| py::set_error(cytnx_error_exc, e.what()); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Instead of manually registering a custom exception translator and declaring a static py::exception object, you can use pybind11's built-in py::register_exception helper. It automatically creates the custom Python exception class, registers the exception translator to map the C++ exception to the Python exception using e.what(), and returns a reference to the registered exception object so you can set its docstring. This simplifies the code and avoids manual exception translation boilerplate.
| static py::exception<cytnx::error> cytnx_error_exc(m, "CytnxError", PyExc_RuntimeError); | |
| cytnx_error_exc.attr("__doc__") = | |
| "Raised when a cytnx C++ operation fails (via cytnx_error_msg)."; | |
| py::register_exception_translator([](std::exception_ptr p) { | |
| try { | |
| if (p) std::rethrow_exception(p); | |
| } catch (const cytnx::error &e) { | |
| py::set_error(cytnx_error_exc, e.what()); | |
| } | |
| }); | |
| py::register_exception<cytnx::error>(m, "CytnxError", PyExc_RuntimeError) | |
| .attr("__doc__") = "Raised when a cytnx C++ operation fails (via cytnx_error_msg)."; |
There was a problem hiding this comment.
Done in e9d0687 — switched to py::register_exception<cytnx::error>(m, "CytnxError", PyExc_RuntimeError) and set the docstring on the returned object, dropping the manual py::exception/register_exception_translator/set_error boilerplate. Behavior is unchanged: the 4 tests (type + subclass, message body, submodule InvM path, and the negative fall-through pinning that a pybind TypeError/RuntimeError stays un-reclassified) all pass against a freshly built module.
48a73a0 to
f75fbc0
Compare
Code ReviewMaps the C++ The new code — correct and idiomatic
Issues & Risks1. Stale copy of the #983 base — rebase before merge (medium, process).
A 3-way merge into the base will likely keep the base's newer lines, so it may not regress in practice, but the diverged snapshot makes the review scope confusing and is fragile. Recommendation (matching the PR's own stacking note): merge #983 first, then rebase this onto 2. Comment slightly misstates the mechanism (nit).
Exception translators are consulted at exception-raise time, not binding time — registration order relative to 3. Scope of 4. Test brittleness (nit). SummaryThe actual contribution — the translator and its tests — is correct, idiomatic for pybind11 3.x, and well-tested (including the important negative fall-through). The one real action item is rebasing onto the merged #983 so the diff stops carrying a stale copy of that PR's files; the rest are nits. Posted by Claude Code on behalf of @pcchen |
Follow-up: #983 is now approvedHeads-up on stacking. Since this branch was cut, base PR #983 has advanced further (it dropped the None of that needs hand-fixing here: once #983 merges, rebase this onto The new code itself is unaffected and still good: #983's changes don't touch Posted by Claude Code on behalf of @pcchen |
79e8a7d to
8d15ec5
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #989 +/- ##
=======================================
Coverage 30.82% 30.83%
=======================================
Files 229 229
Lines 33348 33349 +1
Branches 14069 14070 +1
=======================================
+ Hits 10281 10283 +2
Misses 15795 15795
+ Partials 7272 7271 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 2 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
…Error Every error raised via cytnx_error_msg previously surfaced in Python as a bare RuntimeError (pybind11's default translation for std::logic_error subclasses), indistinguishable from unrelated failures like pybind11 cast errors or genuine Python RuntimeErrors. Register a module-global exception translator in pybind/cytnx.cpp, before any submodule bindings, that catches cytnx::error and raises cytnx.CytnxError instead. CytnxError is declared with PyExc_RuntimeError as its base (py::exception<cytnx::error> cytnx_error_exc(m, "CytnxError", PyExc_RuntimeError)), so it IS-A RuntimeError: existing `except RuntimeError` call sites keep working unchanged, while new code can `except cytnx.CytnxError` to specifically catch cytnx-originated errors. pybind11 3.0.1 (the fetched version) deprecated calling a py::exception object directly to raise it (PR #4772); py::set_error(exc, message) is the current API and is used here, matching the pattern used internally by pybind11's own py::register_exception<T> helper (static storage + register_exception_translator + set_error). Because register_exception_translator installs a process-wide (module- global) translator, it applies to every submodule binding (Tensor, UniTensor, linalg, ...) without per-submodule wiring; a linalg.InvM error is verified to translate too, not just top-level Tensor methods. Stacks on fix/error-macro-rewrite (#983), which introduces cytnx::error as a std::logic_error subclass -- this branch cannot merge before it. Co-Authored-By: Claude <noreply@anthropic.com>
…review Address the @gemini-code-assist and @pcchen reviews on #989: - Replace the hand-written static py::exception + register_exception_translator + py::set_error boilerplate with pybind11's py::register_exception<> helper (Gemini). It creates the CytnxError type, registers a translator that catches only cytnx::error and formats via e.what(), and returns the exception object for the docstring -- identical behavior, less code. - Fix the misleading comment: exception translators are consulted when an exception propagates, not at binding time, so registration order relative to the submodule bindings is irrelevant (pcchen). Note in the comment/docstring that only cytnx_error_msg-originated errors are reclassified. - test_message_content: match a stable substring of the error body ("number of elements") instead of "reshape", which only appears in the __PRETTY_FUNCTION__ section and is formatting-coupled (pcchen). Verified: pybind/cytnx.cpp compiles clean with the project flags, and all 4 tests in pytests/binding_exception_test.py pass against a freshly built module (type + subclass, message body, submodule InvM path, and the negative fall-through pinning that a pybind TypeError/RuntimeError stays un-reclassified). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8d15ec5 to
e9d0687
Compare
|
Thanks @pcchen — all points addressed. 1. Rebase (main action item). Already resolved: the branch is now based on post-#983 2. Comment misstates the mechanism. Fixed in e9d0687. The comment now says translators are module-global and consulted when an exception propagates (not at binding time), so registration order relative to the submodule bindings is irrelevant. 3. Scope of 4. Test brittleness. Fixed. Separately I took the Gemini suggestion and collapsed the manual Verification: |
pcchen
left a comment
There was a problem hiding this comment.
Approving. The translator now uses the canonical py::register_exception<cytnx::error>(m, "CytnxError", PyExc_RuntimeError) one-liner — cleaner than the manual py::exception + register_exception_translator, and the updated comment accurately describes translator semantics (module-global, consulted when an exception propagates, order-independent relative to bindings), resolving my earlier note. CytnxError subclasses RuntimeError so existing except RuntimeError code keeps working, non-cytnx exceptions still fall through to default translation, and the tests pin all of it (type/subclass, message content, submodule InvM path, and the negative not-reclassified case). The message-content test now matches a stable error-body substring rather than the function name — nice.
Verified the branch is exactly master + the two translator commits (touching only pybind/cytnx.cpp + pytests/binding_exception_test.py), and Linux BuildAndTest (compile + pytest) is green. codecov/patch is advisory. Good to merge once the remaining macOS/wheel jobs finish.
Posted by Claude Code on behalf of @pcchen
Problem
Every cytnx C++ error surfaced in Python as a bare
RuntimeError(pybind11's fallback forstd::logic_error), indistinguishable from any other failure — user code could not catch cytnx errors specifically.Fix
Registers a pybind11 exception translator in
PYBIND11_MODULEmappingcytnx::error(thrown bycytnx_error_msgsince the error-header rewrite) to a newcytnx.CytnxError. It subclassesRuntimeError, so all existingexcept RuntimeError:code keeps working. Uses the non-deprecatedpy::set_errorAPI with the static-lifetime pattern pybind11 3.x itself uses internally; non-cytnx exceptions fall through to default translation untouched (pinned by test). The exception carries a docstring forhelp().Testing
pytests/binding_exception_test.py(4 tests): type + subclass relationship, message content, a submodule error path (linalg.InvM) proving module-global coverage, and negative fall-through pinning (a pybindTypeErrorand a plain pybind-castRuntimeErrorboth stay un-reclassified).Stacking
Base branch is
fix/error-macro-rewrite(PR #983) —cytnx::erroronly exists there. Merge #983 first, then retarget this PR tomaster.🤖 Generated with Claude Code