Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Used TypeVarTuple and ParamSpec in several places #652

Merged
merged 21 commits into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a8aa4b3
Used TypeVarTuple and ParamSpec in several places to improve type ann…
agronholm Dec 14, 2023
5448133
Merge branch 'master' into typevartuples
agronholm Dec 14, 2023
2c5c94d
Added conditional dependency on typing_extensions
agronholm Dec 14, 2023
2f876cb
Ignored mypy error caused by a mypy bug
agronholm Dec 14, 2023
9617c35
Update pyproject.toml
agronholm Dec 15, 2023
a25514f
Converted some local functions in tests to coroutine functions
agronholm Dec 15, 2023
4cf038b
Improved annotations in _BlockingAsyncContextManager
agronholm Dec 15, 2023
6bfae84
Improved annotations in BlockingPortal
agronholm Dec 15, 2023
83df7e8
Added more uses of TypeVarTuple
agronholm Dec 16, 2023
5f3e274
Used TypeVarTuple in AsyncBackend.run()
agronholm Dec 16, 2023
b21dc7d
Improved annotations on BlockingPortal._call_func()
agronholm Dec 16, 2023
6257a92
Merge branch 'master' into typevartuples
agronholm Dec 16, 2023
8fbbf44
Used ParamSpec in Trio's _call_in_runner_task()
agronholm Dec 16, 2023
ef77d25
Link to #560 in the changelog
agronholm Dec 16, 2023
72bebdf
Added more missing TypeVarTuple uses to AsyncBackend
agronholm Dec 16, 2023
6cad8fc
Made T_co actually covariant
agronholm Dec 16, 2023
19766dc
Fixed return type annotation for TaskGroup.start()
agronholm Dec 16, 2023
c8d7032
Improved type annotations of `BlockingPortal.start_task()`
agronholm Dec 16, 2023
246c7dc
Narrowed down the type of _call_queue in Trio TestRunner
agronholm Dec 16, 2023
d64b211
Updated the changelog entry to match the scope of the changes
agronholm Dec 16, 2023
14f150e
Also mentioned BlockingPortal.call() in the changelog
agronholm Dec 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
Lura Skye)
- Enabled the ``Event`` and ``CapacityLimiter`` classes to be instantiated outside an
event loop thread
- Improved type annotations of numerous methods and functions including ``anyio.run()``,
``TaskGroup.start_soon()``, ``anyio.from_thread.run()``,
``anyio.to_thread.run_sync()`` and ``anyio.to_process.run_sync()`` by making use of
PEP 646 ``TypeVarTuple`` to allow the positional arguments to be validated by static
type checkers (`#560 <https://github.com/agronholm/anyio/issues/560>`_)
- Fixed adjusting the total number of tokens in a ``CapacityLimiter`` on asyncio failing
to wake up tasks waiting to acquire the limiter in certain edge cases (fixed with help
from Egor Blagov)
Expand All @@ -18,6 +23,7 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
- Fixed cancellation propagating on asyncio from a task group to child tasks if the task
hosting the task group is in a shielded cancel scope
(`#642 <https://github.com/agronholm/anyio/issues/642>`_)
- Fixed the type annotation of ``anyio.Path.samefile()`` to match Typeshed

