Update 2025-04-24_11:44:19
This commit is contained in:
@ -0,0 +1,194 @@
|
||||
import collections
|
||||
import functools
|
||||
import itertools
|
||||
import operator
|
||||
|
||||
|
||||
# from jaraco.collections 3.5.1
|
||||
class DictStack(list, collections.abc.Mapping):
|
||||
"""
|
||||
A stack of dictionaries that behaves as a view on those dictionaries,
|
||||
giving preference to the last.
|
||||
|
||||
>>> stack = DictStack([dict(a=1, c=2), dict(b=2, a=2)])
|
||||
>>> stack['a']
|
||||
2
|
||||
>>> stack['b']
|
||||
2
|
||||
>>> stack['c']
|
||||
2
|
||||
>>> len(stack)
|
||||
3
|
||||
>>> stack.push(dict(a=3))
|
||||
>>> stack['a']
|
||||
3
|
||||
>>> set(stack.keys()) == set(['a', 'b', 'c'])
|
||||
True
|
||||
>>> set(stack.items()) == set([('a', 3), ('b', 2), ('c', 2)])
|
||||
True
|
||||
>>> dict(**stack) == dict(stack) == dict(a=3, c=2, b=2)
|
||||
True
|
||||
>>> d = stack.pop()
|
||||
>>> stack['a']
|
||||
2
|
||||
>>> d = stack.pop()
|
||||
>>> stack['a']
|
||||
1
|
||||
>>> stack.get('b', None)
|
||||
>>> 'c' in stack
|
||||
True
|
||||
"""
|
||||
|
||||
def __iter__(self):
|
||||
dicts = list.__iter__(self)
|
||||
return iter(set(itertools.chain.from_iterable(c.keys() for c in dicts)))
|
||||
|
||||
def __getitem__(self, key):
|
||||
for scope in reversed(tuple(list.__iter__(self))):
|
||||
if key in scope:
|
||||
return scope[key]
|
||||
raise KeyError(key)
|
||||
|
||||
push = list.append
|
||||
|
||||
def __contains__(self, other):
|
||||
return collections.abc.Mapping.__contains__(self, other)
|
||||
|
||||
def __len__(self):
|
||||
return len(list(iter(self)))
|
||||
|
||||
|
||||
# from jaraco.collections 3.7
|
||||
class RangeMap(dict):
|
||||
"""
|
||||
A dictionary-like object that uses the keys as bounds for a range.
|
||||
Inclusion of the value for that range is determined by the
|
||||
key_match_comparator, which defaults to less-than-or-equal.
|
||||
A value is returned for a key if it is the first key that matches in
|
||||
the sorted list of keys.
|
||||
|
||||
One may supply keyword parameters to be passed to the sort function used
|
||||
to sort keys (i.e. key, reverse) as sort_params.
|
||||
|
||||
Let's create a map that maps 1-3 -> 'a', 4-6 -> 'b'
|
||||
|
||||
>>> r = RangeMap({3: 'a', 6: 'b'}) # boy, that was easy
|
||||
>>> r[1], r[2], r[3], r[4], r[5], r[6]
|
||||
('a', 'a', 'a', 'b', 'b', 'b')
|
||||
|
||||
Even float values should work so long as the comparison operator
|
||||
supports it.
|
||||
|
||||
>>> r[4.5]
|
||||
'b'
|
||||
|
||||
But you'll notice that the way rangemap is defined, it must be open-ended
|
||||
on one side.
|
||||
|
||||
>>> r[0]
|
||||
'a'
|
||||
>>> r[-1]
|
||||
'a'
|
||||
|
||||
One can close the open-end of the RangeMap by using undefined_value
|
||||
|
||||
>>> r = RangeMap({0: RangeMap.undefined_value, 3: 'a', 6: 'b'})
|
||||
>>> r[0]
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
KeyError: 0
|
||||
|
||||
One can get the first or last elements in the range by using RangeMap.Item
|
||||
|
||||
>>> last_item = RangeMap.Item(-1)
|
||||
>>> r[last_item]
|
||||
'b'
|
||||
|
||||
.last_item is a shortcut for Item(-1)
|
||||
|
||||
>>> r[RangeMap.last_item]
|
||||
'b'
|
||||
|
||||
Sometimes it's useful to find the bounds for a RangeMap
|
||||
|
||||
>>> r.bounds()
|
||||
(0, 6)
|
||||
|
||||
RangeMap supports .get(key, default)
|
||||
|
||||
>>> r.get(0, 'not found')
|
||||
'not found'
|
||||
|
||||
>>> r.get(7, 'not found')
|
||||
'not found'
|
||||
|
||||
One often wishes to define the ranges by their left-most values,
|
||||
which requires use of sort params and a key_match_comparator.
|
||||
|
||||
>>> r = RangeMap({1: 'a', 4: 'b'},
|
||||
... 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')
|
||||
|
||||
That wasn't nearly as easy as before, so an alternate constructor
|
||||
is provided:
|
||||
|
||||
>>> r = RangeMap.left({1: 'a', 4: 'b', 7: RangeMap.undefined_value})
|
||||
>>> r[1], r[2], r[3], r[4], r[5], r[6]
|
||||
('a', 'a', 'a', 'b', 'b', 'b')
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, source, sort_params={}, key_match_comparator=operator.le):
|
||||
dict.__init__(self, source)
|
||||
self.sort_params = sort_params
|
||||
self.match = key_match_comparator
|
||||
|
||||
@classmethod
|
||||
def left(cls, source):
|
||||
return cls(
|
||||
source, sort_params=dict(reverse=True), key_match_comparator=operator.ge
|
||||
)
|
||||
|
||||
def __getitem__(self, item):
|
||||
sorted_keys = sorted(self.keys(), **self.sort_params)
|
||||
if isinstance(item, RangeMap.Item):
|
||||
result = self.__getitem__(sorted_keys[item])
|
||||
else:
|
||||
key = self._find_first_match_(sorted_keys, item)
|
||||
result = dict.__getitem__(self, key)
|
||||
if result is RangeMap.undefined_value:
|
||||
raise KeyError(key)
|
||||
return result
|
||||
|
||||
def get(self, key, default=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
|
||||
never raises a KeyError.
|
||||
"""
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def _find_first_match_(self, keys, item):
|
||||
is_match = functools.partial(self.match, item)
|
||||
matches = list(filter(is_match, keys))
|
||||
if matches:
|
||||
return matches[0]
|
||||
raise KeyError(item)
|
||||
|
||||
def bounds(self):
|
||||
sorted_keys = sorted(self.keys(), **self.sort_params)
|
||||
return (sorted_keys[RangeMap.first_item], sorted_keys[RangeMap.last_item])
|
||||
|
||||
# some special values for the RangeMap
|
||||
undefined_value = type(str('RangeValueUndefined'), (), {})()
|
||||
|
||||
class Item(int):
|
||||
"RangeMap Item"
|
||||
|
||||
first_item = Item(0)
|
||||
last_item = Item(-1)
|
Reference in New Issue
Block a user