From 07d8dbb5a2b5d510bd3460baa36af6eda314383a Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Tue, 27 Jun 2023 10:03:40 -0400 Subject: [PATCH 1/9] feat: Add default arg to top. maint: Updates for dropping py27. --- pqdict/__init__.py | 189 +++++++++++++++++++++------------------------ 1 file changed, 88 insertions(+), 101 deletions(-) diff --git a/pqdict/__init__.py b/pqdict/__init__.py index 0fe68a6..02da391 100644 --- a/pqdict/__init__.py +++ b/pqdict/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Priority Queue Dictionary (pqdict) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -30,25 +29,19 @@ Documentation at . -:copyright: (c) 2012-2015 by Nezar Abdennur. +:copyright: (c) 2012-2023 by Nezar Abdennur. :license: MIT, see LICENSE for more details. """ -try: - from collections.abc import MutableMapping as _MutableMapping -except ImportError: - # 2.7 compatability - from collections import MutableMapping as _MutableMapping # type: ignore - -from six.moves import range +from collections.abc import MutableMapping from operator import lt, gt __version__ = "1.2.1" -__all__ = ["pqdict", "PQDict", "minpq", "maxpq", "nlargest", "nsmallest"] +__all__ = ["pqdict", "nlargest", "nsmallest"] -class _Node(object): +class Node: __slots__ = ("key", "value", "prio") def __init__(self, key, value, prio): @@ -57,34 +50,37 @@ def __init__(self, key, value, prio): self.prio = prio def __repr__(self): - return self.__class__.__name__ + "(%s, %s, %s)" % ( - repr(self.key), - repr(self.value), - repr(self.prio), - ) + return f"{self.__class__.__name__}({self.key}, {self.value}, {self.prio})" -class pqdict(_MutableMapping): # pyright: ignore # Argument to class must be a base class (reportGeneralTypeIssues +class pqdict(MutableMapping): """ A collection that maps hashable objects (keys) to priority-determining values. The mapping is mutable so items may be added, removed and have their priority level updated. + The default behavior is that of a min-priority queue, i.e. the item with + the *smallest* priority value is given *highest* priority. This behavior + can be reversed by specifying ``reverse=True`` or by providing a custom + comparator function via the ``precedes`` keyword argument. + Parameters ---------- data : mapping or iterable, optional Input data, e.g. a dictionary or a sequence of items. key : callable, optional Optional priority key function to transform values into priority keys - for sorting. By default, the values are not transformed. - reverse : bool, optional + for comparison. By default, the values are used directly as priority + keys and are not transformed. + reverse : bool, optional [default: ``False``] If ``True``, *larger* priority keys give items *higher* priority. Default is ``False``. - precedes : callable, optional (overrides ``reverse``) + precedes : callable, optional (overrides ``reverse``) [default: ``operator.lt``] Function that determines precedence of a pair of priority keys. The default comparator is ``operator.lt``, meaning *smaller* priority keys - give items *higher* priority. - + give items *higher* priority. The callable must have the form + ``precedes(prio1, prio2) -> bool`` and return ``True`` if ``prio1`` + has higher priority than ``prio2``. """ def __init__(self, data=None, key=None, reverse=False, precedes=lt): @@ -97,15 +93,13 @@ def __init__(self, data=None, key=None, reverse=False, precedes=lt): if key is None or callable(key): self._keyfn = key else: - raise ValueError( - "`key` function must be a callable; got {}".format(type(key)) - ) + raise ValueError(f"`key` function must be a callable; got {key}") if callable(precedes): self._precedes = precedes else: raise ValueError( - "`precedes` function must be a callable; got {}".format(type(precedes)) + f"`precedes` function must be a callable; got {precedes}" ) # The heap @@ -128,44 +122,47 @@ def keyfn(self): return self._keyfn if self._keyfn is not None else lambda x: x def __repr__(self): - things = ", ".join( - ["%s: %s" % (repr(node.key), repr(node.value)) for node in self._heap] - ) - return self.__class__.__name__ + "({" + things + "})" + things = ", ".join([f"{node.key}: {node.value}" for node in self._heap]) + return f"{self.__class__.__name__}({things})" + + @classmethod + def minpq(cls, *args, **kwargs): + return cls(dict(*args, **kwargs), precedes=lt) + + @classmethod + def maxpq(cls, *args, **kwargs): + return cls(dict(*args, **kwargs), precedes=gt) ############ # dict API # ############ __marker = object() - __eq__ = _MutableMapping.__eq__ - __ne__ = _MutableMapping.__ne__ - keys = _MutableMapping.keys - values = _MutableMapping.values - items = _MutableMapping.items - get = _MutableMapping.get - clear = _MutableMapping.clear - update = _MutableMapping.update - setdefault = _MutableMapping.setdefault + # __eq__ = MutableMapping.__eq__ + # __ne__ = MutableMapping.__ne__ + # keys = MutableMapping.keys + # values = MutableMapping.values + # items = MutableMapping.items + # get = MutableMapping.get + # clear = MutableMapping.clear + # update = MutableMapping.update + # setdefault = MutableMapping.setdefault @classmethod def fromkeys(cls, iterable, value, **kwargs): """ Return a new pqict mapping keys from an iterable to the same value. - """ return cls(((k, value) for k in iterable), **kwargs) def __len__(self): """ Return number of items in the pqdict. - """ return len(self._heap) def __contains__(self, key): """ Return ``True`` if key is in the pqdict. - """ return key in self._position @@ -173,7 +170,6 @@ def __iter__(self): """ Return an iterator over the keys of the pqdict. The order of iteration is arbitrary! Use ``popkeys`` to iterate over keys in priority order. - """ for node in self._heap: yield node.key @@ -182,14 +178,13 @@ def __getitem__(self, key): """ Return the priority value of ``key``. Raises a ``KeyError`` if not in the pqdict. - """ return self._heap[self._position[key]].value # raises KeyError def __setitem__(self, key, value): """ - Assign a priority value to ``key``. - + Assign a priority value to ``key``. If ``key`` is already in the + pqdict, its priority value is updated. """ heap = self._heap position = self._position @@ -200,7 +195,7 @@ def __setitem__(self, key, value): # add n = len(heap) prio = keygen(value) if keygen is not None else value - heap.append(_Node(key, value, prio)) + heap.append(Node(key, value, prio)) position[key] = n self._swim(n) else: @@ -213,7 +208,6 @@ def __setitem__(self, key, value): def __delitem__(self, key): """ Remove item. Raises a ``KeyError`` if key is not in the pqdict. - """ heap = self._heap position = self._position @@ -231,32 +225,38 @@ def __delitem__(self, key): def copy(self): """ Return a shallow copy of a pqdict. - """ other = self.__class__(key=self._keyfn, precedes=self._precedes) other._position = self._position.copy() - other._heap = [_Node(node.key, node.value, node.prio) for node in self._heap] + other._heap = [Node(node.key, node.value, node.prio) for node in self._heap] return other def pop(self, key=__marker, default=__marker): """ - If ``key`` is in the pqdict, remove it and return its priority value, - else return ``default``. If ``default`` is not provided and ``key`` is - not in the pqdict, raise a ``KeyError``. - - If ``key`` is not provided, remove the top item and return its key, or - raise ``KeyError`` if the pqdict is empty. + Dict-style pop: + If ``key`` is provided and is in the pqdict, remove the item and return + its priority value. If ``key`` is not in the pqdict, return ``default`` + if provided, otherwise raise a ``KeyError``. + Priority queue-style pop: + If ``key`` is not provided, remove the top item and return its key. If + the pqdict is empty, return ``default`` if provided, otherwise raise a + ``KeyError``. """ heap = self._heap position = self._position + # pq semantics: remove and return top *key* (value is discarded) if key is self.__marker: if not heap: - raise KeyError("pqdict is empty") + if default is self.__marker: + raise KeyError("pqdict is empty") + else: + return default key = heap[0].key del self[key] return key + # dict semantics: remove and return *value* mapped from key try: pos = position.pop(key) # raises KeyError @@ -278,23 +278,40 @@ def pop(self, key=__marker, default=__marker): ###################### # Priority Queue API # ###################### - def top(self): + def top(self, default=__marker): """ - Return the key of the item with highest priority. Raises ``KeyError`` - if pqdict is empty. - + Return the key of the item with highest priority. If ``default`` is + provided and pqdict is empty, then return``default``, otherwise raise + ``KeyError``. """ try: node = self._heap[0] except IndexError: - raise KeyError("pqdict is empty") + if default is self.__marker: + raise KeyError("pqdict is empty") + else: + return default return node.key + def topvalue(self, default=__marker): + """ + Return the value of the item with highest priority. If ``default`` is + provided and pqdict is empty, then return``default``, otherwise raise + ``KeyError``. + """ + try: + node = self._heap[0] + except IndexError: + if default is self.__marker: + raise KeyError("pqdict is empty") + else: + return default + return node.value + def popitem(self): """ Remove and return the item with highest priority. Raises ``KeyError`` if pqdict is empty. - """ heap = self._heap position = self._position @@ -318,7 +335,6 @@ def topitem(self): """ Return the item with highest priority. Raises ``KeyError`` if pqdict is empty. - """ try: node = self._heap[0] @@ -329,10 +345,9 @@ def topitem(self): def additem(self, key, value): """ Add a new item. Raises ``KeyError`` if key is already in the pqdict. - """ if key in self._position: - raise KeyError("%s is already in the queue" % repr(key)) + raise KeyError(f"{key} is already in the queue") self[key] = value def pushpopitem(self, key, value): @@ -340,15 +355,14 @@ def pushpopitem(self, key, value): Equivalent to inserting a new item followed by removing the top priority item, but faster. Raises ``KeyError`` if the new key is already in the pqdict. - """ heap = self._heap position = self._position precedes = self._precedes prio = self._keyfn(value) if self._keyfn else value - node = _Node(key, value, prio) + node = Node(key, value, prio) if key in self: - raise KeyError("%s is already in the queue" % repr(key)) + raise KeyError(f"{key} is already in the queue") if heap and precedes(heap[0].prio, node.prio): node, heap[0] = heap[0], node position[key] = 0 @@ -360,7 +374,6 @@ def updateitem(self, key, new_val): """ Update the priority value of an existing item. Raises ``KeyError`` if key is not in the pqdict. - """ if key not in self._position: raise KeyError(key) @@ -371,12 +384,11 @@ def replace_key(self, key, new_key): Replace the key of an existing heap node in place. Raises ``KeyError`` if the key to replace does not exist or if the new key is already in the pqdict. - """ heap = self._heap position = self._position if new_key in self: - raise KeyError("%s is already in the queue" % repr(new_key)) + raise KeyError(f"{new_key} is already in the queue") pos = position.pop(key) # raises appropriate KeyError position[new_key] = pos heap[pos].key = new_key @@ -385,7 +397,6 @@ def swap_priority(self, key1, key2): """ Fast way to swap the priority level of two items in the pqdict. Raises ``KeyError`` if either key does not exist. - """ heap = self._heap position = self._position @@ -398,7 +409,6 @@ def swap_priority(self, key1, key2): def popkeys(self): """ Heapsort iterator over keys in descending order of priority level. - """ try: while True: @@ -409,7 +419,6 @@ def popkeys(self): def popvalues(self): """ Heapsort iterator over values in descending order of priority level. - """ try: while True: @@ -420,7 +429,6 @@ def popvalues(self): def popitems(self): """ Heapsort iterator over items in descending order of priority level. - """ try: while True: @@ -432,7 +440,6 @@ def heapify(self, key=__marker): """ Repair a broken heap. If the state of an item's priority value changes you can re-sort the relevant item only by providing ``key``. - """ if key is self.__marker: n = len(self._heap) @@ -525,30 +532,12 @@ def _swim(self, pos, top=0): heap[pos] = node position[node.key] = pos - def topvalue(self, default=__marker): - """ - :param default: if provided, then returning it when priority queue is empty. - If default is not provided and priority queue is empty, - then raising KeyError (as returned by ".topitem()") - :return: the top value from priority queue - """ - try: - _, v = self.topitem() - return v - except KeyError: - if default is self.__marker: - raise # no default value provided so raising the KeyError - else: - return default ########### # Aliases # ########### -PQDict = pqdict # deprecated - - def minpq(*args, **kwargs): return pqdict(dict(*args, **kwargs), precedes=lt) @@ -584,8 +573,7 @@ def nlargest(n, mapping, key=None): Returns ------- - list of up to n keys from the mapping - + list of up to n keys from the mapping associated with the largest values """ try: it = mapping.iteritems() @@ -593,7 +581,7 @@ def nlargest(n, mapping, key=None): it = iter(mapping.items()) pq = pqdict(key=key, precedes=lt) try: - for i in range(n): + for _ in range(n): pq.additem(*next(it)) except StopIteration: pass @@ -629,8 +617,7 @@ def nsmallest(n, mapping, key=None): Returns ------- - list of up to n keys from the mapping - + list of up to n keys from the mapping associated with the smallest values """ try: it = mapping.iteritems() @@ -638,7 +625,7 @@ def nsmallest(n, mapping, key=None): it = iter(mapping.items()) pq = pqdict(key=key, precedes=gt) try: - for i in range(n): + for _ in range(n): pq.additem(*next(it)) except StopIteration: pass From 91d7bb9046bfdfbe5ada328386bd0136afe77c2f Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Tue, 27 Jun 2023 10:03:54 -0400 Subject: [PATCH 2/9] maint: Update type stub --- pqdict/__init__.pyi | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pqdict/__init__.pyi b/pqdict/__init__.pyi index af27854..a2b4f6d 100644 --- a/pqdict/__init__.pyi +++ b/pqdict/__init__.pyi @@ -3,6 +3,7 @@ from collections.abc import MutableMapping K = TypeVar("K") V = TypeVar("V") +_T = TypeVar("_T") class pqdict(MutableMapping[K, V]): def __init__( @@ -17,6 +18,10 @@ class pqdict(MutableMapping[K, V]): @property def keyfn(self) -> Callable[[V], Any] | None: ... @classmethod + def minpq(cls, *args: Any, **kwargs: Any) -> Self: ... + @classmethod + def maxpq(cls, *args: Any, **kwargs: Any) -> Self: ... + @classmethod def fromkeys(cls, iterable: Iterable[Any], value: Any, **kwargs: Any) -> Self: ... def __len__(self) -> int: ... def __contains__(self, key: object) -> bool: ... @@ -25,7 +30,9 @@ class pqdict(MutableMapping[K, V]): def __setitem__(self, key: K, value: V) -> None: ... def __delitem__(self, key: K) -> None: ... def copy(self) -> Self: ... - def top(self) -> K: ... + def pop(self, key: K, default: V) -> V: ... + def top(self, default: V = ...) -> K: ... + def topvalue(self, default: V = ...) -> V: ... def popitem(self) -> tuple[K, V]: ... def topitem(self) -> tuple[K, V]: ... def additem(self, key: K, value: V) -> None: ... @@ -37,9 +44,7 @@ class pqdict(MutableMapping[K, V]): def popvalues(self) -> Iterator[V]: ... def popitems(self) -> Iterator[tuple[K, V]]: ... def heapify(self, key: K = ...) -> None: ... - def topvalue(self, default: V = ...) -> V: ... -PQDict = pqdict def minpq(*args: Any, **kwargs: Any) -> pqdict[Any, Any]: ... def maxpq(*args: Any, **kwargs: Any) -> pqdict[Any, Any]: ... From 457be5c9aef1f317d51470282df978896d7af3ce Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Wed, 28 Jun 2023 09:59:47 -0400 Subject: [PATCH 3/9] typing: Remove stub and inline types --- pqdict/__init__.py | 128 ++++++++++++++++++++++++++------------------ pqdict/__init__.pyi | 52 ------------------ 2 files changed, 75 insertions(+), 105 deletions(-) delete mode 100644 pqdict/__init__.pyi diff --git a/pqdict/__init__.py b/pqdict/__init__.py index 02da391..001361d 100644 --- a/pqdict/__init__.py +++ b/pqdict/__init__.py @@ -33,23 +33,41 @@ :license: MIT, see LICENSE for more details. """ -from collections.abc import MutableMapping -from operator import lt, gt - +from collections.abc import Iterable, Mapping, MutableMapping +from operator import gt, lt +from typing import ( + Any, + Callable, + Dict, + Iterable, + Iterator, + List, + Mapping, + Optional, + Self, + Tuple, + Union, +) __version__ = "1.2.1" __all__ = ["pqdict", "nlargest", "nsmallest"] +DictInputs = Union[Mapping[Any, Any], Iterable[Tuple[Any, Any]]] + class Node: __slots__ = ("key", "value", "prio") - def __init__(self, key, value, prio): + key: Any + value: Any + prio: Any + + def __init__(self, key: Any, value: Any, prio: Any) -> None: self.key = key self.value = value self.prio = prio - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({self.key}, {self.value}, {self.prio})" @@ -82,8 +100,16 @@ class pqdict(MutableMapping): ``precedes(prio1, prio2) -> bool`` and return ``True`` if ``prio1`` has higher priority than ``prio2``. """ - - def __init__(self, data=None, key=None, reverse=False, precedes=lt): + _heap: List[Node] + _position: Dict[Any, int] + + def __init__( + self, + data: Optional[DictInputs] = None, + key: Optional[Callable[[Any], Any]] = None, + reverse: bool = False, + precedes: Callable[[Any, Any], bool] = lt, + ) -> None: if reverse: if precedes == lt: precedes = gt @@ -98,9 +124,7 @@ def __init__(self, data=None, key=None, reverse=False, precedes=lt): if callable(precedes): self._precedes = precedes else: - raise ValueError( - f"`precedes` function must be a callable; got {precedes}" - ) + raise ValueError(f"`precedes` function must be a callable; got {precedes}") # The heap self._heap = [] @@ -112,31 +136,31 @@ def __init__(self, data=None, key=None, reverse=False, precedes=lt): self.update(data) @property - def precedes(self): + def precedes(self) -> Callable[[Any, Any], bool]: """Priority key precedence function""" return self._precedes @property - def keyfn(self): + def keyfn(self) -> Callable[[Any], Any]: """Priority key function""" return self._keyfn if self._keyfn is not None else lambda x: x - def __repr__(self): + def __repr__(self) -> str: things = ", ".join([f"{node.key}: {node.value}" for node in self._heap]) return f"{self.__class__.__name__}({things})" @classmethod - def minpq(cls, *args, **kwargs): + def minpq(cls, *args: Any, **kwargs: Any) -> Self: return cls(dict(*args, **kwargs), precedes=lt) @classmethod - def maxpq(cls, *args, **kwargs): + def maxpq(cls, *args: Any, **kwargs: Any) -> Self: return cls(dict(*args, **kwargs), precedes=gt) ############ # dict API # ############ - __marker = object() + __marker: object = object() # __eq__ = MutableMapping.__eq__ # __ne__ = MutableMapping.__ne__ # keys = MutableMapping.keys @@ -148,25 +172,25 @@ def maxpq(cls, *args, **kwargs): # setdefault = MutableMapping.setdefault @classmethod - def fromkeys(cls, iterable, value, **kwargs): + def fromkeys(cls, iterable: Iterable, value: Any, **kwargs: Any) -> Self: """ Return a new pqict mapping keys from an iterable to the same value. """ return cls(((k, value) for k in iterable), **kwargs) - def __len__(self): + def __len__(self) -> int: """ Return number of items in the pqdict. """ return len(self._heap) - def __contains__(self, key): + def __contains__(self, key: Any) -> bool: """ Return ``True`` if key is in the pqdict. """ return key in self._position - def __iter__(self): + def __iter__(self) -> Iterator[Any]: """ Return an iterator over the keys of the pqdict. The order of iteration is arbitrary! Use ``popkeys`` to iterate over keys in priority order. @@ -174,14 +198,14 @@ def __iter__(self): for node in self._heap: yield node.key - def __getitem__(self, key): + def __getitem__(self, key: Any) -> Any: """ Return the priority value of ``key``. Raises a ``KeyError`` if not in the pqdict. """ return self._heap[self._position[key]].value # raises KeyError - def __setitem__(self, key, value): + def __setitem__(self, key: Any, value: Any) -> None: """ Assign a priority value to ``key``. If ``key`` is already in the pqdict, its priority value is updated. @@ -205,7 +229,7 @@ def __setitem__(self, key, value): heap[pos].prio = prio self._reheapify(pos) - def __delitem__(self, key): + def __delitem__(self, key: Any) -> None: """ Remove item. Raises a ``KeyError`` if key is not in the pqdict. """ @@ -222,7 +246,7 @@ def __delitem__(self, key): self._reheapify(pos) del node_to_delete - def copy(self): + def copy(self) -> Self: """ Return a shallow copy of a pqdict. """ @@ -231,7 +255,11 @@ def copy(self): other._heap = [Node(node.key, node.value, node.prio) for node in self._heap] return other - def pop(self, key=__marker, default=__marker): + def pop( + self, + key: Any = __marker, + default: Any = __marker, + ) -> Any: """ Dict-style pop: If ``key`` is provided and is in the pqdict, remove the item and return @@ -278,7 +306,7 @@ def pop(self, key=__marker, default=__marker): ###################### # Priority Queue API # ###################### - def top(self, default=__marker): + def top(self, default: Any = __marker) -> Any: """ Return the key of the item with highest priority. If ``default`` is provided and pqdict is empty, then return``default``, otherwise raise @@ -293,7 +321,7 @@ def top(self, default=__marker): return default return node.key - def topvalue(self, default=__marker): + def topvalue(self, default: Any = __marker) -> Any: """ Return the value of the item with highest priority. If ``default`` is provided and pqdict is empty, then return``default``, otherwise raise @@ -308,7 +336,7 @@ def topvalue(self, default=__marker): return default return node.value - def popitem(self): + def popitem(self) -> Tuple[Any, Any]: """ Remove and return the item with highest priority. Raises ``KeyError`` if pqdict is empty. @@ -331,7 +359,7 @@ def popitem(self): del position[node.key] return node.key, node.value - def topitem(self): + def topitem(self) -> Tuple[Any, Any]: """ Return the item with highest priority. Raises ``KeyError`` if pqdict is empty. @@ -342,7 +370,7 @@ def topitem(self): raise KeyError("pqdict is empty") return node.key, node.value - def additem(self, key, value): + def additem(self, key: Any, value: Any) -> None: """ Add a new item. Raises ``KeyError`` if key is already in the pqdict. """ @@ -350,7 +378,7 @@ def additem(self, key, value): raise KeyError(f"{key} is already in the queue") self[key] = value - def pushpopitem(self, key, value): + def pushpopitem(self, key: Any, value: Any) -> Tuple[Any, Any]: """ Equivalent to inserting a new item followed by removing the top priority item, but faster. Raises ``KeyError`` if the new key is @@ -370,7 +398,7 @@ def pushpopitem(self, key, value): self._sink(0) return node.key, node.value - def updateitem(self, key, new_val): + def updateitem(self, key: Any, new_val: Any) -> None: """ Update the priority value of an existing item. Raises ``KeyError`` if key is not in the pqdict. @@ -379,7 +407,7 @@ def updateitem(self, key, new_val): raise KeyError(key) self[key] = new_val - def replace_key(self, key, new_key): + def replace_key(self, key: Any, new_key: Any) -> None: """ Replace the key of an existing heap node in place. Raises ``KeyError`` if the key to replace does not exist or if the new key is already in @@ -393,7 +421,7 @@ def replace_key(self, key, new_key): position[new_key] = pos heap[pos].key = new_key - def swap_priority(self, key1, key2): + def swap_priority(self, key1: Any, key2: Any) -> None: """ Fast way to swap the priority level of two items in the pqdict. Raises ``KeyError`` if either key does not exist. @@ -406,7 +434,7 @@ def swap_priority(self, key1, key2): heap[pos1].key, heap[pos2].key = key2, key1 position[key1], position[key2] = pos2, pos1 - def popkeys(self): + def popkeys(self) -> Iterator[Any]: """ Heapsort iterator over keys in descending order of priority level. """ @@ -416,7 +444,7 @@ def popkeys(self): except KeyError: return - def popvalues(self): + def popvalues(self) -> Iterator[Any]: """ Heapsort iterator over values in descending order of priority level. """ @@ -426,7 +454,7 @@ def popvalues(self): except KeyError: return - def popitems(self): + def popitems(self) -> Iterator[Tuple[Any, Any]]: """ Heapsort iterator over items in descending order of priority level. """ @@ -436,7 +464,7 @@ def popitems(self): except KeyError: return - def heapify(self, key=__marker): + def heapify(self, key: Any = __marker) -> None: """ Repair a broken heap. If the state of an item's priority value changes you can re-sort the relevant item only by providing ``key``. @@ -461,7 +489,7 @@ def heapify(self, key=__marker): # http://algs4.cs.princeton.edu/24pq/. The way I like to think of it, an # item that is too "heavy" (low-priority) should sink down the tree, while # one that is too "light" should float or swim up. - def _reheapify(self, pos): + def _reheapify(self, pos: int) -> None: # update existing node: # bubble up or down depending on values of parent and children heap = self._heap @@ -479,7 +507,7 @@ def _reheapify(self, pos): if precedes(heap[child_pos].prio, heap[pos].prio): self._sink(pos) - def _sink(self, top=0): + def _sink(self, top: int = 0) -> None: # "Sink-to-the-bottom-then-swim" algorithm (Floyd, 1964) # Tends to reduce the number of comparisons when inserting "heavy" # items at the top, e.g. during a heap pop. See heapq for more details. @@ -512,7 +540,7 @@ def _sink(self, top=0): position[node.key] = pos self._swim(pos, top) - def _swim(self, pos, top=0): + def _swim(self, pos: int, top: int = 0) -> None: heap = self._heap position = self._position precedes = self._precedes @@ -538,11 +566,11 @@ def _swim(self, pos, top=0): ########### -def minpq(*args, **kwargs): +def minpq(*args: Any, **kwargs: Any) -> pqdict: return pqdict(dict(*args, **kwargs), precedes=lt) -def maxpq(*args, **kwargs): +def maxpq(*args: Any, **kwargs: Any) -> pqdict: return pqdict(dict(*args, **kwargs), precedes=gt) @@ -551,7 +579,7 @@ def maxpq(*args, **kwargs): ############# -def nlargest(n, mapping, key=None): +def nlargest(n: int, mapping: Mapping, key: Optional[Callable[[Any], Any]] = None): """ Takes a mapping and returns the n keys associated with the largest values in descending order. If the mapping has fewer than n items, all its keys @@ -575,10 +603,7 @@ def nlargest(n, mapping, key=None): ------- list of up to n keys from the mapping associated with the largest values """ - try: - it = mapping.iteritems() - except AttributeError: - it = iter(mapping.items()) + it = iter(mapping.items()) pq = pqdict(key=key, precedes=lt) try: for _ in range(n): @@ -595,7 +620,7 @@ def nlargest(n, mapping, key=None): return out -def nsmallest(n, mapping, key=None): +def nsmallest(n: int, mapping: Mapping, key: Optional[Callable[[Any], Any]] = None): """ Takes a mapping and returns the n keys associated with the smallest values in ascending order. If the mapping has fewer than n items, all its keys are @@ -619,10 +644,7 @@ def nsmallest(n, mapping, key=None): ------- list of up to n keys from the mapping associated with the smallest values """ - try: - it = mapping.iteritems() - except AttributeError: - it = iter(mapping.items()) + it = iter(mapping.items()) pq = pqdict(key=key, precedes=gt) try: for _ in range(n): diff --git a/pqdict/__init__.pyi b/pqdict/__init__.pyi deleted file mode 100644 index a2b4f6d..0000000 --- a/pqdict/__init__.pyi +++ /dev/null @@ -1,52 +0,0 @@ -from typing import TypeVar, Callable, Any, Iterable, Self, Iterator, Mapping -from collections.abc import MutableMapping - -K = TypeVar("K") -V = TypeVar("V") -_T = TypeVar("_T") - -class pqdict(MutableMapping[K, V]): - def __init__( - self, - data: Any = ..., - key: Callable[[V], Any] | None = ..., - reverse: bool = ..., - precedes: Callable[[K, K], Any] = ..., - ) -> None: ... - @property - def precedes(self) -> Callable[[K, K], Any]: ... - @property - def keyfn(self) -> Callable[[V], Any] | None: ... - @classmethod - def minpq(cls, *args: Any, **kwargs: Any) -> Self: ... - @classmethod - def maxpq(cls, *args: Any, **kwargs: Any) -> Self: ... - @classmethod - def fromkeys(cls, iterable: Iterable[Any], value: Any, **kwargs: Any) -> Self: ... - def __len__(self) -> int: ... - def __contains__(self, key: object) -> bool: ... - def __iter__(self) -> Iterator[K]: ... - def __getitem__(self, key: K) -> V: ... - def __setitem__(self, key: K, value: V) -> None: ... - def __delitem__(self, key: K) -> None: ... - def copy(self) -> Self: ... - def pop(self, key: K, default: V) -> V: ... - def top(self, default: V = ...) -> K: ... - def topvalue(self, default: V = ...) -> V: ... - def popitem(self) -> tuple[K, V]: ... - def topitem(self) -> tuple[K, V]: ... - def additem(self, key: K, value: V) -> None: ... - def pushpopitem(self, key: K, value: V) -> tuple[K, V]: ... - def updateitem(self, key: K, new_val: V) -> None: ... - def replace_key(self, key: K, new_key: K) -> None: ... - def swap_priority(self, key1: K, key2: K) -> None: ... - def popkeys(self) -> Iterator[K]: ... - def popvalues(self) -> Iterator[V]: ... - def popitems(self) -> Iterator[tuple[K, V]]: ... - def heapify(self, key: K = ...) -> None: ... - - -def minpq(*args: Any, **kwargs: Any) -> pqdict[Any, Any]: ... -def maxpq(*args: Any, **kwargs: Any) -> pqdict[Any, Any]: ... -def nlargest(n: int, mapping: Mapping[K, V], key: Callable[[V], Any] | None = ...) -> list[K]: ... -def nsmallest(n: int, mapping: Mapping[K, V], key: Callable[[V], Any] | None = ...) -> list[K]: ... From 25a0de530ddbe4f9cc79329a5fd73837b881980e Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Fri, 30 Jun 2023 23:46:24 -0400 Subject: [PATCH 4/9] typing: Annotate Self the old way --- pqdict/__init__.py | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/pqdict/__init__.py b/pqdict/__init__.py index 001361d..89f73e4 100644 --- a/pqdict/__init__.py +++ b/pqdict/__init__.py @@ -29,11 +29,11 @@ Documentation at . -:copyright: (c) 2012-2023 by Nezar Abdennur. +:copyright: (c) 2013-2023 by Nezar Abdennur. :license: MIT, see LICENSE for more details. """ -from collections.abc import Iterable, Mapping, MutableMapping +from collections.abc import MutableMapping from operator import gt, lt from typing import ( Any, @@ -44,15 +44,18 @@ List, Mapping, Optional, - Self, Tuple, + Type, + TypeVar, Union, ) -__version__ = "1.2.1" + +__version__ = "1.3.0-dev" __all__ = ["pqdict", "nlargest", "nsmallest"] DictInputs = Union[Mapping[Any, Any], Iterable[Tuple[Any, Any]]] +Tpqdict = TypeVar("Tpqdict", bound="pqdict") class Node: @@ -100,6 +103,7 @@ class pqdict(MutableMapping): ``precedes(prio1, prio2) -> bool`` and return ``True`` if ``prio1`` has higher priority than ``prio2``. """ + _heap: List[Node] _position: Dict[Any, int] @@ -150,11 +154,11 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({things})" @classmethod - def minpq(cls, *args: Any, **kwargs: Any) -> Self: + def minpq(cls: Type[Tpqdict], *args: Any, **kwargs: Any) -> Tpqdict: return cls(dict(*args, **kwargs), precedes=lt) @classmethod - def maxpq(cls, *args: Any, **kwargs: Any) -> Self: + def maxpq(cls: Type[Tpqdict], *args: Any, **kwargs: Any) -> Tpqdict: return cls(dict(*args, **kwargs), precedes=gt) ############ @@ -172,7 +176,9 @@ def maxpq(cls, *args: Any, **kwargs: Any) -> Self: # setdefault = MutableMapping.setdefault @classmethod - def fromkeys(cls, iterable: Iterable, value: Any, **kwargs: Any) -> Self: + def fromkeys( + cls: Type[Tpqdict], iterable: Iterable, value: Any, **kwargs: Any + ) -> Tpqdict: """ Return a new pqict mapping keys from an iterable to the same value. """ @@ -246,7 +252,7 @@ def __delitem__(self, key: Any) -> None: self._reheapify(pos) del node_to_delete - def copy(self) -> Self: + def copy(self: Tpqdict) -> Tpqdict: """ Return a shallow copy of a pqdict. """ @@ -261,15 +267,19 @@ def pop( default: Any = __marker, ) -> Any: """ - Dict-style pop: - If ``key`` is provided and is in the pqdict, remove the item and return - its priority value. If ``key`` is not in the pqdict, return ``default`` - if provided, otherwise raise a ``KeyError``. + Hybrid pop method. - Priority queue-style pop: - If ``key`` is not provided, remove the top item and return its key. If - the pqdict is empty, return ``default`` if provided, otherwise raise a - ``KeyError``. + Dictionary pop with ``key``: + * If ``key`` is provided and is in the pqdict, remove the item and + return its **value**. + * If ``key`` is not in the pqdict, return ``default`` if provided, + otherwise raise a ``KeyError``. + + Priority Queue pop without ``key``: + * If ``key`` is not provided, remove the top item and return its + **key**. + * If the pqdict is empty, return ``default`` if provided, otherwise + raise a ``KeyError``. """ heap = self._heap position = self._position @@ -481,7 +491,9 @@ def heapify(self, key: Any = __marker) -> None: raise KeyError(key) self._reheapify(pos) - # Heap algorithms + ################### + # Heap algorithms # + ################### # The names of the heap operations in `heapq` (sift up/down) refer to the # motion of the nodes being compared to, rather than the node being # operated on as is usually done in textbooks (i.e. bubble down/up, From 0c36a9730df803c5aa134978ab99db5bb5d00b38 Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Sat, 1 Jul 2023 00:08:11 -0400 Subject: [PATCH 5/9] feat!: Deprecate minpq and maxpq module functions --- pqdict/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pqdict/__init__.py b/pqdict/__init__.py index 89f73e4..2a21453 100644 --- a/pqdict/__init__.py +++ b/pqdict/__init__.py @@ -49,6 +49,7 @@ TypeVar, Union, ) +from warnings import warn __version__ = "1.3.0-dev" @@ -579,10 +580,22 @@ def _swim(self, pos: int, top: int = 0) -> None: def minpq(*args: Any, **kwargs: Any) -> pqdict: + warn( + "The `minpq` module function is deprecated and will be removed in v1.4. " + "Use the classmethod `pqdict.minpq()` instead.", + DeprecationWarning, + stacklevel=2 + ) return pqdict(dict(*args, **kwargs), precedes=lt) def maxpq(*args: Any, **kwargs: Any) -> pqdict: + warn( + "The `maxpq` module function is deprecated and will be removed in v1.4. " + "Use the classmethod `pqdict.maxpq()` instead.", + DeprecationWarning, + stacklevel=2 + ) return pqdict(dict(*args, **kwargs), precedes=gt) From 51bc4761ea04d655f7cb332bdc9941842597d3e4 Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Sat, 1 Jul 2023 15:38:48 -0400 Subject: [PATCH 6/9] feat: Add popvalue to complete the series --- pqdict/__init__.py | 77 ++++++++++++++++++--------- tests/test_pqdict.py | 121 +++++++++++++++++++++++++++++-------------- 2 files changed, 133 insertions(+), 65 deletions(-) diff --git a/pqdict/__init__.py b/pqdict/__init__.py index 2a21453..ae31adb 100644 --- a/pqdict/__init__.py +++ b/pqdict/__init__.py @@ -51,7 +51,6 @@ ) from warnings import warn - __version__ = "1.3.0-dev" __all__ = ["pqdict", "nlargest", "nsmallest"] @@ -84,7 +83,8 @@ class pqdict(MutableMapping): The default behavior is that of a min-priority queue, i.e. the item with the *smallest* priority value is given *highest* priority. This behavior can be reversed by specifying ``reverse=True`` or by providing a custom - comparator function via the ``precedes`` keyword argument. + precedence function via the ``precedes`` keyword argument. Alternatively, + use the explicit :meth:`pqdict.minpq` or :meth:`pqdict.maxpq` class methods. Parameters ---------- @@ -97,12 +97,12 @@ class pqdict(MutableMapping): reverse : bool, optional [default: ``False``] If ``True``, *larger* priority keys give items *higher* priority. Default is ``False``. - precedes : callable, optional (overrides ``reverse``) [default: ``operator.lt``] + precedes : callable, optional [default: ``operator.lt``] Function that determines precedence of a pair of priority keys. The default comparator is ``operator.lt``, meaning *smaller* priority keys give items *higher* priority. The callable must have the form ``precedes(prio1, prio2) -> bool`` and return ``True`` if ``prio1`` - has higher priority than ``prio2``. + has higher priority than ``prio2``. Overrides ``reverse``. """ _heap: List[Node] @@ -156,10 +156,12 @@ def __repr__(self) -> str: @classmethod def minpq(cls: Type[Tpqdict], *args: Any, **kwargs: Any) -> Tpqdict: + """Create a pqdict with min-priority semantics: smallest is highest.""" return cls(dict(*args, **kwargs), precedes=lt) @classmethod def maxpq(cls: Type[Tpqdict], *args: Any, **kwargs: Any) -> Tpqdict: + """Create a pqdict with max-priority semantics: largest is highest.""" return cls(dict(*args, **kwargs), precedes=gt) ############ @@ -181,7 +183,7 @@ def fromkeys( cls: Type[Tpqdict], iterable: Iterable, value: Any, **kwargs: Any ) -> Tpqdict: """ - Return a new pqict mapping keys from an iterable to the same value. + Return a new pqdict mapping keys from an iterable to the same value. """ return cls(((k, value) for k in iterable), **kwargs) @@ -347,6 +349,36 @@ def topvalue(self, default: Any = __marker) -> Any: return default return node.value + def topitem(self) -> Tuple[Any, Any]: + """ + Return the item with highest priority. Raises ``KeyError`` if pqdict is + empty. + """ + try: + node = self._heap[0] + except IndexError: + raise KeyError("pqdict is empty") + return node.key, node.value + + def popvalue(self, default: Any = __marker) -> Any: + """ + Remove and return the value of the item with highest priority. If + ``default`` is provided and pqdict is empty, then return``default``, + otherwise raise ``KeyError``. + """ + heap = self._heap + position = self._position + + if not heap: + if default is self.__marker: + raise KeyError("pqdict is empty") + else: + return default + + value = heap[0].value + del self[heap[0].key] + return value + def popitem(self) -> Tuple[Any, Any]: """ Remove and return the item with highest priority. Raises ``KeyError`` @@ -370,17 +402,6 @@ def popitem(self) -> Tuple[Any, Any]: del position[node.key] return node.key, node.value - def topitem(self) -> Tuple[Any, Any]: - """ - Return the item with highest priority. Raises ``KeyError`` if pqdict is - empty. - """ - try: - node = self._heap[0] - except IndexError: - raise KeyError("pqdict is empty") - return node.key, node.value - def additem(self, key: Any, value: Any) -> None: """ Add a new item. Raises ``KeyError`` if key is already in the pqdict. @@ -447,7 +468,7 @@ def swap_priority(self, key1: Any, key2: Any) -> None: def popkeys(self) -> Iterator[Any]: """ - Heapsort iterator over keys in descending order of priority level. + Remove items, returning keys in descending order of priority rank. """ try: while True: @@ -457,7 +478,7 @@ def popkeys(self) -> Iterator[Any]: def popvalues(self) -> Iterator[Any]: """ - Heapsort iterator over values in descending order of priority level. + Remove items, returning values in descending order of priority rank. """ try: while True: @@ -467,7 +488,7 @@ def popvalues(self) -> Iterator[Any]: def popitems(self) -> Iterator[Tuple[Any, Any]]: """ - Heapsort iterator over items in descending order of priority level. + Remove and return items in descending order of priority rank. """ try: while True: @@ -610,9 +631,6 @@ def nlargest(n: int, mapping: Mapping, key: Optional[Callable[[Any], Any]] = Non in descending order. If the mapping has fewer than n items, all its keys are returned. - Equivalent to: - ``next(zip(*heapq.nlargest(mapping.items(), key=lambda x: x[1])))`` - Parameters ---------- n : int @@ -627,6 +645,12 @@ def nlargest(n: int, mapping: Mapping, key: Optional[Callable[[Any], Any]] = Non Returns ------- list of up to n keys from the mapping associated with the largest values + + Notes + ----- + This function is equivalent to: + + >>> [item[0] for item in heapq.nlargest(n, mapping.items(), lambda x: x[1])] """ it = iter(mapping.items()) pq = pqdict(key=key, precedes=lt) @@ -651,9 +675,6 @@ def nsmallest(n: int, mapping: Mapping, key: Optional[Callable[[Any], Any]] = No in ascending order. If the mapping has fewer than n items, all its keys are returned. - Equivalent to: - ``next(zip(*heapq.nsmallest(mapping.items(), key=lambda x: x[1])))`` - Parameters ---------- n : int @@ -668,6 +689,12 @@ def nsmallest(n: int, mapping: Mapping, key: Optional[Callable[[Any], Any]] = No Returns ------- list of up to n keys from the mapping associated with the smallest values + + Notes + ----- + This function is equivalent to: + + >>> [item[0] for item in heapq.nsmallest(n, mapping.items(), lambda x: x[1])] """ it = iter(mapping.items()) pq = pqdict(key=key, precedes=gt) diff --git a/tests/test_pqdict.py b/tests/test_pqdict.py index 0f7b904..bac28bf 100644 --- a/tests/test_pqdict.py +++ b/tests/test_pqdict.py @@ -1,14 +1,12 @@ -# -*- coding: utf-8 -*- -from datetime import datetime, timedelta -from itertools import combinations import operator -import sys import random +import sys +from datetime import datetime, timedelta +from itertools import combinations import pytest -from pqdict import pqdict, minpq, maxpq, nlargest, nsmallest - +from pqdict import maxpq, minpq, nlargest, nsmallest, pqdict sample_keys = ["A", "B", "C", "D", "E", "F", "G"] sample_values = [5, 8, 7, 3, 9, 12, 1] @@ -60,8 +58,9 @@ def _check_index(pq): assert key == node.key -# The next group of tests were originally in class TestNew - +########## +# Creation +########## def test_constructor(): # sequence of pairs @@ -72,7 +71,7 @@ def test_constructor(): # dictionary pq2 = pqdict({"A": 5, "B": 8, "C": 7, "D": 3, "E": 9, "F": 12, "G": 1}) # keyword arguments - pq3 = minpq(A=5, B=8, C=7, D=3, E=9, F=12, G=1) + pq3 = pqdict.minpq(A=5, B=8, C=7, D=3, E=9, F=12, G=1) assert pq0 == pq1 == pq2 == pq3 @@ -89,21 +88,23 @@ def test_equality(): # pqdict == regular dict if they have same key/value pairs adict = dict(sample_items) assert pq1 == adict + # TODO: FIX? # pqdicts evaluate as equal even if they have different # key functions and/or precedence functions - pq3 = maxpq(sample_items) + pq3 = pqdict.maxpq(sample_items) + assert pq1 == pq3 def test_minpq(): - pq = minpq(A=5, B=8, C=7, D=3, E=9, F=12, G=1) + pq = pqdict.minpq(A=5, B=8, C=7, D=3, E=9, F=12, G=1) assert list(pq.popvalues()) == [1, 3, 5, 7, 8, 9, 12] assert pq.precedes == operator.lt def test_maxpq(): - pq = maxpq(A=5, B=8, C=7, D=3, E=9, F=12, G=1) + pq = pqdict.maxpq(A=5, B=8, C=7, D=3, E=9, F=12, G=1) assert list(pq.popvalues()) == [12, 9, 8, 7, 5, 3, 1] assert pq.precedes == operator.gt @@ -129,8 +130,19 @@ def test_fromkeys(): assert pq.pop() == "spam" -# The next group of tests were originally in class TestDictAPI +def test_factory_functions(): + pq = minpq(A=5, B=8, C=7, D=3, E=9, F=12, G=1) + assert list(pq.popvalues()) == [1, 3, 5, 7, 8, 9, 12] + assert pq.precedes == operator.lt + pq = maxpq(A=5, B=8, C=7, D=3, E=9, F=12, G=1) + assert list(pq.popvalues()) == [12, 9, 8, 7, 5, 3, 1] + assert pq.precedes == operator.gt + + +################ +# Dictionary API +################ def test_len(): pq = pqdict() @@ -247,7 +259,10 @@ def test_items(): assert sorted(sample_values) == [item[1] for item in pq.popitems()] -# The next group of tests were originally inclass TestPQAPI +#################### +# Priority Queue API +#################### + def test_keyfn(): pq = pqdict() assert pq.keyfn(5) == 5 @@ -270,8 +285,9 @@ def test_precedes(): def test_pop(): + # Dict-pop # pop selected item - return value - pq = minpq(A=5, B=8, C=1) + pq = pqdict.minpq(A=5, B=8, C=1) value = pq.pop("B") assert value == 8 pq.pop("A") @@ -280,11 +296,15 @@ def test_pop(): pq.pop("A") with pytest.raises(KeyError): pq.pop("does_not_exist") + assert pq.pop("does_not_exist", "default") == "default" + + # PQ-pop # no args and empty - throws with pytest.raises(KeyError): pq.pop() # pq is now empty + assert pq.pop(default="default") == "default" # no args - return top key - pq = minpq(A=5, B=8, C=1) + pq = pqdict.minpq(A=5, B=8, C=1) assert pq.pop() == "C" @@ -293,6 +313,7 @@ def test_top(): pq = pqdict() with pytest.raises(KeyError): pq.top() + assert pq.top("default") == "default" # non-empty for num_items in range(1, 30): items = generate_data("float", num_items) @@ -301,7 +322,7 @@ def test_top(): def test_popitem(): - pq = minpq(A=5, B=8, C=1) + pq = pqdict.minpq(A=5, B=8, C=1) # pop top item key, value = pq.popitem() assert key == "C" @@ -343,7 +364,7 @@ def test_updateitem(): def test_pushpopitem(): - pq = minpq(A=5, B=8, C=1) + pq = pqdict.minpq(A=5, B=8, C=1) assert pq.pushpopitem("D", 10) == ("C", 1) assert pq.pushpopitem("E", 5) == ("E", 5) with pytest.raises(KeyError): @@ -351,7 +372,7 @@ def test_pushpopitem(): def test_replace_key(): - pq = minpq(A=5, B=8, C=1) + pq = pqdict.minpq(A=5, B=8, C=1) pq.replace_key("A", "Alice") pq.replace_key("B", "Bob") _check_index(pq) @@ -366,7 +387,7 @@ def test_replace_key(): def test_swap_priority(): - pq = minpq(A=5, B=8, C=1) + pq = pqdict.minpq(A=5, B=8, C=1) pq.swap_priority("A", "C") _check_index(pq) assert pq["A"] == 1 @@ -394,18 +415,7 @@ def test_destructive_iteration(): assert values_heapsorted == sorted(values) -# The next group of tests were originally in class TestOperations - - -@pytest.mark.skipif("sys.version_info <= (3,0)") -def test_uncomparable(): - # non-comparable priority keys (Python 3 only) - # Python 3 has stricter comparison than Python 2 - pq = pqdict() - pq.additem("a", []) - with pytest.raises(TypeError): - pq.additem("b", 5) - +# Heap invariant tests and key/value types def test_heapify(): for size in range(30): @@ -469,7 +479,7 @@ def test_updates_and_deletes(): def test_topvalue1(): - pq = maxpq() + pq = pqdict.maxpq() pq['a'] = 10 pq['b'] = 20 pq['c'] = 5 @@ -495,19 +505,15 @@ def test_topvalue2(): n = 1000 low, high = 0, 1000000 - vals = [] - - pq = minpq() + pq = pqdict.minpq() for i in range(n): v = rnd.randint(low, high) - pq[i] = v vals.append(v) popped_values = [] - while pq: popped_values.append(pq.topvalue()) pq.pop() @@ -515,6 +521,38 @@ def test_topvalue2(): assert sorted(vals) == popped_values +def test_popvalue1(): + pq = pqdict.maxpq() + pq['a'] = 10 + pq['b'] = 20 + pq['c'] = 5 + assert pq.popvalue() == 20 + assert pq.popvalue() == 10 + assert pq.popvalue() == 5 + assert pq.popvalue(-1) == -1 + assert pq.popvalue(default=-10) == -10 + + +def test_popvalue2(): + rnd = random.Random(0) + + n = 1000 + low, high = 0, 1000000 + vals = [] + pq = pqdict.minpq() + + for i in range(n): + v = rnd.randint(low, high) + pq[i] = v + vals.append(v) + + popped_values = [] + while pq: + popped_values.append(pq.popvalue()) + + assert sorted(vals) == popped_values + + def test_edgecases(): keys = ["A", "B", "C", "D", "E", "F", "G"] values = [1, 1, 1, 1, 1, 1, 1] @@ -548,7 +586,7 @@ def test_datetime(): def test_repair(): mutable_value = [3] - pq = minpq(A=[1], B=[2], C=mutable_value) + pq = pqdict.minpq(A=[1], B=[2], C=mutable_value) assert pq[pq.top()] == [1] mutable_value[0] = 0 assert pq[pq.top()] == [1] @@ -556,7 +594,10 @@ def test_repair(): assert pq[pq.top()] == [0] -# The next test was originally in class TestModuleFunctions +######################## +# Module-level functions +######################## + def test_nbest(): top3 = nlargest(3, dict(sample_items)) assert list(top3) == ["F", "E", "B"] From b57dd7ff3c749fd5a0b179e600ac56ec7f4377fe Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Sat, 1 Jul 2023 15:39:10 -0400 Subject: [PATCH 7/9] docs: Update API docs --- docs/conf.py | 2 +- docs/pqdict.rst | 56 ++++++++++++++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e1aae00..2812568 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -86,7 +86,7 @@ def _get_version(): # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/pqdict.rst b/docs/pqdict.rst index f2b6f14..162787a 100644 --- a/docs/pqdict.rst +++ b/docs/pqdict.rst @@ -12,9 +12,11 @@ pqdict class .. autoclass:: pqdict :no-members: - .. automethod:: fromkeys + .. automethod:: minpq - .. automethod:: heapify([key]) + .. automethod:: maxpq + + .. automethod:: fromkeys | @@ -44,9 +46,14 @@ pqdict class .. method:: pop([key[, default]]) - If ``key`` is in the pqdict, remove it and return its priority value, - else return ``default``. If ``default`` is not provided and ``key`` is - not in the pqdict, raise a ``KeyError``. + Hybrid pop method. + + With ``key``, perform a dictionary pop: + + * If ``key`` is in the pqdict, remove the item and return its + **value**. + * If ``key`` is not in the pqdict, return ``default`` if provided, + otherwise raise a ``KeyError``. .. automethod:: clear @@ -59,7 +66,9 @@ pqdict class Iterators and Views .. warning:: - For the following sequences, order is arbitrary. + For the sequences returned by the following methods, the **iteration order is arbitrary**. + + See further below for sorted iterators :meth:`popkeys`, :meth:`popvalues`, and :meth:`popitems`. .. method:: iter(pq) @@ -69,9 +78,6 @@ pqdict class .. automethod:: items - .. note:: - In Python 2, the above methods return lists rather than views and ``pqdict`` includes additional iterator methods ``iterkeys()``, ``itervalues()`` and ``iteritems()``. - | **Priority Queue API** @@ -79,33 +85,42 @@ pqdict class .. automethod:: top .. automethod:: topvalue + + .. automethod:: topitem - .. method:: pop([key[, default]]) + .. method:: pop(*, [default]) - If ``key`` is not provided, remove the top item and return its key, or - raise ``KeyError`` if the pqdict is empty. + Hybrid pop method. - .. automethod:: additem + Without ``key``, perform a priority queue pop: - .. automethod:: updateitem - - .. automethod:: topitem + * Remove the top item and return its **key**. + * If the pqdict is empty, return ``default`` if provided, otherwise + raise a ``KeyError``. + .. automethod:: popvalue + .. automethod:: popitem + .. automethod:: additem + + .. automethod:: updateitem + .. automethod:: pushpopitem(key, value) .. automethod:: replace_key .. automethod:: swap_priority - Heapsort Iterators + .. automethod:: heapify([key]) + + Sorted Iterators .. note:: Iteration is in descending order of priority. - .. danger:: - Heapsort iterators are destructive: they are generators that pop items out of the heap, which amounts to performing a heapsort. + .. warning:: + Sorted iterators are destructive: they are generators that pop items out of the heap, which amounts to performing a heapsort. .. automethod:: popkeys @@ -113,9 +128,6 @@ pqdict class .. automethod:: popitems - .. warning:: - The names of the heapsort iterators in v0.5 (iterkeys, itervalues, iteritems) were changed in v0.6 to be more transparent: These names are not provided at all in Python 3, and in Python 2 they are now part of the dictionary API. - Functions --------- From 752ab22642b2b7840c30f8c2ae9d7aa916c5df33 Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Sat, 1 Jul 2023 15:43:55 -0400 Subject: [PATCH 8/9] docs: Update readme --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index 2c3ed17..6bb4566 100644 --- a/README.rst +++ b/README.rst @@ -3,8 +3,6 @@ Priority Queue Dictionary (pqdict) A priority queue dictionary maps hashable objects (keys) to priority-determining values. It provides a hybrid dictionary/priority queue API. -Works with Python 2.7+, 3.4+, and PyPy. - .. image:: https://github.com/nvictus/priority-queue-dictionary/actions/workflows/package_lint-test.yml/badge.svg :target: https://github.com/nvictus/priority-queue-dictionary/actions/workflows/package_lint-test.yml :alt: CI From d612ba254144e12f3a2b5d50cef6078efc3b447c Mon Sep 17 00:00:00 2001 From: Nezar Abdennur Date: Sat, 1 Jul 2023 15:49:35 -0400 Subject: [PATCH 9/9] ci: Add py36 and py37 to CI --- .github/workflows/package_lint-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/package_lint-test.yml b/.github/workflows/package_lint-test.yml index 2adb962..106b11e 100644 --- a/.github/workflows/package_lint-test.yml +++ b/.github/workflows/package_lint-test.yml @@ -12,11 +12,11 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 # py36 is EOL on ubuntu-latest strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "pypy3.9"] steps: - uses: actions/checkout@v3