Skip to content

Commit

Permalink
Merge pull request #199 from jodal/proxy-typing
Browse files Browse the repository at this point in the history
Add helpers for typing proxies
  • Loading branch information
jodal committed Jun 19, 2023
2 parents d0f5154 + 58e5ea4 commit c98d1bb
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 1 deletion.
10 changes: 10 additions & 0 deletions docs/api/typing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
==========
Type hints
==========

.. automodule:: pykka.typing
:members: proxy_field, proxy_method

.. versionadded:: 4.0

.. autoclass:: pykka.typing.ActorMemberMixin
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Project resources
api/messages
api/logging
api/debug
api/typing


License
Expand Down
3 changes: 2 additions & 1 deletion 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 Expand Up @@ -55,7 +56,7 @@ branch = true
source = ["pykka"]

[tool.coverage.report]
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING"]
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING", '\.\.\.']

[tool.mypy]
disallow_untyped_defs = true
Expand Down
114 changes: 114 additions & 0 deletions src/pykka/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""The :mod:`pykka.typing` module contains helpers to improve type hints.
Since Pykka 4.0, Pykka has complete type hints for the public API, tested using
both `Mypy <https://www.mypy-lang.org/>`_ and `Pyright
<https://github.com/microsoft/pyright>`_.
Due to the dynamic nature of :class:`~pykka.ActorProxy` objects, it is not
possible to automatically type them correctly. This module contains helpers to
manually create additional classes that correctly describe the type hints for
the proxy objects. In cases where a proxy objects is used a lot, this might be
worth the extra effort to increase development speed and catch bugs earlier.
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 (
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]:
...

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


def proxy_field(field: T) -> Future[T]:
"""Type a field on an actor proxy.
.. versionadded:: 4.0
"""
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.
.. versionadded:: 4.0
"""
return field # type: ignore[return-value]


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

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 c98d1bb

Please sign in to comment.