diff --git a/src/backing/containers/py_item.rs b/src/backing/containers/py_item.rs index 81c38af..363c853 100644 --- a/src/backing/containers/py_item.rs +++ b/src/backing/containers/py_item.rs @@ -1,4 +1,7 @@ -use std::cmp::Ordering; +use std::{ + cmp::Ordering, + hash::{Hash, Hasher}, +}; use pyo3::prelude::*; @@ -44,3 +47,10 @@ impl PartialEq for PyItem { Python::with_gil(|py| self.0.bind(py).eq(other.0.bind(py)).unwrap_or(false)) } } + +impl Hash for PyItem { + fn hash(&self, state: &mut H) { + // TODO: Should warn or fail instead of defaulting to 0 + Python::with_gil(|py| self.0.bind(py).hash().unwrap_or(0).hash(state)) + } +} diff --git a/src/backing/indexed/binary_heap.rs b/src/backing/indexed/binary_heap.rs index e5ec273..50c3878 100644 --- a/src/backing/indexed/binary_heap.rs +++ b/src/backing/indexed/binary_heap.rs @@ -50,4 +50,8 @@ impl Indexed fn remove(&mut self, data: D) -> bool { todo!() } + + fn contains(&self, data: &D) -> bool { + todo!() + } } diff --git a/src/backing/indexed/mod.rs b/src/backing/indexed/mod.rs index 1a39cb0..baf2158 100644 --- a/src/backing/indexed/mod.rs +++ b/src/backing/indexed/mod.rs @@ -13,4 +13,6 @@ pub trait IndexedBacking bool; /// Remove an item from the queue fn remove(&mut self, data: D) -> bool; + /// Check if an item is already in the queue + fn contains(&self, data: &D) -> bool; } diff --git a/src/queue/indexed.rs b/src/queue/indexed.rs index c1f433f..382a697 100644 --- a/src/queue/indexed.rs +++ b/src/queue/indexed.rs @@ -1,38 +1,65 @@ use pyo3::{ - exceptions::{PyIndexError, PyStopIteration}, + exceptions::{PyIndexError, PyKeyError, PyRuntimeError, PyStopIteration, PyTypeError}, prelude::*, - types::PyType, + types::{PyDict, PyType}, }; -use crate::backing::indexed::IndexedBacking; +use crate::backing::{ + containers::{Pair, PyItem}, + indexed::{IndexedBacking, IndexedBinaryHeap}, +}; #[pyclass] pub struct IndexedQueue { - backing: Box, f64>>, + backing: Box>, } +// TODO: Too much of this is just too similar to the other queues +// Normally I'd solve this with inheritance, but I'm not so familiar with rust trait default implementations yet +// In my limited understanding they don't seem to play nice with the Pyo3 bindings #[pymethods] impl IndexedQueue { #[new] #[pyo3(signature = (items=None))] fn new(items: Option>) -> PyResult { if let Some(py_object) = items { - todo!() + Python::with_gil(|py| Self::from_any(py_object.bind(py))).and_then(|vec| { + Ok(Self { + backing: Box::new(IndexedBinaryHeap::from_iter(vec)), + }) + }) } else { - todo!() // TBD: Determine best way to make Python object hashable from Rust + Ok(Self { + backing: Box::new(IndexedBinaryHeap::new()), + }) } } - fn __setitem__(mut self_: PyRefMut<'_, Self>, key: Py, value: Py) { - todo!() + fn __setitem__(mut self_: PyRefMut<'_, Self>, key: Py, value: f64) -> PyResult<()> { + let data = PyItem::new(key); + if self_.backing.contains(&data) { + if self_.backing.update(data, value) { + Ok(()) + } else { + Err(PyErr::new::("Key could not be updated")) + } + } else { + Ok(self_.backing.add(Pair::new(data, value))) + } } - fn __delitem__(mut self_: PyRefMut<'_, Self>, key: Py) { - todo!() + fn __delitem__(mut self_: PyRefMut<'_, Self>, key: Py) -> PyResult<()> { + if self_.backing.remove(PyItem::new(key)) { + Ok(()) + } else { + Err(PyErr::new::( + "Key was not contained in queue", + )) + } } - fn __contains__(mut self_: PyRefMut<'_, Self>, key: Py) { - todo!() + fn __contains__(self_: PyRef<'_, Self>, key: Py) -> bool { + self_.backing.contains(&PyItem::new(key)) } /// Enables generic typing @@ -51,7 +78,7 @@ impl IndexedQueue { fn __next__(mut self_: PyRefMut<'_, Self>) -> PyResult> { if let Some(item) = self_.backing.pop() { - Ok(item.data()) + Ok(item.data().data()) } else { Err(PyErr::new::(())) } @@ -59,9 +86,55 @@ impl IndexedQueue { fn pop(mut self_: PyRefMut<'_, Self>) -> PyResult> { if let Some(item) = self_.backing.pop() { - Ok(item.data()) + Ok(item.data().data()) } else { Err(PyErr::new::(())) } } } + +impl<'py> IndexedQueue { + /// Tries to a Python object into a vector suitable for ingestion into the backing + fn from_any(object: &Bound<'py, PyAny>) -> PyResult>> { + if let Ok(vec) = object.extract::, f64)>>() { + Ok(Self::from_vec(vec)) + } else { + if object.is_instance_of::() { + if let Ok(dict) = object.downcast::() { + Self::from_dict(dict) + } else { + Err(PyErr::new::( + "Argument claimed to be a dict but wasn't", + )) + } + } else { + Err(PyErr::new::( + "Argument was not a properly-formed dict, list, or tuple", + )) + } + } + } + + /// Converts a vector of Python objects and priorities into a vector of items + fn from_vec(list: Vec<(Py, f64)>) -> Vec> { + list.into_iter() + .map(|(data, priority)| Pair::new(PyItem::new(data), priority)) + .collect() + } + + /// Converts a Python dictionary into a vector of items + fn from_dict(dict: &Bound<'py, PyDict>) -> PyResult>> { + if let Ok(items) = dict + .into_iter() + .map(|(data, priority)| match priority.extract::() { + Ok(value) => Ok(Pair::new(PyItem::new(data.unbind()), value)), + Err(err) => Err(err), + }) + .collect::, _>>() + { + Ok(items) + } else { + Err(PyErr::new::("Dict keys were not numbers")) + } + } +}