diff --git a/ChangeLog b/ChangeLog index 6218eaa..ffcf95e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,8 @@ 0.7: + - Add helpful `locked` decorator that can + lock a method using a found attribute (a lock + object or list of lock objects) in the + instance the method is attached to. 0.6: - Allow the sleep function to be provided (so that various alternatives other than time.sleep can diff --git a/fasteners/__init__.py b/fasteners/__init__.py index fe4bb17..2425d65 100644 --- a/fasteners/__init__.py +++ b/fasteners/__init__.py @@ -20,6 +20,7 @@ from __future__ import absolute_import +from fasteners.lock import locked # noqa from fasteners.lock import read_locked # noqa from fasteners.lock import write_locked # noqa diff --git a/fasteners/lock.py b/fasteners/lock.py index 2b66256..7393fe4 100644 --- a/fasteners/lock.py +++ b/fasteners/lock.py @@ -17,12 +17,19 @@ # License for the specific language governing permissions and limitations # under the License. +try: + from contextlib import ExitStack # noqa +except ImportError: + from contextlib2 import ExitStack # noqa + import collections import contextlib import threading import six +from fasteners import _utils + try: # Used for the reader-writer lock get the right # thread 'hack' (needed below). @@ -228,3 +235,63 @@ class ReaderWriterLock(object): with self._cond: self._writer = None self._cond.notify_all() + + +@contextlib.contextmanager +def try_lock(lock): + """Attempts to acquire a lock, and auto releases if acquired (on exit).""" + # NOTE(harlowja): the keyword argument for 'blocking' does not work + # in py2.x and only is fixed in py3.x (this adjustment is documented + # and/or debated in http://bugs.python.org/issue10789); so we'll just + # stick to the format that works in both (oddly the keyword argument + # works in py2.x but only with reentrant locks). + was_locked = lock.acquire(False) + try: + yield was_locked + finally: + if was_locked: + lock.release() + + +def locked(*args, **kwargs): + """A locking **method** decorator. + + It will look for a provided attribute (typically a lock or a list + of locks) on the first argument of the function decorated (typically this + is the 'self' object) and before executing the decorated function it + activates the given lock or list of locks as a context manager, + automatically releasing that lock on exit. + + NOTE(harlowja): if no attribute name is provided then by default the + attribute named '_lock' is looked for (this attribute is expected to be + the lock/list of locks object/s) in the instance object this decorator + is attached to. + """ + + def decorator(f): + attr_name = kwargs.get('lock', '_lock') + + @six.wraps(f) + def wrapper(self, *args, **kwargs): + attr_value = getattr(self, attr_name) + if isinstance(attr_value, (tuple, list)): + with ExitStack() as stack: + for lock in attr_value: + stack.enter_context(lock) + return f(self, *args, **kwargs) + else: + lock = attr_value + with lock: + return f(self, *args, **kwargs) + + return wrapper + + # This is needed to handle when the decorator has args or the decorator + # doesn't have args, python is rather weird here... + if kwargs or not args: + return decorator + else: + if len(args) == 1: + return decorator(args[0]) + else: + return decorator diff --git a/setup.py b/setup.py index 9dc0030..03b212b 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,9 @@ with open("README.rst", "r") as readme: install_requires = [ 'six', + # Only needed for <= python 3.3, replace me with requirement + # markers in the future... + 'contextlib2', ] setup(