from typing import Callable


def with_lock(func: Callable):
    """Use as decorator (@withlock) around object methods that need locking.

    Note: The object must have a self._lock = threading.Lock() property.
    Locking thus works on the object level (no two locked methods of the same
    object can be called asynchronously).

    Args:
        func (Callable): The function to decorate/wrap.

    Returns:
        Callable: The wrapped (object-level locked) function.
    """

    def wrapper(self, *a, **k):
        try:
            with self._lock:
                return func(self, *a, **k)
        except AttributeError:
            raise AttributeError(
                "Object {} must have a `self._lock` property (assigned to a "
                "threading.RLock() object in its constructor)!".format(self))

    return wrapper
