From 1e5bbbb91badb767b955ae71ac10bc22e41a9817 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Sun, 16 Jun 2024 12:37:57 +0200 Subject: [PATCH 1/7] suppress PyRight/MyPy inconsistency (closes #142) --- asyncstdlib/contextlib.pyi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/asyncstdlib/contextlib.pyi b/asyncstdlib/contextlib.pyi index eea0d45..fdaf7d3 100644 --- a/asyncstdlib/contextlib.pyi +++ b/asyncstdlib/contextlib.pyi @@ -74,7 +74,10 @@ 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, From 90389c285cc60ab3785b1aa13ff523e7eca7cff0 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Sun, 16 Jun 2024 12:48:15 +0200 Subject: [PATCH 2/7] type hint non-suppressing contexts (closes #143) --- asyncstdlib/asynctools.py | 3 +-- asyncstdlib/contextlib.py | 7 +++---- asyncstdlib/contextlib.pyi | 4 ++-- asyncstdlib/itertools.py | 7 +++---- asyncstdlib/itertools.pyi | 2 +- 5 files changed, 10 insertions(+), 13 deletions(-) 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 fdaf7d3..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 @@ -84,7 +84,7 @@ class nullcontext(AsyncContextManager[T]): 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]]: ... From c6ec1cb80a5ebcad9f9e22d7d0904bed746f74aa Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Sun, 16 Jun 2024 13:21:32 +0200 Subject: [PATCH 3/7] check Py3.13 (pre-release) --- .github/workflows/unittests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index a88b715..a5b5b25 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' ] From 00b82309f55e47f6963081a994dfa121c39029a2 Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Sun, 16 Jun 2024 13:24:56 +0200 Subject: [PATCH 4/7] allow testing prerelease versions --- .github/workflows/unittests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index a5b5b25..f5eb0f4 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -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 From 969731323e3829621fa4261c453746e6fcb70b6e Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Sun, 16 Jun 2024 13:33:06 +0200 Subject: [PATCH 5/7] add upper bound for classmethod support --- unittests/test_functools_lru.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/unittests/test_functools_lru.py b/unittests/test_functools_lru.py index 62cba68..c9807a1 100644 --- a/unittests/test_functools_lru.py +++ b/unittests/test_functools_lru.py @@ -9,6 +9,8 @@ def method_counter(size): class Counter: + kind = None + def __init__(self): self._count = 0 @@ -23,6 +25,7 @@ async def count(self): def classmethod_counter(size): class Counter: _count = 0 + kind = classmethod def __init__(self): type(self)._count = 0 @@ -41,6 +44,8 @@ def staticmethod_counter(size): _count = 0 class Counter: + kind = staticmethod + def __init__(self): nonlocal _count _count = 0 @@ -94,11 +99,11 @@ async def test_method_clear(size, counter_factory): async def test_method_discard(size, counter_factory): """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): From ca368076874c39bc8e901adf4c58d7864a9a072d Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Sun, 16 Jun 2024 13:38:52 +0200 Subject: [PATCH 6/7] document Python version support --- docs/source/api/functools.rst | 3 +++ 1 file changed, 3 insertions(+) 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=...) From e89745468c7236e3b294920422883163e01bcb3e Mon Sep 17 00:00:00 2001 From: Max Fischer Date: Sun, 16 Jun 2024 14:02:07 +0200 Subject: [PATCH 7/7] type hint tests --- unittests/test_functools_lru.py | 48 ++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/unittests/test_functools_lru.py b/unittests/test_functools_lru.py index c9807a1..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,7 +8,12 @@ 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 @@ -22,7 +28,7 @@ async def count(self): return Counter -def classmethod_counter(size): +def classmethod_counter(size: "int | None") -> "type[Counter]": class Counter: _count = 0 kind = classmethod @@ -39,34 +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) @@ -81,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): @@ -96,7 +110,9 @@ 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 not ( @@ -116,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): @@ -138,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):