pyo3 0.30.1

Bindings to Python interpreter
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
#![deny(clippy::undocumented_unsafe_blocks)]

//! Types and APIs for Python [`contextvars.Context`][1] objects.
//!
//! On GIL-enabled Python 3.14 and newer, this module also provides safe bindings for watching
//! changes to the current context.
//!
//! [1]: https://docs.python.org/3/library/contextvars.html#contextvars.Context

use crate::{PyAny, ffi};

#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
use crate::{Borrowed, PyResult, Python, err};
#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
use core::ffi::c_int;

/// Represents a Python [`contextvars.Context`][1] object.
///
/// Values of this type are accessed via PyO3's smart pointers, e.g. as
/// [`Py<PyContext>`][crate::Py] or [`Bound<'py, PyContext>`][crate::Bound].
///
/// [1]: https://docs.python.org/3/library/contextvars.html#contextvars.Context
#[repr(transparent)]
pub struct PyContext(PyAny);

pyobject_native_type_core!(
    PyContext,
    pyobject_native_static_type_object!(ffi::PyContext_Type),
    "contextvars",
    "Context",
    #module=Some("contextvars"),
    #checkfunction=ffi::PyContext_CheckExact
);

// TODO: enable support on free-threaded builds once
// https://github.com/python/cpython/issues/155619 is fixed
#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
impl PyContext {
    /// Registers a context watcher for the current interpreter.
    ///
    /// Use [`watch_callback!`] to create the callback passed to this method.
    /// The returned [`BoundContextWatcherGuard`] removes the watcher when dropped; call
    /// [`BoundContextWatcherGuard::unbind`] if the watcher needs to outlive the current Python
    /// attachment.
    ///
    /// Panics and returned [`PyErr`][crate::PyErr] values are reported as unraisable exceptions and
    /// never unwind across the C boundary.
    #[doc(alias = "PyContext_AddWatcher")]
    pub fn add_watcher(
        py: Python<'_>,
        callback: WatchCallback,
    ) -> PyResult<BoundContextWatcherGuard<'_>> {
        // SAFETY:
        // - `py` proves that the thread is attached
        // - `callback` contains a static C-compatible function
        let watcher_id = err::error_on_negative_with_result(py, unsafe {
            ffi::PyContext_AddWatcher(callback.0)
        })?;

        Ok(BoundContextWatcherGuard {
            watcher_id,
            py,
            active: true,
        })
    }
}

/// An event passed to a context watcher.
///
/// This enum is non-exhaustive because CPython may add context watcher events in future versions.
#[doc(alias = "PyContextEvent")]
#[derive(Debug)]
#[non_exhaustive]
#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
pub enum ContextEvent<'a, 'py> {
    /// The current context changed.
    ///
    /// The value is the new current context, or `None` when there is no current context.
    Switched(Option<Borrowed<'a, 'py, PyContext>>),

    /// An event which is not known to this version of PyO3.
    Unknown {
        /// The raw CPython event value.
        raw_event: ffi::PyContextEvent,

        /// The event-specific object, if one was provided.
        object: Option<Borrowed<'a, 'py, PyAny>>,
    },
}

/// A Python-bound guard which keeps a context watcher registered.
///
/// The watcher is registered for the current Python interpreter and is removed when this guard is
/// dropped. Use [`clear`][Self::clear] to remove it explicitly and observe any error returned by
/// CPython.
///
/// This guard is bound to the [`Python`] attachment used to create it. It therefore cannot be sent
/// to another thread, moved outside that attachment, or moved into [`Python::detach`].
///
/// Use [`unbind`][Self::unbind] to convert this guard into a [`ContextWatcherGuard`] which can be
/// stored outside the current attachment.
#[must_use = "dropping the guard immediately unregisters the context watcher"]
#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
pub struct BoundContextWatcherGuard<'py> {
    watcher_id: c_int,
    py: Python<'py>,
    active: bool,
}

