nautilus_common/python/
factory.rs1use ahash::AHashMap;
24use nautilus_core::python::to_pynotimplemented_err;
25use parking_lot::Mutex;
26use pyo3::{Py, PyAny, PyResult, Python};
27
28pub type FactoryExtractor<T> = fn(Python<'_>, Py<PyAny>) -> PyResult<Box<T>>;
30
31#[derive(Debug)]
35pub struct FactoryRegistry<T: ?Sized> {
36 label: &'static str,
37 extractors_by_type: Mutex<AHashMap<String, FactoryExtractor<T>>>,
38}
39
40impl<T: ?Sized> FactoryRegistry<T> {
41 #[must_use]
43 pub fn new(label: &'static str) -> Self {
44 Self {
45 label,
46 extractors_by_type: Mutex::new(AHashMap::new()),
47 }
48 }
49
50 pub fn register(
59 &self,
60 type_name: String,
61 extractor: FactoryExtractor<T>,
62 ) -> anyhow::Result<()> {
63 let mut extractors = self.extractors_by_type.lock();
64
65 if let Some(registered) = extractors.get(&type_name) {
66 if std::ptr::fn_addr_eq(*registered, extractor) {
67 return Ok(());
68 }
69
70 anyhow::bail!(
71 "A different {label} extractor is already registered for '{type_name}'",
72 label = self.label
73 );
74 }
75
76 extractors.insert(type_name, extractor);
77 Ok(())
78 }
79
80 pub fn extract(&self, py: Python<'_>, factory: Py<PyAny>) -> PyResult<Box<T>> {
86 let type_name = factory
87 .getattr(py, "__class__")?
88 .getattr(py, "__name__")?
89 .extract::<String>(py)?;
90 let extractors = self.extractors_by_type.lock();
91
92 match extractors.get(&type_name) {
93 Some(extractor) => extractor(py, factory),
94 None => Err(to_pynotimplemented_err(format!(
95 "No {label} extractor registered for '{type_name}'",
96 label = self.label
97 ))),
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use std::fmt::Debug;
105
106 use pyo3::{exceptions::PyNotImplementedError, types::PyDict};
107 use rstest::rstest;
108
109 use super::*;
110
111 trait StubFactory: Debug + Send + Sync {
112 fn name(&self) -> &'static str;
113 }
114
115 #[derive(Debug)]
116 #[pyo3::pyclass(name = "StubFactoryOne")]
117 struct StubFactoryOne;
118
119 impl StubFactory for StubFactoryOne {
120 fn name(&self) -> &'static str {
121 "one"
122 }
123 }
124
125 #[expect(
126 clippy::unnecessary_wraps,
127 reason = "signature must match the FactoryExtractor fn pointer"
128 )]
129 fn extract_one(_py: Python<'_>, _factory: Py<PyAny>) -> PyResult<Box<dyn StubFactory>> {
130 Ok(Box::new(StubFactoryOne))
131 }
132
133 #[expect(
134 clippy::unnecessary_wraps,
135 reason = "signature must match the FactoryExtractor fn pointer"
136 )]
137 fn extract_conflicting(_py: Python<'_>, _factory: Py<PyAny>) -> PyResult<Box<dyn StubFactory>> {
138 Ok(Box::new(StubFactoryOne))
139 }
140
141 #[rstest]
142 fn test_extract_resolves_registered_python_class() {
143 Python::initialize();
144 let registry = FactoryRegistry::<dyn StubFactory>::new("stub factory");
145 registry
146 .register("StubFactoryOne".to_string(), extract_one)
147 .unwrap();
148
149 Python::attach(|py| {
150 let factory = Py::new(py, StubFactoryOne).unwrap().into_any();
151
152 let extracted = registry.extract(py, factory).unwrap();
153
154 assert_eq!(extracted.name(), "one");
155 });
156 }
157
158 #[rstest]
159 fn test_extract_rejects_unregistered_python_class() {
160 Python::initialize();
161 let registry = FactoryRegistry::<dyn StubFactory>::new("stub factory");
162
163 Python::attach(|py| {
164 let factory = PyDict::new(py).unbind().into_any();
165
166 let error = registry.extract(py, factory).unwrap_err();
167
168 assert!(error.is_instance_of::<PyNotImplementedError>(py));
169 assert_eq!(
170 error.to_string(),
171 "NotImplementedError: No stub factory extractor registered for 'dict'"
172 );
173 });
174 }
175
176 #[rstest]
177 fn test_register_is_idempotent_for_the_same_extractor() {
178 let registry = FactoryRegistry::<dyn StubFactory>::new("stub factory");
179
180 registry
181 .register("StubFactoryOne".to_string(), extract_one)
182 .unwrap();
183
184 assert!(
185 registry
186 .register("StubFactoryOne".to_string(), extract_one)
187 .is_ok()
188 );
189 }
190
191 #[rstest]
192 fn test_register_rejects_a_conflicting_extractor() {
193 let registry = FactoryRegistry::<dyn StubFactory>::new("stub factory");
194 registry
195 .register("StubFactoryOne".to_string(), extract_one)
196 .unwrap();
197
198 let error = registry
199 .register("StubFactoryOne".to_string(), extract_conflicting)
200 .unwrap_err();
201
202 assert_eq!(
203 error.to_string(),
204 "A different stub factory extractor is already registered for 'StubFactoryOne'"
205 );
206 }
207}