Skip to content

Commit 6ae6e4e

Browse files
committed
Fully typed RangeMap and avoid complete iterations to find matches
1 parent 0d9c357 commit 6ae6e4e

2 files changed

Lines changed: 31 additions & 16 deletions

File tree

jaraco/collections/__init__.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@
88
import random
99
import re
1010
from collections.abc import Container, Iterable, Mapping
11-
from typing import Any, Callable, Union
11+
from typing import TYPE_CHECKING, Any, Callable, Dict, TypeVar, Union, overload
1212

1313
import jaraco.text
1414

15+
if TYPE_CHECKING:
16+
from _typeshed import SupportsKeysAndGetItem
17+
from typing_extensions import Self
18+
19+
_T = TypeVar('_T')
20+
_VT = TypeVar('_VT')
21+
1522
_Matchable = Union[Callable, Container, Iterable, re.Pattern]
1623

1724

@@ -119,7 +126,7 @@ def dict_map(function, dictionary):
119126
return dict((key, function(value)) for key, value in dictionary.items())
120127

121128

122-
class RangeMap(dict):
129+
class RangeMap(Dict[int, _VT]):
123130
"""
124131
A dictionary-like object that uses the keys as bounds for a range.
125132
Inclusion of the value for that range is determined by the
@@ -186,7 +193,7 @@ class RangeMap(dict):
186193
which requires use of sort params and a key_match_comparator.
187194
188195
>>> r = RangeMap({1: 'a', 4: 'b'},
189-
... sort_params=dict(reverse=True),
196+
... sort_params={'reverse': True},
190197
... key_match_comparator=operator.ge)
191198
>>> r[1], r[2], r[3], r[4], r[5], r[6]
192199
('a', 'a', 'a', 'b', 'b', 'b')
@@ -202,21 +209,23 @@ class RangeMap(dict):
202209

203210
def __init__(
204211
self,
205-
source,
212+
source: SupportsKeysAndGetItem[int, _VT] | Iterable[tuple[int, _VT]],
206213
sort_params: Mapping[str, Any] = {},
207-
key_match_comparator=operator.le,
214+
key_match_comparator: Callable[[int, int], bool] = operator.le,
208215
):
209216
dict.__init__(self, source)
210217
self.sort_params = sort_params
211218
self.match = key_match_comparator
212219

213220
@classmethod
214-
def left(cls, source):
221+
def left(
222+
cls, source: SupportsKeysAndGetItem[int, _VT] | Iterable[tuple[int, _VT]]
223+
) -> Self:
215224
return cls(
216-
source, sort_params=dict(reverse=True), key_match_comparator=operator.ge
225+
source, sort_params={'reverse': True}, key_match_comparator=operator.ge
217226
)
218227

219-
def __getitem__(self, item):
228+
def __getitem__(self, item: int) -> _VT:
220229
sorted_keys = sorted(self.keys(), **self.sort_params)
221230
if isinstance(item, RangeMap.Item):
222231
result = self.__getitem__(sorted_keys[item])
@@ -227,7 +236,11 @@ def __getitem__(self, item):
227236
raise KeyError(key)
228237
return result
229238

230-
def get(self, key, default=None):
239+
@overload # type: ignore[override] # Signature simplified over dict and Mapping
240+
def get(self, key: int, default: _T) -> _VT | _T: ...
241+
@overload
242+
def get(self, key: int, default: None = None) -> _VT | None: ...
243+
def get(self, key: int, default: _T | None = None) -> _VT | _T | None:
231244
"""
232245
Return the value for key if key is in the dictionary, else default.
233246
If default is not given, it defaults to None, so that this method
@@ -238,22 +251,23 @@ def get(self, key, default=None):
238251
except KeyError:
239252
return default
240253

241-
def _find_first_match_(self, keys, item):
254+
def _find_first_match_(self, keys: Iterable[int], item: int) -> int:
242255
is_match = functools.partial(self.match, item)
243-
matches = list(filter(is_match, keys))
244-
if matches:
245-
return matches[0]
246-
raise KeyError(item)
256+
matches = filter(is_match, keys)
257+
try:
258+
return next(matches)
259+
except StopIteration:
260+
raise KeyError(item) from None
247261

248-
def bounds(self):
262+
def bounds(self) -> tuple[int, int]:
249263
sorted_keys = sorted(self.keys(), **self.sort_params)
250264
return (sorted_keys[RangeMap.first_item], sorted_keys[RangeMap.last_item])
251265

252266
# some special values for the RangeMap
253267
undefined_value = type('RangeValueUndefined', (), {})()
254268

255269
class Item(int):
256-
"RangeMap Item"
270+
'RangeMap Item'
257271

258272
first_item = Item(0)
259273
last_item = Item(-1)

newsfragments/16.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fully typed ``RangeMap`` and avoid complete iterations to find matches -- by :user:`Avasam`

0 commit comments

Comments
 (0)