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 +36 -0
- iheap/_core.py +21 -0
- iheap/_iheap.pyi +155 -0
- iheap/_objheap.py +80 -0
- iheap/_pqdict.py +638 -0
- iheap/_pyheap.py +294 -0
- iheap/py.typed +0 -0
- iheap-0.2.0.dist-info/METADATA +259 -0
- iheap-0.2.0.dist-info/RECORD +11 -0
- iheap-0.2.0.dist-info/WHEEL +4 -0
- iheap-0.2.0.dist-info/licenses/LICENSE +21 -0
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)
|