Skip to content

Commit

Permalink
Deprecate Py3.6 and Py3.7 (#119)
Browse files Browse the repository at this point in the history
* remove tests for outdated versions

* update GH actions

* remove old versions from metadata

* remove typing backports

* simplify ScopedIter

* remove slot_get helper (closes #78)

* ignore typing ellipsis

* improved type hinting for reduce

* simplify batched

* aclose type check
  • Loading branch information
maxfischer2781 authored Jan 15, 2024
1 parent 77a16f6 commit e5a36f8
Show file tree
Hide file tree
Showing 19 changed files with 169 additions and 184 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ exclude_lines =
if __name__ == '__main__'
@overload
if TYPE_CHECKING
...
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v2
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ on:

jobs:
build:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [
'3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12',
'pypy-3.6', 'pypy-3.8', 'pypy-3.10'
'3.8', '3.9', '3.10', '3.11', '3.12',
'pypy-3.8', 'pypy-3.10'
]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: '3.9'
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand Down
37 changes: 21 additions & 16 deletions asyncstdlib/_core.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
"""
Internal helpers to safely build async abstractions
While some of these helpers have public siblings
(e.g. :py:class:`~.ScopedIter` and :py:func:`~.asynctools.scoped_iter`)
they are purposely kept separate.
Any helpers in this module are *not* bound to maintaining a public interface,
and offer less convenience to save on overhead.
"""
from inspect import iscoroutinefunction
from typing import (
Any,
Expand Down Expand Up @@ -55,41 +64,38 @@ async def _aiter_sync(iterable: Iterable[T]) -> AsyncIterator[T]:


class ScopedIter(Generic[T]):
"""Context manager that provides and cleans up an iterator for an iterable"""
"""
Context manager that provides and cleans up an iterator for an iterable
Note that unlike :py:func:`~.asynctools.scoped_iter`, this helper does
*not* borrow the iterator automatically. Use :py:func:`~.borrow` if needed.
"""

__slots__ = ("_iterable", "_iterator")
__slots__ = ("_iterator",)

def __init__(self, iterable: AnyIterable[T]):
self._iterable: Optional[AnyIterable[T]] = iterable
self._iterator: Optional[AsyncIterator[T]] = None
self._iterator: AsyncIterator[T] = aiter(iterable)

async def __aenter__(self) -> AsyncIterator[T]:
assert (
self._iterable is not None
), f"{self.__class__.__name__} is not re-entrant"
self._iterator = aiter(self._iterable)
self._iterable = None
return self._iterator

async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> bool:
) -> None:
try:
aclose = self._iterator.aclose() # type: ignore
except AttributeError:
pass
else:
await aclose
return False


async def borrow(iterator: AsyncIterator[T]) -> AsyncGenerator[T, None]:
def borrow(iterator: AsyncIterator[T]) -> AsyncGenerator[T, None]:
"""Borrow an async iterator for iteration, preventing it from being closed"""
async for item in iterator:
yield item
return (item async for item in iterator)


def awaitify(
Expand All @@ -112,8 +118,7 @@ def __init__(self, function: Union[Callable[..., T], Callable[..., Awaitable[T]]
self._async_call: Optional[Callable[..., Awaitable[T]]] = None

def __call__(self, *args: Any, **kwargs: Any) -> Awaitable[T]:
async_call = self._async_call
if async_call is None:
if (async_call := self._async_call) is None:
value = self.__wrapped__(*args, **kwargs)
if isinstance(value, Awaitable):
self._async_call = self.__wrapped__ # type: ignore
Expand Down
23 changes: 15 additions & 8 deletions asyncstdlib/_lrucache.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,18 @@ def __wrapped__(self) -> AC:

def cache_parameters(self) -> CacheParameters:
"""Get the parameters of the cache"""
...

def cache_info(self) -> CacheInfo:
"""
Get the current performance and boundary of the cache
as a :py:class:`~typing.NamedTuple`
"""
...

def cache_clear(self) -> None:
"""Evict all call argument patterns and their results from the cache"""
...

def cache_discard(self, *args: Any, **kwargs: Any) -> None:
"""
Expand All @@ -95,9 +98,11 @@ def cache_discard(self, *args: Any, **kwargs: Any) -> None:
the descriptor must support wrapping descriptors for this method
to detect implicit arguments such as ``self``.
"""
# Maintainers note:
# "support wrapping descriptors" means that the wrapping descriptor has to use
# the cache as a descriptor as well, i.e. invoke its ``__get__`` method instead
# of just passing in `self`/`cls`/... directly.
...


class LRUAsyncBoundCallable(LRUAsyncCallable[AC]):
Expand Down Expand Up @@ -212,7 +217,8 @@ def lru_cache(
elif callable(maxsize):
# used as function decorator, first arg is the function to be wrapped
fast_wrapper = CachedLRUAsyncCallable(cast(AC, maxsize), typed, 128)
return update_wrapper(fast_wrapper, maxsize)
update_wrapper(fast_wrapper, maxsize)
return fast_wrapper
elif maxsize is not None:
raise TypeError(
"first argument to 'lru_cache' must be an int, a callable or None"
Expand All @@ -227,7 +233,8 @@ def lru_decorator(function: AC) -> LRUAsyncCallable[AC]:
wrapper = UncachedLRUAsyncCallable(function, typed)
else:
wrapper = CachedLRUAsyncCallable(function, typed, maxsize)
return update_wrapper(wrapper, function)
update_wrapper(wrapper, function)
return wrapper

return lru_decorator

Expand Down Expand Up @@ -301,11 +308,11 @@ class UncachedLRUAsyncCallable(LRUAsyncCallable[AC]):
__get__ = cache__get

def __init__(self, call: AC, typed: bool):
self.__wrapped__ = call
self.__wrapped__ = call # type: ignore[reportIncompatibleMethodOverride]
self.__misses = 0
self.__typed = typed

async def __call__(self, *args, **kwargs): # type: ignore
async def __call__(self, *args: Any, **kwargs: Any) -> Any: # type: ignore[reportIncompatibleVariableOverride]
self.__misses += 1
return await self.__wrapped__(*args, **kwargs)

Expand Down Expand Up @@ -339,13 +346,13 @@ class MemoizedLRUAsyncCallable(LRUAsyncCallable[AC]):
__get__ = cache__get

def __init__(self, call: AC, typed: bool):
self.__wrapped__ = call
self.__wrapped__ = call # type: ignore[reportIncompatibleMethodOverride]
self.__hits = 0
self.__misses = 0
self.__typed = typed
self.__cache: Dict[Union[CallKey, int, str], Any] = {}

async def __call__(self, *args, **kwargs): # type: ignore
async def __call__(self, *args: Any, **kwargs: Any) -> Any: # type: ignore[reportIncompatibleVariableOverride]
key = CallKey.from_call(args, kwargs, typed=self.__typed)
try:
result = self.__cache[key]
Expand Down Expand Up @@ -394,14 +401,14 @@ class CachedLRUAsyncCallable(LRUAsyncCallable[AC]):
__get__ = cache__get

def __init__(self, call: AC, typed: bool, maxsize: int):
self.__wrapped__ = call
self.__wrapped__ = call # type: ignore[reportIncompatibleMethodOverride]
self.__hits = 0
self.__misses = 0
self.__typed = typed
self.__maxsize = maxsize
self.__cache: OrderedDict[Union[int, str, CallKey], Any] = OrderedDict()

async def __call__(self, *args, **kwargs): # type: ignore
async def __call__(self, *args: Any, **kwargs: Any) -> Any: # type: ignore[reportIncompatibleVariableOverride]
key = CallKey.from_call(args, kwargs, typed=self.__typed)
try:
result = self.__cache[key]
Expand Down
11 changes: 1 addition & 10 deletions asyncstdlib/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
This module is for internal use only. Do *not* put any new
"async typing" definitions here.
"""
import sys
from typing import (
TypeVar,
Hashable,
Expand All @@ -16,15 +15,7 @@
Awaitable,
)

if sys.version_info >= (3, 8):
from typing import Protocol, AsyncContextManager, ContextManager, TypedDict
else:
from typing_extensions import (
Protocol,
AsyncContextManager,
ContextManager,
TypedDict,
)
from typing import Protocol, AsyncContextManager, ContextManager, TypedDict

__all__ = [
"Protocol",
Expand Down
20 changes: 1 addition & 19 deletions asyncstdlib/_utility.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TypeVar, Any, Optional, Callable
from typing import TypeVar, Optional, Callable

from ._typing import Protocol

Expand Down Expand Up @@ -29,21 +29,3 @@ def decorator(thing: D) -> D:
return thing

return decorator


def slot_get(instance: object, name: str) -> Any:
"""
Emulate ``instance.name`` using slot lookup as used for special methods
This invokes the descriptor protocol, i.e. it calls the attribute's
``__get__`` if available.
"""

owner = type(instance)
attribute = getattr(owner, name)
try:
descriptor_get = attribute.__get__
except AttributeError:
return attribute
else:
return descriptor_get(instance, owner)
Loading

0 comments on commit e5a36f8

Please sign in to comment.