Skip to content

Commit

Permalink
Test and typing maintenance (#145)
Browse files Browse the repository at this point in the history
* suppress PyRight/MyPy inconsistency (closes #142)

* type hint non-suppressing contexts (closes #143)

* check Py3.13 (pre-release)

* document classmethod Python version support
  • Loading branch information
maxfischer2781 authored Jun 17, 2024
1 parent f9c80dd commit 635f031
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 34 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
]

Expand All @@ -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
Expand Down
3 changes: 1 addition & 2 deletions asyncstdlib/asynctools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}>"
Expand Down
7 changes: 3 additions & 4 deletions asyncstdlib/contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions asyncstdlib/contextlib.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,25 @@ 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

@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",
Expand Down
7 changes: 3 additions & 4 deletions asyncstdlib/itertools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion asyncstdlib/itertools.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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]]: ...
Expand Down
3 changes: 3 additions & 0 deletions docs/source/api/functools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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=...)
Expand Down
61 changes: 42 additions & 19 deletions unittests/test_functools_lru.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Callable, Any
import sys

import pytest
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down

0 comments on commit 635f031

Please sign in to comment.