@@ -18,9 +18,11 @@ mod inner {
1818
1919 use pyo3:: prelude:: * ;
2020
21+ #[ cfg( any( not( all( Py_GIL_DISABLED , Py_3_14 ) ) , all( feature = "macros" , Py_3_8 ) ) ) ]
2122 use pyo3:: sync:: MutexExt ;
2223 use pyo3:: types:: { IntoPyDict , PyList } ;
2324
25+ #[ cfg( any( not( all( Py_GIL_DISABLED , Py_3_14 ) ) , all( feature = "macros" , Py_3_8 ) ) ) ]
2426 use std:: sync:: { Mutex , PoisonError } ;
2527
2628 use uuid:: Uuid ;
@@ -118,49 +120,88 @@ mod inner {
118120 }
119121
120122 // sys.unraisablehook not available until Python 3.8
121- #[ cfg( all( feature = "macros" , Py_3_8 , not( Py_GIL_DISABLED ) ) ) ]
122- #[ pyclass( crate = "pyo3" ) ]
123- pub struct UnraisableCapture {
124- pub capture : Option < ( PyErr , Py < PyAny > ) > ,
125- old_hook : Option < Py < PyAny > > ,
123+ #[ cfg( all( feature = "macros" , Py_3_8 ) ) ]
124+ pub struct UnraisableCapture < ' py > {
125+ hook : Bound < ' py , UnraisableCaptureHook > ,
126126 }
127127
128- #[ cfg( all( feature = "macros" , Py_3_8 , not( Py_GIL_DISABLED ) ) ) ]
128+ #[ cfg( all( feature = "macros" , Py_3_8 ) ) ]
129+ impl < ' py > UnraisableCapture < ' py > {
130+ /// Runs the closure `f` with a custom sys.unraisablehook installed.
131+ ///
132+ /// `f`
133+ pub fn enter < R > ( py : Python < ' py > , f : impl FnOnce ( & Self ) -> R ) -> R {
134+ // unraisablehook is a global, so only one thread can be using this struct at a time.
135+ static UNRAISABLE_HOOK_MUTEX : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
136+
137+ // NB this is best-effort, other tests could always modify sys.unraisablehook directly.
138+ let mutex_guard = UNRAISABLE_HOOK_MUTEX
139+ . lock_py_attached ( py)
140+ . unwrap_or_else ( PoisonError :: into_inner) ;
141+
142+ let guard = Self {
143+ hook : UnraisableCaptureHook :: install ( py) ,
144+ } ;
145+
146+ let result = f ( & guard) ;
147+
148+ drop ( guard) ;
149+ drop ( mutex_guard) ;
150+
151+ result
152+ }
153+
154+ /// Takes the captured unraisable error, if any.
155+ pub fn take_capture ( & self ) -> Option < ( PyErr , Bound < ' py , PyAny > ) > {
156+ let mut guard = self . hook . get ( ) . capture . lock ( ) . unwrap ( ) ;
157+ guard. take ( ) . map ( |( e, o) | ( e, o. into_bound ( self . hook . py ( ) ) ) )
158+ }
159+ }
160+
161+ #[ cfg( all( feature = "macros" , Py_3_8 ) ) ]
162+ impl Drop for UnraisableCapture < ' _ > {
163+ fn drop ( & mut self ) {
164+ let py = self . hook . py ( ) ;
165+ self . hook . get ( ) . uninstall ( py) ;
166+ }
167+ }
168+
169+ #[ cfg( all( feature = "macros" , Py_3_8 ) ) ]
170+ #[ pyclass( crate = "pyo3" , frozen) ]
171+ struct UnraisableCaptureHook {
172+ pub capture : Mutex < Option < ( PyErr , Py < PyAny > ) > > ,
173+ old_hook : Py < PyAny > ,
174+ }
175+
176+ #[ cfg( all( feature = "macros" , Py_3_8 ) ) ]
129177 #[ pymethods( crate = "pyo3" ) ]
130- impl UnraisableCapture {
131- pub fn hook ( & mut self , unraisable : Bound < ' _ , PyAny > ) {
178+ impl UnraisableCaptureHook {
179+ pub fn hook ( & self , unraisable : Bound < ' _ , PyAny > ) {
132180 let err = PyErr :: from_value ( unraisable. getattr ( "exc_value" ) . unwrap ( ) ) ;
133181 let instance = unraisable. getattr ( "object" ) . unwrap ( ) ;
134- self . capture = Some ( ( err, instance. into ( ) ) ) ;
182+ self . capture . lock ( ) . unwrap ( ) . replace ( ( err, instance. into ( ) ) ) ;
135183 }
136184 }
137185
138- #[ cfg( all( feature = "macros" , Py_3_8 , not ( Py_GIL_DISABLED ) ) ) ]
139- impl UnraisableCapture {
140- pub fn install ( py : Python < ' _ > ) -> Py < Self > {
186+ #[ cfg( all( feature = "macros" , Py_3_8 ) ) ]
187+ impl UnraisableCaptureHook {
188+ fn install ( py : Python < ' _ > ) -> Bound < ' _ , Self > {
141189 let sys = py. import ( "sys" ) . unwrap ( ) ;
190+
142191 let old_hook = sys. getattr ( "unraisablehook" ) . unwrap ( ) . into ( ) ;
192+ let capture = Mutex :: new ( None ) ;
143193
144- let capture = Py :: new (
145- py,
146- UnraisableCapture {
147- capture : None ,
148- old_hook : Some ( old_hook) ,
149- } ,
150- )
151- . unwrap ( ) ;
194+ let capture = Bound :: new ( py, UnraisableCaptureHook { capture, old_hook } ) . unwrap ( ) ;
152195
153- sys. setattr ( "unraisablehook" , capture. getattr ( py , "hook" ) . unwrap ( ) )
196+ sys. setattr ( "unraisablehook" , capture. getattr ( "hook" ) . unwrap ( ) )
154197 . unwrap ( ) ;
155198
156199 capture
157200 }
158201
159- pub fn uninstall ( & mut self , py : Python < ' _ > ) {
160- let old_hook = self . old_hook . take ( ) . unwrap ( ) ;
161-
202+ fn uninstall ( & self , py : Python < ' _ > ) {
162203 let sys = py. import ( "sys" ) . unwrap ( ) ;
163- sys. setattr ( "unraisablehook" , old_hook) . unwrap ( ) ;
204+ sys. setattr ( "unraisablehook" , & self . old_hook ) . unwrap ( ) ;
164205 }
165206 }
166207
@@ -170,6 +211,7 @@ mod inner {
170211
171212 /// catch_warnings is not thread-safe, so only one thread can be using this struct at
172213 /// a time.
214+ #[ cfg( not( all( Py_GIL_DISABLED , Py_3_14 ) ) ) ] // Python 3.14t has thread-safe catch_warnings
173215 static CATCH_WARNINGS_MUTEX : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
174216
175217 impl < ' py > CatchWarnings < ' py > {
@@ -178,6 +220,7 @@ mod inner {
178220 f : impl FnOnce ( & Bound < ' py , PyList > ) -> PyResult < R > ,
179221 ) -> PyResult < R > {
180222 // NB this is best-effort, other tests could always call the warnings API directly.
223+ #[ cfg( not( all( Py_GIL_DISABLED , Py_3_14 ) ) ) ]
181224 let _mutex_guard = CATCH_WARNINGS_MUTEX
182225 . lock_py_attached ( py)
183226 . unwrap_or_else ( PoisonError :: into_inner) ;
0 commit comments