#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
impl BoundContextWatcherGuard<'_> {
    /// Removes this watcher from the current Python interpreter.
    ///
    /// Dropping the guard also removes the watcher, but cannot report a failure to the caller.
    #[doc(alias = "PyContext_ClearWatcher")]
    pub fn clear(mut self) -> PyResult<()> {
        self.active = false;
        clear_watcher(self.py, self.watcher_id)
    }

    /// Removes the connection to the current Python attachment, allowing the guard to be stored
    /// outside it or sent to another thread.
    ///
    /// Dropping the returned guard automatically attaches to Python to remove the watcher. To avoid
    /// that attachment, convert it back with [`ContextWatcherGuard::into_bound`] before dropping it,
    /// or call [`ContextWatcherGuard::clear`] while attached.
    pub fn unbind(mut self) -> ContextWatcherGuard {
        self.active = false;
        ContextWatcherGuard {
            watcher_id: self.watcher_id,
            active: true,
        }
    }
}

#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
impl Drop for BoundContextWatcherGuard<'_> {
    fn drop(&mut self) {
        if !self.active {
            return;
        }

        self.active = false;
        clear_watcher_on_drop(self.py, self.watcher_id);
    }
}

/// An unbound guard which keeps a context watcher registered.
///
/// Unlike [`BoundContextWatcherGuard`], this guard is not tied to a particular [`Python`]
/// attachment, so it can be stored outside that attachment or sent to another thread.
///
/// Dropping this guard automatically attaches to Python to remove the watcher. Use
/// [`clear`][Self::clear] to remove it with an existing attachment and observe any error returned by
/// CPython, or [`into_bound`][Self::into_bound] to recover a bound guard.
///
/// If Python cannot be attached during drop, the watcher remains registered. This does not create a
/// dangling function pointer because [`watch_callback!`] creates a static, monomorphized trampoline.
///
/// # Example
///
/// ```rust
/// use pyo3::prelude::*;
/// use pyo3::types::context::{watch_callback, ContextEvent};
/// use pyo3::types::PyContext;
///
/// fn context_changed(
///     _py: Python<'_>,
///     _event: ContextEvent<'_, '_>,
/// ) -> PyResult<()> {
///     Ok(())
/// }
///
/// # fn main() -> PyResult<()> {
/// let watcher = Python::attach(|py| -> PyResult<_> {
///     Ok(PyContext::add_watcher(py, watch_callback!(context_changed))?.unbind())
/// })?;
///
/// // The guard can be stored until an attachment is available for explicit cleanup.
/// Python::attach(|py| watcher.clear(py))
/// # }
/// ```
#[must_use = "dropping the guard unregisters the context watcher"]
#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
pub struct ContextWatcherGuard {
    watcher_id: c_int,
    active: bool,
}

#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
impl ContextWatcherGuard {
    /// Removes this watcher from the current Python interpreter.
    ///
    /// Dropping the guard also removes the watcher, but cannot report a failure to the caller.
    #[doc(alias = "PyContext_ClearWatcher")]
    pub fn clear(mut self, py: Python<'_>) -> PyResult<()> {
        self.active = false;
        clear_watcher(py, self.watcher_id)
    }

    /// Connects this guard to the given Python attachment.
    ///
    /// PyO3 does not currently support using a module from multiple interpreters, so `py` is the
    /// attachment for the interpreter in which this watcher was registered.
    pub fn into_bound<'py>(mut self, py: Python<'py>) -> BoundContextWatcherGuard<'py> {
        self.active = false;
        BoundContextWatcherGuard {
            watcher_id: self.watcher_id,
            py,
            active: true,
        }
    }
}

#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
impl Drop for ContextWatcherGuard {
    fn drop(&mut self) {
        if !self.active {
            return;
        }

        self.active = false;
        let watcher_id = self.watcher_id;
        let _ = Python::try_attach(|py| clear_watcher_on_drop(py, watcher_id));
    }
}

