From e0a20dbaa79e0fae3c1b346ae46ad348f25f345e Mon Sep 17 00:00:00 2001 From: Vizonex Date: Mon, 4 Aug 2025 21:04:36 -0500 Subject: [PATCH 1/3] add typehintings and refactor lock objects using __slots__ and document more functions that users have direct access to --- aiorwlock/__init__.py | 63 +++++++++++----- examples/context_manager.py | 6 +- examples/simple.py | 6 +- tests/conftest.py | 8 +- tests/test_corner_cases.py | 45 +++++++----- tests/test_rwlock.py | 141 +++++++++++++++++++++--------------- 6 files changed, 162 insertions(+), 107 deletions(-) diff --git a/aiorwlock/__init__.py b/aiorwlock/__init__.py index 639cc3d..aa25e03 100644 --- a/aiorwlock/__init__.py +++ b/aiorwlock/__init__.py @@ -3,7 +3,7 @@ from collections import deque from typing import Any, Deque, List, Tuple -__all__ = ('RWLock', '__version__') +__all__ = ("RWLock", "__version__") def __getattr__(name: str) -> object: @@ -30,7 +30,19 @@ class _RWLockCore: _WL = 2 _loop = None - def __init__(self, fast: bool): + __slots__ = ( + "_RL", + "_WL", + "_loop", + "_do_yeild", + "_read_waiters", + "_write_waiters", + "_r_state", + "_w_state", + "_owning", + ) + + def __init__(self, fast: bool) -> None: self._do_yield = not fast self._read_waiters: Deque[asyncio.Future[None]] = deque() self._write_waiters: Deque[asyncio.Future[None]] = deque() @@ -39,6 +51,10 @@ def __init__(self, fast: bool): # tasks will be few, so a list is not inefficient self._owning: List[Tuple[asyncio.Task[Any], int]] = [] + # TODO: There is a Bug when different Loops are in use with using RWLocks + # However this might have to do with version differences + # SEE: https://github.com/aio-libs/aiorwlock/issues/468 + def _get_loop(self) -> asyncio.AbstractEventLoop: """ From: https://github.com/python/cpython/blob/3.12/Lib/asyncio/mixins.py @@ -51,7 +67,7 @@ def _get_loop(self) -> asyncio.AbstractEventLoop: if self._loop is None: self._loop = loop if loop is not self._loop: - raise RuntimeError(f'{self!r} is bound to a different event loop') + raise RuntimeError(f"{self!r} is bound to a different event loop") return loop @property @@ -82,11 +98,7 @@ async def acquire_read(self) -> bool: 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)) await self._yield_after_acquire(self._RL) @@ -120,7 +132,7 @@ async def acquire_write(self) -> bool: return True elif (me, self._RL) in self._owning: if self._r_state > 0: - raise RuntimeError('Cannot upgrade RWLock from read to write') + raise RuntimeError("Cannot upgrade RWLock from read to write") if self._r_state == 0 and self._w_state == 0: self._w_state += 1 @@ -157,7 +169,7 @@ def _release(self, lock_type: int) -> None: try: self._owning.remove((me, lock_type)) except ValueError as exc: - raise RuntimeError('Cannot release an un-acquired lock') from exc + raise RuntimeError("Cannot release an un-acquired lock") from exc if lock_type == self._RL: self._r_state -= 1 else: @@ -186,10 +198,11 @@ def _wake_up(self) -> None: class _ContextManagerMixin: + __slots__ = () + def __enter__(self) -> None: - raise RuntimeError( - '"await" should be used as context manager expression' - ) + # TODO: This Error Should really hint at either "async with" or "await" instead of just "await" + raise RuntimeError('"await" should be used as context manager expression') def __exit__(self, *args: Any) -> None: # This must exist because __enter__ exists, even though that @@ -214,41 +227,51 @@ def release(self) -> None: # Lock objects to access the _RWLockCore in reader or writer mode class _ReaderLock(_ContextManagerMixin): + __slots__ = ("_lock",) + def __init__(self, lock: _RWLockCore) -> None: self._lock = lock @property def locked(self) -> bool: + """Determines wether or not the reader lock is owned""" return self._lock.read_locked async def acquire(self) -> None: + """acquires the reading lock""" await self._lock.acquire_read() def release(self) -> None: + """releases the reading lock""" self._lock.release_read() def __repr__(self) -> str: - status = 'locked' if self._lock._r_state > 0 else 'unlocked' - return f'' + status = "locked" if self._lock._r_state > 0 else "unlocked" + return f"" class _WriterLock(_ContextManagerMixin): - def __init__(self, lock: _RWLockCore): + __slots__ = ("_lock",) + + def __init__(self, lock: _RWLockCore) -> None: self._lock = lock @property def locked(self) -> bool: + """determines wether or not writing lock is owned""" return self._lock.write_locked async def acquire(self) -> None: + """acquires the writing lock""" await self._lock.acquire_write() def release(self) -> None: + """releases the writing lock""" self._lock.release_write() def __repr__(self) -> str: - status = 'locked' if self._lock._w_state > 0 else 'unlocked' - return f'' + status = "locked" if self._lock._w_state > 0 else "unlocked" + return f"" class RWLock: @@ -260,6 +283,8 @@ class RWLock: core = _RWLockCore + __slots__ = ("core", "_reader_lock", "_writer_lock",) + def __init__(self, *, fast: bool = False) -> None: core = self.core(fast) self._reader_lock = _ReaderLock(core) @@ -282,4 +307,4 @@ def writer(self) -> _WriterLock: def __repr__(self) -> str: rl = self.reader_lock.__repr__() wl = self.writer_lock.__repr__() - return f'' + return f"" diff --git a/examples/context_manager.py b/examples/context_manager.py index 51be53e..9cfb8df 100644 --- a/examples/context_manager.py +++ b/examples/context_manager.py @@ -3,17 +3,17 @@ import aiorwlock -async def go(): +async def go() -> None: rwlock = aiorwlock.RWLock() # acquire reader lock async with rwlock.reader_lock: - print('inside reader lock') + print("inside reader lock") await asyncio.sleep(0.1) # acquire writer lock async with rwlock.writer_lock: - print('inside writer lock') + print("inside writer lock") await asyncio.sleep(0.1) diff --git a/examples/simple.py b/examples/simple.py index 625cbee..967df55 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -3,13 +3,13 @@ import aiorwlock -async def go(): +async def go() -> None: rwlock = aiorwlock.RWLock() # acquire reader lock await rwlock.reader_lock.acquire() try: - print('inside reader lock') + print("inside reader lock") await asyncio.sleep(0.1) finally: @@ -18,7 +18,7 @@ async def go(): # acquire writer lock await rwlock.writer_lock.acquire() try: - print('inside writer lock') + print("inside writer lock") await asyncio.sleep(0.1) finally: diff --git a/tests/conftest.py b/tests/conftest.py index f09799d..41557e1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,13 @@ +import asyncio + import pytest -@pytest.fixture(scope='module', params=[True, False], ids=['fast', 'slow']) -def fast_track(request): +@pytest.fixture(scope="module", params=[True, False], ids=["fast", "slow"]) +def fast_track(request: pytest.FixtureRequest) -> bool: return request.param @pytest.fixture -def loop(event_loop): +def loop(event_loop: asyncio.AbstractEventLoop): return event_loop diff --git a/tests/test_corner_cases.py b/tests/test_corner_cases.py index 42f20c9..c2a97d8 100644 --- a/tests/test_corner_cases.py +++ b/tests/test_corner_cases.py @@ -1,5 +1,6 @@ import asyncio import contextlib +from typing import Any, Generator import pytest @@ -9,7 +10,9 @@ @contextlib.contextmanager -def should_fail(timeout, loop): +def should_fail( + timeout: float, loop: asyncio.AbstractEventLoop +) -> Generator[None, Any, None]: task = asyncio.current_task(loop) handle = loop.call_later(timeout, task.cancel) @@ -19,12 +22,12 @@ def should_fail(timeout, loop): handle.cancel() return else: - msg = f'Inner task expected to be cancelled: {task}' + msg = f"Inner task expected to be cancelled: {task}" pytest.fail(msg) @pytest.mark.asyncio -async def test_get_write_then_read(loop): +async def test_get_write_then_read(loop: asyncio.AbstractEventLoop) -> None: rwlock = RWLock() rl = rwlock.reader @@ -39,7 +42,9 @@ async def test_get_write_then_read(loop): @pytest.mark.asyncio -async def test_get_write_then_read_and_write_again(loop): +async def test_get_write_then_read_and_write_again( + loop: asyncio.AbstractEventLoop, +) -> None: rwlock = RWLock() rl = rwlock.reader wl = rwlock.writer @@ -52,7 +57,7 @@ async def get_write_lock(): with should_fail(0.1, loop): async with wl: assert wl.locked - writes.append('should not be here') + writes.append("should not be here") ensure_future(get_write_lock()) @@ -68,7 +73,7 @@ async def get_write_lock(): @pytest.mark.asyncio -async def test_writers_deadlock(loop): +async def test_writers_deadlock(loop: asyncio.AbstractEventLoop) -> None: rwlock = RWLock() rl = rwlock.reader wl = rwlock.writer @@ -84,7 +89,7 @@ async def test_writers_deadlock(loop): # See asyncio.Lock deadlock issue: # https://github.com/python/cpython/pull/1031 - async def coro(): + async def coro() -> None: async with wl: assert wl.locked await asyncio.sleep(0.2, loop) @@ -106,12 +111,12 @@ async def coro(): @pytest.mark.asyncio -async def test_readers_cancel(loop): +async def test_readers_cancel(loop: asyncio.AbstractEventLoop) -> None: rwlock = RWLock() rl = rwlock.reader wl = rwlock.writer - async def coro(lock): + async def coro(lock: RWLock): async with lock: assert lock.locked await asyncio.sleep(0.2, loop) @@ -132,11 +137,11 @@ async def coro(lock): @pytest.mark.asyncio -async def test_canceled_inside_acquire(loop): +async def test_canceled_inside_acquire(loop: asyncio.AbstractEventLoop) -> None: rwlock = RWLock() rl = rwlock.reader - async def coro(lock): + async def coro(lock: RWLock): async with lock: pass @@ -153,24 +158,24 @@ async def coro(lock): @pytest.mark.asyncio -async def test_race_multiple_writers(loop): +async def test_race_multiple_writers(loop: asyncio.AbstractEventLoop) -> None: seq = [] - async def write_wait(lock): + async def write_wait(lock: RWLock): async with lock.reader: await asyncio.sleep(0.1) - seq.append('READ') + seq.append("READ") async with lock.writer: - seq.append('START1') + seq.append("START1") await asyncio.sleep(0.1) - seq.append('FIN1') + seq.append("FIN1") - async def write(lock): + async def write(lock: RWLock): async with lock.writer: - seq.append('START2') + seq.append("START2") await asyncio.sleep(0.1) - seq.append('FIN2') + seq.append("FIN2") lock = RWLock(fast=True) await asyncio.gather(write_wait(lock), write(lock)) - assert seq == ['READ', 'START2', 'FIN2', 'START1', 'FIN1'] + assert seq == ["READ", "START2", "FIN2", "START1", "FIN1"] diff --git a/tests/test_rwlock.py b/tests/test_rwlock.py index 56bf528..99dc46a 100644 --- a/tests/test_rwlock.py +++ b/tests/test_rwlock.py @@ -1,4 +1,5 @@ import asyncio +from typing import Any, Callable, Coroutine import pytest @@ -8,7 +9,9 @@ class Bunch: """A bunch of Tasks. Port python threading tests""" - def __init__(self, f, n, wait_before_exit=False): + def __init__( + self, f: Callable[..., Coroutine[Any, Any, Any]], n: int, wait_before_exit=False + ) -> None: """ Construct a bunch of `n` tasks running the same function `f`. If `wait_before_exit` is True, the tasks won't terminate until @@ -17,13 +20,13 @@ def __init__(self, f, n, wait_before_exit=False): self._loop = asyncio.get_event_loop() self.f = f self.n = n - self.started = [] - self.finished = [] + self.started: list[asyncio.Task[Any]] = [] + self.finished: list[asyncio.Task[Any]] = [] self._can_exit = not wait_before_exit - self._futures = [] + self._futures: list[asyncio.Task[Any]] = [] - async def task(): + async def task() -> None: tid = asyncio.current_task() self.started.append(tid) try: @@ -37,10 +40,10 @@ async def task(): t = asyncio.Task(task()) self._futures.append(t) - async def wait_for_finished(self): + async def wait_for_finished(self) -> None: await asyncio.gather(*self._futures) - def do_finish(self): + def do_finish(self) -> None: self._can_exit = True @@ -49,66 +52,66 @@ async def _wait(): @pytest.mark.asyncio -async def test_ctor_loop_reader(loop): +async def test_ctor_loop_reader(loop: asyncio.AbstractEventLoop) -> None: rwlock = RWLock().reader_lock assert rwlock._lock._get_loop() is loop @pytest.mark.asyncio -async def test_ctor_noloop_reader(loop): +async def test_ctor_noloop_reader(loop: asyncio.AbstractEventLoop) -> None: asyncio.set_event_loop(loop) rwlock = RWLock().reader_lock assert rwlock._lock._get_loop() is loop @pytest.mark.asyncio -async def test_ctor_loop_writer(loop): +async def test_ctor_loop_writer(loop: asyncio.AbstractEventLoop) -> None: rwlock = RWLock().writer_lock assert rwlock._lock._get_loop() is loop @pytest.mark.asyncio -async def test_ctor_noloop_writer(loop): +async def test_ctor_noloop_writer(loop: asyncio.AbstractEventLoop) -> None: asyncio.set_event_loop(loop) rwlock = RWLock().writer_lock assert rwlock._lock._get_loop() is loop @pytest.mark.asyncio -async def test_repr(loop): +async def test_repr(loop: asyncio.AbstractEventLoop) -> None: rwlock = RWLock() - assert 'RWLock' in rwlock.__repr__() - assert 'WriterLock: [unlocked' in rwlock.__repr__() - assert 'ReaderLock: [unlocked' in rwlock.__repr__() + assert "RWLock" in rwlock.__repr__() + assert "WriterLock: [unlocked" in rwlock.__repr__() + assert "ReaderLock: [unlocked" in rwlock.__repr__() # reader lock __repr__ await rwlock.reader_lock.acquire() - assert 'ReaderLock: [locked]' in rwlock.__repr__() + assert "ReaderLock: [locked]" in rwlock.__repr__() rwlock.reader_lock.release() - assert 'ReaderLock: [unlocked]' in rwlock.__repr__() + assert "ReaderLock: [unlocked]" in rwlock.__repr__() # writer lock __repr__ await rwlock.writer_lock.acquire() - assert 'WriterLock: [locked]' in rwlock.__repr__() + assert "WriterLock: [locked]" in rwlock.__repr__() rwlock.writer_lock.release() - assert 'WriterLock: [unlocked]' in rwlock.__repr__() + assert "WriterLock: [unlocked]" in rwlock.__repr__() @pytest.mark.asyncio -async def test_release_unlocked(loop): +async def test_release_unlocked(loop: asyncio.AbstractEventLoop) -> None: rwlock = RWLock() with pytest.raises(RuntimeError): rwlock.reader_lock.release() @pytest.mark.asyncio -async def test_many_readers(loop, fast_track): +async def test_many_readers(loop: asyncio.AbstractEventLoop, fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) N = 5 locked = [] nlocked = [] - async def f(): + async def f() -> None: await rwlock.reader_lock.acquire() try: locked.append(1) @@ -124,7 +127,7 @@ async def f(): @pytest.mark.asyncio -async def test_read_upgrade_write_release(loop): +async def test_read_upgrade_write_release(loop: asyncio.AbstractEventLoop) -> None: rwlock = RWLock() await rwlock.writer_lock.acquire() await rwlock.reader_lock.acquire() @@ -148,7 +151,9 @@ async def test_read_upgrade_write_release(loop): @pytest.mark.asyncio -async def test_reader_recursion(loop, fast_track): +async def test_reader_recursion( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) N = 5 locked = [] @@ -174,7 +179,9 @@ async def f(): @pytest.mark.asyncio -async def test_writer_recursion(loop, fast_track): +async def test_writer_recursion( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) N = 5 locked = [] @@ -200,7 +207,9 @@ async def f(): @pytest.mark.asyncio -async def test_writer_then_reader_recursion(loop, fast_track): +async def test_writer_then_reader_recursion( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) N = 5 locked = [] @@ -226,7 +235,7 @@ async def f(): @pytest.mark.asyncio -async def test_writer_recursion_fail(loop): +async def test_writer_recursion_fail(loop: asyncio.AbstractEventLoop) -> None: rwlock = RWLock() N = 5 locked = [] @@ -245,14 +254,16 @@ async def f(): @pytest.mark.asyncio -async def test_readers_writers(loop, fast_track): +async def test_readers_writers( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) N = 5 rlocked = [] wlocked = [] nlocked = [] - async def r(): + async def r() -> None: await rwlock.reader_lock.acquire() try: rlocked.append(1) @@ -263,7 +274,7 @@ async def r(): finally: rwlock.reader_lock.release() - async def w(): + async def w() -> None: await rwlock.writer_lock.acquire() try: wlocked.append(1) @@ -298,14 +309,14 @@ async def w(): @pytest.mark.asyncio -async def test_writer_success(loop): +async def test_writer_success(loop: asyncio.AbstractEventLoop) -> None: # Verify that a writer can get access rwlock = RWLock() N = 5 reads = 0 writes = 0 - async def r(): + async def r() -> None: # read until we achive write successes nonlocal reads, writes while writes < 2: @@ -317,7 +328,7 @@ async def r(): finally: rwlock.reader_lock.release() - async def w(): + async def w() -> None: nonlocal reads, writes while reads == 0: await _wait() @@ -344,14 +355,14 @@ async def w(): @pytest.mark.asyncio -async def test_writer_success_fast(loop): +async def test_writer_success_fast(loop: asyncio.AbstractEventLoop) -> None: # Verify that a writer can get access rwlock = RWLock(fast=True) N = 5 reads = 0 writes = 0 - async def r(): + async def r() -> None: # read until we achive write successes nonlocal reads, writes while writes < 2: @@ -364,7 +375,7 @@ async def r(): finally: rwlock.reader_lock.release() - async def w(): + async def w() -> None: nonlocal reads, writes while reads == 0: await _wait() @@ -391,14 +402,14 @@ async def w(): @pytest.mark.asyncio -async def test_writer_success_with_statement(loop): +async def test_writer_success_with_statement(loop: asyncio.AbstractEventLoop) -> None: # Verify that a writer can get access rwlock = RWLock() N = 5 reads = 0 writes = 0 - async def r(): + async def r() -> None: # read until we achive write successes nonlocal reads, writes while writes < 2: @@ -407,7 +418,7 @@ async def r(): reads += 1 # print("current reads", reads) - async def w(): + async def w() -> None: nonlocal reads, writes while reads == 0: await _wait() @@ -430,7 +441,9 @@ async def w(): @pytest.mark.asyncio -async def test_raise_error_on_with_for_reader_lock(loop, fast_track): +async def test_raise_error_on_with_for_reader_lock( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) with pytest.raises(RuntimeError): with rwlock.reader_lock: @@ -438,7 +451,9 @@ async def test_raise_error_on_with_for_reader_lock(loop, fast_track): @pytest.mark.asyncio -async def test_raise_error_on_with_for_writer_lock(loop, fast_track): +async def test_raise_error_on_with_for_writer_lock( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) with pytest.raises(RuntimeError): with rwlock.writer_lock: @@ -446,7 +461,7 @@ async def test_raise_error_on_with_for_writer_lock(loop, fast_track): @pytest.mark.asyncio -async def test_read_locked(loop, fast_track): +async def test_read_locked(loop: asyncio.AbstractEventLoop, fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) assert not rwlock.reader_lock.locked async with rwlock.reader_lock: @@ -454,7 +469,7 @@ async def test_read_locked(loop, fast_track): @pytest.mark.asyncio -async def test_write_locked(loop, fast_track): +async def test_write_locked(loop: asyncio.AbstractEventLoop, fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) assert not rwlock.writer_lock.locked async with rwlock.writer_lock: @@ -462,12 +477,14 @@ async def test_write_locked(loop, fast_track): @pytest.mark.asyncio -async def test_write_read_lock_multiple_tasks(loop, fast_track): +async def test_write_read_lock_multiple_tasks( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) rl = rwlock.reader wl = rwlock.writer - async def coro(): + async def coro() -> None: async with rl: assert not wl.locked assert rl.locked @@ -484,7 +501,9 @@ async def coro(): @pytest.mark.asyncio -async def test_read_context_manager(loop, fast_track): +async def test_read_context_manager( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) reader = rwlock.reader_lock assert not reader.locked @@ -493,7 +512,9 @@ async def test_read_context_manager(loop, fast_track): @pytest.mark.asyncio -async def test_write_context_manager(loop, fast_track): +async def test_write_context_manager( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) writer = rwlock.writer_lock assert not writer.locked @@ -502,7 +523,9 @@ async def test_write_context_manager(loop, fast_track): @pytest.mark.asyncio -async def test_await_read_lock(loop, fast_track): +async def test_await_read_lock( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) reader = rwlock.reader_lock assert not reader.locked @@ -511,7 +534,9 @@ async def test_await_read_lock(loop, fast_track): @pytest.mark.asyncio -async def test_await_write_lock(loop, fast_track): +async def test_await_write_lock( + loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: rwlock = RWLock(fast=fast_track) writer = rwlock.writer_lock assert not writer.locked @@ -520,16 +545,14 @@ async def test_await_write_lock(loop, fast_track): @pytest.mark.asyncio -async def test_writer_ambiguous_loops(fast_track): +async def test_writer_ambiguous_loops(fast_track: bool) -> None: loop = asyncio.new_event_loop() try: lock = RWLock(fast=fast_track) lock._writer_lock._lock._loop = loop - with pytest.raises( - RuntimeError, match='is bound to a different event loop' - ): + with pytest.raises(RuntimeError, match="is bound to a different event loop"): async with lock.writer_lock: pass finally: @@ -537,24 +560,24 @@ async def test_writer_ambiguous_loops(fast_track): @pytest.mark.asyncio -async def test_reader_ambiguous_loops(fast_track): +async def test_reader_ambiguous_loops(fast_track: bool) -> None: loop = asyncio.new_event_loop() try: lock = RWLock(fast=fast_track) lock._reader_lock._lock._loop = loop - with pytest.raises( - RuntimeError, match='is bound to a different event loop' - ): + with pytest.raises(RuntimeError, match="is bound to a different event loop"): async with lock.reader_lock: pass finally: loop.close() -def test_created_outside_of_coroutine(event_loop, fast_track): - async def main(): +def test_created_outside_of_coroutine( + event_loop: asyncio.AbstractEventLoop, fast_track: bool +) -> None: + async def main() -> None: async with lock.reader_lock: pass async with lock.writer_lock: From 852327cb2242c30bc27a273009cca74da07e32d3 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 5 Aug 2025 10:26:07 -0500 Subject: [PATCH 2/3] typo fix, thanks agronholm --- aiorwlock/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiorwlock/__init__.py b/aiorwlock/__init__.py index aa25e03..54e981e 100644 --- a/aiorwlock/__init__.py +++ b/aiorwlock/__init__.py @@ -34,7 +34,7 @@ class _RWLockCore: "_RL", "_WL", "_loop", - "_do_yeild", + "_do_yield", "_read_waiters", "_write_waiters", "_r_state", From cab4e5702c6c78ebdb89f9d77aec237fc457ca03 Mon Sep 17 00:00:00 2001 From: Vizonex Date: Tue, 5 Aug 2025 10:40:25 -0500 Subject: [PATCH 3/3] fix slot issues and repair tests to work with current pytest-asyncio --- aiorwlock/__init__.py | 11 +++-- tests/conftest.py | 6 +-- tests/test_corner_cases.py | 23 +++++----- tests/test_rwlock.py | 90 +++++++++++++++++--------------------- 4 files changed, 56 insertions(+), 74 deletions(-) diff --git a/aiorwlock/__init__.py b/aiorwlock/__init__.py index 54e981e..1314f2e 100644 --- a/aiorwlock/__init__.py +++ b/aiorwlock/__init__.py @@ -1,7 +1,7 @@ import asyncio import threading from collections import deque -from typing import Any, Deque, List, Tuple +from typing import Any, Deque, List, Tuple, Optional __all__ = ("RWLock", "__version__") @@ -28,18 +28,16 @@ def __getattr__(name: str) -> object: class _RWLockCore: _RL = 1 _WL = 2 - _loop = None + __slots__ = ( - "_RL", - "_WL", - "_loop", "_do_yield", "_read_waiters", "_write_waiters", "_r_state", "_w_state", "_owning", + "_loop" ) def __init__(self, fast: bool) -> None: @@ -50,6 +48,7 @@ def __init__(self, fast: bool) -> None: self._w_state: int = 0 # tasks will be few, so a list is not inefficient self._owning: List[Tuple[asyncio.Task[Any], int]] = [] + self._loop: Optional[asyncio.AbstractEventLoop] = None # TODO: There is a Bug when different Loops are in use with using RWLocks # However this might have to do with version differences @@ -283,7 +282,7 @@ class RWLock: core = _RWLockCore - __slots__ = ("core", "_reader_lock", "_writer_lock",) + __slots__ = ("_reader_lock", "_writer_lock",) def __init__(self, *, fast: bool = False) -> None: core = self.core(fast) diff --git a/tests/conftest.py b/tests/conftest.py index 41557e1..0e664f9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,9 @@ import asyncio import pytest +import pytest_asyncio @pytest.fixture(scope="module", params=[True, False], ids=["fast", "slow"]) def fast_track(request: pytest.FixtureRequest) -> bool: return request.param - - -@pytest.fixture -def loop(event_loop: asyncio.AbstractEventLoop): - return event_loop diff --git a/tests/test_corner_cases.py b/tests/test_corner_cases.py index c2a97d8..7458621 100644 --- a/tests/test_corner_cases.py +++ b/tests/test_corner_cases.py @@ -10,9 +10,8 @@ @contextlib.contextmanager -def should_fail( - timeout: float, loop: asyncio.AbstractEventLoop -) -> Generator[None, Any, None]: +def should_fail(timeout: float) -> Generator[None, Any, None]: + loop = asyncio.get_running_loop() task = asyncio.current_task(loop) handle = loop.call_later(timeout, task.cancel) @@ -27,7 +26,7 @@ def should_fail( @pytest.mark.asyncio -async def test_get_write_then_read(loop: asyncio.AbstractEventLoop) -> None: +async def test_get_write_then_read() -> None: rwlock = RWLock() rl = rwlock.reader @@ -42,13 +41,11 @@ async def test_get_write_then_read(loop: asyncio.AbstractEventLoop) -> None: @pytest.mark.asyncio -async def test_get_write_then_read_and_write_again( - loop: asyncio.AbstractEventLoop, -) -> None: +async def test_get_write_then_read_and_write_again() -> None: + loop = asyncio.get_event_loop() rwlock = RWLock() rl = rwlock.reader wl = rwlock.writer - f = loop.create_future() writes = [] @@ -73,7 +70,8 @@ async def get_write_lock(): @pytest.mark.asyncio -async def test_writers_deadlock(loop: asyncio.AbstractEventLoop) -> None: +async def test_writers_deadlock() -> None: + loop = asyncio.get_event_loop() rwlock = RWLock() rl = rwlock.reader wl = rwlock.writer @@ -111,7 +109,8 @@ async def coro() -> None: @pytest.mark.asyncio -async def test_readers_cancel(loop: asyncio.AbstractEventLoop) -> None: +async def test_readers_cancel() -> None: + loop = asyncio.get_event_loop() rwlock = RWLock() rl = rwlock.reader wl = rwlock.writer @@ -137,7 +136,7 @@ async def coro(lock: RWLock): @pytest.mark.asyncio -async def test_canceled_inside_acquire(loop: asyncio.AbstractEventLoop) -> None: +async def test_canceled_inside_acquire() -> None: rwlock = RWLock() rl = rwlock.reader @@ -158,7 +157,7 @@ async def coro(lock: RWLock): @pytest.mark.asyncio -async def test_race_multiple_writers(loop: asyncio.AbstractEventLoop) -> None: +async def test_race_multiple_writers() -> None: seq = [] async def write_wait(lock: RWLock): diff --git a/tests/test_rwlock.py b/tests/test_rwlock.py index 99dc46a..4fb0884 100644 --- a/tests/test_rwlock.py +++ b/tests/test_rwlock.py @@ -52,33 +52,38 @@ async def _wait(): @pytest.mark.asyncio -async def test_ctor_loop_reader(loop: asyncio.AbstractEventLoop) -> None: +async def test_ctor_loop_reader() -> None: + loop = asyncio.get_event_loop() rwlock = RWLock().reader_lock assert rwlock._lock._get_loop() is loop @pytest.mark.asyncio -async def test_ctor_noloop_reader(loop: asyncio.AbstractEventLoop) -> None: +async def test_ctor_noloop_reader() -> None: + loop = asyncio.get_event_loop() asyncio.set_event_loop(loop) rwlock = RWLock().reader_lock assert rwlock._lock._get_loop() is loop @pytest.mark.asyncio -async def test_ctor_loop_writer(loop: asyncio.AbstractEventLoop) -> None: +async def test_ctor_loop_writer() -> None: + loop = asyncio.get_event_loop() rwlock = RWLock().writer_lock assert rwlock._lock._get_loop() is loop @pytest.mark.asyncio -async def test_ctor_noloop_writer(loop: asyncio.AbstractEventLoop) -> None: +async def test_ctor_noloop_writer() -> None: + loop = asyncio.get_event_loop() asyncio.set_event_loop(loop) rwlock = RWLock().writer_lock assert rwlock._lock._get_loop() is loop @pytest.mark.asyncio -async def test_repr(loop: asyncio.AbstractEventLoop) -> None: +async def test_repr() -> None: + loop = asyncio.get_event_loop() rwlock = RWLock() assert "RWLock" in rwlock.__repr__() assert "WriterLock: [unlocked" in rwlock.__repr__() @@ -98,14 +103,15 @@ async def test_repr(loop: asyncio.AbstractEventLoop) -> None: @pytest.mark.asyncio -async def test_release_unlocked(loop: asyncio.AbstractEventLoop) -> None: +async def test_release_unlocked() -> None: + loop = asyncio.get_event_loop() rwlock = RWLock() with pytest.raises(RuntimeError): rwlock.reader_lock.release() @pytest.mark.asyncio -async def test_many_readers(loop: asyncio.AbstractEventLoop, fast_track: bool) -> None: +async def test_many_readers(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) N = 5 locked = [] @@ -127,7 +133,8 @@ async def f() -> None: @pytest.mark.asyncio -async def test_read_upgrade_write_release(loop: asyncio.AbstractEventLoop) -> None: +async def test_read_upgrade_write_release() -> None: + loop = asyncio.get_event_loop() rwlock = RWLock() await rwlock.writer_lock.acquire() await rwlock.reader_lock.acquire() @@ -151,9 +158,7 @@ async def test_read_upgrade_write_release(loop: asyncio.AbstractEventLoop) -> No @pytest.mark.asyncio -async def test_reader_recursion( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_reader_recursion(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) N = 5 locked = [] @@ -179,9 +184,7 @@ async def f(): @pytest.mark.asyncio -async def test_writer_recursion( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_writer_recursion(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) N = 5 locked = [] @@ -207,9 +210,7 @@ async def f(): @pytest.mark.asyncio -async def test_writer_then_reader_recursion( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_writer_then_reader_recursion(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) N = 5 locked = [] @@ -235,7 +236,8 @@ async def f(): @pytest.mark.asyncio -async def test_writer_recursion_fail(loop: asyncio.AbstractEventLoop) -> None: +async def test_writer_recursion_fail() -> None: + loop = asyncio.get_event_loop() rwlock = RWLock() N = 5 locked = [] @@ -254,9 +256,7 @@ async def f(): @pytest.mark.asyncio -async def test_readers_writers( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_readers_writers(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) N = 5 rlocked = [] @@ -309,7 +309,8 @@ async def w() -> None: @pytest.mark.asyncio -async def test_writer_success(loop: asyncio.AbstractEventLoop) -> None: +async def test_writer_success() -> None: + loop = asyncio.get_event_loop() # Verify that a writer can get access rwlock = RWLock() N = 5 @@ -355,7 +356,8 @@ async def w() -> None: @pytest.mark.asyncio -async def test_writer_success_fast(loop: asyncio.AbstractEventLoop) -> None: +async def test_writer_success_fast() -> None: + loop = asyncio.get_event_loop() # Verify that a writer can get access rwlock = RWLock(fast=True) N = 5 @@ -402,7 +404,8 @@ async def w() -> None: @pytest.mark.asyncio -async def test_writer_success_with_statement(loop: asyncio.AbstractEventLoop) -> None: +async def test_writer_success_with_statement() -> None: + loop = asyncio.get_event_loop() # Verify that a writer can get access rwlock = RWLock() N = 5 @@ -441,9 +444,7 @@ async def w() -> None: @pytest.mark.asyncio -async def test_raise_error_on_with_for_reader_lock( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_raise_error_on_with_for_reader_lock(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) with pytest.raises(RuntimeError): with rwlock.reader_lock: @@ -451,9 +452,7 @@ async def test_raise_error_on_with_for_reader_lock( @pytest.mark.asyncio -async def test_raise_error_on_with_for_writer_lock( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_raise_error_on_with_for_writer_lock(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) with pytest.raises(RuntimeError): with rwlock.writer_lock: @@ -461,7 +460,7 @@ async def test_raise_error_on_with_for_writer_lock( @pytest.mark.asyncio -async def test_read_locked(loop: asyncio.AbstractEventLoop, fast_track: bool) -> None: +async def test_read_locked(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) assert not rwlock.reader_lock.locked async with rwlock.reader_lock: @@ -469,7 +468,7 @@ async def test_read_locked(loop: asyncio.AbstractEventLoop, fast_track: bool) -> @pytest.mark.asyncio -async def test_write_locked(loop: asyncio.AbstractEventLoop, fast_track: bool) -> None: +async def test_write_locked(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) assert not rwlock.writer_lock.locked async with rwlock.writer_lock: @@ -477,9 +476,8 @@ async def test_write_locked(loop: asyncio.AbstractEventLoop, fast_track: bool) - @pytest.mark.asyncio -async def test_write_read_lock_multiple_tasks( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_write_read_lock_multiple_tasks(fast_track: bool) -> None: + loop = asyncio.get_event_loop() rwlock = RWLock(fast=fast_track) rl = rwlock.reader wl = rwlock.writer @@ -501,9 +499,7 @@ async def coro() -> None: @pytest.mark.asyncio -async def test_read_context_manager( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_read_context_manager(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) reader = rwlock.reader_lock assert not reader.locked @@ -512,9 +508,7 @@ async def test_read_context_manager( @pytest.mark.asyncio -async def test_write_context_manager( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_write_context_manager(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) writer = rwlock.writer_lock assert not writer.locked @@ -523,9 +517,7 @@ async def test_write_context_manager( @pytest.mark.asyncio -async def test_await_read_lock( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_await_read_lock(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) reader = rwlock.reader_lock assert not reader.locked @@ -534,9 +526,7 @@ async def test_await_read_lock( @pytest.mark.asyncio -async def test_await_write_lock( - loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +async def test_await_write_lock(fast_track: bool) -> None: rwlock = RWLock(fast=fast_track) writer = rwlock.writer_lock assert not writer.locked @@ -574,9 +564,7 @@ async def test_reader_ambiguous_loops(fast_track: bool) -> None: loop.close() -def test_created_outside_of_coroutine( - event_loop: asyncio.AbstractEventLoop, fast_track: bool -) -> None: +def test_created_outside_of_coroutine(fast_track: bool) -> None: async def main() -> None: async with lock.reader_lock: pass @@ -584,4 +572,4 @@ async def main() -> None: pass lock = RWLock(fast=fast_track) - event_loop.run_until_complete(main()) + asyncio.get_event_loop().run_until_complete(main())