#!/usr/bin/env python3
"""
General-purpose utilities used throughout the project.
"""
import functools
import contextlib
[docs]class UsageError (Exception):
pass
[docs]class NoParentError (UsageError):
def __init__(self, widget):
super().__init__("widget '{}' has not been attached to the GUI.".format(widget))
[docs]class HoldUpdatesMixin:
[docs] def __init__(self, num_holds=0):
super().__init__()
self._num_holds = num_holds
self._pending_updates = []
[docs] def pause_updates(self):
self._num_holds += 1
[docs] def resume_updates(self):
self._num_holds = max(self._num_holds - 1, 0)
if self._num_holds == 0:
for update, args, kwargs in self._filter_pending_updates():
update(self, *args, **kwargs)
self._pending_updates = []
[docs] def discard_updates(self):
self._num_holds = max(self._num_holds - 1, 0)
if self._num_holds == 0:
self._pending_updates = []
[docs] @contextlib.contextmanager
def hold_updates(self):
self.pause_updates()
yield
self.resume_updates()
[docs] @contextlib.contextmanager
def suppress_updates(self):
self.pause_updates()
yield
self.discard_updates()
[docs] def _filter_pending_updates(self):
"""
Return all the updates that need to be applied, from a list of all the
updates that were called while the hold was active. This method is
meant to be overridden by subclasses that want to customize how held
updates are applied.
The `self._pending_updates` member variable is a list containing a
(method, args, kwargs) tuple for each update that was called while
updates were being held. This list is in the order that the updates
were actually called, and any updates that were called more than once
will appear in this list more than once.
This method should yield or return a list of the tuples in the same
format representing the updates that should be applied, in the order
they should be applied. The default implementation filters out
duplicate updates without changing their order. In cases where it
matters, the last call to each update is used to determine the order.
"""
from more_itertools import unique_everseen as unique
yield from reversed(list(unique(reversed(self._pending_updates))))
[docs]def update_function(method):
@functools.wraps(method)
def wrapped_method(self, *args, **kwargs):
if self._num_holds > 0:
self._pending_updates.append((method, args, kwargs))
else:
method(self, *args, **kwargs)
return wrapped_method
[docs]def register_event_type(*event_types):
def decorator(cls):
for event_type in event_types:
cls.register_event_type(event_type)
return cls
return decorator
[docs]def late_binding_property(fget=None, fset=None, fdel=None, doc=None):
import functools
if fget is not None:
def __get__(obj, objtype=None, name=fget.__name__):
fget = getattr(obj, name)
return fget()
fget = functools.update_wrapper(__get__, fget)
if fset is not None:
def __set__(obj, value, name=fset.__name__):
fset = getattr(obj, name)
return fset(value)
fset = functools.update_wrapper(__set__, fset)
if fdel is not None:
def __delete__(obj, name=fdel.__name__):
fdel = getattr(obj, name)
return fdel()
fdel = functools.update_wrapper(__delete__, fdel)
return property(fget, fset, fdel, doc)
[docs]def first_not_none(iterable):
n = 0
for x in iterable:
if x is not None:
return x
n += 1
raise ValueError("No values given." if n == 0 else f"All {n} values `None`.")
[docs]def clamp(value, low, high):
return max(low, min(value, high))