Skip to content

Commit 08d260e

Browse files
tanbroclaude
andcommitted
feat(lock): add aclose() method for async locks, deprecate close()
- Add BaseAsyncSadLock.aclose() for compatibility with contextlib.aclosing (Python 3.10+) - Deprecate BaseAsyncSadLock.close() in favor of aclose(), will be removed in v0.9.0 - Fix __aexit__ to call aclose() instead of deprecated close() - Add test cases for aclose() and deprecation warning Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 16c9d1f commit 08d260e

3 files changed

Lines changed: 130 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# CHANGELOG
22

3+
## v0.8.1 (in development)
4+
5+
- 🆕 **New Features:**
6+
- **Added `BaseAsyncSadLock.aclose()` method**: Async close method for compatibility with `contextlib.aclosing` (Python 3.10+)
7+
8+
- ⚠️ **Deprecations:**
9+
- **`BaseAsyncSadLock.close()` is deprecated**: It will be removed in v0.9.0, use `aclose()` instead
10+
311
## v0.8.0
412

513
> 📅 **Date** 2026-02-12
@@ -59,12 +67,12 @@
5967
6068
- 🐛 Bug-fix:
6169
- Issue #4: PostgreSQL xact lock in context manager produces warning #4
62-
- ✅ Changes:
63-
- `typing-extensions` required for Python earlier than 3.12
64-
- 🖊️ Modifications:
65-
- Add some `override` decorators
66-
- 🎯 CI:
67-
- update pre-commit hooks
70+
- ✅ Changes:
71+
- `typing-extensions` required for Python earlier than 3.12
72+
- 🖊️ Modifications:
73+
- Add some `override` decorators
74+
- 🎯 CI:
75+
- update pre-commit hooks
6876

6977
## v0.6.1
7078

@@ -98,7 +106,7 @@
98106
Date: 2023-12-06
99107

100108
- New:
101-
- `contextual_timeout` parameter for with statement
109+
- `contextual_timeout` parameter for "with" statement
102110
- Support Python 3.12
103111

104112
## v0.4
@@ -109,15 +117,15 @@ Date: 2023-06-17
109117
- remove `acquired` property, it's alias of `locked`
110118
- remove setter of `locked` property
111119

112-
- Optimize:
113-
- re-arrange package's structure
114-
- Many optimizations
120+
- Optimize:
121+
- re-arrange package's structure
122+
- Many optimizations
115123

116-
- CI/Test:
117-
- GitHub action: Python 3.8~3.11 x SQLAlchemy 1.x/2.x matrix testing
118-
- Local compose: Python 3.7~3.11 x SQLAlchemy 1.x/2.x matrix testing
124+
- CI/Test:
125+
- GitHub action: Python 3.8~3.11 x SQLAlchemy 1.x/2.x matrix testing
126+
- Local compose: Python 3.7~3.11 x SQLAlchemy 1.x/2.x matrix testing
119127

120-
- Doc: Update to Sphinx 7.x, and Furo theme
128+
- Doc: Update to Sphinx 7.x, and Furo theme
121129

122130
## v0.3.1
123131

@@ -132,14 +140,14 @@ Date: 2023-06-13
132140
- Remove:
133141
- Python 3.6 support
134142

135-
- Tests:
136-
- New docker compose based tests, from python 3.7 to 3.11, both SQLAlchemy 1.x and 2.x
143+
- Tests:
144+
- New docker compose based tests, from python 3.7 to 3.11, both SQLAlchemy 1.x and 2.x
137145

138-
- Docs:
139-
- Update to newer Sphinx docs
146+
- Docs:
147+
- Update to newer Sphinx docs
140148

141-
- Build:
142-
- Move all project meta to pyproject.toml, remove setup.cfg and setup.py
149+
- Build:
150+
- Move all project meta to pyproject.toml, remove setup.cfg and setup.py
143151

144152
## v0.2.1
145153

@@ -181,10 +189,10 @@ Date: 2021-03-14
181189
- Remove SQLAlchemy version requires earlier than 1.4 in setup, it's not supported, actually.
182190
- Adjust PostgreSQL lock's constructor arguments order
183191

184-
- Add:
192+
- Add:
185193

186-
- More test cases, and add test/deploy workflow in GitHub actions.
187-
- Add docker-compose test scripts
194+
- More test cases, and add test/deploy workflow in GitHub actions.
195+
- Add docker-compose test scripts
188196

189197
## v0.2a2
190198

src/sqlalchemy_dlock/lock/base.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
import warnings
23
from abc import ABC, abstractmethod
34
from threading import local
45
from typing import Callable, Generic, Optional, TypeVar, Union, final
@@ -270,7 +271,7 @@ async def __aenter__(self) -> Self:
270271

271272
@final
272273
async def __aexit__(self, exc_type, exc_value, exc_tb):
273-
await self.close()
274+
await self.aclose()
274275

