[Python-modules-commits] [locket] 01/03: import locket_0.2.0.orig.tar.gz

Diane Trout diane at moszumanska.debian.org
Wed Dec 14 05:13:19 UTC 2016


This is an automated email from the git hooks/post-receive script.

diane pushed a commit to branch master
in repository locket.

commit 771fba7d5a0abbb305af5dbbe44307d441502789
Author: Diane Trout <diane at ghic.org>
Date:   Tue Dec 13 21:00:50 2016 -0800

    import locket_0.2.0.orig.tar.gz
---
 .gitignore            |   6 ++
 .travis.yml           |  14 +++
 LICENSE               |  26 +++++
 MANIFEST.in           |   1 +
 README.rst            |  47 +++++++++
 locket/__init__.py    | 166 ++++++++++++++++++++++++++++++
 makefile              |  27 +++++
 setup.py              |  33 ++++++
 test-requirements.txt |   2 +
 tests/__init__.py     |   0
 tests/locker.py       |  37 +++++++
 tests/locket_tests.py | 280 ++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/tempdir.py      |  13 +++
 tox.ini               |   7 ++
 14 files changed, 659 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c504e9d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/_virtualenv
+*.pyc
+/locket.egg-info
+/README
+/MANIFEST
+/.tox
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..917e240
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,14 @@
+language: 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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d59a0df
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright (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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..8dce522
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include LICENSE README.rst
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..f21a10c
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,47 @@
+locket.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.
diff --git a/locket/__init__.py b/locket/__init__.py
new file mode 100644
index 0000000..8f38bb9
--- /dev/null
+++ b/locket/__init__.py
@@ -0,0 +1,166 @@
+import 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
diff --git a/makefile b/makefile
new file mode 100644
index 0000000..8ab1d94
--- /dev/null
+++ b/makefile
@@ -0,0 +1,27 @@
+.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
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..3cd8ed0
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,33 @@
+#!/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 at 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',
+    ],
+)
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..38dfffe
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,2 @@
+nose>=1.2.1,<2
+spur.local>=0.3.7,<0.4
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/locker.py b/tests/locker.py
new file mode 100644
index 0000000..198d7ec
--- /dev/null
+++ b/tests/locker.py
@@ -0,0 +1,37 @@
+#!/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")
diff --git a/tests/locket_tests.py b/tests/locket_tests.py
new file mode 100644
index 0000000..8a257ac
--- /dev/null
+++ b/tests/locket_tests.py
@@ -0,0 +1,280 @@
+import 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()
+
+
+ at 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
+
+
+ at 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
+
+
+ at 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
+
+
+ at 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
+
+
+ at 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
+
+
+ at 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
+
+
+ at 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
+
+
+ at 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
+
+
+ at 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
+            
+
+ at 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()
+
+
+ at 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()
+
+
+ at 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()
+
+
+ at 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()
+
+
+ at 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")
diff --git a/tests/tempdir.py b/tests/tempdir.py
new file mode 100644
index 0000000..b977c1f
--- /dev/null
+++ b/tests/tempdir.py
@@ -0,0 +1,13 @@
+import contextlib
+import tempfile
+import shutil
+
+
+ at contextlib.contextmanager
+def create_temporary_dir():
+    try:
+        temp_dir = tempfile.mkdtemp()
+        yield temp_dir
+    finally:
+        shutil.rmtree(temp_dir)
+
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..000d8c0
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,7 @@
+[tox]
+envlist = py26,py27,py32,py33,py34,pypy
+[testenv]
+changedir = {envtmpdir}
+deps=-r{toxinidir}/test-requirements.txt
+commands=
+    nosetests {toxinidir}/tests

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/locket.git



More information about the Python-modules-commits mailing list