#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
fn clear_watcher(py: Python<'_>, watcher_id: c_int) -> PyResult<()> {
    // SAFETY:
    // - `py` proves that the thread is attached to an interpreter; PyO3 does not currently support
    //   attaching to more than one interpreter, so this is the interpreter for which the watcher
    //   was registered
    // - `watcher_id` was returned by `PyContext_AddWatcher`
    err::error_on_negative(py, unsafe { ffi::PyContext_ClearWatcher(watcher_id) })
}

#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
fn clear_watcher_on_drop(_py: Python<'_>, watcher_id: c_int) {
    // A destructor must not replace an exception which was already pending. The Python token proves
    // that this thread is attached to an interpreter; PyO3 does not currently support attaching to
    // more than one interpreter, so this is the same interpreter the watcher was registered on. The
    // raw exception API is intentional because `PyErr::take` may resume a `PanicException`, while
    // `Drop` must preserve it without unwinding.
    //
    // SAFETY:
    // - the thread is attached, as guaranteed by `_py`
    // - `PyErr_GetRaisedException` returns an owned reference or NULL
    // - `watcher_id` was returned by `PyContext_AddWatcher`
    // - `PyErr_SetRaisedException` steals the owned reference returned above
    unsafe {
        let pending_exception = ffi::PyErr_GetRaisedException();
        if ffi::PyContext_ClearWatcher(watcher_id) == -1 {
            ffi::PyErr_WriteUnraisable(core::ptr::null_mut());
        }

        if !pending_exception.is_null() {
            // Be defensive in case an unraisable hook itself left an exception set.
            ffi::PyErr_Clear();
            ffi::PyErr_SetRaisedException(pending_exception);
        }
    }
}

/// Callback type for context watchers.
///
/// Values of this type are created by [`watch_callback!`].
#[repr(transparent)]
#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
pub struct WatchCallback(ffi::PyContext_WatchCallback);

/// Creates a context watcher callback from a safe Rust function.
///
/// The function must be a path with this signature:
///
/// ```rust
/// use pyo3::prelude::*;
/// use pyo3::types::context::{watch_callback, ContextEvent};
/// use pyo3::types::PyContext;
///
/// fn context_changed(
///     _py: Python<'_>,
///     _event: ContextEvent<'_, '_>,
/// ) -> PyResult<()> {
///     Ok(())
/// }
///
/// # fn main() -> PyResult<()> {
/// Python::attach(|py| {
///     let _watcher = PyContext::add_watcher(py, watch_callback!(context_changed))?;
///     Ok(())
/// })
/// # }
/// ```
///
/// A function path is required because CPython's context watcher callback has no user-data
/// pointer. The macro creates a unique static trampoline for the function, avoiding global callback
/// storage. State can still be shared through safe static synchronization primitives.
///
#[macro_export]
#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
macro_rules! watch_callback {
    ($callback:path) => {{
        struct Callback;

        impl $crate::types::context::impl_::ContextWatcherCallbackDef for Callback {
            const CALLBACK: $crate::types::context::impl_::ContextWatcherCallback = $callback;
        }

        $crate::types::context::impl_::new_watch_callback::<Callback>()
    }};
}

#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
pub use crate::watch_callback;

/// Implementation details used by [`watch_callback!`].
#[doc(hidden)]
#[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))]
pub mod impl_ {
    use crate::{ffi_ptr_ext::FfiPtrExt, types::PyAnyMethods};

    use super::*;

