From 0995e6db90b4eca57b30335c269677623e6c312e Mon Sep 17 00:00:00 2001 From: Michael Bradley Date: Wed, 8 Jan 2025 22:52:25 +1300 Subject: [PATCH 1/2] Get basic incomplete Python queue API working --- Cargo.toml | 2 +- src/backing/item.rs | 21 +++++++++------ src/backing/keyed/mod.rs | 4 ++- src/backing/pure/binary_heap.rs | 45 +++++++++++++++------------------ src/backing/pure/mod.rs | 4 +-- src/lib.rs | 12 +++------ src/queue/mod.rs | 1 + src/queue/pure.rs | 25 ++++++++++++++++++ 8 files changed, 69 insertions(+), 45 deletions(-) create mode 100644 src/queue/mod.rs create mode 100644 src/queue/pure.rs diff --git a/Cargo.toml b/Cargo.toml index 0da9dff..db86304 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,4 @@ name = "pyority_queue" crate-type = ["cdylib", "rlib"] [dependencies] -pyo3 = "0.23.3" +pyo3 = { version = "0.23.3", features = ["py-clone"] } diff --git a/src/backing/item.rs b/src/backing/item.rs index 424bf45..55ac901 100644 --- a/src/backing/item.rs +++ b/src/backing/item.rs @@ -2,12 +2,13 @@ use std::cmp::Ordering; /// Helper struct to associate an item with its priority #[derive(Debug, Clone, Copy)] -pub struct Item { +// I mean I guess P should be Ord but I want to use f64 so whatever +pub struct Item { data: D, priority: P, } -impl Item { +impl Item { /// Creates a new instance fn new(data: D, priority: P) -> Self { Self { data, priority } @@ -20,22 +21,26 @@ impl Item { } // The relevant Ord implementations are based just on the priority -impl Ord for Item { +impl Ord for Item { fn cmp(&self, other: &Self) -> Ordering { - self.priority.cmp(&other.priority) + // Yeah this is bad design + // My excuse is that i'm still learning Rust + self.priority + .partial_cmp(&other.priority) + .unwrap_or(Ordering::Equal) } } -impl PartialOrd for Item { +impl PartialOrd for Item { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + self.priority.partial_cmp(&other.priority) } } -impl PartialEq for Item { +impl PartialEq for Item { fn eq(&self, other: &Self) -> bool { self.priority == other.priority } } -impl Eq for Item {} +impl Eq for Item {} diff --git a/src/backing/keyed/mod.rs b/src/backing/keyed/mod.rs index 58077c5..e9bc311 100644 --- a/src/backing/keyed/mod.rs +++ b/src/backing/keyed/mod.rs @@ -2,7 +2,9 @@ use super::{item::Item, pure::PureBacking}; /// A data structure usable for backing a "keyed" queue -pub trait KeyedBacking: PureBacking> { +pub trait KeyedBacking: + PureBacking> +{ /// Update an item's priority fn update(data: D, priority: P) -> Result<(), ()>; /// Remove an item from the queue diff --git a/src/backing/pure/binary_heap.rs b/src/backing/pure/binary_heap.rs index 4d79d97..6569a66 100644 --- a/src/backing/pure/binary_heap.rs +++ b/src/backing/pure/binary_heap.rs @@ -4,7 +4,7 @@ use super::PureBacking; /// A binary min-heap backed by an array #[derive(Debug)] -pub struct BinaryHeap { +pub struct BinaryHeap { data: Vec, } @@ -33,18 +33,23 @@ impl fmt::Display for SiftError { /// Whether a sift operation succeeded type SiftResult = Result<(), SiftError>; -impl BinaryHeap { +impl BinaryHeap { + /// Instantiates a new (empty) binary heap + pub fn new() -> Self { + Self { data: vec![] } + } + /// Fix an index representing a node with valid children but that may violate the heap property compared to its immediate parent fn sift_up(&mut self, i: usize) -> SiftResult { if i == 0 { // Base case, at root so nothing to do Ok(()) - } else if let Some(child) = self.data.get(i).copied() { + } else if let Some(child) = self.data.get(i).cloned() { let parent_index = (i - 1) / 2; // Check if the heap property is violated if child < self.data[parent_index] { // Swap child with parent - self.data[i] = self.data[parent_index]; + self.data[i] = self.data[parent_index].clone(); self.data[parent_index] = child; // Repeat process with parent @@ -64,12 +69,12 @@ impl BinaryHeap { // Tried to sift a non-existent index Err(SiftError::new(i, self.data.len())) } else { - if let Some(first_child) = self.data.get(i * 2 + 1).copied() { + if let Some(first_child) = self.data.get(i * 2 + 1).cloned() { let smaller_child_index; let smaller_child; // Find the smallest child and its index - if let Some(second_child) = self.data.get(i * 2 + 2).copied() { + if let Some(second_child) = self.data.get(i * 2 + 2).cloned() { // Both children, use the smaller one if first_child < second_child { smaller_child = first_child; @@ -86,7 +91,7 @@ impl BinaryHeap { if smaller_child < self.data[i] { // Swap parent with child - self.data[smaller_child_index] = self.data[i]; + self.data[smaller_child_index] = self.data[i].clone(); self.data[i] = smaller_child; // Repeat process with child @@ -103,23 +108,19 @@ impl BinaryHeap { } } -impl FromIterator for BinaryHeap { +impl FromIterator for BinaryHeap { fn from_iter>(iter: U) -> Self { let mut this = Self { data: Vec::from_iter(iter), }; for i in (0..=(this.data.len() / 2)).rev() { - this.sift_down(i); + this.sift_down(i).expect("Index error during heapify"); } this } } -impl PureBacking for BinaryHeap { - fn new() -> Self { - Self { data: vec![] } - } - +impl PureBacking for BinaryHeap { fn add(&mut self, item: T) { // Append item self.data.push(item); @@ -132,22 +133,18 @@ impl PureBacking for BinaryHeap { // No extra processing 0 | 1 => self.data.pop(), _ => { - let last = self + // Get the original root + let root = self.data[0].clone(); + + // Move final item to the root and sift down to regain heap property + self.data[0] = self .data .pop() .expect("Vector claimed not to be empty but was"); - let root = self - .data - .get_mut(0) - .expect("Vector claimed to have multiple items but didn't"); - - // Move final item to the root and sift down to regain heap property - let best = *root; - *root = last; self.sift_down(0).unwrap(); // Return original root - Some(best) + Some(root) } } } diff --git a/src/backing/pure/mod.rs b/src/backing/pure/mod.rs index 9d82347..9c43b14 100644 --- a/src/backing/pure/mod.rs +++ b/src/backing/pure/mod.rs @@ -2,9 +2,7 @@ pub mod binary_heap; /// A data structure usable for backing a "pure" queue -pub trait PureBacking: FromIterator { - /// Instantiates a new data structure - fn new() -> Self; +pub trait PureBacking: Send + Sync { /// Places an item into the queue fn add(&mut self, item: T); /// Removes the item with minimum priority, if it exists diff --git a/src/lib.rs b/src/lib.rs index c2bf43e..64be5b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,12 @@ pub mod backing; +pub mod queue; use pyo3::prelude::*; +use queue::pure::PureQueue; -/// Formats the sum of two numbers as string. -#[pyfunction] -fn sum_as_string(a: usize, b: usize) -> PyResult { - Ok((a + b).to_string()) -} - -/// A Python module implemented in Rust. +/// Bindings for the Rust queue implementations #[pymodule] fn pyority_queue(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; + m.add_class::()?; Ok(()) } diff --git a/src/queue/mod.rs b/src/queue/mod.rs new file mode 100644 index 0000000..9ec004d --- /dev/null +++ b/src/queue/mod.rs @@ -0,0 +1 @@ +pub mod pure; diff --git a/src/queue/pure.rs b/src/queue/pure.rs new file mode 100644 index 0000000..4c03a26 --- /dev/null +++ b/src/queue/pure.rs @@ -0,0 +1,25 @@ +// A "pure" priority queue that supports duplicates, but not arbitrary deletions or weight updates +use crate::backing::{ + item::Item, + pure::{binary_heap::BinaryHeap, PureBacking}, +}; +use pyo3::prelude::*; + +#[pyclass] +pub struct PureQueue { + backing: Box, f64>>>, +} + +#[pymethods] +impl PureQueue { + #[new] + fn new() -> Self { + Self { + backing: Box::new(BinaryHeap::new()), + } + } + + fn __len__(self_: PyRef<'_, Self>) -> usize { + self_.backing.len() + } +} From b4ca806d768e94506565ff6e56d315bfb282f3c1 Mon Sep 17 00:00:00 2001 From: Michael Bradley Date: Wed, 8 Jan 2025 22:54:40 +1300 Subject: [PATCH 2/2] Update desired Python API, add mostly failing PureQueue tests for TDD --- main.py | 19 ------------- pyority_queue.pyi | 69 ++++++++++++++++++++++++++++++++++++--------- requirements.txt | 6 +++- tests/pure_queue.py | 43 ++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 33 deletions(-) delete mode 100755 main.py create mode 100644 tests/pure_queue.py diff --git a/main.py b/main.py deleted file mode 100755 index 9426ef3..0000000 --- a/main.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/env python -from pyority_queue import PureQueue, KeyedQueue - - -def main() -> None: - pure = PureQueue() - pure["second"] = 3.4 - pure["first"] = 1.2 - pure["third"] = 5.6 - print(*pure) - - keyed = KeyedQueue({"third": 5.6, "second": 3.4, "first": 7.8}) - del keyed["third"] - keyed["first"] = 1.2 - print(*keyed) - - -if __name__ == "__main__": - main() diff --git a/pyority_queue.pyi b/pyority_queue.pyi index d2893fd..e76f406 100644 --- a/pyority_queue.pyi +++ b/pyority_queue.pyi @@ -1,12 +1,14 @@ -from typing import Self +from abc import ABC +from typing import Hashable, Iterable, Self -class PureQueue[T]: - """A min-queue that allows duplicates and provides a minimal API allowing insertions and the ability to iterate over the queue in-order""" - def __init__(self, items: dict[T, float] | None = None) -> None: - """ - :param items: An optional mapping of items to priorities to initialize the queue with - """ +type Comparable = float | int + + +class Queue[Data](ABC): + """ + Common queue methods providing a minimal API for iteration. + """ def __len__(self) -> int: """ @@ -18,12 +20,44 @@ class PureQueue[T]: :return: An iterator over the queue """ - def __next__(self) -> T: + def __next__(self) -> Data: """ :return: The next item in the queue """ - def __setitem__(self, key: T, value: float) -> None: + +class PureQueue[Data: Comparable](Queue[Data]): + """ + A min-queue that directly orders its items, allowing duplicates. + """ + + def __init__(self, items: Iterable[Data] | None = None) -> None: + """ + :param items: An optional list of priorities with which to initialize the queue + """ + + def pop(self) -> Data: + """ + :return: The next item in the queue + """ + + def insert(self, item: Data) -> None: + """ + :param item: Item to insert into the queue + """ + + +class KeyedQueue[Data, Priority: Comparable](Queue[Data]): + """ + A min-queue that allows arbitrary data associated with some priority, allowing duplicates of both data and priority. + """ + + def __init__(self, items: dict[Data, Priority] | Iterable[tuple[Data, Priority]] | None = None) -> None: + """ + :param items: An optional mapping/list of items to priorities with which to initialize the queue + """ + + def __setitem__(self, key: Data, value: Priority) -> None: """ Inserts a new item into the queue :param key: The item to insert @@ -31,16 +65,25 @@ class PureQueue[T]: """ -class KeyedQueue[T](PureQueue[T]): - """A min-queue that disallows duplicates but offers the ability to update priorities and delete arbitrary items""" - def __setitem__(self, key: T, value: float) -> None: +class IndexedQueue[Data: Hashable, Priority: Comparable](Queue[Data]): + """ + A min-queue that allows arbitrary data associated with some priority. + Disallows duplicate data but offers the ability to update priorities and delete arbitrary items. + """ + + def __init__(self, items: dict[Data, Priority] | Iterable[tuple[Data, Priority]] | None = None) -> None: + """ + :param items: An optional mapping/list of items to priorities with which to initialize the queue + """ + + def __setitem__(self, key: Data, value: Priority) -> None: """ Inserts an item into the queue, or updates its priority if it already exists :param key: The item to insert or update :param value: The priority of the item """ - def __delitem__(self, key: T) -> None: + def __delitem__(self, key: Data) -> None: """ :param key: The item to delete from the queue """ diff --git a/requirements.txt b/requirements.txt index 880f18f..ba45a50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ +iniconfig==2.0.0 maturin==1.8.1 --e git+ssh://git@git.mmbradley.ca:222/MichaelBradley/pyority_queue.git@a09b71cdb32e2637971629623200cd639f5dcccb#egg=pyority_queue +packaging==24.2 +pluggy==1.5.0 +-e git+ssh://git@git.mmbradley.ca:222/MichaelBradley/pyority_queue.git@661e1d220ae71002d66d2ba2d3dfddfe4ff1035d#egg=pyority_queue +pytest==8.3.4 diff --git a/tests/pure_queue.py b/tests/pure_queue.py new file mode 100644 index 0000000..ce32069 --- /dev/null +++ b/tests/pure_queue.py @@ -0,0 +1,43 @@ +import pytest + +from pyority_queue import PureQueue + + +type PureQueueInitializer = list[int | float] | tuple[int | float, ...] + + +def test_empty_creation(): + queue = PureQueue() + assert len(queue) == 0 + + +@pytest.mark.parametrize("items", ([], [0, 1, 2], (0, 1, 2), (0.0, 1.0, 2.0), range(100))) +def test_creation(items: PureQueueInitializer): + queue = PureQueue() + assert len(queue) == len(items) + +@pytest.mark.parametrize("items", ([], (0,), (-1, 3), range(100))) +def test_iteration(items: PureQueueInitializer): + queue = PureQueue(items) + assert len(list(queue)) == len(items) + + +@pytest.mark.parametrize("items", ([], (-3, 5), (3.0, 2.0, 1.0), (3, 6, 8, 5, 7, 4, 2, 0), range(100))) +def test_sorting(items: PureQueueInitializer): + queue = PureQueue(items) + assert list(queue) == sorted(items) + + +def test_insertion(): + queue = PureQueue[int]((4, 2, 8, 6)) + queue.insert(7) + queue.insert(0) + queue.insert(3) + assert list(queue) == [0, 2, 3, 4, 6, 7, 8] + + +def test_duplicates(): + queue = PureQueue[int]((0, 0, 0, 5, 5)) + queue.insert(3) + queue.insert(3) + assert list(queue) == [0, 0, 0, 3, 3, 5, 5]