iheap 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
iheap/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """Indexed priority queues with a Rust core.
2
+
3
+ * :class:`IndexedHeap` -- low-level heap of float priorities addressed by
4
+ integer handles. Holds no Python objects.
5
+ * :class:`ObjectHeap` -- the same, for any comparable priorities (strings,
6
+ tuples, ...), in pure Python.
7
+ * :class:`PQDict` -- ``pqdict``-style mapping of keys to values ordered by
8
+ priority. Keys and values live in Python; only priorities reach the core.
9
+
10
+ :data:`NATIVE` tells whether the Rust core is in use. Where no prebuilt wheel
11
+ exists for the interpreter (for example free-threaded CPython 3.13), a
12
+ pure-Python core with the same API is used instead.
13
+ """
14
+
15
+ from importlib.metadata import PackageNotFoundError
16
+ from importlib.metadata import version as _version
17
+
18
+ from ._core import NATIVE, IndexedHeap
19
+ from ._objheap import ObjectHeap
20
+ from ._pqdict import PQDict, maxpq, minpq, nlargest, nsmallest
21
+
22
+ __all__ = [
23
+ "NATIVE",
24
+ "IndexedHeap",
25
+ "ObjectHeap",
26
+ "PQDict",
27
+ "maxpq",
28
+ "minpq",
29
+ "nlargest",
30
+ "nsmallest",
31
+ ]
32
+
33
+ try:
34
+ __version__ = _version("iheap")
35
+ except PackageNotFoundError: # running from a source checkout
36
+ __version__ = "0+unknown"
iheap/_core.py ADDED
@@ -0,0 +1,21 @@
1
+ """Selects the ``IndexedHeap`` implementation.
2
+
3
+ The native extension (Rust) is used whenever it can be imported. Otherwise,
4
+ or when the environment variable ``IHEAP_PURE_PYTHON`` is set to a non-empty
5
+ value, the pure-Python implementation with the same API is used.
6
+ """
7
+
8
+ import os
9
+
10
+ from ._pyheap import IndexedHeap
11
+
12
+ NATIVE = False
13
+ if not os.environ.get("IHEAP_PURE_PYTHON"):
14
+ try:
15
+ from ._iheap import IndexedHeap # type: ignore[assignment] # noqa: F811
16
+ except ImportError: # no native wheel for this interpreter, e.g. CPython 3.13t
17
+ pass
18
+ else:
19
+ NATIVE = True
20
+
21
+ __all__ = ["NATIVE", "IndexedHeap"]
iheap/_iheap.pyi ADDED
@@ -0,0 +1,155 @@
1
+ from typing import Any, final
2
+
3
+ @final
4
+ class IndexedHeap:
5
+ """Native indexed priority queue of float priorities addressed by handles.
6
+
7
+ [`push`][iheap.IndexedHeap.push] returns an integer *handle* that stays
8
+ valid until its entry is popped or removed, and lets you update or remove
9
+ that entry in ``O(log n)``. Handles are small non-negative integers below
10
+ [`handle_bound`][iheap.IndexedHeap.handle_bound] and are recycled, so you
11
+ can keep per-entry data in a plain list indexed by handle. The heap
12
+ itself holds no Python objects.
13
+
14
+ Priorities must be convertible to ``float`` and not NaN. Integers are
15
+ exact up to 2**53.
16
+
17
+ Thread safety: a heap may be shared between threads, including on
18
+ free-threaded Python. Each call runs under an internal lock, so
19
+ concurrent calls are serialized; sequences of calls are not atomic.
20
+
21
+ Args:
22
+ reverse: If true, the largest priority is on top (max-heap).
23
+ capacity: Number of entries to pre-allocate room for.
24
+
25
+ Examples:
26
+ >>> heap = IndexedHeap()
27
+ >>> a, b = heap.push(3.0), heap.push(1.0)
28
+ >>> heap.peek() == (b, 1.0)
29
+ True
30
+ >>> heap.update(a, 0.5)
31
+ 3.0
32
+ >>> heap.pop() == (a, 0.5)
33
+ True
34
+ """
35
+
36
+ def __init__(self, *, reverse: bool = False, capacity: int = 0) -> None: ...
37
+ @property
38
+ def reverse(self) -> bool:
39
+ """``True`` for a max-heap."""
40
+ @property
41
+ def handle_bound(self) -> int:
42
+ """Exclusive upper bound of every live handle (the handle high-water mark)."""
43
+ def __len__(self) -> int:
44
+ """Number of live entries."""
45
+ def __bool__(self) -> bool: ...
46
+ def __contains__(self, handle: int) -> bool:
47
+ """Whether ``handle`` refers to a live entry."""
48
+ def push(self, priority: float) -> int:
49
+ """Insert an entry in ``O(log n)``.
50
+
51
+ Args:
52
+ priority: Priority of the new entry.
53
+
54
+ Returns:
55
+ The handle of the new entry.
56
+
57
+ Raises:
58
+ ValueError: ``priority`` is NaN.
59
+ TypeError: ``priority`` is not convertible to ``float``.
60
+ """
61
+ def push_many(self, priorities: list[float]) -> list[int]:
62
+ """Insert many entries at once.
63
+
64
+ Large batches are appended and re-heapified in ``O(n)``. Atomic: if any
65
+ priority is invalid, nothing is inserted. On an empty heap after
66
+ construction or [`clear`][iheap.IndexedHeap.clear], the returned
67
+ handles are ``0, 1, ..., len(priorities) - 1``.
68
+
69
+ Args:
70
+ priorities: Priorities of the new entries.
71
+
72
+ Returns:
73
+ Handles of the new entries, in input order.
74
+
75
+ Raises:
76
+ ValueError: A priority is NaN.
77
+ """
78
+ def peek(self) -> tuple[int, float] | None:
79
+ """Return ``(handle, priority)`` of the top entry, or ``None`` if empty."""
80
+ def pop(self) -> tuple[int, float]:
81
+ """Remove and return the top entry in ``O(log n)``.
82
+
83
+ Returns:
84
+ ``(handle, priority)`` of the removed entry. The handle becomes
85
+ invalid and may be reused by later pushes.
86
+
87
+ Raises:
88
+ IndexError: The heap is empty.
89
+ """
90
+ def push_pop(self, priority: float) -> tuple[int, float] | None:
91
+ """Fused push-then-pop in a single pass (like ``heapq.heappushpop``).
92
+
93
+ Args:
94
+ priority: Priority of the new entry.
95
+
96
+ Returns:
97
+ ``None`` if the new entry would be popped right away (the heap is
98
+ empty or its top does not strictly precede ``priority``); the heap
99
+ is then unchanged. Otherwise the top entry is popped, **its handle
100
+ is reassigned to the new entry**, and ``(handle, popped_priority)``
101
+ is returned.
102
+
103
+ Raises:
104
+ ValueError: ``priority`` is NaN.
105
+ """
106
+ def pop_many(self, n: int) -> list[tuple[int, float]]:
107
+ """Pop up to ``n`` entries.
108
+
109
+ Returns:
110
+ ``(handle, priority)`` pairs in priority order.
111
+ """
112
+ def priority(self, handle: int) -> float:
113
+ """Return the priority of ``handle``.
114
+
115
+ Raises:
116
+ KeyError: ``handle`` is not live.
117
+ """
118
+ def update(self, handle: int, priority: float) -> float:
119
+ """Change the priority of ``handle`` in ``O(log n)``.
120
+
121
+ Returns:
122
+ The previous priority.
123
+
124
+ Raises:
125
+ KeyError: ``handle`` is not live.
126
+ ValueError: ``priority`` is NaN.
127
+ """
128
+ def update_many(self, handles: list[int], priorities: list[float]) -> None:
129
+ """Change many priorities at once.
130
+
131
+ Re-heapifies in ``O(n)`` when that is cheaper than individual updates.
132
+ Atomic: on error nothing changes. If a handle appears more than once,
133
+ the last priority wins.
134
+
135
+ Raises:
136
+ KeyError: A handle is not live.
137
+ ValueError: The lists differ in length, or a priority is NaN.
138
+ """
139
+ def remove(self, handle: int) -> float:
140
+ """Remove ``handle`` in ``O(log n)``.
141
+
142
+ Returns:
143
+ The priority of the removed entry.
144
+
145
+ Raises:
146
+ KeyError: ``handle`` is not live.
147
+ """
148
+ def clear(self) -> None:
149
+ """Remove every entry. Handle numbering restarts at 0."""
150
+ def items(self) -> list[tuple[int, float]]:
151
+ """Return live ``(handle, priority)`` pairs in handle order (not priority order)."""
152
+ def copy(self) -> IndexedHeap:
153
+ """Return an independent copy, handles included."""
154
+ def __copy__(self) -> IndexedHeap: ...
155
+ def __deepcopy__(self, memo: Any) -> IndexedHeap: ...
iheap/_objheap.py ADDED
@@ -0,0 +1,80 @@
1
+ """``ObjectHeap``: an indexed heap over arbitrary comparable priorities.
2
+
3
+ The native and float cores only order numbers. ``ObjectHeap`` orders any
4
+ priorities that support ``<`` among themselves: strings, tuples (e.g.
5
+ ``(priority, sequence)`` for FIFO ties), dates, ``Decimal`` exactly, and so
6
+ on. It reuses the pure-Python heap, which only ever compares keys with ``<``
7
+ and ``>=``; a max-heap wraps keys in :class:`_Reversed`, which inverts both.
8
+ It runs at pure-Python speed.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ from ._pyheap import IndexedHeap as _PyHeap
16
+
17
+
18
+ class _Reversed:
19
+ """Key wrapper that inverts the order of the wrapped priority."""
20
+
21
+ __slots__ = ("value",)
22
+
23
+ def __init__(self, value: Any) -> None:
24
+ self.value = value
25
+
26
+ def __lt__(self, other: _Reversed) -> bool:
27
+ return other.value < self.value
28
+
29
+ def __ge__(self, other: _Reversed) -> bool:
30
+ return not other.value < self.value
31
+
32
+
33
+ class ObjectHeap(_PyHeap):
34
+ """Indexed heap whose priorities are any mutually comparable objects.
35
+
36
+ Same API, handle semantics and thread safety as
37
+ [`IndexedHeap`][iheap.IndexedHeap], but priorities are stored as given
38
+ (not converted to ``float``) and ordered with ``<``. They must be totally
39
+ ordered and comparable with each other.
40
+
41
+ Raises:
42
+ TypeError: A new priority cannot be compared with those in the heap
43
+ (checked against the current top before anything changes).
44
+ ValueError: A priority is NaN-like (``p != p``).
45
+
46
+ Examples:
47
+ >>> heap = ObjectHeap()
48
+ >>> h = heap.push("banana")
49
+ >>> _ = heap.push_many(["cherry", "apple"])
50
+ >>> heap.pop()[1]
51
+ 'apple'
52
+ >>> heap.update(h, "zucchini")
53
+ 'banana'
54
+ >>> heap.peek()[1]
55
+ 'cherry'
56
+ """
57
+
58
+ __slots__ = ()
59
+
60
+ def _key(self, priority: Any) -> Any:
61
+ if priority != priority:
62
+ raise ValueError("priority must not be NaN")
63
+ return _Reversed(priority) if self._sign < 0 else priority
64
+
65
+ def _decode(self, key: Any) -> Any:
66
+ return key.value if self._sign < 0 else key
67
+
68
+ def _check(self, keys: list[Any]) -> None:
69
+ # Mixing incomparable types (say `str` and `int`) raises inside a
70
+ # sift, half-way through moving entries. Comparing every new key with
71
+ # a reference first (the current top, else the first new key) makes
72
+ # such operations fail with no effect.
73
+ if self._heap:
74
+ reference = self._keys[self._heap[0]]
75
+ elif keys:
76
+ reference = keys[0]
77
+ else:
78
+ return
79
+ for key in keys:
80
+ bool(key < reference)