🐛 fix(asyncio): add __exit__ to BaseAsyncFileLock and fix __del__ loop handling#518
Conversation
gaborbernat
left a comment
There was a problem hiding this comment.
Please add tests that would catch a regression of this.
|
Added regression tests in Bug 1 — missing
Bug 2 — stored event loop in
Both groups run for |
…ed loop __del__)
10 parametrised tests covering both bugs fixed in this PR:
Bug 1 — missing __exit__ on BaseAsyncFileLock:
- test_sync_with_raises_not_implemented_error_not_attribute_error
Uses `with` (sync) on an async lock; must raise NotImplementedError,
not AttributeError (which happened when __exit__ was absent).
- test_exit_method_exists_on_async_lock
Asserts that __exit__ is defined and callable on the class.
- test_exit_raises_not_implemented_error
Calls __exit__ directly; must raise NotImplementedError.
Bug 2 — stored event loop in __del__:
- test_del_after_loop_close_does_not_raise
Acquires/releases in a dedicated thread with its own loop, closes
the loop, then calls __del__ — must not raise RuntimeError.
- test_gc_after_loop_close_does_not_raise
Same scenario via gc.collect() to simulate real GC behaviour.
Both test groups run for AsyncFileLock and AsyncSoftFileLock.
Uses threading to isolate the test loop from pytest-asyncio's loop.
338 upstream tests pass.
|
CI failing you need to fix the linter too. |
|
Thanks for the feedback @gaborbernat — the CI failure and the review are valid. Root cause of the type failure The original fix added Fix applied Added an explicit def __exit__(self, exc_type, exc_value, traceback) -> NoReturn:
msg = "Do not use `with` for asyncio locks, use `async with` instead."
raise NotImplementedError(msg)Tests The regression suite ( Pushing the corrected |
|
Hey, just wanted to let you know that the CI is still failing. |
…ed loop __del__)
10 parametrised tests covering both bugs fixed in this PR:
Bug 1 — missing __exit__ on BaseAsyncFileLock:
- test_sync_with_raises_not_implemented_error_not_attribute_error
Uses `with` (sync) on an async lock; must raise NotImplementedError,
not AttributeError (which happened when __exit__ was absent).
- test_exit_method_exists_on_async_lock
Asserts that __exit__ is defined and callable on the class.
- test_exit_raises_not_implemented_error
Calls __exit__ directly; must raise NotImplementedError.
Bug 2 — stored event loop in __del__:
- test_del_after_loop_close_does_not_raise
Acquires/releases in a dedicated thread with its own loop, closes
the loop, then calls __del__ — must not raise RuntimeError.
- test_gc_after_loop_close_does_not_raise
Same scenario via gc.collect() to simulate real GC behaviour.
Both test groups run for AsyncFileLock and AsyncSoftFileLock.
Uses threading to isolate the test loop from pytest-asyncio's loop.
338 upstream tests pass.
for more information, see https://pre-commit.ci
The pre-commit autoupdate bumped ty to 0.0.29, which strictly validates error codes in suppress comments. The old mypy-style `# type: ignore[assignment, misc]` comments no longer suppressed `invalid-assignment` diagnostics, breaking the type check CI job. Updated `__init__.py` fallbacks to use `# ty: ignore[invalid-assignment]` and restructured `conftest.py` to import inside the function body, eliminating the optional module-level binding entirely. Removed the stale `# type: ignore[override]` from `BaseAsyncFileLock.__exit__` that ty was silently ignoring anyway. Replaced the thread-per-test pattern in the __del__ regression tests with `ThreadPoolExecutor`, which propagates exceptions back to the calling thread automatically — removing dead `except RuntimeError` branches that could never execute after the fix. Added explicit tests for `pytest_sessionfinish` so the conftest restructure is covered.
The old docstrings just restated the error message and added a redundant all-caps NOTE. The actual reason — that acquisition and release are coroutines and can't be awaited inside a sync context manager — was never explained.
…elock Dedicated regression test files accumulate and fragment the test suite. The two cases — sync context manager rejection and __del__ safety after loop close — belong alongside the other async lock behavior tests.
Using
withon an async lock raisedAttributeErrorinstead of a clearNotImplementedError, because__exit__was missing — Python calls it to clean up after__enter__raises, but had nothing to call. Separately,__del__calledasyncio.get_running_loop()unconditionally, which raisesRuntimeErrorduring garbage collection when no event loop is active.__exit__is added toBaseAsyncFileLockand raisesNotImplementedErrorwith a message explaining that acquire and release are coroutines and cannot be awaited inside a sync context manager. 🐛 The__del__implementation now catches theRuntimeErrorfromget_running_loop()and falls back to a stored loop, avoiding the crash during GC after a loop has been closed.The error messages in both
__enter__and__exit__explain why sync context managers are rejected, not just what to do instead. ✨ The__del__docstring is similarly trimmed to state the intent rather than describe the implementation.