Skip to content

Commit

Permalink
Add helpers for typing proxies
Browse files Browse the repository at this point in the history
  • Loading branch information
jodal committed Jun 18, 2023
1 parent d0f5154 commit 4b746d6
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 0 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ include = ["docs", "examples", "tests"]

[tool.poetry.dependencies]
python = "^3.8.0"
typing-extensions = { version = "^4.0.0", python = "<3.10" }

[tool.poetry.group.black.dependencies]
black = "^23.3.0"
Expand Down
92 changes: 92 additions & 0 deletions src/pykka/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""The :mod:`pykka.typing` module contains helpers to improve type hints.
Example usage::
from typing import cast
from pykka import ActorProxy, ThreadingActor
from pykka.typing import ActorMemberMixin, proxy_field, proxy_method
class CircleActor(ThreadingActor):
pi = 3.14
def area(self, radius: float) -> float:
return self.pi * radius**2
class CircleProxy(ActorMemberMixin, ActorProxy[CircleActor]):
pi = proxy_field(CircleActor.pi)
area = proxy_method(CircleActor.area)
proxy = cast(CircleProxy, CircleActor.start().proxy())
reveal_type(proxy.stop)
# Revealed type is 'Callable[[], pykka.Future[None]]'
reveal_type(proxy.pi)
# Revealed type is 'pykka.Future[float]'
reveal_type(proxy.area))
# Revealed type is 'Callable[[float], pykka.Future[float]]'
"""

from __future__ import annotations

import sys
from typing import TYPE_CHECKING, Any, Callable, Generic, Protocol, TypeVar

from pykka import Actor

if TYPE_CHECKING:
from pykka import Future

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

Check warning on line 48 in src/pykka/typing.py

View check run for this annotation

Codecov / codecov/patch

src/pykka/typing.py#L48

Added line #L48 was not covered by tests
Concatenate,
ParamSpec,
)

__all__ = [
"ActorMemberMixin",
"proxy_field",
"proxy_method",
]


T = TypeVar("T")
P = ParamSpec("P")
R = TypeVar("R", covariant=True)


class Method(Protocol, Generic[P, R]):
def __get__(self, instance: Any, owner: type | None = None) -> Callable[P, R]:
...

Check warning on line 67 in src/pykka/typing.py

View check run for this annotation

Codecov / codecov/patch

src/pykka/typing.py#L67

Added line #L67 was not covered by tests

def __call__(self, obj: Any, *args: P.args, **kwargs: P.kwargs) -> R:
...

Check warning on line 70 in src/pykka/typing.py

View check run for this annotation

Codecov / codecov/patch

src/pykka/typing.py#L70

Added line #L70 was not covered by tests


def proxy_field(field: T) -> Future[T]:
"""Type a field on an actor proxy."""
return field # type: ignore[return-value]


def proxy_method(
field: Callable[Concatenate[Any, P], T],
) -> Method[P, Future[T]]:
"""Type a method on an actor proxy."""
return field # type: ignore[return-value]


class ActorMemberMixin:
"""Mixin class for typing actor members accessible through a proxy."""

stop = proxy_method(Actor.stop)
on_start = proxy_method(Actor.on_start)
on_stop = proxy_method(Actor.on_stop)
on_failure = proxy_method(Actor.on_failure)
on_receive = proxy_method(Actor.on_receive)
69 changes: 69 additions & 0 deletions tests/proxy/test_typed_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Iterator, cast

import pytest

import pykka
from pykka import Actor, ActorProxy
from pykka.typing import ActorMemberMixin, proxy_field, proxy_method

if TYPE_CHECKING:
from tests.types import Runtime


@dataclass
class Constants:
pi: float


class CircleActor(Actor):
constants = pykka.traversable(Constants(pi=3.14))
text: str = "The fox crossed the road."

def area(self, radius: float) -> float:
return self.constants.pi * radius**2


class ConstantsProxy:
pi = proxy_field(CircleActor.constants.pi)


class FooProxy(ActorMemberMixin, ActorProxy[CircleActor]):
numbers: ConstantsProxy
text = proxy_field(CircleActor.text)
area = proxy_method(CircleActor.area)


@pytest.fixture(scope="module")
def actor_class(runtime: Runtime) -> type[CircleActor]:
class FooActorImpl(CircleActor, runtime.actor_class): # type: ignore[name-defined] # noqa: E501
pass

return FooActorImpl


@pytest.fixture()
def proxy(
actor_class: type[CircleActor],
) -> Iterator[FooProxy]:
proxy = cast(FooProxy, actor_class.start().proxy())
yield proxy
proxy.stop()


def test_proxy_field(proxy: FooProxy) -> None:
assert proxy.text.get() == "The fox crossed the road."


def test_proxy_traversable_object_field(proxy: FooProxy) -> None:
assert proxy.constants.pi.get() == 3.14


def test_proxy_method(proxy: FooProxy) -> None:
assert proxy.area(2.0).get() == 12.56


def test_proxy_to_actor_methods(proxy: FooProxy) -> None:
assert proxy.stop().get() is None

0 comments on commit 4b746d6

Please sign in to comment.