diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index a88b715..f5eb0f4 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: python-version: [ - '3.8', '3.9', '3.10', '3.11', '3.12', + '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.8', 'pypy-3.10' ] @@ -22,6 +22,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/asyncstdlib/asynctools.py b/asyncstdlib/asynctools.py index 5de673e..c0b2daf 100644 --- a/asyncstdlib/asynctools.py +++ b/asyncstdlib/asynctools.py @@ -110,10 +110,9 @@ async def __aenter__(self) -> AsyncIterator[T]: self._borrowed_iter = _ScopedAsyncIterator(self._iterator) return self._borrowed_iter - async def __aexit__(self, *args: Any) -> bool: + async def __aexit__(self, *args: Any) -> None: await self._borrowed_iter._aclose_wrapper() # type: ignore await self._iterator.aclose() # type: ignore - return False def __repr__(self) -> str: return f"<{self.__class__.__name__} of {self._iterator!r} at 0x{(id(self)):x}>" diff --git a/asyncstdlib/contextlib.py b/asyncstdlib/contextlib.py index 0b925a4..bb2c0b9 100644 --- a/asyncstdlib/contextlib.py +++ b/asyncstdlib/contextlib.py @@ -199,9 +199,8 @@ def __init__(self, thing: AClose): async def __aenter__(self) -> AClose: return self.thing - async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: await self.thing.aclose() - return False closing = Closing @@ -239,8 +238,8 @@ def __init__(self, enter_result: T = None): async def __aenter__(self) -> T: return self.enter_result - async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: - return False + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + return None nullcontext = NullContext diff --git a/asyncstdlib/contextlib.pyi b/asyncstdlib/contextlib.pyi index eea0d45..e6e683b 100644 --- a/asyncstdlib/contextlib.pyi +++ b/asyncstdlib/contextlib.pyi @@ -66,7 +66,7 @@ class closing(Generic[AClose]): exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool: ... + ) -> None: ... class nullcontext(AsyncContextManager[T]): enter_result: T @@ -74,14 +74,17 @@ class nullcontext(AsyncContextManager[T]): @overload def __init__(self: nullcontext[None], enter_result: None = ...) -> None: ... @overload - def __init__(self: nullcontext[T], enter_result: T) -> None: ... + def __init__( + self: nullcontext[T], # pyright: ignore[reportInvalidTypeVarUse] + enter_result: T, + ) -> None: ... async def __aenter__(self: nullcontext[T]) -> T: ... async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool: ... + ) -> None: ... SE = TypeVar( "SE", diff --git a/asyncstdlib/itertools.py b/asyncstdlib/itertools.py index 3e47be7..eb29a26 100644 --- a/asyncstdlib/itertools.py +++ b/asyncstdlib/itertools.py @@ -335,8 +335,8 @@ class NoLock: async def __aenter__(self) -> None: pass - async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: - return False + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + return None async def tee_peer( @@ -460,9 +460,8 @@ def __iter__(self) -> Iterator[AnyIterable[T]]: async def __aenter__(self) -> "Tee[T]": return self - async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: await self.aclose() - return False async def aclose(self) -> None: for child in self._children: diff --git a/asyncstdlib/itertools.pyi b/asyncstdlib/itertools.pyi index 0ce1d91..d97a266 100644 --- a/asyncstdlib/itertools.pyi +++ b/asyncstdlib/itertools.pyi @@ -132,7 +132,7 @@ class tee(Generic[T]): def __getitem__(self, item: slice) -> tuple[AsyncIterator[T], ...]: ... def __iter__(self) -> Iterator[AnyIterable[T]]: ... async def __aenter__(self: Self) -> Self: ... - async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: ... + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ... async def aclose(self) -> None: ... def pairwise(iterable: AnyIterable[T]) -> AsyncIterator[tuple[T, T]]: ... diff --git a/docs/source/api/functools.rst b/docs/source/api/functools.rst index d2aa122..f1d52a7 100644 --- a/docs/source/api/functools.rst +++ b/docs/source/api/functools.rst @@ -86,6 +86,9 @@ the ``__wrapped__`` callable may be wrapped with a new cache of different size. .. versionchanged:: Python3.9 :py:func:`classmethod` properly wraps caches. + .. versionchanged:: Python3.13 + :py:func:`classmethod` no longer wraps caches in a way that supports `cache_discard`. + .. versionadded:: 3.10.4 .. automethod:: cache_info() -> (hits=..., misses=..., maxsize=..., currsize=...) diff --git a/unittests/test_functools_lru.py b/unittests/test_functools_lru.py index 62cba68..1a509bc 100644 --- a/unittests/test_functools_lru.py +++ b/unittests/test_functools_lru.py @@ -1,3 +1,4 @@ +from typing import Callable, Any import sys import pytest @@ -7,8 +8,15 @@ from .utility import sync -def method_counter(size): +class Counter: + kind: object + count: Any + + +def method_counter(size: "int | None") -> "type[Counter]": class Counter: + kind = None + def __init__(self): self._count = 0 @@ -20,9 +28,10 @@ async def count(self): return Counter -def classmethod_counter(size): +def classmethod_counter(size: "int | None") -> "type[Counter]": class Counter: _count = 0 + kind = classmethod def __init__(self): type(self)._count = 0 @@ -36,32 +45,40 @@ async def count(cls): return Counter -def staticmethod_counter(size): +def staticmethod_counter(size: "int | None") -> "type[Counter]": # I'm sorry for writing this test – please don't do this at home! - _count = 0 + count: int = 0 class Counter: + kind = staticmethod + def __init__(self): - nonlocal _count - _count = 0 + nonlocal count + count = 0 @staticmethod @a.lru_cache(maxsize=size) async def count(): - nonlocal _count - _count += 1 - return _count + nonlocal count + count += 1 + return count return Counter -counter_factories = [method_counter, classmethod_counter, staticmethod_counter] +counter_factories: "list[Callable[[int | None], type[Counter]]]" = [ + method_counter, + classmethod_counter, + staticmethod_counter, +] @pytest.mark.parametrize("size", [0, 3, 10, None]) @pytest.mark.parametrize("counter_factory", counter_factories) @sync -async def test_method_plain(size, counter_factory): +async def test_method_plain( + size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]" +): """Test caching without resetting""" counter_type = counter_factory(size) @@ -76,7 +93,9 @@ async def test_method_plain(size, counter_factory): @pytest.mark.parametrize("size", [0, 3, 10, None]) @pytest.mark.parametrize("counter_factory", counter_factories) @sync -async def test_method_clear(size, counter_factory): +async def test_method_clear( + size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]" +): """Test caching with resetting everything""" counter_type = counter_factory(size) for _instance in range(4): @@ -91,14 +110,16 @@ async def test_method_clear(size, counter_factory): @pytest.mark.parametrize("size", [0, 3, 10, None]) @pytest.mark.parametrize("counter_factory", counter_factories) @sync -async def test_method_discard(size, counter_factory): +async def test_method_discard( + size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]" +): """Test caching with resetting specific item""" counter_type = counter_factory(size) - if ( - sys.version_info < (3, 9) - and type(counter_type.__dict__["count"]) is classmethod + if not ( + (3, 9) <= sys.version_info[:2] <= (3, 12) + or counter_type.kind is not classmethod ): - pytest.skip("classmethod does not respect descriptors up to 3.8") + pytest.skip("classmethod only respects descriptors between 3.9 and 3.12") for _instance in range(4): instance = counter_type() for reset in range(5): @@ -111,7 +132,9 @@ async def test_method_discard(size, counter_factory): @pytest.mark.parametrize("size", [0, 3, 10, None]) @pytest.mark.parametrize("counter_factory", counter_factories) @sync -async def test_method_metadata(size, counter_factory): +async def test_method_metadata( + size: "int | None", counter_factory: "Callable[[int | None], type[Counter]]" +): """Test cache metadata on methods""" tp = counter_factory(size) for instance in range(4): @@ -133,7 +156,7 @@ async def test_method_metadata(size, counter_factory): @pytest.mark.parametrize("size", [None, 0, 10, 128]) -def test_wrapper_attributes(size): +def test_wrapper_attributes(size: "int | None"): class Bar: @a.lru_cache async def method(self, int_arg: int):