From ecb405bf337e0b62956d279ffc7df3d3ee29a3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 26 Dec 2015 10:25:01 +0200 Subject: [PATCH 1/8] Remove force=True from release and check if lock is actually held. Raise error if expired or not acquired. Closes #25. --- docs/usage.rst | 16 ++++----- src/redis_lock/__init__.py | 50 +++++++++++++++++------------ tests/helper.py | 2 +- tests/test_redis_lock.py | 66 ++++++++++++++++++++++---------------- 4 files changed, 75 insertions(+), 59 deletions(-) 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/src/redis_lock/__init__.py b/src/redis_lock/__init__.py index 618c852..67975a3 100644 --- a/src/redis_lock/__init__.py +++ b/src/redis_lock/__init__.py @@ -10,13 +10,15 @@ 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 """ @@ -128,6 +130,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 +150,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 @@ -280,26 +285,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): 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): From 4e86ebf1bb9ef9079bd9427917d7c77bc831cb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 26 Dec 2015 10:25:15 +0200 Subject: [PATCH 2/8] Add py3.5 in the test grid. --- .travis.yml | 8 +++++ setup.cfg | 1 + tox.ini | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8e7eb79..cf4a4da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,6 +38,14 @@ env: - TOXENV=3.4-1.7-nocover - TOXENV=3.4-1.8,coveralls,codecov - TOXENV=3.4-1.8-nocover + - TOXENV=3.5-1.5,coveralls,codecov + - TOXENV=3.5-1.5-nocover + - TOXENV=3.5-1.6,coveralls,codecov + - TOXENV=3.5-1.6-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 diff --git a/setup.cfg b/setup.cfg index c6fc24f..6543c0a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,6 +65,7 @@ python_versions = 2.7 3.3 3.4 + 3.5 pypy dependencies = diff --git a/tox.ini b/tox.ini index 404e398..0b3674e 100644 --- a/tox.ini +++ b/tox.ini @@ -34,6 +34,14 @@ envlist = 3.4-1.7-nocover, 3.4-1.8, 3.4-1.8-nocover, + 3.5-1.5, + 3.5-1.5-nocover, + 3.5-1.6, + 3.5-1.6-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, @@ -499,6 +507,98 @@ deps = redis==2.10.3 Django==1.8.4 +[testenv:3.5-1.5] +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.5.12 + +[testenv:3.5-1.5-nocover] +basepython = {env:TOXPYTHON:python3.5} +deps = + {[testenv]deps} + django-redis==3.8.4 + redis==2.10.3 + Django==1.5.12 + +[testenv:3.5-1.6] +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.6.11 + +[testenv:3.5-1.6-nocover] +basepython = {env:TOXPYTHON:python3.5} +deps = + {[testenv]deps} + django-redis==3.8.4 + redis==2.10.3 + Django==1.6.11 + +[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 = From 74e739b38195e1a4a7b2aa5d5a40a31d5837b3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 26 Dec 2015 10:27:30 +0200 Subject: [PATCH 3/8] Refactor _eval_script a bit so you don't need to hardcode the number of keys anymore (infer it from the arguments being passed). Also use clearer variables for the script exit code. It's just an error code, not an actual result. --- src/redis_lock/__init__.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/redis_lock/__init__.py b/src/redis_lock/__init__.py index 67975a3..92394f1 100644 --- a/src/redis_lock/__init__.py +++ b/src/redis_lock/__init__.py @@ -24,12 +24,12 @@ """ 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 @@ -102,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): @@ -160,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): @@ -226,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): """ @@ -354,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) From 565d9c0d58b89e70d0930a1b845de3f4e6276a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 6 Jan 2016 01:25:16 +0200 Subject: [PATCH 4/8] Skel updates. --- .cookiecutterrc | 64 +++++++++++++++++++--------------------- .editorconfig | 13 ++++++++ .gitignore | 1 + .travis.yml | 11 +++++-- README.rst | 16 +++++----- ci/bootstrap.py | 4 +++ ci/templates/.travis.yml | 16 ++++++---- ci/templates/tox.ini | 9 +++--- docs/conf.py | 16 ++++++++-- docs/index.rst | 7 ++--- docs/readme.rst | 4 --- docs/requirements.txt | 3 +- setup.py | 6 +++- tox.ini | 9 +++--- 14 files changed, 109 insertions(+), 70 deletions(-) create mode 100644 .editorconfig 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 cf4a4da..036ab8f 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 @@ -71,6 +73,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/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..274def8 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,12 +56,11 @@ passenv = * [testenv:check] -basepython = python3.4 deps = docutils check-manifest flake8 - readme + readme-renderer pygments skip_install = true usedevelop = false @@ -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/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/tox.ini b/tox.ini index 0b3674e..6f85082 100644 --- a/tox.ini +++ b/tox.ini @@ -56,6 +56,9 @@ envlist = docs [testenv] +basepython = + {docs,spell}: python2.7 + {clean,check,report,extension-coveralls,coveralls,codecov}: python3.5 setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes @@ -75,7 +78,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 @@ -100,12 +103,11 @@ passenv = * [testenv:check] -basepython = python3.4 deps = docutils check-manifest flake8 - readme + readme-renderer pygments skip_install = true usedevelop = false @@ -137,7 +139,6 @@ commands = [testenv:report] -basepython = python3.4 deps = coverage skip_install = true usedevelop = false From f277047774f64843386ef6923356497a9f4d2213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 6 Jan 2016 07:05:53 +0200 Subject: [PATCH 5/8] Don't test django <=1.6 on py3.5 --- .travis.yml | 4 ---- setup.cfg | 4 ++-- tox.ini | 50 -------------------------------------------------- 3 files changed, 2 insertions(+), 56 deletions(-) diff --git a/.travis.yml b/.travis.yml index 036ab8f..e6b3eba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,10 +40,6 @@ env: - TOXENV=3.4-1.7-nocover - TOXENV=3.4-1.8,coveralls,codecov - TOXENV=3.4-1.8-nocover - - TOXENV=3.5-1.5,coveralls,codecov - - TOXENV=3.5-1.5-nocover - - TOXENV=3.5-1.6,coveralls,codecov - - TOXENV=3.5-1.6-nocover - TOXENV=3.5-1.7,coveralls,codecov - TOXENV=3.5-1.7-nocover - TOXENV=3.5-1.8,coveralls,codecov diff --git a/setup.cfg b/setup.cfg index 6543c0a..0ba5a5d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -74,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/tox.ini b/tox.ini index 6f85082..70c0123 100644 --- a/tox.ini +++ b/tox.ini @@ -34,10 +34,6 @@ envlist = 3.4-1.7-nocover, 3.4-1.8, 3.4-1.8-nocover, - 3.5-1.5, - 3.5-1.5-nocover, - 3.5-1.6, - 3.5-1.6-nocover, 3.5-1.7, 3.5-1.7-nocover, 3.5-1.8, @@ -508,52 +504,6 @@ deps = redis==2.10.3 Django==1.8.4 -[testenv:3.5-1.5] -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.5.12 - -[testenv:3.5-1.5-nocover] -basepython = {env:TOXPYTHON:python3.5} -deps = - {[testenv]deps} - django-redis==3.8.4 - redis==2.10.3 - Django==1.5.12 - -[testenv:3.5-1.6] -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.6.11 - -[testenv:3.5-1.6-nocover] -basepython = {env:TOXPYTHON:python3.5} -deps = - {[testenv]deps} - django-redis==3.8.4 - redis==2.10.3 - Django==1.6.11 - [testenv:3.5-1.7] basepython = {env:TOXPYTHON:python3.5} setenv = From 1f9d92a28ebb400045deea89594df2966e9d78f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 6 Jan 2016 07:06:02 +0200 Subject: [PATCH 6/8] Add missing ifle to manifest. --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) 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 From 8534376fb1053296cd30c9f5278f5e99cf806ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 6 Jan 2016 07:06:10 +0200 Subject: [PATCH 7/8] Flake8 this too. --- ci/templates/tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/templates/tox.ini b/ci/templates/tox.ini index 274def8..e251304 100644 --- a/ci/templates/tox.ini +++ b/ci/templates/tox.ini @@ -67,7 +67,7 @@ usedevelop = false commands = python setup.py check --strict --metadata --restructuredtext check-manifest {toxinidir} - flake8 src tests + flake8 src tests setup.py [testenv:coveralls] deps = From 80d3f24d40d9525aaf48b32860791f0e107195fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Wed, 6 Jan 2016 09:19:23 +0200 Subject: [PATCH 8/8] Fix space. --- src/redis_lock/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redis_lock/__init__.py b/src/redis_lock/__init__.py index 92394f1..1c47723 100644 --- a/src/redis_lock/__init__.py +++ b/src/redis_lock/__init__.py @@ -236,7 +236,7 @@ def extend(self, expire=None): raise NotExpirable("Lock %s has no assigned expiration time" % self._name) elif error: - raise RuntimeError("Unsupported error code %s from EXTEND script" % error) + raise RuntimeError("Unsupported error code %s from EXTEND script" % error) def _lock_renewer(self, interval): """