From 6ae6e4e621242ed5f3e5855aff92194a16e9f764 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 27 Jul 2024 00:23:03 -0400 Subject: [PATCH 1/8] Fully typed ``RangeMap`` and avoid complete iterations to find matches --- jaraco/collections/__init__.py | 46 ++++++++++++++++++++++------------ newsfragments/16.feature.rst | 1 + 2 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 newsfragments/16.feature.rst diff --git a/jaraco/collections/__init__.py b/jaraco/collections/__init__.py index 5c276da..2a53226 100644 --- a/jaraco/collections/__init__.py +++ b/jaraco/collections/__init__.py @@ -8,10 +8,17 @@ import random import re from collections.abc import Container, Iterable, Mapping -from typing import Any, Callable, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, TypeVar, Union, overload import jaraco.text +if TYPE_CHECKING: + from _typeshed import SupportsKeysAndGetItem + from typing_extensions import Self + +_T = TypeVar('_T') +_VT = TypeVar('_VT') + _Matchable = Union[Callable, Container, Iterable, re.Pattern] @@ -119,7 +126,7 @@ def dict_map(function, dictionary): return dict((key, function(value)) for key, value in dictionary.items()) -class RangeMap(dict): +class RangeMap(Dict[int, _VT]): """ A dictionary-like object that uses the keys as bounds for a range. Inclusion of the value for that range is determined by the @@ -186,7 +193,7 @@ class RangeMap(dict): which requires use of sort params and a key_match_comparator. >>> r = RangeMap({1: 'a', 4: 'b'}, - ... sort_params=dict(reverse=True), + ... sort_params={'reverse': True}, ... key_match_comparator=operator.ge) >>> r[1], r[2], r[3], r[4], r[5], r[6] ('a', 'a', 'a', 'b', 'b', 'b') @@ -202,21 +209,23 @@ class RangeMap(dict): def __init__( self, - source, + source: SupportsKeysAndGetItem[int, _VT] | Iterable[tuple[int, _VT]], sort_params: Mapping[str, Any] = {}, - key_match_comparator=operator.le, + key_match_comparator: Callable[[int, int], bool] = operator.le, ): dict.__init__(self, source) self.sort_params = sort_params self.match = key_match_comparator @classmethod - def left(cls, source): + def left( + cls, source: SupportsKeysAndGetItem[int, _VT] | Iterable[tuple[int, _VT]] + ) -> Self: return cls( - source, sort_params=dict(reverse=True), key_match_comparator=operator.ge + source, sort_params={'reverse': True}, key_match_comparator=operator.ge ) - def __getitem__(self, item): + def __getitem__(self, item: int) -> _VT: sorted_keys = sorted(self.keys(), **self.sort_params) if isinstance(item, RangeMap.Item): result = self.__getitem__(sorted_keys[item]) @@ -227,7 +236,11 @@ def __getitem__(self, item): raise KeyError(key) return result - def get(self, key, default=None): + @overload # type: ignore[override] # Signature simplified over dict and Mapping + def get(self, key: int, default: _T) -> _VT | _T: ... + @overload + def get(self, key: int, default: None = None) -> _VT | None: ... + def get(self, key: int, default: _T | None = None) -> _VT | _T | None: """ Return the value for key if key is in the dictionary, else default. If default is not given, it defaults to None, so that this method @@ -238,14 +251,15 @@ def get(self, key, default=None): except KeyError: return default - def _find_first_match_(self, keys, item): + def _find_first_match_(self, keys: Iterable[int], item: int) -> int: is_match = functools.partial(self.match, item) - matches = list(filter(is_match, keys)) - if matches: - return matches[0] - raise KeyError(item) + matches = filter(is_match, keys) + try: + return next(matches) + except StopIteration: + raise KeyError(item) from None - def bounds(self): + def bounds(self) -> tuple[int, int]: sorted_keys = sorted(self.keys(), **self.sort_params) return (sorted_keys[RangeMap.first_item], sorted_keys[RangeMap.last_item]) @@ -253,7 +267,7 @@ def bounds(self): undefined_value = type('RangeValueUndefined', (), {})() class Item(int): - "RangeMap Item" + 'RangeMap Item' first_item = Item(0) last_item = Item(-1) diff --git a/newsfragments/16.feature.rst b/newsfragments/16.feature.rst new file mode 100644 index 0000000..740b18c --- /dev/null +++ b/newsfragments/16.feature.rst @@ -0,0 +1 @@ +Fully typed ``RangeMap`` and avoid complete iterations to find matches -- by :user:`Avasam` From 5e14b56c2a35e6c4b95d23ef6fc70cb40a258bd2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Jul 2024 12:39:25 -0400 Subject: [PATCH 2/8] Use triple-quotes in function def. Co-authored-by: Avasam --- jaraco/collections/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jaraco/collections/__init__.py b/jaraco/collections/__init__.py index 2a53226..5fdec35 100644 --- a/jaraco/collections/__init__.py +++ b/jaraco/collections/__init__.py @@ -267,7 +267,7 @@ def bounds(self) -> tuple[int, int]: undefined_value = type('RangeValueUndefined', (), {})() class Item(int): - 'RangeMap Item' + """RangeMap Item""" first_item = Item(0) last_item = Item(-1) From 1e3d156d1e30c3a3f1b885e3da72d1c5fe56a9f2 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 27 Jul 2024 12:42:55 -0400 Subject: [PATCH 3/8] Revert `{'reverse': True}` to `dict(reverse=True)` --- jaraco/collections/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/jaraco/collections/__init__.py b/jaraco/collections/__init__.py index 5fdec35..0f4fc93 100644 --- a/jaraco/collections/__init__.py +++ b/jaraco/collections/__init__.py @@ -16,8 +16,8 @@ from _typeshed import SupportsKeysAndGetItem from typing_extensions import Self -_T = TypeVar('_T') -_VT = TypeVar('_VT') +_T = TypeVar("_T") +_VT = TypeVar("_VT") _Matchable = Union[Callable, Container, Iterable, re.Pattern] @@ -193,7 +193,7 @@ class RangeMap(Dict[int, _VT]): which requires use of sort params and a key_match_comparator. >>> r = RangeMap({1: 'a', 4: 'b'}, - ... sort_params={'reverse': True}, + ... sort_params=dict(reverse=True), ... key_match_comparator=operator.ge) >>> r[1], r[2], r[3], r[4], r[5], r[6] ('a', 'a', 'a', 'b', 'b', 'b') @@ -222,7 +222,7 @@ def left( cls, source: SupportsKeysAndGetItem[int, _VT] | Iterable[tuple[int, _VT]] ) -> Self: return cls( - source, sort_params={'reverse': True}, key_match_comparator=operator.ge + source, sort_params=dict(reverse=True), key_match_comparator=operator.ge ) def __getitem__(self, item: int) -> _VT: @@ -264,7 +264,7 @@ def bounds(self) -> tuple[int, int]: return (sorted_keys[RangeMap.first_item], sorted_keys[RangeMap.last_item]) # some special values for the RangeMap - undefined_value = type('RangeValueUndefined', (), {})() + undefined_value = type("RangeValueUndefined", (), {})() class Item(int): """RangeMap Item""" @@ -521,7 +521,7 @@ def _safe_getitem(cont, key, missing_result): # raise the original exception, but use the original class # name, not 'super'. (message,) = e.args - message = message.replace('super', self.__class__.__name__, 1) + message = message.replace("super", self.__class__.__name__, 1) e.args = (message,) raise @@ -544,7 +544,7 @@ def invert_map(map): """ res = dict((v, k) for k, v in map.items()) if not len(res) == len(map): - raise ValueError('Key conflict in inverted mapping') + raise ValueError("Key conflict in inverted mapping") return res @@ -786,7 +786,7 @@ class FrozenDict(collections.abc.Mapping, collections.abc.Hashable): True """ - __slots__ = ['__data'] + __slots__ = ["__data"] def __new__(cls, *args, **kwargs): self = super().__new__(cls) @@ -1014,7 +1014,7 @@ class FreezableDefaultDict(collections.defaultdict): # type: ignore """ def __missing__(self, key): - return getattr(self, '_frozen', super().__missing__)(key) + return getattr(self, "_frozen", super().__missing__)(key) def freeze(self): self._frozen = lambda key: self.default_factory() From 110720168b811d9ef47d8abe1d0f3538960fe1ed Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 27 Jul 2024 13:04:22 -0400 Subject: [PATCH 4/8] Make RangeMap keys a TypeVar bound to _SupportsComparison --- jaraco/collections/__init__.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/jaraco/collections/__init__.py b/jaraco/collections/__init__.py index 0f4fc93..9ad5ba0 100644 --- a/jaraco/collections/__init__.py +++ b/jaraco/collections/__init__.py @@ -13,10 +13,13 @@ import jaraco.text if TYPE_CHECKING: + from _operator import _SupportsComparison + from _typeshed import SupportsKeysAndGetItem from typing_extensions import Self _T = TypeVar("_T") +_RangeMapKT = TypeVar("_RangeMapKT", bound=_SupportsComparison) _VT = TypeVar("_VT") _Matchable = Union[Callable, Container, Iterable, re.Pattern] @@ -126,7 +129,7 @@ def dict_map(function, dictionary): return dict((key, function(value)) for key, value in dictionary.items()) -class RangeMap(Dict[int, _VT]): +class RangeMap(Dict[_RangeMapKT, _VT]): """ A dictionary-like object that uses the keys as bounds for a range. Inclusion of the value for that range is determined by the @@ -209,9 +212,11 @@ class RangeMap(Dict[int, _VT]): def __init__( self, - source: SupportsKeysAndGetItem[int, _VT] | Iterable[tuple[int, _VT]], + source: ( + SupportsKeysAndGetItem[_RangeMapKT, _VT] | Iterable[tuple[_RangeMapKT, _VT]] + ), sort_params: Mapping[str, Any] = {}, - key_match_comparator: Callable[[int, int], bool] = operator.le, + key_match_comparator: Callable[[_RangeMapKT, _RangeMapKT], bool] = operator.le, ): dict.__init__(self, source) self.sort_params = sort_params @@ -219,13 +224,16 @@ def __init__( @classmethod def left( - cls, source: SupportsKeysAndGetItem[int, _VT] | Iterable[tuple[int, _VT]] + cls, + source: ( + SupportsKeysAndGetItem[_RangeMapKT, _VT] | Iterable[tuple[_RangeMapKT, _VT]] + ), ) -> Self: return cls( source, sort_params=dict(reverse=True), key_match_comparator=operator.ge ) - def __getitem__(self, item: int) -> _VT: + def __getitem__(self, item: _RangeMapKT) -> _VT: sorted_keys = sorted(self.keys(), **self.sort_params) if isinstance(item, RangeMap.Item): result = self.__getitem__(sorted_keys[item]) @@ -237,10 +245,10 @@ def __getitem__(self, item: int) -> _VT: return result @overload # type: ignore[override] # Signature simplified over dict and Mapping - def get(self, key: int, default: _T) -> _VT | _T: ... + def get(self, key: _RangeMapKT, default: _T) -> _VT | _T: ... @overload - def get(self, key: int, default: None = None) -> _VT | None: ... - def get(self, key: int, default: _T | None = None) -> _VT | _T | None: + def get(self, key: _RangeMapKT, default: None = None) -> _VT | None: ... + def get(self, key: _RangeMapKT, default: _T | None = None) -> _VT | _T | None: """ Return the value for key if key is in the dictionary, else default. If default is not given, it defaults to None, so that this method @@ -251,7 +259,9 @@ def get(self, key: int, default: _T | None = None) -> _VT | _T | None: except KeyError: return default - def _find_first_match_(self, keys: Iterable[int], item: int) -> int: + def _find_first_match_( + self, keys: Iterable[_RangeMapKT], item: _RangeMapKT + ) -> _RangeMapKT: is_match = functools.partial(self.match, item) matches = filter(is_match, keys) try: @@ -259,7 +269,7 @@ def _find_first_match_(self, keys: Iterable[int], item: int) -> int: except StopIteration: raise KeyError(item) from None - def bounds(self) -> tuple[int, int]: + def bounds(self) -> tuple[_RangeMapKT, _RangeMapKT]: sorted_keys = sorted(self.keys(), **self.sort_params) return (sorted_keys[RangeMap.first_item], sorted_keys[RangeMap.last_item]) From b163082702b812509c8254947a2144c93d2b4055 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 27 Jul 2024 13:26:51 -0400 Subject: [PATCH 5/8] Revert accidental quote change again --- jaraco/collections/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/jaraco/collections/__init__.py b/jaraco/collections/__init__.py index 9ad5ba0..e2fd11a 100644 --- a/jaraco/collections/__init__.py +++ b/jaraco/collections/__init__.py @@ -18,9 +18,9 @@ from _typeshed import SupportsKeysAndGetItem from typing_extensions import Self -_T = TypeVar("_T") -_RangeMapKT = TypeVar("_RangeMapKT", bound=_SupportsComparison) -_VT = TypeVar("_VT") +_T = TypeVar('_T') +_RangeMapKT = TypeVar('_RangeMapKT', bound=_SupportsComparison) +_VT = TypeVar('_VT') _Matchable = Union[Callable, Container, Iterable, re.Pattern] @@ -274,7 +274,7 @@ def bounds(self) -> tuple[_RangeMapKT, _RangeMapKT]: return (sorted_keys[RangeMap.first_item], sorted_keys[RangeMap.last_item]) # some special values for the RangeMap - undefined_value = type("RangeValueUndefined", (), {})() + undefined_value = type('RangeValueUndefined', (), {})() class Item(int): """RangeMap Item""" @@ -531,7 +531,7 @@ def _safe_getitem(cont, key, missing_result): # raise the original exception, but use the original class # name, not 'super'. (message,) = e.args - message = message.replace("super", self.__class__.__name__, 1) + message = message.replace('super', self.__class__.__name__, 1) e.args = (message,) raise @@ -554,7 +554,7 @@ def invert_map(map): """ res = dict((v, k) for k, v in map.items()) if not len(res) == len(map): - raise ValueError("Key conflict in inverted mapping") + raise ValueError('Key conflict in inverted mapping') return res @@ -796,7 +796,7 @@ class FrozenDict(collections.abc.Mapping, collections.abc.Hashable): True """ - __slots__ = ["__data"] + __slots__ = ['__data'] def __new__(cls, *args, **kwargs): self = super().__new__(cls) @@ -1024,7 +1024,7 @@ class FreezableDefaultDict(collections.defaultdict): # type: ignore """ def __missing__(self, key): - return getattr(self, "_frozen", super().__missing__)(key) + return getattr(self, '_frozen', super().__missing__)(key) def freeze(self): self._frozen = lambda key: self.default_factory() From 08b2cb88ea418ec78af93bd6e6994c472cb677b0 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 27 Jul 2024 13:40:20 -0400 Subject: [PATCH 6/8] _SupportsComparison doesn't exist at runtime --- jaraco/collections/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jaraco/collections/__init__.py b/jaraco/collections/__init__.py index e2fd11a..0da197d 100644 --- a/jaraco/collections/__init__.py +++ b/jaraco/collections/__init__.py @@ -18,9 +18,13 @@ from _typeshed import SupportsKeysAndGetItem from typing_extensions import Self + _RangeMapKT = TypeVar('_RangeMapKT', bound=_SupportsComparison) + _T = TypeVar('_T') -_RangeMapKT = TypeVar('_RangeMapKT', bound=_SupportsComparison) _VT = TypeVar('_VT') +# _SupportsComparison doesn't exist at runtime, +# but _RangeMapKT is used in RangeMap's superclass' type parameters +_RangeMapKT = TypeVar('_RangeMapKT') _Matchable = Union[Callable, Container, Iterable, re.Pattern] From 4c53f9b74a4cc08bcb223e800045cc41f01739b6 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 27 Jul 2024 13:46:40 -0400 Subject: [PATCH 7/8] Make mypy happy --- jaraco/collections/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/jaraco/collections/__init__.py b/jaraco/collections/__init__.py index 0da197d..0a3d4a9 100644 --- a/jaraco/collections/__init__.py +++ b/jaraco/collections/__init__.py @@ -19,12 +19,13 @@ from typing_extensions import Self _RangeMapKT = TypeVar('_RangeMapKT', bound=_SupportsComparison) +else: + # _SupportsComparison doesn't exist at runtime, + # but _RangeMapKT is used in RangeMap's superclass' type parameters + _RangeMapKT = TypeVar('_RangeMapKT') _T = TypeVar('_T') _VT = TypeVar('_VT') -# _SupportsComparison doesn't exist at runtime, -# but _RangeMapKT is used in RangeMap's superclass' type parameters -_RangeMapKT = TypeVar('_RangeMapKT') _Matchable = Union[Callable, Container, Iterable, re.Pattern] From 228c7ba412c7422c94f6eaf18fcb458b483f9f45 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Aug 2024 13:40:36 -0400 Subject: [PATCH 8/8] Apply nitpick ignores to unblock docs builds. --- docs/conf.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 63766e0..f982072 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,3 +54,16 @@ nitpick_ignore += [ ('py:class', 're.Pattern'), ] + +# jaraco/jaraco.collections#16 +nitpick_ignore += [ + ('py:class', 'SupportsKeysAndGetItem'), + ('py:class', '_RangeMapKT'), + ('py:class', '_VT'), + ('py:class', '_T'), + ('py:class', 'jaraco.collections._RangeMapKT'), + ('py:class', 'jaraco.collections._VT'), + ('py:class', 'jaraco.collections._T'), + ('py:obj', 'jaraco.collections._RangeMapKT'), + ('py:obj', 'jaraco.collections._VT'), +]