pykka-4.5.0/.readthedocs.yml0000644000000000000000000000050515262536163012664 0ustar00version: 2 build: os: "ubuntu-26.04" tools: python: "3.14" jobs: build: html: - zensical build --strict post_build: - mkdir -p $READTHEDOCS_OUTPUT/html/ - cp -r site/* $READTHEDOCS_OUTPUT/html/ python: install: - method: uv command: sync groups: - docs pykka-4.5.0/AGENTS.md0000644000000000000000000000230415262536163011100 0ustar00# AGENTS.md Guidance for coding agents that work in this repo. Pykka is a Python implementation of the actor model. It supports Python 3.11 and newer, and has no runtime dependencies. ## Layout - `src/pykka/`: the library - `tests/`: the pytest suite - `benchmarks/`: pyperf benchmarks, see `benchmarks/README.md` - `examples/`: runnable examples - `docs/`: the Zensical site ## Checks CI runs each check as a tox env. Run them with uv: ```sh uv run tox -e 3.15 # pytest on Python 3.15 uv run tox -e 3.15 -- tests/test_actor.py # one test file uv run tox -e mypy uv run tox -e pyright uv run tox -e ty uv run tox -e ruff-format uv run tox -e ruff-check uv run tox -e docs uv run tox -e zizmor ``` ## Rules - Keep the public API stable. Do not change or remove public names or behavior unless you are asked to. Mopidy and other projects use Pykka, and Debian and other distributions package it. - Do not add runtime dependencies. - mypy, basedpyright, and ty run in strict mode. Add precise type annotations. - Add or change tests for each change in behavior. - Update `docs/` and `examples/` when behavior that users see changes. - Use Conventional Commits for commit messages. pykka-4.5.0/zensical.toml0000644000000000000000000000672315262536163012313 0ustar00[project] site_name = "Pykka" site_description = "Pykka makes it easier to build concurrent applications." site_url = "https://pykka.readthedocs.io/stable/" copyright = "Copyright © 2010-2026 Stein Magnus Jodal and contributors" repo_name = "jodal/pykka" repo_url = "https://github.com/jodal/pykka" edit_uri = "edit/main/docs/" extra_css = ["stylesheets/extra.css"] extra_javascript = ["javascript/readthedocs.js"] nav = [ {"Introduction" = [ "index.md", ]}, {"Getting started" = [ "getting-started/index.md", "getting-started/model.md", "getting-started/actors.md", "getting-started/proxies.md", ]}, {"Guides" = [ "guides/index.md", "guides/logging.md", "guides/testing.md", "guides/deadlocks.md", "guides/proxy-typing.md", ]}, {"Reference" = [ "reference/index.md", "reference/actors.md", "reference/proxies.md", "reference/futures.md", "reference/registry.md", "reference/runtimes.md", "reference/exceptions.md", "reference/messages.md", "reference/debug.md", "reference/typing.md", ]}, {"Examples" = [ "examples/index.md", "examples/basic.md", "examples/proxy.md", "examples/cooperation.md", "examples/worker-pool.md", "examples/mopidy.md", ]}, ] [project.theme] custom_dir = "docs/_theme" features = [ "content.action.edit", "navigation.footer", "navigation.indexes", "navigation.instant", "navigation.instant.progress", "navigation.sections", ] [project.theme.icon] logo = "fontawesome/solid/spiral" repo = "fontawesome/brands/github" [[project.theme.palette]] media = "(prefers-color-scheme)" primary = "purple" accent = "deep purple" toggle.icon = "lucide/sun-moon" toggle.name = "Switch to light mode" [[project.theme.palette]] media = "(prefers-color-scheme: light)" scheme = "default" primary = "purple" accent = "deep purple" toggle.icon = "lucide/sun" toggle.name = "Switch to dark mode" [[project.theme.palette]] media = "(prefers-color-scheme: dark)" scheme = "slate" primary = "purple" accent = "deep purple" toggle.icon = "lucide/moon" toggle.name = "Switch to light mode" [[project.extra.social]] icon = "fontawesome/brands/github" link = "https://github.com/jodal/pykka" [project.plugins.exclude] glob = ["_theme/*"] [project.plugins.mkdocstrings] default_handler = "python" [project.plugins.mkdocstrings.handlers.python] inventories = ["https://docs.python.org/3/objects.inv"] paths = ["src"] [project.plugins.mkdocstrings.handlers.python.options] docstring_style = "google" docstring_section_style = "list" filters = [ "!^_[^_]", "!^__post_init__", "!^__len__", "!^__repr__", "!^__str__", ] separate_signature = true show_docstring_examples = true show_root_heading = true show_root_toc_entry = false show_symbol_type_heading = true show_symbol_type_toc = true show_signature_annotations = true [project.markdown_extensions.pymdownx.blocks.admonition] [project.markdown_extensions.pymdownx.blocks.definition] [project.markdown_extensions.pymdownx.highlight] anchor_linenums = true default_lang = "python" line_spans = "__span" pygments_lang_class = true [project.markdown_extensions.pymdownx.inlinehilite] [project.markdown_extensions.pymdownx.magiclink] repo_url_shorthand = true user = "jodal" repo = "pykka" [project.markdown_extensions.pymdownx.snippets] [project.markdown_extensions.pymdownx.superfences] pykka-4.5.0/.github/FUNDING.yml0000644000000000000000000000001615262536163012750 0ustar00github: jodal pykka-4.5.0/.github/dependabot.yml0000644000000000000000000000126715262536163013774 0ustar00version: 2 updates: - package-ecosystem: pip directory: "/" labels: - dependencies schedule: interval: monthly cooldown: default-days: 7 groups: docs: patterns: - "mkdocs*" - "zensical" tests: patterns: - "pytest*" linters: patterns: - "mypy" - "basedpyright" - "ruff" - "ty" - "tox*" - "zizmor" - package-ecosystem: github-actions directory: "/" labels: - dependencies schedule: interval: monthly cooldown: default-days: 7 groups: actions: patterns: - "*" pykka-4.5.0/.github/workflows/ci.yml0000644000000000000000000000431315262536163014312 0ustar00name: CI on: pull_request: push: branches: - main permissions: contents: read jobs: build: name: Build runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 with: persist-credentials: false - uses: hynek/build-and-inspect-python-package@2abe76da66d0a6a4a227101f9348ee855797cfa5 # v3.0.1 main: strategy: fail-fast: false matrix: include: - name: "Docs" python: "3.15" tox: docs - name: "pytest (3.11)" python: "3.11" tox: "3.11" - name: "pytest (3.12)" python: "3.12" tox: "3.12" - name: "pytest (3.13)" python: "3.13" tox: "3.13" - name: "pytest (3.14)" python: "3.14" tox: "3.14" - name: "pytest (3.15)" python: "3.15" tox: "3.15" coverage: true - name: "mypy" python: "3.15" tox: mypy - name: "pyright" python: "3.15" tox: pyright - name: "ruff format" python: "3.15" tox: ruff-format - name: "ruff lint" python: "3.15" tox: ruff-check - name: "ty" python: "3.15" tox: ty - name: "zizmor" python: "3.15" tox: zizmor name: ${{ matrix.name }} runs-on: ubuntu-24.04 permissions: contents: read id-token: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 with: persist-credentials: false - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: python-version: ${{ matrix.python }} - run: uv tool install tox --with tox-uv - run: tox -e ${{ matrix.tox }} if: ${{ ! matrix.coverage }} - run: tox -e ${{ matrix.tox }} -- --cov-report=xml if: ${{ matrix.coverage }} - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 if: ${{ matrix.coverage }} with: use_oidc: true pykka-4.5.0/.github/workflows/release.yml0000644000000000000000000000137415262536163015343 0ustar00name: Release on: release: types: [published] permissions: {} jobs: release: runs-on: ubuntu-24.04 environment: name: pypi url: https://pypi.org/project/pykka/ permissions: id-token: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 with: persist-credentials: false - uses: hynek/build-and-inspect-python-package@2abe76da66d0a6a4a227101f9348ee855797cfa5 # v3.0.1 id: build - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: ${{ steps.build.outputs.artifact-name }} path: dist - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 pykka-4.5.0/benchmarks/README.md0000644000000000000000000000311615262536163013173 0ustar00# Benchmarks Use these benchmarks before a release to find out if the new code is slower than the last release. The benchmarks use [pyperf](https://pyperf.readthedocs.io/). ## How to run a benchmark Run the benchmark by hand, on one machine. Run the last release and the new code one after the other, with the same Python version. Do not store the results in this repo. Timings from different machines, or from the same machine at different times, cannot be compared. 1. Run the last release from PyPI, for example Pykka 4.4.2: ```sh uv run --no-project --python 3.14 --with pykka==4.4.2 --with pyperf \ python benchmarks/threading_actors.py \ --rigorous --affinity=2 -o old.json ``` 2. Run the new code from this repo: ```sh uv run --python 3.14 --group benchmark \ python benchmarks/threading_actors.py \ --rigorous --affinity=2 -o new.json ``` 3. Compare the two results: ```sh uv run --group benchmark python -m pyperf compare_to old.json new.json ``` ## How to get stable results Pin the benchmark to one CPU core with `--affinity`. On two cores, most of a round trip is the time it takes to wake a sleeping core, and that changes from run to run by up to 3x. Use `--rigorous` to get more runs and values. Trust a result only if the spread (`std dev`) is about 1% or less for the cases that have a limit. The docstring of each benchmark tells which cases have a limit. ## CI CI does not run the benchmarks. It only runs the `test_*.py` files in this directory. These run each case a few times to make sure that the benchmarks still work. pykka-4.5.0/benchmarks/__init__.py0000644000000000000000000000000015262536163014012 0ustar00pykka-4.5.0/benchmarks/test_threading_actors.py0000644000000000000000000000055215262536163016646 0ustar00from __future__ import annotations from typing import TYPE_CHECKING import pytest from benchmarks.threading_actors import BENCHMARKS if TYPE_CHECKING: from collections.abc import Callable @pytest.mark.parametrize("func", BENCHMARKS.values(), ids=BENCHMARKS.keys()) def test_benchmark_runs(func: Callable[[int], float]) -> None: assert func(10) > 0 pykka-4.5.0/benchmarks/threading_actors.py0000644000000000000000000000652115262536163015611 0ustar00"""Benchmark of Pykka's threading actors. See `README.md` in this directory for how to run the benchmark. A release is blocked if the mean of `ask` or one of the proxy cases is more than 10% slower than the last release. The means of these cases repeat within 0.5% between runs. `tell` and `start_stop` are only reported. The mean of `tell` changes by up to 13% between runs, and `start_stop` mostly measures how fast the kernel makes and joins a thread. """ from __future__ import annotations import importlib.metadata import time from typing import TYPE_CHECKING, Any from pykka import ThreadingActor if TYPE_CHECKING: from collections.abc import Callable class SomeObject: pykka_traversable = False cat = "bar.cat" def func(self) -> None: pass class AnActor(ThreadingActor): bar = SomeObject() bar.pykka_traversable = True foo = "foo" def func(self) -> None: pass def on_receive(self, message: Any) -> Any: return message def bench_tell(loops: int) -> float: ref = AnActor.start() start = time.perf_counter() for _ in range(loops): ref.tell("ping") ref.ask("ping") # Wait until the actor has handled all messages. elapsed = time.perf_counter() - start ref.stop() return elapsed def bench_ask(loops: int) -> float: ref = AnActor.start() start = time.perf_counter() for _ in range(loops): ref.ask("ping") elapsed = time.perf_counter() - start ref.stop() return elapsed def bench_proxy_call(loops: int) -> float: ref = AnActor.start() proxy = ref.proxy() start = time.perf_counter() for _ in range(loops): proxy.func().get() elapsed = time.perf_counter() - start ref.stop() return elapsed def bench_proxy_attr(loops: int) -> float: ref = AnActor.start() proxy = ref.proxy() start = time.perf_counter() for _ in range(loops): proxy.foo.get() elapsed = time.perf_counter() - start ref.stop() return elapsed def bench_proxy_traversable_call(loops: int) -> float: ref = AnActor.start() proxy = ref.proxy() start = time.perf_counter() for _ in range(loops): proxy.bar.func().get() elapsed = time.perf_counter() - start ref.stop() return elapsed def bench_proxy_traversable_attr(loops: int) -> float: ref = AnActor.start() proxy = ref.proxy() start = time.perf_counter() for _ in range(loops): proxy.bar.cat.get() elapsed = time.perf_counter() - start ref.stop() return elapsed def bench_start_stop(loops: int) -> float: start = time.perf_counter() for _ in range(loops): AnActor.start().stop() return time.perf_counter() - start BENCHMARKS: dict[str, Callable[[int], float]] = { "tell": bench_tell, "ask": bench_ask, "proxy_call": bench_proxy_call, "proxy_attr": bench_proxy_attr, "proxy_traversable_call": bench_proxy_traversable_call, "proxy_traversable_attr": bench_proxy_traversable_attr, "start_stop": bench_start_stop, } def main() -> None: import pyperf # noqa: PLC0415 # pyright: ignore[reportMissingTypeStubs] runner: Any = pyperf.Runner() runner.metadata["pykka_version"] = importlib.metadata.version("pykka") for name, func in BENCHMARKS.items(): runner.bench_time_func(name, func) if __name__ == "__main__": main() pykka-4.5.0/docs/index.md0000644000000000000000000000304715262536163012163 0ustar00# Introduction > _Pykka makes it easier to build concurrent applications._ Pykka is a Python implementation of the [actor model](getting-started/model.md). 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/): ```sh python3 -m pip install pykka ``` Next up, check out the [Getting started](getting-started/index.md) docs or the [Reference](reference/index.md). ## Project resources - [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) ## History and inspiration Pykka was originally created around 2011 as a formalization of concurrency patterns that emerged in the [Mopidy music server](https://www.mopidy.com/). The original Pykka source code wasn't extracted from Mopidy, but it built and improved on the concepts from Mopidy. Mopidy was later ported to build on Pykka instead of its own concurrency abstractions. Much of the naming of concepts and methods in Pykka was inspired by [Jonas Bonér](https://jonasboner.com/)'s conference talks around 2010 on Akka, a JVM implementation of the actor model. Pykka is not a Python port of Akka, and supports far fewer features. pykka-4.5.0/docs/_theme/main.html0000644000000000000000000000021015262536163013572 0ustar00{% extends "base.html" %} {% block site_meta %} {{ super() }} {% endblock %}pykka-4.5.0/docs/examples/basic.md0000644000000000000000000000132715262536163013752 0ustar00# Basic actor This example shows: - how to define an actor by creating a class that subclasses [`ThreadingActor`][pykka.ThreadingActor] and defines the [`on_receive()`][pykka.Actor.on_receive] method, - how the actor can store internal state, here done by modifying `self._stored_messages`, - how to start and stop an actor, using [`start()`][pykka.Actor.start] and [`stop()`][pykka.ActorRef.stop], and - how to interact with the actor from the outside, using [`tell()`][pykka.ActorRef.tell] and [`ask()`][pykka.ActorRef.ask]. ## Example ```title="examples/basic.py" --8<-- "examples/basic.py" ``` ## Output ```console $ uv run examples/basic.py [{'no': 'Norway', 'se': 'Sweden'}, {'a': 3, 'b': 4, 'c': 5}] ``` pykka-4.5.0/docs/examples/cooperation.md0000644000000000000000000000124115262536163015206 0ustar00# Cooperating actors This example shows how multiple actors can be set up to cooperate with each other by passing either [`ActorRef`][pykka.ActorRef] or [`ActorProxy`][pykka.ActorProxy] instances to actors, either at setup time as arguments to [`start()`][pykka.Actor.start] or later through messages. ## Example ```title="examples/cooperation.py" --8<-- "examples/cooperation.py" ``` ## Output ```console $ uv run examples/cooperation.py Adder is adding 1 to 0 Bookkeeper got 1 back Adder is adding 1 to 1 Bookkeeper got 2 back Adder is adding 1 to 2 Bookkeeper got 3 back Adder is adding 1 to 3 Bookkeeper got 4 back Adder is adding 1 to 4 Bookkeeper got 5 back ``` pykka-4.5.0/docs/examples/index.md0000644000000000000000000000112115262536163013770 0ustar00# Examples The next pages contains a few simple examples of how to use Pykka. - [Basic actor](basic.md) - a simple example of a message-passing actor. - [Actor with proxy](proxy.md) - an example of an actor with a proxy. - [Cooperating actors](cooperation.md) - an example of two actors cooperating. - [Worker pool](worker-pool.md) - an example using a pool of actors to split up work. - [Mopidy music server](mopidy.md) - a real-life example of using Pykka. All of the examples can also be found in the [`examples/` directory on GitHub](https://github.com/jodal/pykka/tree/main/examples). pykka-4.5.0/docs/examples/mopidy.md0000644000000000000000000000160015262536163014164 0ustar00# Mopidy music server Pykka was originally created around 2011 as a formalization of concurrency patterns that emerged in the [Mopidy music server](https://www.mopidy.com/). The original Pykka source code wasn't extracted from Mopidy, but it built and improved on the concepts from Mopidy. Mopidy was later ported to build on Pykka instead of its own concurrency abstractions. Mopidy still use Pykka extensively to keep independent parts, like the MPD and HTTP servers or the Spotify and Youtube Music integrations, running independently. Every one of Mopidy's more than 100 extensions has at least one Pykka actor. By running each extension as an independent actor, errors and bugs in one extension is attempted isolated, to reduce the effect on the rest of the system. You can browse the [Mopidy source code](https://github.com/mopidy/mopidy) to find many real life examples of Pykka usage. pykka-4.5.0/docs/examples/proxy.md0000644000000000000000000000243715262536163014055 0ustar00# Actor with proxy This example shows how to define an actor by creating a class that subclasses [`ThreadingActor`][pykka.ThreadingActor] and defines attributes and methods that can be accessed through a proxy. This makes the actor almost as easy to use as a regular class, with the only difference being that the method calls and attribute accesses are asynchronous and return [futures][pykka.Future]. This actor does not override the [`on_receive()`][pykka.Actor.on_receive] method, but it is entirely possible to combine the two approaches and use both regular message passing to the [`on_receive()`][pykka.Actor.on_receive] method and proxy-based access to methods and attributes on the same actor. ## Example ```title="examples/proxy.py" --8<-- "examples/proxy.py" ``` ## Output ```console $ uv run examples/proxy.py MainThread: calling AnActor.foo() ... MainThread: calling AnActor.bar() ... AnActor-1 (_actor_loop): this was printed by AnActor.foo() MainThread: printing result ... (blocking) MainThread: this was returned by AnActor.bar() after a delay MainThread: reading AnActor.field ... MainThread: printing result ... (blocking) MainThread: this is the value of AnActor.field MainThread: writing AnActor.field ... MainThread: printing new field value ... (blocking) MainThread: new value ``` pykka-4.5.0/docs/examples/worker-pool.md0000644000000000000000000000370315262536163015151 0ustar00# Worker pool This example shows how to use a pool of workers to fan out work to multiple actors and then collect the result using [`get_all()`][pykka.get_all]. The example perform DNS lookups concurrently. It creates a pool of 4 workers and distributes the work of resolving a list of IP addresses among them. Each worker performs the DNS lookup and returns the result to the main thread, which collects and prints the results. ## Example ```title="examples/worker_pool.py" --8<-- "examples/worker_pool.py" ``` ## Output ```console $ uv run examples/worker_pool.py Finished resolving 193.35.52.3 Finished resolving 193.35.52.2 Finished resolving 193.35.52.1 Finished resolving 193.35.52.4 Failed resolving 193.35.52.8 Failed resolving 193.35.52.6 Failed resolving 193.35.52.5 Failed resolving 193.35.52.7 Failed resolving 193.35.52.12 Failed resolving 193.35.52.9 Failed resolving 193.35.52.11 Failed resolving 193.35.52.10 Failed resolving 193.35.52.14 Failed resolving 193.35.52.13 Failed resolving 193.35.52.15 Failed resolving 193.35.52.16 Finished resolving 193.35.52.18 Finished resolving 193.35.52.19 Failed resolving 193.35.52.17 [(IPv4Address('193.35.52.1'), 'merete.samfundet.no'), (IPv4Address('193.35.52.2'), 'osl.samfundet.no'), (IPv4Address('193.35.52.3'), 'fnis.samfundet.no'), (IPv4Address('193.35.52.4'), 'trd.samfundet.no.52.35.193.in-addr.arpa'), (IPv4Address('193.35.52.5'), None), (IPv4Address('193.35.52.6'), None), (IPv4Address('193.35.52.7'), None), (IPv4Address('193.35.52.8'), None), (IPv4Address('193.35.52.9'), None), (IPv4Address('193.35.52.10'), None), (IPv4Address('193.35.52.11'), None), (IPv4Address('193.35.52.12'), None), (IPv4Address('193.35.52.13'), None), (IPv4Address('193.35.52.14'), None), (IPv4Address('193.35.52.15'), None), (IPv4Address('193.35.52.16'), None), (IPv4Address('193.35.52.17'), None), (IPv4Address('193.35.52.18'), 'bablefisk.samfundet.no'), (IPv4Address('193.35.52.19'), 'altostratus.samfundet.no')] ``` pykka-4.5.0/docs/getting-started/actors.md0000644000000000000000000001333715262536163015457 0ustar00# Message-processing actor In its most basic form, a Pykka actor is a class with an [`on_receive(message)`][pykka.Actor.on_receive] method: ```py import pykka class Greeter(pykka.ThreadingActor): def on_receive(self, message): print("Hi there!") ``` ## Starting an actor To start an actor, you call the class' [`start()`][pykka.Actor.start] method, which starts the actor and returns an actor reference which can be used to communicate with the running actor: ```py actor_ref = Greeter.start() ``` If you need to pass arguments to the actor upon creation, you can pass them to the [`start()`][pykka.Actor.start] method. The actor receives the arguments using the regular `__init__()` method: ```py import pykka class Greeter(pykka.ThreadingActor): def __init__(self, greeting="Default greeting"): super().__init__() self.greeting = greeting def on_receive(self, message): print(self.greeting) actor_ref = Greeter.start(greeting="Hi you!") ``` It can be useful to know that the `__init__()` method is run in the execution context that starts the actor. There are also hooks for running code in the actor's own execution context when the actor [starts][pykka.Actor.on_start], when it [stops][pykka.Actor.on_stop], and when [an unhandled exception is raised][pykka.Actor.on_failure]. Check out the full API docs for the details. ## Stopping an actor To stop an actor, you can call [`stop()`][pykka.ActorRef.stop] on the [`ActorRef`][pykka.ActorRef] object: ```py actor_ref.stop() ``` If an actor wants to stop itself, it can call [`stop()`][pykka.Actor.stop] on itself. It will then exit as soon as it has finished processing the messages currently queued in its inbox. ```py self.stop() ``` Once an actor has been stopped, it cannot be restarted. ## Sending messages To send a message to the actor, you can either use the [`tell()`][pykka.ActorRef.tell] method or the [`ask()`][pykka.ActorRef.ask] method on the [`ActorRef`][pykka.ActorRef] object: - [`tell(message)`][pykka.ActorRef.tell] will fire off a message without waiting for an answer. In other words, it will never block. - [`ask(message)`][pykka.ActorRef.ask] will by default block until an answer is returned, potentially forever. If you provide a `timeout` keyword argument, you can specify for how long it should wait for an answer. If you want an answer, but don't need it right away because you have other stuff you can do first, you can use `ask(block=False)`, and it will immediately return a "future" object. The message itself can be of any type, for example a dict or your own message class type. /// warning | Mutable messages For performance reasons, Pykka **does not** clone the message you send before delivering it to the receiver. You are yourself responsible for either using immutable data structures or to [`copy.deepcopy()`][copy.deepcopy] the data you're sending off to other actors. /// Summarized in code: ```py actor_ref.tell("Hi!") # => Returns nothing. Will never block. answer = actor_ref.ask("Hi?") # => May block forever waiting for an answer answer = actor_ref.ask("Hi?", timeout=3) # => May wait 3s for an answer, then raises exception if no answer. future = actor_ref.ask("Hi?", block=False) # => Will return a future object immediately. answer = future.get() # => May block forever waiting for an answer answer = future.get(timeout=0.1) # => May wait 0.1s for an answer, then raises exception if no answer. ``` ## Replying to messages If a message is sent using [`actor_ref.ask()`][pykka.ActorRef.ask] you can reply to the sender of the message by simply returning a value from the [`on_receive(message)`][pykka.Actor.on_receive] method: ```py import pykka class Greeter(pykka.ThreadingActor): def on_receive(self, message): return "Hi there!" actor_ref = Greeter.start() answer = actor_ref.ask("Hi?") print(answer) # => "Hi there!" ``` /// note | Returning `None` `None` is a valid response, so if you return `None` explicitly, or don't return at all, a response containing `None` will be returned to the sender. /// From the point of view of the actor it doesn't matter whether the message was sent using [`tell()`][pykka.ActorRef.tell] or [`ask()`][pykka.ActorRef.ask]. When the sender doesn't expect a response the [`on_receive()`][pykka.Actor.on_receive] return value will be ignored. The situation is similar in regard to exceptions: when [`ask()`][pykka.ActorRef.ask] is used and you raise an exception from within the [`on_receive()`][pykka.Actor.on_receive] method, the exception will propagate to the sender: ```py import pykka class Raiser(pykka.ThreadingActor): def on_receive(self, message): raise Exception("Oops") actor_ref = Raiser.start() try: actor_ref.ask("How are you?") except Exception as e: print(repr(e)) # => Exception("Oops") ``` ## Unhandled messages The default implementation of [`on_receive()`][pykka.Actor.on_receive] logs the message at the [`WARNING`][logging.WARNING] level and returns `None`. If your actor handles only some messages, call `super().on_receive(message)` for the other messages to get the same log message: ```py import pykka class Greeter(pykka.ThreadingActor): def on_receive(self, message): if message == "Hi?": return "Hi there!" return super().on_receive(message) actor_ref = Greeter.start() actor_ref.tell("Bye?") # => Logs "Unexpected message received by Greeter (urn:uuid:...): Bye?" ``` See the [logging guide](../guides/logging.md) for how to see Pykka's log messages. Messages that are sent to a stopped actor are not logged. Instead, [`tell()`][pykka.ActorRef.tell] raises [`ActorDeadError`][pykka.ActorDeadError], and the future returned by [`ask()`][pykka.ActorRef.ask] gets [`ActorDeadError`][pykka.ActorDeadError]. pykka-4.5.0/docs/getting-started/index.md0000644000000000000000000000064115262536163015265 0ustar00# Getting started To get started with Pykka, the following sections will take you through: - [The actor model](model.md) - learn about the actor model concepts and rules that Pykka is based on. - [Message-passing actors](actors.md) - learn how to create, start, and stop plain message-passing actors. - [Actor proxies](proxies.md) - learn how to use actor proxies to interact with actors in a convenient way. pykka-4.5.0/docs/getting-started/model.md0000644000000000000000000000241515262536163015257 0ustar00# The actor model Pykka is a Python implementation of the actor model, which is a model that treats message-passing "actors" as the basic building blocks of concurrent computation. You can learn more about the actor model in general at [Wikipedia](https://en.wikipedia.org/wiki/Actor_model). ## Rules 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. - An actor is an execution unit that executes concurrently with other actors. - An actor does not share state with anybody else, but it can have its own state. - An actor can only communicate with other actors by sending and receiving messages. It can only send messages to actors whose address it has. - When an actor receives a message it may take actions like: - altering its own state, e.g. so that it can react differently to a future message, - sending messages to other actors, or - starting new actors. None of the actions are required, and they may be applied in any order. - An actor only processes one message at a time. In other words, a single actor does not give you any concurrency, and it does not need to use locks internally to protect its own state. pykka-4.5.0/docs/getting-started/proxies.md0000644000000000000000000000672615262536163015661 0ustar00# Actor proxies With the basic building blocks provided by actors and futures, we have everything we need to build more advanced abstractions. Pykka provides a single abstraction on top of the basic actor model, called "actor proxies". You can use Pykka without proxies, but we've found it to be a very convenient abstraction when building [Mopidy](https://www.mopidy.com/). ## Create an actor and a proxy Let's create an actor and start it: ```py import pykka class Calculator(pykka.ThreadingActor): def __init__(self): super().__init__() self.last_result = None def add(self, a, b=None): if b is not None: self.last_result = a + b else: self.last_result += a return self.last_result def sub(self, a, b=None): if b is not None: self.last_result = a - b else: self.last_result -= a return self.last_result actor_ref = Calculator.start() ``` You can create a proxy from any reference to a running actor: ```py proxy = actor_ref.proxy() ``` The proxy object will use introspection to figure out what public attributes and methods the actor has, and then mirror the full API of the actor. Any attribute or method prefixed with underscore will be ignored, which is the convention for keeping stuff private in Python. ## Accessing attributes and calling methods When we access attributes or call methods on the proxy, it will ask the actor to access the given attribute or call the given method, and return the result to us. All results are wrapped in "future" objects, so you must use the [`Future.get()`][pykka.Future.get] method to get the actual data: ```py future = proxy.add(1, 3) future.get() # => 4 proxy.last_result.get() # => 4 ``` Since an actor only processes one message at the time and all messages are kept in order, you don't need to add the call to [`get()`][pykka.Future.get] just to block processing until the actor has completed processing your last message: ```py proxy.sub(5) proxy.add(3) proxy.last_result.get() # => 2 ``` Since assignment doesn't return anything, it works just like on regular objects: ```py proxy.last_result = 17 proxy.last_result.get() # => 17 ``` Under the hood, the proxy does everything by sending messages to the actor using the regular [`ActorRef.ask()`][pykka.ActorRef.ask] method that we talked about previously. By doing so, it maintains the actor model restrictions. The only "magic" happening here is some basic introspection and automatic building of three different message types; one for method calls, one for attribute reads, and one for attribute writes. ## Traversable attributes Sometimes you'll want to access an actor attribute's own methods or attributes through a proxy. For this case, Pykka supports "traversable attributes". By marking an actor attribute as traversable, Pykka will not return the attribute when accessed, but wrap it in a new proxy which is returned instead. To mark an attribute as traversable, simply mark it with the [`pykka.traversable()`][pykka.traversable] function: ```py import pykka class AnActor(pykka.ThreadingActor): playback = pykka.traversable(Playback()) class Playback(object): def play(self): return True proxy = AnActor.start().proxy() play_success = proxy.playback.play().get() ``` You can access methods and attributes nested as deep as you like, as long as all attributes on the path between the actor and the method or attribute on the end are marked as traversable. pykka-4.5.0/docs/guides/deadlocks.md0000644000000000000000000000704615262536163014270 0ustar00# Deadlock debugging This is a complete example of how to use [`log_thread_tracebacks()`][pykka.debug.log_thread_tracebacks] as a signal handler to debug deadlocks. ## Setting up a deadlock To illustrate how to debug a deadlock, we can use the following script to set up a deadlock situation. ```title="examples/deadlock.py" --8<-- "examples/deadlock.py" ``` Running the script outputs the following: ```console ❯ uv run examples/deadlock.py Setting up logging to get output from signal handler... Registering signal handler... Starting actors... DEBUG:pykka:Registered ActorA (urn:uuid:09d5a2ab-6d1b-4fb6-aa7f-9b3d519b2378) DEBUG:pykka:Starting ActorA (urn:uuid:09d5a2ab-6d1b-4fb6-aa7f-9b3d519b2378) DEBUG:pykka:Registered ActorB (urn:uuid:9b4c697c-fe1c-44eb-b596-bf4dd8e3c4e9) DEBUG:pykka:Starting ActorB (urn:uuid:9b4c697c-fe1c-44eb-b596-bf4dd8e3c4e9) Now doing something stupid that will deadlock the actors... DEBUG:__main__:This is foo calling bar DEBUG:__main__:This is bar calling foo; BOOM! Making main thread relax; not block, not quit 1) Use `kill -SIGUSR1 1953031` to log thread tracebacks 2) Then `kill 1953031` to terminate the process ``` ## How to debug the deadlock The two actors are now deadlocked waiting for each other, while the main thread is idling, ready to process any signals. To debug the deadlock, send the `SIGUSR1` signal to the process, which has process ID 1953031 in this example: ```sh kill -SIGUSR1 1953031 ``` This makes the main thread log the current traceback for each thread. The logging output shows that the two actors are both waiting for data from the other actor: ```console hl_lines="15 35" CRITICAL:pykka:Current state of ActorB-2 (_actor_loop) (ident: 140349254620864): File "threading.py", line 1043, in _bootstrap self._bootstrap_inner() File "threading.py", line 1081, in _bootstrap_inner self._context.run(self.run) File "threading.py", line 1023, in run self._target(*self._args, **self._kwargs) File "pykka/_actor.py", line 229, in _actor_loop self._actor_loop_running() File "pykka/_actor.py", line 242, in _actor_loop_running response = self._handle_receive(envelope.message) File "pykka/_actor.py", line 352, in _handle_receive return callee(*message.args, **message.kwargs) File "examples/deadlock.py", line 35, in bar return self.a.foo().get() File "pykka/_threading.py", line 69, in get self._condition.wait(timeout=remaining) File "threading.py", line 369, in wait waiter.acquire() CRITICAL:pykka:Current state of ActorA-1 (_actor_loop) (ident: 140349263013568): File "threading.py", line 1043, in _bootstrap self._bootstrap_inner() File "threading.py", line 1081, in _bootstrap_inner self._context.run(self.run) File "threading.py", line 1023, in run self._target(*self._args, **self._kwargs) File "pykka/_actor.py", line 229, in _actor_loop self._actor_loop_running() File "pykka/_actor.py", line 242, in _actor_loop_running response = self._handle_receive(envelope.message) File "pykka/_actor.py", line 352, in _handle_receive return callee(*message.args, **message.kwargs) File "examples/deadlock.py", line 25, in foo return b.bar().get() File "pykka/_threading.py", line 69, in get self._condition.wait(timeout=remaining) File "threading.py", line 369, in wait waiter.acquire() CRITICAL:pykka:Current state of MainThread (ident: 140349271754624): File "examples/deadlock.py", line 59, in time.sleep(1) File "pykka/debug.py", line 62, in log_thread_tracebacks stack = "".join(traceback.format_stack(frame)) ``` pykka-4.5.0/docs/guides/index.md0000644000000000000000000000060115262536163013434 0ustar00# Guides This section contains guides and tutorials for using Pykka. - [Logging](logging.md) - learn how to configure logging in Pykka. - [Unit testing](testing.md) - learn how to test code that uses Pykka. - [Deadlock debugging](deadlocks.md) - learn how to debug deadlocks in Pykka applications. - [Proxy typing](proxy-typing.md) - learn how to add type hints for actor proxies. pykka-4.5.0/docs/guides/logging.md0000644000000000000000000000632515262536163013764 0ustar00# Logging Pykka uses Python's standard [`logging`][logging] module for logging debug messages and any unhandled exceptions in the actors. All log messages emitted by Pykka are issued to the logger named `pykka`, or a sub-logger of it. ## Log levels Pykka logs at several different log levels, so that you can filter out the parts you're not interested in: /// define [`logging.CRITICAL`][logging.CRITICAL] (highest) - This level is only used by the debug helpers in [`pykka.debug`][pykka.debug]. [`logging.ERROR`][logging.ERROR] - Exceptions raised by an actor that are not captured into a reply future are logged at this level. [`logging.WARNING`][logging.WARNING] - [Unhandled messages](../getting-started/actors.md#unhandled-messages) and other potential programming errors are logged at this level. [`logging.INFO`][logging.INFO] - Exceptions raised by an actor that are captured into a reply future are logged at this level. If the future result is used elsewhere, the exceptions is reraised there too. If the future result isn't used, the log message is the only trace of the exception happening. To catch bugs earlier, it is recommended to show log messages this level during development. [`logging.DEBUG`][logging.DEBUG] (lowest) - Every time an actor is started or stopped, and registered or unregistered in the actor registry, a message is logged at this level. /// In summary, you probably want to always let log messages at [`WARNING`][logging.WARNING] and higher through, while [`INFO`][logging.INFO] should also be kept on during development. ## Log handlers Out of the box, Pykka is set up with [`logging.NullHandler`][logging.NullHandler] as the only log record handler. This is the recommended approach for logging in libraries, so that the application developer using the library will have full control over how the log messages from the library will be exposed to the application's users. In other words, if you want to see the log messages from Pykka anywhere, you need to add a useful handler to the root logger, or the logger named `pykka`, to get any log output from Pykka. The defaults provided by [`logging.basicConfig`][logging.basicConfig] is enough to get debug log messages from Pykka: ```py import logging logging.basicConfig(level=logging.DEBUG) ``` ## Recommended setup If your application is already using [`logging`][logging], and you want debug log output from your own application, but not from Pykka, you can ignore debug log messages from Pykka by increasing the threshold on the Pykka logger to [`INFO`][logging.INFO] level or higher: ```py import logging logging.basicConfig(level=logging.DEBUG) logging.getLogger("pykka").setLevel(logging.INFO) ``` Given that you've fixed all unhandled exceptions logged at the [`INFO`][logging.INFO] level during development, you probably want to disable logging from Pykka at the [`INFO`][logging.INFO] level in production to avoid logging exceptions that are properly handled: ```py import logging logging.basicConfig(level=logging.DEBUG) logging.getLogger("pykka").setLevel(logging.WARNING) ``` For more details on how to use [`logging`][logging], please refer to the Python standard library documentation. pykka-4.5.0/docs/guides/proxy-typing.md0000644000000000000000000000134115262536163015020 0ustar00# Proxy typing Since Pykka 4.0, Pykka has complete type hints for the public API, tested using both [Mypy](https://www.mypy-lang.org/) and [Pyright](https://github.com/microsoft/pyright). Due to the dynamic nature of [`ActorProxy`][pykka.ActorProxy] objects, it is not possible to automatically type them correctly. The [`pykka.typing` module][pykka.typing] contains helpers to manually create additional classes that correctly describe the type hints for the proxy objects. In cases where a proxy objects is used a lot, this might be worth the extra effort to increase development speed and catch bugs earlier. ## Example ```py title="examples/proxy_typing.py" --8<-- "examples/proxy_typing.py" ``` pykka-4.5.0/docs/guides/testing.md0000644000000000000000000000247315262536163014013 0ustar00# Unit testing Pykka actors can be tested using the regular Python testing tools like [pytest](https://pytest.org/), [`unittest`][unittest], and [`unittest.mock`][unittest.mock]. ## General approach To test actors in a setting as close to production as possible, a typical pattern is the following: 1. In the test setup, start an actor together with any actors/collaborators it depends on. The dependencies will often be replaced by mocks to control their behavior. 2. In the test, [`ask()`][pykka.ActorRef.ask] or [`tell()`][pykka.ActorRef.tell] the actor something. 3. In the test, assert on the actor's state or the return value from the [`ask()`][pykka.ActorRef.ask] call. 4. In the test teardown, stop the actor to properly clean up before the next test. ## Example Let's look at an example actor that we want to test: ```title="examples/testing.py" --8<-- "examples/testing.py" ``` We can test this actor with [pytest](https://pytest.org/) by mocking the consumer and asserting that it receives a newly produced item: ```title="examples/testing_test.py" --8<-- "examples/testing_test.py" ``` If this way of setting up and tearing down test resources is unfamiliar to you, it is strongly recommended to read up on pytest's great [fixture](https://docs.pytest.org/en/stable/fixture.html) feature. pykka-4.5.0/docs/javascript/readthedocs.js0000644000000000000000000000204515262536163015520 0ustar00// Use CustomEvent to generate the version selector document.addEventListener("readthedocs-addons-data-ready", function (event) { const config = event.detail.data(); const versioning = `
`; // 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.md0000644000000000000000000000005615262536163014302 0ustar00# Actors ::: pykka.Actor ::: pykka.ActorRef pykka-4.5.0/docs/reference/debug.md0000644000000000000000000000004115262536163014067 0ustar00# Debug helpers ::: pykka.debug pykka-4.5.0/docs/reference/exceptions.md0000644000000000000000000000007215262536163015166 0ustar00# Exceptions ::: pykka.ActorDeadError ::: pykka.Timeout pykka-4.5.0/docs/reference/futures.md0000644000000000000000000000005715262536163014505 0ustar00# Futures ::: pykka.Future ::: pykka.get_all pykka-4.5.0/docs/reference/index.md0000644000000000000000000000141215262536163014113 0ustar00# 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.md0000644000000000000000000000004615262536163014615 0ustar00# Message objects ::: pykka.messages pykka-4.5.0/docs/reference/proxies.md0000644000000000000000000000012015262536163014470 0ustar00# Proxies ::: pykka.ActorProxy ::: pykka.CallableProxy ::: pykka.traversable pykka-4.5.0/docs/reference/registry.md0000644000000000000000000000004415262536163014654 0ustar00# Registry ::: pykka.ActorRegistry pykka-4.5.0/docs/reference/runtimes.md0000644000000000000000000000117115262536163014654 0ustar00# 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.md0000644000000000000000000000004315262536163014315 0ustar00# Typing helpers ::: pykka.typing pykka-4.5.0/docs/stylesheets/extra.css0000644000000000000000000000110015262536163014727 0ustar00.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.py0000755000000000000000000000161315262536163013053 0ustar00#!/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.py0000755000000000000000000000171315262536163014315 0ustar00#!/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.py0000755000000000000000000000271615262536163013545 0ustar00#!/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.py0000755000000000000000000000357715262536163013166 0ustar00#!/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.py0000644000000000000000000000243015262536163014540 0ustar00from 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.py0000644000000000000000000000050315262536163013441 0ustar00from 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.py0000644000000000000000000000225415262536163014505 0ustar00from __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.py0000755000000000000000000000215715262536163014340 0ustar00#!/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__.py0000644000000000000000000000144615262536163013622 0ustar00"""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.py0000644000000000000000000003230715262536163013332 0ustar00from __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.py0000644000000000000000000000144215262536163014033 0ustar00from __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.py0000644000000000000000000000035615262536163014402 0ustar00__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.py0000644000000000000000000002414415262536163013534 0ustar00from __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.py0000644000000000000000000000543315262536163015122 0ustar00from __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.py0000644000000000000000000002552315262536163013405 0ustar00from __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.py0000644000000000000000000001563115262536163012777 0ustar00from __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.py0000644000000000000000000001316615262536163014074 0ustar00from __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.py0000644000000000000000000001172715262536163014172 0ustar00from __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.py0000644000000000000000000000046715262536163013370 0ustar00from __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.py0000644000000000000000000000451615262536163013152 0ustar00"""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.py0000644000000000000000000000407315262536163013671 0ustar00"""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.typed0000644000000000000000000000000015262536163013171 0ustar00pykka-4.5.0/src/pykka/typing.py0000644000000000000000000000272615262536163013377 0ustar00"""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__.py0000644000000000000000000000000015262536163013037 0ustar00pykka-4.5.0/tests/conftest.py0000644000000000000000000000742615262536163013150 0ustar00from __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.py0000644000000000000000000000336515262536163013577 0ustar00import 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.py0000644000000000000000000002147215262536163013467 0ustar00from __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.py0000644000000000000000000000035515262536163014171 0ustar00from 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.py0000644000000000000000000002302015262536163013660 0ustar00from __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.py0000644000000000000000000001410615262536163014001 0ustar00from __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.py0000644000000000000000000000160715262536163014164 0ustar00from 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.py0000644000000000000000000001164315262536163013132 0ustar00from __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.py0000644000000000000000000001660715262536163014233 0ustar00from __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.py0000644000000000000000000000310415262536163015504 0ustar00from __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.py0000644000000000000000000000142515262536163012460 0ustar00from __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__.py0000644000000000000000000000000015262536163014220 0ustar00pykka-4.5.0/tests/proxy/test_attribute_access.py0000644000000000000000000000630615262536163017063 0ustar00from __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.py0000644000000000000000000000346515262536163017704 0ustar00from __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.py0000644000000000000000000000546515262536163015173 0ustar00from __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.py0000644000000000000000000001220415262536163014712 0ustar00from __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.py0000644000000000000000000001012715262536163017540 0ustar00from __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.py0000644000000000000000000000664315262536163016055 0ustar00from __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.py0000644000000000000000000000316215262536163016122 0ustar00from __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/.gitignore0000644000000000000000000000014715262536163011570 0ustar00*.egg-info /*.lock /.*_cache/ /.coverage /.jj/ /.tox/ /.venv/ /dist/ /docs/_build/ /site/ __pycache__/ pykka-4.5.0/LICENSE0000644000000000000000000002613615262536163010613 0ustar00 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.md0000644000000000000000000000305015262536163011053 0ustar00# 🌀 Pykka _Pykka makes it easier to build concurrent applications._ [![CI](https://img.shields.io/github/actions/workflow/status/jodal/pykka/ci.yml?branch=main)](https://github.com/jodal/pykka/actions/workflows/ci.yml) [![Docs](https://img.shields.io/readthedocs/pykka)](https://pykka.readthedocs.io/en/latest/) [![Coverage](https://img.shields.io/codecov/c/gh/jodal/pykka)](https://codecov.io/gh/jodal/pykka) [![PyPI](https://img.shields.io/pypi/v/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.toml0000644000000000000000000001175215262536163012520 0ustar00[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-INFO0000644000000000000000000000417115262536163010676 0ustar00Metadata-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._ [![CI](https://img.shields.io/github/actions/workflow/status/jodal/pykka/ci.yml?branch=main)](https://github.com/jodal/pykka/actions/workflows/ci.yml) [![Docs](https://img.shields.io/readthedocs/pykka)](https://pykka.readthedocs.io/en/latest/) [![Coverage](https://img.shields.io/codecov/c/gh/jodal/pykka)](https://codecov.io/gh/jodal/pykka) [![PyPI](https://img.shields.io/pypi/v/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).