pax_global_header00006660000000000000000000000064140016477300014514gustar00rootroot0000000000000052 comment=64dd4c28d5932e273cad305cea0d8d89b3230646 locket.py-0.2.1/000077500000000000000000000000001400164773000134245ustar00rootroot00000000000000locket.py-0.2.1/.github/000077500000000000000000000000001400164773000147645ustar00rootroot00000000000000locket.py-0.2.1/.github/workflows/000077500000000000000000000000001400164773000170215ustar00rootroot00000000000000locket.py-0.2.1/.github/workflows/tests.yml000066400000000000000000000007531400164773000207130ustar00rootroot00000000000000name: Tests on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3] steps: - uses: actions/checkout@v2 - name: Use Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - run: pip install tox - run: tox -e py locket.py-0.2.1/.gitignore000066400000000000000000000000741400164773000154150ustar00rootroot00000000000000/_virtualenv *.pyc /locket.egg-info /README /MANIFEST /.tox locket.py-0.2.1/LICENSE000066400000000000000000000024311400164773000144310ustar00rootroot00000000000000Copyright (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 HOLDER 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. locket.py-0.2.1/MANIFEST.in000066400000000000000000000000331400164773000151560ustar00rootroot00000000000000include LICENSE README.rst locket.py-0.2.1/README.rst000066400000000000000000000027641400164773000151240ustar00rootroot00000000000000locket.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. Installation ------------ .. code-block:: sh pip install locket locket.py-0.2.1/locket/000077500000000000000000000000001400164773000147055ustar00rootroot00000000000000locket.py-0.2.1/locket/__init__.py000066400000000000000000000107741400164773000170270ustar00rootroot00000000000000import 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) _locks[path] = lock finally: _locks_lock.release() return _Locker(lock, **kwargs) def _create_lock_file(path): thread_lock = _ThreadLock(path) file_lock = _LockFile(path) 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, timeout, retry_period): acquired_locks = [] try: for lock in self._locks: lock.acquire(timeout, retry_period) 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() class _ThreadLock(object): def __init__(self, path): self._path = path self._lock = threading.Lock() def acquire(self, timeout=None, retry_period=None): if timeout is None: self._lock.acquire() else: _acquire_non_blocking( acquire=lambda: self._lock.acquire(False), timeout=timeout, retry_period=retry_period, path=self._path, ) def release(self): self._lock.release() class _LockFile(object): def __init__(self, path): self._path = path self._file = None def acquire(self, timeout=None, retry_period=None): if self._file is None: self._file = open(self._path, "wb") if 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=timeout, retry_period=retry_period, path=self._path, ) def release(self): _unlock_file(self._file) self._file.close() self._file = None class _Locker(object): """ A lock wrapper to always apply the given *timeout* and *retry_period* to acquire() calls. """ def __init__(self, lock, timeout=None, retry_period=None): self._lock = lock self._timeout = timeout self._retry_period = retry_period def acquire(self): self._lock.acquire(self._timeout, self._retry_period) def release(self): self._lock.release() def __enter__(self): self.acquire() return self def __exit__(self, *args): self.release() locket.py-0.2.1/makefile000066400000000000000000000011331400164773000151220ustar00rootroot00000000000000.PHONY: test upload clean bootstrap test: sh -c '. _virtualenv/bin/activate; nosetests tests' upload: _virtualenv/bin/python setup.py sdist bdist_wheel 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: python3 -m venv _virtualenv _virtualenv/bin/pip install --upgrade pip _virtualenv/bin/pip install --upgrade setuptools _virtualenv/bin/pip install --upgrade wheel locket.py-0.2.1/setup.cfg000066400000000000000000000000341400164773000152420ustar00rootroot00000000000000[bdist_wheel] universal = 1 locket.py-0.2.1/setup.py000066400000000000000000000024021400164773000151340ustar00rootroot00000000000000#!/usr/bin/env python import os from setuptools import setup def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() setup( name='locket', version='0.2.1', 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", python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', license="BSD-2-Clause", classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Operating System :: Unix', 'Operating System :: Microsoft :: Windows', ], ) locket.py-0.2.1/test-requirements.txt000066400000000000000000000000461400164773000176650ustar00rootroot00000000000000nose>=1.2.1,<2 spur.local>=0.3.7,<0.4 locket.py-0.2.1/tests/000077500000000000000000000000001400164773000145665ustar00rootroot00000000000000locket.py-0.2.1/tests/__init__.py000066400000000000000000000000001400164773000166650ustar00rootroot00000000000000locket.py-0.2.1/tests/locker.py000066400000000000000000000013111400164773000164130ustar00rootroot00000000000000#!/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.1/tests/locket_tests.py000066400000000000000000000170651400164773000176540ustar00rootroot00000000000000import 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 thread_cannot_obtain_lock_using_same_path_with_different_arguments_without_release(lock_path): lock1 = locket.lock_file(lock_path, timeout=None) lock2 = locket.lock_file(lock_path, timeout=0) lock1.acquire() try: lock2.acquire() assert False, "Expected LockError" except locket.LockError: pass @test def the_same_lock_file_object_is_used_for_the_same_path(lock_path): # We explicitly check the same lock is used to ensure that the lock isn't # re-entrant, even if the underlying platform lock is re-entrant. first_lock = locket.lock_file(lock_path, timeout=0) second_lock = locket.lock_file(lock_path, timeout=0) assert first_lock._lock is second_lock._lock @test def the_same_lock_file_object_is_used_for_the_same_path_with_different_arguments(lock_path): # We explicitly check the same lock is used to ensure that the lock isn't # re-entrant, even if the underlying platform lock is re-entrant. first_lock = locket.lock_file(lock_path, timeout=None) second_lock = locket.lock_file(lock_path, timeout=0) assert first_lock._lock is second_lock._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._lock is not second_lock._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.1/tests/tempdir.py000066400000000000000000000003351400164773000166050ustar00rootroot00000000000000import 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.1/tox.ini000066400000000000000000000002631400164773000147400ustar00rootroot00000000000000[tox] envlist = py27,py34,py35,py36,py37,py38,py39,pypy,pypy3 [testenv] changedir = {envtmpdir} deps=-r{toxinidir}/test-requirements.txt commands= nosetests {toxinidir}/tests