use pyo3::exceptions::PyAttributeError;
use pyo3::exceptions::PyKeyError;
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use pyo3::types::PyFrozenSet;
use pyo3::types::PyIterator;
use pyo3::types::PyList;
use pyo3::types::PySet;
use pyo3::types::PyTuple;
use pyo3::types::PyType;
use std::collections::HashMap;
const MUTATION_ERROR: &str = "'frozendict' object does not support mutation";
const ACCESS_DENIED: &str = "Access is Denied!";
type Obj = Py<PyAny>;
#[pyclass(name = "FrozenDict", frozen)]
pub struct FrozenDict {
entries: Box<[(isize, Obj, Obj)]>,
hash: isize,
cached_keys: Obj,
cached_values: Obj,
cached_items: Obj,
}
impl FrozenDict {
fn from_pairs(py: Python<'_>, pairs: Vec<(Obj, Obj)>) -> PyResult<Self> {
let mut map: HashMap<isize, Vec<(Obj, Obj)>> = HashMap::with_capacity(pairs.len());
for (k, v) in pairs {
let hash = k.bind(py).hash()?;
let bucket = map.entry(hash).or_default();
let mut found = false;
for (ek, ev) in bucket.iter_mut() {
if ek.bind(py).eq(k.bind(py))? {
*ev = v.clone_ref(py);
found = true;
break;
}
}
if !found {
bucket.push((k, v));
}
}
let mut deduped: Vec<(isize, Obj, Obj)> = Vec::with_capacity(map.len());
for (hash, bucket) in map {
for (k, v) in bucket {
deduped.push((hash, k, v));
}
}
deduped.sort_unstable_by_key(|&(h, _, _)| h);
let hash = compute_python_hash(py, &deduped)?;
let cached_keys = PyList::new(
py,
deduped
.iter()
.map(|(_, k, _)| k.clone_ref(py))
.collect::<Vec<_>>(),
)?
.into_any()
.unbind();
let cached_values = PyList::new(
py,
deduped
.iter()
.map(|(_, _, v)| v.clone_ref(py))
.collect::<Vec<_>>(),
)?
.into_any()
.unbind();
let cached_items = PyList::new(
py,
deduped
.iter()
.map(|(_, k, v)| {
PyTuple::new(py, [k.clone_ref(py), v.clone_ref(py)])
.map(|t| t.into_any().unbind())
})
.collect::<PyResult<Vec<_>>>()?,
)?
.into_any()
.unbind();
Ok(Self {
entries: deduped.into_boxed_slice(),
hash,
cached_keys,
cached_values,
cached_items,
})
}
#[inline]
fn into_py_object(py: Python<'_>, pairs: Vec<(Obj, Obj)>) -> PyResult<Obj> {
Ok(Py::new(py, Self::from_pairs(py, pairs)?)?.into_any())
}
#[inline]
fn num_entries(&self) -> usize {
self.entries.len()
}
#[inline]
fn pairs(&self) -> &[(isize, Obj, Obj)] {
&self.entries
}
#[inline]
fn find_entry(
&self,
py: Python<'_>,
key: &Bound<'_, PyAny>,
hash: isize,
) -> PyResult<Option<usize>> {
match self.entries.binary_search_by(|&(h, _, _)| h.cmp(&hash)) {
Err(_) => Ok(None),
Ok(idx) => {
let mut i = idx;
while i > 0 && self.entries[i - 1].0 == hash {
i -= 1;
}
while i < self.entries.len() && self.entries[i].0 == hash {
if self.entries[i].1.bind(py).eq(key)? {
return Ok(Some(i));
}
i += 1;
}
Ok(None)
}
}
}
}
fn compute_python_hash(py: Python<'_>, pairs: &[(isize, Obj, Obj)]) -> PyResult<isize> {
let mut combined: u64 = 0;
for (_, k, v) in pairs {
let pair_hash = PyTuple::new(py, [k, v])?.hash()?;
combined ^= pair_hash as u64;
}
Ok(combined as isize)
}
fn freeze_value(val: Bound<'_, PyAny>) -> PyResult<Obj> {
let py = val.py();
if let Ok(dict) = val.cast::<PyDict>() {
let pairs: Vec<(Obj, Obj)> = dict
.iter()
.map(|(k, v)| {
let frozen_v = freeze_value(v)?;
Ok((k.unbind(), frozen_v))
})
.collect::<PyResult<_>>()?;
return FrozenDict::into_py_object(py, pairs);
}
if val.is_instance_of::<FrozenDict>() {
return Ok(val.unbind());
}
if let Ok(list) = val.cast::<PyList>() {
let elements: Vec<Obj> = list
.iter()
.map(|item| freeze_value(item))
.collect::<PyResult<_>>()?;
return Ok(PyTuple::new(py, elements)?.into_any().unbind());
}
if let Ok(set) = val.cast::<PySet>() {
let frozen = PyFrozenSet::new(py, set.iter().collect::<Vec<_>>().iter())?;
return Ok(frozen.into_any().unbind());
}
Ok(val.unbind())
}
fn extract_pairs(source: &Bound<'_, PyAny>) -> PyResult<Vec<(Obj, Obj)>> {
let py = source.py();
if let Ok(dict) = source.cast::<PyDict>() {
return dict
.iter()
.map(|(k, v)| Ok((k.unbind(), freeze_value(v)?)))
.collect::<PyResult<_>>();
}
if let Ok(fd) = source.extract::<PyRef<'_, FrozenDict>>() {
return Ok(fd
.pairs()
.iter()
.map(|(_, k, v)| (k.clone_ref(py), v.clone_ref(py)))
.collect());
}
if source.hasattr("keys")? {
let keys = source.call_method0("keys")?;
let mut pairs = Vec::new();
for k in keys.try_iter()? {
let k = k?;
let v = source.get_item(&k)?;
pairs.push((k.unbind(), freeze_value(v)?));
}
return Ok(pairs);
}
let mut pairs: Vec<(Obj, Obj)> = Vec::new();
for item in PyIterator::from_object(source)? {
let item = item?;
let pair = item.cast::<PyTuple>()?;
if pair.len() != 2 {
return Err(PyTypeError::new_err(
"each item must be a (key, value) pair",
));
}
pairs.push((pair.get_item(0)?.unbind(), freeze_value(pair.get_item(1)?)?));
}
Ok(pairs)
}
#[pymethods]
impl FrozenDict {
#[new]
#[pyo3(signature = (*args, **kwargs))]
pub fn __new__(
py: Python<'_>,
args: &Bound<'_, PyTuple>,
kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<Self> {
let mut pairs: Vec<(Obj, Obj)> = Vec::new();
if args.len() > 1 {
return Err(PyTypeError::new_err(
"frozendict expected at most 1 positional argument",
));
}
if let Ok(first) = args.get_item(0) {
pairs.extend(extract_pairs(&first)?);
}
if let Some(kw) = kwargs {
for (k, v) in kw.iter() {
pairs.push((k.unbind(), freeze_value(v)?));
}
}
Self::from_pairs(py, pairs)
}
pub fn __hash__(&self) -> isize {
self.hash
}
pub fn __len__(&self) -> usize {
self.entries.len()
}
pub fn __contains__(&self, py: Python<'_>, key: &Bound<'_, PyAny>) -> PyResult<bool> {
Ok(self.find_entry(py, key, key.hash()?)?.is_some())
}
pub fn __getitem__(&self, py: Python<'_>, key: &Bound<'_, PyAny>) -> PyResult<Obj> {
let hash = key.hash()?;
match self.find_entry(py, key, hash)? {
Some(idx) => Ok(self.entries[idx].2.clone_ref(py)),
None => Err(PyKeyError::new_err(key.clone().unbind())),
}
}
pub fn __iter__(&self, py: Python<'_>) -> PyResult<Obj> {
let iterator = self.cached_keys.bind(py).try_iter()?;
Ok(iterator.into_any().unbind())
}
pub fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
let mut parts: Vec<String> = Vec::with_capacity(self.entries.len());
for (_, k, v) in self.entries.iter() {
parts.push(format!("{}: {}", k.bind(py).repr()?, v.bind(py).repr()?));
}
Ok(format!("frozendict({{{}}})", parts.join(", ")))
}
pub fn __eq__(&self, py: Python<'_>, other: &Bound<'_, PyAny>) -> PyResult<bool> {
if let Ok(other_fd) = other.extract::<PyRef<'_, FrozenDict>>() {
if self.num_entries() != other_fd.num_entries() {
return Ok(false);
}
for (_, k1, v1) in self.entries.iter() {
let hash = k1.bind(py).hash()?;
let mut found = false;
if let Ok(idx) = other_fd.entries.binary_search_by(|&(h, _, _)| h.cmp(&hash)) {
let mut i = idx;
while i > 0 && other_fd.entries[i - 1].0 == hash {
i -= 1;
}
while i < other_fd.entries.len() && other_fd.entries[i].0 == hash {
if other_fd.entries[i].1.bind(py).eq(k1.bind(py))? {
if !v1.bind(py).eq(other_fd.entries[i].2.bind(py))? {
return Ok(false);
}
found = true;
break;
}
i += 1;
}
}
if !found {
return Ok(false);
}
}
return Ok(true);
}
if let Ok(dict) = other.cast::<PyDict>() {
if self.num_entries() != dict.len() {
return Ok(false);
}
for (_, k, v) in self.entries.iter() {
match dict.get_item(k.bind(py))? {
Some(dv) => {
if !v.bind(py).eq(&dv)? {
return Ok(false);
}
}
None => return Ok(false),
}
}
return Ok(true);
}
Ok(false)
}
pub fn __or__(&self, py: Python<'_>, other: &Bound<'_, PyAny>) -> PyResult<Obj> {
let mut pairs: Vec<(Obj, Obj)> = self
.entries
.iter()
.map(|(_, k, v)| (k.clone_ref(py), v.clone_ref(py)))
.collect();
pairs.extend(extract_pairs(other)?);
Self::into_py_object(py, pairs)
}
pub fn keys(&self, py: Python<'_>) -> PyResult<Obj> {
Ok(self.cached_keys.clone_ref(py))
}
pub fn values(&self, py: Python<'_>) -> PyResult<Obj> {
Ok(self.cached_values.clone_ref(py))
}
pub fn items(&self, py: Python<'_>) -> PyResult<Obj> {
Ok(self.cached_items.clone_ref(py))
}
#[pyo3(signature = (key, default=None))]
pub fn get(
&self,
py: Python<'_>,
key: &Bound<'_, PyAny>,
default: Option<Obj>,
) -> PyResult<Obj> {
let hash = key.hash()?;
match self.find_entry(py, key, hash)? {
Some(idx) => Ok(self.entries[idx].2.clone_ref(py)),
None => Ok(default.unwrap_or_else(|| py.None())),
}
}
pub fn copy(&self, py: Python<'_>) -> PyResult<Obj> {
let pairs: Vec<(Obj, Obj)> = self
.entries
.iter()
.map(|(_, k, v)| (k.clone_ref(py), v.clone_ref(py)))
.collect();
Self::into_py_object(py, pairs)
}
#[classmethod]
#[pyo3(signature = (keys, value=None))]
pub fn fromkeys(
_cls: &Bound<'_, PyType>,
py: Python<'_>,
keys: &Bound<'_, PyAny>,
value: Option<Obj>,
) -> PyResult<Obj> {
let fill = value.unwrap_or_else(|| py.None());
let pairs: Vec<(Obj, Obj)> = PyIterator::from_object(keys)?
.map(|k| k.map(|k| (k.unbind(), fill.clone_ref(py))))
.collect::<PyResult<_>>()?;
Self::into_py_object(py, pairs)
}
#[pyo3(signature = (num_spaces=4))]
pub fn pretty_repr(&self, py: Python<'_>, num_spaces: usize) -> PyResult<String> {
let indent = " ".repeat(num_spaces);
let mut out = String::from("frozendict({\n");
for (_, k, v) in self.entries.iter() {
out.push_str(&format!(
"{indent}{}: {},\n",
k.bind(py).repr()?,
v.bind(py).repr()?
));
}
out.push_str("})");
Ok(out)
}
pub fn __delattr__(&self, _name: &str) -> PyResult<()> {
Err(PyTypeError::new_err(MUTATION_ERROR))
}
pub fn __delitem__(&self, _key: &Bound<'_, PyAny>) -> PyResult<()> {
Err(PyTypeError::new_err(MUTATION_ERROR))
}
pub fn __setattr__(&self, _name: &str, _value: &Bound<'_, PyAny>) -> PyResult<()> {
Err(PyTypeError::new_err(MUTATION_ERROR))
}
pub fn __setitem__(&self, _key: &Bound<'_, PyAny>, _value: &Bound<'_, PyAny>) -> PyResult<()> {
Err(PyTypeError::new_err(MUTATION_ERROR))
}
pub fn __dir__(&self) -> PyResult<Vec<String>> {
Err(PyAttributeError::new_err(ACCESS_DENIED))
}
#[pyo3(signature = (*_args, **_kwargs))]
pub fn pop(
&self,
_args: &Bound<'_, PyTuple>,
_kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<Obj> {
Err(PyTypeError::new_err(MUTATION_ERROR))
}
#[pyo3(signature = (*_args, **_kwargs))]
pub fn update(
&self,
_args: &Bound<'_, PyTuple>,
_kwargs: Option<&Bound<'_, PyDict>>,
) -> PyResult<()> {
Err(PyTypeError::new_err(MUTATION_ERROR))
}
#[pyo3(signature = (_key, _default=None))]
pub fn setdefault(&self, _key: &Bound<'_, PyAny>, _default: Option<Obj>) -> PyResult<Obj> {
Err(PyTypeError::new_err(MUTATION_ERROR))
}
pub fn clear(&self) -> PyResult<()> {
Err(PyTypeError::new_err(MUTATION_ERROR))
}
pub fn popitem(&self) -> PyResult<Obj> {
Err(PyTypeError::new_err(MUTATION_ERROR))
}
}
pub fn register_python_module(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<FrozenDict>()?;
Ok(())
}