99//! Exceptions that C code cannot propagate are reported through the embedded
1010//! interpreter's `sys.unraisablehook`. When this thread is running RustPython,
1111//! that hook is handed to RustPython's `sys.unraisablehook`.
12+ //!
13+ //! Warnings go the same way. The embedded `warnings.showwarning` is handed to
14+ //! this interpreter's `warnings.showwarning` while a RustPython thread is
15+ //! running, so a recorder installed here observes them. The embedded filters
16+ //! still decide whether `showwarning` is called. If the function here raises,
17+ //! that exception propagates to the embedded caller. With no RustPython thread,
18+ //! or if delivery is already in progress, the embedded function runs.
1219
20+ use core:: cell:: Cell ;
1321use core:: ffi:: CStr ;
14- use std:: sync:: Once ;
22+ use std:: sync:: { Once , OnceLock } ;
1523
1624use pyo3:: exceptions:: PyRuntimeError ;
1725use pyo3:: prelude:: * ;
18- use pyo3:: types:: { PyAnyMethods , PyCFunction , PyDict , PyTuple } ;
26+ use pyo3:: types:: { PyAnyMethods , PyCFunction , PyDict , PyTuple , PyType as CPythonType } ;
1927use rustpython_vm:: {
2028 AsObject , PyObjectRef , PyPayload , PyResult , VirtualMachine ,
21- builtins:: { PyTuple as RustPyTuple , PyType } ,
29+ builtins:: { PyStr , PyTuple as RustPyTuple , PyType } ,
30+ function:: { FuncArgs , KwArgs } ,
2231 types:: { AtomicPyTypeFlags , PyTypeFlags , PyTypeSlots } ,
2332} ;
2433
2534use crate :: _pyo3:: module_state;
2635use crate :: convert:: to_rustpython;
27- use crate :: errors:: cpython_exc_to_rustpython;
36+ use crate :: errors:: { cpython_exc_to_rustpython, matching_builtin_type , rustpython_exc_to_cpython } ;
2837use crate :: gil:: run_rustpython;
2938
3039/// Initialize CPython on first use and install the unraisable forwarder.
@@ -37,6 +46,7 @@ pub(crate) fn ensure_cpython_initialized() {
3746 static INIT : Once = Once :: new ( ) ;
3847 INIT . call_once ( initialize_cpython) ;
3948 install_unraisable_hook ( ) ;
49+ install_warning_hook ( ) ;
4050}
4151
4252fn initialize_cpython ( ) {
@@ -91,6 +101,138 @@ fn set_unraisable_hook(py: Python<'_>) -> pyo3::PyResult<()> {
91101 py. import ( "sys" ) ?. setattr ( "unraisablehook" , hook)
92102}
93103
104+ static ORIGINAL_SHOWWARNING : OnceLock < Py < PyAny > > = OnceLock :: new ( ) ;
105+
106+ std:: thread_local! {
107+ static IN_SHOWWARNING : Cell <bool > = const { Cell :: new( false ) } ;
108+ }
109+
110+ /// Set while a warning is being delivered, so a nested report uses the
111+ /// embedded function instead of calling back into the same delivery.
112+ struct ShowwarningGuard ;
113+
114+ impl ShowwarningGuard {
115+ fn enter ( ) -> Option < Self > {
116+ IN_SHOWWARNING . with ( |flag| {
117+ if flag. get ( ) {
118+ None
119+ } else {
120+ flag. set ( true ) ;
121+ Some ( Self )
122+ }
123+ } )
124+ }
125+ }
126+
127+ impl Drop for ShowwarningGuard {
128+ fn drop ( & mut self ) {
129+ IN_SHOWWARNING . with ( |flag| flag. set ( false ) ) ;
130+ }
131+ }
132+
133+ fn install_warning_hook ( ) {
134+ static HOOK : Once = Once :: new ( ) ;
135+ HOOK . call_once ( || {
136+ Python :: attach ( |py| {
137+ if let Err ( err) = set_warning_hook ( py) {
138+ panic ! ( "failed to install the embedded warning hook: {err}" ) ;
139+ }
140+ } ) ;
141+ } ) ;
142+ }
143+
144+ fn set_warning_hook ( py : Python < ' _ > ) -> pyo3:: PyResult < ( ) > {
145+ let warnings = py. import ( "warnings" ) ?;
146+ let original = warnings. getattr ( "showwarning" ) ?;
147+ let _ = ORIGINAL_SHOWWARNING . set ( original. unbind ( ) ) ;
148+ let hook = PyCFunction :: new_closure ( py, Some ( c"showwarning" ) , None , showwarning_hook) ?;
149+ warnings. setattr ( "showwarning" , hook)
150+ }
151+
152+ fn showwarning_hook (
153+ args : & Bound < ' _ , PyTuple > ,
154+ kwargs : Option < & Bound < ' _ , PyDict > > ,
155+ ) -> pyo3:: PyResult < ( ) > {
156+ let Some ( _guard) = ShowwarningGuard :: enter ( ) else {
157+ return call_original_showwarning ( args, kwargs) ;
158+ } ;
159+ let py = args. py ( ) ;
160+ let forwarded = rustpython_vm:: vm:: thread:: try_with_current_vm ( |vm| {
161+ vm. attach_for_callback ( || forward_showwarning ( py, args, kwargs, vm) )
162+ } ) ;
163+ match forwarded {
164+ Some ( result) => result,
165+ None => call_original_showwarning ( args, kwargs) ,
166+ }
167+ }
168+
169+ fn forward_showwarning (
170+ py : Python < ' _ > ,
171+ args : & Bound < ' _ , PyTuple > ,
172+ kwargs : Option < & Bound < ' _ , PyDict > > ,
173+ vm : & VirtualMachine ,
174+ ) -> pyo3:: PyResult < ( ) > {
175+ let call_args =
176+ showwarning_args ( args, kwargs, vm) . map_err ( |err| rustpython_exc_to_cpython ( py, err, vm) ) ?;
177+ run_rustpython ( py, || {
178+ let warnings = vm. import ( "warnings" , 0 ) ?;
179+ let show = warnings. get_attr ( "showwarning" , vm) ?;
180+ show. call ( call_args, vm) ?;
181+ Ok ( ( ) )
182+ } )
183+ . map_err ( |err| rustpython_exc_to_cpython ( py, err, vm) )
184+ }
185+
186+ fn showwarning_args (
187+ args : & Bound < ' _ , PyTuple > ,
188+ kwargs : Option < & Bound < ' _ , PyDict > > ,
189+ vm : & VirtualMachine ,
190+ ) -> PyResult < FuncArgs > {
191+ let positional = args
192+ . iter ( )
193+ . enumerate ( )
194+ . map ( |( index, arg) | showwarning_value ( index == 1 , & arg, vm) )
195+ . collect :: < PyResult < Vec < _ > > > ( ) ?;
196+ let kwargs = kwargs
197+ . into_iter ( )
198+ . flatten ( )
199+ . map ( |( key, value) | {
200+ let key = to_rustpython ( & key, vm) ?;
201+ let key = key
202+ . downcast_ref :: < PyStr > ( )
203+ . ok_or_else ( || vm. new_type_error ( "keywords must be strings" ) ) ?;
204+ let is_category = key. to_str ( ) == Some ( "category" ) ;
205+ let value = showwarning_value ( is_category, & value, vm) ?;
206+ Ok ( ( key. as_wtf8 ( ) . to_owned ( ) , value) )
207+ } )
208+ . collect :: < PyResult < KwArgs > > ( ) ?;
209+ Ok ( FuncArgs :: new ( positional, kwargs) )
210+ }
211+
212+ /// `category` is a warning class. Hand this interpreter's class across so a
213+ /// filter or recorder here sees the class it already imported.
214+ fn showwarning_value ( category : bool , value : & Bound < ' _ , PyAny > , vm : & VirtualMachine ) -> PyResult {
215+ if category
216+ && let Ok ( ty) = value. cast :: < CPythonType > ( )
217+ && let Some ( mapped) = matching_builtin_type ( ty, vm)
218+ && mapped. fast_issubclass ( vm. ctx . exceptions . warning )
219+ {
220+ return Ok ( mapped. into ( ) ) ;
221+ }
222+ to_rustpython ( value, vm)
223+ }
224+
225+ fn call_original_showwarning (
226+ args : & Bound < ' _ , PyTuple > ,
227+ kwargs : Option < & Bound < ' _ , PyDict > > ,
228+ ) -> pyo3:: PyResult < ( ) > {
229+ let Some ( original) = ORIGINAL_SHOWWARNING . get ( ) else {
230+ return Ok ( ( ) ) ;
231+ } ;
232+ original. bind ( args. py ( ) ) . call ( args, kwargs) ?;
233+ Ok ( ( ) )
234+ }
235+
94236/// What happened to one unraisable exception.
95237enum Forwarded {
96238 /// RustPython's hook ran and returned.
0 commit comments