**4.1.0**

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies = [
"exceptiongroup >= 1.0.2; python_version < '3.11'",
gschaffner marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also suggest picking f7d3f5f and 43e3e26. Like T_co, T_Retval should only be used in covariant positions and marking it as such will cause Mypy to error if it's ever accidentally used in a position with the wrong variance.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've picked f7d3f5f, but unsure about 43e3e26.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(in case anyone is looking at this in the future, there's some more discussion in a Gitter thread: https://matrix.to/#/!JfFIjeKHlqEVmAsxYP:gitter.im/$jPGgnv0ghzCsa33gJ2SFE1YRmFqVFoqwQIeBMMFThqw?via=gitter.im)

gschaffner marked this conversation as resolved.
Show resolved Hide resolved
gschaffner marked this conversation as resolved.
Show resolved Hide resolved
"idna >= 2.8",
"sniffio >= 1.1",
"typing_extensions >= 4.1; python_version < '3.11'",
]
dynamic = ["version"]

Expand Down
46 changes: 32 additions & 14 deletions src/anyio/_backends/_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,22 @@
from ..lowlevel import RunVar
from ..streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream

if sys.version_info >= (3, 10):
from typing import ParamSpec
else:
from typing_extensions import ParamSpec

if sys.version_info >= (3, 11):
from asyncio import Runner
from typing import TypeVarTuple, Unpack
else:
import contextvars
import enum
import signal
from asyncio import coroutines, events, exceptions, tasks

from exceptiongroup import BaseExceptionGroup
from typing_extensions import TypeVarTuple, Unpack

class _State(enum.Enum):
CREATED = "created"
Expand Down Expand Up @@ -271,6 +278,8 @@ def _do_shutdown(future: asyncio.futures.Future) -> None:

T_Retval = TypeVar("T_Retval")
T_contra = TypeVar("T_contra", contravariant=True)
PosArgsT = TypeVarTuple("PosArgsT")
P = ParamSpec("P")

_root_task: RunVar[asyncio.Task | None] = RunVar("_root_task")

Expand Down Expand Up @@ -682,8 +691,8 @@ async def __aexit__(

def _spawn(
self,
func: Callable[..., Awaitable[Any]],
args: tuple,
func: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
args: tuple[Unpack[PosArgsT]],
name: object,
task_status_future: asyncio.Future | None = None,
) -> asyncio.Task:
Expand Down Expand Up @@ -752,7 +761,10 @@ def task_done(_task: asyncio.Task) -> None:
return task

def start_soon(
self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None
self,
func: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
*args: Unpack[PosArgsT],
name: object = None,
) -> None:
self._spawn(func, args, name)

Expand Down Expand Up @@ -875,11 +887,11 @@ def __init__(self) -> None:

def _spawn_task_from_thread(
self,
func: Callable,
args: tuple[Any, ...],
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
args: tuple[Unpack[PosArgsT]],
kwargs: dict[str, Any],
name: object,
future: Future,
future: Future[T_Retval],
) -> None:
AsyncIOBackend.run_sync_from_thread(
partial(self._task_group.start_soon, name=name),
Expand Down Expand Up @@ -1883,7 +1895,10 @@ async def _run_tests_and_fixtures(
future.set_result(retval)

async def _call_in_runner_task(
self, func: Callable[..., Awaitable[T_Retval]], *args: object, **kwargs: object
self,
func: Callable[P, Awaitable[T_Retval]],
*args: P.args,
**kwargs: P.kwargs,
) -> T_Retval:
gschaffner marked this conversation as resolved.
Show resolved Hide resolved
if not self._runner_task:
self._send_stream, receive_stream = create_memory_object_stream[
Expand Down Expand Up @@ -1949,8 +1964,8 @@ class AsyncIOBackend(AsyncBackend):
@classmethod
def run(
cls,
func: Callable[..., Awaitable[T_Retval]],
args: tuple,
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
args: tuple[Unpack[PosArgsT]],
kwargs: dict[str, Any],
options: dict[str, Any],
) -> T_Retval:
Expand Down Expand Up @@ -2062,8 +2077,8 @@ def create_capacity_limiter(cls, total_tokens: float) -> abc.CapacityLimiter:
@classmethod
async def run_sync_in_worker_thread(
cls,
func: Callable[..., T_Retval],
args: tuple[Any, ...],
func: Callable[[Unpack[PosArgsT]], T_Retval],
args: tuple[Unpack[PosArgsT]],
abandon_on_cancel: bool = False,
limiter: abc.CapacityLimiter | None = None,
) -> T_Retval:
Expand Down Expand Up @@ -2133,8 +2148,8 @@ def check_cancelled(cls) -> None:
@classmethod
def run_async_from_thread(
cls,
func: Callable[..., Awaitable[T_Retval]],
args: tuple[Any, ...],
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
args: tuple[Unpack[PosArgsT]],
token: object,
) -> T_Retval:
async def task_wrapper(scope: CancelScope) -> T_Retval:
Expand All @@ -2160,7 +2175,10 @@ async def task_wrapper(scope: CancelScope) -> T_Retval:

@classmethod
def run_sync_from_thread(
cls, func: Callable[..., T_Retval], args: tuple[Any, ...], token: object
cls,
func: Callable[[Unpack[PosArgsT]], T_Retval],
args: tuple[Unpack[PosArgsT]],
token: object,
) -> T_Retval:
@wraps(func)
def wrapper() -> None:
Expand Down
47 changes: 34 additions & 13 deletions src/anyio/_backends/_trio.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,22 @@
from ..abc._eventloop import AsyncBackend
from ..streams.memory import MemoryObjectSendStream

if sys.version_info < (3, 11):
if sys.version_info >= (3, 10):
from typing import ParamSpec
else:
from typing_extensions import ParamSpec

if sys.version_info >= (3, 11):
from typing import TypeVarTuple, Unpack
else:
from exceptiongroup import BaseExceptionGroup
from typing_extensions import TypeVarTuple, Unpack

T = TypeVar("T")
T_Retval = TypeVar("T_Retval")
T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType)
PosArgsT = TypeVarTuple("PosArgsT")
P = ParamSpec("P")


#
Expand Down Expand Up @@ -167,7 +177,12 @@ async def __aexit__(
finally:
self._active = False

def start_soon(self, func: Callable, *args: object, name: object = None) -> None:
def start_soon(
self,
func: Callable[[Unpack[PosArgsT]], Awaitable[Any]],
*args: Unpack[PosArgsT],
name: object = None,
) -> None:
if not self._active:
raise RuntimeError(
"This task group is not active; no new tasks can be started."
Expand Down Expand Up @@ -201,11 +216,11 @@ def __init__(self) -> None:

def _spawn_task_from_thread(
self,
func: Callable,
args: tuple,
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
args: tuple[Unpack[PosArgsT]],
kwargs: dict[str, Any],
name: object,
future: Future,
future: Future[T_Retval],
) -> None:
trio.from_thread.run_sync(
partial(self._task_group.start_soon, name=name),
Expand Down Expand Up @@ -754,7 +769,10 @@ def _main_task_finished(self, outcome: object) -> None:
self._send_stream = None

def _call_in_runner_task(
self, func: Callable[..., Awaitable[T_Retval]], *args: object, **kwargs: object
self,
func: Callable[P, Awaitable[T_Retval]],
*args: P.args,
**kwargs: P.kwargs,
) -> T_Retval:
if self._send_stream is None:
trio.lowlevel.start_guest_run(
Expand Down Expand Up @@ -808,8 +826,8 @@ class TrioBackend(AsyncBackend):
@classmethod
def run(
cls,
func: Callable[..., Awaitable[T_Retval]],
args: tuple,
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
args: tuple[Unpack[PosArgsT]],
kwargs: dict[str, Any],
options: dict[str, Any],
) -> T_Retval:
gschaffner marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -868,8 +886,8 @@ def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter:
@classmethod
async def run_sync_in_worker_thread(
cls,
func: Callable[..., T_Retval],
args: tuple[Any, ...],
func: Callable[[Unpack[PosArgsT]], T_Retval],
args: tuple[Unpack[PosArgsT]],
abandon_on_cancel: bool = False,
limiter: abc.CapacityLimiter | None = None,
) -> T_Retval:
Expand All @@ -891,15 +909,18 @@ def check_cancelled(cls) -> None:
@classmethod
def run_async_from_thread(
cls,
func: Callable[..., Awaitable[T_Retval]],
args: tuple[Any, ...],
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
args: tuple[Unpack[PosArgsT]],
token: object,
) -> T_Retval:
return trio.from_thread.run(func, *args)

@classmethod
def run_sync_from_thread(
cls, func: Callable[..., T_Retval], args: tuple[Any, ...], token: object
cls,
func: Callable[[Unpack[PosArgsT]], T_Retval],
args: tuple[Unpack[PosArgsT]],
token: object,
) -> T_Retval:
return trio.from_thread.run_sync(func, *args)

Expand Down
11 changes: 9 additions & 2 deletions src/anyio/_core/_eventloop.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,26 @@

import sniffio

if sys.version_info >= (3, 11):
from typing import TypeVarTuple, Unpack
else:
from typing_extensions import TypeVarTuple, Unpack

if TYPE_CHECKING:
from ..abc import AsyncBackend

# This must be updated when new backends are introduced
BACKENDS = "asyncio", "trio"

T_Retval = TypeVar("T_Retval")
PosArgsT = TypeVarTuple("PosArgsT")

threadlocals = threading.local()


def run(
func: Callable[..., Awaitable[T_Retval]],
*args: object,
func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
*args: Unpack[PosArgsT],
backend: str = "asyncio",
backend_options: dict[str, Any] | None = None,
) -> T_Retval:
Expand Down
9 changes: 3 additions & 6 deletions src/anyio/_core/_fileio.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
AsyncIterator,
Final,
Generic,
cast,
overload,
)

Expand Down Expand Up @@ -211,7 +210,7 @@ async def __anext__(self) -> Path:
if nextval is None:
raise StopAsyncIteration from None

return Path(cast("PathLike[str]", nextval))
return Path(nextval)


class Path:
Expand Down Expand Up @@ -518,7 +517,7 @@ def relative_to(self, *other: str | PathLike[str]) -> Path:

async def readlink(self) -> Path:
target = await to_thread.run_sync(os.readlink, self._path)
return Path(cast(str, target))
return Path(target)

async def rename(self, target: str | pathlib.PurePath | Path) -> Path:
if isinstance(target, Path):
Expand All @@ -545,9 +544,7 @@ def rglob(self, pattern: str) -> AsyncIterator[Path]:
async def rmdir(self) -> None:
await to_thread.run_sync(self._path.rmdir)

async def samefile(
self, other_path: str | bytes | int | pathlib.Path | Path
) -> bool:
async def samefile(self, other_path: str | PathLike[str]) -> bool:
if isinstance(other_path, Path):
other_path = other_path._path

Expand Down
Loading