Skip to main content

pyo3/types/
context.rs

1#![deny(clippy::undocumented_unsafe_blocks)]
2
3//! Types and APIs for Python [`contextvars.Context`][1] objects.
4//!
5//! On GIL-enabled Python 3.14 and newer, this module also provides safe bindings for watching
6//! changes to the current context.
7//!
8//! [1]: https://docs.python.org/3/library/contextvars.html#contextvars.Context
9
10use crate::{ffi, PyAny};
11
12#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
13use crate::{
14    err::{error_on_minusone, error_on_minusone_with_result},
15    Borrowed, PyResult, Python,
16};
17#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
18use core::ffi::c_int;
19
20/// Represents a Python [`contextvars.Context`][1] object.
21///
22/// Values of this type are accessed via PyO3's smart pointers, e.g. as
23/// [`Py<PyContext>`][crate::Py] or [`Bound<'py, PyContext>`][crate::Bound].
24///
25/// [1]: https://docs.python.org/3/library/contextvars.html#contextvars.Context
26#[repr(transparent)]
27pub struct PyContext(PyAny);
28
29pyobject_native_type_core!(
30    PyContext,
31    pyobject_native_static_type_object!(ffi::PyContext_Type),
32    "contextvars",
33    "Context",
34    #module=Some("contextvars"),
35    #checkfunction=ffi::PyContext_CheckExact
36);
37
38// TODO: enable support on free-threaded builds once
39// https://github.com/python/cpython/issues/155619 is fixed
40#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
41impl PyContext {
42    /// Registers a context watcher for the current interpreter.
43    ///
44    /// Use [`watch_callback!`] to create the callback passed to this method.
45    /// The returned [`BoundContextWatcherGuard`] removes the watcher when dropped; call
46    /// [`BoundContextWatcherGuard::unbind`] if the watcher needs to outlive the current Python
47    /// attachment.
48    ///
49    /// Panics and returned [`PyErr`][crate::PyErr] values are reported as unraisable exceptions and
50    /// never unwind across the C boundary.
51    #[doc(alias = "PyContext_AddWatcher")]
52    pub fn add_watcher(
53        py: Python<'_>,
54        callback: WatchCallback,
55    ) -> PyResult<BoundContextWatcherGuard<'_>> {
56        // SAFETY:
57        // - `py` proves that the thread is attached
58        // - `callback` contains a static C-compatible function
59        let watcher_id =
60            error_on_minusone_with_result(py, unsafe { ffi::PyContext_AddWatcher(callback.0) })?;
61
62        Ok(BoundContextWatcherGuard {
63            watcher_id,
64            py,
65            active: true,
66        })
67    }
68}
69
70/// An event passed to a context watcher.
71///
72/// This enum is non-exhaustive because CPython may add context watcher events in future versions.
73#[doc(alias = "PyContextEvent")]
74#[derive(Debug)]
75#[non_exhaustive]
76#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
77pub enum ContextEvent<'a, 'py> {
78    /// The current context changed.
79    ///
80    /// The value is the new current context, or `None` when there is no current context.
81    Switched(Option<Borrowed<'a, 'py, PyContext>>),
82
83    /// An event which is not known to this version of PyO3.
84    Unknown {
85        /// The raw CPython event value.
86        raw_event: ffi::PyContextEvent,
87
88        /// The event-specific object, if one was provided.
89        object: Option<Borrowed<'a, 'py, PyAny>>,
90    },
91}
92
93/// A Python-bound guard which keeps a context watcher registered.
94///
95/// The watcher is registered for the current Python interpreter and is removed when this guard is
96/// dropped. Use [`clear`][Self::clear] to remove it explicitly and observe any error returned by
97/// CPython.
98///
99/// This guard is bound to the [`Python`] attachment used to create it. It therefore cannot be sent
100/// to another thread, moved outside that attachment, or moved into [`Python::detach`].
101///
102/// Use [`unbind`][Self::unbind] to convert this guard into a [`ContextWatcherGuard`] which can be
103/// stored outside the current attachment.
104#[must_use = "dropping the guard immediately unregisters the context watcher"]
105#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
106pub struct BoundContextWatcherGuard<'py> {
107    watcher_id: c_int,
108    py: Python<'py>,
109    active: bool,
110}
111
112#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
113impl BoundContextWatcherGuard<'_> {
114    /// Removes this watcher from the current Python interpreter.
115    ///
116    /// Dropping the guard also removes the watcher, but cannot report a failure to the caller.
117    #[doc(alias = "PyContext_ClearWatcher")]
118    pub fn clear(mut self) -> PyResult<()> {
119        self.active = false;
120        clear_watcher(self.py, self.watcher_id)
121    }
122
123    /// Removes the connection to the current Python attachment, allowing the guard to be stored
124    /// outside it or sent to another thread.
125    ///
126    /// Dropping the returned guard automatically attaches to Python to remove the watcher. To avoid
127    /// that attachment, convert it back with [`ContextWatcherGuard::into_bound`] before dropping it,
128    /// or call [`ContextWatcherGuard::clear`] while attached.
129    pub fn unbind(mut self) -> ContextWatcherGuard {
130        self.active = false;
131        ContextWatcherGuard {
132            watcher_id: self.watcher_id,
133            active: true,
134        }
135    }
136}
137
138#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
139impl Drop for BoundContextWatcherGuard<'_> {
140    fn drop(&mut self) {
141        if !self.active {
142            return;
143        }
144
145        self.active = false;
146        clear_watcher_on_drop(self.py, self.watcher_id);
147    }
148}
149
150/// An unbound guard which keeps a context watcher registered.
151///
152/// Unlike [`BoundContextWatcherGuard`], this guard is not tied to a particular [`Python`]
153/// attachment, so it can be stored outside that attachment or sent to another thread.
154///
155/// Dropping this guard automatically attaches to Python to remove the watcher. Use
156/// [`clear`][Self::clear] to remove it with an existing attachment and observe any error returned by
157/// CPython, or [`into_bound`][Self::into_bound] to recover a bound guard.
158///
159/// If Python cannot be attached during drop, the watcher remains registered. This does not create a
160/// dangling function pointer because [`watch_callback!`] creates a static, monomorphized trampoline.
161///
162/// # Example
163///
164/// ```rust
165/// use pyo3::prelude::*;
166/// use pyo3::types::context::{watch_callback, ContextEvent};
167/// use pyo3::types::PyContext;
168///
169/// fn context_changed(
170///     _py: Python<'_>,
171///     _event: ContextEvent<'_, '_>,
172/// ) -> PyResult<()> {
173///     Ok(())
174/// }
175///
176/// # fn main() -> PyResult<()> {
177/// let watcher = Python::attach(|py| -> PyResult<_> {
178///     Ok(PyContext::add_watcher(py, watch_callback!(context_changed))?.unbind())
179/// })?;
180///
181/// // The guard can be stored until an attachment is available for explicit cleanup.
182/// Python::attach(|py| watcher.clear(py))
183/// # }
184/// ```
185#[must_use = "dropping the guard unregisters the context watcher"]
186#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
187pub struct ContextWatcherGuard {
188    watcher_id: c_int,
189    active: bool,
190}
191
192#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
193impl ContextWatcherGuard {
194    /// Removes this watcher from the current Python interpreter.
195    ///
196    /// Dropping the guard also removes the watcher, but cannot report a failure to the caller.
197    #[doc(alias = "PyContext_ClearWatcher")]
198    pub fn clear(mut self, py: Python<'_>) -> PyResult<()> {
199        self.active = false;
200        clear_watcher(py, self.watcher_id)
201    }
202
203    /// Connects this guard to the given Python attachment.
204    ///
205    /// PyO3 does not currently support using a module from multiple interpreters, so `py` is the
206    /// attachment for the interpreter in which this watcher was registered.
207    pub fn into_bound<'py>(mut self, py: Python<'py>) -> BoundContextWatcherGuard<'py> {
208        self.active = false;
209        BoundContextWatcherGuard {
210            watcher_id: self.watcher_id,
211            py,
212            active: true,
213        }
214    }
215}
216
217#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
218impl Drop for ContextWatcherGuard {
219    fn drop(&mut self) {
220        if !self.active {
221            return;
222        }
223
224        self.active = false;
225        let watcher_id = self.watcher_id;
226        let _ = Python::try_attach(|py| clear_watcher_on_drop(py, watcher_id));
227    }
228}
229
230#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
231fn clear_watcher(py: Python<'_>, watcher_id: c_int) -> PyResult<()> {
232    // SAFETY:
233    // - `py` proves that the thread is attached to an interpreter; PyO3 does not currently support
234    //   attaching to more than one interpreter, so this is the interpreter for which the watcher
235    //   was registered
236    // - `watcher_id` was returned by `PyContext_AddWatcher`
237    error_on_minusone(py, unsafe { ffi::PyContext_ClearWatcher(watcher_id) })
238}
239
240#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
241fn clear_watcher_on_drop(_py: Python<'_>, watcher_id: c_int) {
242    // A destructor must not replace an exception which was already pending. The Python token proves
243    // that this thread is attached to an interpreter; PyO3 does not currently support attaching to
244    // more than one interpreter, so this is the same interpreter the watcher was registered on. The
245    // raw exception API is intentional because `PyErr::take` may resume a `PanicException`, while
246    // `Drop` must preserve it without unwinding.
247    //
248    // SAFETY:
249    // - the thread is attached, as guaranteed by `_py`
250    // - `PyErr_GetRaisedException` returns an owned reference or NULL
251    // - `watcher_id` was returned by `PyContext_AddWatcher`
252    // - `PyErr_SetRaisedException` steals the owned reference returned above
253    unsafe {
254        let pending_exception = ffi::PyErr_GetRaisedException();
255        if ffi::PyContext_ClearWatcher(watcher_id) == -1 {
256            ffi::PyErr_WriteUnraisable(core::ptr::null_mut());
257        }
258
259        if !pending_exception.is_null() {
260            // Be defensive in case an unraisable hook itself left an exception set.
261            ffi::PyErr_Clear();
262            ffi::PyErr_SetRaisedException(pending_exception);
263        }
264    }
265}
266
267/// Callback type for context watchers.
268///
269/// Values of this type are created by [`watch_callback!`].
270#[repr(transparent)]
271#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
272pub struct WatchCallback(ffi::PyContext_WatchCallback);
273
274/// Creates a context watcher callback from a safe Rust function.
275///
276/// The function must be a path with this signature:
277///
278/// ```rust
279/// use pyo3::prelude::*;
280/// use pyo3::types::context::{watch_callback, ContextEvent};
281/// use pyo3::types::PyContext;
282///
283/// fn context_changed(
284///     _py: Python<'_>,
285///     _event: ContextEvent<'_, '_>,
286/// ) -> PyResult<()> {
287///     Ok(())
288/// }
289///
290/// # fn main() -> PyResult<()> {
291/// Python::attach(|py| {
292///     let _watcher = PyContext::add_watcher(py, watch_callback!(context_changed))?;
293///     Ok(())
294/// })
295/// # }
296/// ```
297///
298/// A function path is required because CPython's context watcher callback has no user-data
299/// pointer. The macro creates a unique static trampoline for the function, avoiding global callback
300/// storage. State can still be shared through safe static synchronization primitives.
301///
302#[macro_export]
303#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
304macro_rules! watch_callback {
305    ($callback:path) => {{
306        struct Callback;
307
308        impl $crate::types::context::impl_::ContextWatcherCallbackDef for Callback {
309            const CALLBACK: $crate::types::context::impl_::ContextWatcherCallback = $callback;
310        }
311
312        $crate::types::context::impl_::new_watch_callback::<Callback>()
313    }};
314}
315
316#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
317pub use crate::watch_callback;
318
319/// Implementation details used by [`watch_callback!`].
320#[doc(hidden)]
321#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
322pub mod impl_ {
323    use crate::{ffi_ptr_ext::FfiPtrExt, types::PyAnyMethods};
324
325    use super::*;
326
327    /// The safe callback signature accepted by context watcher trampolines.
328    pub type ContextWatcherCallback =
329        for<'a, 'py> fn(Python<'py>, ContextEvent<'a, 'py>) -> PyResult<()>;
330
331    /// Associates a generated trampoline type with its Rust callback.
332    pub trait ContextWatcherCallbackDef {
333        /// The Rust callback invoked by the generated C trampoline.
334        const CALLBACK: ContextWatcherCallback;
335    }
336
337    pub fn new_watch_callback<Callback: ContextWatcherCallbackDef>() -> WatchCallback {
338        WatchCallback(context_watcher::<Callback>)
339    }
340
341    unsafe fn event_from_raw<'a, 'py>(
342        py: Python<'py>,
343        event: ffi::PyContextEvent,
344        object: *mut ffi::PyObject,
345    ) -> PyResult<ContextEvent<'a, 'py>> {
346        match event {
347            ffi::Py_CONTEXT_SWITCHED => {
348                // SAFETY: `Py_CONTEXT_SWITCHED` is documented to always have None or a context object passed
349                let object = unsafe { object.assume_borrowed_unchecked(py) };
350
351                if object.is_none() {
352                    Ok(ContextEvent::Switched(None))
353                } else {
354                    Ok(ContextEvent::Switched(Some(object.cast()?)))
355                }
356            }
357
358            raw_event => {
359                // SAFETY: the caller guarantees that `object` follows the contract for `event`.
360                let object = unsafe { object.assume_borrowed_or_opt(py) };
361                Ok(ContextEvent::Unknown { raw_event, object })
362            }
363        }
364    }
365
366    /// C-compatible trampoline for a context watcher callback.
367    ///
368    /// # Safety
369    ///
370    /// - The thread must be attached to Python.
371    /// - `object` must follow the contract for the supplied `event`.
372    pub unsafe extern "C" fn context_watcher<Callback: ContextWatcherCallbackDef>(
373        event: ffi::PyContextEvent,
374        object: *mut ffi::PyObject,
375    ) -> c_int {
376        // A context watcher may be called with an exception already set. Save it before invoking
377        // arbitrary Rust code so that safe PyO3 APIs can be used normally inside the callback. The
378        // raw exception API is intentional because `PyErr::take` may resume a `PanicException`,
379        // which must not unwind across this C boundary.
380        //
381        // SAFETY: the caller guarantees that the thread is attached.
382        let pending_exception = unsafe { ffi::PyErr_GetRaisedException() };
383
384        // SAFETY:
385        // - the caller guarantees that the thread is attached and `object` follows the contract
386        //   for `event`
387        // - `trampoline` catches panics and converts callback errors into a Python exception
388        // - the callback's higher-ranked signature prevents borrowed event data from escaping
389        let result = unsafe {
390            crate::impl_::trampoline::trampoline(|py| {
391                let event = event_from_raw(py, event, object)?;
392
393                (Callback::CALLBACK)(py, event)?;
394
395                if crate::PyErr::occurred(py) {
396                    return Err(crate::PyErr::fetch(py));
397                }
398
399                Ok(0)
400            })
401        };
402
403        if pending_exception.is_null() {
404            return result;
405        }
406
407        // When an exception was already pending on entry, CPython requires the callback to return
408        // 0 with that same exception still set. Report a new callback error ourselves before
409        // restoring the original exception.
410        //
411        // SAFETY:
412        // - the thread is attached
413        // - `object` is valid for the duration of the callback or NULL
414        // - `pending_exception` is an owned reference from `PyErr_GetRaisedException`
415        // - `PyErr_SetRaisedException` steals that reference
416        unsafe {
417            if result == -1 {
418                ffi::PyErr_WriteUnraisable(object);
419            }
420
421            // Be defensive in case an unraisable hook itself left an exception set.
422            ffi::PyErr_Clear();
423            ffi::PyErr_SetRaisedException(pending_exception);
424        }
425
426        0
427    }
428}
429
430#[cfg(all(test, Py_3_14, not(Py_GIL_DISABLED)))]
431mod watcher_tests {
432    use super::impl_::{context_watcher, ContextWatcherCallback, ContextWatcherCallbackDef};
433    use super::{ContextEvent, PyContext};
434    use crate::exceptions::{PyRuntimeError, PyValueError};
435    use crate::platform::sync::non_poison::{Mutex, MutexGuard};
436    #[cfg(feature = "macros")]
437    use crate::test_utils::UnraisableCapture;
438    use crate::types::PyAnyMethods;
439    use crate::{ffi, PyErr, PyResult, Python};
440    use alloc::string::ToString;
441    use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
442    use static_assertions::{assert_impl_all, assert_not_impl_any};
443
444    // Context watchers are interpreter-global and limited to eight slots, so tests which register
445    // watchers must not run concurrently.
446    static WATCHER_TEST_MUTEX: Mutex<()> = Mutex::new(());
447    static SWITCH_COUNT: AtomicUsize = AtomicUsize::new(0);
448    static SAW_CONTEXT: AtomicBool = AtomicBool::new(false);
449
450    fn acquire_watcher_test_lock() -> MutexGuard<'static, ()> {
451        WATCHER_TEST_MUTEX.lock()
452    }
453
454    fn run_context_switch(py: Python<'_>) {
455        py.run(
456            c"import contextvars; contextvars.Context().run(lambda: None)",
457            None,
458            None,
459        )
460        .unwrap();
461    }
462
463    fn assert_no_context_switches(py: Python<'_>, count_before: usize) {
464        run_context_switch(py);
465        assert_eq!(SWITCH_COUNT.load(Ordering::Relaxed), count_before);
466    }
467
468    #[allow(clippy::unnecessary_wraps, reason = "context watcher callback")]
469    fn record_switch(_py: Python<'_>, event: ContextEvent<'_, '_>) -> PyResult<()> {
470        if let ContextEvent::Switched(context) = event {
471            SWITCH_COUNT.fetch_add(1, Ordering::Relaxed);
472            if let Some(context) = context {
473                assert!(context.is_exact_instance_of::<PyContext>());
474                SAW_CONTEXT.store(true, Ordering::Relaxed);
475            }
476        }
477        Ok(())
478    }
479
480    #[test]
481    fn watcher_is_cleared_on_drop() {
482        let _guard = acquire_watcher_test_lock();
483        Python::attach(|py| {
484            SWITCH_COUNT.store(0, Ordering::Relaxed);
485            SAW_CONTEXT.store(false, Ordering::Relaxed);
486
487            let watcher = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
488            run_context_switch(py);
489
490            let count_after_first_run = SWITCH_COUNT.load(Ordering::Relaxed);
491            assert!(count_after_first_run >= 2);
492            assert!(SAW_CONTEXT.load(Ordering::Relaxed));
493
494            drop(watcher);
495
496            assert_no_context_switches(py, count_after_first_run);
497        });
498    }
499
500    #[test]
501    fn watcher_can_be_cleared_explicitly() {
502        let _guard = acquire_watcher_test_lock();
503        Python::attach(|py| {
504            SWITCH_COUNT.store(0, Ordering::Relaxed);
505
506            let watcher = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
507            watcher.clear().unwrap();
508
509            assert_no_context_switches(py, 0);
510        });
511    }
512
513    #[test]
514    fn multiple_watchers_can_register_the_same_callback() {
515        let _guard = acquire_watcher_test_lock();
516        Python::attach(|py| {
517            SWITCH_COUNT.store(0, Ordering::Relaxed);
518
519            let first = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
520            let second = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
521
522            run_context_switch(py);
523            assert!(SWITCH_COUNT.load(Ordering::Relaxed) >= 4);
524
525            drop(first);
526            let count_with_both = SWITCH_COUNT.load(Ordering::Relaxed);
527            run_context_switch(py);
528            assert!(SWITCH_COUNT.load(Ordering::Relaxed) >= count_with_both + 2);
529
530            drop(second);
531            let count_after_drop = SWITCH_COUNT.load(Ordering::Relaxed);
532            assert_no_context_switches(py, count_after_drop);
533        });
534    }
535
536    #[test]
537    fn dropping_watcher_preserves_a_pending_exception() {
538        let _guard = acquire_watcher_test_lock();
539        Python::attach(|py| {
540            let watcher = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
541            PyValueError::new_err("original error").restore(py);
542
543            drop(watcher);
544
545            let error = PyErr::fetch(py);
546            assert!(error.is_instance_of::<PyValueError>(py));
547        });
548    }
549
550    fn fail_callback(_py: Python<'_>, _event: ContextEvent<'_, '_>) -> PyResult<()> {
551        Err(PyRuntimeError::new_err("watcher failed"))
552    }
553
554    struct FailingCallback;
555
556    impl ContextWatcherCallbackDef for FailingCallback {
557        const CALLBACK: ContextWatcherCallback = fail_callback;
558    }
559
560    #[test]
561    fn callback_error_is_returned_without_a_pending_exception() {
562        Python::attach(|py| {
563            // SAFETY: the thread is attached and None is valid for Py_CONTEXT_SWITCHED.
564            let result = unsafe {
565                context_watcher::<FailingCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
566            };
567
568            assert_eq!(result, -1);
569            let error = PyErr::fetch(py);
570            assert!(error.is_instance_of::<PyRuntimeError>(py));
571        });
572    }
573
574    #[allow(clippy::unnecessary_wraps, reason = "context watcher callback")]
575    fn restore_error_callback(py: Python<'_>, _event: ContextEvent<'_, '_>) -> PyResult<()> {
576        PyRuntimeError::new_err("watcher restored error").restore(py);
577        Ok(())
578    }
579
580    struct RestoringErrorCallback;
581
582    impl ContextWatcherCallbackDef for RestoringErrorCallback {
583        const CALLBACK: ContextWatcherCallback = restore_error_callback;
584    }
585
586    #[test]
587    fn callback_cannot_return_success_with_an_exception_set() {
588        Python::attach(|py| {
589            // SAFETY: the thread is attached and None is valid for Py_CONTEXT_SWITCHED.
590            let result = unsafe {
591                context_watcher::<RestoringErrorCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
592            };
593
594            assert_eq!(result, -1);
595            let error = PyErr::fetch(py);
596            assert!(error.is_instance_of::<PyRuntimeError>(py));
597            assert_eq!(error.to_string(), "RuntimeError: watcher restored error");
598        });
599    }
600
601    #[test]
602    #[cfg(feature = "macros")]
603    fn callback_error_preserves_a_pending_exception() {
604        Python::attach(|py| {
605            UnraisableCapture::enter(py, |capture| {
606                PyValueError::new_err("original error").restore(py);
607
608                // SAFETY: the thread is attached and None is valid for Py_CONTEXT_SWITCHED.
609                let result = unsafe {
610                    context_watcher::<FailingCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
611                };
612
613                assert_eq!(result, 0);
614
615                let original_error = PyErr::fetch(py);
616                assert!(original_error.is_instance_of::<PyValueError>(py));
617                assert_eq!(original_error.to_string(), "ValueError: original error");
618
619                let (watcher_error, object) =
620                    capture.take_capture().expect("missing unraisable error");
621                assert!(watcher_error.is_instance_of::<PyRuntimeError>(py));
622                assert!(object.is_none());
623            });
624        });
625    }
626
627    #[test]
628    #[cfg(feature = "macros")]
629    fn registered_callback_errors_are_unraisable() {
630        let _guard = acquire_watcher_test_lock();
631        Python::attach(|py| {
632            UnraisableCapture::enter(py, |capture| {
633                let watcher = PyContext::add_watcher(py, watch_callback!(fail_callback)).unwrap();
634
635                run_context_switch(py);
636
637                let (watcher_error, _) = capture.take_capture().expect("missing unraisable error");
638                assert!(watcher_error.is_instance_of::<PyRuntimeError>(py));
639
640                drop(watcher);
641            });
642        });
643    }
644
645    #[cfg(all(wip_feature_std, panic = "unwind"))]
646    fn panic_callback(_py: Python<'_>, _event: ContextEvent<'_, '_>) -> PyResult<()> {
647        panic!("context watcher panic")
648    }
649
650    #[cfg(all(wip_feature_std, panic = "unwind"))]
651    struct PanickingCallback;
652
653    #[cfg(all(wip_feature_std, panic = "unwind"))]
654    impl ContextWatcherCallbackDef for PanickingCallback {
655        const CALLBACK: ContextWatcherCallback = panic_callback;
656    }
657
658    #[cfg(all(wip_feature_std, panic = "unwind"))]
659    #[test]
660    fn callback_panic_does_not_cross_ffi_boundary() {
661        Python::attach(|py| {
662            // SAFETY: the thread is attached and None is valid for Py_CONTEXT_SWITCHED.
663            let result = unsafe {
664                context_watcher::<PanickingCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
665            };
666
667            assert_eq!(result, -1);
668            assert!(PyErr::occurred(py));
669
670            // SAFETY: the test has observed and intentionally discards the panic exception.
671            unsafe { ffi::PyErr_Clear() };
672        });
673    }
674
675    static UNKNOWN_EVENT: AtomicU32 = AtomicU32::new(0);
676
677    #[allow(clippy::unnecessary_wraps, reason = "context watcher callback")]
678    fn record_unknown(_py: Python<'_>, event: ContextEvent<'_, '_>) -> PyResult<()> {
679        if let ContextEvent::Unknown { raw_event, object } = event {
680            UNKNOWN_EVENT.store(raw_event, Ordering::Relaxed);
681            assert!(object.is_none());
682        }
683        Ok(())
684    }
685
686    struct UnknownCallback;
687
688    impl ContextWatcherCallbackDef for UnknownCallback {
689        const CALLBACK: ContextWatcherCallback = record_unknown;
690    }
691
692    #[test]
693    fn unknown_events_are_forwarded() {
694        const FUTURE_EVENT: ffi::PyContextEvent = 123;
695
696        Python::attach(|_py| {
697            UNKNOWN_EVENT.store(0, Ordering::Relaxed);
698
699            // SAFETY: the thread is attached and unknown events accept a null object.
700            let result =
701                unsafe { context_watcher::<UnknownCallback>(FUTURE_EVENT, core::ptr::null_mut()) };
702
703            assert_eq!(result, 0);
704            assert_eq!(UNKNOWN_EVENT.load(Ordering::Relaxed), FUTURE_EVENT);
705        });
706    }
707
708    #[test]
709    fn context_watcher_guard_traits() {
710        assert_not_impl_any!(super::BoundContextWatcherGuard<'_>: Send, Sync);
711        assert_impl_all!(super::ContextWatcherGuard: Send, Sync);
712    }
713
714    #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled
715    #[test]
716    fn unbound_watcher_attaches_on_drop_from_another_thread() {
717        let _guard = acquire_watcher_test_lock();
718        SWITCH_COUNT.store(0, Ordering::Relaxed);
719        let watcher = Python::attach(|py| {
720            PyContext::add_watcher(py, watch_callback!(record_switch))
721                .unwrap()
722                .unbind()
723        });
724
725        Python::attach(run_context_switch);
726        assert!(SWITCH_COUNT.load(Ordering::Relaxed) >= 2);
727
728        std::thread::spawn(move || drop(watcher)).join().unwrap();
729        let count_after_drop = SWITCH_COUNT.load(Ordering::Relaxed);
730
731        Python::attach(|py| assert_no_context_switches(py, count_after_drop));
732    }
733
734    #[test]
735    fn unbound_watcher_can_be_cleared_with_an_attachment() {
736        let _guard = acquire_watcher_test_lock();
737        SWITCH_COUNT.store(0, Ordering::Relaxed);
738        let watcher = Python::attach(|py| {
739            PyContext::add_watcher(py, watch_callback!(record_switch))
740                .unwrap()
741                .unbind()
742        });
743
744        let count_after_clear = Python::attach(|py| {
745            watcher.clear(py).unwrap();
746            SWITCH_COUNT.load(Ordering::Relaxed)
747        });
748
749        Python::attach(|py| assert_no_context_switches(py, count_after_clear));
750    }
751
752    #[test]
753    fn unbound_watcher_can_be_rebound() {
754        let _guard = acquire_watcher_test_lock();
755        SWITCH_COUNT.store(0, Ordering::Relaxed);
756        let watcher = Python::attach(|py| {
757            PyContext::add_watcher(py, watch_callback!(record_switch))
758                .unwrap()
759                .unbind()
760        });
761
762        Python::attach(|py| {
763            let watcher = watcher.into_bound(py);
764            run_context_switch(py);
765
766            let count_before_drop = SWITCH_COUNT.load(Ordering::Relaxed);
767            assert!(count_before_drop >= 2);
768            drop(watcher);
769
770            assert_no_context_switches(py, count_before_drop);
771        });
772    }
773}
774
775#[cfg(test)]
776mod tests {
777    use super::PyContext;
778    use crate::types::PyAnyMethods;
779    use crate::Python;
780
781    #[test]
782    fn context_type() {
783        Python::attach(|py| {
784            let context = py
785                .import(c"contextvars")
786                .unwrap()
787                .getattr(c"Context")
788                .unwrap()
789                .call0()
790                .unwrap();
791
792            assert!(context.is_exact_instance_of::<PyContext>());
793            context.cast::<PyContext>().unwrap();
794        });
795    }
796}