    /// The safe callback signature accepted by context watcher trampolines.
    pub type ContextWatcherCallback =
        for<'a, 'py> fn(Python<'py>, ContextEvent<'a, 'py>) -> PyResult<()>;

    /// Associates a generated trampoline type with its Rust callback.
    pub trait ContextWatcherCallbackDef {
        /// The Rust callback invoked by the generated C trampoline.
        const CALLBACK: ContextWatcherCallback;
    }

    pub fn new_watch_callback<Callback: ContextWatcherCallbackDef>() -> WatchCallback {
        WatchCallback(context_watcher::<Callback>)
    }

    unsafe fn event_from_raw<'a, 'py>(
        py: Python<'py>,
        event: ffi::PyContextEvent,
        object: *mut ffi::PyObject,
    ) -> PyResult<ContextEvent<'a, 'py>> {
        match event {
            ffi::Py_CONTEXT_SWITCHED => {
                // SAFETY: `Py_CONTEXT_SWITCHED` is documented to always have None or a context object passed
                let object = unsafe { object.assume_borrowed_unchecked(py) };

                if object.is_none() {
                    Ok(ContextEvent::Switched(None))
                } else {
                    Ok(ContextEvent::Switched(Some(object.cast()?)))
                }
            }

            raw_event => {
                // SAFETY: the caller guarantees that `object` follows the contract for `event`.
                let object = unsafe { object.assume_borrowed_or_opt(py) };
                Ok(ContextEvent::Unknown { raw_event, object })
            }
        }
    }

    /// C-compatible trampoline for a context watcher callback.
    ///
    /// # Safety
    ///
    /// - The thread must be attached to Python.
    /// - `object` must follow the contract for the supplied `event`.
    pub unsafe extern "C" fn context_watcher<Callback: ContextWatcherCallbackDef>(
        event: ffi::PyContextEvent,
        object: *mut ffi::PyObject,
    ) -> c_int {
        // A context watcher may be called with an exception already set. Save it before invoking
        // arbitrary Rust code so that safe PyO3 APIs can be used normally inside the callback. The
        // raw exception API is intentional because `PyErr::take` may resume a `PanicException`,
        // which must not unwind across this C boundary.
        //
        // SAFETY: the caller guarantees that the thread is attached.
        let pending_exception = unsafe { ffi::PyErr_GetRaisedException() };

        // SAFETY:
        // - the caller guarantees that the thread is attached and `object` follows the contract
        //   for `event`
        // - `trampoline` catches panics and converts callback errors into a Python exception
        // - the callback's higher-ranked signature prevents borrowed event data from escaping
        let result = unsafe {
            crate::impl_::trampoline::trampoline(|py| {
                let event = event_from_raw(py, event, object)?;

                (Callback::CALLBACK)(py, event)?;

                if crate::PyErr::occurred(py) {
                    return Err(crate::PyErr::fetch(py));
                }

                Ok(0)
            })
        };

        if pending_exception.is_null() {
            return result;
        }

        // When an exception was already pending on entry, CPython requires the callback to return
        // 0 with that same exception still set. Report a new callback error ourselves before
        // restoring the original exception.
        //
        // SAFETY:
        // - the thread is attached
        // - `object` is valid for the duration of the callback or NULL
        // - `pending_exception` is an owned reference from `PyErr_GetRaisedException`
        // - `PyErr_SetRaisedException` steals that reference
        unsafe {
            if result == -1 {
                ffi::PyErr_WriteUnraisable(object);
            }

            // Be defensive in case an unraisable hook itself left an exception set.
            ffi::PyErr_Clear();
            ffi::PyErr_SetRaisedException(pending_exception);
        }

        0
    }
}

#[cfg(all(test, Py_3_14, not(Py_GIL_DISABLED)))]
mod watcher_tests {
    use super::impl_::{ContextWatcherCallback, ContextWatcherCallbackDef, context_watcher};
    use super::{ContextEvent, PyContext};
    use crate::exceptions::{PyRuntimeError, PyValueError};
    use crate::platform::sync::nonpoison::{Mutex, MutexGuard};
    #[cfg(feature = "macros")]
    use crate::test_utils::UnraisableCapture;
    use crate::types::PyAnyMethods;
    use crate::{PyErr, PyResult, Python, ffi};
    use alloc::string::ToString;
    use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
    use static_assertions::{assert_impl_all, assert_not_impl_any};

    // Context watchers are interpreter-global and limited to eight slots, so tests which register
    // watchers must not run concurrently.
    static WATCHER_TEST_MUTEX: Mutex<()> = Mutex::new(());
    static SWITCH_COUNT: AtomicUsize = AtomicUsize::new(0);
    static SAW_CONTEXT: AtomicBool = AtomicBool::new(false);

