nautilus_common/python/
wrappers.rs1use std::{cell::RefCell, rc::Rc};
25
26use ahash::AHashMap;
27use nautilus_core::python::to_pyruntime_err;
28use nautilus_model::identifiers::ComponentId;
29use pyo3::prelude::*;
30
31use super::msgbus::PyMessageBusScope;
32
33thread_local! {
34 static PYTHON_WRAPPERS: RefCell<AHashMap<ComponentId, RegisteredWrapper>> =
35 RefCell::new(AHashMap::new());
36}
37
38pub fn retain_python_wrapper(
40 component_id: ComponentId,
41 wrapper: Py<PyAny>,
42 message_bus: Rc<PyMessageBusScope>,
43) {
44 let displaced = PYTHON_WRAPPERS.with_borrow_mut(|wrappers| {
45 wrappers.insert(
46 component_id,
47 RegisteredWrapper {
48 wrapper,
49 message_bus,
50 },
51 )
52 });
53
54 if displaced.is_some() {
55 log::warn!("Replaced the retained Python wrapper for {component_id}");
56 }
57
58 drop(displaced);
61}
62
63pub fn release_python_wrapper(component_id: ComponentId) {
65 let released = PYTHON_WRAPPERS.with_borrow_mut(|wrappers| wrappers.remove(&component_id));
66
67 drop(released);
68}
69
70#[must_use]
72pub fn get_python_wrapper(component_id: ComponentId) -> Option<Py<PyAny>> {
73 Python::attach(|py| {
74 PYTHON_WRAPPERS.with_borrow(|wrappers| {
75 wrappers
76 .get(&component_id)
77 .map(|registered| registered.wrapper.clone_ref(py))
78 })
79 })
80}
81
82pub fn get_python_message_bus(wrapper: &Bound<'_, PyAny>) -> PyResult<Rc<PyMessageBusScope>> {
91 PYTHON_WRAPPERS.with_borrow(|wrappers| {
92 wrappers
93 .values()
94 .find(|registered| registered.wrapper.as_ptr() == wrapper.as_ptr())
95 .map(|registered| Rc::clone(®istered.message_bus))
96 .ok_or_else(|| {
97 to_pyruntime_err("Component must be registered on the calling runtime thread")
98 })
99 })
100}
101
102struct RegisteredWrapper {
103 wrapper: Py<PyAny>,
104 message_bus: Rc<PyMessageBusScope>,
105}
106
107#[cfg(test)]
108mod tests {
109 use pyo3::{ffi::c_str, types::PyModule, wrap_pyfunction};
110 use rstest::rstest;
111
112 use super::*;
113
114 #[pyfunction]
116 fn wrapper_is_retained(component_id: &str) -> bool {
117 get_python_wrapper(ComponentId::from(component_id)).is_some()
118 }
119
120 #[rstest]
121 fn test_wrapper_finalization_re_enters_an_unborrowed_registry() {
122 Python::initialize();
123
124 Python::attach(|py| {
125 let module = PyModule::new(py, "test_wrapper_finalization").unwrap();
126 module
127 .add_function(wrap_pyfunction!(wrapper_is_retained, &module).unwrap())
128 .unwrap();
129
130 let code = c_str!(
131 r#"
132OBSERVED = []
133
134
135class Finalizing:
136 def __del__(self):
137 OBSERVED.append(wrapper_is_retained("Finalizing-Component"))
138"#
139 );
140 py.run(code, Some(&module.dict()), None).unwrap();
141
142 let finalizing = module.getattr("Finalizing").unwrap();
143 let component_id = ComponentId::from("Finalizing-Component");
144
145 retain_python_wrapper(
146 component_id,
147 finalizing.call0().unwrap().unbind(),
148 Rc::default(),
149 );
150
151 retain_python_wrapper(
154 component_id,
155 finalizing.call0().unwrap().unbind(),
156 Rc::default(),
157 );
158 release_python_wrapper(component_id);
159
160 let observed = module
161 .getattr("OBSERVED")
162 .unwrap()
163 .extract::<Vec<bool>>()
164 .unwrap();
165
166 assert_eq!(observed, vec![true, false]);
167 });
168 }
169}