Skip to content

Commit da74f91

Browse files
committed
prepare for 3.14
1 parent e44cf85 commit da74f91

File tree

6 files changed

+48
-48
lines changed

6 files changed

+48
-48
lines changed

src/async_utils/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
__author__ = "Michael Hall"
1010
__license__ = "Apache-2.0"
1111
__copyright__ = "Copyright 2020-Present Michael Hall"
12-
__version__ = "2025.07.15b"
12+
__version__ = "2025.08.03b"
1313

1414
import os
1515
import sys
@@ -19,8 +19,8 @@
1919
# - Check use of concurrent.futures.Future before extending this version.
2020
# - update `_misc._ensure_annotations.py` before extending this version.
2121
# - ensure `task_cache.__WrappedSignature` still works
22-
if (_vi.major, _vi.minor) > (3, 13):
23-
msg: str = """This library is not tested for use on python versions above 3.13
22+
if (_vi.major, _vi.minor) > (3, 14):
23+
msg: str = """This library is not tested for use on python versions above 3.14
2424
This library relies on a few internal details that are not safe to rely upon
2525
without checking this consistently.
2626
"""

src/async_utils/_qs.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,19 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
# With thanks to everyone I've ever discussed concurrency or datastructures with
16-
1715
from __future__ import annotations
1816

1917
import asyncio
18+
import sys
2019
import threading
2120
from collections import deque
2221
from collections.abc import Generator
23-
24-
# PYUPDATE: 3.14 release + 3.14 minimum: reaudit
25-
# heapq methods are not threadsafe pre 3.14
26-
# see: GH: cpython 135036
2722
from heapq import heappop, heappush
2823

2924
from . import _typings as t
3025

26+
THREAD_SAFE_HEAPQ = sys.version_info[:2] >= (3, 14)
27+
3128
TYPE_CHECKING = False
3229
if TYPE_CHECKING:
3330
import typing
@@ -604,30 +601,43 @@ def _get(self, /) -> T:
604601
class PriorityQueue[T: HeapqOrderable](BaseQueue[T]):
605602
"""A thread-safe queue with both sync and async access methods."""
606603

607-
__slots__ = ("_data", "_lock")
604+
__slots__ = ("_data",) if THREAD_SAFE_HEAPQ else ("_data", "_lock")
608605

609606
def __init_subclass__(cls) -> t.Never:
610607
msg = "Don't subclass this"
611608
raise RuntimeError(msg)
612609

613610
__final__ = True
614611

615-
def __init__(self, /, maxsize: int | None = None) -> None:
616-
super().__init__(maxsize)
617-
self._data: list[T] = []
618-
# heapq not threadsafe till 3.14
619-
self._lock = threading.RLock()
612+
if THREAD_SAFE_HEAPQ:
613+
614+
def __init__(self, /, maxsize: int | None = None) -> None:
615+
super().__init__(maxsize)
616+
self._data: list[T] = []
617+
618+
def _put(self, item: T, /) -> None:
619+
heappush(self._data, item)
620+
621+
def _get(self, /) -> T:
622+
return heappop(self._data)
623+
624+
else:
625+
626+
def __init__(self, /, maxsize: int | None = None) -> None:
627+
super().__init__(maxsize)
628+
self._data: list[T] = []
629+
self._lock = threading.RLock()
630+
631+
def _put(self, item: T, /) -> None:
632+
with self._lock:
633+
heappush(self._data, item)
634+
635+
def _get(self, /) -> T:
636+
with self._lock:
637+
return heappop(self._data)
620638

621639
def _qsize(self, /) -> int:
622640
return len(self._data)
623641

624642
def _items(self, /) -> list[T]:
625643
return self._data.copy()
626-
627-
def _put(self, item: T, /) -> None:
628-
with self._lock:
629-
heappush(self._data, item)
630-
631-
def _get(self, /) -> T:
632-
with self._lock:
633-
return heappop(self._data)

src/async_utils/lockout.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@
1616

1717
import asyncio
1818
import concurrent.futures as cf
19-
20-
# PYUPDATE: 3.14 release + 3.14 minimum: reaudit
21-
# heapq methods are not threadsafe pre 3.14
22-
# see: GH: cpython 135036
2319
import heapq
2420
import threading
2521
import time
@@ -93,7 +89,6 @@ async def __aenter__(self) -> None:
9389
now = time.monotonic()
9490
# There must not be an async context switch between here
9591
# and replacing the lockout when lockout is in the future
96-
# PYUPDATE: The lock here can be removed at 3.14 minimum
9792
with self._internal_lock:
9893
ts = heapq.heappop(self._lockouts)
9994
if (sleep_for := ts - now) > 0:

src/async_utils/lru.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
# see: GH: cpython 135036
2222
import heapq
2323
import math
24+
import sys
2425
import threading
2526
import time
2627

2728
from . import _typings as t
2829

30+
THREAD_SAFE_HEAPQ = sys.version_info[:2] >= (3, 14)
31+
2932
__all__ = ("LRU", "TTLLRU")
3033

3134

@@ -260,20 +263,19 @@ def __getitem__(self, key: K, /) -> V:
260263
now = time.monotonic()
261264
if now > ts:
262265
raise KeyError
263-
self._cache[key] = ts, val
264266
return val
265267

266268
def __setitem__(self, key: K, value: V, /) -> None:
267269
ts = time.monotonic() + self._ttl
268270
with self._internal_lock:
269271
heapq.heappush(self._expirations, (ts, key))
270-
self._cache[key] = (ts, value)
271-
self._remove_some_expired()
272-
if len(self._cache) > self._maxsize:
273-
try:
274-
self._cache.pop(next(iter(self._cache)))
275-
except (KeyError, StopIteration):
276-
pass
272+
self._cache[key] = (ts, value)
273+
self._remove_some_expired()
274+
if len(self._cache) > self._maxsize:
275+
try:
276+
self._cache.pop(next(iter(self._cache)))
277+
except (KeyError, StopIteration):
278+
pass
277279

278280
def get[T](self, key: K, default: T, /) -> V | T:
279281
"""Get a value by key or default value.
@@ -309,5 +311,6 @@ def remove(self, key: K, /) -> None:
309311
key:
310312
The key to remove.
311313
"""
312-
self._remove_some_expired()
313-
self._cache.pop(key, None)
314+
with self._internal_lock:
315+
self._remove_some_expired()
316+
self._cache.pop(key, None)

src/async_utils/priority_sem.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@
1818
import asyncio
1919
import concurrent.futures as cf
2020
import contextvars
21-
22-
# PYUPDATE: 3.14 release + 3.14 minimum: reaudit
23-
# heapq methods are not threadsafe pre 3.14
24-
# see: GH: cpython 135036
2521
import heapq
2622
import threading
2723
import time
@@ -123,13 +119,9 @@ def __init__(self, value: int = 1) -> None:
123119
raise ValueError(msg)
124120
self._waiters: list[_PriorityWaiter] | None = None
125121
self._value: int = value
126-
# PYUPDATE: 3.14 minimum heapq safety
127122
self._internal_lock: threading.RLock = threading.RLock()
128123

129124
def __locked(self) -> bool:
130-
# Must do a comparison based on priority then FIFO
131-
# in the case of existing waiters
132-
# not guaranteed to be immediately available
133125
with self._internal_lock:
134126
return self._value == 0 or (
135127
any(not w.cancelled() for w in (self._waiters or ()))

src/async_utils/task_cache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def _chain_fut[R](c_fut: cf.Future[R], a_fut: asyncio.Future[R]) -> None:
7777
class _WrappedSignature[**P, R]:
7878
#: PYUPGRADE: Ensure inspect.signature still accepts this
7979
# as func.__signature__
80-
# Known working: py 3.12.0 - py3.14b4 range inclusive
80+
# Known working: py 3.12.0 - py3.14rc1 range inclusive
8181
def __init__(self, f: TaskCoroFunc[P, R], w: TaskFunc[P, R]) -> None:
8282
self._f: Callable[..., t.Any] = f # anotation needed for inspect use below....
8383
self._w = w
@@ -193,7 +193,7 @@ def wrapped(*args: P.args, **kwargs: P.kwargs) -> asyncio.Task[R]:
193193

194194
return a_fut
195195

196-
# PYUPGRADE: 3.14.0 recheck (last checked 3.14b4), 3.15+
196+
# PYUPGRADE: 3.14.0 recheck (last checked 3.14rc1), 3.15+
197197
wrapped.__signature__ = _WrappedSignature(coro, wrapped) # type: ignore[attr-defined]
198198

199199
return wrapped

0 commit comments

Comments
 (0)