    fn acquire_watcher_test_lock() -> MutexGuard<'static, ()> {
        WATCHER_TEST_MUTEX.lock()
    }

    fn run_context_switch(py: Python<'_>) {
        py.run(
            c"import contextvars; contextvars.Context().run(lambda: None)",
            None,
            None,
        )
        .unwrap();
    }

    fn assert_no_context_switches(py: Python<'_>, count_before: usize) {
        run_context_switch(py);
        assert_eq!(SWITCH_COUNT.load(Ordering::Relaxed), count_before);
    }

    #[allow(clippy::unnecessary_wraps, reason = "context watcher callback")]
    fn record_switch(_py: Python<'_>, event: ContextEvent<'_, '_>) -> PyResult<()> {
        if let ContextEvent::Switched(context) = event {
            SWITCH_COUNT.fetch_add(1, Ordering::Relaxed);
            if let Some(context) = context {
                assert!(context.is_exact_instance_of::<PyContext>());
                SAW_CONTEXT.store(true, Ordering::Relaxed);
            }
        }
        Ok(())
    }

    #[test]
    fn watcher_is_cleared_on_drop() {
        let _guard = acquire_watcher_test_lock();
        Python::attach(|py| {
            SWITCH_COUNT.store(0, Ordering::Relaxed);
            SAW_CONTEXT.store(false, Ordering::Relaxed);

            let watcher = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
            run_context_switch(py);

            let count_after_first_run = SWITCH_COUNT.load(Ordering::Relaxed);
            assert!(count_after_first_run >= 2);
            assert!(SAW_CONTEXT.load(Ordering::Relaxed));

            drop(watcher);

            assert_no_context_switches(py, count_after_first_run);
        });
    }

    #[test]
    fn watcher_can_be_cleared_explicitly() {
        let _guard = acquire_watcher_test_lock();
        Python::attach(|py| {
            SWITCH_COUNT.store(0, Ordering::Relaxed);

            let watcher = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
            watcher.clear().unwrap();

            assert_no_context_switches(py, 0);
        });
    }

    #[test]
    fn multiple_watchers_can_register_the_same_callback() {
        let _guard = acquire_watcher_test_lock();
        Python::attach(|py| {
            SWITCH_COUNT.store(0, Ordering::Relaxed);

            let first = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
            let second = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();

            run_context_switch(py);
            assert!(SWITCH_COUNT.load(Ordering::Relaxed) >= 4);

            drop(first);
            let count_with_both = SWITCH_COUNT.load(Ordering::Relaxed);
            run_context_switch(py);
            assert!(SWITCH_COUNT.load(Ordering::Relaxed) >= count_with_both + 2);

            drop(second);
            let count_after_drop = SWITCH_COUNT.load(Ordering::Relaxed);
            assert_no_context_switches(py, count_after_drop);
        });
    }

    #[test]
    fn dropping_watcher_preserves_a_pending_exception() {
        let _guard = acquire_watcher_test_lock();
        Python::attach(|py| {
            let watcher = PyContext::add_watcher(py, watch_callback!(record_switch)).unwrap();
            PyValueError::new_err("original error").restore(py);

            drop(watcher);

            let error = PyErr::fetch(py);
            assert!(error.is_instance_of::<PyValueError>(py));
        });
    }

    fn fail_callback(_py: Python<'_>, _event: ContextEvent<'_, '_>) -> PyResult<()> {
        Err(PyRuntimeError::new_err("watcher failed"))
    }

    struct FailingCallback;

    impl ContextWatcherCallbackDef for FailingCallback {
        const CALLBACK: ContextWatcherCallback = fail_callback;
    }

