pax_global_header00006660000000000000000000000064125267314160014521gustar00rootroot0000000000000052 comment=bf54f5f941aeb88f55a68966ce70d3e5741b2ff2 locket.py-0.2.0/000077500000000000000000000000001252673141600134305ustar00rootroot00000000000000locket.py-0.2.0/.gitignore000066400000000000000000000000741252673141600154210ustar00rootroot00000000000000/_virtualenv *.pyc /locket.egg-info /README /MANIFEST /.tox locket.py-0.2.0/.travis.yml000066400000000000000000000003711252673141600155420ustar00rootroot00000000000000language: python python: - "2.6" - "2.7" - "3.2" - "3.3" - "pypy" install: - "touch README" - "pip install . --use-mirrors" - "pip install -r test-requirements.txt --use-mirrors" script: nosetests tests notifications: email: false locket.py-0.2.0/LICENSE000066400000000000000000000027621252673141600144440ustar00rootroot00000000000000Copyright (c) 2012, Michael Williamson All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project. locket.py-0.2.0/MANIFEST.in000066400000000000000000000000331252673141600151620ustar00rootroot00000000000000include LICENSE README.rst locket.py-0.2.0/README.rst000066400000000000000000000026631252673141600151260ustar00rootroot00000000000000locket.py ========= Locket implements a lock that can be used by multiple processes provided they use the same path. .. code-block:: python import locket # Wait for lock with locket.lock_file("path/to/lock/file"): perform_action() # Raise error if lock cannot be acquired immediately with locket.lock_file("path/to/lock/file", timeout=0): perform_action() # Raise error if lock cannot be acquired after thirty seconds with locket.lock_file("path/to/lock/file", timeout=30): perform_action() # Without context managers: lock = locket.lock_file("path/to/lock/file") try: lock.acquire() perform_action() finally: lock.release() Locks largely behave as (non-reentrant) `Lock` instances from the `threading` module in the standard library. Specifically, their behaviour is: * Locks are uniquely identified by the file being locked, both in the same process and across different processes. * Locks are either in a locked or unlocked state. * When the lock is unlocked, calling `acquire()` returns immediately and changes the lock state to locked. * When the lock is locked, calling `acquire()` will block until the lock state changes to unlocked, or until the timeout expires. * If a process holds a lock, any thread in that process can call `release()` to change the state to unlocked. * Behaviour of locks after `fork` is undefined. locket.py-0.2.0/locket/000077500000000000000000000000001252673141600147115ustar00rootroot00000000000000locket.py-0.2.0/locket/__init__.py000066400000000000000000000104401252673141600170210ustar00rootroot00000000000000import time import errno import threading import weakref __all__ = ["lock_file"] try: import fcntl except ImportError: try: import msvcrt except ImportError: raise ImportError("Platform not supported (failed to import fcntl, msvcrt)") else: _lock_file_blocking_available = False def _lock_file_non_blocking(file_): try: msvcrt.locking(file_.fileno(), msvcrt.LK_NBLCK, 1) return True # TODO: check errno except IOError: return False def _unlock_file(file_): msvcrt.locking(file_.fileno(), msvcrt.LK_UNLCK, 1) else: _lock_file_blocking_available = True def _lock_file_blocking(file_): fcntl.flock(file_.fileno(), fcntl.LOCK_EX) def _lock_file_non_blocking(file_): try: fcntl.flock(file_.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) return True except IOError as error: if error.errno in [errno.EACCES, errno.EAGAIN]: return False else: raise def _unlock_file(file_): fcntl.flock(file_.fileno(), fcntl.LOCK_UN) _locks_lock = threading.Lock() _locks = weakref.WeakValueDictionary() def lock_file(path, **kwargs): _locks_lock.acquire() try: lock = _locks.get(path) if lock is None: lock = _create_lock_file(path, **kwargs) _locks[path] = lock return lock finally: _locks_lock.release() def _create_lock_file(path, **kwargs): thread_lock = _ThreadLock(path, **kwargs) file_lock = _LockFile(path, **kwargs) return _LockSet([thread_lock, file_lock]) class LockError(Exception): pass def _acquire_non_blocking(acquire, timeout, retry_period, path): if retry_period is None: retry_period = 0.05 start_time = time.time() while True: success = acquire() if success: return elif (timeout is not None and time.time() - start_time > timeout): raise LockError("Couldn't lock {0}".format(path)) else: time.sleep(retry_period) class _LockSet(object): def __init__(self, locks): self._locks = locks def acquire(self): acquired_locks = [] try: for lock in self._locks: lock.acquire() acquired_locks.append(lock) except: for acquired_lock in reversed(acquired_locks): # TODO: handle exceptions acquired_lock.release() raise def release(self): for lock in reversed(self._locks): # TODO: Handle exceptions lock.release() def __enter__(self): self.acquire() return self def __exit__(self, *args): self.release() class _ThreadLock(object): def __init__(self, path, timeout=None, retry_period=None): self._path = path self._timeout = timeout self._retry_period = retry_period self._lock = threading.Lock() def acquire(self): if self._timeout is None: self._lock.acquire() else: _acquire_non_blocking( acquire=lambda: self._lock.acquire(False), timeout=self._timeout, retry_period=self._retry_period, path=self._path, ) def release(self): self._lock.release() class _LockFile(object): def __init__(self, path, timeout=None, retry_period=None): self._path = path self._timeout = timeout self._retry_period = retry_period self._file = None self._thread_lock = threading.Lock() def acquire(self): if self._file is None: self._file = open(self._path, "w") if self._timeout is None and _lock_file_blocking_available: _lock_file_blocking(self._file) else: _acquire_non_blocking( acquire=lambda: _lock_file_non_blocking(self._file), timeout=self._timeout, retry_period=self._retry_period, path=self._path, ) def release(self): _unlock_file(self._file) self._file.close() self._file = None locket.py-0.2.0/makefile000066400000000000000000000010221252673141600151230ustar00rootroot00000000000000.PHONY: test upload clean bootstrap test: sh -c '. _virtualenv/bin/activate; nosetests tests' upload: python setup.py sdist upload make clean register: python setup.py register clean: rm -f MANIFEST rm -rf dist bootstrap: _virtualenv _virtualenv/bin/pip install -e . ifneq ($(wildcard test-requirements.txt),) _virtualenv/bin/pip install -r test-requirements.txt endif make clean _virtualenv: virtualenv _virtualenv _virtualenv/bin/pip install --upgrade pip _virtualenv/bin/pip install --upgrade setuptools locket.py-0.2.0/setup.py000066400000000000000000000021071252673141600151420ustar00rootroot00000000000000#!/usr/bin/env python import os from distutils.core import setup def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() setup( name='locket', version='0.2.0', description='File-based locks for Python for Linux and Windows', long_description=read("README.rst"), author='Michael Williamson', author_email='mike@zwobble.org', url='http://github.com/mwilliamson/locket.py', packages=['locket'], keywords="lock filelock lockfile process", classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Operating System :: Unix', 'Operating System :: Microsoft :: Windows', ], ) locket.py-0.2.0/test-requirements.txt000066400000000000000000000000461252673141600176710ustar00rootroot00000000000000nose>=1.2.1,<2 spur.local>=0.3.7,<0.4 locket.py-0.2.0/tests/000077500000000000000000000000001252673141600145725ustar00rootroot00000000000000locket.py-0.2.0/tests/__init__.py000066400000000000000000000000001252673141600166710ustar00rootroot00000000000000locket.py-0.2.0/tests/locker.py000066400000000000000000000013111252673141600164170ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import print_function import sys import os import locket def _print(output): print(output) sys.stdout.flush() if __name__ == "__main__": print(os.getpid()) lock_path = sys.argv[1] if sys.argv[2] == "None": timeout = None else: timeout = float(sys.argv[2]) lock = locket.lock_file(lock_path, timeout=timeout) _print("Send newline to stdin to acquire") sys.stdin.readline() try: lock.acquire() except locket.LockError: _print("LockError") exit(1) _print("Acquired") _print("Send newline to stdin to release") sys.stdin.readline() lock.release() _print("Released") locket.py-0.2.0/tests/locket_tests.py000066400000000000000000000152311252673141600176510ustar00rootroot00000000000000import functools import os import io import sys import time import signal from nose.tools import istest, nottest import spur import locket from .tempdir import create_temporary_dir local_shell = spur.LocalShell() @nottest def test(func): @functools.wraps(func) @istest def run_test(): with create_temporary_dir() as temp_dir: lock_path = os.path.join(temp_dir, "some-lock") return func(lock_path) return run_test @test def single_process_can_obtain_uncontested_lock(lock_path): has_run = False with locket.lock_file(lock_path): has_run = True assert has_run @test def lock_can_be_acquired_with_timeout_of_zero(lock_path): has_run = False with locket.lock_file(lock_path, timeout=0): has_run = True assert has_run @test def lock_is_released_by_context_manager_exit(lock_path): has_run = False # Keep a reference to first_lock so it holds onto the lock first_lock = locket.lock_file(lock_path, timeout=0) with first_lock: pass with locket.lock_file(lock_path, timeout=0): has_run = True assert has_run @test def can_use_acquire_and_release_to_control_lock(lock_path): has_run = False lock = locket.lock_file(lock_path) lock.acquire() try: has_run = True finally: lock.release() assert has_run @test def thread_cannot_obtain_lock_using_same_object_twice_without_release(lock_path): with locket.lock_file(lock_path, timeout=0) as lock: try: lock.acquire() assert False, "Expected LockError" except locket.LockError: pass @test def thread_cannot_obtain_lock_using_same_path_twice_without_release(lock_path): with locket.lock_file(lock_path, timeout=0): lock = locket.lock_file(lock_path, timeout=0) try: lock.acquire() assert False, "Expected LockError" except locket.LockError: pass @test def the_same_lock_file_object_is_used_for_the_same_path(lock_path): first_lock = locket.lock_file(lock_path, timeout=0) second_lock = locket.lock_file(lock_path, timeout=0) assert first_lock is second_lock @test def different_file_objects_are_used_for_different_paths(lock_path): first_lock = locket.lock_file(lock_path, timeout=0) second_lock = locket.lock_file(lock_path + "-2", timeout=0) assert first_lock is not second_lock @test def lock_file_blocks_until_lock_is_available(lock_path): locker_1 = Locker(lock_path) locker_2 = Locker(lock_path) assert not locker_1.has_lock() assert not locker_2.has_lock() locker_1.acquire() time.sleep(0.1) locker_2.acquire() time.sleep(0.1) assert locker_1.has_lock() assert not locker_2.has_lock() locker_1.release() time.sleep(0.1) assert not locker_1.has_lock() assert locker_2.has_lock() locker_2.release() time.sleep(0.1) assert not locker_1.has_lock() assert not locker_2.has_lock() @test def lock_is_released_if_holding_process_is_brutally_killed(lock_path): locker_1 = Locker(lock_path) locker_2 = Locker(lock_path) assert not locker_1.has_lock() assert not locker_2.has_lock() locker_1.acquire() time.sleep(0.1) locker_2.acquire() time.sleep(0.1) assert locker_1.has_lock() assert not locker_2.has_lock() locker_1.kill(signal.SIGKILL) time.sleep(0.1) assert locker_2.has_lock() @test def can_set_timeout_to_zero_to_raise_exception_if_lock_cannot_be_acquired(lock_path): locker_1 = Locker(lock_path) locker_2 = Locker(lock_path, timeout=0) assert not locker_1.has_lock() assert not locker_2.has_lock() locker_1.acquire() time.sleep(0.1) locker_2.acquire() time.sleep(0.1) assert locker_1.has_lock() assert not locker_2.has_lock() locker_1.release() time.sleep(0.1) assert not locker_1.has_lock() assert not locker_2.has_lock() assert locker_2.has_error() @test def error_is_raised_after_timeout_has_expired(lock_path): locker_1 = Locker(lock_path) locker_2 = Locker(lock_path, timeout=0.5) assert not locker_1.has_lock() assert not locker_2.has_lock() locker_1.acquire() time.sleep(0.1) locker_2.acquire() time.sleep(0.1) assert locker_1.has_lock() assert not locker_2.has_lock() assert not locker_2.has_error() time.sleep(1) assert locker_1.has_lock() assert not locker_2.has_lock() assert locker_2.has_error() @test def lock_is_acquired_if_available_before_timeout_expires(lock_path): locker_1 = Locker(lock_path) locker_2 = Locker(lock_path, timeout=2) assert not locker_1.has_lock() assert not locker_2.has_lock() locker_1.acquire() time.sleep(0.1) locker_2.acquire() time.sleep(0.1) assert locker_1.has_lock() assert not locker_2.has_lock() assert not locker_2.has_error() time.sleep(0.5) locker_1.release() time.sleep(0.1) assert not locker_1.has_lock() assert locker_2.has_lock() def _lockers(number_of_lockers, lock_path): return tuple(Locker(lock_path) for i in range(number_of_lockers)) class Locker(object): def __init__(self, path, timeout=None): self._stdout = io.BytesIO() self._stderr = io.BytesIO() self._process = local_shell.spawn( [sys.executable, _locker_script_path, path, str(timeout)], stdout=self._stdout, stderr=self._stderr, ) def acquire(self): self._process.stdin_write(b"\n") def release(self): self._process.stdin_write(b"\n") def wait_for_lock(self): start_time = time.time() while not self.has_lock(): if not self._process.is_running(): raise self._process.wait_for_result().to_error() time.sleep(0.1) if time.time() - start_time > 1: raise RuntimeError("Could not acquire lock, stdout:\n{0}".format(self._stdout.getvalue())) def has_lock(self): lines = self._stdout_lines() return "Acquired" in lines and "Released" not in lines def has_error(self): return "LockError" in self._stdout_lines() def kill(self, signal): pid = int(self._stdout_lines()[0].strip()) os.kill(pid, signal) def _stdout_lines(self): output = self._stdout.getvalue().decode("ascii") return [line.strip() for line in output.split("\n")] _locker_script_path = os.path.join(os.path.dirname(__file__), "locker.py") locket.py-0.2.0/tests/tempdir.py000066400000000000000000000003351252673141600166110ustar00rootroot00000000000000import contextlib import tempfile import shutil @contextlib.contextmanager def create_temporary_dir(): try: temp_dir = tempfile.mkdtemp() yield temp_dir finally: shutil.rmtree(temp_dir) locket.py-0.2.0/tox.ini000066400000000000000000000002431252673141600147420ustar00rootroot00000000000000[tox] envlist = py26,py27,py32,py33,py34,pypy [testenv] changedir = {envtmpdir} deps=-r{toxinidir}/test-requirements.txt commands= nosetests {toxinidir}/tests