././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/LICENSE0000644000000000000000000002613614510735656010612 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. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/README.md0000644000000000000000000000305614510735656011060 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. For a quickstart guide and a complete API reference, see the [documentation](https://pykka.readthedocs.io/). ## Installation Pykka requires Python 3.8 or newer. Pykka is available from [PyPI](https://pypi.org/project/pykka/): ``` python3 -m pip install pykka ``` ## 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-2023 Stein Magnus Jodal and contributors. Pykka is licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/Makefile0000644000000000000000000001073414510735656012172 0ustar00# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pykka.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pykka.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Pykka" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pykka" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/_build/.gitignore0000644000000000000000000000000014510735656013741 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/api/actors.rst0000644000000000000000000000015414510735656013323 0ustar00====== Actors ====== .. autoclass:: pykka.Actor :members: .. autoclass:: pykka.ActorRef :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/api/debug.rst0000644000000000000000000000725014510735656013122 0ustar00============= Debug helpers ============= .. automodule:: pykka.debug :members: Deadlock debugging ================== This is a complete example of how to use :func:`log_thread_tracebacks` to debug deadlocks: .. literalinclude:: ../../examples/deadlock_debugging.py Running the script outputs the following:: Setting up logging to get output from signal handler... Registering signal handler... Starting actors... DEBUG:pykka:Registered DeadlockActorA (urn:uuid:60803d09-cf5a-46cc-afdc-0c813e2e6647) DEBUG:pykka:Starting DeadlockActorA (urn:uuid:60803d09-cf5a-46cc-afdc-0c813e2e6647) DEBUG:pykka:Registered DeadlockActorB (urn:uuid:626adc83-ae35-439c-866a-85a3e29fd42c) DEBUG:pykka:Starting DeadlockActorB (urn:uuid:626adc83-ae35-439c-866a-85a3e29fd42c) Now doing something stupid that will deadlock the actors... DEBUG:root:This is foo calling bar DEBUG:root:This is bar calling foo; BOOM! Making main thread relax; not block, not quit 1) Use `kill -SIGUSR1 2284` to log thread tracebacks 2) Then `kill 2284` to terminate the process 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 PID 2284 in this example:: kill -SIGUSR1 2284 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:: CRITICAL:pykka:Current state of DeadlockActorB-2 (ident: 140151493752576): File "/usr/lib/python3.6/threading.py", line 884, in _bootstrap self._bootstrap_inner() File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner self.run() File "/usr/lib/python3.6/threading.py", line 864, in run self._target(*self._args, **self._kwargs) File ".../pykka/actor.py", line 195, in _actor_loop response = self._handle_receive(message) File ".../pykka/actor.py", line 297, in _handle_receive return callee(*message['args'], **message['kwargs']) File "examples/deadlock_debugging.py", line 25, in bar return self.a.foo().get() File ".../pykka/threading.py", line 47, in get self._data = self._queue.get(True, timeout) File "/usr/lib/python3.6/queue.py", line 164, in get self.not_empty.wait() File "/usr/lib/python3.6/threading.py", line 295, in wait waiter.acquire() CRITICAL:pykka:Current state of DeadlockActorA-1 (ident: 140151572883200): File "/usr/lib/python3.6/threading.py", line 884, in _bootstrap self._bootstrap_inner() File "/usr/lib/python3.6/threading.py", line 916, in _bootstrap_inner self.run() File "/usr/lib/python3.6/threading.py", line 864, in run self._target(*self._args, **self._kwargs) File ".../pykka/actor.py", line 195, in _actor_loop response = self._handle_receive(message) File ".../pykka/actor.py", line 297, in _handle_receive return callee(*message['args'], **message['kwargs']) File "examples/deadlock_debugging.py", line 15, in foo return b.bar().get() File ".../pykka/threading.py", line 47, in get self._data = self._queue.get(True, timeout) File "/usr/lib/python3.6/queue.py", line 164, in get self.not_empty.wait() File "/usr/lib/python3.6/threading.py", line 295, in wait waiter.acquire() CRITICAL:pykka:Current state of MainThread (ident: 140151593330496): File ".../examples/deadlock_debugging.py", line 49, in time.sleep(1) File ".../pykka/debug.py", line 63, in log_thread_tracebacks stack = ''.join(traceback.format_stack(frame)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/api/exceptions.rst0000644000000000000000000000015414510735656014211 0ustar00========== Exceptions ========== .. autoexception:: pykka.ActorDeadError .. autoexception:: pykka.Timeout ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/api/futures.rst0000644000000000000000000000014414510735656013524 0ustar00======= Futures ======= .. autoclass:: pykka.Future :members: .. autofunction:: pykka.get_all ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/api/logging.rst0000644000000000000000000000607414510735656013465 0ustar00.. _logging: ======= Logging ======= Pykka uses Python's standard :mod:`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: :attr:`~logging.CRITICAL` (highest) This level is only used by the debug helpers in :mod:`pykka.debug`. :attr:`~logging.ERROR` Exceptions raised by an actor that are not captured into a reply future are logged at this level. :attr:`~logging.WARNING` Unhandled messages and other potential programming errors are logged at this level. :attr:`~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. :attr:`~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 :attr:`~logging.WARNING` and higher through, while :attr:`~logging.INFO` should also be kept on during development. Log handlers ============ Out of the box, Pykka is set up with :class:`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 :func:`logging.basicConfig` is enough to get debug log messages from Pykka:: import logging logging.basicConfig(level=logging.DEBUG) Recommended setup ================= If your application is already using :mod:`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 :attr:`~logging.INFO` level or higher:: import logging logging.basicConfig(level=logging.DEBUG) logging.getLogger('pykka').setLevel(logging.INFO) Given that you've fixed all unhandled exceptions logged at the :attr:`~logging.INFO` level during development, you probably want to disable logging from Pykka at the :attr:`~logging.INFO` level in production to avoid logging exceptions that are properly handled:: import logging logging.basicConfig(level=logging.DEBUG) logging.getLogger('pykka').setLevel(logging.WARNING) For more details on how to use :mod:`logging`, please refer to the Python standard library documentation. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/api/messages.rst0000644000000000000000000000014414510735656013636 0ustar00======== Messages ======== .. automodule:: pykka.messages :members: .. versionadded:: 2.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/api/module.rst0000644000000000000000000000020414510735656013311 0ustar00====== Module ====== .. module:: pykka .. attribute:: __version__ Pykka's :pep:`386` and :pep:`396` compatible version number ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/api/proxies.rst0000644000000000000000000000027414510735656013524 0ustar00======= Proxies ======= .. autoclass:: pykka.ActorProxy :members: .. autoclass:: pykka.CallableProxy :members: .. automethod:: __call__ .. autofunction:: pykka.traversable ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/api/registry.rst0000644000000000000000000000011514510735656013675 0ustar00======== Registry ======== .. autoclass:: pykka.ActorRegistry :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/api/typing.rst0000644000000000000000000000026014510735656013340 0ustar00========== Type hints ========== .. automodule:: pykka.typing :members: proxy_field, proxy_method .. versionadded:: 4.0 .. autoclass:: pykka.typing.ActorMemberMixin ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/conf.py0000644000000000000000000000142214510735656012023 0ustar00"""Pykka documentation build configuration file.""" import toml project = "Pykka" author = "Stein Magnus Jodal and contributors" copyright = f"2010-2023, {author}" # noqa: A001 release = toml.load("../pyproject.toml")["tool"]["poetry"]["version"] version = ".".join(release.split(".")[:2]) extensions = [ "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", ] html_theme = "sphinx_rtd_theme" html_use_modindex = True html_use_index = True html_split_index = False html_show_sourcelink = True modindex_common_prefix = ["pykka."] autodoc_member_order = "bysource" extlinks = { "issue": ("https://github.com/jodal/pykka/issues/%s", "#%s"), } intersphinx_mapping = { "python": ("https://docs.python.org/3", None), } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/examples.rst0000644000000000000000000001064314510735656013101 0ustar00======== Examples ======== The ``examples/`` dir in `Pykka's Git repo `_ includes some runnable examples of Pykka usage. Plain actor =========== .. literalinclude:: ../examples/plain_actor.py Output:: [{'no': 'Norway', 'se': 'Sweden'}, {'a': 3, 'b': 4, 'c': 5}] Actor with proxy ================ .. literalinclude:: ../examples/typed_actor.py Output:: MainThread: calling AnActor.proc() ... MainThread: calling AnActor.func() ... MainThread: printing result ... (blocking) AnActor-1: this was printed by AnActor.proc() MainThread: this was returned by AnActor.func() 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 MainThread: calling AnActor.proc() ... MainThread: calling AnActor.func() ... MainThread: printing result ... (blocking) AnActor-1: this was printed by AnActor.proc() MainThread: this was returned by AnActor.func() after a delay MainThread: reading AnActor.field ... MainThread: printing result ... (blocking) MainThread: new value MainThread: writing AnActor.field ... MainThread: printing new field value ... (blocking) MainThread: new value MainThread: calling AnActor.proc() ... MainThread: calling AnActor.func() ... AnActor-1: this was printed by AnActor.proc() MainThread: printing result ... (blocking) MainThread: this was returned by AnActor.func() after a delay MainThread: reading AnActor.field ... MainThread: printing result ... (blocking) MainThread: new value MainThread: writing AnActor.field ... MainThread: printing new field value ... (blocking) MainThread: new value Multiple cooperating actors =========================== .. literalinclude:: ../examples/counter.py Output:: Adder (urn:uuid:f50029eb-7cea-4ab9-98bf-a5bf65af8b8f) is increasing 0 Bookkeeper (urn:uuid:4f2d4e78-7a33-4c4f-86ac-7c415a7205f4) got 1 back Adder (urn:uuid:f50029eb-7cea-4ab9-98bf-a5bf65af8b8f) is increasing 1 Bookkeeper (urn:uuid:4f2d4e78-7a33-4c4f-86ac-7c415a7205f4) got 2 back Adder (urn:uuid:f50029eb-7cea-4ab9-98bf-a5bf65af8b8f) is increasing 2 Bookkeeper (urn:uuid:4f2d4e78-7a33-4c4f-86ac-7c415a7205f4) got 3 back Adder (urn:uuid:f50029eb-7cea-4ab9-98bf-a5bf65af8b8f) is increasing 3 Bookkeeper (urn:uuid:4f2d4e78-7a33-4c4f-86ac-7c415a7205f4) got 4 back Adder (urn:uuid:f50029eb-7cea-4ab9-98bf-a5bf65af8b8f) is increasing 4 Bookkeeper (urn:uuid:4f2d4e78-7a33-4c4f-86ac-7c415a7205f4) got 5 back Adder (urn:uuid:f50029eb-7cea-4ab9-98bf-a5bf65af8b8f) is increasing 5 Bookkeeper (urn:uuid:4f2d4e78-7a33-4c4f-86ac-7c415a7205f4) got 6 back Adder (urn:uuid:f50029eb-7cea-4ab9-98bf-a5bf65af8b8f) is increasing 6 Bookkeeper (urn:uuid:4f2d4e78-7a33-4c4f-86ac-7c415a7205f4) got 7 back Adder (urn:uuid:f50029eb-7cea-4ab9-98bf-a5bf65af8b8f) is increasing 7 Bookkeeper (urn:uuid:4f2d4e78-7a33-4c4f-86ac-7c415a7205f4) got 8 back Adder (urn:uuid:f50029eb-7cea-4ab9-98bf-a5bf65af8b8f) is increasing 8 Bookkeeper (urn:uuid:4f2d4e78-7a33-4c4f-86ac-7c415a7205f4) got 9 back Adder (urn:uuid:f50029eb-7cea-4ab9-98bf-a5bf65af8b8f) is increasing 9 Bookkeeper (urn:uuid:4f2d4e78-7a33-4c4f-86ac-7c415a7205f4) got 10 back Pool of actors sharing work =========================== .. literalinclude:: ../examples/resolver.py Mopidy music server =================== Pykka was originally created back in 2011 as a formalization of concurrency patterns that emerged in the `Mopidy music server `_. 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 frontend servers or the Spotify and Google 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 `_ to find many real life examples of Pykka usage. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/index.rst0000644000000000000000000000370014510735656012366 0ustar00===== Pykka ===== Pykka is a Python implementation of the `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. For details and code examples, see the `Pykka documentation `_. Pykka is available from PyPI. To install it, run:: pip install pykka Pykka works with Python 3.8 or newer. Inpiration ========== Much of the naming of concepts and methods in Pykka is taken from the `Akka `_ project which implements actors on the JVM. Though, Pykka does not aim to be a Python port of Akka, and supports far fewer features. Notably, Pykka **does not** support the following features: - Supervision: Linking actors, supervisors, or supervisor groups. - Remoting: Communicating with actors running on other hosts. - Routers: Pykka does not come with a set of predefined message routers, though you may make your own actors for routing messages. Project resources ================= - `Documentation `_ - `Source code `_ - `Releases `_ - `Issue tracker `_ - `Contributors `_ - `Users `_ .. toctree:: :maxdepth: 2 :caption: Usage quickstart examples runtimes/index testing .. toctree:: :maxdepth: 2 :caption: Reference api/module api/actors api/proxies api/futures api/registry api/exceptions api/messages api/logging api/debug api/typing License ======= Pykka is copyright 2010-2023 Stein Magnus Jodal and contributors. Pykka is licensed under the `Apache License, Version 2.0 `_. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/quickstart.rst0000644000000000000000000002357714510735656013467 0ustar00========== Quickstart ========== Pykka is a Python implementation of the `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. Rules of the actor model ======================== - 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. The actor implementations ========================= Pykka's actor API comes with the following implementations: - Threads: Each :class:`~pykka.ThreadingActor` is executed by a regular thread, i.e. :class:`threading.Thread`. As handles for future results, it uses :class:`~pykka.ThreadingFuture` which is a thin wrapper around a :class:`queue.Queue`. It has no dependencies outside Python itself. :class:`~pykka.ThreadingActor` plays well together with non-actor threads. Pykka 2 and earlier shipped with some alternative implementations that were removed in Pykka 3: - gevent: Each actor was executed by a `gevent `_ greenlet. - Eventlet: Each actor was executed by an `Eventlet `_ greenlet. A basic actor ============= In its most basic form, a Pykka actor is a class with an :meth:`~pykka.Actor.on_receive` method:: import pykka class Greeter(pykka.ThreadingActor): def on_receive(self, message): print('Hi there!') To start an actor, you call the class' method :meth:`~pykka.Actor.start`, which starts the actor and returns an actor reference which can be used to communicate with the running actor:: actor_ref = Greeter.start() If you need to pass arguments to the actor upon creation, you can pass them to the :meth:`~pykka.Actor.start` method, and receive them using the regular ``__init__()`` method:: import pykka class Greeter(pykka.ThreadingActor): def __init__(self, greeting='Hi there!'): 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, when it stops, and when an unhandled exception is raised. Check out the full API docs for the details. To stop an actor, you can either call :meth:`~pykka.ActorRef.stop` on the :class:`~pykka.ActorRef`:: actor_ref.stop() Or, if an actor wants to stop itself, it can simply do so:: 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 :meth:`~pykka.ActorRef.tell` method or the :meth:`~pykka.ActorRef.ask` method on the ``actor_ref`` object. :meth:`~pykka.ActorRef.tell` will fire off a message without waiting for an answer. In other words, it will never block. :meth:`~pykka.ActorRef.ask` will by default block until an answer is returned, potentially forever. If you provide a ``timeout`` keyword argument to :meth:`~pykka.ActorRef.ask`, 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 pass ``block=False``, and :meth:`~pykka.ActorRef.ask` will immediately return a "future" object. The message itself can be of any type, for example a dict or your own message class type. Summarized in code:: 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. .. warning:: 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 :func:`copy.deepcopy` the data you're sending off to other actors. Replying to messages -------------------- If a message is sent using ``actor_ref.ask()`` you can reply to the sender of the message by simply returning a value from the :meth:`~pykka.Actor.on_receive` method:: 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!' :class:`None` is a valid response so if you return :class:`None` explicitly, or don't return at all, a response containing :class:`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 :meth:`~pykka.ActorRef.tell` or :meth:`~pykka.ActorRef.ask`. When the sender doesn't expect a response the :meth:`~pykka.Actor.on_receive` return value will be ignored. The situation is similar in regard to exceptions: when :meth:`~pykka.ActorRef.ask` is used and you raise an exception from within :meth:`~pykka.Actor.on_receive` method, the exception will propagate to the sender:: 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') Actor proxies ============= With the basic building blocks provided by actors and futures, we got everything we need to build more advanced abstractions. Pykka provides a single abstraction on top of the basic actor model, named "actor proxies". You can use Pykka without proxies, but we've found it to be a very convenient abstraction when building `Mopidy `_. Let's create an actor and start it:: 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:: 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. 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 :meth:`~pykka.Future.get` method to get the actual data:: 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 :meth:`~pykka.Future.get` just to block processing until the actor has completed processing your last message:: proxy.sub(5) proxy.add(3) proxy.last_result.get() # => 2 Since assignment doesn't return anything, it works just like on regular objects:: 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 :meth:`~pykka.ActorRef.ask` method 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 on proxies --------------------------------- Sometimes you'll want to access an actor attribute's 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 :func:`~pykka.traversable` function:: 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. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/runtimes/index.rst0000644000000000000000000000102614510735656014233 0ustar00======== Runtimes ======== By default, Pykka builds on top of Python's regular threading concurrency model, via the standard library modules :mod:`threading` and :mod:`queue`. Pykka 2 and earlier shipped with some alternative implementations that ran on top of :mod:`gevent` or :mod:`eventlet`. These alternative implementations were removed in Pykka 3. Note that Pykka does no attempt at supporting a mix of concurrency runtimes. Such a future feature has briefly been discussed in issue :issue:`11`. .. toctree:: threading ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0188503 pykka-4.0.1/docs/runtimes/threading.rst0000644000000000000000000000042714510735656015075 0ustar00========= Threading ========= Installation ============ The default threading runtime has no dependencies other than Pykka itself and the Python standard library. API === .. autoclass:: pykka.ThreadingFuture :members: .. autoclass:: pykka.ThreadingActor :members: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/docs/testing.rst0000644000000000000000000000233414510735656012736 0ustar00======= Testing ======= Pykka actors can be tested using the regular Python testing tools like `pytest `_, :mod:`unittest`, and :mod:`unittest.mock`. 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, :meth:`~pykka.ActorRef.ask` or :meth:`~pykka.ActorRef.tell` the actor something. 3. In the test, assert on the actor's state or the return value from the :meth:`~pykka.ActorRef.ask`. 4. In the test teardown, stop the actor to properly clean up before the next test. An example ========== Let's look at an example actor that we want to test: .. literalinclude:: ../examples/producer.py We can test this actor with `pytest`_ by mocking the consumer and asserting that it receives a newly produced item: .. literalinclude:: ../examples/producer_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 `_ feature. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/examples/counter.py0000755000000000000000000000116714510735656013454 0ustar00#!/usr/bin/env python3 import pykka class Adder(pykka.ThreadingActor): def add_one(self, i): print(f"{self} is increasing {i}") return i + 1 class Bookkeeper(pykka.ThreadingActor): def __init__(self, adder): super().__init__() self.adder = adder def count_to(self, target): i = 0 while i < target: i = self.adder.add_one(i).get() print(f"{self} got {i} back") if __name__ == "__main__": adder = Adder.start().proxy() bookkeeper = Bookkeeper.start(adder).proxy() bookkeeper.count_to(10).get() pykka.ActorRegistry.stop_all() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/examples/deadlock_debugging.py0000755000000000000000000000236514510735656015557 0ustar00#!/usr/bin/env python3 import logging import os import signal import time import pykka import pykka.debug class DeadlockActorA(pykka.ThreadingActor): def foo(self, b): logging.debug("This is foo calling bar") return b.bar().get() class DeadlockActorB(pykka.ThreadingActor): def __init__(self, a): super().__init__() self.a = a def bar(self): logging.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 = DeadlockActorA.start().proxy() b = DeadlockActorB.start(a).proxy() print("Now doing something stupid that will deadlock the actors...") a.foo(b) time.sleep(0.01) # Yield to actors, so we get output in a readable order 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/examples/plain_actor.py0000755000000000000000000000106514510735656014265 0ustar00#!/usr/bin/env python3 import pykka GetMessages = object() class PlainActor(pykka.ThreadingActor): def __init__(self): super().__init__() self.stored_messages = [] def on_receive(self, message): if message is GetMessages: return self.stored_messages self.stored_messages.append(message) return None if __name__ == "__main__": actor = PlainActor.start() actor.tell({"no": "Norway", "se": "Sweden"}) actor.tell({"a": 3, "b": 4, "c": 5}) print(actor.ask(GetMessages)) actor.stop() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/examples/producer.py0000644000000000000000000000040414510735656013606 0ustar00import pykka class ProducerActor(pykka.ThreadingActor): def __init__(self, consumer): super().__init__() self.consumer = consumer def produce(self): new_item = {"item": 1, "new": True} self.consumer.consume(new_item) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/examples/producer_test.py0000644000000000000000000000150114510735656014644 0ustar00import pytest from producer import ProducerActor @pytest.fixture() def consumer_mock(mocker): return mocker.Mock() @pytest.fixture() def producer(consumer_mock): # 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, producer): # 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}) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/examples/resolver.py0000755000000000000000000000243614510735656013636 0ustar00#!/usr/bin/env python3 """Resolve a bunch of IP addresses using a pool of resolver actors. Based on example contributed by Kristian Klette . Either run without arguments: ./resolver.py Or specify pool size and IPs to resolve: ./resolver.py 3 193.35.52.{1,2,3,4,5,6,7,8,9} """ import pprint import socket import sys import pykka class Resolver(pykka.ThreadingActor): def resolve(self, ip): try: info = socket.gethostbyaddr(ip) print(f"Finished resolving {ip}") return info[0] except Exception: print(f"Failed resolving {ip}") return None def run(pool_size, *ips): # Start resolvers resolvers = [Resolver.start().proxy() for _ in range(pool_size)] # Distribute work by mapping IPs to resolvers (not blocking) hosts = [] for i, ip in enumerate(ips): hosts.append(resolvers[i % len(resolvers)].resolve(ip)) # Gather results (blocking) ip_to_host = zip(ips, pykka.get_all(hosts)) pprint.pprint(list(ip_to_host)) # Clean up pykka.ActorRegistry.stop_all() if __name__ == "__main__": if len(sys.argv[1:]) >= 2: run(int(sys.argv[1]), *sys.argv[2:]) else: ips = [f"193.35.52.{i}" for i in range(1, 50)] run(10, *ips) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/examples/typed_actor.py0000755000000000000000000000263514510735656014313 0ustar00#!/usr/bin/env python3 import threading import time import pykka class AnActor(pykka.ThreadingActor): field = "this is the value of AnActor.field" def proc(self): log("this was printed by AnActor.proc()") def func(self): time.sleep(0.5) # Block a bit to make it realistic return "this was returned by AnActor.func() after a delay" def log(msg): thread_name = threading.current_thread().name print(f"{thread_name}: {msg}") if __name__ == "__main__": actor = AnActor.start().proxy() for _ in range(3): # Method with side effect log("calling AnActor.proc() ...") actor.proc() # Method with return value log("calling AnActor.func() ...") result = actor.func() # Does not block, returns a future log("printing result ... (blocking)") log(result.get()) # Blocks until ready # Field reading log("reading AnActor.field ...") result = actor.field # Does not block, returns a future log("printing result ... (blocking)") log(result.get()) # Blocks until ready # Field writing log("writing AnActor.field ...") actor.field = "new value" # Assignment does not block result = actor.field # Does not block, returns a future log("printing new field value ... (blocking)") log(result.get()) # Blocks until ready actor.stop() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/pyproject.toml0000644000000000000000000000751514510735656012521 0ustar00[tool.poetry] name = "pykka" version = "4.0.1" description = "Pykka is a Python implementation of the actor model" authors = ["Stein Magnus Jodal "] license = "Apache-2.0" readme = "README.md" homepage = "https://github.com/jodal/pykka" repository = "https://github.com/jodal/pykka" documentation = "https://pykka.readthedocs.io/" keywords = ["actor", "concurrency", "threading"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", ] include = ["docs", "examples", "tests"] [tool.poetry.dependencies] python = "^3.8.0" typing-extensions = { version = "^4.0.0", python = "<3.10" } [tool.poetry.group.black.dependencies] black = "^23.9.1" [tool.poetry.group.dev.dependencies] tox = "^4.11.3" [tool.poetry.group.docs.dependencies] sphinx = "^6.2.1" sphinx_rtd_theme = "^1.3.0" toml = "^0.10.2" [tool.poetry.group.mypy.dependencies] mypy = "^1.5.1" [tool.poetry.group.pyright.dependencies] pyright = "^1.1.327" [tool.poetry.group.ruff.dependencies] ruff = "^0.0.290" [tool.poetry.group.tests.dependencies] pytest = "^7.4.2" pytest-cov = "^4.1.0" pytest-mock = "^3.11.1" [tool.black] target-version = ["py38", "py39", "py310", "py311", "py312"] [tool.coverage.paths] source = ["src"] [tool.coverage.run] branch = true source = ["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.ruff] select = [ "A", # flake8-builtins "ANN", # flake8-annotations "ARG", # flake8-unused-arguments "B", # flake8-bugbear "C4", # flake8-comprehensions "C90", # mccabe "D", # pydocstyle "DTZ", # flake8-datetimez "E", # pycodestyle "ERA", # eradicate "F", # pyflakes "FBT", # flake8-boolean-trap "I", # isort "INP", # flake8-no-pep420 "ISC", # flake8-implicit-str-concat "N", # pep8-naming "PGH", # pygrep-hooks "PIE", # flake8-pie "PLC", # pylint convention "PLE", # pylint error "PLR", # pylint refactor "PLW", # pylint warning "PT", # flake8-pytest-style "PTH", # flake8-use-pathlib "Q", # flake8-quotes "RET", # flake8-return "RSE", # flake8-raise "RUF", # ruff "SIM", # flake8-simplify "SLF", # flake8-self "T20", # flake8-print "TCH", # flake8-type-checking "TID", # flake8-tidy-imports "TRY", # tryceratops "UP", # pyupgrade "W", # pycodestyle ] ignore = [ "A003", # builtin-attribute-shadowing "ANN101", # missing-type-self "ANN102", # missing-type-cls "ANN401", # any-type "D203", # one-blank-line-before-class "D213", # multi-line-summary-second-line "PLR2004", # magic-value-comparison "RET504", # unnecessary-assign "TRY003", # raise-vanilla-args # # Equivalent to `pyupgrade --keep-runtime-typing`: "UP006", # deprecated-collection-type "UP007", # typing-union ] target-version = "py38" [tool.ruff.per-file-ignores] "docs/*" = [ "D", # pydocstyle "INP001", # flake8-no-pep420 ] "examples/*" = [ "ANN", # flake8-annotations "D", # pydocstyle "INP001", # flake8-no-pep420 "T20", # flake8-print ] "tests/*" = [ "ANN", # flake8-annotations "ARG", # flake8-unused-arguments "D", # pydocstyle "TRY002", # raise-vanilla-class ] [tool.ruff.isort] known-first-party = ["pykka"] [tool.pyright] pythonVersion = "3.8" venvPath = "." venv = ".venv" typeCheckingMode = "strict" # Already coverd by tests and careful import ordering: reportImportCycles = false # Already covered by flake8-self: reportPrivateUsage = false [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/__init__.py0000644000000000000000000000206414510735656013616 0ustar00"""Pykka is a Python implementation of the actor model.""" import importlib.metadata as _importlib_metadata 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", ] #: Pykka's :pep:`396` and :pep:`440` compatible version number __version__: str try: __version__ = _importlib_metadata.version(__name__) except _importlib_metadata.PackageNotFoundError: __version__ = "unknown" _logging.getLogger(__name__).addHandler(_logging.NullHandler()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/_actor.py0000644000000000000000000003033614510735656013331 0ustar00from __future__ import annotations import logging import sys import threading import uuid from typing import TYPE_CHECKING, Any, Optional, Protocol, TypeVar 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") A = TypeVar("A", bound="Actor") class ActorInbox(Protocol): def put(self, envelope: Envelope[Any], /) -> None: ... def get(self) -> Envelope[Any]: ... def empty(self) -> bool: ... class Actor: """An actor is an execution unit that executes concurrently with other actors. To create an actor: 1. subclass one of the :class:`Actor` implementations: - :class:`~pykka.ThreadingActor` 2. implement your methods, including :meth:`__init__`, as usual, 3. call :meth:`Actor.start` on your actor class, passing the method any arguments for your constructor. To stop an actor, call :meth:`Actor.stop()` or :meth:`ActorRef.stop()`. For example:: 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: type[A], *args: Any, **kwargs: Any, ) -> ActorRef[A]: """Start an actor. Starting an actor also registers it in the :class:`ActorRegistry `. Any arguments passed to :meth:`start` will be passed on to the class constructor. Behind the scenes, the following is happening when you call :meth:`start`: 1. The actor is created: 1. :attr:`actor_urn` is initialized with the assigned URN. 2. :attr:`actor_inbox` is initialized with a new actor inbox. 3. :attr:`actor_ref` is initialized with a :class:`pykka.ActorRef` object for safely communicating with the actor. 4. At this point, your :meth:`__init__()` code can run. 2. The actor is registered in :class:`pykka.ActorRegistry`. 3. The actor receive loop is started by the actor's associated thread/greenlet. :returns: a :class:`ActorRef` 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() # noqa: SLF001 return obj.actor_ref @staticmethod def _create_actor_inbox() -> ActorInbox: """Create an inbox for the actor. Internal method for implementors of new actor types. """ raise NotImplementedError("Use a subclass of Actor") @staticmethod def _create_future() -> Future[Any]: """Create a future for the actor. Internal method for implementors of new actor types. """ raise NotImplementedError("Use a subclass of Actor") def _start_actor_loop(self) -> None: """Create and start the actor's event loop. Internal method for implementors of new actor types. """ raise NotImplementedError("Use a subclass of Actor") #: The actor URN string is a universally unique identifier for the actor. #: It may be used for looking up a specific actor using #: :meth:`ActorRegistry.get_by_urn`. actor_urn: str #: The actor's inbox. Use :meth:`ActorRef.tell`, :meth:`ActorRef.ask`, and #: friends to put messages in the inbox. actor_inbox: ActorInbox _actor_ref: ActorRef[Any] @property def actor_ref(self: A) -> ActorRef[A]: """The actor's :class:`ActorRef` instance.""" # This property only exists to improve the typing of the ActorRef. return self._actor_ref #: A :class:`threading.Event` representing whether or not the actor should #: continue processing messages. Use :meth:`stop` to change it. actor_stopped: threading.Event def __init__( self, *_args: Any, **_kwargs: Any, ) -> None: """Create actor. Your are free to override :meth:`__init__`, but you must call your superclass' :meth:`__init__` to ensure that fields :attr:`actor_urn`, :attr:`actor_inbox`, and :attr:`actor_ref` are initialized. You can use :func:`super`:: super().__init__() Or call you superclass directly:: pykka.ThreadingActor.__init__(self) :meth:`__init__` is called before the actor is started and registered in :class:`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 :meth:`ActorRef.stop` with ``block=False``. """ 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: 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: 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: self._handle_failure(*sys.exc_info()) except BaseException: 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 " f"handling the message" ), None, ) ) def on_start(self) -> None: """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. For :class:`ThreadingActor`, this method is executed in the actor's own thread, while :meth:`__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: """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 :meth:`on_failure` hook is called instead. For :class:`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: Optional[type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> 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] # noqa: E501 ) ActorRegistry.unregister(self.actor_ref) self.actor_stopped.set() def on_failure( self, exception_type: Optional[type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> 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. For :class:`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 :func:`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. :param message: the message to handle :type message: any :returns: anything that should be sent as a reply to the sender """ logger.warning(f"Unexpected message received by {self}: {message}") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/_envelope.py0000644000000000000000000000160614510735656014034 0ustar00from __future__ import annotations from typing import TYPE_CHECKING, Any, Generic, Optional, 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. :param message: the message to send :type message: any :param reply_to: the future to reply to if there is a response :type reply_to: :class:`pykka.Future` """ # Using slots speeds up envelope creation with ~20% __slots__ = ["message", "reply_to"] message: T reply_to: Optional[Future[Any]] def __init__(self, message: T, reply_to: Optional[Future[Any]] = 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})" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/_exceptions.py0000644000000000000000000000035614510735656014401 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.""" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/_future.py0000644000000000000000000002273114510735656013533 0ustar00from __future__ import annotations import functools from typing import ( TYPE_CHECKING, Any, Callable, Generator, Generic, Iterable, Optional, TypeVar, cast, ) if TYPE_CHECKING: from typing_extensions import TypeAlias 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[[Optional[float]], T] 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 :meth:`Future.get` or ``await`` the future. """ _get_hook: Optional[GetHookFunc[T]] _get_hook_result: Optional[T] def __init__(self) -> None: super().__init__() self._get_hook = None self._get_hook_result = None def __repr__(self) -> str: return "" def get( self, *, timeout: Optional[float] = 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 :class:`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 :exc:`pykka.Timeout`. The encapsulated value can be retrieved multiple times. The future will only block the first time the value is accessed. :param timeout: seconds to wait before timeout :type timeout: float or :class:`None` :raise: :exc:`pykka.Timeout` if timeout is reached :raise: encapsulated value if it is an exception :return: encapsulated value if it is not an exception """ if self._get_hook is not None: if self._get_hook_result is None: self._get_hook_result = self._get_hook(timeout) return self._get_hook_result raise NotImplementedError def set( self, value: Optional[T] = None, ) -> None: """Set the encapsulated value. :param value: the encapsulated value or nothing :type value: any object or :class:`None` :raise: an exception if set is called multiple times """ raise NotImplementedError def set_exception( self, exc_info: Optional[OptExcInfo] = None, ) -> None: """Set an exception as the encapsulated value. You can pass an ``exc_info`` three-tuple, as returned by :func:`sys.exc_info`. If you don't pass ``exc_info``, :func:`sys.exc_info` will be called and the value returned by it used. In other words, if you're calling :meth:`set_exception`, without any arguments, from an except block, the exception you're currently handling will automatically be set on the future. :param exc_info: the encapsulated exception :type exc_info: three-tuple of (exc_class, exc_instance, traceback) """ raise NotImplementedError def set_get_hook( self, func: GetHookFunc[T], ) -> None: """Set a function to be executed when :meth:`get` is called. The function will be called when :meth:`get` is called, with the ``timeout`` value as the only argument. The function's return value will be returned from :meth:`get`. .. versionadded:: 1.2 :param func: called to produce return value of :meth:`get` :type func: function accepting a timeout value """ 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, :meth:`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 :exc:`TypeError` will be raised when :meth:`get` is called. Example:: >>> import pykka >>> f = pykka.ThreadingFuture() >>> g = f.filter(lambda x: x > 10) >>> g >>> f.set(range(5, 15)) >>> f.get() [5, 6, 7, 8, 9, 10, 11, 12, 13, 14] >>> g.get() [11, 12, 13, 14] .. versionadded:: 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 :meth:`join`. The new future returns a list with the results from all the joined futures. Example:: >>> import pykka >>> a = pykka.ThreadingFuture() >>> b = pykka.ThreadingFuture() >>> c = pykka.ThreadingFuture() >>> f = a.join(b, c) >>> a.set('def') >>> b.set(123) >>> c.set(False) >>> f.get() ['def', 123, False] .. versionadded:: 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:: >>> 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' .. versionadded:: 1.2 .. versionchanged:: 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 is unpredictable and makes regular use cases like extracting a single field from a dict difficult, thus the behavior has been simplified. Now, 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 :exc:`TypeError` is raised. :meth:`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:: >>> 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 .. versionadded:: 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[Any]], *, timeout: Optional[float] = None, ) -> Iterable[Any]: """Collect all values encapsulated in the list of futures. If ``timeout`` is not :class:`None`, the method will wait for a reply for ``timeout`` seconds, and then raise :exc:`pykka.Timeout`. :param futures: futures for the results to collect :type futures: list of :class:`pykka.Future` :param timeout: seconds to wait before timeout :type timeout: float or :class:`None` :raise: :exc:`pykka.Timeout` if timeout is reached :returns: list of results """ return [future.get(timeout=timeout) for future in futures] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/_introspection.py0000644000000000000000000000524014510735656015115 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) if attr == proxy: 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: for attr_name in dir(attr): attr_paths_to_visit.append((*attr_path, attr_name)) 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: raise AttributeError( f"type object {parent.__class__.__name__!r} " f"has no attribute {attr_name!r}" ) 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/_proxy.py0000644000000000000000000002515114510735656013401 0ustar00from __future__ import annotations import logging from typing import TYPE_CHECKING, Any, Generic, Optional, 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]): """An :class:`ActorProxy` wraps an :class:`ActorRef ` instance. The proxy allows the referenced actor to be used through regular method calls and field access. You can create an :class:`ActorProxy` from any :class:`ActorRef `:: actor_ref = MyActor.start() actor_proxy = ActorProxy(actor_ref) You can also get an :class:`ActorProxy` by using :meth:`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 :class:`Future ` object back. To get the enclosed value from the future, you must call :meth:`get() ` on the returned future:: print(actor_proxy.string_attribute.get()) print(actor_proxy.count().get() + 1) If you call a method just for it's side effects and do not care about the return value, you do not need to accept the returned future or call :meth:`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 :meth:`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 :class:`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 :attr:`actor_ref ` attribute:: proxy_to_myself_in_the_future = self.actor_ref.proxy() If you create a proxy in your actor's constructor or :meth:`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 ``_``. **Examples** An example of :class:`ActorProxy` usage: .. literalinclude:: ../../examples/counter.py :param actor_ref: reference to the actor to proxy :type actor_ref: :class:`pykka.ActorRef` :raise: :exc:`pykka.ActorDeadError` if actor is not available """ #: The actor's :class:`pykka.ActorRef` instance. actor_ref: ActorRef[A] _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: Optional[AttrPath] = None, ) -> None: if not actor_ref.is_alive(): raise ActorDeadError(f"{actor_ref} not found") self.actor_ref = actor_ref self._actor = actor_ref._actor # noqa: SLF001 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 if self._attr_path != other._attr_path: return False return True 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: raise AttributeError(f"{self} has no attribute {name!r}") 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. :class:`CallableProxy` instances are returned when accessing methods on a :class:`ActorProxy` without calling them. Example:: 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 :meth:`~pykka.ActorRef.ask` semantics. Returns a future which will yield the called method's return value. If the call raises an exception is set on the future, and will be reraised by :meth:`~pykka.Future.get`. If the future is left unused, the exception will not be reraised. Either way, the exception will also be logged. See :ref:`logging` 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 :meth:`~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 :meth:`~pykka.Actor.on_failure` hook is called instead. .. versionadded:: 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 :class:`~pykka.ActorProxy`. Used as a function to mark a single attribute:: class AnActor(pykka.ThreadingActor): playback = pykka.traversable(Playback()) class Playback(object): 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(object): 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 :class:`True`:: class AnActor(pykka.ThreadingActor): playback = Playback() class Playback(object): 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 .. versionadded:: 2.0 """ if hasattr(obj, "__slots__"): raise ValueError( "pykka.traversable() cannot be used to mark " "an object using slots as traversable." ) obj._pykka_traversable = True # type: ignore[attr-defined] # noqa: SLF001 return obj ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/_ref.py0000644000000000000000000001565214510735656013001 0ustar00from __future__ import annotations from typing import ( TYPE_CHECKING, Any, Generic, Literal, Optional, TypeVar, Union, 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. :class:`ActorRef` instances are returned by :meth:`Actor.start` and the lookup methods in :class:`ActorRegistry `. You should never need to create :class:`ActorRef` instances yourself. :param actor: the actor to wrap :type actor: :class:`Actor` """ #: The class of the referenced actor. actor_class: type[A] #: See :attr:`Actor.actor_urn`. actor_urn: str #: See :attr:`Actor.actor_inbox`. actor_inbox: ActorInbox #: See :attr:`Actor.actor_stopped`. actor_stopped: Event def __init__( self: ActorRef[A], actor: A, ) -> None: self._actor = 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 :meth:`is_alive` returns :class:`True`. :return: Returns :class:`True` if actor is alive, :class:`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. :param message: message to send :type message: any :raise: :exc:`pykka.ActorDeadError` if actor is not available :return: nothing """ if not self.is_alive(): raise ActorDeadError(f"{self} not found") self.actor_inbox.put(Envelope(message)) @overload def ask( self, message: Any, *, block: Literal[False], timeout: Optional[float] = None, ) -> Future[Any]: ... @overload def ask( self, message: Any, *, block: Literal[True], timeout: Optional[float] = None, ) -> Any: ... @overload def ask( self, message: Any, *, block: bool = True, timeout: Optional[float] = None, ) -> Union[Any, Future[Any]]: ... def ask( self, message: Any, *, block: bool = True, timeout: Optional[float] = None, ) -> Union[Any, Future[Any]]: """Send message to actor and wait for the reply. The message can be of any type. If ``block`` is :class:`False`, it will immediately return a :class:`Future ` instead of blocking. If ``block`` is :class:`True`, and ``timeout`` is :class:`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 :exc:`pykka.Timeout`. :param message: message to send :type message: any :param block: whether to block while waiting for a reply :type block: boolean :param timeout: seconds to wait before timeout if blocking :type timeout: float or :class:`None` :raise: :exc:`pykka.Timeout` if timeout is reached if blocking :raise: any exception returned by the receiving actor if blocking :return: :class:`pykka.Future`, or response if blocking """ future = self.actor_class._create_future() # noqa: SLF001 try: if not self.is_alive(): raise ActorDeadError(f"{self} not found") # 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: Optional[float] = None, ) -> bool: ... @overload def stop( self, *, block: Literal[False], timeout: Optional[float] = None, ) -> Future[bool]: ... @overload def stop( self, *, block: bool = True, timeout: Optional[float] = None, ) -> Union[Any, Future[Any]]: ... def stop( self, *, block: bool = True, timeout: Optional[float] = None, ) -> Union[Any, Future[Any]]: """Send a message to the actor, asking it to stop. Returns :class:`True` if actor is stopped or was being stopped at the time of the call. :class:`False` if actor was already dead. If ``block`` is :class:`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 :exc:`pykka.ActorDeadError` after it stops. The actor may not be restarted. ``block`` and ``timeout`` works as for :meth:`ask`. :return: :class:`pykka.Future`, or a boolean result if blocking """ ask_future = self.ask(_ActorStop(), block=False) def _stop_result_converter(timeout: Optional[float]) -> 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) if block: return converted_future.get(timeout=timeout) return converted_future def proxy(self: ActorRef[A]) -> ActorProxy[A]: """Wrap the :class:`ActorRef` in an :class:`ActorProxy `. Using this method like this:: proxy = AnActor.start().proxy() is analogous to:: proxy = ActorProxy(AnActor.start()) :raise: :exc:`pykka.ActorDeadError` if actor is not available :return: :class:`pykka.ActorProxy` """ return ActorProxy(actor_ref=self) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/_registry.py0000644000000000000000000001433414510735656014071 0ustar00from __future__ import annotations import logging import threading from typing import ( TYPE_CHECKING, Any, ClassVar, Literal, Optional, TypeVar, Union, 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: Union[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. :param message: the message to send :type message: any :param target_class: optional actor class to broadcast the message to :type target_class: 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. :returns: list of :class:`pykka.ActorRef` """ 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. :param actor_class: actor class, or any superclass of the actor :type actor_class: class :returns: list of :class:`pykka.ActorRef` """ 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. :param actor_class_name: actor class name :type actor_class_name: string :returns: list of :class:`pykka.ActorRef` """ 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, ) -> Optional[ActorRef[Any]]: """Get an actor by its universally unique URN. :param actor_urn: actor URN :type actor_urn: string :returns: :class:`pykka.ActorRef` or :class:`None` if not found """ 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 :class:`ActorRef` in the registry. This is done automatically when an actor is started, e.g. by calling :meth:`Actor.start() `. :param actor_ref: reference to the actor to register :type actor_ref: :class:`pykka.ActorRef` """ 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: Optional[float] = None, ) -> Union[list[bool], list[Future[bool]]]: ... @classmethod def stop_all( cls, *, block: bool = True, timeout: Optional[float] = None, ) -> Union[list[bool], list[Future[bool]]]: """Stop all running actors. ``block`` and ``timeout`` works as for :meth:`ActorRef.stop() `. If ``block`` is :class:`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 :meth:`on_stop() ` method. :returns: If not blocking, a list with a future for each stop action. If blocking, a list of return values from :meth:`pykka.ActorRef.stop`. """ return [ ref.stop(block=block, timeout=timeout) for ref in reversed(cls.get_all()) ] @classmethod def unregister( cls, actor_ref: ActorRef[A], ) -> None: """Remove an :class:`ActorRef ` from the registry. This is done automatically when an actor is stopped, e.g. by calling :meth:`Actor.stop() `. :param actor_ref: reference to the actor to unregister :type actor_ref: :class:`pykka.ActorRef` """ 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)") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/_threading.py0000644000000000000000000000767714510735656014202 0ustar00from __future__ import annotations import queue import sys import threading from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, Optional, TypeVar from pykka import Actor, Future, Timeout if TYPE_CHECKING: from pykka._actor import ActorInbox from pykka._envelope import Envelope from pykka._types import OptExcInfo __all__ = ["ThreadingActor", "ThreadingFuture"] T = TypeVar("T") class ThreadingFutureResult(NamedTuple): value: Optional[Any] = None exc_info: Optional[OptExcInfo] = None class ThreadingFuture(Future[T]): """Implementation of :class:`Future` for use with regular Python threads`. The future is implemented using a :class:`queue.Queue`. The future does *not* make a copy of the object which is :meth:`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. .. versionchanged:: 0.14 Previously, the encapsulated value was a copy made with :func:`copy.deepcopy`, unless the encapsulated value was a future, in which case the original future was encapsulated. """ def __init__(self) -> None: super().__init__() self._queue: queue.Queue[ThreadingFutureResult] = queue.Queue(maxsize=1) self._result: Optional[ThreadingFutureResult] = None def get( self, *, timeout: Optional[float] = None, ) -> Any: try: return super().get(timeout=timeout) except NotImplementedError: pass try: if self._result is None: self._result = self._queue.get(True, timeout) 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 except queue.Empty: raise Timeout(f"{timeout} seconds") from None else: return self._result.value def set( self, value: Optional[Any] = None, ) -> None: self._queue.put(ThreadingFutureResult(value=value), block=False) def set_exception( self, exc_info: Optional[OptExcInfo] = None, ) -> None: assert exc_info is None or len(exc_info) == 3 if exc_info is None: exc_info = sys.exc_info() self._queue.put(ThreadingFutureResult(exc_info=exc_info)) class ThreadingActor(Actor): """Implementation of :class:`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 (:class:`True`) or not (:class:`False`). This must be set before :meth:`pykka.Actor.start` is called, otherwise :exc:`RuntimeError` is raised. 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 :meth:`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) thread.name = thread.name.replace("Thread", self.__class__.__name__) thread.daemon = self.use_daemon_thread thread.start() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/_types.py0000644000000000000000000000063314510735656013362 0ustar00from __future__ import annotations from types import TracebackType from typing import TYPE_CHECKING, Optional, Tuple, Type if TYPE_CHECKING: from typing_extensions import TypeAlias AttrPath: TypeAlias = Tuple[str, ...] # OptExcInfo matches the return type of sys.exc_info() in typeshed OptExcInfo = Tuple[ Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType], ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/debug.py0000644000000000000000000000432014510735656013142 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 :attr:`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. To use this function as a signal handler, setup logging with a :attr:`logging.CRITICAL` threshold or lower and make your main thread register this with the :mod:`signal` module:: import logging import signal import pykka.debug logging.basicConfig(level=logging.DEBUG) signal.signal(signal.SIGUSR1, pykka.debug.log_thread_tracebacks) If your application deadlocks, send the `SIGUSR1` signal to the process:: kill -SIGUSR1 Signal handler caveats: - The function *must* be registered as a signal handler by your main thread. If not, :func:`signal.signal` will raise a :exc:`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 :func:`time.sleep` is OK. The signal will awaken your main thread. Blocking your main thread on e.g. :func:`queue.Queue.get` or :meth:`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. .. versionadded:: 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}") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/messages.py0000644000000000000000000000357414510735656013675 0ustar00"""The :mod:`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 :meth:`~pykka.ActorRef.ask` and :class:`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 :meth:`~pykka.ActorRef.tell` instead of :meth:`~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, Dict, NamedTuple, Tuple 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.""" #: List with the path from the actor to the method. attr_path: AttrPath #: List with positional arguments. args: Tuple[Any, ...] #: Dict with keyword arguments. kwargs: Dict[str, Any] class ProxyGetAttr(NamedTuple): """Message to ask the actor to return the value of the attribute.""" #: List with the path from the actor to the attribute. attr_path: AttrPath class ProxySetAttr(NamedTuple): """Message to ask the actor to set the attribute to the value.""" #: List with the path from the actor to the attribute. attr_path: AttrPath #: The value to set the attribute to. value: Any ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/py.typed0000644000000000000000000000000014510735656013170 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/src/pykka/typing.py0000644000000000000000000000663614510735656013402 0ustar00"""The :mod:`pykka.typing` module contains helpers to improve type hints. Since Pykka 4.0, Pykka has complete type hints for the public API, tested using both `Mypy `_ and `Pyright `_. Due to the dynamic nature of :class:`~pykka.ActorProxy` objects, it is not possible to automatically type them correctly. This module contains helpers to manually create additional classes that correctly describe the type hints for the proxy objects. In cases where a proxy objects is used a lot, this might be worth the extra effort to increase development speed and catch bugs earlier. Example usage:: from typing import cast from pykka import ActorProxy, ThreadingActor from pykka.typing import ActorMemberMixin, proxy_field, proxy_method # 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: reveal_type(proxy.stop) # Revealed type is 'Callable[[], pykka.Future[None]]' reveal_type(proxy.pi) # Revealed type is 'pykka.Future[float]' reveal_type(proxy.area)) # Revealed type is 'Callable[[float], pykka.Future[float]]' """ from __future__ import annotations import sys from typing import TYPE_CHECKING, Any, Callable, Generic, Protocol, TypeVar from pykka import Actor if TYPE_CHECKING: from pykka import Future if sys.version_info >= (3, 10): from typing import ( Concatenate, ParamSpec, ) else: from typing_extensions import ( Concatenate, ParamSpec, ) __all__ = [ "ActorMemberMixin", "proxy_field", "proxy_method", ] T = TypeVar("T") P = ParamSpec("P") R_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. .. versionadded:: 4.0 """ return field # type: ignore[return-value] def proxy_method( field: Callable[Concatenate[Any, P], T], ) -> Method[P, Future[T]]: """Type a method on an actor proxy. .. versionadded:: 4.0 """ return field # type: ignore[return-value] class ActorMemberMixin: """Mixin class for typing Actor methods which are accessible via proxy instances. .. versionadded:: 4.0 """ stop = proxy_method(Actor.stop) on_start = proxy_method(Actor.on_start) on_stop = proxy_method(Actor.on_stop) on_failure = proxy_method(Actor.on_failure) on_receive = proxy_method(Actor.on_receive) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/__init__.py0000644000000000000000000000000014510735656013036 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/conftest.py0000644000000000000000000000736714510735656013153 0ustar00from __future__ import annotations import logging import threading import time from typing import ( TYPE_CHECKING, Any, Iterator, 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 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] # noqa: E501 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] # noqa: E501 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] # noqa: E501 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: 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)] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/log_handler.py0000644000000000000000000000341414510735656013571 0ustar00import collections import logging import threading import time from enum import Enum from typing import Any, Dict, List class LogLevel(str, Enum): DEBUG = "debug" INFO = "info" WARNING = "warning" ERROR = "error" CRITICAL = "critical" class PykkaTestLogHandler(logging.Handler): lock: threading.RLock # type: ignore[assignment] 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) raise Exception(f"Timeout: Waited {timeout:d}s for log message") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/performance.py0000644000000000000000000000302714510735656013614 0ustar00# ruff: noqa: T201 from __future__ import annotations import time from typing import Any, Callable from pykka import ActorRegistry, ThreadingActor def time_it(func: Callable[[], Any]) -> None: start = time.time() func() elapsed = time.time() - start print(f"{func.__name__!r} took {elapsed:.3f}s") 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 __init__(self) -> None: super().__init__() self.cat = "quox" def func(self) -> None: pass def test_direct_plain_attribute_access() -> None: actor = AnActor.start().proxy() for _ in range(10000): actor.foo.get() def test_direct_callable_attribute_access() -> None: actor = AnActor.start().proxy() for _ in range(10000): actor.func().get() def test_traversable_plain_attribute_access() -> None: actor = AnActor.start().proxy() for _ in range(10000): actor.bar.cat.get() def test_traversable_callable_attribute_access() -> None: actor = AnActor.start().proxy() for _ in range(10000): actor.bar.func().get() if __name__ == "__main__": try: time_it(test_direct_plain_attribute_access) time_it(test_direct_callable_attribute_access) time_it(test_traversable_plain_attribute_access) time_it(test_traversable_callable_attribute_access) finally: ActorRegistry.stop_all() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/proxy/__init__.py0000644000000000000000000000000014510735656014217 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/proxy/test_attribute_access.py0000644000000000000000000000606214510735656017061 0ustar00from __future__ import annotations from typing import TYPE_CHECKING, Iterator, NoReturn import pytest from pykka import Actor if TYPE_CHECKING: 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] # noqa: E501 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" # type: ignore[attr-defined] 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: E501, 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" # type: ignore[attr-defined] 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] # noqa: E501 @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() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/proxy/test_dynamic_method_calls.py0000644000000000000000000000344514510735656017701 0ustar00from __future__ import annotations from typing import TYPE_CHECKING, Iterator import pytest from pykka import Actor if TYPE_CHECKING: 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] # noqa: E501 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" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/proxy/test_mocking.py0000644000000000000000000000545314510735656015167 0ustar00from __future__ import annotations from typing import TYPE_CHECKING, Iterator, NoReturn import pytest from pykka import Actor if TYPE_CHECKING: 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] # noqa: E501 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() # XXX 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/proxy/test_proxy.py0000644000000000000000000001141014510735656014707 0ustar00from __future__ import annotations from typing import TYPE_CHECKING, Iterator import pytest import pykka from pykka import Actor, ActorDeadError, ActorProxy from tests.log_handler import LogLevel, PykkaTestLogHandler if TYPE_CHECKING: from tests.types import Runtime class NestedObject: pass class ActorForProxying(Actor): a_nested_object = pykka.traversable(NestedObject()) a_class_attr = "class_attr" 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] # noqa: E501 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 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] # noqa: E501 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() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/proxy/test_static_method_calls.py0000644000000000000000000000765014510735656017546 0ustar00from __future__ import annotations from typing import TYPE_CHECKING, Any, Iterator, NoReturn import pytest from pykka import Actor if TYPE_CHECKING: 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, *_args: Any) -> 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] # noqa: E501 @pytest.fixture(scope="module") def actor_class(runtime: Runtime) -> type[StaticMethodActor]: class StaticMethodActorImpl(StaticMethodActor, runtime.actor_class): # type: ignore[name-defined] # noqa: E501 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!" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/proxy/test_traversable.py0000644000000000000000000000662514510735656016054 0ustar00from __future__ import annotations from typing import TYPE_CHECKING, Iterator import pytest import pykka from pykka import Actor if TYPE_CHECKING: 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__ = ["pykka_traversable", "inner"] 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] # noqa: E501 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/proxy/test_typed_proxy.py0000644000000000000000000000314014510735656016115 0ustar00from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Iterator, cast import pytest import pykka from pykka import Actor, ActorProxy from pykka.typing import ActorMemberMixin, proxy_field, proxy_method if TYPE_CHECKING: from tests.types import Runtime @dataclass class Constants: pi: float class CircleActor(Actor): constants = pykka.traversable(Constants(pi=3.14)) text: str = "The fox crossed the road." def area(self, radius: float) -> float: return self.constants.pi * radius**2 class ConstantsProxy: pi = proxy_field(CircleActor.constants.pi) class FooProxy(ActorMemberMixin, ActorProxy[CircleActor]): numbers: ConstantsProxy text = proxy_field(CircleActor.text) area = proxy_method(CircleActor.area) @pytest.fixture(scope="module") def actor_class(runtime: Runtime) -> type[CircleActor]: class FooActorImpl(CircleActor, runtime.actor_class): # type: ignore[name-defined] # noqa: E501 pass return FooActorImpl @pytest.fixture() def proxy( actor_class: type[CircleActor], ) -> Iterator[FooProxy]: proxy = cast(FooProxy, actor_class.start().proxy()) yield proxy proxy.stop() def test_proxy_field(proxy: FooProxy) -> None: assert proxy.text.get() == "The fox crossed the road." def test_proxy_traversable_object_field(proxy: FooProxy) -> None: assert proxy.constants.pi.get() == 3.14 def test_proxy_method(proxy: FooProxy) -> None: assert proxy.area(2.0).get() == 12.56 def test_proxy_to_actor_methods(proxy: FooProxy) -> None: assert proxy.stop().get() is None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/test_actor.py0000644000000000000000000002120514510735656013460 0ustar00from __future__ import annotations import uuid from typing import TYPE_CHECKING, Any, Iterator import pytest from pykka import Actor, ActorDeadError, ActorRegistry if TYPE_CHECKING: 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, *args: Any) -> 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] # noqa: E501 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] # noqa: E501 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/test_envelope.py0000644000000000000000000000035514510735656014170 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=)" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/test_future.py0000644000000000000000000002010214510735656013655 0ustar00from __future__ import annotations import asyncio import sys import traceback import types from typing import TYPE_CHECKING, Any, Generator, Iterable, List import pytest from pykka import Future, Timeout, get_all if TYPE_CHECKING: from pytest_mock import MockerFixture from tests.types import Runtime def run_async(coroutine: Any) -> Any: loop = asyncio.get_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_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) 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) 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/test_logging.py0000644000000000000000000001411214510735656013775 0ustar00from __future__ import annotations import logging from typing import TYPE_CHECKING, Any, Iterator, NoReturn, Optional import pytest from pykka import Actor from tests.log_handler import LogLevel, PykkaTestLogHandler if TYPE_CHECKING: 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: Optional[type[BaseException]], exception_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> 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] # noqa: E501 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] == 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] == 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}:" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/test_messages.py0000644000000000000000000000160714510735656014163 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" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/test_ref.py0000644000000000000000000001162514510735656013131 0ustar00from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable, Iterator import pytest from pykka import Actor, ActorDeadError, Timeout if TYPE_CHECKING: 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] # noqa: E501 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" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/test_registry.py0000644000000000000000000001574314510735656014232 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] # noqa: E501 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] # noqa: E501 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] # noqa: E501 started_actors[0] ) started_actors[ 1 ].stop.side_effect = lambda *a, **kw: stopped_actors.append( # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType] # noqa: E501 started_actors[1] ) started_actors[ 2 ].stop.side_effect = lambda *a, **kw: stopped_actors.append( # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType] # noqa: E501 started_actors[2] ) ActorRegistry.get_all.return_value = ( # type: ignore[attr-defined] # pyright: ignore[reportFunctionMemberAccess] # noqa: E501 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[ActorA], ) -> None: ActorRegistry.broadcast({"command": "foo"}, target_class=actor_a_class) for actor_ref in ActorRegistry.get_by_class(actor_a_class): received_messages = actor_ref.proxy().received_messages.get() assert {"command": "foo"} in received_messages for actor_ref in ActorRegistry.get_by_class(actor_b_class): received_messages = actor_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], ) -> None: ActorRegistry.broadcast({"command": "foo"}, target_class="ActorA") for actor_a_ref in ActorRegistry.get_by_class(actor_a_class): received_messages = actor_a_ref.proxy().received_messages.get() assert {"command": "foo"} in received_messages for actor_b_ref in ActorRegistry.get_by_class(actor_b_class): received_messages = actor_b_ref.proxy().received_messages.get() assert {"command": "foo"} not in received_messages ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/test_threading_actor.py0000644000000000000000000000305014510735656015503 0ustar00from __future__ import annotations import threading from typing import TYPE_CHECKING, Iterator import pytest from pykka import ThreadingActor if TYPE_CHECKING: 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1696840622.0228503 pykka-4.0.1/tests/types.py0000644000000000000000000000144214510735656012456 0ustar00from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Callable, Optional, Protocol if TYPE_CHECKING: 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: Optional[float] = 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.0.1/PKG-INFO0000644000000000000000000000503300000000000010620 0ustar00Metadata-Version: 2.1 Name: pykka Version: 4.0.1 Summary: Pykka is a Python implementation of the actor model Home-page: https://github.com/jodal/pykka License: Apache-2.0 Keywords: actor,concurrency,threading Author: Stein Magnus Jodal Author-email: stein.magnus@jodal.no Requires-Python: >=3.8.0,<4.0.0 Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Software Development :: Libraries Requires-Dist: typing-extensions (>=4.0.0,<5.0.0) ; python_version < "3.10" Project-URL: Documentation, https://pykka.readthedocs.io/ Project-URL: Repository, https://github.com/jodal/pykka 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. For a quickstart guide and a complete API reference, see the [documentation](https://pykka.readthedocs.io/). ## Installation Pykka requires Python 3.8 or newer. Pykka is available from [PyPI](https://pypi.org/project/pykka/): ``` python3 -m pip install pykka ``` ## 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-2023 Stein Magnus Jodal and contributors. Pykka is licensed under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).