| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- from abc import ABC, abstractmethod
-
- """
- Subclasses of list, set, and dict with special behaviors.
- """
-
- # Abstract collections
-
- class AbstractMutableList(list, ABC):
- """
- Abstract list with hooks for custom logic before and after mutation
- operations.
- """
- @abstractmethod
- def pre_mutate(self):
- """
- Called just prior to any mutation operation.
- """
- raise NotImplementedError('subclass must implement pre_mutate')
- @abstractmethod
- def post_mutate(self):
- """
- Called just after any mutating operation before returning the result
- to the caller.
- """
- raise NotImplementedError('subclass must implement post_mutate')
- # Mutating method hooks
- def __delitem__(self, *args):
- self.pre_mutate()
- ret_val = super().__delitem__(*args)
- self.post_mutate()
- return ret_val
- def __iadd__(self, *args):
- self.pre_mutate()
- ret_val = super().__iadd__(*args)
- self.post_mutate()
- return ret_val
- def __imul__(self, *args):
- self.pre_mutate()
- ret_val = super().__imul__(*args)
- self.post_mutate()
- return ret_val
- def __setitem__(self, *args):
- self.pre_mutate()
- ret_val = super().__setitem__(*args)
- self.post_mutate()
- return ret_val
- def append(self, *args):
- self.pre_mutate()
- ret_val = super().append(*args)
- self.post_mutate()
- return ret_val
- def clear(self, *args):
- self.pre_mutate()
- ret_val = super().clear(*args)
- self.post_mutate()
- return ret_val
- def extend(self, *args):
- self.pre_mutate()
- ret_val = super().extend(*args)
- self.post_mutate()
- return ret_val
- def insert(self, *args):
- self.pre_mutate()
- ret_val = super().insert(*args)
- self.post_mutate()
- return ret_val
- def pop(self, *args):
- self.pre_mutate()
- ret_val = super().pop(*args)
- self.post_mutate()
- return ret_val
- def remove(self, *args):
- self.pre_mutate()
- ret_val = super().remove(*args)
- self.post_mutate()
- return ret_val
- def reverse(self, *args):
- self.pre_mutate()
- ret_val = super().reverse(*args)
- self.post_mutate()
- return ret_val
- def sort(self, *args):
- self.pre_mutate()
- ret_val = super().sort(*args)
- self.post_mutate()
- return ret_val
-
- class AbstractMutableSet(set, ABC):
- """
- Abstract set with hooks for custom logic before and after mutation
- operations.
- """
- @abstractmethod
- def pre_mutate(self):
- """
- Called just prior to any mutation operation.
- """
- raise NotImplementedError('subclass must implement pre_mutate')
- @abstractmethod
- def post_mutate(self):
- """
- Called just after any mutating operation before returning the result
- to the caller.
- """
- raise NotImplementedError('subclass must implement post_mutate')
- # Mutating method hooks
- def __iand__(self, *args):
- self.pre_mutate()
- ret_val = super().__iand__(*args)
- self.post_mutate()
- return ret_val
- def __ior__(self, *args):
- self.pre_mutate()
- ret_val = super().__ior__(*args)
- self.post_mutate()
- return ret_val
- def __isub__(self, *args):
- self.pre_mutate()
- ret_val = super().__isub__(*args)
- self.post_mutate()
- return ret_val
- def __ixor__(self, *args):
- self.pre_mutate()
- ret_val = super().__ixor__(*args)
- self.post_mutate()
- return ret_val
- def add(self, *args):
- self.pre_mutate()
- ret_val = super().add(*args)
- self.post_mutate()
- return ret_val
- def clear(self, *args):
- self.pre_mutate()
- ret_val = super().clear(*args)
- self.post_mutate()
- return ret_val
- def difference_update(self, *args):
- self.pre_mutate()
- ret_val = super().difference_update(*args)
- self.post_mutate()
- return ret_val
- def discard(self, *args):
- self.pre_mutate()
- ret_val = super().discard(*args)
- self.post_mutate()
- return ret_val
- def intersection_update(self, *args):
- self.pre_mutate()
- ret_val = super().intersection_update(*args)
- self.post_mutate()
- return ret_val
- def pop(self, *args):
- self.pre_mutate()
- ret_val = super().pop(*args)
- self.post_mutate()
- return ret_val
- def remove(self, *args):
- self.pre_mutate()
- ret_val = super().remove(*args)
- self.post_mutate()
- return ret_val
- def symmetric_difference_update(self, *args):
- self.pre_mutate()
- ret_val = super().symmetric_difference_update(*args)
- self.post_mutate()
- return ret_val
- def update(self, *args):
- self.pre_mutate()
- ret_val = super().update(*args)
- self.post_mutate()
- return ret_val
-
- class AbstractMutableDict(dict, ABC):
- """
- Abstract dict with hooks for custom logic before and after mutation
- operations.
- """
- @abstractmethod
- def pre_mutate(self):
- """
- Called just prior to any mutation operation.
- """
- raise NotImplementedError('subclass must implement pre_mutate')
- @abstractmethod
- def post_mutate(self):
- """
- Called just after any mutating operation before returning the result
- to the caller.
- """
- raise NotImplementedError('subclass must implement post_mutate')
- # Mutating method hooks
- def __delitem__(self, *args):
- self.pre_mutate()
- ret_val = super().__delitem__(*args)
- self.post_mutate()
- return ret_val
- def __ior__(self, *args):
- self.pre_mutate()
- ret_val = super().__ior__(*args)
- self.post_mutate()
- return ret_val
- def __setitem__(self, *args):
- self.pre_mutate()
- ret_val = super().__setitem__(*args)
- self.post_mutate()
- return ret_val
- def clear(self, *args):
- self.pre_mutate()
- ret_val = super().clear(*args)
- self.post_mutate()
- return ret_val
- def pop(self, *args):
- self.pre_mutate()
- ret_val = super().pop(*args)
- self.post_mutate()
- return ret_val
- def popitem(self, *args):
- self.pre_mutate()
- ret_val = super().popitem(*args)
- self.post_mutate()
- return ret_val
- def update(self, *args):
- self.pre_mutate()
- ret_val = super().update(*args)
- self.post_mutate()
- return ret_val
-
- # Collections with limited number of elements
-
- class SizeBoundList(AbstractMutableList):
- """
- Subclass of `list` that enforces a maximum number of elements.
-
- If the number of elements following a mutating operation exceeds
- `self.max_element_count`, then each element will be tested for its "age,"
- and the "oldest" elements will be removed until the total size is back
- within the limit. "Age" is determined via a provided lambda function. It is
- a value with arbitrary numeric value that is used comparitively to find
- the element with the smallest value.
-
- The `element_age` lambda takes two arguments: the element index and the
- element value. It must return values that can be compared to one another
- (e.g. int, float, datetime).
-
- `self.element_age` and `self.max_element_count` can be modified at runtime,
- however elements will only be discarded following the next mutating
- operation. Call `self.purge_old_elements()` to force resizing.
- """
- def __init__(self, max_element_count: int, element_age, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.element_age = element_age
- self.max_element_count = max_element_count
- self.is_culling = False
- self.post_mutate()
-
- def purge_old_elements(self):
- """
- Manually purges elements. Purging usually occurs automatically, but
- this can be called if `self.max_element_count` or `self.element_age`
- are modified at runtime.
- """
- self.post_mutate()
-
- def pre_mutate(self):
- pass
-
- def post_mutate(self):
- if self.is_culling:
- return
- self.is_culling = True
- while len(self) > self.max_element_count:
- oldest_age = None
- oldest_index = -1
- for i in range(len(self)):
- elem = self[i]
- age = self.element_age(i, elem)
- if oldest_age is None or age < oldest_age:
- oldest_age = age
- oldest_index = i
- self.pop(oldest_index)
- self.is_culling = False
-
- def copy():
- return SizeBoundList(max_element_count, element_age, super())
-
- class SizeBoundSet(AbstractMutableSet):
- """
- Subclass of `set` that enforces a maximum number of elements.
-
- If the number of elements following a mutating operation exceeds
- `self.max_element_count`, then each element will be tested for its "age,"
- and the "oldest" elements will be removed until the total size is back
- within the limit. "Age" is determined via a provided lambda function. It is
- a value with arbitrary numeric value that is used comparitively to find
- the element with the smallest value.
-
- The `element_age` lambda takes one argument: the element value. It must
- return values that can be compared to one another (e.g. int, float,
- datetime).
-
- `self.element_age` and `self.max_element_count` can be modified at runtime,
- however elements will only be discarded following the next mutating
- operation. Call `self.purge_old_elements()` to force resizing.
- """
- def __init__(self, max_element_count: int, element_age, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.element_age = element_age
- self.max_element_count = max_element_count
- self.is_culling = False
- self.post_mutate()
-
- def purge_old_elements(self):
- """
- Manually purges elements. Purging usually occurs automatically, but
- this can be called if `self.max_element_count` or `self.element_age`
- are modified at runtime.
- """
- self.post_mutate()
-
- def pre_mutate(self):
- pass
- def post_mutate(self):
- if self.is_culling:
- return
- self.is_culling = True
- while len(self) > self.max_element_count:
- oldest_age = None
- oldest_elem = None
- for elem in self:
- age = self.element_age(elem)
- if oldest_age is None or age < oldest_age:
- oldest_age = age
- oldest_elem = elem
- self.remove(oldest_elem)
- self.is_culling = False
-
- def copy():
- return SizeBoundSet(max_element_count, element_age, super())
-
- class SizeBoundDict(AbstractMutableDict):
- """
- Subclass of `dict` that enforces a maximum number of elements.
-
- If the number of elements following a mutating operation exceeds
- `self.max_element_count`, then each element will be tested for its "age,"
- and the "oldest" elements will be removed until the total size is back
- within the limit. "Age" is determined via a provided lambda function. It is
- a value with arbitrary numeric value that is used comparitively to find
- the element with the smallest value.
-
- The `element_age` lambda takes two arguments: the key and the value of a
- dict pair. It must return values that can be compared to one another (e.g.
- int, float, datetime).
-
- `self.element_age` and `self.max_element_count` can be modified at runtime,
- however elements will only be discarded following the next mutating
- operation. Call `self.purge_old_elements()` to force resizing.
- """
- def __init__(self, max_element_count = 10, element_age = (lambda key, value: float(key)), *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.element_age = element_age
- self.max_element_count = max_element_count
- self.is_culling = False
- self.post_mutate()
-
- def purge_old_elements(self):
- """
- Manually purges elements. Purging usually occurs automatically, but
- this can be called if `self.max_element_count` or `self.element_age`
- are modified at runtime.
- """
- self.post_mutate()
-
- def pre_mutate(self):
- pass
- def post_mutate(self):
- if self.is_culling:
- return
- self.is_culling = True
- while len(self) > self.max_element_count:
- oldest_age = None
- oldest_key = None
- for key, value in self.items():
- age = self.element_age(key, value)
- if oldest_age is None or age < oldest_age:
- oldest_age = age
- oldest_key = key
- del self[oldest_key]
- self.is_culling = False
-
- def copy():
- return SizeBoundDict(max_element_count, element_age, super())
-
- # Collections with limited age of elements
-
- class AgeBoundList(AbstractMutableList):
- """
- Subclass of `list` that enforces a maximum "age" of elements.
-
- After each mutating operation, the minimum and maximum "age" of the elements
- are determined. If the span between the newest and oldest exceeds
- `self.max_age` then then the oldest elements will be purged until that is
- no longer the case. "Age" is determined via a provided lambda function. It
- is a value with arbitrary numeric value that is used comparitively.
-
- The `element_age` lambda takes two arguments: the element index and the
- element value. It must return values that can be compared to one another
- and be added and subtracted (e.g. int, float, datetime). If the lambda
- returns `datetime`s, the `max_age` should be a `timedelta`.
-
- `self.element_age` and `self.max_age` can be modified at runtime,
- however elements will only be discarded following the next mutating
- operation. Call `self.purge_old_elements()` to force resizing.
- """
- def __init__(self, max_age, element_age, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.max_age = max_age
- self.element_age = element_age
- self.is_culling = False
-
- def purge_old_elements(self):
- """
- Manually purges elements. Purging usually occurs automatically, but
- this can be called if `self.max_age` or `self.element_age` are modified
- at runtime.
- """
- self.post_mutate()
-
- def pre_mutate(self):
- pass
- def post_mutate(self):
- if self.is_culling or len(self) <= 1:
- return
- self.is_culling = True
- min_age = None
- max_age = None
- ages = {}
- for i in range(len(self)):
- elem = self[i]
- age = self.element_age(i, elem)
- ages[i] = age
- if min_age is None or age < min_age:
- min_age = age
- if max_age is None or age > max_age:
- max_age = age
- cutoff = max_age - self.max_age
- if min_age >= cutoff:
- self.is_culling = False
- return
- for i in reversed(range(len(self))):
- if ages[i] < cutoff:
- del self[i]
- self.is_culling = False
-
- class AgeBoundSet(AbstractMutableSet):
- """
- Subclass of `set` that enforces a maximum "age" of elements.
-
- After each mutating operation, the minimum and maximum "age" of the elements
- are determined. If the span between the newest and oldest exceeds
- `self.max_age` then then the oldest elements will be purged until that is
- no longer the case. "Age" is determined via a provided lambda function. It
- is a value with arbitrary numeric value that is used comparitively.
-
- The `element_age` lambda takes one argument: the element value. It must
- return values that can be compared to one another and be added and
- subtracted (e.g. int, float, datetime). If the lambda returns `datetime`s,
- the `max_age` should be a `timedelta`.
-
- `self.element_age` and `self.max_age` can be modified at runtime,
- however elements will only be discarded following the next mutating
- operation. Call `self.purge_old_elements()` to force resizing.
- """
- def __init__(self, max_age, element_age, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.max_age = max_age
- self.element_age = element_age
- self.is_culling = False
-
- def purge_old_elements(self):
- """
- Manually purges elements. Purging usually occurs automatically, but
- this can be called if `self.max_age` or `self.element_age` are modified
- at runtime.
- """
- self.post_mutate()
-
- def pre_mutate(self):
- pass
- def post_mutate(self):
- if self.is_culling or len(self) <= 1:
- return
- self.is_culling = True
- min_age = None
- max_age = None
- ages = {}
- for elem in self:
- age = self.element_age(elem)
- ages[elem] = age
- if min_age is None or age < min_age:
- min_age = age
- if max_age is None or age > max_age:
- max_age = age
- cutoff = max_age - self.max_age
- if min_age >= cutoff:
- self.is_culling = False
- return
- for elem, age in ages.items():
- if age < cutoff:
- self.remove(elem)
- self.is_culling = False
-
- class AgeBoundDict(AbstractMutableDict):
- """
- Subclass of `dict` that enforces a maximum "age" of elements.
-
- After each mutating operation, the minimum and maximum "age" of the elements
- are determined. If the span between the newest and oldest exceeds
- `self.max_age` then then the oldest elements will be purged until that is
- no longer the case. "Age" is determined via a provided lambda function. It
- is a value with arbitrary numeric value that is used comparitively.
-
- The `element_age` lambda takes two arguments: the key and value of a pair.
- It must return values that can be compared to one another and be added and
- subtracted (e.g. int, float, datetime). If the lambda returns `datetime`s,
- the `max_age` should be a `timedelta`.
-
- `self.element_age` and `self.max_age` can be modified at runtime,
- however elements will only be discarded following the next mutating
- operation. Call `self.purge_old_elements()` to force resizing.
- """
- def __init__(self, max_age, element_age, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.max_age = max_age
- self.element_age = element_age
- self.is_culling = False
-
- def purge_old_elements(self):
- """
- Manually purges elements. Purging usually occurs automatically, but
- this can be called if `self.max_age` or `self.element_age` are modified
- at runtime.
- """
- self.post_mutate()
-
- def pre_mutate(self):
- pass
- def post_mutate(self):
- if self.is_culling or len(self) <= 1:
- return
- self.is_culling = True
- min_age = None
- max_age = None
- ages = {}
- for key, value in self.items():
- age = self.element_age(key, value)
- ages[key] = age
- if min_age is None or age < min_age:
- min_age = age
- if max_age is None or age > max_age:
- max_age = age
- cutoff = max_age - self.max_age
- if min_age >= cutoff:
- self.is_culling = False
- return
- for key, age in ages.items():
- if age < cutoff:
- del self[key]
- self.is_culling = False
|