@@ -471,13 +471,24 @@ inline const char *obj_class_name(PyObject *obj) {
471
471
472
472
std::string error_string ();
473
473
474
+ // The code in this struct is very unusual, to minimize the chances of
475
+ // masking bugs (elsewhere) by errors during the error handling (here).
476
+ // This is meant to be a lifeline for troubleshooting long-running processes
477
+ // that crash under conditions that are virtually impossible to reproduce.
478
+ // Low-level implementation alternatives are preferred to higher-level ones
479
+ // that might raise cascading exceptions. Last-ditch-kind-of attempts are made
480
+ // to report as much of the original error as possible, even if there are
481
+ // secondary issues obtaining some of the details.
474
482
struct error_fetch_and_normalize {
475
- // Immediate normalization is long-established behavior (starting with
476
- // https://github.com/pybind/pybind11/commit/135ba8deafb8bf64a15b24d1513899eb600e2011
477
- // from Sep 2016) and safest. Normalization could be deferred, but this could mask
478
- // errors elsewhere, the performance gain is very minor in typical situations
479
- // (usually the dominant bottleneck is EH unwinding), and the implementation here
480
- // would be more complex.
483
+ // This comment only applies to Python <= 3.11:
484
+ // Immediate normalization is long-established behavior (starting with
485
+ // https://github.com/pybind/pybind11/commit/135ba8deafb8bf64a15b24d1513899eb600e2011
486
+ // from Sep 2016) and safest. Normalization could be deferred, but this could mask
487
+ // errors elsewhere, the performance gain is very minor in typical situations
488
+ // (usually the dominant bottleneck is EH unwinding), and the implementation here
489
+ // would be more complex.
490
+ // Starting with Python 3.12, PyErr_Fetch() normalizes exceptions immediately.
491
+ // Any errors during normalization are tracked under __notes__.
481
492
explicit error_fetch_and_normalize (const char *called) {
482
493
PyErr_Fetch (&m_type.ptr (), &m_value.ptr (), &m_trace.ptr ());
483
494
if (!m_type) {
@@ -492,6 +503,14 @@ struct error_fetch_and_normalize {
492
503
" of the original active exception type." );
493
504
}
494
505
m_lazy_error_string = exc_type_name_orig;
506
+ #if PY_VERSION_HEX >= 0x030C0000
507
+ // The presence of __notes__ is likely due to exception normalization
508
+ // errors, although that is not necessarily true, therefore insert a
509
+ // hint only:
510
+ if (PyObject_HasAttrString (m_value.ptr (), " __notes__" )) {
511
+ m_lazy_error_string += " [WITH __notes__]" ;
512
+ }
513
+ #else
495
514
// PyErr_NormalizeException() may change the exception type if there are cascading
496
515
// failures. This can potentially be extremely confusing.
497
516
PyErr_NormalizeException (&m_type.ptr (), &m_value.ptr (), &m_trace.ptr ());
@@ -506,12 +525,12 @@ struct error_fetch_and_normalize {
506
525
+ " failed to obtain the name "
507
526
" of the normalized active exception type." );
508
527
}
509
- #if defined(PYPY_VERSION_NUM) && PYPY_VERSION_NUM < 0x07030a00
528
+ # if defined(PYPY_VERSION_NUM) && PYPY_VERSION_NUM < 0x07030a00
510
529
// This behavior runs the risk of masking errors in the error handling, but avoids a
511
530
// conflict with PyPy, which relies on the normalization here to change OSError to
512
531
// FileNotFoundError (https://github.com/pybind/pybind11/issues/4075).
513
532
m_lazy_error_string = exc_type_name_norm;
514
- #else
533
+ # else
515
534
if (exc_type_name_norm != m_lazy_error_string) {
516
535
std::string msg = std::string (called)
517
536
+ " : MISMATCH of original and normalized "
@@ -523,6 +542,7 @@ struct error_fetch_and_normalize {
523
542
msg += " : " + format_value_and_trace ();
524
543
pybind11_fail (msg);
525
544
}
545
+ # endif
526
546
#endif
527
547
}
528
548
@@ -558,6 +578,40 @@ struct error_fetch_and_normalize {
558
578
}
559
579
}
560
580
}
581
+ #if PY_VERSION_HEX >= 0x030B0000
582
+ auto notes
583
+ = reinterpret_steal<object>(PyObject_GetAttrString (m_value.ptr (), " __notes__" ));
584
+ if (!notes) {
585
+ PyErr_Clear (); // No notes is good news.
586
+ } else {
587
+ auto len_notes = PyList_Size (notes.ptr ());
588
+ if (len_notes < 0 ) {
589
+ result += " \n FAILURE obtaining len(__notes__): " + detail::error_string ();
590
+ } else {
591
+ result += " \n __notes__ (len=" + std::to_string (len_notes) + " ):" ;
592
+ for (ssize_t i = 0 ; i < len_notes; i++) {
593
+ PyObject *note = PyList_GET_ITEM (notes.ptr (), i);
594
+ auto note_bytes = reinterpret_steal<object>(
595
+ PyUnicode_AsEncodedString (note, " utf-8" , " backslashreplace" ));
596
+ if (!note_bytes) {
597
+ result += " \n FAILURE obtaining __notes__[" + std::to_string (i)
598
+ + " ]: " + detail::error_string ();
599
+ } else {
600
+ char *buffer = nullptr ;
601
+ Py_ssize_t length = 0 ;
602
+ if (PyBytes_AsStringAndSize (note_bytes.ptr (), &buffer, &length)
603
+ == -1 ) {
604
+ result += " \n FAILURE formatting __notes__[" + std::to_string (i)
605
+ + " ]: " + detail::error_string ();
606
+ } else {
607
+ result += ' \n ' ;
608
+ result += std::string (buffer, static_cast <std::size_t >(length));
609
+ }
610
+ }
611
+ }
612
+ }
613
+ }
614
+ #endif
561
615
} else {
562
616
result = " <MESSAGE UNAVAILABLE>" ;
563
617
}
0 commit comments