Repository: aio-libs/aiorwlock

Issue:
Errors caused by no running event loop
I've been using the library for a while in various projects, and after recent python updates I'm now getting the same error in all of them:
`"env/lib/python3.10/site-packages/aiorwlock/__init__.py", line 242, in __init__
    loop = asyncio.get_running_loop()
RuntimeError: no running event loop
`

I tested it on the example in the README of this repo and it gives a deprecation warning of the same thing:
`python main.py 
main.py:19: DeprecationWarning: There is no current event loop
`

I'm using python v3.10.7, and aiorwlock v1.3.0 straight from pip

Thanks!

Discussion:
rijenkii: Ran CI locally, force-pushed a new version that passes all actions.
rijenkii: Added unit tests that cover a lock that is bound to a different loop from the current one.

Patch:
diff --git a/aiorwlock/__init__.py b/aiorwlock/__init__.py
index 396853c..a091860 100644
--- a/aiorwlock/__init__.py
+++ b/aiorwlock/__init__.py
@@ -1,5 +1,5 @@
 import asyncio
-import warnings
+import threading
 from collections import deque
 from typing import Any, Deque, List, Optional, Tuple
 
@@ -17,15 +17,17 @@ __all__ = ('RWLock',)
 # implementation based on:
 # http://bugs.python.org/issue8800
 
+_global_lock = threading.Lock()
+
 
 # The internal lock object managing the RWLock state.
 class _RWLockCore:
     _RL = 1
     _WL = 2
+    _loop = None
 
-    def __init__(self, fast: bool, loop: Loop):
+    def __init__(self, fast: bool):
         self._do_yield = not fast
-        self._loop: Loop = loop
         self._read_waiters: Deque[Future[None]] = deque()
         self._write_waiters: Deque[Future[None]] = deque()
         self._r_state: int = 0
@@ -33,6 +35,21 @@ class _RWLockCore:
         # tasks will be few, so a list is not inefficient
         self._owning: List[Tuple[Task[Any], int]] = []
 
+    def _get_loop(self) -> Loop:
+        """
+        From: https://github.com/python/cpython/blob/3.12/Lib/asyncio/mixins.py
+        """
+
+        loop = asyncio.get_event_loop()
+
+        if self._loop is None:
+            with _global_lock:
+                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')
+        return loop
+
     @property
     def read_locked(self) -> bool:
         return self._r_state > 0
@@ -71,7 +88,7 @@ class _RWLockCore:
             await self._yield_after_acquire(self._RL)
             return True
 
-        fut = self._loop.create_future()
+        fut = self._get_loop().create_future()
         self._read_waiters.append(fut)
         try:
             await fut
@@ -107,7 +124,7 @@ class _RWLockCore:
             await self._yield_after_acquire(self._WL)
             return True
 
-        fut = self._loop.create_future()
+        fut = self._get_loop().create_future()
         self._write_waiters.append(fut)
         try:
             await fut
@@ -130,7 +147,7 @@ class _RWLockCore:
 
     def _release(self, lock_type: int) -> None:
         # assert lock_type in (self._RL, self._WL)
-        me = asyncio.current_task(loop=self._loop)
+        me = asyncio.current_task(loop=self._get_loop())
         assert me is not None  # nosec
 
         try:
@@ -240,17 +257,7 @@ class RWLock:
     core = _RWLockCore
 
     def __init__(self, *, fast: bool = False) -> None:
-        loop = asyncio.get_running_loop()
-        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)
+        core = self.core(fast)
         self._reader_lock = _ReaderLock(core)
         self._writer_lock = _WriterLock(core)
 
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 251d2cd..46e009e 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,12 +1,12 @@
 -e .
-bandit==1.7.5; implementation_name=="cpython"
-black==23.11.0; implementation_name=="cpython"
-flake8-bugbear==23.11.28; implementation_name=="cpython"
+bandit==1.7.6; implementation_name=="cpython"
+black==23.12.1; implementation_name=="cpython"
+flake8-bugbear==23.12.2; implementation_name=="cpython"
 flake8-quotes==3.3.2; implementation_name=="cpython"
 flake8==6.1.0; implementation_name=="cpython"
-isort==5.12.0; implementation_name=="cpython"
-mypy==1.7.1; implementation_name=="cpython"
+isort==5.13.2; implementation_name=="cpython"
+mypy==1.8.0; implementation_name=="cpython"
 pyroma==4.2; implementation_name=="cpython"
-pytest-asyncio==0.22.0
+pytest-asyncio==0.23.2
 pytest-cov==4.1.0
 pytest==7.4.3


Test patch:
diff --git a/tests/test_rwlock.py b/tests/test_rwlock.py
index 1657be5..56bf528 100644
--- a/tests/test_rwlock.py
+++ b/tests/test_rwlock.py
@@ -51,27 +51,27 @@ async def _wait():
 @pytest.mark.asyncio
 async def test_ctor_loop_reader(loop):
     rwlock = RWLock().reader_lock
-    assert rwlock._lock._loop is loop
+    assert rwlock._lock._get_loop() is 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
+    assert rwlock._lock._get_loop() is loop
 
 
 @pytest.mark.asyncio
 async def test_ctor_loop_writer(loop):
     rwlock = RWLock().writer_lock
-    assert rwlock._lock._loop is loop
+    assert rwlock._lock._get_loop() is 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
+    assert rwlock._lock._get_loop() is loop
 
 
 @pytest.mark.asyncio
@@ -517,3 +517,48 @@ async def test_await_write_lock(loop, fast_track):
     assert not writer.locked
     async with writer:
         assert writer.locked
+
+
+@pytest.mark.asyncio
+async def test_writer_ambiguous_loops(fast_track):
+    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'
+        ):
+            async with lock.writer_lock:
+                pass
+    finally:
+        loop.close()
+
+
+@pytest.mark.asyncio
+async def test_reader_ambiguous_loops(fast_track):
+    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'
+        ):
+            async with lock.reader_lock:
+                pass
+    finally:
+        loop.close()
+
+
+def test_created_outside_of_coroutine(event_loop, fast_track):
+    async def main():
+        async with lock.reader_lock:
+            pass
+        async with lock.writer_lock:
+            pass
+
+    lock = RWLock(fast=fast_track)
+    event_loop.run_until_complete(main())