From ebe77ea419567b4d0fe21d37b9911a2cc32e0f63 Mon Sep 17 00:00:00 2001 From: Claudio Usai Date: Sat, 18 Apr 2026 00:39:41 +0200 Subject: [PATCH 1/2] calendar_queue: fix missing heapify call after elements deletion --- src/calendar_queue/calendar_queue.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/calendar_queue/calendar_queue.py b/src/calendar_queue/calendar_queue.py index 4aba06b..c80a25a 100644 --- a/src/calendar_queue/calendar_queue.py +++ b/src/calendar_queue/calendar_queue.py @@ -8,6 +8,7 @@ from asyncio import Queue, QueueFull, TimerHandle from heapq import heappop as heap_pop from heapq import heappush as heap_push +from heapq import heapify import math import sys from time import time @@ -237,6 +238,10 @@ def delete_items( if selector(item): del_items.append(self._queue.pop(q_len - 1 - i)) + # Restore heap invariant after arbitrary deletions. + if del_items: + heapify(self._queue) + self._update_timer() return del_items From 47ad811625853b56c3167fd256918b1fc4240552 Mon Sep 17 00:00:00 2001 From: Claudio Usai Date: Sat, 18 Apr 2026 00:40:10 +0200 Subject: [PATCH 2/2] tests: add tests case for ensuring order stays correct after deletion --- tests/test_calendar_queue.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_calendar_queue.py b/tests/test_calendar_queue.py index 973b03c..f461210 100644 --- a/tests/test_calendar_queue.py +++ b/tests/test_calendar_queue.py @@ -111,6 +111,41 @@ async def test_delete_items(): assert cq.qsize() == 5 +@pytest.mark.asyncio +async def test_delete_restores_heap_property(): + """Ensure deleting arbitrary items restores the heap property so + subsequent pops return items in increasing timestamp order. + """ + + cq = CalendarQueue() + + base_ts = time() + + items = [ + (base_ts + 10, "a"), + (base_ts + 20, "b"), + (base_ts + 30, "c"), + (base_ts + 40, "d"), + ] + + for item in items: + cq.put_nowait(item) + + # remove the middle element + deleted = cq.delete_items(lambda x: x[1] == "b") + + assert len(deleted) == 1 and deleted[0] == (base_ts + 20, "b") + + popped = [] + while cq.qsize() > 0: + popped.append(cq.get_nowait()) + + assert [p[1] for p in popped] == ["a", "c", "d"] + assert popped[0][0] == pytest.approx(base_ts + 10, abs=ABS_TOLERANCE) + assert popped[1][0] == pytest.approx(base_ts + 30, abs=ABS_TOLERANCE) + assert popped[2][0] == pytest.approx(base_ts + 40, abs=ABS_TOLERANCE) + + @pytest.mark.asyncio async def test_far_schedule(): """Test putting an event scheduled far in time.