diff --git a/.cookiecutterrc b/.cookiecutterrc index 929affc..e758075 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -1,34 +1,32 @@ -# This file exists so you can easily regenerate your project. -# -# Unfortunatelly cookiecutter can't use this right away so -# you have to copy this file to ~/.cookiecutterrc +# Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) -default_context: - - appveyor: 'yes' - c_extension_optional: 'no' - c_extension_support: 'no' - codacy: 'yes' - codeclimate: 'yes' - codecov: 'yes' - command_line_interface: 'no' - coveralls: 'yes' - distribution_name: 'python-redis-lock' - email: 'contact@ionelmc.ro' - full_name: 'Ionel Cristian Mărieș' - github_username: 'ionelmc' - landscape: 'yes' - package_name: 'redis_lock' - project_name: 'redis-lock' - project_short_description: 'Lock context manager implemented via redis SETNX/BLPOP.' - release_date: '2015-08-19' - repo_name: 'python-redis-lock' - requiresio: 'yes' - scrutinizer: 'yes' - sphinx_theme: 'sphinx-py3doc-enhanced-theme' - test_matrix_configurator: 'yes' - test_runner: 'pytest' - travis: 'yes' - version: '2.2.0' - website: 'http://blog.ionelmc.ro' - year: '2013-2015' +cookiecutter: + appveyor: 'no' + bin_name: nameless + c_extension_cython: 'no' + c_extension_optional: 'no' + c_extension_support: 'no' + codacy: 'yes' + codeclimate: 'yes' + codecov: 'yes' + command_line_interface: 'no' + coveralls: 'yes' + distribution_name: python-redis-lock + email: contact@ionelmc.ro + full_name: Ionel Cristian Mărieș + github_username: ionelmc + landscape: 'yes' + package_name: redis_lock + project_name: redis-lock + project_short_description: Lock context manager implemented via redis SETNX/BLPOP. + release_date: today + repo_name: python-redis-lock + requiresio: 'yes' + scrutinizer: 'yes' + sphinx_theme: sphinx-py3doc-enhanced-theme + test_matrix_configurator: 'yes' + test_runner: pytest + travis: 'yes' + version: 2.2.0 + website: http://blog.ionelmc.ro + year: 2013-2015 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4000618 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# see http://editorconfig.org +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 +charset = utf-8 + +[*.{bat,cmd,ps1}] +end_of_line = crlf diff --git a/.gitignore b/.gitignore index cae27ab..9e8fc16 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,5 @@ docs/_build .cache .pytest .bootstrap +.appveyor.token *.bak diff --git a/.travis.yml b/.travis.yml index 8e7eb79..e6b3eba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,13 @@ language: python -python: 2.7 +python: '3.5' sudo: false env: global: - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - SEGFAULT_SIGNALS=all matrix: - TOXENV=check + - TOXENV=2.6-1.4,coveralls,codecov - TOXENV=2.6-1.4-nocover - TOXENV=2.6-1.5,coveralls,codecov @@ -38,6 +40,10 @@ env: - TOXENV=3.4-1.7-nocover - TOXENV=3.4-1.8,coveralls,codecov - TOXENV=3.4-1.8-nocover + - TOXENV=3.5-1.7,coveralls,codecov + - TOXENV=3.5-1.7-nocover + - TOXENV=3.5-1.8,coveralls,codecov + - TOXENV=3.5-1.8-nocover - TOXENV=pypy-1.4,coveralls,codecov - TOXENV=pypy-1.4-nocover - TOXENV=pypy-1.5,coveralls,codecov @@ -63,6 +69,11 @@ script: after_failure: - more .tox/log/* | cat - more .tox/*/log/* | cat +before_cache: + - rm -rf $HOME/.cache/pip/log +cache: + directories: + - $HOME/.cache/pip notifications: email: on_success: never diff --git a/MANIFEST.in b/MANIFEST.in index 49b230a..86daeba 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,6 +9,7 @@ include .coveragerc include .cookiecutterrc include .isort.cfg include .pylintrc +include .editorconfig include AUTHORS.rst include CHANGELOG.rst diff --git a/README.rst b/README.rst index 6103b6e..dec42c7 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ -========== -redis-lock -========== +======== +Overview +======== + +.. start-badges .. list-table:: :stub-columns: 1 @@ -8,7 +10,7 @@ redis-lock * - docs - |docs| * - tests - - | |travis| |appveyor| |requires| + - | |travis| |requires| | |coveralls| |codecov| | |landscape| |scrutinizer| |codacy| |codeclimate| * - package @@ -22,10 +24,6 @@ redis-lock :alt: Travis-CI Build Status :target: https://travis-ci.org/ionelmc/python-redis-lock -.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/ionelmc/python-redis-lock?branch=master&svg=true - :alt: AppVeyor Build Status - :target: https://ci.appveyor.com/project/ionelmc/python-redis-lock - .. |requires| image:: https://requires.io/github/ionelmc/python-redis-lock/requirements.svg?branch=master :alt: Requirements Status :target: https://requires.io/github/ionelmc/python-redis-lock/requirements/?branch=master @@ -73,6 +71,8 @@ redis-lock :alt: Scrutinizer Status :target: https://scrutinizer-ci.com/g/ionelmc/python-redis-lock/ +.. end-badges + Lock context manager implemented via redis SETNX/BLPOP. * Free software: BSD license diff --git a/ci/bootstrap.py b/ci/bootstrap.py index 466bb8f..fdfbaf6 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -31,14 +31,17 @@ exec(compile(open(activate, "rb").read(), activate, "exec"), dict(__file__=activate)) import jinja2 + import matrix + jinja = jinja2.Environment( loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True ) + tox_environments = {} for (alias, conf) in matrix.from_file(join(base_path, "setup.cfg")).items(): python = conf["python_versions"] @@ -57,6 +60,7 @@ if "environment_variables" in conf: tox_environments[alias].update(env_vars=env_vars.split()) + for name in os.listdir(join("ci", "templates")): with open(join(base_path, name), "w") as fh: fh.write(jinja.get_template(name).render(tox_environments=tox_environments)) diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml index d6074d0..b2c08b4 100644 --- a/ci/templates/.travis.yml +++ b/ci/templates/.travis.yml @@ -1,15 +1,16 @@ language: python -python: 2.7 +python: '3.5' sudo: false env: global: - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - SEGFAULT_SIGNALS=all matrix: - TOXENV=check -{% for env, config in tox_environments|dictsort %} - - TOXENV={{ env }}{% if config.cover %},coveralls,codecov{% endif %} - +{% for env, config in tox_environments|dictsort %}{{ '' }} + - TOXENV={{ env }}{% if config.cover %},coveralls,codecov{% endif -%} {% endfor %} + before_install: - python --version - uname -a @@ -25,6 +26,11 @@ script: after_failure: - more .tox/log/* | cat - more .tox/*/log/* | cat +before_cache: + - rm -rf $HOME/.cache/pip/log +cache: + directories: + - $HOME/.cache/pip notifications: email: on_success: never diff --git a/ci/templates/tox.ini b/ci/templates/tox.ini index a42bc6e..e251304 100644 --- a/ci/templates/tox.ini +++ b/ci/templates/tox.ini @@ -9,6 +9,9 @@ envlist = docs [testenv] +basepython = + {docs,spell}: python2.7 + {clean,check,report,extension-coveralls,coveralls,codecov}: python3.5 setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes @@ -28,7 +31,7 @@ setenv = commands = sphinx-build -b spelling docs dist/docs skip_install = true -usedevelop = true +usedevelop = false deps = -r{toxinidir}/docs/requirements.txt sphinxcontrib-spelling @@ -53,19 +56,18 @@ passenv = * [testenv:check] -basepython = python3.4 deps = docutils check-manifest flake8 - readme + readme-renderer pygments skip_install = true usedevelop = false commands = python setup.py check --strict --metadata --restructuredtext check-manifest {toxinidir} - flake8 src tests + flake8 src tests setup.py [testenv:coveralls] deps = @@ -90,7 +92,6 @@ commands = [testenv:report] -basepython = python3.4 deps = coverage skip_install = true usedevelop = false diff --git a/docs/conf.py b/docs/conf.py index f51f585..24fcb7b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,8 @@ 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', - 'sphinxcontrib.napoleon' + 'sphinx.ext.napoleon', + 'sphinx.ext.extlinks', ] if os.getenv('SPELLCHECK'): extensions += 'sphinxcontrib.spelling', @@ -25,6 +26,13 @@ author = 'Ionel Cristian Mărieș' copyright = '{0}, {1}'.format(year, author) version = release = '2.3.0' + +pygments_style = 'trac' +templates_path = ['.'] +extlinks = { + 'issue': ('https://github.com/ionelmc/python-redis-lock/issues/%s', '#'), + 'pr': ('https://github.com/ionelmc/python-redis-lock/pull/%s', 'PR #'), +} import sphinx_py3doc_enhanced_theme html_theme = "sphinx_py3doc_enhanced_theme" html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] @@ -32,8 +40,6 @@ 'githuburl': 'https://github.com/ionelmc/python-redis-lock/' } -pygments_style = 'trac' -templates_path = ['.'] html_use_smartypants = True html_last_updated_fmt = '%b %d, %Y' html_split_index = True @@ -41,3 +47,7 @@ '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], } html_short_title = '%s-%s' % (project, version) + +napoleon_use_ivar = True +napoleon_use_rtype = False +napoleon_use_param = False diff --git a/docs/index.rst b/docs/index.rst index f18d284..40f35b5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,6 @@ -Welcome to redis-lock's documentation! -======================================== - -Contents: +======== +Contents +======== .. toctree:: :maxdepth: 2 diff --git a/docs/readme.rst b/docs/readme.rst index 6be290b..72a3355 100644 --- a/docs/readme.rst +++ b/docs/readme.rst @@ -1,5 +1 @@ -######## -Overview -######## - .. include:: ../README.rst diff --git a/docs/requirements.txt b/docs/requirements.txt index 1632a96..ef4a013 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,3 @@ -sphinx -sphinxcontrib-napoleon +sphinx>=1.3 sphinx-py3doc-enhanced-theme -e . diff --git a/docs/usage.rst b/docs/usage.rst index db2b289..4a2224b 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -51,13 +51,9 @@ The above example could be rewritten using context manager:: time.sleep(5) In cases, where lock not necessarily in acquired state, and -user need to ensure, that it's released, ``force`` parameter could be used:: - - lock = Lock(conn, "foo") - try: - if lock.acquire(block=False): - print("Got the lock. Do crazy dance") - else: - print("Didn't get the lock. Do normal dance") - finally: - lock.release(force=True) +user need to ensure, that it has a matching ``id``, example:: + + lock1 = Lock(conn, "foo") + lock1.acquire() + lock2 = Lock(conn, "foo", id=lock1.id) + lock2.release() diff --git a/setup.cfg b/setup.cfg index c6fc24f..0ba5a5d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,6 +65,7 @@ python_versions = 2.7 3.3 3.4 + 3.5 pypy dependencies = @@ -73,8 +74,8 @@ dependencies = 1.6: redis==2.10.3 &python_versions[2.6] 1.4: django-redis==3.8.4 redis==2.10.3 Django==1.4.22 !python_versions[3.*] !python_versions[2.6] - 1.5: django-redis==3.8.4 redis==2.10.3 Django==1.5.12 !python_versions[2.6] - 1.6: django-redis==3.8.4 redis==2.10.3 Django==1.6.11 !python_versions[2.6] + 1.5: django-redis==3.8.4 redis==2.10.3 Django==1.5.12 !python_versions[2.6] !python_versions[3.5] + 1.6: django-redis==3.8.4 redis==2.10.3 Django==1.6.11 !python_versions[2.6] !python_versions[3.5] 1.7: django-redis==3.8.4 redis==2.10.3 Django==1.7.10 !python_versions[2.6] 1.8: django-redis==4.2.0 redis==2.10.3 Django==1.8.4 !python_versions[2.6] diff --git a/setup.py b/setup.py index efbe74f..57df13d 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,10 @@ def read(*names, **kwargs): version='2.3.0', license='BSD', description='Lock context manager implemented via redis SETNX/BLPOP.', - long_description='%s\n%s' % (read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), + long_description='%s\n%s' % ( + re.compile('^.. start-badges.*^.. end-badges', re.M|re.S).sub('', read('README.rst')), + re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) + ), author='Ionel Cristian Mărieș', author_email='contact@ionelmc.ro', url='https://github.com/ionelmc/python-redis-lock', @@ -50,6 +53,7 @@ def read(*names, **kwargs): 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Utilities', diff --git a/src/redis_lock/__init__.py b/src/redis_lock/__init__.py index 618c852..1c47723 100644 --- a/src/redis_lock/__init__.py +++ b/src/redis_lock/__init__.py @@ -10,24 +10,26 @@ logger = getLogger(__name__) +# Check if the id match. If not, return an error code. UNLOCK_SCRIPT = b""" - if redis.call("get", KEYS[1]) == ARGV[1] then + if redis.call("get", KEYS[1]) ~= ARGV[1] then + return 1 + else redis.call("del", KEYS[2]) redis.call("lpush", KEYS[2], 1) redis.call("expire", KEYS[2], 1) - return redis.call("del", KEYS[1]) - else + redis.call("del", KEYS[1]) return 0 end """ UNLOCK_SCRIPT_HASH = sha1(UNLOCK_SCRIPT).hexdigest() +# Covers both cases when key doesn't exist and doesn't equal to lock's id EXTEND_SCRIPT = b""" - -- Covers both cases when key doesn't exist and doesn't equal to lock's id if redis.call("get", KEYS[1]) ~= ARGV[2] then - return -1 + return 1 elseif redis.call("ttl", KEYS[1]) < 0 then - return -2 + return 2 else redis.call("expire", KEYS[1], ARGV[1]) return 0 @@ -100,15 +102,18 @@ class NotExpirable(RuntimeError): ])) -def _eval_script(redis, script_id, *args, **kwargs): +def _eval_script(redis, script_id, *keys, **kwargs): """Tries to call ``EVALSHA`` with the `hash` and then, if it fails, calls regular ``EVAL`` with the `script`. """ + args = kwargs.pop('args', ()) + if kwargs: + raise TypeError("Unexpected keyword arguments %s" % kwargs.keys()) try: - return redis.evalsha(SCRIPTS[script_id], *args, **kwargs) + return redis.evalsha(SCRIPTS[script_id], len(keys), *keys + args) except NoScriptError: logger.warn("%s not cached.", SCRIPTS[script_id + 2]) - return redis.eval(SCRIPTS[script_id + 1], *args, **kwargs) + return redis.eval(SCRIPTS[script_id + 1], len(keys), *keys + args) class Lock(object): @@ -128,6 +133,9 @@ def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False) :param id: The ID (redis value) the lock should have. A random value is generated when left at the default. + + Note that if you specify this then the lock is marked as "held". Acquires + won't be possible. :param auto_renewal: If set to True, Lock will automatically renew the lock so that it doesn't expire for as long as the lock is held (acquire() called @@ -145,7 +153,7 @@ def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False) self._client = redis_client self._expire = expire if expire is None else int(expire) self._id = urandom(16) if id is None else id - self._held = False + self._held = id is not None self._name = 'lock:'+name self._signal = 'lock-signal:'+name self._lock_renewal_interval = expire*2/3 if auto_renewal else None @@ -155,7 +163,7 @@ def reset(self): """ Forcibly deletes the lock. Use this with care. """ - _eval_script(self._client, RESET, 2, self._name, self._signal) + _eval_script(self._client, RESET, self._name, self._signal) @property def id(self): @@ -221,15 +229,14 @@ def extend(self, expire=None): "To extend a lock 'expire' must be provided as an " "argument to extend() method or at initialization time." ) - result = _eval_script(self._client, EXTEND, 1, - self._name, expire, self._id) - if result == 0: - return result - elif result == -1: - raise NotAcquired("Lock %s is not acquired" % self._name) - elif result == -2: + error = _eval_script(self._client, EXTEND, self._name, args=(expire, self._id)) + if error == 1: + raise NotAcquired("Lock %s is not acquired or it already expired." % self._name) + elif error == 2: raise NotExpirable("Lock %s has no assigned expiration time" % self._name) + elif error: + raise RuntimeError("Unsupported error code %s from EXTEND script" % error) def _lock_renewer(self, interval): """ @@ -280,26 +287,31 @@ def __enter__(self): assert acquired, "Lock wasn't acquired, but blocking=True" return self - def __exit__(self, exc_type=None, exc_value=None, traceback=None, force=False): - if not (self._held or force): - raise NotAcquired("This Lock instance didn't acquire the lock.") - if self._lock_renewal_thread is not None: - self._stop_lock_renewer() - logger.debug("Releasing %r.", self._name) - _eval_script(self._client, UNLOCK, - 2, self._name, self._signal, self._id) + def __exit__(self, exc_type=None, exc_value=None, traceback=None): + self.release() + + def release(self): + """Releases the lock, that was acquired with the same object. - self._held = False + .. note:: - def release(self, force=False): - """Releases the lock, that was acquired in the same Python context. + If you want to release a lock that you acquired in a different place you have two choices: - :param force: - If ``False`` - fail with exception if this instance was not in - acquired state in the same Python context. - If ``True`` - fail silently. + * Use ``Lock("name", id=id_from_other_place).release()`` + * Use ``Lock("name").reset()`` """ - return self.__exit__(force=force) + if not self._held: + raise NotAcquired("This Lock instance didn't acquire the lock.") + if self._lock_renewal_thread is not None: + self._stop_lock_renewer() + logger.debug("Releasing %r.", self._name) + error = _eval_script(self._client, UNLOCK, self._name, self._signal, args=(self._id,)) + if error == 1: + raise NotAcquired("Lock %s is not acquired or it already expired." % self._name) + elif error: + raise RuntimeError("Unsupported error code %s from EXTEND script." % error) + else: + self._held = False class InterruptableThread(threading.Thread): @@ -344,4 +356,4 @@ def reset_all(redis_client): """ Forcibly deletes all locks if its remains (like a crash reason). Use this with care. """ - _eval_script(redis_client, RESET_ALL, 0) + _eval_script(redis_client, RESET_ALL) diff --git a/tests/helper.py b/tests/helper.py index 302184b..9b34451 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,4 +1,4 @@ -from __future__ import print_function +from __future__ import print_function, division import logging import os diff --git a/tests/test_redis_lock.py b/tests/test_redis_lock.py index 1f96d2d..9b78517 100644 --- a/tests/test_redis_lock.py +++ b/tests/test_redis_lock.py @@ -1,4 +1,4 @@ -from __future__ import print_function +from __future__ import print_function, division import os import platform @@ -105,9 +105,10 @@ def test_timeout(conn): def test_timeout_expire(conn): - with Lock(conn, "foobar", expire=1): - lock = Lock(conn, "foobar") - assert lock.acquire(timeout=2) + lock1 = Lock(conn, "foobar", expire=1) + lock1.acquire() + lock2 = Lock(conn, "foobar") + assert lock2.acquire(timeout=2) def test_timeout_expire_with_renewal(conn): @@ -152,18 +153,19 @@ def test_invalid_timeout(conn): def test_expire(conn): - with Lock(conn, "foobar", expire=TIMEOUT/4): - with TestProcess(sys.executable, HELPER, 'test_expire') as proc: - with dump_on_error(proc.read): - name = 'lock:foobar' - wait_for_strings( - proc.read, TIMEOUT, - 'Getting %r ...' % name, - 'Got lock for %r.' % name, - 'Releasing %r.' % name, - 'UNLOCK_SCRIPT not cached.', - 'DIED.', - ) + lock = Lock(conn, "foobar", expire=TIMEOUT/4) + lock.acquire() + with TestProcess(sys.executable, HELPER, 'test_expire') as proc: + with dump_on_error(proc.read): + name = 'lock:foobar' + wait_for_strings( + proc.read, TIMEOUT, + 'Getting %r ...' % name, + 'Got lock for %r.' % name, + 'Releasing %r.' % name, + 'UNLOCK_SCRIPT not cached.', + 'DIED.', + ) lock = Lock(conn, "foobar") try: assert lock.acquire(blocking=False) == True @@ -216,11 +218,11 @@ def test_extend_another_instance(conn): """ name = 'foobar' key_name = 'lock:' + name - lock = Lock(conn, name, id='spam', expire=100) + lock = Lock(conn, name, expire=100) lock.acquire() assert 0 <= conn.ttl(key_name) <= 100 - another_lock = Lock(conn, name, id='spam') + another_lock = Lock(conn, name, id=lock.id) another_lock.extend(1000) assert conn.ttl(key_name) > 100 @@ -232,15 +234,16 @@ def test_extend_another_instance_different_id_fail(conn): """ name = 'foobar' key_name = 'lock:' + name - lock = Lock(conn, name, expire=100, id='spam') + lock = Lock(conn, name, expire=100) lock.acquire() assert 0 <= conn.ttl(key_name) <= 100 - another_lock = Lock(conn, name, id='eggs') + another_lock = Lock(conn, name) with pytest.raises(NotAcquired): another_lock.extend(1000) assert conn.ttl(key_name) <= 100 + assert lock.id != another_lock.id def test_double_acquire(conn): @@ -353,11 +356,12 @@ def workerfn(go, count_lock, count): def test_reset(conn): - with Lock(conn, "foobar") as lock: - lock.reset() - new_lock = Lock(conn, "foobar") - new_lock.acquire(blocking=False) - new_lock.release() + lock = Lock(conn, "foobar") + lock.reset() + new_lock = Lock(conn, "foobar") + new_lock.acquire(blocking=False) + new_lock.release() + pytest.raises(NotAcquired, lock.release) def test_reset_all(conn): @@ -379,8 +383,12 @@ def test_owner_id(conn): lock = Lock(conn, "foobar-tok", expire=TIMEOUT/4, id=unique_identifier) lock_id = lock.id assert lock_id == unique_identifier - lock.acquire(blocking=False) - assert lock.get_owner_id() == unique_identifier + + +def test_get_owner_id(conn): + lock = Lock(conn, "foobar-tok") + lock.acquire() + assert lock.get_owner_id() == lock.id lock.release() @@ -395,7 +403,9 @@ def test_token(conn): def test_bogus_release(conn): lock = Lock(conn, "foobar-tok") pytest.raises(NotAcquired, lock.release) - lock.release(force=True) + lock.acquire() + lock2 = Lock(conn, "foobar-tok", id=lock.id) + lock2.release() def test_release_from_nonblocking_leaving_garbage(conn): diff --git a/tox.ini b/tox.ini index 404e398..70c0123 100644 --- a/tox.ini +++ b/tox.ini @@ -34,6 +34,10 @@ envlist = 3.4-1.7-nocover, 3.4-1.8, 3.4-1.8-nocover, + 3.5-1.7, + 3.5-1.7-nocover, + 3.5-1.8, + 3.5-1.8-nocover, pypy-1.4, pypy-1.4-nocover, pypy-1.5, @@ -48,6 +52,9 @@ envlist = docs [testenv] +basepython = + {docs,spell}: python2.7 + {clean,check,report,extension-coveralls,coveralls,codecov}: python3.5 setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes @@ -67,7 +74,7 @@ setenv = commands = sphinx-build -b spelling docs dist/docs skip_install = true -usedevelop = true +usedevelop = false deps = -r{toxinidir}/docs/requirements.txt sphinxcontrib-spelling @@ -92,12 +99,11 @@ passenv = * [testenv:check] -basepython = python3.4 deps = docutils check-manifest flake8 - readme + readme-renderer pygments skip_install = true usedevelop = false @@ -129,7 +135,6 @@ commands = [testenv:report] -basepython = python3.4 deps = coverage skip_install = true usedevelop = false @@ -499,6 +504,52 @@ deps = redis==2.10.3 Django==1.8.4 +[testenv:3.5-1.7] +basepython = {env:TOXPYTHON:python3.5} +setenv = + {[testenv]setenv} + WITH_COVERAGE=yes +usedevelop = true +commands = + {posargs:py.test --cov --cov-report=term-missing -vv} +deps = + {[testenv]deps} + pytest-cov + django-redis==3.8.4 + redis==2.10.3 + Django==1.7.10 + +[testenv:3.5-1.7-nocover] +basepython = {env:TOXPYTHON:python3.5} +deps = + {[testenv]deps} + django-redis==3.8.4 + redis==2.10.3 + Django==1.7.10 + +[testenv:3.5-1.8] +basepython = {env:TOXPYTHON:python3.5} +setenv = + {[testenv]setenv} + WITH_COVERAGE=yes +usedevelop = true +commands = + {posargs:py.test --cov --cov-report=term-missing -vv} +deps = + {[testenv]deps} + pytest-cov + django-redis==4.2.0 + redis==2.10.3 + Django==1.8.4 + +[testenv:3.5-1.8-nocover] +basepython = {env:TOXPYTHON:python3.5} +deps = + {[testenv]deps} + django-redis==4.2.0 + redis==2.10.3 + Django==1.8.4 + [testenv:pypy-1.4] basepython = {env:TOXPYTHON:pypy} setenv =