    #[test]
    fn callback_error_is_returned_without_a_pending_exception() {
        Python::attach(|py| {
            // SAFETY: the thread is attached and None is valid for Py_CONTEXT_SWITCHED.
            let result = unsafe {
                context_watcher::<FailingCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
            };

            assert_eq!(result, -1);
            let error = PyErr::fetch(py);
            assert!(error.is_instance_of::<PyRuntimeError>(py));
        });
    }

    #[allow(clippy::unnecessary_wraps, reason = "context watcher callback")]
    fn restore_error_callback(py: Python<'_>, _event: ContextEvent<'_, '_>) -> PyResult<()> {
        PyRuntimeError::new_err("watcher restored error").restore(py);
        Ok(())
    }

    struct RestoringErrorCallback;

    impl ContextWatcherCallbackDef for RestoringErrorCallback {
        const CALLBACK: ContextWatcherCallback = restore_error_callback;
    }

    #[test]
    fn callback_cannot_return_success_with_an_exception_set() {
        Python::attach(|py| {
            // SAFETY: the thread is attached and None is valid for Py_CONTEXT_SWITCHED.
            let result = unsafe {
                context_watcher::<RestoringErrorCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
            };

            assert_eq!(result, -1);
            let error = PyErr::fetch(py);
            assert!(error.is_instance_of::<PyRuntimeError>(py));
            assert_eq!(error.to_string(), "RuntimeError: watcher restored error");
        });
    }

    #[test]
    #[cfg(feature = "macros")]
    fn callback_error_preserves_a_pending_exception() {
        Python::attach(|py| {
            UnraisableCapture::enter(py, |capture| {
                PyValueError::new_err("original error").restore(py);

                // SAFETY: the thread is attached and None is valid for Py_CONTEXT_SWITCHED.
                let result = unsafe {
                    context_watcher::<FailingCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
                };

                assert_eq!(result, 0);

                let original_error = PyErr::fetch(py);
                assert!(original_error.is_instance_of::<PyValueError>(py));
                assert_eq!(original_error.to_string(), "ValueError: original error");

                let (watcher_error, object) =
                    capture.take_capture().expect("missing unraisable error");
                assert!(watcher_error.is_instance_of::<PyRuntimeError>(py));
                assert!(object.is_none());
            });
        });
    }

    #[test]
    #[cfg(feature = "macros")]
    fn registered_callback_errors_are_unraisable() {
        let _guard = acquire_watcher_test_lock();
        Python::attach(|py| {
            UnraisableCapture::enter(py, |capture| {
                let watcher = PyContext::add_watcher(py, watch_callback!(fail_callback)).unwrap();

                run_context_switch(py);

                let (watcher_error, _) = capture.take_capture().expect("missing unraisable error");
                assert!(watcher_error.is_instance_of::<PyRuntimeError>(py));

                drop(watcher);
            });
        });
    }

    #[cfg(all(wip_feature_std, panic = "unwind"))]
    fn panic_callback(_py: Python<'_>, _event: ContextEvent<'_, '_>) -> PyResult<()> {
        panic!("context watcher panic")
    }

    #[cfg(all(wip_feature_std, panic = "unwind"))]
    struct PanickingCallback;

    #[cfg(all(wip_feature_std, panic = "unwind"))]
    impl ContextWatcherCallbackDef for PanickingCallback {
        const CALLBACK: ContextWatcherCallback = panic_callback;
    }

    #[cfg(all(wip_feature_std, panic = "unwind"))]
    #[test]
    fn callback_panic_does_not_cross_ffi_boundary() {
        Python::attach(|py| {
            // SAFETY: the thread is attached and None is valid for Py_CONTEXT_SWITCHED.
            let result = unsafe {
                context_watcher::<PanickingCallback>(ffi::Py_CONTEXT_SWITCHED, ffi::Py_None())
            };

            assert_eq!(result, -1);
            assert!(PyErr::occurred(py));

            // SAFETY: the test has observed and intentionally discards the panic exception.
            unsafe { ffi::PyErr_Clear() };
        });
    }

    static UNKNOWN_EVENT: AtomicU32 = AtomicU32::new(0);

