Control: tags 977054 + patch Control: tags 977054 + pending Dear maintainer,
I've prepared an NMU for aiorwlock (versioned as 1.0.0-0.1) and uploaded it to DELAYED/1. Please feel free to tell me if I should cancel it. cu Adrian
diff -Nru aiorwlock-0.6.0/aiorwlock/__init__.py aiorwlock-1.0.0/aiorwlock/__init__.py --- aiorwlock-0.6.0/aiorwlock/__init__.py 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/aiorwlock/__init__.py 2020-10-23 11:09:58.000000000 +0300 @@ -1,31 +1,25 @@ import asyncio -import sys - +import warnings from collections import deque -from asyncio import Task, Future # noqa -from typing import Any, List, Tuple, Optional, Callable # noqa - -try: - from typing import Deque -except ImportError: - Deque = deque # type: ignore - +from typing import Any, Deque, List, Optional, Tuple Loop = asyncio.AbstractEventLoop OptLoop = Optional[Loop] +# silence LGTM service alerts +Future = asyncio.Future +Task = asyncio.Task -__version__ = '0.6.0' -__all__ = ['RWLock'] - -PY_35 = sys.version_info >= (3, 5, 3) +__version__ = '1.0.0' +__all__ = ('RWLock',) -def current_task(loop: OptLoop = None) -> 'Task[Any]': - _loop = loop or asyncio.get_event_loop() # type: Loop +def _current_task(loop: OptLoop = None) -> 'Task[Any]': + _loop: Loop = loop or asyncio.get_event_loop() if hasattr(asyncio, 'current_task'): t = asyncio.current_task(loop=_loop) else: + # remove once python 3.6 deprecated t = asyncio.Task.current_task(loop=_loop) if t is None: raise RuntimeError('Loop is not running') @@ -40,15 +34,15 @@ _RL = 1 _WL = 2 - def __init__(self, fast: bool, loop: OptLoop): + def __init__(self, fast: bool, loop: Loop): self._do_yield = not fast - self._loop = loop or asyncio.get_event_loop() # type: Loop - self._read_waiters = deque() # type: Deque[Future[None]] - self._write_waiters = deque() # type: Deque[Future[None]] - self._r_state = 0 # type: int - self._w_state = 0 # type: int + self._loop: Loop = loop + self._read_waiters: Deque[Future[None]] = deque() + self._write_waiters: Deque[Future[None]] = deque() + self._r_state: int = 0 + self._w_state: int = 0 # tasks will be few, so a list is not inefficient - self._owning = [] # type: List[Tuple[Task[Any], int]] + self._owning: List[Tuple[Task[Any], int]] = [] @property def read_locked(self) -> bool: @@ -58,23 +52,33 @@ def write_locked(self) -> bool: return self._w_state > 0 + async def _yield_after_acquire(self, lock_type: int) -> None: + if self._do_yield: + try: + await asyncio.sleep(0.0, loop=self._loop) + except asyncio.CancelledError: + self._release(lock_type) + self._wake_up() + raise + # Acquire the lock in read mode. async def acquire_read(self) -> bool: - me = current_task(loop=self._loop) + me = _current_task(loop=self._loop) if (me, self._RL) in self._owning or (me, self._WL) in self._owning: self._r_state += 1 self._owning.append((me, self._RL)) - if self._do_yield: - await asyncio.sleep(0.0, loop=self._loop) + await self._yield_after_acquire(self._RL) return True - if (not self._write_waiters and - self._r_state >= 0 and self._w_state == 0): + if ( + not self._write_waiters + and self._r_state >= 0 + and self._w_state == 0 + ): self._r_state += 1 self._owning.append((me, self._RL)) - if self._do_yield: - await asyncio.sleep(0.0, loop=self._loop) + await self._yield_after_acquire(self._RL) return True fut = self._loop.create_future() @@ -95,13 +99,12 @@ # Acquire the lock in write mode. A 'waiting' count is maintained, # ensuring that 'readers' will yield to writers. async def acquire_write(self) -> bool: - me = current_task(loop=self._loop) + me = _current_task(loop=self._loop) if (me, self._WL) in self._owning: self._w_state += 1 self._owning.append((me, self._WL)) - if self._do_yield: - await asyncio.sleep(0.0, loop=self._loop) + await self._yield_after_acquire(self._WL) return True elif (me, self._RL) in self._owning: if self._r_state > 0: @@ -110,8 +113,7 @@ if self._r_state == 0 and self._w_state == 0: self._w_state += 1 self._owning.append((me, self._WL)) - if self._do_yield: - await asyncio.sleep(0.0, loop=self._loop) + await self._yield_after_acquire(self._WL) return True fut = self._loop.create_future() @@ -137,7 +139,7 @@ def _release(self, lock_type: int) -> None: # assert lock_type in (self._RL, self._WL) - me = current_task(loop=self._loop) + me = _current_task(loop=self._loop) try: self._owning.remove((me, lock_type)) except ValueError: @@ -174,10 +176,10 @@ class _ContextManagerMixin: - def __enter__(self) -> None: raise RuntimeError( - '"await" should be used as context manager expression') + '"await" should be used as context manager expression' + ) def __exit__(self, *args: Any) -> None: # This must exist because __enter__ exists, even though that @@ -202,7 +204,6 @@ # Lock objects to access the _RWLockCore in reader or writer mode class _ReaderLock(_ContextManagerMixin): - def __init__(self, lock: _RWLockCore) -> None: self._lock = lock @@ -222,7 +223,6 @@ class _WriterLock(_ContextManagerMixin): - def __init__(self, lock: _RWLockCore): self._lock = lock @@ -251,7 +251,25 @@ core = _RWLockCore def __init__(self, *, fast: bool = False, loop: OptLoop = None) -> None: - self._loop = loop or asyncio.get_event_loop() # type: Loop + if loop is None: + loop = asyncio.get_event_loop() + else: + warnings.warn( + 'Passing "loop" argument ' + 'is deprecated since aiorwlock 1.0 and scheduled for removal ' + 'in aiorwlock 2.0', + DeprecationWarning, + stacklevel=2, + ) + if not loop.is_running(): + warnings.warn( + 'Instantiation of RWLock outside of async function context ' + 'is deprecated since aiorwlock 1.0 and scheduled for removal ' + 'in aiorwlock 2.0', + DeprecationWarning, + stacklevel=2, + ) + self._loop: Loop = loop core = self.core(fast, self._loop) self._reader_lock = _ReaderLock(core) self._writer_lock = _WriterLock(core) diff -Nru aiorwlock-0.6.0/CHANGES.rst aiorwlock-1.0.0/CHANGES.rst --- aiorwlock-0.6.0/CHANGES.rst 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/CHANGES.rst 2020-10-23 11:09:58.000000000 +0300 @@ -1,6 +1,20 @@ Changes ------- +1.0.0 (2020-12-32) +^^^^^^^^^^^^^^^^^^ + +* Fix a bug with cancelation during acquire #170 (thanks @romasku) + +* Deprecate passing explicit `loop` argument to `RWLock` constuctor + +* Deprecate creation of `RWLock` instance outside of async function context + +* Minimal supported version is Python 3.6 + +* The library works with Python 3.8 and Python 3.9 seamlessly + + 0.6.0 (2018-12-18) ^^^^^^^^^^^^^^^^^^ * Wake up all readers after writer releases lock #60 (thanks @ranyixu) diff -Nru aiorwlock-0.6.0/CONTRIBUTORS.txt aiorwlock-1.0.0/CONTRIBUTORS.txt --- aiorwlock-0.6.0/CONTRIBUTORS.txt 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/CONTRIBUTORS.txt 1970-01-01 02:00:00.000000000 +0200 @@ -1,3 +0,0 @@ -Acknowledgements ----------------- -Andrew Svetlov https://github.com/asvetlov \ No newline at end of file diff -Nru aiorwlock-0.6.0/debian/changelog aiorwlock-1.0.0/debian/changelog --- aiorwlock-0.6.0/debian/changelog 2019-07-10 16:24:28.000000000 +0300 +++ aiorwlock-1.0.0/debian/changelog 2021-02-10 01:45:28.000000000 +0200 @@ -1,3 +1,11 @@ +aiorwlock (1.0.0-0.1) unstable; urgency=high + + * Non-maintainer upload. + * New upstream release. + - Fixes FTBFS with pytest 6. (Closes: #977054) + + -- Adrian Bunk <b...@debian.org> Wed, 10 Feb 2021 01:45:28 +0200 + aiorwlock (0.6.0-2) unstable; urgency=medium * Upload to unstable. diff -Nru aiorwlock-0.6.0/examples/context_manager.py aiorwlock-1.0.0/examples/context_manager.py --- aiorwlock-0.6.0/examples/context_manager.py 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/examples/context_manager.py 2020-10-23 11:09:58.000000000 +0300 @@ -1,4 +1,5 @@ import asyncio + import aiorwlock diff -Nru aiorwlock-0.6.0/examples/simple.py aiorwlock-1.0.0/examples/simple.py --- aiorwlock-0.6.0/examples/simple.py 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/examples/simple.py 2020-10-23 11:09:58.000000000 +0300 @@ -1,4 +1,5 @@ import asyncio + import aiorwlock diff -Nru aiorwlock-0.6.0/.github/workflows/ci.yml aiorwlock-1.0.0/.github/workflows/ci.yml --- aiorwlock-0.6.0/.github/workflows/ci.yml 1970-01-01 02:00:00.000000000 +0200 +++ aiorwlock-1.0.0/.github/workflows/ci.yml 2020-10-23 11:09:58.000000000 +0300 @@ -0,0 +1,125 @@ +name: CI + +on: + push: + branches: + - master + tags: [ 'v*' ] + pull_request: + branches: + - master + schedule: + - cron: '0 6 * * *' # Daily 6AM UTC build + + +jobs: + + lint: + name: Linter + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" # - name: Cache + - name: Cache PyPI + uses: actions/cache@v2 + with: + key: pip-lint-${{ hashFiles('requirements-dev.txt') }} + path: ${{ steps.pip-cache.outputs.dir }} + restore-keys: | + pip-lint- + - name: Install dependencies + uses: py-actions/py-dependency-install@v2 + with: + path: requirements-dev.txt + - name: Install itself + run: | + pip install . + - name: Run lint + run: | + make lint + - name: Prepare twine checker + run: | + pip install -U twine wheel + python setup.py sdist bdist_wheel + - name: Run twine checker + run: | + twine check dist/* + + test: + name: Test + needs: lint + strategy: + matrix: + pyver: [3.6, 3.7, 3.8, 3.9, pypy3] + fail-fast: false + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Python ${{ matrix.pyver }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.pyver }} + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" # - name: Cache + - name: Cache PyPI + uses: actions/cache@v2 + with: + key: pip-ci-${{ matrix.pyver }}-${{ hashFiles('requirements-dev.txt') }} + path: ${{ steps.pip-cache.outputs.dir }} + restore-keys: | + pip-ci- + - name: Install dependencies + uses: py-actions/py-dependency-install@v2 + with: + path: requirements-dev.txt + - name: Run unittests + env: + COLOR: 'yes' + run: | + make cov + python -m coverage xml + - name: Upload coverage + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + flags: unit + fail_ci_if_error: false + + deploy: + name: Deploy + runs-on: ubuntu-latest + needs: test + # Run only on pushing a tag + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install dependencies + run: + python -m pip install -U pip wheel twine + - name: Make dists + run: + python setup.py sdist bdist_wheel + - name: PyPI upload + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + twine upload dist/* diff -Nru aiorwlock-0.6.0/.gitignore aiorwlock-1.0.0/.gitignore --- aiorwlock-0.6.0/.gitignore 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/.gitignore 2020-10-23 11:09:58.000000000 +0300 @@ -58,4 +58,10 @@ .idea .coverage.* -coverage \ No newline at end of file +coverage + +.DS_Store +.mypy_cache/ +cscope.files +cscope.out +tags diff -Nru aiorwlock-0.6.0/LICENSE aiorwlock-1.0.0/LICENSE --- aiorwlock-0.6.0/LICENSE 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/LICENSE 2020-10-23 11:09:58.000000000 +0300 @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2015-2016 Andrew Svetlov, Nikolay Novik + Copyright 2015-2019 Andrew Svetlov, Nikolay Novik Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff -Nru aiorwlock-0.6.0/Makefile aiorwlock-1.0.0/Makefile --- aiorwlock-0.6.0/Makefile 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/Makefile 2020-10-23 11:09:58.000000000 +0300 @@ -1,8 +1,20 @@ # Some simple testing tasks (sorry, UNIX only). +FILES := aiorwlock tests examples setup.py + flake: - flake8 aiorwlock tests examples setup.py + flake8 $(FILES) + +fmt: + isort ${FILES} + black -S -l 79 ${FILES} + +lint: bandit pyroma + isort --check-only --diff ${FILES} + black -S -l 79 --check $(FILES) + mypy --show-error-codes --disallow-untyped-calls --strict aiorwlock + flake8 $(FILES) test: flake pytest -s @@ -10,9 +22,6 @@ vtest: pytest -v -checkrst: - python setup.py check --restructuredtext - pyroma: pyroma -d . @@ -20,9 +29,9 @@ bandit -r ./aiorwlock mypy: - mypy aiorwlock --ignore-missing-imports --disallow-untyped-calls --no-site-packages --strict + mypy aiorwlock --disallow-untyped-calls --strict -cov cover coverage: flake checkrst pyroma bandit +cov cover coverage: pytest -sv --cov=aiorwlock --cov-report=term --cov-report=html ./tests @echo "open file://`pwd`/htmlcov/index.html" diff -Nru aiorwlock-0.6.0/README.rst aiorwlock-1.0.0/README.rst --- aiorwlock-0.6.0/README.rst 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/README.rst 2020-10-23 11:09:58.000000000 +0300 @@ -1,12 +1,12 @@ aiorwlock ========= -.. image:: https://travis-ci.com/aio-libs/aiorwlock.svg?branch=master - :target: https://travis-ci.com/aio-libs/aiorwlock -.. image:: https://coveralls.io/repos/jettify/aiorwlock/badge.png?branch=master - :target: https://coveralls.io/r/aio-libs/aiorwlock?branch=master +.. image:: https://github.com/aio-libs/aiorwlock/workflows/CI/badge.svg + :target: https://github.com/aio-libs/aiorwlock/actions?query=workflow%3ACI +.. image:: https://codecov.io/gh/aio-libs/aiorwlock/branch/master/graph/badge.svg + :target: https://codecov.io/gh/aio-libs/aiorwlock .. image:: https://badges.gitter.im/Join%20Chat.svg - :target: https://gitter.im/aio-libs/Lobby - :alt: Chat on Gitter + :target: https://gitter.im/aio-libs/Lobby + :alt: Chat on Gitter Read write lock for asyncio_ . A ``RWLock`` maintains a pair of associated locks, one for read-only operations and one for writing. The read lock may be @@ -25,27 +25,33 @@ Implementation is almost direct port from this patch_. -Example with async def ----------------------- +Example +------- -Requires Python 3.5+ +Requires Python 3.5.3+ .. code:: python - import asyncio - import aiorwlock - loop = asyncio.get_event_loop() + import asyncio + import aiorwlock + + + async def go(): + rwlock = aiorwlock.RWLock() + + # acquire reader lock, multiple coroutines allowed to hold the lock + async with rwlock.reader_lock: + print('inside reader lock') + await asyncio.sleep(0.1) + # acquire writer lock, only one coroutine can hold the lock + async with rwlock.writer_lock: + print('inside writer lock') + await asyncio.sleep(0.1) - async def go(): - rwlock = aiorwlock.RWLock(loop=loop) - async with rwlock.writer: - # or same way you can acquire reader lock - # async with rwlock.reader: pass - print("inside writer") - await asyncio.sleep(0.1, loop=loop) - loop.run_until_complete(go()) + loop = asyncio.get_event_loop() + loop.run_until_complete(go()) Fast path @@ -64,11 +70,17 @@ minor speedup. +TLA+ Specification +------------------ + +TLA+ specification of ``aiorwlock`` provided in this repository. + + License ------- ``aiorwlock`` is offered under the Apache 2 license. -.. _asyncio: http://docs.python.org/3.4/library/asyncio.html +.. _asyncio: http://docs.python.org/3.8/library/asyncio.html .. _patch: http://bugs.python.org/issue8800 diff -Nru aiorwlock-0.6.0/requirements-dev.txt aiorwlock-1.0.0/requirements-dev.txt --- aiorwlock-0.6.0/requirements-dev.txt 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/requirements-dev.txt 2020-10-23 11:09:58.000000000 +0300 @@ -1,10 +1,13 @@ -e . -bandit==1.5.1 -flake8-bugbear==18.8.0 -flake8-quotes==1.0.0 -flake8==3.6.0 -pyroma==2.4 -pytest-cov==2.6.0 -pytest==4.0.1 -uvloop==0.11.3 -mypy==0.650 +bandit==1.6.2; implementation_name=="cpython" +black==20.8b1; implementation_name=="cpython" +flake8-bugbear==20.1.4; implementation_name=="cpython" +flake8-quotes==3.2.0; implementation_name=="cpython" +flake8==3.8.4; implementation_name=="cpython" +isort==5.6.4; implementation_name=="cpython" +mypy==0.790; implementation_name=="cpython" +pyroma==2.6; implementation_name=="cpython" +pytest-asyncio==0.14.0 +pytest-cov==2.10.1 +pytest==6.1.1 +uvloop==0.14.0; implementation_name=="cpython" diff -Nru aiorwlock-0.6.0/setup.cfg aiorwlock-1.0.0/setup.cfg --- aiorwlock-0.6.0/setup.cfg 1970-01-01 02:00:00.000000000 +0200 +++ aiorwlock-1.0.0/setup.cfg 2020-10-23 11:09:58.000000000 +0300 @@ -0,0 +1,6 @@ +[tool:pytest] +filterwarnings=error +testpaths = tests/ + +[mypy-pytest] +ignore_missing_imports = true diff -Nru aiorwlock-0.6.0/setup.py aiorwlock-1.0.0/setup.py --- aiorwlock-0.6.0/setup.py 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/setup.py 2020-10-23 11:09:58.000000000 +0300 @@ -1,59 +1,64 @@ import os import re -import sys -from setuptools import setup +from setuptools import setup install_requires = [] -if sys.version_info < (3, 5, 3): - raise RuntimeError('aiorwlock requires Python 3.5.3+') - - def read(f): return open(os.path.join(os.path.dirname(__file__), f)).read().strip() def read_version(): regexp = re.compile(r"^__version__\W*=\W*'([\d.abrc]+)'") - init_py = os.path.join(os.path.dirname(__file__), - 'aiorwlock', '__init__.py') + init_py = os.path.join( + os.path.dirname(__file__), 'aiorwlock', '__init__.py' + ) with open(init_py) as f: for line in f: match = regexp.match(line) if match is not None: return match.group(1) - else: - raise RuntimeError('Cannot find version in aiorwlock/__init__.py') + raise RuntimeError('Cannot find version in aiorwlock/__init__.py') classifiers = [ 'License :: OSI Approved :: Apache Software License', 'Intended Audience :: Developers', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Operating System :: OS Independent', 'Development Status :: 4 - Beta', 'Framework :: AsyncIO', ] - -setup(name='aiorwlock', - version=read_version(), - description=('Read write lock for asyncio.'), - long_description='\n\n'.join((read('README.rst'), read('CHANGES.rst'))), - classifiers=classifiers, - platforms=['POSIX'], - author='Nikolay Novik', - author_email='nickolaino...@gmail.com', - url='https://github.com/aio-libs/aiorwlock', - download_url='https://pypi.python.org/pypi/aiorwlock', - license='Apache 2', - packages=['aiorwlock'], - install_requires=install_requires, - keywords=['aiorwlock', 'lock', 'asyncio'], - zip_safe=True, - include_package_data=True) +project_urls = { + 'Website': 'https://github.com/aio-libs/aiorwlock', + 'Issues': 'https://github.com/aio-libs/aiorwlock/issues', +} + + +setup( + name='aiorwlock', + version=read_version(), + description=('Read write lock for asyncio.'), + long_description='\n\n'.join((read('README.rst'), read('CHANGES.rst'))), + classifiers=classifiers, + platforms=['POSIX'], + author='Nikolay Novik', + author_email='nickolaino...@gmail.com', + url='https://github.com/aio-libs/aiorwlock', + download_url='https://pypi.python.org/pypi/aiorwlock', + license='Apache 2', + packages=['aiorwlock'], + install_requires=install_requires, + keywords=['aiorwlock', 'lock', 'asyncio'], + zip_safe=True, + project_urls=project_urls, + python_requires='>=3.6.0', + include_package_data=True, +) diff -Nru aiorwlock-0.6.0/spec/aiorwlock.tla aiorwlock-1.0.0/spec/aiorwlock.tla --- aiorwlock-0.6.0/spec/aiorwlock.tla 1970-01-01 02:00:00.000000000 +0200 +++ aiorwlock-1.0.0/spec/aiorwlock.tla 2020-10-23 11:09:58.000000000 +0300 @@ -0,0 +1,70 @@ +----------------------------- MODULE aiorwlock ------------------------------ +EXTENDS Naturals, Sequences, Integers, FiniteSets +CONSTANTS Task +ASSUME /\ Task # {} + +VARIABLES RState, + WState, + Lock + +----------------------------------------------------------------------------- +TypeOK == /\ Lock \in [Task -> {"Read", "Write", "WriteRead", "Waiting", "Finished"}] + /\ RState >= 0 + /\ WState >= 0 /\ WState <= 2 +LockInit == Lock = [t \in Task |-> "Waiting"] /\ RState = 0 /\ WState = 0 +----------------------------------------------------------------------------- + + +Rlocked == RState > 0 +Wlocked == WState > 0 +Unlocked == RState = 0 /\ WState = 0 + +WOwn(t) == Lock[t] \in {"Write"} + +RAquire(t) == \/ /\ ~Wlocked + /\ Lock' = [Lock EXCEPT ![t] = "Read"] + /\ RState' = RState + 1 + /\ UNCHANGED WState + /\ Lock[t] \in {"Waiting"} + \/ /\ WOwn(t) + /\ Lock' = [Lock EXCEPT ![t] = "WriteRead"] + /\ RState' = RState + 1 + /\ UNCHANGED WState + +WAquire(t) == /\ Unlocked + /\ Lock' = [Lock EXCEPT ![t] = "Write"] + /\ WState' = WState + 1 + /\ UNCHANGED RState + /\ Lock[t] \in {"Waiting"} + + +RRelease(t) == \/ /\ Rlocked /\ Lock[t] = "Read" + /\ RState' = RState - 1 /\ Lock' = [Lock EXCEPT ![t] = "Finished"] + /\ UNCHANGED WState + \/ /\ Rlocked /\ Lock[t] = "WriteRead" + /\ RState' = RState - 1 /\ Lock' = [Lock EXCEPT ![t] = "Write"] + /\ UNCHANGED WState + +WRelease(t) == \/ /\ Wlocked /\ Lock[t] = "Write" + /\ WState' = WState - 1 /\ Lock' = [Lock EXCEPT ![t] = "Finished"] + /\ UNCHANGED RState + \/ /\ Wlocked /\ Lock[t] = "WriteRead" + /\ WState' = WState - 1 /\ Lock' = [Lock EXCEPT ![t] = "Read"] + /\ UNCHANGED RState +----------------------------------------------------------------------------- + +Next == \E t \in Task: RAquire(t) \/ WAquire(t) \/ RRelease(t) \/ WRelease(t) + +Spec == LockInit /\ [][Next]_<<RState, WState, Lock>> + + +LockInv == + \A t1 \in Task : \A t2 \in (Task \ {t1}): ~ + (/\ Lock[t1] \in {"Write", "WriteRead"} + /\ Lock[t2] \in {"Read", "Write", "WriteRead"}) +----------------------------------------------------------------------------- + +THEOREM Spec => [](TypeOK /\ LockInv) + +============================================================================= + diff -Nru aiorwlock-0.6.0/tests/conftest.py aiorwlock-1.0.0/tests/conftest.py --- aiorwlock-0.6.0/tests/conftest.py 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/tests/conftest.py 2020-10-23 11:09:58.000000000 +0300 @@ -1,17 +1,22 @@ import asyncio import gc -import uvloop import pytest +try: + import uvloop +except ImportError: + uvloop = None + @pytest.fixture(scope='module', params=[True, False], ids=['fast', 'slow']) def fast_track(request): return request.param -@pytest.fixture(scope='session', params=[True, False], - ids=['debug:true', 'debug:false']) +@pytest.fixture( + scope='session', params=[True, False], ids=['debug:true', 'debug:false'] +) def debug(request): return request.param @@ -22,10 +27,10 @@ @pytest.fixture -def loop(request, loop_type, debug): +def event_loop(request, loop_type, debug): # old_loop = asyncio.get_event_loop() asyncio.set_event_loop(None) - if loop_type == 'uvloop': + if loop_type == 'uvloop' and uvloop is not None: loop = uvloop.new_event_loop() else: loop = asyncio.new_event_loop() @@ -39,30 +44,6 @@ gc.collect() -@pytest.mark.tryfirst -def pytest_pycollect_makeitem(collector, name, obj): - if collector.funcnamefilter(name): - item = pytest.Function(name, parent=collector) - if 'run_loop' in item.keywords: - return list(collector._genfunctions(name, obj)) - - -@pytest.mark.tryfirst -def pytest_pyfunc_call(pyfuncitem): - """ - Run asyncio marked test functions in an event loop instead of a normal - function call. - """ - if 'run_loop' in pyfuncitem.keywords: - funcargs = pyfuncitem.funcargs - loop = funcargs['loop'] - testargs = {arg: funcargs[arg] - for arg in pyfuncitem._fixtureinfo.argnames} - loop.run_until_complete(pyfuncitem.obj(**testargs)) - return True - - -def pytest_runtest_setup(item): - if 'run_loop' in item.keywords and 'loop' not in item.fixturenames: - # inject an event loop fixture for all async tests - item.fixturenames.append('loop') +@pytest.fixture +def loop(event_loop): + return event_loop diff -Nru aiorwlock-0.6.0/tests/test_corner_cases.py aiorwlock-1.0.0/tests/test_corner_cases.py --- aiorwlock-0.6.0/tests/test_corner_cases.py 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/tests/test_corner_cases.py 2020-10-23 11:09:58.000000000 +0300 @@ -1,16 +1,16 @@ -import pytest import asyncio import contextlib -from aiorwlock import RWLock, current_task +import pytest +from aiorwlock import RWLock, _current_task ensure_future = asyncio.ensure_future @contextlib.contextmanager def should_fail(timeout, loop): - task = current_task(loop) + task = _current_task(loop) handle = loop.call_later(timeout, task.cancel) try: @@ -19,12 +19,13 @@ handle.cancel() return else: - assert False, ('Inner task expected to be cancelled', task) + msg = 'Inner task expected to be cancelled: {}'.format(task) + pytest.fail(msg) -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_get_write_then_read(loop): - rwlock = RWLock(loop=loop) + rwlock = RWLock() rl = rwlock.reader wl = rwlock.writer @@ -37,9 +38,9 @@ assert rl.locked -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_get_write_then_read_and_write_again(loop): - rwlock = RWLock(loop=loop) + rwlock = RWLock() rl = rwlock.reader wl = rwlock.writer @@ -48,27 +49,27 @@ async def get_write_lock(): await f - with should_fail(.1, loop): + with should_fail(0.1, loop): async with wl: assert wl.locked writes.append('should not be here') - ensure_future(get_write_lock(), loop=loop) + ensure_future(get_write_lock()) async with wl: assert wl.locked async with rl: f.set_result(None) - await asyncio.sleep(0.12, loop=loop) + await asyncio.sleep(0.12) # second task can not append to writes assert writes == [] assert rl.locked -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_writers_deadlock(loop): - rwlock = RWLock(loop=loop) + rwlock = RWLock() rl = rwlock.reader wl = rwlock.writer @@ -86,12 +87,12 @@ async def coro(): async with wl: assert wl.locked - await asyncio.sleep(.2, loop) + await asyncio.sleep(0.2, loop) async with rl: assert rl.locked - task_b = ensure_future(coro(), loop=loop) - task_c = ensure_future(coro(), loop=loop) + task_b = ensure_future(coro()) + task_c = ensure_future(coro()) await asyncio.sleep(0.1, loop) # cancel lock waiter right after release task_b.cancel() @@ -104,9 +105,9 @@ assert not wl.locked -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_readers_cancel(loop): - rwlock = RWLock(loop=loop) + rwlock = RWLock() rl = rwlock.reader wl = rwlock.writer @@ -117,8 +118,8 @@ async with wl: assert wl.locked - task_b = ensure_future(coro(rl), loop=loop) - task_c = ensure_future(coro(rl), loop=loop) + task_b = ensure_future(coro(rl)) + task_c = ensure_future(coro(rl)) await asyncio.sleep(0.1, loop) task_b.cancel() @@ -128,3 +129,24 @@ assert task_c.done() assert not rl.locked assert not wl.locked + + +@pytest.mark.asyncio +async def test_canceled_inside_acquire(loop): + rwlock = RWLock() + rl = rwlock.reader + + async def coro(lock): + async with lock: + pass + + task = ensure_future(coro(rl)) + await asyncio.sleep(0) + task.cancel() + + try: + await task + except asyncio.CancelledError: + pass + + assert not rl.locked diff -Nru aiorwlock-0.6.0/tests/test_rwlock.py aiorwlock-1.0.0/tests/test_rwlock.py --- aiorwlock-0.6.0/tests/test_rwlock.py 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/tests/test_rwlock.py 2020-10-23 11:09:58.000000000 +0300 @@ -1,19 +1,20 @@ import asyncio -from aiorwlock import RWLock, current_task import pytest +from aiorwlock import RWLock, _current_task + class Bunch(object): """A bunch of Tasks. Port python threading tests""" - def __init__(self, f, n, wait_before_exit=False, loop=None): + def __init__(self, f, n, wait_before_exit=False): """ Construct a bunch of `n` tasks running the same function `f`. If `wait_before_exit` is True, the tasks won't terminate until do_finish() is called. """ - self._loop = loop or asyncio.get_event_loop() + self._loop = asyncio.get_event_loop() self.f = f self.n = n self.started = [] @@ -23,56 +24,75 @@ self._futures = [] async def task(): - tid = current_task(loop=self._loop) + tid = _current_task() self.started.append(tid) try: await f() finally: self.finished.append(tid) while not self._can_exit: - await asyncio.sleep(0.01, loop=self._loop) + await asyncio.sleep(0.01) for _ in range(n): - t = asyncio.Task(task(), loop=self._loop) + t = asyncio.Task(task()) self._futures.append(t) async def wait_for_finished(self): - await asyncio.gather(*self._futures, loop=self._loop) + await asyncio.gather(*self._futures) def do_finish(self): self._can_exit = True -async def _wait(loop=None): - _loop = loop or asyncio.get_event_loop() - await asyncio.sleep(0.01, loop=_loop) +async def _wait(): + await asyncio.sleep(0.01) + + +def test_ctor_deprecated_implicit_not_running(loop): + with pytest.warns(DeprecationWarning): + RWLock() + + +def test_ctor_deprecated_explicit_non_running(loop): + with pytest.warns(DeprecationWarning): + RWLock(loop=loop) -def test_ctor_loop_reader(loop): - rwlock = RWLock(loop=loop).reader_lock +@pytest.mark.asyncio +async def test_ctor_loop_deprecated_arg(loop): + with pytest.warns(DeprecationWarning): + RWLock(loop=loop) + + +@pytest.mark.asyncio +async def test_ctor_loop_reader(loop): + rwlock = RWLock().reader_lock assert rwlock._lock._loop is loop -def test_ctor_noloop_reader(loop): +@pytest.mark.asyncio +async def test_ctor_noloop_reader(loop): asyncio.set_event_loop(loop) rwlock = RWLock().reader_lock assert rwlock._lock._loop is loop -def test_ctor_loop_writer(loop): - rwlock = RWLock(loop=loop).writer_lock +@pytest.mark.asyncio +async def test_ctor_loop_writer(loop): + rwlock = RWLock().writer_lock assert rwlock._lock._loop is loop -def test_ctor_noloop_writer(loop): +@pytest.mark.asyncio +async def test_ctor_noloop_writer(loop): asyncio.set_event_loop(loop) rwlock = RWLock().writer_lock assert rwlock._lock._loop is loop -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_repr(loop): - rwlock = RWLock(loop=loop) + rwlock = RWLock() assert 'RWLock' in rwlock.__repr__() assert 'WriterLock: [unlocked' in rwlock.__repr__() assert 'ReaderLock: [unlocked' in rwlock.__repr__() @@ -90,16 +110,16 @@ assert 'WriterLock: [unlocked]' in rwlock.__repr__() -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_release_unlocked(loop): - rwlock = RWLock(loop=loop) + rwlock = RWLock() with pytest.raises(RuntimeError): rwlock.reader_lock.release() -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_many_readers(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) N = 5 locked = [] nlocked = [] @@ -108,20 +128,20 @@ await rwlock.reader_lock.acquire() try: locked.append(1) - await _wait(loop=loop) + await _wait() nlocked.append(len(locked)) - await _wait(loop=loop) + await _wait() locked.pop(-1) finally: rwlock.reader_lock.release() - await Bunch(f, N, loop=loop).wait_for_finished() + await Bunch(f, N).wait_for_finished() assert max(nlocked) > 1 -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_read_upgrade_write_release(loop): - rwlock = RWLock(loop=loop) + rwlock = RWLock() await rwlock.writer_lock.acquire() await rwlock.reader_lock.acquire() await rwlock.reader_lock.acquire() @@ -143,10 +163,10 @@ rwlock.reader_lock.release() -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_reader_recursion(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) N = 5 locked = [] nlocked = [] @@ -157,22 +177,22 @@ await rwlock.reader_lock.acquire() try: locked.append(1) - await _wait(loop=loop) + await _wait() nlocked.append(len(locked)) - await _wait(loop=loop) + await _wait() locked.pop(-1) finally: rwlock.reader_lock.release() finally: rwlock.reader_lock.release() - await Bunch(f, N, loop=loop).wait_for_finished() + await Bunch(f, N).wait_for_finished() assert max(nlocked) > 1 -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_writer_recursion(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) N = 5 locked = [] nlocked = [] @@ -183,22 +203,22 @@ await rwlock.writer_lock.acquire() try: locked.append(1) - await _wait(loop=loop) + await _wait() nlocked.append(len(locked)) - await _wait(loop=loop) + await _wait() locked.pop(-1) finally: rwlock.writer_lock.release() finally: rwlock.writer_lock.release() - await Bunch(f, N, loop=loop).wait_for_finished() + await Bunch(f, N).wait_for_finished() assert max(nlocked) == 1 -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_writer_then_reader_recursion(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) N = 5 locked = [] nlocked = [] @@ -209,22 +229,22 @@ await rwlock.reader_lock.acquire() try: locked.append(1) - await _wait(loop=loop) + await _wait() nlocked.append(len(locked)) - await _wait(loop=loop) + await _wait() locked.pop(-1) finally: rwlock.reader_lock.release() finally: rwlock.writer_lock.release() - await Bunch(f, N, loop=loop).wait_for_finished() + await Bunch(f, N).wait_for_finished() assert max(nlocked) == 1 -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_writer_recursion_fail(loop): - rwlock = RWLock(loop=loop) + rwlock = RWLock() N = 5 locked = [] @@ -237,13 +257,13 @@ finally: rwlock.reader_lock.release() - await Bunch(f, N, loop=loop).wait_for_finished() + await Bunch(f, N).wait_for_finished() assert len(locked) == N -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_readers_writers(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) N = 5 rlocked = [] wlocked = [] @@ -253,9 +273,9 @@ await rwlock.reader_lock.acquire() try: rlocked.append(1) - await _wait(loop=loop) + await _wait() nlocked.append((len(rlocked), len(wlocked))) - await _wait(loop=loop) + await _wait() rlocked.pop(-1) finally: rwlock.reader_lock.release() @@ -264,22 +284,25 @@ await rwlock.writer_lock.acquire() try: wlocked.append(1) - await _wait(loop=loop) + await _wait() nlocked.append((len(rlocked), len(wlocked))) - await _wait(loop=loop) + await _wait() wlocked.pop(-1) finally: rwlock.writer_lock.release() - b1 = Bunch(r, N, loop=loop) - b2 = Bunch(w, N, loop=loop) + b1 = Bunch(r, N) + b2 = Bunch(w, N) - await asyncio.sleep(0.0001, loop=loop) + await asyncio.sleep(0.0001) await b1.wait_for_finished() await b2.wait_for_finished() - r, w, = zip(*nlocked) + ( + r, + w, + ) = zip(*nlocked) assert max(r) > 1 assert max(w) == 1 @@ -291,10 +314,10 @@ assert w == 0 -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_writer_success(loop): # Verify that a writer can get access - rwlock = RWLock(loop=loop) + rwlock = RWLock() N = 5 reads = 0 writes = 0 @@ -314,10 +337,10 @@ async def w(): nonlocal reads, writes while reads == 0: - await _wait(loop=loop) + await _wait() for _ in range(2): - await _wait(loop=loop) + await _wait() # print("current pre-writes", reads) await rwlock.writer_lock.acquire() @@ -327,8 +350,8 @@ finally: rwlock.writer_lock.release() - b1 = Bunch(r, N, loop=loop) - b2 = Bunch(w, 1, loop=loop) + b1 = Bunch(r, N) + b2 = Bunch(w, 1) await b1.wait_for_finished() await b2.wait_for_finished() @@ -337,10 +360,10 @@ # print('>>>>>>>>>>>', writes, reads) -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_writer_success_fast(loop): # Verify that a writer can get access - rwlock = RWLock(loop=loop, fast=True) + rwlock = RWLock(fast=True) N = 5 reads = 0 writes = 0 @@ -354,17 +377,17 @@ try: reads += 1 # print("current reads", reads) - await asyncio.sleep(0, loop=loop) + await asyncio.sleep(0) finally: rwlock.reader_lock.release() async def w(): nonlocal reads, writes while reads == 0: - await _wait(loop=loop) + await _wait() for _ in range(2): - await _wait(loop=loop) + await _wait() # print("current pre-writes", reads) await rwlock.writer_lock.acquire() @@ -374,8 +397,8 @@ finally: rwlock.writer_lock.release() - b1 = Bunch(r, N, loop=loop) - b2 = Bunch(w, 1, loop=loop) + b1 = Bunch(r, N) + b2 = Bunch(w, 1) await b1.wait_for_finished() await b2.wait_for_finished() @@ -384,10 +407,10 @@ # print('>>>>>>>>>>>', writes, reads) -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_writer_success_with_statement(loop): # Verify that a writer can get access - rwlock = RWLock(loop=loop) + rwlock = RWLock() N = 5 reads = 0 writes = 0 @@ -404,17 +427,17 @@ async def w(): nonlocal reads, writes while reads == 0: - await _wait(loop=loop) + await _wait() for _ in range(2): - await _wait(loop=loop) + await _wait() # print("current pre-writes", reads) async with rwlock.writer_lock: writes += 1 - b1 = Bunch(r, N, loop=loop) - b2 = Bunch(w, 1, loop=loop) + b1 = Bunch(r, N) + b2 = Bunch(w, 1) await b1.wait_for_finished() await b2.wait_for_finished() @@ -423,39 +446,41 @@ # print('>>>>>>>>>>>', writes, reads) -def test_raise_error_on_with_for_reader_lock(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) +@pytest.mark.asyncio +async def test_raise_error_on_with_for_reader_lock(loop, fast_track): + rwlock = RWLock(fast=fast_track) with pytest.raises(RuntimeError): with rwlock.reader_lock: pass -def test_raise_error_on_with_for_writer_lock(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) +@pytest.mark.asyncio +async def test_raise_error_on_with_for_writer_lock(loop, fast_track): + rwlock = RWLock(fast=fast_track) with pytest.raises(RuntimeError): with rwlock.writer_lock: pass -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_read_locked(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) assert not rwlock.reader_lock.locked async with rwlock.reader_lock: assert rwlock.reader_lock.locked -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_write_locked(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) assert not rwlock.writer_lock.locked async with rwlock.writer_lock: assert rwlock.writer_lock.locked -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_write_read_lock_multiple_tasks(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) rl = rwlock.reader wl = rwlock.writer @@ -468,7 +493,7 @@ async with wl: assert wl.locked assert not rl.locked - task = asyncio.Task(coro(), loop=loop) + task = asyncio.Task(coro()) await asyncio.sleep(0.1, loop) await task assert not rl.locked @@ -477,42 +502,42 @@ def test_current_task(loop): with pytest.raises(RuntimeError): - current_task(loop=loop) + _current_task(loop) with pytest.raises(RuntimeError): - current_task() + _current_task() -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_read_context_manager(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) reader = rwlock.reader_lock assert not reader.locked async with reader: assert reader.locked -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_write_context_manager(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) writer = rwlock.writer_lock assert not writer.locked async with writer: assert writer.locked -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_await_read_lock(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) reader = rwlock.reader_lock assert not reader.locked async with reader: assert reader.locked -@pytest.mark.run_loop +@pytest.mark.asyncio async def test_await_write_lock(loop, fast_track): - rwlock = RWLock(loop=loop, fast=fast_track) + rwlock = RWLock(fast=fast_track) writer = rwlock.writer_lock assert not writer.locked async with writer: diff -Nru aiorwlock-0.6.0/.travis.yml aiorwlock-1.0.0/.travis.yml --- aiorwlock-0.6.0/.travis.yml 2018-12-17 21:09:26.000000000 +0200 +++ aiorwlock-1.0.0/.travis.yml 1970-01-01 02:00:00.000000000 +0200 @@ -1,39 +0,0 @@ -dist: xenial -sudo: required - -language: python -python: -- 3.5.3 -- 3.6 - -matrix: - include: - - python: '3.7' - env: MYPY="y" - -install: - - pip install -r requirements-dev.txt - - pip install . - - pip install codecov - -script: - - | - if [ "$MYPY" = "y" ]; then - make mypy - fi; - - make cov - -after_success: - - codecov - - -deploy: - provider: pypi - user: aio-libs-bot - password: - secure: "xrYq78Jpw3rfHNc+LDS3IXi0cDZukAgaDuJ+xbkRLwijeSTR8LA9ZFU0OYK2X/+YBrtegBKB15ZkEkjfN702MNaR3wqHGxs+EWVcZc6Yy/lipGTjfGJEiDcIF+wKzFR5wqSL8Hrj66cDnY7dK07jfHc5KswTUizmiYLqGnAJGI+zQVnHCIv6W6iC06LY9eLcBXi514toMN9ku+anNBtzQ14gjW4N0ccCGKaeeAzPsvrMcnpjUh1xOhFG/aTBFpEPLbJ7ZUS87kPabf9/WsLS0tEW8ggSgAZT9GTljbe4I8/Vg14EJCQ4KSpcQ8z9gsf1AAB8XEYIN0jx2ub/c6gYwlazoOln6EE+WbNTHqEx0SiCkHrL7gIOoXEWYG/xPGraYQIi1xD6Yl6nmi0bixUQOgsYGKYb131ycyMJjsHGTdRHnqnja0ev9VIWr5zBu1IKtE/46QRMFYpg5PPAVX211wVsvZwH8plueIA1WRfzwZQoAiZDhQ4Wusql6jFVleeqKy4UldYy3SFoFL0qzlwvm/cx7dkP97NoW8Xtoizz0CJt/BeFGViCaRYbi3Z45rjSaeVWhfcwnIaRdd3FgadxWhJXEohgnoafwCteNz3NGU0lbxBfuOfYEOFgcwdYMzF0KjsGwbfekdAa/ZcLxnuvmJXFXPo/YrWusVw4nBR/9pU=" - distributions: sdist bdist_wheel - on: - tags: true - all_branches: true - python: 3.7