Skip to content

Commit 635f031

Browse files
Test and typing maintenance (#145)
* suppress PyRight/MyPy inconsistency (closes #142) * type hint non-suppressing contexts (closes #143) * check Py3.13 (pre-release) * document classmethod Python version support
1 parent f9c80dd commit 635f031

File tree

8 files changed

+61
-34
lines changed

8 files changed

+61
-34
lines changed

Diff for: .github/workflows/unittests.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
matrix:
1414
python-version: [
15-
'3.8', '3.9', '3.10', '3.11', '3.12',
15+
'3.8', '3.9', '3.10', '3.11', '3.12', '3.13',
1616
'pypy-3.8', 'pypy-3.10'
1717
]
1818

@@ -22,6 +22,7 @@ jobs:
2222
uses: actions/setup-python@v5
2323
with:
2424
python-version: ${{ matrix.python-version }}
25+
allow-prereleases: true
2526
- name: Install dependencies
2627
run: |
2728
python -m pip install --upgrade pip

Diff for: asyncstdlib/asynctools.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,9 @@ async def __aenter__(self) -> AsyncIterator[T]:
110110
self._borrowed_iter = _ScopedAsyncIterator(self._iterator)
111111
return self._borrowed_iter
112112

113-
async def __aexit__(self, *args: Any) -> bool:
113+
async def __aexit__(self, *args: Any) -> None:
114114
await self._borrowed_iter._aclose_wrapper() # type: ignore
115115
await self._iterator.aclose() # type: ignore
116-
return False
117116

118117
def __repr__(self) -> str:
119118
return f"<{self.__class__.__name__} of {self._iterator!r} at 0x{(id(self)):x}>"

Diff for: asyncstdlib/contextlib.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,8 @@ def __init__(self, thing: AClose):
199199
async def __aenter__(self) -> AClose:
200200
return self.thing
201201

202-
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
202+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
203203
await self.thing.aclose()
204-
return False
205204

206205

207206
closing = Closing
@@ -239,8 +238,8 @@ def __init__(self, enter_result: T = None):
239238
async def __aenter__(self) -> T:
240239
return self.enter_result
241240

242-
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
243-
return False
241+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
242+
return None
244243

245244

246245
nullcontext = NullContext

Diff for: asyncstdlib/contextlib.pyi

+6-3
Original file line numberDiff line numberDiff line change
@@ -66,22 +66,25 @@ class closing(Generic[AClose]):
6666
exc_type: type[BaseException] | None,
6767
exc_val: BaseException | None,
6868
exc_tb: TracebackType | None,
69-
) -> bool: ...
69+
) -> None: ...
7070

7171
class nullcontext(AsyncContextManager[T]):
7272
enter_result: T
7373

7474
@overload
7575
def __init__(self: nullcontext[None], enter_result: None = ...) -> None: ...
7676
@overload
77-
def __init__(self: nullcontext[T], enter_result: T) -> None: ...
77+
def __init__(
78+
self: nullcontext[T], # pyright: ignore[reportInvalidTypeVarUse]
79+
enter_result: T,
80+
) -> None: ...
7881
async def __aenter__(self: nullcontext[T]) -> T: ...
7982
async def __aexit__(
8083
self,
8184
exc_type: type[BaseException] | None,
8285
exc_val: BaseException | None,
8386
exc_tb: TracebackType | None,
84-
) -> bool: ...
87+
) -> None: ...
8588