    #[allow(clippy::unnecessary_wraps, reason = "context watcher callback")]
    fn record_unknown(_py: Python<'_>, event: ContextEvent<'_, '_>) -> PyResult<()> {
        if let ContextEvent::Unknown { raw_event, object } = event {
            UNKNOWN_EVENT.store(raw_event, Ordering::Relaxed);
            assert!(object.is_none());
        }
        Ok(())
    }

    struct UnknownCallback;

    impl ContextWatcherCallbackDef for UnknownCallback {
        const CALLBACK: ContextWatcherCallback = record_unknown;
    }

    #[test]
    fn unknown_events_are_forwarded() {
        const FUTURE_EVENT: ffi::PyContextEvent = 123;

        Python::attach(|_py| {
            UNKNOWN_EVENT.store(0, Ordering::Relaxed);

            // SAFETY: the thread is attached and unknown events accept a null object.
            let result =
                unsafe { context_watcher::<UnknownCallback>(FUTURE_EVENT, core::ptr::null_mut()) };

            assert_eq!(result, 0);
            assert_eq!(UNKNOWN_EVENT.load(Ordering::Relaxed), FUTURE_EVENT);
        });
    }

    #[test]
    fn context_watcher_guard_traits() {
        assert_not_impl_any!(super::BoundContextWatcherGuard<'_>: Send, Sync);
        assert_impl_all!(super::ContextWatcherGuard: Send, Sync);
    }

    #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled
    #[test]
    fn unbound_watcher_attaches_on_drop_from_another_thread() {
        let _guard = acquire_watcher_test_lock();
        SWITCH_COUNT.store(0, Ordering::Relaxed);
        let watcher = Python::attach(|py| {
            PyContext::add_watcher(py, watch_callback!(record_switch))
                .unwrap()
                .unbind()
        });

        Python::attach(run_context_switch);
        assert!(SWITCH_COUNT.load(Ordering::Relaxed) >= 2);

        std::thread::spawn(move || drop(watcher)).join().unwrap();
        let count_after_drop = SWITCH_COUNT.load(Ordering::Relaxed);

        Python::attach(|py| assert_no_context_switches(py, count_after_drop));
    }

    #[test]
    fn unbound_watcher_can_be_cleared_with_an_attachment() {
        let _guard = acquire_watcher_test_lock();
        SWITCH_COUNT.store(0, Ordering::Relaxed);
        let watcher = Python::attach(|py| {
            PyContext::add_watcher(py, watch_callback!(record_switch))
                .unwrap()
                .unbind()
        });

        let count_after_clear = Python::attach(|py| {
            watcher.clear(py).unwrap();
            SWITCH_COUNT.load(Ordering::Relaxed)
        });

        Python::attach(|py| assert_no_context_switches(py, count_after_clear));
    }

    #[test]
    fn unbound_watcher_can_be_rebound() {
        let _guard = acquire_watcher_test_lock();
        SWITCH_COUNT.store(0, Ordering::Relaxed);
        let watcher = Python::attach(|py| {
            PyContext::add_watcher(py, watch_callback!(record_switch))
                .unwrap()
                .unbind()
        });

        Python::attach(|py| {
            let watcher = watcher.into_bound(py);
            run_context_switch(py);

            let count_before_drop = SWITCH_COUNT.load(Ordering::Relaxed);
            assert!(count_before_drop >= 2);
            drop(watcher);

            assert_no_context_switches(py, count_before_drop);
        });
    }
}

#[cfg(test)]
mod tests {
    use super::PyContext;
    use crate::Python;
    use crate::types::PyAnyMethods;

    #[test]
    fn context_type() {
        Python::attach(|py| {
            let context = py
                .import(c"contextvars")
                .unwrap()
                .getattr(c"Context")
                .unwrap()
                .call0()
                .unwrap();

            assert!(context.is_exact_instance_of::<PyContext>());
            context.cast::<PyContext>().unwrap();
        });
    }
}