[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