use std::sync::Mutex;
use ahash::AHashMap;
use nautilus_core::{MUTEX_POISONED, python::to_pynotimplemented_err};
use pyo3::{Py, PyAny, PyResult, Python};
pub type FactoryExtractor<T> = fn(Python<'_>, Py<PyAny>) -> PyResult<Box<T>>;
#[derive(Debug)]
pub struct FactoryRegistry<T: ?Sized> {
label: &'static str,
extractors_by_type: Mutex<AHashMap<String, FactoryExtractor<T>>>,
}
impl<T: ?Sized> FactoryRegistry<T> {
#[must_use]
pub fn new(label: &'static str) -> Self {
Self {
label,
extractors_by_type: Mutex::new(AHashMap::new()),
}
}
pub fn register(
&self,
type_name: String,
extractor: FactoryExtractor<T>,
) -> anyhow::Result<()> {
let mut extractors = self.extractors_by_type.lock().expect(MUTEX_POISONED);
if let Some(registered) = extractors.get(&type_name) {
if std::ptr::fn_addr_eq(*registered, extractor) {
return Ok(());
}
anyhow::bail!(
"A different {label} extractor is already registered for '{type_name}'",
label = self.label
);
}
extractors.insert(type_name, extractor);
Ok(())
}
pub fn extract(&self, py: Python<'_>, factory: Py<PyAny>) -> PyResult<Box<T>> {
let type_name = factory
.getattr(py, "__class__")?
.getattr(py, "__name__")?
.extract::<String>(py)?;
let extractors = self.extractors_by_type.lock().expect(MUTEX_POISONED);
match extractors.get(&type_name) {
Some(extractor) => extractor(py, factory),
None => Err(to_pynotimplemented_err(format!(
"No {label} extractor registered for '{type_name}'",
label = self.label
))),
}
}
}
#[cfg(test)]
mod tests {
use std::fmt::Debug;
use pyo3::{exceptions::PyNotImplementedError, types::PyDict};
use rstest::rstest;
use super::*;
trait StubFactory: Debug + Send + Sync {
fn name(&self) -> &'static str;
}
#[derive(Debug)]
#[pyo3::pyclass(name = "StubFactoryOne")]
struct StubFactoryOne;
impl StubFactory for StubFactoryOne {
fn name(&self) -> &'static str {
"one"
}
}
#[expect(
clippy::unnecessary_wraps,
reason = "signature must match the FactoryExtractor fn pointer"
)]
fn extract_one(_py: Python<'_>, _factory: Py<PyAny>) -> PyResult<Box<dyn StubFactory>> {
Ok(Box::new(StubFactoryOne))
}
#[expect(
clippy::unnecessary_wraps,
reason = "signature must match the FactoryExtractor fn pointer"
)]
fn extract_conflicting(_py: Python<'_>, _factory: Py<PyAny>) -> PyResult<Box<dyn StubFactory>> {
Ok(Box::new(StubFactoryOne))
}
#[rstest]
fn test_extract_resolves_registered_python_class() {
Python::initialize();
let registry = FactoryRegistry::<dyn StubFactory>::new("stub factory");
registry
.register("StubFactoryOne".to_string(), extract_one)
.unwrap();
Python::attach(|py| {
let factory = Py::new(py, StubFactoryOne).unwrap().into_any();
let extracted = registry.extract(py, factory).unwrap();
assert_eq!(extracted.name(), "one");
});
}
#[rstest]
fn test_extract_rejects_unregistered_python_class() {
Python::initialize();
let registry = FactoryRegistry::<dyn StubFactory>::new("stub factory");
Python::attach(|py| {
let factory = PyDict::new(py).unbind().into_any();
let error = registry.extract(py, factory).unwrap_err();
assert!(error.is_instance_of::<PyNotImplementedError>(py));
assert_eq!(
error.to_string(),
"NotImplementedError: No stub factory extractor registered for 'dict'"
);
});
}
#[rstest]
fn test_register_is_idempotent_for_the_same_extractor() {
let registry = FactoryRegistry::<dyn StubFactory>::new("stub factory");
registry
.register("StubFactoryOne".to_string(), extract_one)
.unwrap();
assert!(
registry
.register("StubFactoryOne".to_string(), extract_one)
.is_ok()
);
}
#[rstest]
fn test_register_rejects_a_conflicting_extractor() {
let registry = FactoryRegistry::<dyn StubFactory>::new("stub factory");
registry
.register("StubFactoryOne".to_string(), extract_one)
.unwrap();
let error = registry
.register("StubFactoryOne".to_string(), extract_conflicting)
.unwrap_err();
assert_eq!(
error.to_string(),
"A different stub factory extractor is already registered for 'StubFactoryOne'"
);
}
}