8689
SE = TypeVar(
8790
"SE",

Diff for: asyncstdlib/itertools.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,8 @@ class NoLock:
335335
async def __aenter__(self) -> None:
336336
pass
337337

338-
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
339-
return False
338+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
339+
return None
340340

341341

342342
async def tee_peer(
@@ -460,9 +460,8 @@ def __iter__(self) -> Iterator[AnyIterable[T]]:
460460
async def __aenter__(self) -> "Tee[T]":
461461
return self
462462

463-
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
463+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
464464
await self.aclose()
465-
return False
466465

467466
async def aclose(self) -> None:
468467
for child in self._children:

Diff for: asyncstdlib/itertools.pyi

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ class tee(Generic[T]):
132132
def __getitem__(self, item: slice) -> tuple[AsyncIterator[T], ...]: ...
133133
def __iter__(self) -> Iterator[AnyIterable[T]]: ...
134134
async def __aenter__(self: Self) -> Self: ...
135-
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: ...
135+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ...
136136
async def aclose(self) -> None: ...
137137

138138
def pairwise(iterable: AnyIterable[T]) -> AsyncIterator[tuple[T, T]]: ...

Diff for: docs/source/api/functools.rst

+3
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ the ``__wrapped__`` callable may be wrapped with a new cache of different size.
8686
.. versionchanged:: Python3.9
8787
:py:func:`classmethod` properly wraps caches.
8888

89+
.. versionchanged:: Python3.13
90+
:py:func:`classmethod` no longer wraps caches in a way that supports `cache_discard`.
91+
8992
.. versionadded:: 3.10.4
9093

9194
.. automethod:: cache_info() -> (hits=..., misses=..., maxsize=..., currsize=...)

Diff for: unittests/test_functools_lru.py

+42-19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Callable, Any
12
import sys
23

34
import pytest
@@ -7,8 +8,15 @@
78
from .utility import sync
89

910

10-
def method_counter(size):
11+
class Counter:
12+
kind: object
13+
count: Any
14+
15+
16+
def method_counter(size: "int | None") -> "type[Counter]":
1117
class Counter:
18+
kind = None
19+
1220
def __init__(self):
1321
self._count = 0
1422

@@ -20,9 +28,10 @@ async def count(self):
2028
return Counter
2129

2230

23-
def classmethod_counter(size):
31+
def classmethod_counter(size: "int | None") -> "type[Counter]":
2432
class Counter:
2533
_count = 0
34+
kind = classmethod
2635

2736
def __init__(self):
2837
type(self)._count = 0
@@ -36,32 +45,40 @@ async def count(cls):
3645
return Counter
3746

3847

39-
def staticmethod_counter(size):
48+
def staticmethod_counter(size: "int | None") -> "type[Counter]":
4049
# I'm sorry for writing this test – please don't do this at home!
41-
_count = 0
50+
count: int = 0
4251

4352
class Counter:
53+
kind = staticmethod
54+
4455
def __init__(self):
45-
nonlocal _count
46-
_count = 0
56+
nonlocal count
57+
count = 0
4758

4859
@staticmethod
4960
@a.lru_cache(maxsize=size)
5061
async def count():
51-
nonlocal _count
52-
_count += 1
53-
return _count
62+
nonlocal count
63+
count += 1
64+
return count
5465

5566
return Counter
5667

5768

58-
counter_factories = [method_counter, classmethod_counter, staticmethod_counter]
69+
counter_factories: "list[Callable[[int | None], type[Counter]]]" = [
70+
method_counter,
71+
classmethod_counter,
72+
staticmethod_counter,
73+
]
5974

6075

6176
@pytest.mark.parametrize("size", [0, 3, 10, None])
6277
@pytest.mark.parametrize("counter_factory", counter_factories)
6378
@sync
64-
async def test_method_plain(size, counter_factory):
79+
async def test_method_plain(
80+
size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]"
81+
):
6582
"""Test caching without resetting"""
6683

6784
counter_type = counter_factory(size)
@@ -76,7 +93,9 @@ async def test_method_plain(size, counter_factory):
7693
@pytest.mark.parametrize("size", [0, 3, 10, None])
7794
@pytest.mark.parametrize("counter_factory", counter_factories)
7895
@sync
79-
async def test_method_clear(size, counter_factory):
96+
async def test_method_clear(
97+
size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]"
98+
):
8099
"""Test caching with resetting everything"""
81100
counter_type = counter_factory(size)
82101
for _instance in range(4):
@@ -91,14 +110,16 @@ async def test_method_clear(size, counter_factory):
91110
@pytest.mark.parametrize("size", [0, 3, 10, None])
92111
@pytest.mark.parametrize("counter_factory", counter_factories)
93112
@sync
94-
async def test_method_discard(size, counter_factory):
113+
async def test_method_discard(
114+
size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]"
115+
):
95116
"""Test caching with resetting specific item"""
96117
counter_type = counter_factory(size)
97-
if (
98-
sys.version_info < (3, 9)
99-
and type(counter_type.__dict__["count"]) is classmethod
118+
if not (
119+
(3, 9) <= sys.version_info[:2] <= (3, 12)
120+
or counter_type.kind is not classmethod
100121
):
101-
pytest.skip("classmethod does not respect descriptors up to 3.8")
122+
pytest.skip("classmethod only respects descriptors between 3.9 and 3.12")
102123
for _instance in range(4):
103124
instance = counter_type()
104125
for reset in range(5):
@@ -111,7 +132,9 @@ async def test_method_discard(size, counter_factory):
111132
@pytest.mark.parametrize("size", [0, 3, 10, None])
112133
@pytest.mark.parametrize("counter_factory", counter_factories)
113134
@sync
114-
async def test_method_metadata(size, counter_factory):
135+
async def test_method_metadata(
136+
size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]"
137+
):
115138
"""Test cache metadata on methods"""
116139
tp = counter_factory(size)
117140
for instance in range(4):
@@ -133,7 +156,7 @@ async def test_method_metadata(size, counter_factory):
133156

134157

135158
@pytest.mark.parametrize("size", [None, 0, 10, 128])
136-
def test_wrapper_attributes(size):
159+
def test_wrapper_attributes(size: "int | None"):
137160
class Bar:
138161
@a.lru_cache
139162
async def method(self, int_arg: int):

0 commit comments

Comments
 (0)