`;
// Check if we already added versions and remove them if so.
// This happens when using the "Instant loading" feature.
// See https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#instant-loading
const currentVersions = document.querySelector(".md-version");
if (currentVersions !== null) {
currentVersions.remove();
}
document
.querySelector(".md-header__topic")
.insertAdjacentHTML("beforeend", versioning);
});
pykka-4.5.0/docs/reference/actors.md 0000644 0000000 0000000 00000000056 15262536163 014302 0 ustar 00 # Actors
::: pykka.Actor
::: pykka.ActorRef
pykka-4.5.0/docs/reference/debug.md 0000644 0000000 0000000 00000000041 15262536163 014067 0 ustar 00 # Debug helpers
::: pykka.debug
pykka-4.5.0/docs/reference/exceptions.md 0000644 0000000 0000000 00000000072 15262536163 015166 0 ustar 00 # Exceptions
::: pykka.ActorDeadError
::: pykka.Timeout
pykka-4.5.0/docs/reference/futures.md 0000644 0000000 0000000 00000000057 15262536163 014505 0 ustar 00 # Futures
::: pykka.Future
::: pykka.get_all
pykka-4.5.0/docs/reference/index.md 0000644 0000000 0000000 00000001412 15262536163 014113 0 ustar 00 # Reference
This section contains reference documentation for Pykka's public API.
- [Actors](actors.md) - the `Actor` base class and references to running actors.
- [Proxies](proxies.md) - an `ActorProxy` is a convenient abstraction on top of an `Actor`.
- [Futures](futures.md) - a `Future` is a handle to a result that may not be available yet.
- [Registry](registry.md) - the `ActorRegistry` lets you get references to running actors.
- [Exceptions](exceptions.md) - exceptions defined by Pykka.
- [Message objects](messages.md) - message objects used by Pykka.
- [Debug helpers](debug.md) - helpers for debugging Pykka applications.
- [Typing helpers](typing.md) - type annotations and helpers for Pykka.
- [Runtimes](runtimes.md) - the different runtimes Pykka supports.
pykka-4.5.0/docs/reference/messages.md 0000644 0000000 0000000 00000000046 15262536163 014615 0 ustar 00 # Message objects
::: pykka.messages
pykka-4.5.0/docs/reference/proxies.md 0000644 0000000 0000000 00000000120 15262536163 014470 0 ustar 00 # Proxies
::: pykka.ActorProxy
::: pykka.CallableProxy
::: pykka.traversable
pykka-4.5.0/docs/reference/registry.md 0000644 0000000 0000000 00000000044 15262536163 014654 0 ustar 00 # Registry
::: pykka.ActorRegistry
pykka-4.5.0/docs/reference/runtimes.md 0000644 0000000 0000000 00000001171 15262536163 014654 0 ustar 00 # Runtimes
By default, Pykka builds on top of Python's regular threading concurrency model,
via the standard library module [`threading`][threading].
Pykka 2 and earlier shipped with some alternative implementations that
ran on top of
[`gevent`](https://www.gevent.org/) or
[`eventlet`](https://eventlet.net/).
These alternative implementations were removed in Pykka 3.
Note that Pykka does no attempt at supporting a mix of concurrency runtimes.
## Threading runtime
The default threading runtime has no dependencies other than Pykka itself and
the Python standard library.
::: pykka.ThreadingFuture
::: pykka.ThreadingActor
pykka-4.5.0/docs/reference/typing.md 0000644 0000000 0000000 00000000043 15262536163 014315 0 ustar 00 # Typing helpers
::: pykka.typing
pykka-4.5.0/docs/stylesheets/extra.css 0000644 0000000 0000000 00000001100 15262536163 014727 0 ustar 00 .md-header {
color: var(--md-primary-bg-color);
background-color: var(--md-primary-fg-color);
}
.md-header .md-search__button {
color: var(--md-primary-bg-color--light);
background-color: var(--md-primary-fg-color--light);
}
.md-header .md-search__button:focus,
.md-header .md-search__button:hover {
color: var(--md-primary-bg-color);
background-color: var(--md-primary-fg-color--dark);
}
.md-header .md-search__button::before {
background-color: currentcolor;
}
.md-header .md-search__button::after {
background-color: var(--md-primary-fg-color--dark);
}
pykka-4.5.0/examples/basic.py 0000755 0000000 0000000 00000001613 15262536163 013053 0 ustar 00 #!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "pykka",
# ]
# ///
from typing import Any
import pykka
# Define a unique message to request stored messages
GetMessages = object()
class BasicActor(pykka.ThreadingActor):
def __init__(self) -> None:
super().__init__()
self._stored_messages: list[Any] = []
def on_receive(self, message: Any) -> Any:
if message is GetMessages:
return self._stored_messages
self._stored_messages.append(message)
return None
if __name__ == "__main__":
# Start the actor
actor_ref = BasicActor.start()
# Send some messages to the actor
actor_ref.tell({"no": "Norway", "se": "Sweden"})
actor_ref.tell({"a": 3, "b": 4, "c": 5})
# Retrieve and print stored messages
print(actor_ref.ask(GetMessages))
# Stop the actor
actor_ref.stop()
pykka-4.5.0/examples/cooperation.py 0000755 0000000 0000000 00000001713 15262536163 014315 0 ustar 00 #!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "pykka",
# ]
# ///
import pykka
class Adder(pykka.ThreadingActor):
def add_one(self, i: int) -> int:
print(f"Adder is adding 1 to {i}")
return i + 1
class Bookkeeper(pykka.ThreadingActor):
def __init__(self, adder: pykka.ActorProxy[Adder]) -> None:
super().__init__()
self.adder = adder
def count_to(self, target: int) -> None:
i = 0
while i < target:
i = self.adder.add_one(i).get()
print(f"Bookkeeper got {i} back")
if __name__ == "__main__":
# Start the adder actor
adder = Adder.start().proxy()
# Start the bookkeeper actor, passing it the adder actor's proxy
bookkeeper = Bookkeeper.start(adder).proxy()
# Ask the bookkeeper to count to 5
bookkeeper.count_to(5).get()
# Stop all running actors using the ActorRegistry
pykka.ActorRegistry.stop_all()
pykka-4.5.0/examples/deadlock.py 0000755 0000000 0000000 00000002716 15262536163 013545 0 ustar 00 #!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "pykka",
# ]
# ///
from __future__ import annotations
import logging
import os
import signal
import time
from typing import Any
import pykka
import pykka.debug
log = logging.getLogger(__name__)
class ActorA(pykka.ThreadingActor):
def foo(self, b: pykka.ActorProxy[ActorB]) -> Any:
log.debug("This is foo calling bar")
return b.bar().get()
class ActorB(pykka.ThreadingActor):
def __init__(self, a: pykka.ActorProxy[ActorA]) -> None:
super().__init__()
self.a = a
def bar(self) -> Any:
log.debug("This is bar calling foo; BOOM!")
return self.a.foo().get()
if __name__ == "__main__":
print("Setting up logging to get output from signal handler...")
logging.basicConfig(level=logging.DEBUG)
print("Registering signal handler...")
signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks)
print("Starting actors...")
a = ActorA.start().proxy()
b = ActorB.start(a).proxy()
print("Now doing something stupid that will deadlock the actors...")
a.foo(b)
# Yield to actors, so we get output in a readable order
time.sleep(0.01)
pid = os.getpid()
print("Making main thread relax; not block, not quit")
print(f"1) Use `kill -SIGUSR1 {pid:d}` to log thread tracebacks")
print(f"2) Then `kill {pid:d}` to terminate the process")
while True:
time.sleep(1)
pykka-4.5.0/examples/proxy.py 0000755 0000000 0000000 00000003577 15262536163 013166 0 ustar 00 #!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "pykka",
# ]
# ///
import threading
import time
from typing import cast
from pykka import ActorProxy, ThreadingActor
from pykka.typing import ActorMemberMixin, proxy_field, proxy_method
def log(msg: str) -> None:
thread_name = threading.current_thread().name
print(f"{thread_name}: {msg}")
class FooActor(ThreadingActor):
field = "this is the value of FooActor.field"
def bar(self) -> None:
log("this was printed by FooActor.bar()")
def baz(self) -> str:
time.sleep(0.5) # Block a bit to make it realistic
return "this was returned by FooActor.baz() after a delay"
class FooProxy(ActorMemberMixin, ActorProxy[FooActor]):
field = proxy_field(FooActor.field)
bar = proxy_method(FooActor.bar)
baz = proxy_method(FooActor.baz)
if __name__ == "__main__":
# Start the actor and get a proxy to it
proxy = cast("FooProxy", FooActor.start().proxy())
# Method with side effect
log("calling FooActor.bar() ...")
proxy.bar()
# Method with return value
log("calling FooActor.baz() ...")
# Does not block, returns a future:
result = proxy.baz()
log("printing result ... (blocking)")
# Blocks until ready:
log(result.get())
# Field reading
log("reading FooActor.field ...")
# Does not block, returns a future:
result = proxy.field
log("printing result ... (blocking)")
# Blocks until ready:
log(result.get())
# Field writing
log("writing FooActor.field ...")
# Assignment does not block:
proxy.field = "new value" # type: ignore[assignment] # ty: ignore[invalid-assignment]
# Does not block, returns a future:
result = proxy.field
log("printing new field value ... (blocking)")
# Blocks until ready:
log(result.get())
# Stop the actor
proxy.stop()
pykka-4.5.0/examples/proxy_typing.py 0000644 0000000 0000000 00000002430 15262536163 014540 0 ustar 00 from typing import TYPE_CHECKING, cast
from pykka import ActorProxy, ThreadingActor
from pykka.typing import ActorMemberMixin, proxy_field, proxy_method
# 1) The actor class to be proxied is defined as usual:
class CircleActor(ThreadingActor):
pi = 3.14
def area(self, radius: float) -> float:
return self.pi * radius**2
# 2) In addition, a proxy class is defined, which inherits from
# ActorMemberMixin to get the correct type hints for the actor methods:
class CircleProxy(ActorMemberMixin, ActorProxy[CircleActor]):
# For each field on the proxy, a proxy_field is defined:
pi = proxy_field(CircleActor.pi)
# For each method on the proxy, a proxy_method is defined:
area = proxy_method(CircleActor.area)
# 3) The actor is started like usual, and a proxy is created as usual, but
# the proxy is casted to the recently defined proxy class:
proxy = cast("CircleProxy", CircleActor.start().proxy())
# Now, the type hints for the proxy are correct:
if TYPE_CHECKING:
reveal_type(proxy.stop) # noqa: F821
# Revealed type is 'Callable[[], pykka.Future[None]]'
reveal_type(proxy.pi) # noqa: F821
# Revealed type is 'pykka.Future[float]'
reveal_type(proxy.area) # noqa: F821
# Revealed type is 'Callable[[float], pykka.Future[float]]'
pykka-4.5.0/examples/testing.py 0000644 0000000 0000000 00000000503 15262536163 013441 0 ustar 00 from typing import Any
import pykka
class ProducerActor(pykka.ThreadingActor):
def __init__(self, consumer: pykka.ActorProxy[Any]) -> None:
super().__init__()
self.consumer = consumer
def produce(self) -> None:
new_item = {"item": 1, "new": True}
self.consumer.consume(new_item)
pykka-4.5.0/examples/testing_test.py 0000644 0000000 0000000 00000002254 15262536163 014505 0 ustar 00 from __future__ import annotations
from typing import TYPE_CHECKING, Any
import pytest
from testing import ProducerActor
if TYPE_CHECKING:
from collections.abc import Generator
from unittest.mock import Mock
from pytest_mock import MockerFixture
from pykka import ActorProxy
@pytest.fixture
def consumer_mock(mocker: MockerFixture) -> Any:
return mocker.Mock()
@pytest.fixture
def producer(consumer_mock: Mock) -> Generator[ActorProxy[ProducerActor]]:
# Step 1: The actor under test is wired up with
# its dependencies and is started.
proxy = ProducerActor.start(consumer_mock).proxy()
yield proxy
# Step 4: The actor is stopped to clean up before the next test.
proxy.stop()
def test_producer_actor(
consumer_mock: Mock,
producer: ActorProxy[ProducerActor],
) -> None:
# Step 2: Interact with the actor.
# We call .get() on the last future returned by the actor to wait
# for the actor to process all messages before asserting anything.
producer.produce().get()
# Step 3: Assert that the return values or actor state is as expected.
consumer_mock.consume.assert_called_once_with({"item": 1, "new": True})
pykka-4.5.0/examples/worker_pool.py 0000755 0000000 0000000 00000002157 15262536163 014340 0 ustar 00 #!/usr/bin/env python3
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "pykka",
# ]
# ///
import pprint
import socket
from ipaddress import IPv4Address
import pykka
class Resolver(pykka.ThreadingActor):
def resolve(self, ip: IPv4Address) -> str | None:
try:
info = socket.gethostbyaddr(str(ip))
print(f"Finished resolving {ip}")
return info[0]
except Exception:
print(f"Failed resolving {ip}")
return None
if __name__ == "__main__":
ip_addresses = [IPv4Address(f"193.35.52.{i}") for i in range(1, 20)]
pool_size = 4
# Start resolvers
pool = [Resolver.start().proxy() for _ in range(pool_size)]
# Distribute work by mapping IPs to resolvers (not blocking)
hosts: list[pykka.Future[str]] = []
for i, ip in enumerate(ip_addresses):
resolver = pool[i % len(pool)]
hosts.append(resolver.resolve(ip))
# Gather results (blocking)
result = list(zip(ip_addresses, pykka.get_all(hosts), strict=True))
pprint.pprint(result)
# Stop all actors
pykka.ActorRegistry.stop_all()
pykka-4.5.0/src/pykka/__init__.py 0000644 0000000 0000000 00000001446 15262536163 013622 0 ustar 00 """Pykka is a Python implementation of the actor model."""
import logging as _logging
from pykka._exceptions import ActorDeadError, Timeout
from pykka._future import Future, get_all
from pykka._proxy import ActorProxy, CallableProxy, traversable
from pykka._ref import ActorRef
from pykka._registry import ActorRegistry
# The following must be imported late, in this specific order.
from pykka._actor import Actor # isort:skip
from pykka._threading import ThreadingActor, ThreadingFuture # isort:skip
__all__ = [
"Actor",
"ActorDeadError",
"ActorProxy",
"ActorRef",
"ActorRegistry",
"CallableProxy",
"Future",
"ThreadingActor",
"ThreadingFuture",
"Timeout",
"get_all",
"traversable",
]
_logging.getLogger(__name__).addHandler(_logging.NullHandler())
pykka-4.5.0/src/pykka/_actor.py 0000644 0000000 0000000 00000032307 15262536163 013332 0 ustar 00 from __future__ import annotations
import abc
import logging
import sys
import threading
import uuid
from typing import TYPE_CHECKING, Any, Protocol, Self
from pykka import ActorDeadError, ActorRef, ActorRegistry, messages
from pykka._introspection import get_attr_directly
if TYPE_CHECKING:
from types import TracebackType
from pykka import Future
from pykka._envelope import Envelope
__all__ = ["Actor"]
logger = logging.getLogger("pykka")
class ActorInbox(Protocol):
def put(self, envelope: Envelope[Any], /) -> None: ...
def get(self) -> Envelope[Any]: ...
def empty(self) -> bool: ...
class Actor(abc.ABC):
"""An actor is an execution unit that executes concurrently with other actors.
To create an actor:
1. subclass one of the [`Actor`][pykka.Actor] implementations:
- [`ThreadingActor`][pykka.ThreadingActor]
2. implement your methods, including `__init__()`, as usual,
3. call [`Actor.start()`][pykka.Actor.start] on your actor class,
passing the method any arguments for your constructor.
To stop an actor, call [`Actor.stop()`][pykka.Actor.stop] or
[`ActorRef.stop()`][pykka.ActorRef.stop].
Example:
```py
import pykka
class MyActor(pykka.ThreadingActor):
def __init__(self, my_arg=None):
super().__init__()
... # My optional init code with access to start() arguments
def on_start(self):
... # My optional setup code in same context as on_receive()
def on_stop(self):
... # My optional cleanup code in same context as on_receive()
def on_failure(self, exception_type, exception_value, traceback):
... # My optional cleanup code in same context as on_receive()
def on_receive(self, message):
... # My optional message handling code for a plain actor
def a_method(self, ...):
... # My regular method to be used through an ActorProxy
my_actor_ref = MyActor.start(my_arg=...)
my_actor_ref.stop()
```
"""
@classmethod
def start(
cls,
*args: Any,
**kwargs: Any,
) -> ActorRef[Self]:
"""Start an actor.
Starting an actor also registers it in the
[`ActorRegistry`][pykka.ActorRegistry].
Any arguments passed to [`start()`][pykka.Actor.start] will be passed on
to the class constructor.
Behind the scenes, the following is happening when you call
[`start()`][pykka.Actor.start]:
1. The actor is created:
1. [`actor_urn`][pykka.Actor.actor_urn] is initialized with the
assigned URN.
2. [`actor_inbox`][pykka.Actor.actor_inbox] is initialized with a
new actor inbox.
3. [`actor_ref`][pykka.Actor.actor_ref] is initialized with a
[`pykka.ActorRef`][pykka.ActorRef] object for safely
communicating with the actor.
4. At this point, your `__init__()` code can run.
2. The actor is registered in [`pykka.ActorRegistry`][pykka.ActorRegistry].
3. The actor's receive loop is started.
Returns:
A ref which can be used to access the actor in a safe manner.
"""
obj = cls(*args, **kwargs)
assert obj.actor_ref is not None, (
"Actor.__init__() have not been called. "
"Did you forget to call super() in your override?"
)
ActorRegistry.register(obj.actor_ref)
logger.debug(f"Starting {obj}")
obj._start_actor_loop()
return obj.actor_ref
@staticmethod
@abc.abstractmethod
def _create_actor_inbox() -> ActorInbox:
"""Create an inbox for the actor.
Internal method for implementors of new actor types.
"""
msg = "Use a subclass of Actor"
raise NotImplementedError(msg)
@staticmethod
@abc.abstractmethod
def _create_future() -> Future[Any]:
"""Create a future for the actor.
Internal method for implementors of new actor types.
"""
msg = "Use a subclass of Actor"
raise NotImplementedError(msg)
@abc.abstractmethod
def _start_actor_loop(self) -> None:
"""Create and start the actor's event loop.
Internal method for implementors of new actor types.
"""
msg = "Use a subclass of Actor"
raise NotImplementedError(msg)
actor_urn: str
"""
The actor URN string is a universally unique identifier for the actor.
It may be used for looking up a specific actor using
[`ActorRegistry.get_by_urn()`][pykka.ActorRegistry.get_by_urn].
"""
actor_inbox: ActorInbox
"""
The actor's inbox.
Use [`ActorRef.tell()`][pykka.ActorRef.tell],
[`ActorRef.ask()`][pykka.ActorRef.ask], and friends to put messages in the
inbox.
"""
_actor_ref: ActorRef[Any]
@property
def actor_ref(self) -> ActorRef[Self]:
"""The actor's [`ActorRef`][pykka.ActorRef] instance."""
# This property only exists to improve the typing of the ActorRef.
return self._actor_ref
actor_stopped: threading.Event
"""
A [`threading.Event`][threading.Event] representing whether or not the actor
should continue processing messages.
Use [`stop()`][pykka.Actor.stop] to change it.
"""
def __init__(
self,
*_args: Any,
**_kwargs: Any,
) -> None:
"""Create actor.
You are free to override `__init__()`, but you must call your
superclass' `__init__()` to ensure that the fields
[`actor_urn`][pykka.Actor.actor_urn],
[`actor_inbox`][pykka.Actor.actor_inbox], and
[`actor_ref`][pykka.Actor.actor_ref] are initialized.
You can use `super()`:
super().__init__()
Or call your superclass directly:
pykka.ThreadingActor.__init__(self)
`__init__()` is called before the actor is started and registered
in the [`ActorRegistry`][pykka.ActorRegistry].
"""
self.actor_urn = uuid.uuid4().urn
self.actor_inbox = self._create_actor_inbox()
self.actor_stopped = threading.Event()
self._actor_ref = ActorRef(self)
def __str__(self) -> str:
return f"{self.__class__.__name__} ({self.actor_urn})"
def stop(self) -> None:
"""Stop the actor.
It's equivalent to calling
[`ActorRef.stop(block=False)`][pykka.ActorRef.stop].
"""
self.actor_ref.tell(messages._ActorStop()) # noqa: SLF001
def _stop(self) -> None:
"""Stop the actor immediately without processing the rest of the inbox."""
ActorRegistry.unregister(self.actor_ref)
self.actor_stopped.set()
logger.debug(f"Stopped {self}")
try:
self.on_stop()
except Exception: # noqa: BLE001
self._handle_failure(*sys.exc_info())
def _actor_loop(self) -> None:
"""Run the actor's core loop.
This is the method that will be executed by the thread or greenlet.
"""
self._actor_loop_setup()
self._actor_loop_running()
self._actor_loop_teardown()
def _actor_loop_setup(self) -> None:
try:
self.on_start()
except Exception: # noqa: BLE001
self._handle_failure(*sys.exc_info())
def _actor_loop_running(self) -> None:
while not self.actor_stopped.is_set():
envelope = self.actor_inbox.get()
try:
response = self._handle_receive(envelope.message)
if envelope.reply_to is not None:
envelope.reply_to.set(response)
except Exception:
if envelope.reply_to is not None:
logger.info(
f"Exception returned from {self} to caller:",
exc_info=sys.exc_info(),
)
envelope.reply_to.set_exception()
else:
self._handle_failure(*sys.exc_info())
try:
self.on_failure(*sys.exc_info())
except Exception: # noqa: BLE001
self._handle_failure(*sys.exc_info())
except BaseException: # noqa: BLE001
exception_value = sys.exc_info()[1]
logger.debug(f"{exception_value!r} in {self}. Stopping all actors.")
self._stop()
ActorRegistry.stop_all()
def _actor_loop_teardown(self) -> None:
while not self.actor_inbox.empty():
envelope = self.actor_inbox.get()
if envelope.reply_to is not None:
if isinstance(envelope.message, messages._ActorStop): # noqa: SLF001
envelope.reply_to.set(None)
else:
envelope.reply_to.set_exception(
exc_info=(
ActorDeadError,
ActorDeadError(
f"{self.actor_ref} stopped before handling the message"
),
None,
)
)
def on_start(self) -> None: # noqa: B027
"""Run code at the beginning of the actor's life.
Hook for doing any setup that should be done *after* the actor is
started, but *before* it starts processing messages.
/// note | Threading runtime
For [`ThreadingActor`][pykka.ThreadingActor], this method is
executed in the actor's own thread, while `__init__()` is executed
in the thread that created the actor.
///
If an exception is raised by this method the stack trace will be
logged, and the actor will stop.
"""
def on_stop(self) -> None: # noqa: B027
"""Run code at the end of the actor's life.
Hook for doing any cleanup that should be done *after* the actor has
processed the last message, and *before* the actor stops.
This hook is *not* called when the actor stops because of an unhandled
exception. In that case, the [`on_failure()`][pykka.Actor.on_failure]
hook is called instead.
/// note | Threading runtime
For [`ThreadingActor`][pykka.ThreadingActor] this method is executed
in the actor's own thread, immediately before the thread exits.
///
If an exception is raised by this method the stack trace will be
logged, and the actor will stop.
"""
def _handle_failure(
self,
exception_type: type[BaseException] | None,
exception_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
"""Log unexpected failures, unregisters and stops the actor."""
logger.error(
f"Unhandled exception in {self}:",
exc_info=(exception_type, exception_value, traceback), # type: ignore[arg-type] # ty: ignore[invalid-argument-type]
)
ActorRegistry.unregister(self.actor_ref)
self.actor_stopped.set()
def on_failure( # noqa: B027
self,
exception_type: type[BaseException] | None,
exception_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
"""Run code when an unhandled exception is raised.
Hook for doing any cleanup *after* an unhandled exception is raised,
and *before* the actor stops.
/// note | Threading runtime
For [`ThreadingActor`][pykka.ThreadingActor] this method is executed in
the actor's own thread, immediately before the thread exits.
///
The method's arguments are the relevant information from
[`sys.exc_info()`][sys.exc_info].
If an exception is raised by this method the stack trace will be
logged, and the actor will stop.
"""
def _handle_receive(self, message: Any) -> Any:
"""Handle messages sent to the actor."""
if isinstance(message, messages._ActorStop): # noqa: SLF001
return self._stop()
if isinstance(message, messages.ProxyCall):
callee = get_attr_directly(self, message.attr_path)
return callee(*message.args, **message.kwargs)
if isinstance(message, messages.ProxyGetAttr):
attr = get_attr_directly(self, message.attr_path)
return attr
if isinstance(message, messages.ProxySetAttr):
parent_attr = get_attr_directly(self, message.attr_path[:-1])
attr_name = message.attr_path[-1]
return setattr(parent_attr, attr_name, message.value)
return self.on_receive(message)
def on_receive(self, message: Any) -> Any:
"""May be implemented for the actor to handle regular non-proxy messages.
The default implementation logs the message at the `WARNING` level and
returns `None`. Call `super().on_receive(message)` for messages that
your actor does not handle, to get the same log message.
Args:
message: the message to handle
Returns:
anything that should be sent as a reply to the sender
"""
logger.warning(f"Unexpected message received by {self}: {message}")
pykka-4.5.0/src/pykka/_envelope.py 0000644 0000000 0000000 00000001442 15262536163 014033 0 ustar 00 from __future__ import annotations
from typing import TYPE_CHECKING, Any, Generic, TypeVar
if TYPE_CHECKING:
from pykka import Future
T = TypeVar("T")
class Envelope(Generic[T]):
"""Envelope to add metadata to a message.
This is an internal type and is not part of the public API.
"""
# Using slots speeds up envelope creation with ~20%
__slots__ = ["message", "reply_to"]
message: T
"""The message to send."""
reply_to: Future[Any] | None
"""The future to reply to if there is a response."""
def __init__(self, message: T, reply_to: Future[Any] | None = None) -> None:
self.message = message
self.reply_to = reply_to
def __repr__(self) -> str:
return f"Envelope(message={self.message!r}, reply_to={self.reply_to!r})"
pykka-4.5.0/src/pykka/_exceptions.py 0000644 0000000 0000000 00000000356 15262536163 014402 0 ustar 00 __all__ = ["ActorDeadError", "Timeout"]
class ActorDeadError(Exception):
"""Exception raised when trying to use a dead or unavailable actor."""
class Timeout(Exception): # noqa: N818
"""Exception raised at future timeout."""
pykka-4.5.0/src/pykka/_future.py 0000644 0000000 0000000 00000024144 15262536163 013534 0 ustar 00 from __future__ import annotations
import functools
from collections.abc import Callable, Generator, Iterable
from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar, cast
if TYPE_CHECKING:
from pykka._types import OptExcInfo
__all__ = ["Future", "get_all"]
T = TypeVar("T")
J = TypeVar("J") # For when T is Iterable[J]
M = TypeVar("M") # Result of Future.map()
R = TypeVar("R") # Result of Future.reduce()
GetHookFunc: TypeAlias = Callable[[float | None], T]
class _Unset:
pass
class Future(Generic[T]):
"""A handle to a value which is available now or in the future.
Typically returned by calls to actor methods or accesses to actor fields.
To get hold of the encapsulated value, call
[`Future.get()`][pykka.Future.get] or `await` the future.
"""
_get_hook: GetHookFunc[T] | None
_get_hook_result: T | _Unset
def __init__(self) -> None:
super().__init__()
self._get_hook = None
self._get_hook_result = _Unset()
def __repr__(self) -> str:
return ""
def get(
self,
*,
timeout: float | None = None,
) -> T:
"""Get the value encapsulated by the future.
If the encapsulated value is an exception, it is raised instead of
returned.
If `timeout` is `None`, as default, the method will block until it gets
a reply, potentially forever. If `timeout` is an integer or float, the
method will wait for a reply for `timeout` seconds, and then raise
[`Timeout`][pykka.Timeout].
The encapsulated value can be retrieved multiple times. The future will
only block the first time the value is accessed.
Args:
timeout: seconds to wait before timeout
Raises:
pykka.Timeout: if timeout is reached
Exception: encapsulated value if it is an exception
Returns:
encapsulated value if it is not an exception
"""
if self._get_hook is not None:
if isinstance(self._get_hook_result, _Unset):
self._get_hook_result = self._get_hook(timeout)
return self._get_hook_result
raise NotImplementedError
def set(
self,
value: T | None = None,
) -> None:
"""Set the encapsulated value.
Args:
value: the encapsulated value or nothing
Raises:
Exception: an exception if `set()` is called multiple times
/// note | Version changed: Pykka 4.3
Calling `set()` on a future that already has a get hook set now
raises an exception.
///
"""
raise NotImplementedError
def set_exception(
self,
exc_info: OptExcInfo | None = None,
) -> None:
"""Set an exception as the encapsulated value.
You can pass an `exc_info` three-tuple, as returned by
[`sys.exc_info()`][sys.exc_info]. If you don't pass `exc_info`,
[`sys.exc_info()`][sys.exc_info] will be called and the value returned
by it used.
In other words, if you're calling `set_exception()`, without any
arguments, from an except block, the exception you're currently handling
will automatically be set on the future.
Args:
exc_info: the encapsulated exception
/// note | Version changed: Pykka 4.3
Calling `set_exception()` on a future that already has a get hook
set now raises an exception.
///
"""
raise NotImplementedError
def set_get_hook(
self,
func: GetHookFunc[T],
) -> None:
"""Set a function to be executed when [`get()`][pykka.Future.get] is called.
The function will be called when [`get()`][pykka.Future.get] is called, with the
`timeout` value as the only argument. The function's return value will
be returned from [`get()`][pykka.Future.get].
Args:
func: callable accepting a timeout value, to produce return value of
`get()`
/// note | Version added: Pykka 1.2
///
/// note | Version changed: Pykka 4.3
Calling `set_get_hook()` on a future that already has a result set
now raises an exception.
///
"""
self._get_hook = func
def filter(
self: Future[Iterable[J]],
func: Callable[[J], bool],
) -> Future[Iterable[J]]:
"""Return a new future with only the items passing the predicate function.
If the future's value is an iterable, `filter()` will return a new
future whose value is another iterable with only the items from the
first iterable for which `func(item)` is true. If the future's value
isn't an iterable, a [`TypeError`][TypeError] will be raised when
[`get()`][pykka.Future.get] is called.
Example:
```pycon
>>> import pykka
>>> f = pykka.ThreadingFuture()
>>> g = f.filter(lambda x: x > 10)
>>> g
>>> f.set(range(5, 15))
>>> f.get()
range(5, 15)
>>> g.get()
[11, 12, 13, 14]
```
/// note | Version added: Pykka 1.2
///
"""
future = self.__class__()
future.set_get_hook(
lambda timeout: list(filter(func, self.get(timeout=timeout)))
)
return future
def join(
self: Future[Any],
*futures: Future[Any],
) -> Future[Iterable[Any]]:
"""Return a new future with a list of the result of multiple futures.
One or more futures can be passed as arguments to `join()`. The new
future returns a list with the results from all the joined futures.
Example:
```pycon
>>> import pykka
>>> f1 = pykka.ThreadingFuture()
>>> f2 = pykka.ThreadingFuture()
>>> f3 = pykka.ThreadingFuture()
>>> f = f1.join(f2, f3)
>>> f1.set('abc')
>>> f2.set(123)
>>> f3.set(False)
>>> f.get()
['abc', 123, False]
```
/// note | Version added: Pykka 1.2
///
"""
future = cast("Future[Iterable[Any]]", self.__class__())
future.set_get_hook(
lambda timeout: [f.get(timeout=timeout) for f in [self, *futures]]
)
return future
def map(
self,
func: Callable[[T], M],
) -> Future[M]:
"""Pass the result of the future through a function.
Example:
```pycon
>>> import pykka
>>> f = pykka.ThreadingFuture()
>>> g = f.map(lambda x: x + 10)
>>> f.set(30)
>>> g.get()
40
>>> f = pykka.ThreadingFuture()
>>> g = f.map(lambda x: x['foo'])
>>> f.set({'foo': 'bar'})
>>> g.get()
'bar'
```
/// note | Version added: Pykka 1.2
///
/// note | Version changed: Pykka 2.0
Previously, if the future's result was an iterable (except a
string), the function was applied to each item in the iterable.
This behavior was unpredictable and made regular use cases like
extracting a single field from a dict difficult, thus the
behavior has been simplified. Since Pykka 2.0, the entire result
value is passed to the function.
///
"""
future = cast("Future[M]", self.__class__())
future.set_get_hook(lambda timeout: func(self.get(timeout=timeout)))
return future
def reduce(
self: Future[Iterable[J]],
func: Callable[[R, J], R],
*args: R,
) -> Future[R]:
"""Reduce a future's iterable result to a single value.
The function of two arguments is applied cumulatively to the items of
the iterable, from left to right. The result of the first function call
is used as the first argument to the second function call, and so on,
until the end of the iterable. If the future's value isn't an iterable,
a [`TypeError`][TypeError] is raised.
`reduce()` accepts an optional second argument, which will be used as an
initial value in the first function call. If the iterable is empty, the
initial value is returned.
Example:
```pycon
>>> import pykka
>>> f = pykka.ThreadingFuture()
>>> g = f.reduce(lambda x, y: x + y)
>>> f.set(['a', 'b', 'c'])
>>> g.get()
'abc'
>>> f = pykka.ThreadingFuture()
>>> g = f.reduce(lambda x, y: x + y)
>>> f.set([1, 2, 3])
>>> (1 + 2) + 3
6
>>> g.get()
6
>>> f = pykka.ThreadingFuture()
>>> g = f.reduce(lambda x, y: x + y, 5)
>>> f.set([1, 2, 3])
>>> ((5 + 1) + 2) + 3
11
>>> g.get()
11
>>> f = pykka.ThreadingFuture()
>>> g = f.reduce(lambda x, y: x + y, 5)
>>> f.set([])
>>> g.get()
5
```
/// note | Version added: Pykka 1.2
///
"""
future = cast("Future[R]", self.__class__())
future.set_get_hook(
lambda timeout: functools.reduce(func, self.get(timeout=timeout), *args)
)
return future
def __await__(self) -> Generator[None, None, T]:
yield
value = self.get()
return value
__iter__ = __await__
def get_all(
futures: Iterable[Future[T]],
*,
timeout: float | None = None,
) -> Iterable[T]:
"""Collect all values encapsulated in the list of futures.
If `timeout` is not `None`, the method will wait for a reply for
`timeout` seconds, and then raise [`pykka.Timeout`][pykka.Timeout].
Args:
futures: futures for the results to collect
timeout: seconds to wait before timeout
Raises:
pykka.Timeout: if timeout is reached
Returns:
list of results
"""
return [future.get(timeout=timeout) for future in futures]
pykka-4.5.0/src/pykka/_introspection.py 0000644 0000000 0000000 00000005433 15262536163 015122 0 ustar 00 from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, NamedTuple
if TYPE_CHECKING:
from pykka import ActorProxy
from pykka._types import AttrPath
logger = logging.getLogger("pykka")
class AttrInfo(NamedTuple):
callable: bool
traversable: bool
def introspect_attrs(
*,
root: Any,
proxy: ActorProxy[Any],
) -> dict[AttrPath, AttrInfo]:
"""Introspects the actor's attributes."""
result: dict[AttrPath, AttrInfo] = {}
attr_paths_to_visit: list[AttrPath] = [(attr_name,) for attr_name in dir(root)]
while attr_paths_to_visit:
attr_path = attr_paths_to_visit.pop(0)
if attr_path[-1].startswith("_"):
# Attribute names starting with _ are considered private and are
# not exposed via ActorProxy.
continue
attr = get_attr_from_parent(root, attr_path)
# Keep `proxy` first to use its `__eq__` method instead of `attr`'s
# unknown implementation.
if proxy == attr:
logger.warning(
f"{root} attribute {'.'.join(attr_path)!r} "
f"is a proxy to itself. "
f"Consider making it private "
f"by renaming it to {'_' + attr_path[-1]!r}."
)
continue
attr_info = AttrInfo(
callable=callable(attr),
traversable=(
getattr(attr, "_pykka_traversable", False) is True
or getattr(attr, "pykka_traversable", False) is True
),
)
result[attr_path] = attr_info
if attr_info.traversable:
attr_paths_to_visit.extend(
[(*attr_path, attr_name) for attr_name in dir(attr)]
)
return result
def get_attr_from_parent(
root: Any,
attr_path: AttrPath,
) -> Any:
"""Get attribute information from `__dict__` on the parent."""
parent = get_attr_directly(root, attr_path[:-1])
parent_attrs = get_obj_dict(parent)
attr_name = attr_path[-1]
try:
return parent_attrs[attr_name]
except KeyError:
msg = (
f"type object {parent.__class__.__name__!r} has no attribute {attr_name!r}"
)
raise AttributeError(msg) from None
def get_attr_directly(
root: Any,
attr_path: AttrPath,
) -> Any:
"""Traverses the path and returns the attribute at the end of the path."""
attr = root
for attr_name in attr_path:
attr = getattr(attr, attr_name)
return attr
def get_obj_dict(obj: Any) -> dict[str, Any]:
"""Combine `__dict__` from `obj` and all its superclasses."""
result: dict[str, Any] = {}
for cls in reversed(obj.__class__.mro()):
result.update(cls.__dict__)
if hasattr(obj, "__dict__"):
result.update(obj.__dict__)
return result
pykka-4.5.0/src/pykka/_proxy.py 0000644 0000000 0000000 00000025523 15262536163 013405 0 ustar 00 from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from pykka import ActorDeadError, messages
from pykka._introspection import AttrInfo, introspect_attrs
if TYPE_CHECKING:
from pykka import Actor, ActorRef, Future
from pykka._types import AttrPath
__all__ = ["ActorProxy"]
logger = logging.getLogger("pykka")
T = TypeVar("T")
A = TypeVar("A", bound="Actor")
class ActorProxy(Generic[A]):
"""A proxy object for an actor's attributes and methods, via an `ActorRef`.
The proxy allows the referenced actor to be used through regular
method calls and field access.
# Creating a proxy
You can create an [`ActorProxy`][pykka.ActorProxy] from any
[`ActorRef`][pykka.ActorRef]:
actor_ref = MyActor.start()
actor_proxy = ActorProxy(actor_ref)
You can also get an [`ActorProxy`][pykka.ActorProxy] by using
[`proxy()`][pykka.ActorRef.proxy]:
actor_proxy = MyActor.start().proxy()
# Attributes and method calls
When reading an attribute or getting a return value from a method, you get a
[`Future`][pykka.Future] object back. To get the enclosed value from the
future, you must call [`get()`][pykka.Future.get] on the returned future:
print(actor_proxy.string_attribute.get())
print(actor_proxy.count().get() + 1)
If you call a method just for its side effects and do not care about the
return value, you do not need to accept the returned future or call
[`get()`][pykka.Future.get] on the future. Simply call the method, and it
will be executed concurrently with your own code:
actor_proxy.method_with_side_effect()
If you want to block your own code from continuing while the other method
is processing, you can use [`get()`][pykka.Future.get] to block until it
completes:
actor_proxy.method_with_side_effect().get()
You can also use the `await` keyword to block until the method completes:
await actor_proxy.method_with_side_effect()
If you access a proxied method as an attribute, without calling it, you
get an [`CallableProxy`][pykka.CallableProxy].
# Proxy to itself
An actor can use a proxy to itself to schedule work for itself. The
scheduled work will only be done after the current message and all messages
already in the inbox are processed.
For example, if an actor can split a time consuming task into multiple
parts, and after completing each part can ask itself to start on the next
part using proxied calls or messages to itself, it can react faster to
other incoming messages as they will be interleaved with the parts of the
time consuming task. This is especially useful for being able to stop the
actor in the middle of a time consuming task.
To create a proxy to yourself, use the actor's
[`actor_ref`][pykka.Actor.actor_ref] attribute:
proxy_to_myself_in_the_future = self.actor_ref.proxy()
If you create a proxy in your actor's constructor or
[`on_start()`][pykka.Actor.on_start] method, you can create a nice API for
deferring work to yourself in the future:
def __init__(self):
...
self._in_future = self.actor_ref.proxy()
...
def do_work(self):
...
self._in_future.do_more_work()
...
def do_more_work(self):
...
To avoid infinite loops during proxy introspection, proxies to self
should be kept as private instance attributes by prefixing the attribute
name with `_`.
Example:
```title="examples/cooperation.py"
--8<-- "examples/cooperation.py"
```
Args:
actor_ref: reference to the actor to proxy
Raises:
pykka.ActorDeadError: if the actor is not alive
"""
actor_ref: ActorRef[A]
"""The actor's [`pykka.ActorRef`][pykka.ActorRef] instance."""
_actor: A
_attr_path: AttrPath
_known_attrs: dict[AttrPath, AttrInfo]
_actor_proxies: dict[AttrPath, ActorProxy[A]]
_callable_proxies: dict[AttrPath, CallableProxy[A]]
def __init__(
self,
*,
actor_ref: ActorRef[A],
attr_path: AttrPath | None = None,
) -> None:
if not actor_ref.is_alive():
msg = f"{actor_ref} not found"
raise ActorDeadError(msg)
if (actor := actor_ref._actor_weakref()) is None: # noqa: SLF001
msg = f"{actor_ref}'s actor weakref has been deallocated"
raise ActorDeadError(msg)
self.actor_ref = actor_ref
self._actor = actor
self._attr_path = attr_path or ()
self._known_attrs = introspect_attrs(root=self._actor, proxy=self)
self._actor_proxies = {}
self._callable_proxies = {}
def __eq__(
self,
other: object,
) -> bool:
if not isinstance(other, ActorProxy):
return False
if self._actor != other._actor: # pyright: ignore[reportUnknownMemberType]
return False
return self._attr_path == other._attr_path
def __hash__(self) -> int:
return hash((self._actor, self._attr_path))
def __repr__(self) -> str:
return f""
def __dir__(self) -> list[str]:
result = ["__class__"]
result += list(self.__class__.__dict__.keys())
result += list(self.__dict__.keys())
result += [attr_path[0] for attr_path in list(self._known_attrs.keys())]
return sorted(result)
def __getattr__(self, name: str) -> Any:
"""Get a field or callable from the actor."""
attr_path: AttrPath = (*self._attr_path, name)
if attr_path not in self._known_attrs:
self._known_attrs = introspect_attrs(root=self._actor, proxy=self)
attr_info = self._known_attrs.get(attr_path)
if attr_info is None:
msg = f"{self} has no attribute {name!r}"
raise AttributeError(msg)
if attr_info.callable:
if attr_path not in self._callable_proxies:
self._callable_proxies[attr_path] = CallableProxy(
actor_ref=self.actor_ref,
attr_path=attr_path,
)
return self._callable_proxies[attr_path]
if attr_info.traversable:
if attr_path not in self._actor_proxies:
self._actor_proxies[attr_path] = ActorProxy(
actor_ref=self.actor_ref,
attr_path=attr_path,
)
return self._actor_proxies[attr_path]
message = messages.ProxyGetAttr(attr_path=attr_path)
return self.actor_ref.ask(message, block=False)
def __setattr__(
self,
name: str,
value: Any,
) -> None:
"""Set a field on the actor.
Blocks until the field is set to check if any exceptions was raised.
"""
if name == "actor_ref" or name.startswith("_"):
return super().__setattr__(name, value)
attr_path = (*self._attr_path, name)
message = messages.ProxySetAttr(attr_path=attr_path, value=value)
self.actor_ref.ask(message)
return None
class CallableProxy(Generic[A]):
"""Proxy to a single method.
[`CallableProxy`][pykka.CallableProxy] instances are returned when accessing
methods on a [`ActorProxy`][pykka.ActorProxy] without calling them.
Example:
```py
proxy = AnActor.start().proxy()
# Ask semantics returns a future. See `__call__()` docs.
future = proxy.do_work()
# Tell semantics are fire and forget. See `defer()` docs.
proxy.do_work.defer()
```
"""
actor_ref: ActorRef[A]
_attr_path: AttrPath
def __init__(
self,
*,
actor_ref: ActorRef[A],
attr_path: AttrPath,
) -> None:
self.actor_ref = actor_ref
self._attr_path = attr_path
def __call__(
self,
*args: Any,
**kwargs: Any,
) -> Future[Any]:
"""Call with [`ask()`][pykka.ActorRef.ask] semantics.
Returns a future which will yield the called method's return value.
If the call raises an exception it is set on the future, and the exception is
reraised by [`get()`][pykka.Future.get]. If the future is left unused,
the exception will not be reraised. Either way, the exception will
also be logged. See the logging section in the docs for details.
"""
message = messages.ProxyCall(
attr_path=self._attr_path, args=args, kwargs=kwargs
)
return self.actor_ref.ask(message, block=False)
def defer(
self,
*args: Any,
**kwargs: Any,
) -> None:
"""Call with [`tell()`][pykka.ActorRef.tell] semantics.
Does not create or return a future.
If the call raises an exception, there is no future to set the
exception on. Thus, the actor's [`on_failure()`][pykka.Actor.on_failure]
hook is called instead.
/// note | Version added: Pykka 2.0
///
"""
message = messages.ProxyCall(
attr_path=self._attr_path, args=args, kwargs=kwargs
)
self.actor_ref.tell(message)
def traversable(obj: T) -> T:
"""Mark an actor attribute as traversable.
The traversable marker makes the actor attribute's own methods and
attributes available to users of the actor through an
[`ActorProxy`][pykka.ActorProxy].
Used as a function to mark a single attribute:
class AnActor(pykka.ThreadingActor):
playback = pykka.traversable(Playback())
class Playback:
def play(self):
return True
This function can also be used as a class decorator, making all instances
of the class traversable:
class AnActor(pykka.ThreadingActor):
playback = Playback()
@pykka.traversable
class Playback:
def play(self):
return True
The third alternative, and the only way in Pykka < 2.0, is to manually
mark a class as traversable by setting the `pykka_traversable` attribute
to `True`:
class AnActor(pykka.ThreadingActor):
playback = Playback()
class Playback:
pykka_traversable = True
def play(self):
return True
When the attribute is marked as traversable, its methods can be executed
in the context of the actor through an actor proxy:
proxy = AnActor.start().proxy()
assert proxy.playback.play().get() is True
/// note | Version added: Pykka 2.0
///
"""
if hasattr(obj, "__slots__"):
msg = (
"pykka.traversable() cannot be used to mark "
"an object using slots as traversable."
)
raise ValueError(msg)
setattr(obj, "_pykka_traversable", True) # noqa: B010
return obj
pykka-4.5.0/src/pykka/_ref.py 0000644 0000000 0000000 00000015631 15262536163 012777 0 ustar 00 from __future__ import annotations
import weakref
from typing import (
TYPE_CHECKING,
Any,
Generic,
Literal,
TypeVar,
cast,
overload,
)
from pykka import ActorDeadError, ActorProxy
from pykka._envelope import Envelope
from pykka.messages import _ActorStop
if TYPE_CHECKING:
from threading import Event
from pykka import Actor, Future
from pykka._actor import ActorInbox
__all__ = ["ActorRef"]
A = TypeVar("A", bound="Actor")
class ActorRef(Generic[A]):
"""Reference to a running actor which may safely be passed around.
[`ActorRef`][pykka.ActorRef] instances are returned by
[`Actor.start()`][pykka.Actor.start] and the lookup methods in
[`ActorRegistry`][pykka.ActorRegistry]. You should never need to create
[`ActorRef`][pykka.ActorRef] instances yourself.
"""
actor_class: type[A]
"""The class of the referenced actor."""
actor_urn: str
"""See [`Actor.actor_urn`][pykka.Actor.actor_urn]."""
actor_inbox: ActorInbox
"""See [`Actor.actor_inbox`][pykka.Actor.actor_inbox]."""
actor_stopped: Event
"""See [`Actor.actor_stopped`][pykka.Actor.actor_stopped]."""
def __init__(
self,
actor: A,
) -> None:
self._actor_weakref = weakref.ref(actor)
self.actor_class = actor.__class__
self.actor_urn = actor.actor_urn
self.actor_inbox = actor.actor_inbox
self.actor_stopped = actor.actor_stopped
def __repr__(self) -> str:
return f""
def __str__(self) -> str:
return f"{self.actor_class.__name__} ({self.actor_urn})"
def is_alive(self) -> bool:
"""Check if actor is alive.
This is based on the actor's stopped flag. The actor is not guaranteed
to be alive and responding even though
[`is_alive()`][pykka.ActorRef.is_alive] returns `True`.
Returns:
`True` if actor is alive, `False` otherwise.
"""
return not self.actor_stopped.is_set()
def tell(
self,
message: Any,
) -> None:
"""Send message to actor without waiting for any response.
Will generally not block, but if the underlying queue is full it will
block until a free slot is available.
Args:
message: message to send
Raises:
ActorDeadError: if actor is not available
"""
if not self.is_alive():
msg = f"{self} not found"
raise ActorDeadError(msg)
self.actor_inbox.put(Envelope(message))
@overload
def ask(
self,
message: Any,
*,
block: Literal[False],
timeout: float | None = None,
) -> Future[Any]: ...
@overload
def ask(
self,
message: Any,
*,
block: Literal[True],
timeout: float | None = None,
) -> Any: ...
@overload
def ask(
self,
message: Any,
*,
block: bool = True,
timeout: float | None = None,
) -> Any | Future[Any]: ...
def ask(
self,
message: Any,
*,
block: bool = True,
timeout: float | None = None,
) -> Any | Future[Any]:
"""Send message to actor and wait for the reply.
The message can be of any type.
If `block` is `False`, it will immediately return a
[`Future`][pykka.Future] instead of blocking.
If `block` is `True`, and `timeout` is `None`, as default, the method
will block until it gets a reply, potentially forever. If `timeout` is
an integer or float, the method will wait for a reply for `timeout`
seconds, and then raise [`pykka.Timeout`][pykka.Timeout].
Args:
message: message to send
block: whether to block while waiting for a reply
timeout: seconds to wait before timeout if blocking
Raises:
Timeout: if timeout is reached if blocking
Exception: any exception returned by the receiving actor if blocking
Returns:
a future if not blocking, or a response if blocking
"""
future = self.actor_class._create_future() # noqa: SLF001
try:
if not self.is_alive():
msg = f"{self} not found"
raise ActorDeadError(msg) # noqa: TRY301
except ActorDeadError:
future.set_exception()
else:
self.actor_inbox.put(Envelope(message, reply_to=future))
if block:
return future.get(timeout=timeout)
return future
@overload
def stop(
self,
*,
block: Literal[True],
timeout: float | None = None,
) -> bool: ...
@overload
def stop(
self,
*,
block: Literal[False],
timeout: float | None = None,
) -> Future[bool]: ...
@overload
def stop(
self,
*,
block: bool = True,
timeout: float | None = None,
) -> bool | Future[bool]: ...
def stop(
self,
*,
block: bool = True,
timeout: float | None = None,
) -> bool | Future[bool]:
"""Send a message to the actor, asking it to stop.
Returns `True` if actor is stopped or was being stopped at the
time of the call. `False` if actor was already dead. If
`block` is `False`, it returns a future wrapping the result.
Messages sent to the actor before the actor is asked to stop will
be processed normally before it stops.
Messages sent to the actor after the actor is asked to stop will
be replied to with [`ActorDeadError`][pykka.ActorDeadError] after it
stops.
The actor may not be restarted.
`block` and `timeout` works as for [`ask()`][pykka.ActorRef.ask].
Returns:
a future if not blocking, or a boolean result if blocking
"""
ask_future = self.ask(_ActorStop(), block=False)
def _stop_result_converter(timeout: float | None) -> bool:
try:
ask_future.get(timeout=timeout)
except ActorDeadError:
return False
else:
return True
converted_future = ask_future.__class__()
converted_future.set_get_hook(_stop_result_converter)
converted_future = cast("Future[bool]", converted_future)
if block:
return converted_future.get(timeout=timeout)
return converted_future
def proxy(self: ActorRef[A]) -> ActorProxy[A]:
"""Wrap the [`ActorRef`][pykka.ActorRef] in an [`ActorProxy`][pykka.ActorProxy].
Using this method like this:
proxy = AnActor.start().proxy()
is analogous to:
proxy = ActorProxy(AnActor.start())
Raises:
ActorDeadError: if actor is not available
Returns:
a proxy object wrapping the actor reference.
"""
return ActorProxy(actor_ref=self)
pykka-4.5.0/src/pykka/_registry.py 0000644 0000000 0000000 00000013166 15262536163 014074 0 ustar 00 from __future__ import annotations
import logging
import threading
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Literal,
TypeVar,
overload,
)
if TYPE_CHECKING:
from pykka import Actor, ActorRef, Future
__all__ = ["ActorRegistry"]
logger = logging.getLogger("pykka")
A = TypeVar("A", bound="Actor")
class ActorRegistry:
"""Registry which provides easy access to all running actors.
Contains global state, but should be thread-safe.
"""
_actor_refs: ClassVar[list[ActorRef[Any]]] = []
_actor_refs_lock: ClassVar[threading.RLock] = threading.RLock()
@classmethod
def broadcast(
cls,
message: Any,
target_class: str | type[Actor] | None = None,
) -> None:
"""Broadcast `message` to all actors of the specified `target_class`.
If no `target_class` is specified, the message is broadcasted to all
actors.
Args:
message: the message to send
target_class: optional actor class or class name
"""
if isinstance(target_class, str):
targets = cls.get_by_class_name(target_class)
elif target_class is not None:
targets = cls.get_by_class(target_class)
else:
targets = cls.get_all()
for ref in targets:
ref.tell(message)
@classmethod
def get_all(cls) -> list[ActorRef[Any]]:
"""Get all running actors."""
with cls._actor_refs_lock:
return cls._actor_refs[:]
@classmethod
def get_by_class(
cls,
actor_class: type[A],
) -> list[ActorRef[A]]:
"""Get all running actors of the given class or a subclass.
Args:
actor_class: actor class, or any superclass of the actor
"""
with cls._actor_refs_lock:
return [
ref
for ref in cls._actor_refs
if issubclass(ref.actor_class, actor_class)
]
@classmethod
def get_by_class_name(
cls,
actor_class_name: str,
) -> list[ActorRef[Any]]:
"""Get all running actors of the given class name."""
with cls._actor_refs_lock:
return [
ref
for ref in cls._actor_refs
if ref.actor_class.__name__ == actor_class_name
]
@classmethod
def get_by_urn(
cls,
actor_urn: str,
) -> ActorRef[Any] | None:
"""Get an actor by its universally unique URN."""
with cls._actor_refs_lock:
refs = [ref for ref in cls._actor_refs if ref.actor_urn == actor_urn]
if not refs:
return None
return refs[0]
@classmethod
def register(
cls,
actor_ref: ActorRef[Any],
) -> None:
"""Register an [`ActorRef`][pykka.ActorRef] in the registry.
This is done automatically when an actor is started, e.g. by calling
[`Actor.start()`][pykka.Actor.start].
"""
with cls._actor_refs_lock:
cls._actor_refs.append(actor_ref)
logger.debug(f"Registered {actor_ref}")
@overload
@classmethod
def stop_all(
cls,
*,
block: Literal[True],
timeout: float | None = ...,
) -> list[bool]: ...
@overload
@classmethod
def stop_all(
cls,
*,
block: Literal[False],
timeout: float | None = ...,
) -> list[Future[bool]]: ...
@overload
@classmethod
def stop_all(
cls,
*,
block: bool = True,
timeout: float | None = None,
) -> list[bool] | list[Future[bool]]: ...
@classmethod
def stop_all(
cls,
*,
block: bool = True,
timeout: float | None = None,
) -> list[bool] | list[Future[bool]]:
"""Stop all running actors.
`block` and `timeout` works as for
[`ActorRef.stop()`][pykka.ActorRef.stop].
If `block` is `True`, the actors are guaranteed to be stopped
in the reverse of the order they were started in. This is helpful if
you have simple dependencies in between your actors, where it is
sufficient to shut down actors in a LIFO manner: last started, first
stopped.
If you have more complex dependencies in between your actors, you
should take care to shut them down in the required order yourself, e.g.
by stopping dependees from a dependency's
[`on_stop()`][pykka.Actor.on_stop] method.
Returns:
a list with the return values for each stop action
"""
return (
[
ref.stop(
block=True,
timeout=timeout,
)
for ref in reversed(cls.get_all())
]
if block
else [
ref.stop(
block=False,
timeout=timeout,
)
for ref in reversed(cls.get_all())
]
)
@classmethod
def unregister(
cls,
actor_ref: ActorRef[A],
) -> None:
"""Remove an [`ActorRef`][pykka.ActorRef] from the registry.
This is done automatically when an actor is stopped, e.g. by calling
[`Actor.stop()`][pykka.Actor.stop].
"""
removed = False
with cls._actor_refs_lock:
if actor_ref in cls._actor_refs:
cls._actor_refs.remove(actor_ref)
removed = True
if removed:
logger.debug(f"Unregistered {actor_ref}")
else:
logger.debug(f"Unregistered {actor_ref} (not found in registry)")
pykka-4.5.0/src/pykka/_threading.py 0000644 0000000 0000000 00000011727 15262536163 014172 0 ustar 00 from __future__ import annotations
import queue
import sys
import threading
import time
from itertools import count
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, TypeVar
from pykka import Actor, Future, Timeout
if TYPE_CHECKING:
from pykka._actor import ActorInbox
from pykka._envelope import Envelope
from pykka._future import GetHookFunc
from pykka._types import OptExcInfo
__all__ = ["ThreadingActor", "ThreadingFuture"]
T = TypeVar("T")
class ThreadingFutureResult(NamedTuple):
value: Any | None = None
exc_info: OptExcInfo | None = None
class ThreadingFuture(Future[T]):
"""Implementation of [`Future`][pykka.Future] for use with regular Python threads.
/// warning | Mutable messages
The future *does not* make a copy of the object which is
[`set()`][pykka.Future.set] on it. It is the setters responsibility to
only pass immutable objects or make a copy of the object before setting
it on the future.
///
/// note | Version changed: Pykka 0.14
Previously, the encapsulated value was a copy made with
[`copy.deepcopy()`][copy.deepcopy], unless the encapsulated value was a
future, in which case the original future was encapsulated.
///
"""
def __init__(self) -> None:
super().__init__()
self._condition: threading.Condition = threading.Condition()
self._result: ThreadingFutureResult | None = None
def get(
self,
*,
timeout: float | None = None,
) -> Any:
deadline: float | None = None if timeout is None else time.monotonic() + timeout
with self._condition:
try:
return super().get(timeout=timeout)
except NotImplementedError:
pass
while self._result is None:
remaining = (
deadline - time.monotonic() if deadline is not None else None
)
if remaining is not None and remaining <= 0.0:
msg = f"{timeout} seconds"
raise Timeout(msg)
self._condition.wait(timeout=remaining)
if self._result.exc_info is not None:
(exc_type, exc_value, exc_traceback) = self._result.exc_info
assert exc_type is not None
if exc_value is None:
exc_value = exc_type()
if exc_value.__traceback__ is not exc_traceback:
raise exc_value.with_traceback(exc_traceback)
raise exc_value
return self._result.value
def set(
self,
value: Any | None = None,
) -> None:
with self._condition:
if self._result is not None or self._get_hook is not None:
raise queue.Full
self._result = ThreadingFutureResult(value=value)
self._condition.notify_all()
def set_exception(
self,
exc_info: OptExcInfo | None = None,
) -> None:
assert exc_info is None or len(exc_info) == 3
if exc_info is None:
exc_info = sys.exc_info()
with self._condition:
if self._result is not None or self._get_hook is not None:
raise queue.Full
self._result = ThreadingFutureResult(exc_info=exc_info)
self._condition.notify_all()
def set_get_hook(
self,
func: GetHookFunc[T],
) -> None:
with self._condition:
if self._result is not None:
raise queue.Full
super().set_get_hook(func)
self._condition.notify_all()
_actor_thread_counter = count(0)
class ThreadingActor(Actor):
"""Implementation of [`Actor`][pykka.Actor] using regular Python threads."""
use_daemon_thread: ClassVar[bool] = False
"""
A boolean value indicating whether this actor is executed on a thread that
is a daemon thread (`True`) or not (`False`). The value is read when
[`Actor.start()`][pykka.Actor.start] is called. Changes after that have no
effect on the running actor.
The entire Python program exits when no alive non-daemon threads are left.
This means that an actor running on a daemon thread may be interrupted at
any time, and there is no guarantee that cleanup will be done or that
[`Actor.on_stop()`][pykka.Actor.on_stop] will be called.
Actors do not inherit the daemon flag from the actor that made it. It
always has to be set explicitly for the actor to run on a daemonic thread.
"""
@staticmethod
def _create_actor_inbox() -> ActorInbox:
inbox: queue.Queue[Envelope[Any]] = queue.Queue()
return inbox
@staticmethod
def _create_future() -> Future[Any]:
return ThreadingFuture()
def _start_actor_loop(self) -> None:
thread = threading.Thread(
target=self._actor_loop,
name=f"{self.__class__.__name__}-{next(_actor_thread_counter)}",
)
thread.daemon = self.use_daemon_thread
thread.start()
pykka-4.5.0/src/pykka/_types.py 0000644 0000000 0000000 00000000467 15262536163 013370 0 ustar 00 from __future__ import annotations
from types import TracebackType
from typing import TypeAlias
AttrPath: TypeAlias = tuple[str, ...]
# OptExcInfo matches the return type of sys.exc_info() in typeshed
OptExcInfo = tuple[
type[BaseException] | None,
BaseException | None,
TracebackType | None,
]
pykka-4.5.0/src/pykka/debug.py 0000644 0000000 0000000 00000004516 15262536163 013152 0 ustar 00 """Debug helpers."""
import logging
import sys
import threading
import traceback
from typing import Any
__all__ = ["log_thread_tracebacks"]
logger = logging.getLogger("pykka")
def log_thread_tracebacks(*_args: Any, **_kwargs: Any) -> None:
"""Log a traceback for each running thread at [`CRITICAL`][logging.CRITICAL] level.
This can be a convenient tool for debugging deadlocks.
The function accepts any arguments so that it can easily be used as e.g. a
signal handler, but it does not use the arguments for anything.
# Setup
To use this function as a signal handler, setup logging with a
[`CRITICAL`][logging.CRITICAL] threshold or lower and make your main thread
register this with the [`signal`][signal] module:
import logging
import signal
import pykka.debug
logging.basicConfig(level=logging.DEBUG)
signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks)
Then, if your application deadlocks, send the `SIGUSR1` signal to the
process:
kill -SIGUSR1
# Limitations
- The function *must* be registered as a signal handler by your main thread.
If not, [`signal.signal()`][signal.signal] will raise a
[`ValueError`][ValueError].
- All signals in Python are handled by the main thread. Thus, the signal
will only be handled, and the tracebacks logged, if your main thread is
available to do some work. Making your main thread idle using
[`time.sleep()`][time.sleep] is OK. The signal will awaken your main thread.
Blocking your main thread on e.g. [`queue.Queue.get()`][queue.Queue.get]
or [`pykka.Future.get()`][pykka.Future.get] will break signal handling,
and thus you won't be able to signal your process to print the thread
tracebacks.
The morale is: setup signals using your main thread, start your actors,
then let your main thread relax for the rest of your application's life
cycle.
/// note | Version added: Pykka 1.1
///
"""
thread_names = {t.ident: t.name for t in threading.enumerate()}
for ident, frame in sys._current_frames().items(): # noqa: SLF001
name = thread_names.get(ident, "?")
stack = "".join(traceback.format_stack(frame))
logger.critical(f"Current state of {name} (ident: {ident}):\n{stack}")
pykka-4.5.0/src/pykka/messages.py 0000644 0000000 0000000 00000004073 15262536163 013671 0 ustar 00 """The `pykka.messages` module contains Pykka's own actor messages.
In general, you should not need to use any of these classes. However, they have
been made part of the public API so that certain optimizations can be done
without touching Pykka's internals.
An example is to combine [`ask()`][pykka.ActorRef.ask] and
[`ProxyCall`][pykka.messages.ProxyCall] to call a method on an actor without
having to spend any resources on creating a proxy object:
reply = actor_ref.ask(
ProxyCall(
attr_path=('my_method',),
args=('foo',),
kwargs={'bar': 'baz'}
)
)
Another example is to use [`tell()`][pykka.ActorRef.tell] instead of
[`ask()`][pykka.ActorRef.ask] for the proxy method call, and thus avoid the
creation of a future for the return value if you don't need it.
It should be noted that these optimizations should only be necessary in very
special circumstances.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, NamedTuple
if TYPE_CHECKING:
from pykka._types import AttrPath
class _ActorStop(NamedTuple): # pyright: ignore[reportUnusedClass]
"""Internal message."""
class ProxyCall(NamedTuple):
"""Message to ask the actor to call the method with the arguments.
/// note | Version added: Pykka 2.0
///
"""
attr_path: AttrPath
"""List with the path from the actor to the method."""
args: tuple[Any, ...]
"""List with positional arguments."""
kwargs: dict[str, Any]
"""Dict with keyword arguments."""
class ProxyGetAttr(NamedTuple):
"""Message to ask the actor to return the value of the attribute.
/// note | Version added: Pykka 2.0
///
"""
attr_path: AttrPath
"""List with the path from the actor to the attribute."""
class ProxySetAttr(NamedTuple):
"""Message to ask the actor to set the attribute to the value.
/// note | Version added: Pykka 2.0
///
"""
attr_path: AttrPath
"""List with the path from the actor to the attribute."""
value: Any
"""The value to set the attribute to."""
pykka-4.5.0/src/pykka/py.typed 0000644 0000000 0000000 00000000000 15262536163 013171 0 ustar 00 pykka-4.5.0/src/pykka/typing.py 0000644 0000000 0000000 00000002726 15262536163 013377 0 ustar 00 """Type hint helpers."""
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Any,
Concatenate,
Generic,
ParamSpec,
Protocol,
TypeVar,
cast,
)
from pykka import Actor
if TYPE_CHECKING:
from collections.abc import Callable
from pykka import Future
__all__ = [
"ActorMemberMixin",
"proxy_field",
"proxy_method",
]
T = TypeVar("T")
P = ParamSpec("P")
R_co = TypeVar("R_co", covariant=True)
class Method(Protocol, Generic[P, R_co]):
def __get__(
self, instance: Any, owner: type | None = None
) -> Callable[P, R_co]: ...
def __call__(self, obj: Any, *args: P.args, **kwargs: P.kwargs) -> R_co: ...
def proxy_field(field: T) -> Future[T]:
"""Type a field on an actor proxy.
/// note | Version added: Pykka 4.0
///
"""
return cast("Future[T]", field)
def proxy_method(
field: Callable[Concatenate[Any, P], T],
) -> Method[P, Future[T]]:
"""Type a method on an actor proxy.
/// note | Version added: Pykka 4.0
///
"""
return cast("Method[P, Future[T]]", field)
class ActorMemberMixin:
"""Mixin class for typing Actor methods which are accessible via proxy instances.
/// note | Version added: Pykka 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)
pykka-4.5.0/tests/__init__.py 0000644 0000000 0000000 00000000000 15262536163 013037 0 ustar 00 pykka-4.5.0/tests/conftest.py 0000644 0000000 0000000 00000007426 15262536163 013150 0 ustar 00 from __future__ import annotations
import logging
import threading
import time
from typing import (
TYPE_CHECKING,
Any,
cast,
)
import pytest
from pykka import ActorRegistry, ThreadingActor, ThreadingFuture
from tests.log_handler import PykkaTestLogHandler
from tests.types import Events, Runtime
if TYPE_CHECKING:
from collections.abc import Iterator
from pykka import Actor, Future
RUNTIMES = {
"threading": pytest.param(
Runtime(
name="threading",
actor_class=ThreadingActor,
event_class=threading.Event,
future_class=ThreadingFuture,
sleep_func=time.sleep,
),
id="threading",
)
}
@pytest.fixture(scope="session", params=RUNTIMES.values())
def runtime(request: pytest.FixtureRequest) -> Runtime:
return cast("Runtime", request.param)
@pytest.fixture
def _stop_all() -> Iterator[None]: # pyright: ignore[reportUnusedFunction]
yield
ActorRegistry.stop_all()
@pytest.fixture
def log_handler() -> Iterator[logging.Handler]:
log_handler = PykkaTestLogHandler()
root_logger = logging.getLogger()
root_logger.addHandler(log_handler)
# pytest sets the root logger level to WARNING. We reset it to NOTSET
# so that all log messages reaches our log handler.
root_logger.setLevel(logging.NOTSET)
yield log_handler
log_handler.close()
@pytest.fixture
def events(runtime: Runtime) -> Events:
return Events(
on_start_was_called=runtime.event_class(),
on_stop_was_called=runtime.event_class(),
on_failure_was_called=runtime.event_class(),
greetings_was_received=runtime.event_class(),
actor_registered_before_on_start_was_called=runtime.event_class(),
)
@pytest.fixture(scope="module")
def early_failing_actor_class(runtime: Runtime) -> type[Actor]:
class EarlyFailingActor(runtime.actor_class): # type: ignore[name-defined]
def __init__(self, events: Events) -> None:
super().__init__()
self.events = events
def on_start(self) -> None:
try:
raise RuntimeError("on_start failure")
finally:
self.events.on_start_was_called.set()
return EarlyFailingActor
@pytest.fixture(scope="module")
def late_failing_actor_class(runtime: Runtime) -> type[Actor]:
class LateFailingActor(runtime.actor_class): # type: ignore[name-defined]
def __init__(self, events: Events) -> None:
super().__init__()
self.events = events
def on_start(self) -> None:
self.stop()
def on_stop(self) -> None:
try:
raise RuntimeError("on_stop failure")
finally:
self.events.on_stop_was_called.set()
return LateFailingActor
@pytest.fixture(scope="module")
def failing_on_failure_actor_class(runtime: Runtime) -> type[Actor]:
class FailingOnFailureActor(runtime.actor_class): # type: ignore[name-defined]
def __init__(self, events: Events) -> None:
super().__init__()
self.events = events
def on_receive(self, message: Any) -> Any:
if message.get("command") == "raise exception":
raise Exception("on_receive failure")
return super().on_receive(message)
def on_failure(self, *args: Any) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
try:
raise RuntimeError("on_failure failure")
finally:
self.events.on_failure_was_called.set()
return FailingOnFailureActor
@pytest.fixture
def future(runtime: Runtime) -> Future[Any]:
return runtime.future_class()
@pytest.fixture
def futures(runtime: Runtime) -> list[Future[Any]]:
return [runtime.future_class() for _ in range(3)]
pykka-4.5.0/tests/log_handler.py 0000644 0000000 0000000 00000003365 15262536163 013577 0 ustar 00 import collections
import logging
import threading
import time
from enum import StrEnum
from typing import Any
class LogLevel(StrEnum):
DEBUG = "debug"
INFO = "info"
WARNING = "warning"
ERROR = "error"
CRITICAL = "critical"
class PykkaTestLogHandler(logging.Handler):
lock: threading.RLock
events: dict[str, threading.Event]
messages: dict[LogLevel, list[logging.LogRecord]]
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.lock = ( # pyright: ignore[reportIncompatibleVariableOverride]
threading.RLock()
)
with self.lock:
self.events = collections.defaultdict(threading.Event)
self.messages = {}
self.reset()
logging.Handler.__init__(self, *args, **kwargs)
def emit(self, record: logging.LogRecord) -> None:
with self.lock:
level = LogLevel(record.levelname.lower())
self.messages[level].append(record)
self.events[level].set()
def reset(self) -> None:
with self.lock:
for level in LogLevel:
self.events[level].clear()
self.messages[level] = []
def wait_for_message(
self, level: LogLevel, num_messages: int = 1, timeout: float = 5
) -> None:
"""Wait until at least `num_messages` log messages have been emitted
to the given log level."""
deadline = time.time() + timeout
while time.time() < deadline:
with self.lock:
if len(self.messages[level]) >= num_messages:
return
self.events[level].clear()
self.events[level].wait(1)
msg = f"Timeout: Waited {timeout:d}s for log message"
raise Exception(msg)
pykka-4.5.0/tests/test_actor.py 0000644 0000000 0000000 00000021472 15262536163 013467 0 ustar 00 from __future__ import annotations
import uuid
from typing import TYPE_CHECKING, Any
import pytest
from pykka import Actor, ActorDeadError, ActorRegistry
if TYPE_CHECKING:
from collections.abc import Iterator
from types import TracebackType
from pykka import ActorRef
from tests.types import Events, Runtime
pytestmark = pytest.mark.usefixtures("_stop_all")
class AnActor(Actor):
def __init__(self, events: Events) -> None:
super().__init__()
self.events = events
def on_start(self) -> None:
self.events.on_start_was_called.set()
if ActorRegistry.get_by_urn(self.actor_urn) is not None:
self.events.actor_registered_before_on_start_was_called.set()
def on_stop(self) -> None:
self.events.on_stop_was_called.set()
def on_failure(
self,
exception_type: type[BaseException] | None,
exception_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
self.events.on_failure_was_called.set()
def on_receive(self, message: Any) -> None:
if message.get("command") == "raise exception":
raise Exception("foo")
if message.get("command") == "raise base exception":
raise BaseException
if message.get("command") == "stop twice":
self.stop()
self.stop()
elif message.get("command") == "message self then stop":
self.actor_ref.tell({"command": "greetings"})
self.stop()
elif message.get("command") == "greetings":
self.events.greetings_was_received.set()
elif message.get("command") == "callback":
message["callback"]()
else:
super().on_receive(message)
class EarlyStoppingActor(Actor):
def __init__(self, events: Events) -> None:
super().__init__()
self.events = events
def on_start(self) -> None:
self.stop()
def on_stop(self) -> None:
self.events.on_stop_was_called.set()
@pytest.fixture(scope="module")
def actor_class(runtime: Runtime) -> type[AnActor]:
class ActorAImpl(AnActor, runtime.actor_class): # type: ignore[name-defined]
pass
return ActorAImpl
@pytest.fixture
def actor_ref(
actor_class: type[AnActor],
events: Events,
) -> Iterator[ActorRef[AnActor]]:
ref = actor_class.start(events)
yield ref
ref.stop()
@pytest.fixture(scope="module")
def early_stopping_actor_class(runtime: Runtime) -> type[EarlyStoppingActor]:
class EarlyStoppingActorImpl(EarlyStoppingActor, runtime.actor_class): # type: ignore[name-defined]
pass
return EarlyStoppingActorImpl
def test_messages_left_in_queue_after_actor_stops_receive_an_error(
runtime: Runtime,
actor_ref: ActorRef[AnActor],
) -> None:
event = runtime.event_class()
actor_ref.tell({"command": "callback", "callback": event.wait})
actor_ref.stop(block=False)
response = actor_ref.ask({"command": "irrelevant"}, block=False)
event.set()
with pytest.raises(ActorDeadError):
response.get(timeout=0.5)
def test_stop_requests_left_in_queue_after_actor_stops_are_handled(
runtime: Runtime,
actor_ref: ActorRef[AnActor],
) -> None:
event = runtime.event_class()
actor_ref.tell({"command": "callback", "callback": event.wait})
actor_ref.stop(block=False)
response = actor_ref.stop(block=False)
event.set()
response.get(timeout=0.5)
def test_actor_has_an_uuid4_based_urn(actor_ref: ActorRef[AnActor]) -> None:
assert uuid.UUID(actor_ref.actor_urn).version == 4
def test_actor_has_unique_uuid(
actor_class: type[AnActor],
events: Events,
) -> None:
actors = [actor_class.start(events) for _ in range(3)]
assert actors[0].actor_urn != actors[1].actor_urn
assert actors[1].actor_urn != actors[2].actor_urn
assert actors[2].actor_urn != actors[0].actor_urn
def test_str_on_raw_actor_contains_actor_class_name(
actor_class: type[AnActor],
events: Events,
) -> None:
unstarted_actor = actor_class(events)
assert "ActorA" in str(unstarted_actor)
def test_str_on_raw_actor_contains_actor_urn(
actor_class: type[AnActor],
events: Events,
) -> None:
unstarted_actor = actor_class(events)
assert unstarted_actor.actor_urn in str(unstarted_actor)
def test_init_can_be_called_with_arbitrary_arguments(runtime: Runtime) -> None:
runtime.actor_class(1, 2, 3, foo="bar")
def test_on_start_is_called_before_first_message_is_processed(
actor_ref: ActorRef[AnActor],
events: Events,
) -> None:
events.on_start_was_called.wait(5)
assert events.on_start_was_called.is_set()
def test_on_start_is_called_after_the_actor_is_registered(
actor_ref: ActorRef[AnActor],
events: Events,
) -> None:
# NOTE: If the actor is registered after the actor is started, this
# test may still occasionally pass, as it is dependant on the exact
# timing of events. When the actor is first registered and then
# started, this test should always pass.
events.on_start_was_called.wait(5)
assert events.on_start_was_called.is_set()
events.actor_registered_before_on_start_was_called.wait(0.1)
assert events.actor_registered_before_on_start_was_called.is_set()
def test_on_start_can_stop_actor_before_receive_loop_is_started(
early_stopping_actor_class: type[AnActor],
events: Events,
) -> None:
# NOTE: This test will pass even if the actor is allowed to start the
# receive loop, but it will cause the test suite to hang, as the actor
# thread is blocking on receiving messages to the actor inbox forever.
# If one made this test specifically for ThreadingActor, one could add
# an assertFalse(actor_thread.is_alive()), which would cause the test
# to fail properly.
actor_ref = early_stopping_actor_class.start(events)
events.on_stop_was_called.wait(5)
assert events.on_stop_was_called.is_set()
assert not actor_ref.is_alive()
def test_on_start_failure_causes_actor_to_stop(
early_failing_actor_class: type[AnActor],
events: Events,
) -> None:
# Actor should not be alive if on_start fails.
actor_ref = early_failing_actor_class.start(events)
events.on_start_was_called.wait(5)
actor_ref.actor_stopped.wait(5)
assert not actor_ref.is_alive()
def test_on_stop_is_called_when_actor_is_stopped(
actor_ref: ActorRef[AnActor],
events: Events,
) -> None:
assert not events.on_stop_was_called.is_set()
actor_ref.stop()
events.on_stop_was_called.wait(5)
assert events.on_stop_was_called.is_set()
def test_on_stop_failure_causes_actor_to_stop(
late_failing_actor_class: type[AnActor],
events: Events,
) -> None:
actor_ref = late_failing_actor_class.start(events)
events.on_stop_was_called.wait(5)
assert not actor_ref.is_alive()
def test_on_failure_is_called_when_exception_cannot_be_returned(
actor_ref: ActorRef[AnActor],
events: Events,
) -> None:
assert not events.on_failure_was_called.is_set()
actor_ref.tell({"command": "raise exception"})
events.on_failure_was_called.wait(5)
assert events.on_failure_was_called.is_set()
assert not events.on_stop_was_called.is_set()
def test_on_failure_failure_causes_actor_to_stop(
failing_on_failure_actor_class: type[AnActor],
events: Events,
) -> None:
actor_ref = failing_on_failure_actor_class.start(events)
actor_ref.tell({"command": "raise exception"})
events.on_failure_was_called.wait(5)
assert not actor_ref.is_alive()
def test_actor_is_stopped_when_unhandled_exceptions_are_raised(
actor_ref: ActorRef[AnActor],
events: Events,
) -> None:
assert not events.on_failure_was_called.is_set()
actor_ref.tell({"command": "raise exception"})
events.on_failure_was_called.wait(5)
assert events.on_failure_was_called.is_set()
assert len(ActorRegistry.get_all()) == 0
def test_all_actors_are_stopped_on_base_exception(
actor_ref: ActorRef[AnActor],
events: Events,
) -> None:
assert len(ActorRegistry.get_all()) == 1
assert not events.on_stop_was_called.is_set()
actor_ref.tell({"command": "raise base exception"})
events.on_stop_was_called.wait(5)
assert events.on_stop_was_called.is_set()
assert len(ActorRegistry.get_all()) == 0
events.on_stop_was_called.wait(5)
assert events.on_stop_was_called.is_set()
assert len(ActorRegistry.get_all()) == 0
def test_actor_can_call_stop_on_self_multiple_times(
actor_ref: ActorRef[AnActor],
) -> None:
actor_ref.ask({"command": "stop twice"})
def test_actor_processes_all_messages_before_stop_on_self_stops_it(
actor_ref: ActorRef[AnActor],
events: Events,
) -> None:
actor_ref.ask({"command": "message self then stop"})
events.greetings_was_received.wait(5)
assert events.greetings_was_received.is_set()
events.on_stop_was_called.wait(5)
assert len(ActorRegistry.get_all()) == 0
pykka-4.5.0/tests/test_envelope.py 0000644 0000000 0000000 00000000355 15262536163 014171 0 ustar 00 from pykka import Future
from pykka._envelope import Envelope
def test_envelope_repr() -> None:
envelope = Envelope("message", reply_to=Future())
assert repr(envelope) == "Envelope(message='message', reply_to=)"
pykka-4.5.0/tests/test_future.py 0000644 0000000 0000000 00000023020 15262536163 013660 0 ustar 00 from __future__ import annotations
import asyncio
import queue
import sys
import traceback
import types
from typing import TYPE_CHECKING, Any
import pytest
from pykka import Future, Timeout, get_all
if TYPE_CHECKING:
from collections.abc import Generator, Iterable
from pytest_mock import MockerFixture
from tests.types import Runtime
def run_async(coroutine: Any) -> Any:
loop = asyncio.new_event_loop()
f = asyncio.ensure_future(coroutine, loop=loop)
return loop.run_until_complete(f)
def test_base_future_get_is_not_implemented() -> None:
future: Future[Any] = Future()
with pytest.raises(NotImplementedError):
future.get()
def test_base_future_set_is_not_implemented() -> None:
future: Future[Any] = Future()
with pytest.raises(NotImplementedError):
future.set(None)
def test_base_future_set_exception_is_not_implemented() -> None:
future: Future[Any] = Future()
with pytest.raises(NotImplementedError):
future.set_exception(None)
def test_set_multiple_times_fails(
future: Future[int],
) -> None:
future.set(0)
with pytest.raises(Exception): # noqa: B017, PT011
future.set(0)
def test_get_all_blocks_until_all_futures_are_available(
futures: list[Future[int]],
) -> None:
futures[0].set(0)
futures[1].set(1)
futures[2].set(2)
result = get_all(futures)
assert result == [0, 1, 2]
def test_get_all_raises_timeout_if_not_all_futures_are_available(
futures: list[Future[int]],
) -> None:
futures[0].set(0)
futures[1].set(1)
# futures[2] has not been set
with pytest.raises(Timeout):
get_all(futures, timeout=0)
def test_get_all_can_be_called_multiple_times(
futures: list[Future[int]],
) -> None:
futures[0].set(0)
futures[1].set(1)
futures[2].set(2)
result1 = get_all(futures)
result2 = get_all(futures)
assert result1 == result2
def test_future_in_future_works(runtime: Runtime) -> None:
inner_future = runtime.future_class()
inner_future.set("foo")
outer_future = runtime.future_class()
outer_future.set(inner_future)
assert outer_future.get().get() == "foo"
def test_get_raises_exception_with_full_traceback(runtime: Runtime) -> None:
exc_class_get = None
exc_class_set = None
exc_instance_get = None
exc_instance_set = None
exc_traceback_get = None
exc_traceback_set = None
future = runtime.future_class()
try:
raise NameError("foo") # noqa: TRY301
except NameError:
exc_class_set, exc_instance_set, exc_traceback_set = sys.exc_info()
future.set_exception()
# We could move to another thread at this point
try:
future.get()
except NameError:
exc_class_get, exc_instance_get, exc_traceback_get = sys.exc_info()
assert exc_class_set == exc_class_get
assert exc_instance_set == exc_instance_get
exc_traceback_list_set = list(reversed(traceback.extract_tb(exc_traceback_set)))
exc_traceback_list_get = list(reversed(traceback.extract_tb(exc_traceback_get)))
# All frames from the first traceback should be included in the
# traceback from the future.get() reraise
assert len(exc_traceback_list_set) < len(exc_traceback_list_get)
for i, frame in enumerate(exc_traceback_list_set):
assert frame == exc_traceback_list_get[i]
def test_future_supports_await_syntax(
future: Future[int],
) -> None:
async def get_value() -> int:
return await future
future.set(1)
assert run_async(get_value()) == 1
def test_future_supports_yield_from_syntax(
future: Future[int],
) -> None:
@types.coroutine
def get_value() -> Generator[None, None, int]:
val = yield from future
return val
future.set(1)
assert run_async(get_value()) == 1
def test_get_hook_is_only_called_once_even_if_result_is_none(
future: Future[int],
mocker: MockerFixture,
) -> None:
hook_func = mocker.Mock(return_value=None)
future.set_get_hook(hook_func)
assert future.get(timeout=0) is None
assert future.get(timeout=0) is None
assert hook_func.call_count == 1
def test_set_is_not_allowed_on_future_with_get_hook(
future: Future[int],
mocker: MockerFixture,
) -> None:
hook_func = mocker.Mock(return_value=123)
future.set_get_hook(hook_func)
with pytest.raises(queue.Full):
future.set(456)
assert future.get(timeout=0) == 123
assert hook_func.call_count == 1
def test_set_exception_is_not_allowed_on_future_with_get_hook(
future: Future[int],
mocker: MockerFixture,
) -> None:
hook_func = mocker.Mock(return_value=123)
future.set_get_hook(hook_func)
try:
raise RuntimeError("failure") # noqa: TRY301
except RuntimeError:
with pytest.raises(queue.Full):
future.set_exception()
assert future.get(timeout=0) == 123
assert hook_func.call_count == 1
def test_set_hook_is_not_allowed_on_future_with_result(
future: Future[int],
) -> None:
future.set(123)
with pytest.raises(queue.Full):
future.set_get_hook(lambda timeout: 456)
assert future.get(timeout=0) == 123
def test_filter_excludes_items_not_matching_predicate(
future: Future[Iterable[int]],
) -> None:
filtered = future.filter(lambda x: x > 10)
future.set([1, 3, 5, 7, 9, 11, 13, 15, 17, 19])
assert filtered.get(timeout=0) == [11, 13, 15, 17, 19]
def test_filter_on_noniterable(
future: Future[int],
) -> None:
filtered = future.filter(lambda x: x > 10) # type: ignore # noqa: PGH003
future.set(1)
with pytest.raises(TypeError):
filtered.get(timeout=0) # pyright: ignore[reportUnknownMemberType]
def test_filter_preserves_the_timeout_kwarg(
future: Future[Iterable[int]],
) -> None:
filtered = future.filter(lambda x: x > 10)
with pytest.raises(Timeout):
filtered.get(timeout=0)
def test_filter_reuses_result_if_called_multiple_times(
future: Future[Iterable[int]],
mocker: MockerFixture,
) -> None:
raise_on_reuse_func = mocker.Mock(side_effect=[False, True, Exception])
filtered = future.filter(raise_on_reuse_func)
future.set([1, 2])
assert filtered.get(timeout=0) == [2]
assert filtered.get(timeout=0) == [2] # First result is reused
assert filtered.get(timeout=0) == [2] # First result is reused
def test_join_combines_multiple_futures_into_one(
futures: list[Future[int]],
) -> None:
joined = futures[0].join(futures[1], futures[2])
futures[0].set(0)
futures[1].set(1)
futures[2].set(2)
assert joined.get(timeout=0) == [0, 1, 2]
def test_join_preserves_timeout_kwarg(
futures: list[Future[int]],
) -> None:
joined = futures[0].join(futures[1], futures[2])
futures[0].set(0)
futures[1].set(1)
# futures[2] has not been set
with pytest.raises(Timeout):
joined.get(timeout=0)
def test_map_returns_future_which_passes_result_through_func(
future: Future[int],
) -> None:
mapped = future.map(lambda x: x + 10)
future.set(30)
assert mapped.get(timeout=0) == 40
def test_map_works_on_dict(
future: Future[dict[str, str]],
) -> None:
# Regression test for issue #64
mapped = future.map(lambda x: x["foo"])
future.set({"foo": "bar"})
assert mapped.get(timeout=0) == "bar"
def test_map_does_not_map_each_value_in_futures_iterable_result(
future: Future[Iterable[int]],
) -> None:
# Behavior changed in Pykka 2.0:
# This used to map each value in the future's result through the func,
# yielding [20, 30, 40].
mapped = future.map(lambda x: x + 10) # type: ignore # noqa: PGH003
future.set([10, 20, 30])
with pytest.raises(TypeError):
mapped.get(timeout=0)
def test_map_preserves_timeout_kwarg(
future: Future[int],
) -> None:
mapped = future.map(lambda x: x + 10)
with pytest.raises(Timeout):
mapped.get(timeout=0)
def test_map_reuses_result_if_called_multiple_times(
future: Future[int],
mocker: MockerFixture,
) -> None:
raise_on_reuse_func = mocker.Mock(side_effect=[10, Exception])
mapped = future.map(raise_on_reuse_func)
future.set(30)
assert mapped.get(timeout=0) == 10
assert mapped.get(timeout=0) == 10 # First result is reused
def test_reduce_applies_function_cumulatively_from_the_left(
future: Future[Iterable[int]],
) -> None:
reduced: Future[int] = future.reduce(lambda x, y: x + y)
future.set([1, 2, 3, 4])
assert reduced.get(timeout=0) == 10
def test_reduce_accepts_an_initial_value(
future: Future[Iterable[int]],
) -> None:
reduced = future.reduce(lambda x, y: x + y, 5)
future.set([1, 2, 3, 4])
assert reduced.get(timeout=0) == 15
def test_reduce_on_noniterable(
future: Future[int],
) -> None:
reduced = future.reduce(lambda x, y: x + y) # type: ignore # noqa: PGH003
future.set(1)
with pytest.raises(TypeError):
reduced.get(timeout=0) # pyright: ignore[reportUnknownMemberType]
def test_reduce_preserves_the_timeout_kwarg(
future: Future[Iterable[int]],
) -> None:
reduced: Future[int] = future.reduce(lambda x, y: x + y)
with pytest.raises(Timeout):
reduced.get(timeout=0)
def test_reduce_reuses_result_if_called_multiple_times(
future: Future[Iterable[int]],
mocker: MockerFixture,
) -> None:
raise_on_reuse_func = mocker.Mock(side_effect=[3, 6, Exception])
reduced = future.reduce(raise_on_reuse_func)
future.set([1, 2, 3])
assert reduced.get(timeout=0) == 6
assert reduced.get(timeout=0) == 6 # First result is reused
assert reduced.get(timeout=0) == 6 # First result is reused
pykka-4.5.0/tests/test_logging.py 0000644 0000000 0000000 00000014106 15262536163 014001 0 ustar 00 from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, NoReturn
import pytest
from pykka import Actor
from tests.log_handler import LogLevel, PykkaTestLogHandler
if TYPE_CHECKING:
from collections.abc import Iterator
from types import TracebackType
from pykka import ActorRef
from tests.types import Events, Runtime
pytestmark = pytest.mark.usefixtures("_stop_all")
class LoggingActor(Actor):
def __init__(self, events: Events) -> None:
super().__init__()
self.events = events
def on_stop(self) -> None:
self.events.on_stop_was_called.set()
def on_failure(
self,
exception_type: type[BaseException] | None,
exception_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
self.events.on_failure_was_called.set()
def on_receive(self, message: Any) -> Any:
if message.get("command") == "raise exception":
return self.raise_exception()
if message.get("command") == "raise base exception":
raise BaseException
return super().on_receive(message)
def raise_exception(self) -> NoReturn:
raise Exception("foo")
@pytest.fixture(scope="module")
def actor_class(runtime: Runtime) -> type[LoggingActor]:
class LoggingActorImpl(LoggingActor, runtime.actor_class): # type: ignore[name-defined]
pass
return LoggingActorImpl
@pytest.fixture
def actor_ref(
actor_class: type[LoggingActor],
events: Events,
) -> Iterator[ActorRef[LoggingActor]]:
ref = actor_class.start(events)
yield ref
ref.stop()
def test_null_handler_is_added_to_avoid_warnings() -> None:
logger = logging.getLogger("pykka")
handler_names = [h.__class__.__name__ for h in logger.handlers]
assert "NullHandler" in handler_names
def test_unexpected_messages_are_logged(
actor_ref: ActorRef[LoggingActor],
log_handler: PykkaTestLogHandler,
) -> None:
actor_ref.ask({"unhandled": "message"})
log_handler.wait_for_message(LogLevel.WARNING)
with log_handler.lock:
assert len(log_handler.messages[LogLevel.WARNING]) == 1
log_record = log_handler.messages[LogLevel.WARNING][0]
assert log_record.getMessage().split(": ")[0] == (
f"Unexpected message received by {actor_ref}"
)
def test_exception_is_logged_when_returned_to_caller(
actor_ref: ActorRef[LoggingActor],
log_handler: PykkaTestLogHandler,
) -> None:
with pytest.raises(Exception, match="foo"):
actor_ref.proxy().raise_exception().get()
log_handler.wait_for_message(LogLevel.INFO)
with log_handler.lock:
assert len(log_handler.messages[LogLevel.INFO]) == 1
log_record = log_handler.messages[LogLevel.INFO][0]
assert log_record.getMessage() == (
f"Exception returned from {actor_ref} to caller:"
)
assert log_record.exc_info
assert log_record.exc_info[0] is Exception
assert str(log_record.exc_info[1]) == "foo"
def test_exception_is_logged_when_not_reply_requested(
actor_ref: ActorRef[LoggingActor],
events: Events,
log_handler: PykkaTestLogHandler,
) -> None:
events.on_failure_was_called.clear()
actor_ref.tell({"command": "raise exception"})
events.on_failure_was_called.wait(5)
assert events.on_failure_was_called.is_set()
log_handler.wait_for_message(LogLevel.ERROR)
with log_handler.lock:
assert len(log_handler.messages[LogLevel.ERROR]) == 1
log_record = log_handler.messages[LogLevel.ERROR][0]
assert log_record.getMessage() == f"Unhandled exception in {actor_ref}:"
assert log_record.exc_info
assert log_record.exc_info[0] is Exception
assert str(log_record.exc_info[1]) == "foo"
def test_base_exception_is_logged(
actor_ref: ActorRef[LoggingActor],
events: Events,
log_handler: PykkaTestLogHandler,
) -> None:
log_handler.reset()
events.on_stop_was_called.clear()
actor_ref.tell({"command": "raise base exception"})
events.on_stop_was_called.wait(5)
assert events.on_stop_was_called.is_set()
log_handler.wait_for_message(LogLevel.DEBUG, num_messages=3)
with log_handler.lock:
assert len(log_handler.messages[LogLevel.DEBUG]) == 3
log_record = log_handler.messages[LogLevel.DEBUG][0]
assert log_record.getMessage() == (
f"BaseException() in {actor_ref}. Stopping all actors."
)
def test_exception_in_on_start_is_logged(
early_failing_actor_class: type[LoggingActor],
events: Events,
log_handler: PykkaTestLogHandler,
) -> None:
log_handler.reset()
actor_ref = early_failing_actor_class.start(events)
events.on_start_was_called.wait(5)
log_handler.wait_for_message(LogLevel.ERROR)
with log_handler.lock:
assert len(log_handler.messages[LogLevel.ERROR]) == 1
log_record = log_handler.messages[LogLevel.ERROR][0]
assert log_record.getMessage() == f"Unhandled exception in {actor_ref}:"
def test_exception_in_on_stop_is_logged(
late_failing_actor_class: type[LoggingActor],
events: Events,
log_handler: PykkaTestLogHandler,
) -> None:
log_handler.reset()
actor_ref = late_failing_actor_class.start(events)
events.on_stop_was_called.wait(5)
log_handler.wait_for_message(LogLevel.ERROR)
with log_handler.lock:
assert len(log_handler.messages[LogLevel.ERROR]) == 1
log_record = log_handler.messages[LogLevel.ERROR][0]
assert log_record.getMessage() == f"Unhandled exception in {actor_ref}:"
def test_exception_in_on_failure_is_logged(
failing_on_failure_actor_class: type[LoggingActor],
events: Events,
log_handler: PykkaTestLogHandler,
) -> None:
log_handler.reset()
actor_ref = failing_on_failure_actor_class.start(events)
actor_ref.tell({"command": "raise exception"})
events.on_failure_was_called.wait(5)
log_handler.wait_for_message(LogLevel.ERROR, num_messages=2)
with log_handler.lock:
assert len(log_handler.messages[LogLevel.ERROR]) == 2
log_record = log_handler.messages[LogLevel.ERROR][0]
assert log_record.getMessage() == f"Unhandled exception in {actor_ref}:"
pykka-4.5.0/tests/test_messages.py 0000644 0000000 0000000 00000001607 15262536163 014164 0 ustar 00 from pykka.messages import ProxyCall, ProxyGetAttr, ProxySetAttr, _ActorStop
def test_actor_stop() -> None:
message = _ActorStop()
assert isinstance(message, _ActorStop)
def test_proxy_call() -> None:
message = ProxyCall(attr_path=("nested", "method"), args=(1,), kwargs={"a": "b"})
assert isinstance(message, ProxyCall)
assert message.attr_path == ("nested", "method")
assert message.args == (1,)
assert message.kwargs == {"a": "b"}
def test_proxy_get_attr() -> None:
message = ProxyGetAttr(attr_path=("nested", "attr"))
assert isinstance(message, ProxyGetAttr)
assert message.attr_path == ("nested", "attr")
def test_proxy_set_attr() -> None:
message = ProxySetAttr(attr_path=("nested", "attr"), value="abcdef")
assert isinstance(message, ProxySetAttr)
assert message.attr_path == ("nested", "attr")
assert message.value == "abcdef"
pykka-4.5.0/tests/test_ref.py 0000644 0000000 0000000 00000011643 15262536163 013132 0 ustar 00 from __future__ import annotations
from typing import TYPE_CHECKING, Any
import pytest
from pykka import Actor, ActorDeadError, Timeout
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
from pykka import ActorRef, Future
from tests.types import Runtime
class ReferencableActor(Actor):
received_messages = None
def __init__(
self,
sleep_func: Callable[[float], None],
received_message: Future[str],
) -> None:
super().__init__()
self.sleep_func = sleep_func
self.received_message = received_message
def on_receive(self, message: str) -> Any:
if message == "ping":
self.sleep_func(0.01)
return "pong"
self.received_message.set(message)
return None
@pytest.fixture(scope="module")
def actor_class(runtime: Runtime) -> type[ReferencableActor]:
class ReferencableActorImpl(ReferencableActor, runtime.actor_class): # type: ignore[name-defined]
pass
return ReferencableActorImpl
@pytest.fixture
def received_message(runtime: Runtime) -> Future[str]:
return runtime.future_class()
@pytest.fixture
def actor_ref(
runtime: Runtime,
actor_class: type[ReferencableActor],
received_message: Future[str],
) -> Iterator[ActorRef[ReferencableActor]]:
ref = actor_class.start(
runtime.sleep_func,
received_message,
)
yield ref
ref.stop()
def test_repr_is_wrapped_in_lt_and_gt(
actor_ref: ActorRef[ReferencableActor],
) -> None:
result = repr(actor_ref)
assert result.startswith("<")
assert result.endswith(">")
def test_repr_reveals_that_this_is_a_ref(
actor_ref: ActorRef[ReferencableActor],
) -> None:
assert "ActorRef" in repr(actor_ref)
def test_repr_contains_actor_class_name(
actor_ref: ActorRef[ReferencableActor],
) -> None:
assert "ReferencableActor" in repr(actor_ref)
def test_repr_contains_actor_urn(
actor_ref: ActorRef[ReferencableActor],
) -> None:
assert actor_ref.actor_urn in repr(actor_ref)
def test_str_contains_actor_class_name(
actor_ref: ActorRef[ReferencableActor],
) -> None:
assert "ReferencableActor" in str(actor_ref)
def test_str_contains_actor_urn(
actor_ref: ActorRef[ReferencableActor],
) -> None:
assert actor_ref.actor_urn in str(actor_ref)
def test_is_alive_returns_true_for_running_actor(
actor_ref: ActorRef[ReferencableActor],
) -> None:
assert actor_ref.is_alive()
def test_is_alive_returns_false_for_dead_actor(
actor_ref: ActorRef[ReferencableActor],
) -> None:
actor_ref.stop()
assert not actor_ref.is_alive()
def test_stop_returns_true_if_actor_is_stopped(
actor_ref: ActorRef[ReferencableActor],
) -> None:
assert actor_ref.stop()
def test_stop_does_not_stop_already_dead_actor(
actor_ref: ActorRef[ReferencableActor],
) -> None:
assert actor_ref.stop()
assert not actor_ref.stop()
def test_tell_delivers_message_to_actors_custom_on_receive(
actor_ref: ActorRef[ReferencableActor],
received_message: Future[str],
) -> None:
actor_ref.tell("a custom message")
assert received_message.get(timeout=1) == "a custom message"
@pytest.mark.parametrize(
"message",
[
123,
123.456,
{"a": "dict"},
("a", "tuple"),
["a", "list"],
Exception("an exception"),
],
)
def test_tell_accepts_any_object_as_the_message(
actor_ref: ActorRef[ReferencableActor],
message: Any,
received_message: Future[Any],
) -> None:
actor_ref.tell(message)
assert received_message.get(timeout=1) == message
def test_tell_fails_if_actor_is_stopped(
actor_ref: ActorRef[ReferencableActor],
) -> None:
actor_ref.stop()
with pytest.raises(ActorDeadError) as exc_info:
actor_ref.tell("a custom message")
assert str(exc_info.value) == f"{actor_ref} not found"
def test_ask_blocks_until_response_arrives(
actor_ref: ActorRef[ReferencableActor],
) -> None:
result = actor_ref.ask("ping")
assert result == "pong"
def test_ask_can_timeout_if_blocked_too_long(
actor_ref: ActorRef[ReferencableActor],
) -> None:
with pytest.raises(Timeout):
actor_ref.ask("ping", timeout=0)
def test_ask_can_return_future_instead_of_blocking(
actor_ref: ActorRef[ReferencableActor],
) -> None:
future = actor_ref.ask("ping", block=False)
assert future.get() == "pong"
def test_ask_fails_if_actor_is_stopped(
actor_ref: ActorRef[ReferencableActor],
) -> None:
actor_ref.stop()
with pytest.raises(ActorDeadError) as exc_info:
actor_ref.ask("ping")
assert str(exc_info.value) == f"{actor_ref} not found"
def test_ask_nonblocking_fails_future_if_actor_is_stopped(
actor_ref: ActorRef[ReferencableActor],
) -> None:
actor_ref.stop()
future = actor_ref.ask("ping", block=False)
with pytest.raises(ActorDeadError) as exc_info:
future.get()
assert str(exc_info.value) == f"{actor_ref} not found"
pykka-4.5.0/tests/test_registry.py 0000644 0000000 0000000 00000016607 15262536163 014233 0 ustar 00 from __future__ import annotations
from typing import TYPE_CHECKING, Any
import pytest
from pykka import Actor, ActorRegistry
if TYPE_CHECKING:
from pytest_mock import MockerFixture
from pykka import ActorRef
from tests.types import Runtime
pytestmark = pytest.mark.usefixtures("_stop_all")
class ActorBase(Actor):
received_messages: list[Any]
def __init__(self) -> None:
super().__init__()
self.received_messages = []
def on_receive(self, message: Any) -> None:
self.received_messages.append(message)
class ActorA(ActorBase):
pass
class ActorB(ActorBase):
pass
@pytest.fixture(scope="module")
def actor_a_class(runtime: Runtime) -> type[ActorA]:
class ActorAImpl(ActorA, runtime.actor_class): # type: ignore[name-defined]
pass
return ActorAImpl
@pytest.fixture(scope="module")
def actor_b_class(runtime: Runtime) -> type[ActorB]:
class ActorBImpl(ActorB, runtime.actor_class): # type: ignore[name-defined]
pass
return ActorBImpl
@pytest.fixture
def actor_ref(actor_a_class: type[ActorA]) -> ActorRef[ActorA]:
return actor_a_class.start()
@pytest.fixture
def a_actor_refs(actor_a_class: type[ActorA]) -> list[ActorRef[ActorA]]:
return [actor_a_class.start() for _ in range(3)]
@pytest.fixture
def b_actor_refs(actor_b_class: type[ActorB]) -> list[ActorRef[ActorB]]:
return [actor_b_class.start() for _ in range(5)]
def test_actor_is_registered_when_started(
actor_ref: ActorRef[ActorA],
) -> None:
assert actor_ref in ActorRegistry.get_all()
def test_actor_is_unregistered_when_stopped(
actor_ref: ActorRef[ActorA],
) -> None:
assert actor_ref in ActorRegistry.get_all()
actor_ref.stop()
assert actor_ref not in ActorRegistry.get_all()
def test_actor_may_be_registered_manually(
actor_ref: ActorRef[ActorA],
) -> None:
ActorRegistry.unregister(actor_ref)
assert actor_ref not in ActorRegistry.get_all()
ActorRegistry.register(actor_ref)
assert actor_ref in ActorRegistry.get_all()
def test_actor_may_be_unregistered_multiple_times_without_error(
actor_ref: ActorRef[ActorA],
) -> None:
ActorRegistry.unregister(actor_ref)
assert actor_ref not in ActorRegistry.get_all()
ActorRegistry.unregister(actor_ref)
assert actor_ref not in ActorRegistry.get_all()
ActorRegistry.register(actor_ref)
assert actor_ref in ActorRegistry.get_all()
def test_all_actors_can_be_stopped_through_registry(
a_actor_refs: list[ActorRef[ActorA]],
b_actor_refs: list[ActorRef[ActorB]],
) -> None:
assert len(ActorRegistry.get_all()) == 8
ActorRegistry.stop_all(block=True)
assert len(ActorRegistry.get_all()) == 0
def test_stop_all_stops_last_started_actor_first_if_blocking(
mocker: MockerFixture,
) -> None:
mocker.patch.object(ActorRegistry, "get_all")
stopped_actors = []
started_actors = [mocker.Mock(name=f"{i}") for i in range(3)]
started_actors[0].stop.side_effect = lambda *a, **kw: stopped_actors.append( # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType]
started_actors[0]
)
started_actors[1].stop.side_effect = lambda *a, **kw: stopped_actors.append( # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType]
started_actors[1]
)
started_actors[2].stop.side_effect = lambda *a, **kw: stopped_actors.append( # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType]
started_actors[2]
)
ActorRegistry.get_all.return_value = ( # type: ignore[attr-defined] # pyright: ignore[reportFunctionMemberAccess] # ty: ignore[unresolved-attribute]
started_actors
)
ActorRegistry.stop_all(block=True)
assert stopped_actors[0] == started_actors[2]
assert stopped_actors[1] == started_actors[1]
assert stopped_actors[2] == started_actors[0]
def test_actors_may_be_looked_up_by_class(
actor_a_class: type[ActorA],
a_actor_refs: list[ActorRef[ActorA]],
b_actor_refs: list[ActorRef[ActorB]],
) -> None:
result = ActorRegistry.get_by_class(actor_a_class)
for a_actor in a_actor_refs:
assert a_actor in result
for b_actor in b_actor_refs:
assert b_actor not in result # type: ignore[comparison-overlap]
def test_actors_may_be_looked_up_by_superclass(
actor_a_class: type[ActorA],
a_actor_refs: list[ActorRef[ActorA]],
b_actor_refs: list[ActorRef[ActorB]],
) -> None:
result = ActorRegistry.get_by_class(actor_a_class)
for a_actor in a_actor_refs:
assert a_actor in result
for b_actor in b_actor_refs:
assert b_actor not in result # type: ignore[comparison-overlap]
def test_actors_may_be_looked_up_by_class_name(
actor_a_class: type[ActorA],
a_actor_refs: list[ActorRef[ActorA]],
b_actor_refs: list[ActorRef[ActorB]],
) -> None:
result = ActorRegistry.get_by_class_name("ActorAImpl")
for a_actor in a_actor_refs:
assert a_actor in result
for b_actor in b_actor_refs:
assert b_actor not in result
def test_actors_may_be_looked_up_by_urn(
actor_ref: ActorRef[ActorA],
) -> None:
result = ActorRegistry.get_by_urn(actor_ref.actor_urn)
assert result == actor_ref
def test_get_by_urn_returns_none_if_not_found() -> None:
result = ActorRegistry.get_by_urn("urn:foo:bar")
assert result is None
def test_broadcast_sends_message_to_all_actors_if_no_target(
a_actor_refs: list[ActorRef[ActorA]],
b_actor_refs: list[ActorRef[ActorB]],
) -> None:
ActorRegistry.broadcast({"command": "foo"})
running_actors = ActorRegistry.get_all()
assert running_actors
for actor_ref in running_actors:
received_messages = actor_ref.proxy().received_messages.get()
assert {"command": "foo"} in received_messages
def test_broadcast_sends_message_to_all_actors_of_given_class(
actor_a_class: type[ActorA],
actor_b_class: type[ActorB],
a_actor_refs: list[ActorRef[ActorA]],
b_actor_refs: list[ActorRef[ActorB]],
) -> None:
ActorRegistry.broadcast({"command": "foo"}, target_class=actor_a_class)
class_a_refs = ActorRegistry.get_by_class(actor_a_class)
assert set(class_a_refs) == set(a_actor_refs)
for actor_a_ref in class_a_refs:
received_messages = actor_a_ref.proxy().received_messages.get()
assert {"command": "foo"} in received_messages
class_b_refs = ActorRegistry.get_by_class(actor_b_class)
assert set(class_b_refs) == set(b_actor_refs)
for actor_b_ref in class_b_refs:
received_messages = actor_b_ref.proxy().received_messages.get()
assert {"command": "foo"} not in received_messages
def test_broadcast_sends_message_to_all_actors_of_given_class_name(
actor_a_class: type[ActorA],
actor_b_class: type[ActorB],
a_actor_refs: list[ActorRef[ActorA]],
b_actor_refs: list[ActorRef[ActorB]],
) -> None:
ActorRegistry.broadcast({"command": "foo"}, target_class="ActorAImpl")
class_a_refs = ActorRegistry.get_by_class_name("ActorAImpl")
assert set(class_a_refs) == set(a_actor_refs)
for actor_a_ref in class_a_refs:
received_messages = actor_a_ref.proxy().received_messages.get()
assert {"command": "foo"} in received_messages
class_b_refs = ActorRegistry.get_by_class_name("ActorBImpl")
assert set(class_b_refs) == set(b_actor_refs)
for actor_b_ref in class_b_refs:
received_messages = actor_b_ref.proxy().received_messages.get()
assert {"command": "foo"} not in received_messages
pykka-4.5.0/tests/test_threading_actor.py 0000644 0000000 0000000 00000003104 15262536163 015504 0 ustar 00 from __future__ import annotations
import threading
from typing import TYPE_CHECKING
import pytest
from pykka import ThreadingActor
if TYPE_CHECKING:
from collections.abc import Iterator
from pykka import ActorRef
class RegularActor(ThreadingActor):
pass
class DaemonActor(ThreadingActor):
use_daemon_thread = True
@pytest.fixture
def regular_actor_ref() -> Iterator[ActorRef[RegularActor]]:
ref = RegularActor.start()
yield ref
ref.stop()
@pytest.fixture
def daemon_actor_ref() -> Iterator[ActorRef[DaemonActor]]:
ref = DaemonActor.start()
yield ref
ref.stop()
def test_actor_thread_is_named_after_pykka_actor_class(
regular_actor_ref: ActorRef[RegularActor],
) -> None:
alive_threads = threading.enumerate()
alive_thread_names = [t.name for t in alive_threads]
named_correctly = [
name.startswith(RegularActor.__name__) for name in alive_thread_names
]
assert any(named_correctly)
def test_actor_thread_is_not_daemonic_by_default(
regular_actor_ref: ActorRef[RegularActor],
) -> None:
alive_threads = threading.enumerate()
actor_threads = [t for t in alive_threads if t.name.startswith("RegularActor")]
assert len(actor_threads) == 1
assert not actor_threads[0].daemon
def test_actor_thread_is_daemonic_if_use_daemon_thread_flag_is_set(
daemon_actor_ref: ActorRef[DaemonActor],
) -> None:
alive_threads = threading.enumerate()
actor_threads = [t for t in alive_threads if t.name.startswith("DaemonActor")]
assert len(actor_threads) == 1
assert actor_threads[0].daemon
pykka-4.5.0/tests/types.py 0000644 0000000 0000000 00000001425 15262536163 012460 0 ustar 00 from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol
if TYPE_CHECKING:
from collections.abc import Callable
from pykka import Actor, Future
class Event(Protocol):
def clear(self) -> None: ...
def is_set(self) -> bool: ...
def set(self) -> None: ...
def wait(self, timeout: float | None = None) -> bool: ...
@dataclass
class Events:
on_start_was_called: Event
on_stop_was_called: Event
on_failure_was_called: Event
greetings_was_received: Event
actor_registered_before_on_start_was_called: Event
@dataclass
class Runtime:
name: str
actor_class: type[Actor]
event_class: type[Event]
future_class: type[Future[Any]]
sleep_func: Callable[[float], None]
pykka-4.5.0/tests/proxy/__init__.py 0000644 0000000 0000000 00000000000 15262536163 014220 0 ustar 00 pykka-4.5.0/tests/proxy/test_attribute_access.py 0000644 0000000 0000000 00000006306 15262536163 017063 0 ustar 00 from __future__ import annotations
from typing import TYPE_CHECKING, NoReturn
import pytest
from pykka import Actor
if TYPE_CHECKING:
from collections.abc import Iterator
from pykka import ActorProxy
from tests.types import Runtime
class PropertyActor(Actor):
an_attr = "an_attr"
_private_attr = "secret"
@property
def a_ro_property(self) -> str:
return "a_ro_property"
_a_rw_property = "a_rw_property"
@property
def a_rw_property(self) -> str:
return self._a_rw_property
@a_rw_property.setter
def a_rw_property(self, value: str) -> None:
self._a_rw_property = value
@pytest.fixture
def actor_class(runtime: Runtime) -> type[PropertyActor]:
class PropertyActorImpl(PropertyActor, runtime.actor_class): # type: ignore[name-defined]
pass
return PropertyActorImpl
@pytest.fixture
def proxy(
actor_class: type[PropertyActor],
) -> Iterator[ActorProxy[PropertyActor]]:
proxy = actor_class.start().proxy()
yield proxy
proxy.stop()
def test_attr_can_be_read_using_get_postfix(
proxy: ActorProxy[PropertyActor],
) -> None:
assert proxy.an_attr.get() == "an_attr"
def test_attr_can_be_set_using_assignment(
proxy: ActorProxy[PropertyActor],
) -> None:
assert proxy.an_attr.get() == "an_attr"
proxy.an_attr = "an_attr_2"
assert proxy.an_attr.get() == "an_attr_2" # pyright: ignore[reportUnknownMemberType,reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
def test_private_attr_access_raises_exception(
proxy: ActorProxy[PropertyActor],
) -> None:
with pytest.raises(AttributeError) as exc_info:
proxy._private_attr.get() # pyright: ignore[reportUnknownMemberType] # noqa: SLF001
assert "has no attribute '_private_attr'" in str(exc_info.value)
def test_missing_attr_access_raises_exception(
proxy: ActorProxy[PropertyActor],
) -> None:
with pytest.raises(AttributeError) as exc_info:
proxy.missing_attr.get()
assert "has no attribute 'missing_attr'" in str(exc_info.value)
def test_property_can_be_read_using_get_postfix(
proxy: ActorProxy[PropertyActor],
) -> None:
assert proxy.a_ro_property.get() == "a_ro_property"
assert proxy.a_rw_property.get() == "a_rw_property"
def test_property_can_be_set_using_assignment(
proxy: ActorProxy[PropertyActor],
) -> None:
proxy.a_rw_property = "a_rw_property_2"
assert proxy.a_rw_property.get() == "a_rw_property_2" # pyright: ignore[reportUnknownMemberType,reportAttributeAccessIssue] # ty: ignore[unresolved-attribute]
def test_read_only_property_cannot_be_set(
proxy: ActorProxy[PropertyActor],
) -> None:
with pytest.raises(AttributeError):
proxy.a_ro_property = "a_ro_property_2"
def test_property_is_not_accessed_when_creating_proxy(runtime: Runtime) -> None:
class ExpensiveSideEffectActor(runtime.actor_class): # type: ignore[name-defined]
@property
def a_property(self) -> NoReturn:
# Imagine code with side effects or heavy resource usage here
raise Exception("Proxy creation accessed property")
actor_ref = ExpensiveSideEffectActor.start()
try:
actor_ref.proxy()
finally:
actor_ref.stop()
pykka-4.5.0/tests/proxy/test_dynamic_method_calls.py 0000644 0000000 0000000 00000003465 15262536163 017704 0 ustar 00 from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from pykka import Actor
if TYPE_CHECKING:
from collections.abc import Iterator
from pykka import ActorProxy, Future
from tests.types import Runtime
class DynamicMethodActor(Actor):
def add_method(self, name: str) -> None:
setattr(self, name, lambda: "returned by " + name)
def use_foo_through_self_proxy(self) -> Future[str]:
return self.actor_ref.proxy().foo() # type: ignore[no-any-return]
@pytest.fixture(scope="module")
def actor_class(runtime: Runtime) -> type[DynamicMethodActor]:
class DynamicMethodActorImpl(DynamicMethodActor, runtime.actor_class): # type: ignore[name-defined]
pass
return DynamicMethodActorImpl
@pytest.fixture
def proxy(
actor_class: type[DynamicMethodActor],
) -> Iterator[ActorProxy[DynamicMethodActor]]:
proxy = actor_class.start().proxy()
yield proxy
proxy.stop()
def test_can_call_method_that_was_added_at_runtime(
proxy: ActorProxy[DynamicMethodActor],
) -> None:
# We need to .get() after .add_method() to be sure that the method has
# been added before we try to use it through the proxy.
proxy.add_method("foo").get()
assert proxy.foo().get() == "returned by foo"
def test_can_proxy_itself_and_use_attrs_added_at_runtime(
proxy: ActorProxy[DynamicMethodActor],
) -> None:
# We don't need to .get() after .add_method() here, because the actor
# will process the .add_method() call before processing the
# .use_foo_through_self_proxy() call, which again will use the new
# method, .foo().
proxy.add_method("foo")
outer_future = proxy.use_foo_through_self_proxy()
inner_future = outer_future.get(timeout=1)
result = inner_future.get(timeout=1)
assert result == "returned by foo"
pykka-4.5.0/tests/proxy/test_mocking.py 0000644 0000000 0000000 00000005465 15262536163 015173 0 ustar 00 from __future__ import annotations
from typing import TYPE_CHECKING, NoReturn
import pytest
from pykka import Actor
if TYPE_CHECKING:
from collections.abc import Iterator
from pytest_mock import MockerFixture
from pykka import ActorProxy
from tests.types import Runtime
pytestmark = pytest.mark.usefixtures("_stop_all")
class ActorForMocking(Actor):
_a_rw_property: str = "a_rw_property"
@property
def a_rw_property(self) -> str:
return self._a_rw_property
@a_rw_property.setter
def a_rw_property(self, value: str) -> None:
self._a_rw_property = value
def a_method(self) -> NoReturn:
raise Exception("This method should be mocked")
@pytest.fixture
def actor_class(runtime: Runtime) -> type[ActorForMocking]:
class ActorForMockingImpl(ActorForMocking, runtime.actor_class): # type: ignore[name-defined]
pass
return ActorForMockingImpl
@pytest.fixture
def proxy(
actor_class: ActorForMocking,
) -> Iterator[ActorProxy[ActorForMocking]]:
proxy = actor_class.start().proxy()
yield proxy
proxy.stop()
def test_actor_with_noncallable_mock_property_works(
actor_class: type[ActorForMocking],
mocker: MockerFixture,
) -> None:
mock = mocker.NonCallableMock()
mock.__get__ = mocker.Mock(return_value="mocked property value")
assert not callable(mock)
actor_class.a_rw_property = mock # type: ignore[method-assign]
proxy = actor_class.start().proxy()
# When using NonCallableMock to fake the property, the value still behaves
# as a property when access through the proxy.
assert proxy.a_rw_property.get() == "mocked property value"
assert mock.__get__.call_count == 1
def test_actor_with_callable_mock_property_does_not_work(
actor_class: type[ActorForMocking],
mocker: MockerFixture,
) -> None:
mock = mocker.Mock()
mock.__get__ = mocker.Mock(return_value="mocked property value")
assert callable(mock)
actor_class.a_rw_property = mock # type: ignore[method-assign]
proxy = actor_class.start().proxy()
# Because Mock and MagicMock are callable by default, they cause the
# property to be wrapped in a `CallableProxy`. Thus, the property no
# longer behaves as a property when mocked and accessed through a proxy.
with pytest.raises(AttributeError) as exc_info:
assert proxy.a_rw_property.get()
assert "'CallableProxy' object has no attribute 'get'" in str(exc_info.value)
def test_actor_with_mocked_method_works(
actor_class: type[ActorForMocking],
mocker: MockerFixture,
) -> None:
mock = mocker.MagicMock(return_value="mocked method return")
mocker.patch.object(actor_class, "a_method", new=mock)
proxy = actor_class.start().proxy()
assert proxy.a_method().get() == "mocked method return"
assert mock.call_count == 1
pykka-4.5.0/tests/proxy/test_proxy.py 0000644 0000000 0000000 00000012204 15262536163 014712 0 ustar 00 from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
import pykka
from pykka import Actor, ActorDeadError, ActorProxy
from tests.log_handler import LogLevel, PykkaTestLogHandler
if TYPE_CHECKING:
from collections.abc import Iterator
from tests.types import Runtime
class NestedObject:
pass
class ObjectWithEqImpl: # noqa: PLW1641
foo: str = "bar"
def __eq__(self, other: object) -> bool:
raise Exception("This is a broken __eq__ implementation")
class ActorForProxying(Actor):
a_nested_object = pykka.traversable(NestedObject())
a_class_attr = "class_attr"
# Include an object with a broken __eq__() implementation to test that it
# doesn't break introspection.
object_with_eq_impl = ObjectWithEqImpl()
def __init__(self) -> None:
super().__init__()
self.an_instance_attr = "an_instance_attr"
def a_method(self) -> None:
pass
@pytest.fixture(scope="module")
def actor_class(runtime: Runtime) -> type[ActorForProxying]:
class ActorForProxyingImpl(ActorForProxying, runtime.actor_class): # type: ignore[name-defined]
pass
return ActorForProxyingImpl
@pytest.fixture
def proxy(
actor_class: type[ActorForProxying],
) -> Iterator[ActorProxy[ActorForProxying]]:
proxy = ActorProxy(actor_ref=actor_class.start())
yield proxy
proxy.stop()
def test_eq_to_self(proxy: ActorProxy[ActorForProxying]) -> None:
assert proxy == proxy # noqa: PLR0124
def test_is_hashable(proxy: ActorProxy[ActorForProxying]) -> None:
assert hash(proxy) == hash(proxy)
def test_eq_to_another_proxy_for_same_actor_and_attr_path(
proxy: ActorProxy[ActorForProxying],
) -> None:
proxy2 = proxy.actor_ref.proxy()
assert proxy == proxy2
def test_not_eq_to_proxy_with_different_attr_path(
proxy: ActorProxy[ActorForProxying],
) -> None:
assert proxy != proxy.a_nested_object
def test_repr_is_wrapped_in_lt_and_gt(proxy: ActorProxy[ActorForProxying]) -> None:
result = repr(proxy)
assert result.startswith("<")
assert result.endswith(">")
def test_repr_reveals_that_this_is_a_proxy(proxy: ActorProxy[ActorForProxying]) -> None:
assert "ActorProxy" in repr(proxy)
def test_repr_contains_actor_class_name(proxy: ActorProxy[ActorForProxying]) -> None:
assert "ActorForProxying" in repr(proxy)
def test_repr_contains_actor_urn(proxy: ActorProxy[ActorForProxying]) -> None:
assert proxy.actor_ref.actor_urn in repr(proxy)
def test_repr_contains_attr_path(proxy: ActorProxy[ActorForProxying]) -> None:
assert "a_nested_object" in repr(proxy.a_nested_object)
def test_str_contains_actor_class_name(proxy: ActorProxy[ActorForProxying]) -> None:
assert "ActorForProxying" in str(proxy)
def test_str_contains_actor_urn(proxy: ActorProxy[ActorForProxying]) -> None:
assert proxy.actor_ref.actor_urn in str(proxy)
def test_dir_on_proxy_lists_attributes_of_the_actor(
proxy: ActorProxy[ActorForProxying],
) -> None:
result = dir(proxy)
assert "a_class_attr" in result
assert "an_instance_attr" in result
assert "a_method" in result
assert "object_with_eq_impl" in result
def test_dir_on_proxy_lists_private_attributes_of_the_proxy(
proxy: ActorProxy[ActorForProxying],
) -> None:
result = dir(proxy)
assert "__class__" in result
assert "__dict__" in result
assert "__getattr__" in result
assert "__setattr__" in result
def test_refs_proxy_method_returns_a_proxy(
actor_class: type[ActorForProxying],
) -> None:
proxy_from_ref_proxy = actor_class.start().proxy()
assert isinstance(proxy_from_ref_proxy, ActorProxy)
proxy_from_ref_proxy.stop().get()
def test_proxy_constructor_raises_exception_if_actor_is_dead(
actor_class: type[Actor],
) -> None:
actor_ref = actor_class.start()
actor_ref.stop()
with pytest.raises(ActorDeadError) as exc_info:
ActorProxy(actor_ref=actor_ref)
assert str(exc_info.value) == f"{actor_ref} not found"
def test_actor_ref_may_be_retrieved_from_proxy_if_actor_is_dead(
proxy: ActorProxy[ActorForProxying],
) -> None:
proxy.actor_ref.stop()
assert not proxy.actor_ref.is_alive()
def test_actor_proxy_does_not_expose_proxy_to_self(
runtime: Runtime,
log_handler: PykkaTestLogHandler,
) -> None:
class SelfReferencingActor(runtime.actor_class): # type: ignore[name-defined]
def __init__(self) -> None:
super().__init__()
self.self_proxy = self.actor_ref.proxy()
self.foo = "bar"
actor_ref = SelfReferencingActor.start()
try:
proxy = actor_ref.proxy()
assert proxy.foo.get() == "bar"
with pytest.raises(AttributeError, match="has no attribute 'self_proxy'"):
proxy.self_proxy.foo.get()
finally:
actor_ref.stop()
log_handler.wait_for_message(LogLevel.WARNING)
with log_handler.lock:
assert len(log_handler.messages[LogLevel.WARNING]) == 2
log_record = log_handler.messages[LogLevel.WARNING][0]
assert (
"attribute 'self_proxy' is a proxy to itself. "
"Consider making it private by renaming it to '_self_proxy'."
) in log_record.getMessage()
pykka-4.5.0/tests/proxy/test_static_method_calls.py 0000644 0000000 0000000 00000010127 15262536163 017540 0 ustar 00 from __future__ import annotations
from typing import TYPE_CHECKING, NoReturn
import pytest
from pykka import Actor
if TYPE_CHECKING:
from collections.abc import Iterator
from types import TracebackType
from pykka import ActorProxy, Future
from tests.types import Events, Runtime
class StaticMethodActor(Actor):
cat: str = "dog"
def __init__(self, events: Events) -> None:
super().__init__()
self.events = events
def on_stop(self) -> None:
self.events.on_stop_was_called.set()
def on_failure(
self,
exception_type: type[BaseException] | None,
exception_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
self.events.on_failure_was_called.set()
def functional_hello(self, s: str) -> str:
return f"Hello, {s}!"
def set_cat(self, s: str) -> None:
self.cat = s
def raise_exception(self) -> NoReturn:
raise Exception("boom!")
def talk_with_self(self) -> Future[str]:
return self.actor_ref.proxy().functional_hello("from the future") # type: ignore[no-any-return]
@pytest.fixture(scope="module")
def actor_class(runtime: Runtime) -> type[StaticMethodActor]:
class StaticMethodActorImpl(StaticMethodActor, runtime.actor_class): # type: ignore[name-defined]
pass
return StaticMethodActorImpl
@pytest.fixture
def proxy(
actor_class: type[StaticMethodActor],
events: Events,
) -> Iterator[ActorProxy[StaticMethodActor]]:
proxy = actor_class.start(events).proxy()
yield proxy
proxy.stop()
def test_functional_method_call_returns_correct_value(
proxy: ActorProxy[StaticMethodActor],
) -> None:
assert proxy.functional_hello("world").get() == "Hello, world!"
assert proxy.functional_hello("moon").get() == "Hello, moon!"
def test_side_effect_of_method_call_is_observable(
proxy: ActorProxy[StaticMethodActor],
) -> None:
assert proxy.cat.get() == "dog"
future = proxy.set_cat("eagle")
assert future.get() is None
assert proxy.cat.get() == "eagle"
def test_side_effect_of_deferred_method_call_is_observable(
proxy: ActorProxy[StaticMethodActor],
) -> None:
assert proxy.cat.get() == "dog"
result = proxy.set_cat.defer("eagle")
assert result is None
assert proxy.cat.get() == "eagle"
def test_exception_in_method_reraised_by_future(
proxy: ActorProxy[StaticMethodActor],
events: Events,
) -> None:
assert not events.on_failure_was_called.is_set()
future = proxy.raise_exception()
with pytest.raises(Exception, match="boom!") as exc_info:
future.get()
assert str(exc_info.value) == "boom!"
assert not events.on_failure_was_called.is_set()
def test_exception_in_deferred_method_call_triggers_on_failure(
proxy: ActorProxy[StaticMethodActor],
events: Events,
) -> None:
assert not events.on_failure_was_called.is_set()
result = proxy.raise_exception.defer()
assert result is None
events.on_failure_was_called.wait(5)
assert events.on_failure_was_called.is_set()
assert not events.on_stop_was_called.is_set()
def test_call_to_unknown_method_raises_attribute_error(
proxy: ActorProxy[StaticMethodActor],
) -> None:
with pytest.raises(AttributeError) as exc_info:
proxy.unknown_method()
result = str(exc_info.value)
assert result.startswith(" None:
with pytest.raises(AttributeError) as exc_info:
proxy.unknown_method.defer()
result = str(exc_info.value)
assert result.startswith(" None:
outer_future = proxy.talk_with_self()
inner_future = outer_future.get(timeout=1)
result = inner_future.get(timeout=1)
assert result == "Hello, from the future!"
pykka-4.5.0/tests/proxy/test_traversable.py 0000644 0000000 0000000 00000006643 15262536163 016055 0 ustar 00 from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
import pykka
from pykka import Actor
if TYPE_CHECKING:
from collections.abc import Iterator
from pykka import ActorProxy
from tests.types import Runtime
class NestedWithNoMarker:
inner = "nested_with_no_marker.inner"
class NestedWithNoMarkerAndSlots:
__slots__ = ["inner"]
def __init__(self) -> None:
self.inner = "nested_with_no_marker_and_slots.inner"
@pykka.traversable
class NestedWithDecoratorMarker:
inner = "nested_with_decorator_marker.inner"
class NestedWithAttrMarker:
pykka_traversable = True
inner = "nested_with_attr_marker.inner"
class NestedWithAttrMarkerAndSlots:
__slots__ = ["inner", "pykka_traversable"]
def __init__(self) -> None:
# Objects using '__slots__' cannot have class attributes.
self.pykka_traversable = True
self.inner = "nested_with_attr_marker_and_slots.inner"
class TraversableObjectsActor(Actor):
nested_with_no_marker = NestedWithNoMarker()
nested_with_function_marker = pykka.traversable(NestedWithNoMarker())
nested_with_decorator_marker = NestedWithDecoratorMarker()
nested_with_attr_marker = NestedWithAttrMarker()
nested_with_attr_marker_and_slots = NestedWithAttrMarkerAndSlots()
@property
def nested_object_property(self) -> NestedWithAttrMarker:
return NestedWithAttrMarker()
@pytest.fixture
def actor_class(runtime: Runtime) -> type[Actor]:
class TraversableObjectsActorImpl(TraversableObjectsActor, runtime.actor_class): # type: ignore[name-defined]
pass
return TraversableObjectsActorImpl
@pytest.fixture
def proxy(
actor_class: type[TraversableObjectsActor],
) -> Iterator[ActorProxy[TraversableObjectsActor]]:
proxy = actor_class.start().proxy()
yield proxy
proxy.stop()
def test_attr_without_marker_cannot_be_traversed(
proxy: ActorProxy[TraversableObjectsActor],
) -> None:
with pytest.raises(AttributeError) as exc_info:
proxy.nested_with_no_marker.inner.get()
assert "object has no attribute 'inner'" in str(exc_info.value)
@pytest.mark.parametrize(
("attr_name", "expected"),
[
("nested_with_function_marker", "nested_with_no_marker.inner"),
("nested_with_decorator_marker", "nested_with_decorator_marker.inner"),
("nested_with_attr_marker", "nested_with_attr_marker.inner"),
(
"nested_with_attr_marker_and_slots",
"nested_with_attr_marker_and_slots.inner",
),
],
)
def test_attr_of_traversable_attr_can_be_read(
proxy: ActorProxy[TraversableObjectsActor],
attr_name: str,
expected: str,
) -> None:
attr = getattr(proxy, attr_name)
assert attr.inner.get() == expected
def test_traversable_object_returned_from_property_is_not_traversed(
proxy: ActorProxy[TraversableObjectsActor],
) -> None:
# In Pykka < 2, it worked like this:
# assert proxy.nested_object_property.inner.get() == 'nested.inner' # noqa: ERA001
# In Pykka >= 2, the property getter always returns a future:
assert proxy.nested_object_property.get().inner == "nested_with_attr_marker.inner"
def test_traversable_cannot_mark_object_using_slots() -> None:
with pytest.raises(Exception, match="cannot be used to mark") as exc_info:
pykka.traversable(NestedWithNoMarkerAndSlots())
assert "cannot be used to mark an object using slots" in str(exc_info.value)
pykka-4.5.0/tests/proxy/test_typed_proxy.py 0000644 0000000 0000000 00000003162 15262536163 016122 0 ustar 00 from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
import pytest
import pykka
from pykka import Actor, ActorProxy
from pykka.typing import ActorMemberMixin, proxy_field, proxy_method
if TYPE_CHECKING:
from collections.abc import Iterator
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]
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
pykka-4.5.0/.gitignore 0000644 0000000 0000000 00000000147 15262536163 011570 0 ustar 00 *.egg-info
/*.lock
/.*_cache/
/.coverage
/.jj/
/.tox/
/.venv/
/dist/
/docs/_build/
/site/
__pycache__/
pykka-4.5.0/LICENSE 0000644 0000000 0000000 00000026136 15262536163 010613 0 ustar 00
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
pykka-4.5.0/README.md 0000644 0000000 0000000 00000003050 15262536163 011053 0 ustar 00 # 🌀 Pykka
_Pykka makes it easier to build concurrent applications._
[](https://github.com/jodal/pykka/actions/workflows/ci.yml)
[](https://pykka.readthedocs.io/en/latest/)
[](https://codecov.io/gh/jodal/pykka)
[](https://pypi.org/project/pykka/)
---
Pykka is a Python implementation of the
[actor model](https://en.wikipedia.org/wiki/Actor_model).
The actor model introduces some simple rules to control
the sharing of state and cooperation between execution units,
which makes it easier to build concurrent applications.
## Installation
Pykka has no dependencies other than Python 3.11 or newer.
It can be installed from [PyPI](https://pypi.org/project/pykka/):
```console
$ python3 -m pip install pykka
```
Next up, check out the [documentation](https://pykka.readthedocs.io/).
## Project resources
- [Documentation](https://pykka.readthedocs.io/)
- [Source code](https://github.com/jodal/pykka)
- [Releases](https://github.com/jodal/pykka/releases)
- [Issue tracker](https://github.com/jodal/pykka/issues)
- [Contributors](https://github.com/jodal/pykka/graphs/contributors)
- [Users](https://github.com/jodal/pykka/wiki/Users)
## License
Pykka is copyright 2010-2026 Stein Magnus Jodal and contributors.
Pykka is licensed under the
[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
pykka-4.5.0/pyproject.toml 0000644 0000000 0000000 00000011752 15262536163 012520 0 ustar 00 [project]
name = "pykka"
version = "4.5.0"
description = "Pykka is a Python implementation of the actor model"
authors = [{ name = "Stein Magnus Jodal", email = "stein.magnus@jodal.no" }]
requires-python = ">=3.11"
readme = "README.md"
license = "Apache-2.0"
keywords = ["actor", "concurrency", "threading"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
]
[project.urls]
"Source code" = "https://github.com/jodal/pykka"
Documentation = "https://pykka.readthedocs.io/"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"tox>=4.21.2",
"tox-uv>=1.23.2",
{ include-group = "benchmark" },
{ include-group = "docs" },
{ include-group = "mypy" },
{ include-group = "pyright" },
{ include-group = "ruff" },
{ include-group = "tests" },
{ include-group = "ty" },
{ include-group = "zizmor" },
]
benchmark = ["pyperf>=2.9.0"]
docs = [
"mkdocstrings-python>=2.0.1",
"zensical>=0.0.69",
]
mypy = ["mypy>=2.4.0"]
pyright = ["basedpyright>=1.39.6"]
ruff = ["ruff>=0.16.3"]
tests = ["pytest>=8.3.5", "pytest-cov>=5.0.0", "pytest-mock>=3.14.0"]
ty = ["ty==0.0.84"]
zizmor = ["zizmor==1.30.1"]
[tool.coverage.paths]
source = ["src/", "*/site-packages/"]
[tool.coverage.run]
branch = true
source_pkgs = ["pykka"]
[tool.coverage.report]
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING", '\.\.\.']
[tool.mypy]
disallow_untyped_defs = true
no_implicit_optional = true
strict_equality = true
warn_return_any = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_unused_configs = true
[[tool.mypy.overrides]]
module = ["pyperf"]
ignore_missing_imports = true
[tool.pyright]
pythonVersion = "3.11"
typeCheckingMode = "strict"
# Already covered by tests and careful import ordering:
reportImportCycles = false
# Already covered by flake8-self:
reportPrivateUsage = false
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"A003", # builtin-attribute-shadowing
"ANN401", # any-type
"CPY", # flake8-pyright
"D203", # one-blank-line-before-class
"D213", # multi-line-summary-second-line
"G004", # logging-f-string
"PLR2004", # magic-value-comparison
"RET504", # unnecessary-assign
"S101", # assert
"TRY003", # raise-vanilla-args
#
# These rules interfere with `ruff format`
"COM812", # missing-trailing-comma
"ISC001", # single-line-implicit-string-concatenation
]
[tool.ruff.lint.per-file-ignores]
"benchmarks/*" = [
"D", # pydocstyle
]
"docs/*" = [
"D", # pydocstyle
"INP001", # implicit-namespace-package
]
"examples/*" = [
"ANN", # flake8-annotations
"BLE001", # blind-except
"D", # pydocstyle
"INP001", # implicit-namespace-package
"T20", # flake8-print
]
"tests/*" = [
"ANN", # flake8-annotations
"ARG", # flake8-unused-arguments
"D", # pydocstyle
"EM101", # raw-string-in-exception
"TRY002", # raise-vanilla-class
]
[tool.rumdl]
flavor = "mkdocs"
[tool.tox]
env_list = [
"3.11",
"3.12",
"3.13",
"3.14",
"3.15",
"docs",
"mypy",
"pyright",
"ruff-format",
"ruff-check",
"ty",
"zizmor",
]
[tool.tox.env_run_base]
package = "wheel"
wheel_build_env = ".pkg"
dependency_groups = ["tests"]
commands = [
[
"pytest",
"--cov",
"--basetemp={envtmpdir}",
{ replace = "posargs", extend = true },
],
]
[tool.tox.env.docs]
dependency_groups = ["docs"]
commands = [["zensical", "build", "--clean", "--strict"]]
[tool.tox.env.mypy]
dependency_groups = ["benchmark", "tests", "mypy"]
commands = [
[
"mypy",
{ replace = "posargs", default = [
"benchmarks",
"examples",
"src",
"tests",
], extend = true },
],
]
[tool.tox.env.pyright]
dependency_groups = ["benchmark", "tests", "pyright"]
commands = [
[
"basedpyright",
{ replace = "posargs", default = [
"benchmarks",
"examples",
"src",
"tests",
], extend = true },
],
]
[tool.tox.env.ruff-format]
skip_install = true
dependency_groups = ["ruff"]
commands = [["ruff", "format", "--check", "{posargs:.}"]]
[tool.tox.env.ruff-check]
skip_install = true
dependency_groups = ["ruff"]
commands = [["ruff", "check", "{posargs:.}"]]
[tool.tox.env.ty]
dependency_groups = ["benchmark", "tests", "ty"]
commands = [
[
"ty",
"check",
{ replace = "posargs", default = [
"benchmarks",
"examples",
"src",
"tests",
], extend = true },
],
]
[tool.tox.env.zizmor]
skip_install = true
dependency_groups = ["zizmor"]
commands = [["zizmor", "{posargs:.}"]]
[[tool.ty.overrides]]
include = ["examples/**"]
[tool.ty.overrides.rules]
undefined-reveal = "ignore"
[[tool.ty.overrides]]
include = ["tests/**"]
[tool.ty.overrides.rules]
unsupported-base = "ignore"
pykka-4.5.0/PKG-INFO 0000644 0000000 0000000 00000004171 15262536163 010676 0 ustar 00 Metadata-Version: 2.5
Name: pykka
Version: 4.5.0
Summary: Pykka is a Python implementation of the actor model
Project-URL: Source code, https://github.com/jodal/pykka
Project-URL: Documentation, https://pykka.readthedocs.io/
Author-email: Stein Magnus Jodal
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: actor,concurrency,threading
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Description-Content-Type: text/markdown
# 🌀 Pykka
_Pykka makes it easier to build concurrent applications._
[](https://github.com/jodal/pykka/actions/workflows/ci.yml)
[](https://pykka.readthedocs.io/en/latest/)
[](https://codecov.io/gh/jodal/pykka)
[](https://pypi.org/project/pykka/)
---
Pykka is a Python implementation of the
[actor model](https://en.wikipedia.org/wiki/Actor_model).
The actor model introduces some simple rules to control
the sharing of state and cooperation between execution units,
which makes it easier to build concurrent applications.
## Installation
Pykka has no dependencies other than Python 3.11 or newer.
It can be installed from [PyPI](https://pypi.org/project/pykka/):
```console
$ python3 -m pip install pykka
```
Next up, check out the [documentation](https://pykka.readthedocs.io/).
## Project resources
- [Documentation](https://pykka.readthedocs.io/)
- [Source code](https://github.com/jodal/pykka)
- [Releases](https://github.com/jodal/pykka/releases)
- [Issue tracker](https://github.com/jodal/pykka/issues)
- [Contributors](https://github.com/jodal/pykka/graphs/contributors)
- [Users](https://github.com/jodal/pykka/wiki/Users)
## License
Pykka is copyright 2010-2026 Stein Magnus Jodal and contributors.
Pykka is licensed under the
[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).