use monty_types::MontyUuid;
use pyo3::{
Bound,
exceptions::{PyAttributeError, PyRuntimeError, PyTypeError, PyValueError},
intern,
prelude::*,
sync::PyOnceLock,
types::{PyBytes, PyDict, PyTuple},
};
pub fn is_class_instance_wrapper(value: &Bound<'_, PyAny>) -> PyResult<bool> {
value.is_instance(get_class_instance_class(value.py())?)
}
pub fn is_class_type_wrapper(value: &Bound<'_, PyAny>) -> PyResult<bool> {
value.is_instance(get_class_type_class(value.py())?)
}
#[derive(Debug)]
pub struct InstanceStore {
objects: Py<PyDict>,
}
impl InstanceStore {
#[must_use]
pub fn new(py: Python<'_>) -> Self {
Self {
objects: PyDict::new(py).unbind(),
}
}
#[must_use]
pub fn clone_ref(&self, py: Python<'_>) -> Self {
Self {
objects: self.objects.clone_ref(py),
}
}
pub fn call_method(
&self,
py: Python<'_>,
uuid: &MontyUuid,
name: &str,
args: &Bound<'_, PyTuple>,
kwargs: &Bound<'_, PyDict>,
) -> PyResult<Py<PyAny>> {
let Some(wrapper) = self.get(py, uuid)? else {
return Err(store_miss_error(name, uuid));
};
wrapper
.bind(py)
.call_method1(intern!(py, "call_method"), (name, args, kwargs))
.map(Bound::unbind)
}
pub fn lookup_lazy_attr(&self, py: Python<'_>, uuid: &MontyUuid, name: &str) -> PyResult<Option<Py<PyAny>>> {
let Some(wrapper) = self.get(py, uuid)? else {
return Ok(None);
};
match wrapper.bind(py).call_method1(intern!(py, "lookup_lazy_attrs"), (name,)) {
Ok(value) => Ok(Some(value.unbind())),
Err(err) if err.is_instance_of::<PyAttributeError>(py) => Ok(None),
Err(err) => Err(err),
}
}
pub(super) fn register(&self, uuid: &MontyUuid, wrapper: &Bound<'_, PyAny>) -> PyResult<()> {
let py = wrapper.py();
self.check_no_alias(uuid, wrapper)?;
self.objects
.bind(py)
.set_item(PyBytes::new(py, uuid.as_bytes()), wrapper)
}
pub(super) fn register_class_type_if_absent(&self, uuid: &MontyUuid, wrapper: &Bound<'_, PyAny>) -> PyResult<()> {
let py = wrapper.py();
self.check_no_alias(uuid, wrapper)?;
self.objects
.bind(py)
.set_default(PyBytes::new(py, uuid.as_bytes()), wrapper)?;
Ok(())
}
fn check_no_alias(&self, uuid: &MontyUuid, wrapper: &Bound<'_, PyAny>) -> PyResult<()> {
let py = wrapper.py();
let value_str = intern!(py, "value");
if let Some(existing) = self.objects.bind(py).get_item(PyBytes::new(py, uuid.as_bytes()))?
&& !existing.getattr(value_str)?.is(&wrapper.getattr(value_str)?)
{
return Err(PyValueError::new_err(format!(
"wrapper id {uuid} already identifies a different object in this session"
)));
}
Ok(())
}
pub(super) fn get(&self, py: Python<'_>, uuid: &MontyUuid) -> PyResult<Option<Py<PyAny>>> {
Ok(self
.objects
.bind(py)
.get_item(PyBytes::new(py, uuid.as_bytes()))?
.map(Bound::unbind))
}
pub(super) fn get_class(&self, py: Python<'_>, uuid: &MontyUuid) -> PyResult<Option<Py<PyAny>>> {
let Some(wrapper) = self.get(py, uuid)? else {
return Ok(None);
};
Ok(Some(wrapper.bind(py).getattr(intern!(py, "value"))?.unbind()))
}
}
pub fn uuid_to_py(py: Python<'_>, uuid: &MontyUuid) -> PyResult<Py<PyAny>> {
static UUID_CLASS: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
let class = UUID_CLASS.import(py, "uuid", "UUID")?;
let kwargs = PyDict::new(py);
kwargs.set_item(intern!(py, "bytes"), PyBytes::new(py, uuid.as_bytes()))?;
class.call((), Some(&kwargs)).map(Bound::unbind)
}
#[derive(Debug, Clone)]
pub(super) struct ClassHeader {
pub(super) name: String,
pub(super) id: MontyUuid,
pub(super) host_defined: bool,
pub(super) is_dataclass: bool,
}
#[pyclass(name = "MontyClassProxy", module = "pydantic_monty", frozen)]
pub struct PyMontyClassProxy {
pub(super) class_type: ClassHeader,
pub(super) instance_id: MontyUuid,
pub(super) attributes: Py<PyDict>,
pub(super) class_attributes: Py<PyDict>,
}
#[pymethods]
impl PyMontyClassProxy {
#[getter]
fn name(&self) -> &str {
&self.class_type.name
}
#[getter]
fn is_dataclass(&self) -> bool {
self.class_type.is_dataclass
}
#[getter]
fn id(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
uuid_to_py(py, &self.instance_id)
}
#[getter]
fn attributes(&self, py: Python<'_>) -> Py<PyDict> {
self.attributes.clone_ref(py)
}
fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
let attrs_repr: String = self.attributes.bind(py).repr()?.extract()?;
Ok(format!(
"MontyClassProxy(name='{}', attributes={attrs_repr})",
self.class_type.name
))
}
fn __eq__(&self, py: Python<'_>, other: &Bound<'_, PyAny>) -> PyResult<bool> {
if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
Ok(self.class_type.name == other.class_type.name
&& self.class_type.is_dataclass == other.class_type.is_dataclass
&& self.attributes.bind(py).eq(other.attributes.bind(py))?)
} else {
Ok(false)
}
}
}
#[pyclass(name = "MontyClassTypeProxy", module = "pydantic_monty", frozen)]
pub struct PyMontyClassTypeProxy {
pub(super) class_type: ClassHeader,
pub(super) attributes: Py<PyDict>,
}
#[pymethods]
impl PyMontyClassTypeProxy {
#[getter]
fn name(&self) -> &str {
&self.class_type.name
}
#[getter]
fn is_dataclass(&self) -> bool {
self.class_type.is_dataclass
}
#[getter]
fn id(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
uuid_to_py(py, &self.class_type.id)
}
#[getter]
fn attributes(&self, py: Python<'_>) -> Py<PyDict> {
self.attributes.clone_ref(py)
}
fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
let attrs_repr: String = self.attributes.bind(py).repr()?.extract()?;
Ok(format!(
"MontyClassTypeProxy(name='{}', attributes={attrs_repr})",
self.class_type.name
))
}
fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool {
other
.extract::<PyRef<'_, Self>>()
.is_ok_and(|other| self.class_type.id == other.class_type.id)
}
}
fn store_miss_error(name: &str, uuid: &MontyUuid) -> PyErr {
PyRuntimeError::new_err(format!(
"no host object registered for method call '{name}' (id {uuid}) — \
the instance store is empty after loading a dump into a fresh session"
))
}
pub(super) fn wrapper_uuid(wrapper: &Bound<'_, PyAny>, kind: &str) -> PyResult<MontyUuid> {
let py = wrapper.py();
let bytes: [u8; 16] = wrapper
.getattr(intern!(py, "id"))?
.getattr(intern!(py, "bytes"))
.and_then(|bytes| bytes.extract())
.map_err(|_| PyTypeError::new_err(format!("{kind}.id must be a uuid.UUID")))?;
Ok(MontyUuid::from_bytes(bytes))
}
fn get_class_instance_class(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
static CLASS_INSTANCE: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
CLASS_INSTANCE.import(py, "pydantic_monty.class_instance", "ClassInstance")
}
fn get_class_type_class(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
static CLASS_TYPE: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
CLASS_TYPE.import(py, "pydantic_monty.class_instance", "ClassType")
}