275276
@final
276277
def __str__(self):
@@ -319,6 +320,38 @@ async def do_release(self, *args, **kwargs):
319320
raise NotImplementedError()
320321

321322
@final
322-
async def close(self, *args, **kwargs):
323+
async def aclose(self, *args, **kwargs):
324+
"""Async close method for compatibility with :func:`contextlib.aclosing` (Python 3.10+).
325+
326+
Example::
327+
328+
from contextlib import aclosing
329+
330+
async with aclosing(create_async_sadlock(some_connection, some_key)) as lock:
331+
# will **NOT** acquire at the begin of with-block
332+
assert not lock.locked
333+
# ...
334+
# lock when need
335+
await lock.acquire()
336+
assert lock.locked
337+
# ...
338+
339+
# `aclose` will be called at the end with-block
340+
assert not lock.locked
341+
342+
.. versionadded:: 0.8.1
343+
"""
323344
if self._acquired:
324345
await self.release(*args, **kwargs)
346+
347+
@final
348+
async def close(self, *args, **kwargs):
349+
""".. deprecated:: 0.8.1
350+
Use :meth:`aclose` instead. Will be removed in 0.9.0.
351+
"""
352+
warnings.warn(
353+
"The 'close' method is deprecated and will be removed in 0.9.0. Use 'aclose' instead.",
354+
DeprecationWarning,
355+
stacklevel=2,
356+
)
357+
return await self.aclose(*args, **kwargs)

tests/asyncio/test_basic.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import sys
12
from contextlib import AsyncExitStack
23
from multiprocessing import cpu_count
34
from random import randint
45
from secrets import token_bytes, token_hex
5-
from unittest import IsolatedAsyncioTestCase
6+
from unittest import IsolatedAsyncioTestCase, skipIf
67
from uuid import uuid4
8+
import warnings
79

810
from sqlalchemy_dlock import create_async_sadlock
911

@@ -220,3 +222,64 @@ async def test_release_unlocked_error(self):
220222
lock1 = create_async_sadlock(conn1, key)
221223
with self.assertRaisesRegex(ValueError, "invoked on an unlocked lock"):
222224
await lock1.release()
225+
226+
async def test_aclose(self):
227+
"""Test aclose() method."""
228+
for engine in get_engines():
229+
async with engine.connect() as conn:
230+
key = uuid4().hex
231+
lock = create_async_sadlock(conn, key)
232+
self.assertFalse(lock.locked)
233+
await lock.acquire()
234+
self.assertTrue(lock.locked)
235+
await lock.aclose()
236+
self.assertFalse(lock.locked)
237+
238+
async def test_aclose_when_unlocked(self):
239+
"""Test aclose() when lock is not acquired (should be no-op)."""
240+
for engine in get_engines():
241+
async with engine.connect() as conn:
242+
key = uuid4().hex
243+
lock = create_async_sadlock(conn, key)
244+
self.assertFalse(lock.locked)
245+
# aclose on unlocked lock should be no-op
246+
await lock.aclose()
247+
self.assertFalse(lock.locked)
248+
249+
@skipIf(sys.version_info < (3, 10), "contextlib.aclosing: New in version 3.10")
250+
async def test_aclosing_context_manager(self):
251+
"""Test aclose() with contextlib.aclosing (Python 3.10+)."""
252+
try:
253+
from contextlib import aclosing
254+
except ImportError:
255+
self.skipTest("contextlib.aclosing not available")
256+
257+
for engine in get_engines():
258+
async with engine.connect() as conn:
259+
key = uuid4().hex
260+
async with aclosing(create_async_sadlock(conn, key)) as lock:
261+
# will NOT acquire at the begin of with-block
262+
self.assertFalse(lock.locked)
263+
# lock when need
264+
await lock.acquire()
265+
self.assertTrue(lock.locked)
266+
# aclose will be called at the end with-block
267+
self.assertFalse(lock.locked)
268+
269+
async def test_close_deprecated(self):
270+
"""Test that close() is deprecated and calls aclose()."""
271+
for engine in get_engines():
272+
async with engine.connect() as conn:
273+
key = uuid4().hex
274+
lock = create_async_sadlock(conn, key)
275+
await lock.acquire()
276+
self.assertTrue(lock.locked)
277+
with warnings.catch_warnings(record=True) as w:
278+
warnings.simplefilter("always")
279+
await lock.close()
280+
# Check that deprecation warning was raised
281+
self.assertEqual(len(w), 1)
282+
self.assertEqual(w[0].category, DeprecationWarning)
283+
self.assertIn("deprecated", str(w[0].message).lower())
284+
self.assertIn("aclose", str(w[0].message).lower())
285+
self.assertFalse(lock.locked)

0 commit comments

Comments
 (0)