Pyjoyment 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/Reactor/Asyncio.py
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Reactor.Asyncio - Low-level event reactor on asyncio
|
|
3
|
+
=========================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Reactor.Asyncio import Asyncio
|
|
7
|
+
|
|
8
|
+
# Watch if handle becomes readable or writable
|
|
9
|
+
reactor = Asyncio()
|
|
10
|
+
reactor.io(first, lambda reactor, writable: print(
|
|
11
|
+
'First handle is writable' if writable else 'First handle is readable'
|
|
12
|
+
))
|
|
13
|
+
|
|
14
|
+
# Or import the module, as in Google Python Style Guide
|
|
15
|
+
|
|
16
|
+
import Pyjo.Reactor.Asyncio
|
|
17
|
+
|
|
18
|
+
# Watch if handle becomes readable or writable
|
|
19
|
+
reactor = Pyjo.Reactor.Asyncio.new()
|
|
20
|
+
reactor.io(first, lambda reactor, writable: print(
|
|
21
|
+
'First handle is writable' if writable else 'First handle is readable'
|
|
22
|
+
))
|
|
23
|
+
|
|
24
|
+
# Change to watching only if handle becomes writable
|
|
25
|
+
reactor.watch(first, False, True)
|
|
26
|
+
|
|
27
|
+
# Watch a file descriptor if it becomes readable
|
|
28
|
+
reactor.io(fd, lambda reactor, writable: print(
|
|
29
|
+
'Second handle is writable' if writable else 'Second handle is readable'
|
|
30
|
+
)).watch(fd, True, False)
|
|
31
|
+
|
|
32
|
+
# Add a timer
|
|
33
|
+
def timeout(reactor):
|
|
34
|
+
reactor.remove(first)
|
|
35
|
+
reactor.remove(fd)
|
|
36
|
+
print('Timeout!')
|
|
37
|
+
|
|
38
|
+
reactor.timer(15, timeout)
|
|
39
|
+
|
|
40
|
+
# Start reactor if necessary
|
|
41
|
+
if not reactor.is_running:
|
|
42
|
+
reactor.start()
|
|
43
|
+
|
|
44
|
+
:mod:`Pyjo.Reactor.Asyncio` is a low-level event reactor based on
|
|
45
|
+
:mod:`asyncio`. It watches handles and runs timers in an asyncio event loop,
|
|
46
|
+
so coroutines, tasks and other users of the loop run along with it.
|
|
47
|
+
|
|
48
|
+
A reactor uses the event loop it is given, or else the loop that is running,
|
|
49
|
+
as inside :func:`asyncio.run`, or a new event loop of its own, and moves its
|
|
50
|
+
handles and timers to the loop that is running. In a loop that runs
|
|
51
|
+
already, :meth:`start` and :meth:`one_tick` return right away, as asyncio
|
|
52
|
+
does not nest event loops, and the handles and timers are watched by that
|
|
53
|
+
loop.
|
|
54
|
+
|
|
55
|
+
The loop watches handles with ``add_reader`` and ``add_writer``, which the
|
|
56
|
+
default event loop of asyncio on Windows, :class:`asyncio.ProactorEventLoop`,
|
|
57
|
+
does not have. There, a loop of its own is an
|
|
58
|
+
:class:`asyncio.SelectorEventLoop`, and a loop that the reactor is given or
|
|
59
|
+
that is running has to be one too, as of
|
|
60
|
+
``asyncio.run(main(), loop_factory=asyncio.SelectorEventLoop)``. The
|
|
61
|
+
:class:`asyncio.SelectorEventLoop` of Windows watches sockets only, not pipes
|
|
62
|
+
or files, up to 512 of them, with :func:`select.select`.
|
|
63
|
+
|
|
64
|
+
In the debug mode of asyncio, as of ``PYTHONDEVMODE``, a loop logs a callback
|
|
65
|
+
that takes longer than its ``slow_callback_duration``, 0.1 seconds. The
|
|
66
|
+
``PYJO_ASYNCIO_SLOW_CALLBACK`` environment variable sets it in seconds for a
|
|
67
|
+
loop that the reactor makes, as on a machine that is busy, where TLS and an
|
|
68
|
+
application of a server take longer. ::
|
|
69
|
+
|
|
70
|
+
PYTHONDEVMODE=1 PYJO_ASYNCIO_SLOW_CALLBACK=5 python myapp.py daemon
|
|
71
|
+
|
|
72
|
+
The end of a ``with`` block closes the event loop that the reactor made.
|
|
73
|
+
|
|
74
|
+
See also
|
|
75
|
+
--------
|
|
76
|
+
|
|
77
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
78
|
+
|
|
79
|
+
Ported from `Mojo::Reactor::Poll <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Reactor/Poll.pm>`_
|
|
80
|
+
of Mojolicious 9.49.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
import Pyjo.Exception
|
|
84
|
+
import Pyjo.Reactor
|
|
85
|
+
|
|
86
|
+
from Pyjo.Reactor import Handle, IOCallback, TimerCallback
|
|
87
|
+
from Pyjo.Util import _call_hint, md5_sum # pyright: ignore[reportPrivateUsage]
|
|
88
|
+
|
|
89
|
+
import asyncio
|
|
90
|
+
import atexit
|
|
91
|
+
import collections
|
|
92
|
+
import os
|
|
93
|
+
import random
|
|
94
|
+
import sys
|
|
95
|
+
import time
|
|
96
|
+
import weakref
|
|
97
|
+
|
|
98
|
+
from collections.abc import Awaitable, Callable, Coroutine, Generator
|
|
99
|
+
from types import TracebackType
|
|
100
|
+
from typing import Any, Self, TypedDict
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class _IO(TypedDict):
|
|
104
|
+
cb: IOCallback
|
|
105
|
+
read: bool
|
|
106
|
+
write: bool
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _Timer(TypedDict):
|
|
110
|
+
cb: TimerCallback | None
|
|
111
|
+
after: float
|
|
112
|
+
recurring: bool
|
|
113
|
+
handle: asyncio.Handle
|
|
114
|
+
deadline: float
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Pyjo_Reactor_Asyncio(Pyjo.Reactor.object):
|
|
118
|
+
"""
|
|
119
|
+
:mod:`Pyjo.Reactor.Asyncio` inherits all methods from
|
|
120
|
+
:mod:`Pyjo.Reactor` and implements the following new ones.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
|
|
124
|
+
"""::
|
|
125
|
+
|
|
126
|
+
reactor = Pyjo.Reactor.Asyncio.new()
|
|
127
|
+
reactor = Pyjo.Reactor.Asyncio.new(loop=asyncio.new_event_loop())
|
|
128
|
+
|
|
129
|
+
Construct a new :mod:`Pyjo.Reactor.Asyncio` object.
|
|
130
|
+
"""
|
|
131
|
+
super().__init__()
|
|
132
|
+
|
|
133
|
+
# Not in Mojolicious: the asyncio event loop
|
|
134
|
+
self._fixed = loop is not None
|
|
135
|
+
self._loop = loop
|
|
136
|
+
self._own: asyncio.AbstractEventLoop | None = None
|
|
137
|
+
self._finalizer: weakref.finalize[..., Any] | None = None
|
|
138
|
+
|
|
139
|
+
self._error: BaseException | None = None
|
|
140
|
+
self._io: dict[int, _IO] = {}
|
|
141
|
+
# A deque, whose popleft takes the first callback as shift of Perl does, where pop(0) of a list moves all the others
|
|
142
|
+
self._next_tick: collections.deque[TimerCallback] = collections.deque()
|
|
143
|
+
self._next_timer: str | None = None
|
|
144
|
+
self._one = 0
|
|
145
|
+
self._run = 0
|
|
146
|
+
self._running = 0
|
|
147
|
+
self._tasks: set[asyncio.Task[Any]] = set()
|
|
148
|
+
self._timers: dict[str, _Timer] = {}
|
|
149
|
+
_REACTORS.add(self)
|
|
150
|
+
|
|
151
|
+
# Not in Mojolicious: a context manager, which closes the event loop that the reactor made where the block ends, as
|
|
152
|
+
# PyPy frees the object only when its garbage collector runs
|
|
153
|
+
def __enter__(self) -> Self:
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
def __exit__(
|
|
157
|
+
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
|
158
|
+
) -> None:
|
|
159
|
+
self.close()
|
|
160
|
+
|
|
161
|
+
# The cleanups that finalizers deferred at the next tick, with call_soon_threadsafe of the event loop, which only
|
|
162
|
+
# appends to a queue, where the timers of the reactor would change its structures, and works from any thread, as the
|
|
163
|
+
# garbage collector runs finalizers in any of them; the reactor stops after them when it watches nothing anymore, as
|
|
164
|
+
# after an event
|
|
165
|
+
def _wake(self) -> None:
|
|
166
|
+
loop = self._loop
|
|
167
|
+
if loop is not None and not loop.is_closed():
|
|
168
|
+
loop.call_soon_threadsafe(Pyjo_Reactor_Asyncio._weak_drain, weakref.ref(self))
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _weak_drain(ref: 'weakref.ref[Any]') -> None:
|
|
172
|
+
Pyjo.Reactor._drain() # pyright: ignore[reportPrivateUsage]
|
|
173
|
+
if (self := ref()) is not None and self._run and (not self._running or not self._watching()):
|
|
174
|
+
self._stop()
|
|
175
|
+
|
|
176
|
+
def _close_loop(self) -> None:
|
|
177
|
+
if (loop := self._own) is not None:
|
|
178
|
+
_cancel(loop)
|
|
179
|
+
_close(loop)
|
|
180
|
+
|
|
181
|
+
# Not in Mojolicious: a loop of its own, which is closed with the reactor by weakref.finalize, which the garbage
|
|
182
|
+
# collector calls before the finalizers of the objects that it frees, as the loop warns when it is freed before the
|
|
183
|
+
# reactor closes it, as among the objects that a free-threaded Python frees only when the garbage collector runs
|
|
184
|
+
def _own_loop(self) -> asyncio.AbstractEventLoop:
|
|
185
|
+
loop = self._own = _new_event_loop()
|
|
186
|
+
_OWN.add(loop)
|
|
187
|
+
self._finalizer = weakref.finalize(self, _close, loop)
|
|
188
|
+
_register()
|
|
189
|
+
return loop
|
|
190
|
+
|
|
191
|
+
# Not in Mojolicious: the asyncio event loop
|
|
192
|
+
@property
|
|
193
|
+
def loop(self) -> asyncio.AbstractEventLoop:
|
|
194
|
+
"""::
|
|
195
|
+
|
|
196
|
+
loop = reactor.loop
|
|
197
|
+
|
|
198
|
+
The :mod:`asyncio` event loop of the reactor: the one it is given, or
|
|
199
|
+
else the loop that is running, as inside :func:`asyncio.run`, or a
|
|
200
|
+
loop of its own. A reactor that is not given a loop follows the loop
|
|
201
|
+
that is running, and moves its handles and timers to it, unless it
|
|
202
|
+
has tasks in another loop, which cannot move. In the child of a fork,
|
|
203
|
+
it moves to a new loop of its own, as the selector of the loop of the
|
|
204
|
+
parent is not the child's.
|
|
205
|
+
"""
|
|
206
|
+
return self._follow()
|
|
207
|
+
|
|
208
|
+
# The loop that is running, which a reactor that is not given a loop follows, or the one of the reactor
|
|
209
|
+
def _follow(self) -> asyncio.AbstractEventLoop:
|
|
210
|
+
loop = self._loop
|
|
211
|
+
if self._fixed and loop is not None:
|
|
212
|
+
return loop
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
running = asyncio.get_running_loop()
|
|
216
|
+
except RuntimeError:
|
|
217
|
+
running = None
|
|
218
|
+
|
|
219
|
+
if running is not None:
|
|
220
|
+
if running is not loop and not len(self._tasks):
|
|
221
|
+
self._move(running)
|
|
222
|
+
elif loop is None or loop.is_closed() or (loop is not self._own and not loop.is_running()):
|
|
223
|
+
if self._own is None or self._own.is_closed():
|
|
224
|
+
self._own_loop()
|
|
225
|
+
assert self._own is not None
|
|
226
|
+
self._move(self._own)
|
|
227
|
+
|
|
228
|
+
assert self._loop is not None
|
|
229
|
+
return self._loop
|
|
230
|
+
|
|
231
|
+
# The child of a fork moves to a new loop, as the selector of the old one, epoll or kqueue, is the one of the parent;
|
|
232
|
+
# the old loop is kept, not closed, which would close the files of the parent
|
|
233
|
+
def _fork(self) -> None:
|
|
234
|
+
if self._fixed or self._loop is None:
|
|
235
|
+
return
|
|
236
|
+
_FORKED.append(self._loop)
|
|
237
|
+
if self._finalizer is not None:
|
|
238
|
+
self._finalizer.detach()
|
|
239
|
+
self._loop = self._own = None
|
|
240
|
+
self._move(self._own_loop())
|
|
241
|
+
|
|
242
|
+
# Move the handles and timers to another loop
|
|
243
|
+
def _move(self, new: asyncio.AbstractEventLoop) -> None:
|
|
244
|
+
old, self._loop = self._loop, new
|
|
245
|
+
alive = old is not None and not old.is_closed()
|
|
246
|
+
for id, timer in self._timers.items():
|
|
247
|
+
after = timer['after']
|
|
248
|
+
if old is not None and alive and after > 0:
|
|
249
|
+
after = max(0.0, timer['deadline'] - old.time())
|
|
250
|
+
timer['handle'].cancel()
|
|
251
|
+
timer['handle'], timer['deadline'] = self._schedule(new, after, id)
|
|
252
|
+
for fd, io in self._io.items():
|
|
253
|
+
if old is not None and alive:
|
|
254
|
+
if io['read']:
|
|
255
|
+
old.remove_reader(fd)
|
|
256
|
+
if io['write']:
|
|
257
|
+
old.remove_writer(fd)
|
|
258
|
+
if io['read']:
|
|
259
|
+
self._add(new, fd, False)
|
|
260
|
+
if io['write']:
|
|
261
|
+
self._add(new, fd, True)
|
|
262
|
+
|
|
263
|
+
def again(self, id: str, after: float | None = None) -> None:
|
|
264
|
+
"""::
|
|
265
|
+
|
|
266
|
+
reactor.again(id)
|
|
267
|
+
reactor.again(id, 0.5)
|
|
268
|
+
|
|
269
|
+
Restart timer and optionally change the invocation time. Note that this
|
|
270
|
+
method requires an active timer.
|
|
271
|
+
"""
|
|
272
|
+
if (timer := self._timers.get(id)) is None:
|
|
273
|
+
Pyjo.Exception.object.throw('Timer not active')
|
|
274
|
+
if after is not None:
|
|
275
|
+
timer['after'] = after
|
|
276
|
+
|
|
277
|
+
# Not in Mojolicious: a later time only moves the deadline, which the timer checks when it is due, as a stream
|
|
278
|
+
# restarts its timer on every read and write, and a handle of asyncio cancelled and scheduled again each time
|
|
279
|
+
# is what costs, where Mojo sets the time of the timer
|
|
280
|
+
loop = self.loop
|
|
281
|
+
after, handle = timer['after'], timer['handle']
|
|
282
|
+
deadline = loop.time() + after
|
|
283
|
+
if (
|
|
284
|
+
after > 0
|
|
285
|
+
and isinstance(handle, asyncio.TimerHandle)
|
|
286
|
+
and not handle.cancelled()
|
|
287
|
+
and handle.when() <= deadline
|
|
288
|
+
):
|
|
289
|
+
timer['deadline'] = deadline
|
|
290
|
+
return
|
|
291
|
+
handle.cancel()
|
|
292
|
+
timer['handle'], timer['deadline'] = self._schedule(loop, after, id)
|
|
293
|
+
|
|
294
|
+
# Not in Mojolicious: a future of the loop
|
|
295
|
+
def awaitable(self, promise: Any) -> Awaitable[None]:
|
|
296
|
+
"""::
|
|
297
|
+
|
|
298
|
+
future = reactor.awaitable(promise)
|
|
299
|
+
|
|
300
|
+
Return a future of :attr:`loop` that is done when the promise is
|
|
301
|
+
fulfilled or rejected, so that ``await promise`` works in any task of
|
|
302
|
+
the loop.
|
|
303
|
+
"""
|
|
304
|
+
future: asyncio.Future[None] = self.loop.create_future()
|
|
305
|
+
|
|
306
|
+
def settle(*values: Any) -> None:
|
|
307
|
+
if not future.done():
|
|
308
|
+
future.set_result(None)
|
|
309
|
+
|
|
310
|
+
promise.then(settle, settle)
|
|
311
|
+
return future
|
|
312
|
+
|
|
313
|
+
# Not in Mojolicious: the release of the event loop of its own, which Perl does in DESTROY, also at the end of a with
|
|
314
|
+
# block
|
|
315
|
+
def close(self) -> None:
|
|
316
|
+
"""::
|
|
317
|
+
|
|
318
|
+
reactor.close()
|
|
319
|
+
|
|
320
|
+
Close the event loop that the reactor made for itself, as the end of a
|
|
321
|
+
``with`` block does, after the tasks that are left in it are cancelled
|
|
322
|
+
and have ended, as :func:`asyncio.run` does, and as the end of the
|
|
323
|
+
program does; an event loop that it was given, or that was running, is
|
|
324
|
+
left to its owner.
|
|
325
|
+
"""
|
|
326
|
+
self._close_loop()
|
|
327
|
+
|
|
328
|
+
# Not in Mojolicious: a task of the loop
|
|
329
|
+
def run_coroutine(
|
|
330
|
+
self, coro: Coroutine[Any, Any, Any], resolve: Callable[[Any], Any], reject: Callable[[Any], Any]
|
|
331
|
+
) -> None:
|
|
332
|
+
"""::
|
|
333
|
+
|
|
334
|
+
reactor.run_coroutine(coro, resolve, reject)
|
|
335
|
+
|
|
336
|
+
Run a coroutine as a task of :attr:`loop`, where it can await anything
|
|
337
|
+
of the loop, and call ``resolve`` with what it returns or ``reject``
|
|
338
|
+
with the exception it raises. A task that is not done is something to
|
|
339
|
+
watch, so that :meth:`start` does not stop before it, and each step of
|
|
340
|
+
the coroutine is an event, which ends :meth:`one_tick`.
|
|
341
|
+
"""
|
|
342
|
+
task = self.loop.create_task(_steps(coro, weakref.WeakMethod(self._step)))
|
|
343
|
+
self._tasks.add(task)
|
|
344
|
+
|
|
345
|
+
def done(task: asyncio.Task[Any]) -> None:
|
|
346
|
+
self._tasks.discard(task)
|
|
347
|
+
if task.cancelled():
|
|
348
|
+
# A task cancelled before its first step never starts the coroutine, which would warn that it was never
|
|
349
|
+
# awaited
|
|
350
|
+
coro.close()
|
|
351
|
+
reject(asyncio.CancelledError())
|
|
352
|
+
elif (e := task.exception()) is not None:
|
|
353
|
+
reject(e)
|
|
354
|
+
else:
|
|
355
|
+
resolve(task.result())
|
|
356
|
+
|
|
357
|
+
task.add_done_callback(done)
|
|
358
|
+
|
|
359
|
+
# Not in Mojolicious: a future of the loop
|
|
360
|
+
def future(self) -> asyncio.Future[Any]:
|
|
361
|
+
"""::
|
|
362
|
+
|
|
363
|
+
future = reactor.future()
|
|
364
|
+
|
|
365
|
+
Return a new :class:`asyncio.Future` of :attr:`loop`, which
|
|
366
|
+
:meth:`Pyjo.Promise.Pyjo_Promise.to_future` turns a promise into.
|
|
367
|
+
"""
|
|
368
|
+
return self.loop.create_future()
|
|
369
|
+
|
|
370
|
+
def io(self, handle: Handle, cb: IOCallback) -> Self:
|
|
371
|
+
"""::
|
|
372
|
+
|
|
373
|
+
reactor = reactor.io(handle, cb)
|
|
374
|
+
|
|
375
|
+
Watch handle for I/O events, invoking the callback whenever handle
|
|
376
|
+
becomes readable or writable. ::
|
|
377
|
+
|
|
378
|
+
# Callback will be executed twice if handle becomes readable and writable
|
|
379
|
+
reactor.io(handle, lambda reactor, writable: print('Handle is writable' if writable else 'Handle is readable'))
|
|
380
|
+
"""
|
|
381
|
+
self._io[_fileno(handle)] = {'cb': cb, 'read': False, 'write': False}
|
|
382
|
+
return self.watch(handle, True, True)
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def is_running(self) -> bool:
|
|
386
|
+
"""::
|
|
387
|
+
|
|
388
|
+
boolean = reactor.is_running
|
|
389
|
+
|
|
390
|
+
Check if reactor is running.
|
|
391
|
+
"""
|
|
392
|
+
return bool(self._running)
|
|
393
|
+
|
|
394
|
+
def next_tick(self, cb: TimerCallback) -> None:
|
|
395
|
+
"""::
|
|
396
|
+
|
|
397
|
+
none = reactor.next_tick(cb)
|
|
398
|
+
|
|
399
|
+
Execute callback as soon as possible, but not before returning or other
|
|
400
|
+
callbacks that have been registered with this method, always returns
|
|
401
|
+
``None``.
|
|
402
|
+
"""
|
|
403
|
+
self._next_tick.append(cb)
|
|
404
|
+
if self._next_timer is None:
|
|
405
|
+
self._next_timer = self.timer(0, Pyjo_Reactor_Asyncio._next)
|
|
406
|
+
|
|
407
|
+
# Not in Mojolicious: a reactor that follows the loop that is running moves there with the timer of the next tick,
|
|
408
|
+
# which a loop that stopped in the middle of a tick, as in Pyjo.Promise.wait, leaves where nothing runs it; its
|
|
409
|
+
# own loop that runs, as most of the time, needs no move
|
|
410
|
+
elif not self._fixed and (loop := self._loop) is not None and not loop.is_running():
|
|
411
|
+
self._follow()
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
def one_tick(self) -> None:
|
|
415
|
+
"""::
|
|
416
|
+
|
|
417
|
+
reactor.one_tick()
|
|
418
|
+
|
|
419
|
+
Run reactor until an event occurs or no events are being watched
|
|
420
|
+
anymore. ::
|
|
421
|
+
|
|
422
|
+
# Don't block longer than 0.5 seconds
|
|
423
|
+
id = reactor.timer(0.5, lambda reactor: None)
|
|
424
|
+
reactor.one_tick()
|
|
425
|
+
reactor.remove(id)
|
|
426
|
+
"""
|
|
427
|
+
# Just one tick
|
|
428
|
+
running = self._running
|
|
429
|
+
if not running:
|
|
430
|
+
self._running = 1
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
# Stop automatically if there is nothing to watch
|
|
434
|
+
if not self._watching():
|
|
435
|
+
return self.stop()
|
|
436
|
+
|
|
437
|
+
# Unlike Mojolicious: asyncio does not nest event loops, the one that runs already watches the events
|
|
438
|
+
loop = self.loop
|
|
439
|
+
if loop.is_running():
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
# Wait for one event
|
|
443
|
+
self._one += 1
|
|
444
|
+
try:
|
|
445
|
+
loop.run_forever()
|
|
446
|
+
finally:
|
|
447
|
+
self._one -= 1
|
|
448
|
+
|
|
449
|
+
# An exception of an unhandled error event
|
|
450
|
+
if (error := self._error) is not None:
|
|
451
|
+
self._error = None
|
|
452
|
+
raise error
|
|
453
|
+
finally:
|
|
454
|
+
if not running:
|
|
455
|
+
self._running = running
|
|
456
|
+
|
|
457
|
+
def recurring(self, after: float, cb: TimerCallback) -> str:
|
|
458
|
+
"""::
|
|
459
|
+
|
|
460
|
+
id = reactor.recurring(0.25, cb)
|
|
461
|
+
|
|
462
|
+
Create a new recurring timer, invoking the callback repeatedly after a
|
|
463
|
+
given amount of time in seconds.
|
|
464
|
+
"""
|
|
465
|
+
return self._timer(True, after, cb)
|
|
466
|
+
|
|
467
|
+
def remove(self, remove: Handle | str) -> bool:
|
|
468
|
+
"""::
|
|
469
|
+
|
|
470
|
+
boolean = reactor.remove(handle)
|
|
471
|
+
boolean = reactor.remove(id)
|
|
472
|
+
|
|
473
|
+
Remove handle or timer.
|
|
474
|
+
"""
|
|
475
|
+
if isinstance(remove, str):
|
|
476
|
+
if (timer := self._timers.pop(remove, None)) is None:
|
|
477
|
+
return False
|
|
478
|
+
timer['handle'].cancel()
|
|
479
|
+
return True
|
|
480
|
+
fd = _fileno(remove)
|
|
481
|
+
if (io := self._io.pop(fd, None)) is None:
|
|
482
|
+
return False
|
|
483
|
+
self._unwatch(fd, io)
|
|
484
|
+
return True
|
|
485
|
+
|
|
486
|
+
def reset(self) -> None:
|
|
487
|
+
"""::
|
|
488
|
+
|
|
489
|
+
reactor.reset()
|
|
490
|
+
|
|
491
|
+
Remove all handles and timers.
|
|
492
|
+
"""
|
|
493
|
+
for fd, io in self._io.items():
|
|
494
|
+
self._unwatch(fd, io)
|
|
495
|
+
for timer in self._timers.values():
|
|
496
|
+
timer['handle'].cancel()
|
|
497
|
+
self._events = {}
|
|
498
|
+
self._io = {}
|
|
499
|
+
self._next_tick.clear()
|
|
500
|
+
self._next_timer = None
|
|
501
|
+
self._running = 0
|
|
502
|
+
self._tasks = set()
|
|
503
|
+
self._timers = {}
|
|
504
|
+
self._stop()
|
|
505
|
+
|
|
506
|
+
def start(self) -> None:
|
|
507
|
+
"""::
|
|
508
|
+
|
|
509
|
+
reactor.start()
|
|
510
|
+
|
|
511
|
+
Start watching for I/O and timer events, this will block until
|
|
512
|
+
:meth:`stop` is called or no events are being watched anymore. ::
|
|
513
|
+
|
|
514
|
+
# Start reactor only if it is not running already
|
|
515
|
+
if not reactor.is_running:
|
|
516
|
+
reactor.start()
|
|
517
|
+
"""
|
|
518
|
+
running = self._running
|
|
519
|
+
self._running = running + 1
|
|
520
|
+
try:
|
|
521
|
+
# Unlike Mojolicious: asyncio does not nest event loops, the one that runs already watches the events
|
|
522
|
+
while self._running and not (loop := self.loop).is_running():
|
|
523
|
+
# Stop automatically if there is nothing to watch
|
|
524
|
+
if not self._watching():
|
|
525
|
+
return self.stop()
|
|
526
|
+
|
|
527
|
+
# Not in Mojolicious: the loop runs until the reactor stops, where Mojo runs one tick after another, as
|
|
528
|
+
# an asyncio loop that stops and runs again for every event is what costs
|
|
529
|
+
self._run += 1
|
|
530
|
+
try:
|
|
531
|
+
loop.run_forever()
|
|
532
|
+
finally:
|
|
533
|
+
self._run -= 1
|
|
534
|
+
|
|
535
|
+
# An exception of an unhandled error event
|
|
536
|
+
if (error := self._error) is not None:
|
|
537
|
+
self._error = None
|
|
538
|
+
raise error
|
|
539
|
+
finally:
|
|
540
|
+
self._running = running
|
|
541
|
+
|
|
542
|
+
def stop(self) -> None:
|
|
543
|
+
"""::
|
|
544
|
+
|
|
545
|
+
reactor.stop()
|
|
546
|
+
|
|
547
|
+
Stop watching for I/O and timer events.
|
|
548
|
+
"""
|
|
549
|
+
self._running = 0
|
|
550
|
+
self._stop()
|
|
551
|
+
|
|
552
|
+
def timer(self, after: float, cb: TimerCallback) -> str:
|
|
553
|
+
"""::
|
|
554
|
+
|
|
555
|
+
id = reactor.timer(0.5, cb)
|
|
556
|
+
|
|
557
|
+
Create a new timer, invoking the callback after a given amount of time
|
|
558
|
+
in seconds.
|
|
559
|
+
"""
|
|
560
|
+
return self._timer(False, after, cb)
|
|
561
|
+
|
|
562
|
+
def watch(self, handle: Handle, read: bool, write: bool) -> Self:
|
|
563
|
+
"""::
|
|
564
|
+
|
|
565
|
+
reactor = reactor.watch(handle, readable, writable)
|
|
566
|
+
|
|
567
|
+
Change I/O events to watch handle for with true and false values. Note
|
|
568
|
+
that this method requires an active I/O watcher. ::
|
|
569
|
+
|
|
570
|
+
# Watch only for readable events
|
|
571
|
+
reactor.watch(handle, True, False)
|
|
572
|
+
|
|
573
|
+
# Watch only for writable events
|
|
574
|
+
reactor.watch(handle, False, True)
|
|
575
|
+
|
|
576
|
+
# Watch for readable and writable events
|
|
577
|
+
reactor.watch(handle, True, True)
|
|
578
|
+
|
|
579
|
+
# Pause watching for events
|
|
580
|
+
reactor.watch(handle, False, False)
|
|
581
|
+
"""
|
|
582
|
+
fd = _fileno(handle)
|
|
583
|
+
if (io := self._io.get(fd)) is None:
|
|
584
|
+
Pyjo.Exception.object.throw('I/O watcher not active')
|
|
585
|
+
|
|
586
|
+
# Not in Mojolicious: only the events that change are added or removed, as each is a call of the selector of
|
|
587
|
+
# asyncio, where Mojo sets the mask of the handle
|
|
588
|
+
loop = self.loop
|
|
589
|
+
read, write = bool(read), bool(write)
|
|
590
|
+
if read != io['read']:
|
|
591
|
+
if read:
|
|
592
|
+
self._add(loop, fd, False)
|
|
593
|
+
else:
|
|
594
|
+
loop.remove_reader(fd)
|
|
595
|
+
if write != io['write']:
|
|
596
|
+
if write:
|
|
597
|
+
self._add(loop, fd, True)
|
|
598
|
+
else:
|
|
599
|
+
loop.remove_writer(fd)
|
|
600
|
+
io['read'], io['write'] = read, write
|
|
601
|
+
|
|
602
|
+
return self
|
|
603
|
+
|
|
604
|
+
# Not in Mojolicious: a loop without add_reader and add_writer, as the ProactorEventLoop of Windows, which
|
|
605
|
+
# asyncio.run makes there, cannot watch handles, and says so
|
|
606
|
+
def _add(self, loop: asyncio.AbstractEventLoop, fd: int, writable: bool) -> None:
|
|
607
|
+
add = loop.add_writer if writable else loop.add_reader
|
|
608
|
+
try:
|
|
609
|
+
add(fd, Pyjo_Reactor_Asyncio._weak_ready, weakref.ref(self), fd, writable)
|
|
610
|
+
except NotImplementedError:
|
|
611
|
+
name = f'{type(loop).__module__}.{type(loop).__qualname__}'
|
|
612
|
+
Pyjo.Exception.object.throw(f'Event loop {name} cannot watch handles, an asyncio.SelectorEventLoop can')
|
|
613
|
+
|
|
614
|
+
# A timer of no time runs before the I/O of the next iteration of the loop, as the timers of Mojo's reactor run in
|
|
615
|
+
# the same tick, after its I/O; call_later would run it after the I/O of the next iteration
|
|
616
|
+
def _schedule(self, loop: asyncio.AbstractEventLoop, after: float, id: str) -> tuple[asyncio.Handle, float]:
|
|
617
|
+
if after <= 0:
|
|
618
|
+
return loop.call_soon(Pyjo_Reactor_Asyncio._weak_alarm, weakref.ref(self), id), loop.time()
|
|
619
|
+
handle = loop.call_later(after, Pyjo_Reactor_Asyncio._weak_alarm, weakref.ref(self), id)
|
|
620
|
+
return handle, handle.when()
|
|
621
|
+
|
|
622
|
+
# The loop refers to the reactor weakly, so that the reactor and its loop do not make a cycle
|
|
623
|
+
@staticmethod
|
|
624
|
+
def _weak_alarm(ref: 'weakref.ref[Any]', id: str) -> None:
|
|
625
|
+
if (reactor := ref()) is not None:
|
|
626
|
+
reactor._alarm(id)
|
|
627
|
+
|
|
628
|
+
@staticmethod
|
|
629
|
+
def _weak_ready(ref: 'weakref.ref[Any]', fd: int, writable: bool) -> None:
|
|
630
|
+
if (reactor := ref()) is not None:
|
|
631
|
+
reactor._ready(fd, writable)
|
|
632
|
+
|
|
633
|
+
# A timer is due
|
|
634
|
+
def _alarm(self, id: str) -> None:
|
|
635
|
+
if (timer := self._timers.get(id)) is None:
|
|
636
|
+
return
|
|
637
|
+
|
|
638
|
+
# Not in Mojolicious: a timer that again moved to a later time waits for it
|
|
639
|
+
loop = self.loop
|
|
640
|
+
if (left := timer['deadline'] - loop.time()) > 0:
|
|
641
|
+
timer['handle'] = loop.call_later(left, Pyjo_Reactor_Asyncio._weak_alarm, weakref.ref(self), id)
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
# Recurring timer
|
|
645
|
+
if timer['recurring']:
|
|
646
|
+
timer['handle'], timer['deadline'] = self._schedule(loop, timer['after'], id)
|
|
647
|
+
|
|
648
|
+
# Normal timer
|
|
649
|
+
else:
|
|
650
|
+
self.remove(id)
|
|
651
|
+
|
|
652
|
+
if (cb := timer['cb']) is not None:
|
|
653
|
+
self._event('Timer', cb)
|
|
654
|
+
|
|
655
|
+
# An event of a watcher or a timer, which ends one_tick, and start once the reactor stops or watches nothing
|
|
656
|
+
def _event(self, what: str, cb: Any, *args: Any) -> None:
|
|
657
|
+
try:
|
|
658
|
+
self._try(what, cb, *args)
|
|
659
|
+
except Exception as e:
|
|
660
|
+
# Not in Mojolicious: asyncio logs an exception of a callback, Perl dies out of one_tick, so it is kept and
|
|
661
|
+
# raised by one_tick or start, unless the loop runs without them
|
|
662
|
+
if not self._one and not self._run:
|
|
663
|
+
raise
|
|
664
|
+
self._error = e
|
|
665
|
+
return self._stop()
|
|
666
|
+
if self._one or (self._run and (not self._running or not self._watching())):
|
|
667
|
+
self._stop()
|
|
668
|
+
|
|
669
|
+
def _id(self) -> str:
|
|
670
|
+
while True:
|
|
671
|
+
id = md5_sum(f't{time.monotonic()}{random.random()}'.encode())
|
|
672
|
+
if id not in self._timers:
|
|
673
|
+
return id
|
|
674
|
+
|
|
675
|
+
def _next(self) -> None:
|
|
676
|
+
self._next_timer = None
|
|
677
|
+
while len(self._next_tick):
|
|
678
|
+
self._next_tick.popleft()(self)
|
|
679
|
+
|
|
680
|
+
# A handle is ready
|
|
681
|
+
def _ready(self, fd: int, writable: bool) -> None:
|
|
682
|
+
if (io := self._io.get(fd)) is not None:
|
|
683
|
+
self._event('I/O watcher', io['cb'], writable)
|
|
684
|
+
|
|
685
|
+
# A step of a coroutine, which ends one_tick
|
|
686
|
+
def _step(self) -> None:
|
|
687
|
+
if self._one and self.loop.is_running():
|
|
688
|
+
self.loop.stop()
|
|
689
|
+
|
|
690
|
+
def _stop(self) -> None:
|
|
691
|
+
if (self._one or self._run) and self.loop.is_running():
|
|
692
|
+
self.loop.stop()
|
|
693
|
+
|
|
694
|
+
def _timer(self, recurring: bool, after: float, cb: TimerCallback) -> str:
|
|
695
|
+
id = self._id()
|
|
696
|
+
handle, deadline = self._schedule(self.loop, after, id)
|
|
697
|
+
self._timers[id] = {'cb': cb, 'after': after, 'recurring': recurring, 'handle': handle, 'deadline': deadline}
|
|
698
|
+
return id
|
|
699
|
+
|
|
700
|
+
def _try(self, what: str, cb: Any, *args: Any) -> None:
|
|
701
|
+
try:
|
|
702
|
+
cb(self, *args)
|
|
703
|
+
except Exception as e:
|
|
704
|
+
# Not in Mojolicious: the arguments of a callback, which Perl does not check
|
|
705
|
+
if isinstance(e, TypeError):
|
|
706
|
+
_call_hint(e, 'the callbacks of a reactor get the reactor first')
|
|
707
|
+
# Unlike Mojolicious: the message of Python has no file and line, the exception keeps the frames
|
|
708
|
+
err = Pyjo.Exception.new(e)
|
|
709
|
+
err.message = f'{what} failed: {err.message}'
|
|
710
|
+
err.__cause__ = e
|
|
711
|
+
self.emit('error', err)
|
|
712
|
+
|
|
713
|
+
def _watching(self) -> bool:
|
|
714
|
+
return bool(len(self._timers) or len(self._io) or len(self._tasks))
|
|
715
|
+
|
|
716
|
+
# The handle is removed from the loop that watches it, without the move to a loop that runs, which leaves out the
|
|
717
|
+
# handles that are not watched anymore, so the loop of before would keep it, and select of Windows fails once the
|
|
718
|
+
# handle is closed
|
|
719
|
+
def _unwatch(self, fd: int, io: _IO) -> None:
|
|
720
|
+
if (loop := self._loop) is not None and not loop.is_closed():
|
|
721
|
+
if io['read']:
|
|
722
|
+
loop.remove_reader(fd)
|
|
723
|
+
if io['write']:
|
|
724
|
+
loop.remove_writer(fd)
|
|
725
|
+
io['read'] = io['write'] = False
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _fileno(handle: Handle) -> int:
|
|
729
|
+
fd = handle if isinstance(handle, int) else handle.fileno()
|
|
730
|
+
if fd < 0:
|
|
731
|
+
Pyjo.Exception.object.throw('Handle is closed')
|
|
732
|
+
return fd
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
# Not in Mojolicious: a step of a coroutine is an event of the reactor, as a step of an async sub of Mojo is a callback of
|
|
736
|
+
# next_tick, so that one_tick returns after it: the step of a task of asyncio is a callback of the loop that ends nothing,
|
|
737
|
+
# so code that waits with one_tick for what the coroutine does, as a server that waits for the response of an action,
|
|
738
|
+
# would wait for the next event of a watcher or a timer
|
|
739
|
+
async def _steps(coro: Coroutine[Any, Any, Any], step: 'weakref.WeakMethod[Callable[[], None]]') -> Any:
|
|
740
|
+
return await _Steps(coro, step)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
class _Steps:
|
|
744
|
+
def __init__(self, coro: Coroutine[Any, Any, Any], step: 'weakref.WeakMethod[Callable[[], None]]') -> None:
|
|
745
|
+
self._coro = coro
|
|
746
|
+
self._step = step
|
|
747
|
+
|
|
748
|
+
def __await__(self) -> Generator[Any, Any, Any]:
|
|
749
|
+
coro = self._coro
|
|
750
|
+
method: Callable[[Any], Any] = coro.send
|
|
751
|
+
arg: Any = None
|
|
752
|
+
while True:
|
|
753
|
+
try:
|
|
754
|
+
result = method(arg)
|
|
755
|
+
except StopIteration as e:
|
|
756
|
+
self._stepped()
|
|
757
|
+
return e.value
|
|
758
|
+
except BaseException:
|
|
759
|
+
self._stepped()
|
|
760
|
+
raise
|
|
761
|
+
self._stepped()
|
|
762
|
+
try:
|
|
763
|
+
arg = yield result
|
|
764
|
+
method = coro.send
|
|
765
|
+
except GeneratorExit:
|
|
766
|
+
coro.close()
|
|
767
|
+
raise
|
|
768
|
+
except BaseException as e:
|
|
769
|
+
arg = e
|
|
770
|
+
method = coro.throw
|
|
771
|
+
|
|
772
|
+
def _stepped(self) -> None:
|
|
773
|
+
if (step := self._step()) is not None:
|
|
774
|
+
step()
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
# Not in Mojolicious: a loop of its own is a SelectorEventLoop on Windows, where the default of asyncio is the
|
|
778
|
+
# ProactorEventLoop, which has no add_reader and add_writer, with the threshold of a slow callback of
|
|
779
|
+
# PYJO_ASYNCIO_SLOW_CALLBACK, as asyncio has no variable of its own for it
|
|
780
|
+
def _new_event_loop() -> asyncio.AbstractEventLoop:
|
|
781
|
+
loop = asyncio.SelectorEventLoop() if sys.platform == 'win32' else asyncio.new_event_loop()
|
|
782
|
+
if duration := os.environ.get('PYJO_ASYNCIO_SLOW_CALLBACK'):
|
|
783
|
+
loop.slow_callback_duration = float(duration)
|
|
784
|
+
return loop
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
# Not in Mojolicious: the event loops that the reactors make for themselves, which are closed at the exit of the
|
|
788
|
+
# interpreter, before it finalizes the objects and the loops of the reactors that still exist would warn that they are
|
|
789
|
+
# not closed
|
|
790
|
+
_OWN: 'weakref.WeakSet[asyncio.AbstractEventLoop]' = weakref.WeakSet()
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
# Not in Mojolicious: the reactors that exist, which move to new loops in the child of a fork, and the loops of the parent,
|
|
794
|
+
# which the child keeps
|
|
795
|
+
_REACTORS: 'weakref.WeakSet[Pyjo_Reactor_Asyncio]' = weakref.WeakSet()
|
|
796
|
+
_FORKED: list[asyncio.AbstractEventLoop] = []
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _after_fork_in_child() -> None:
|
|
800
|
+
for reactor in list(_REACTORS):
|
|
801
|
+
try:
|
|
802
|
+
reactor._fork() # pyright: ignore[reportPrivateUsage]
|
|
803
|
+
except Exception:
|
|
804
|
+
pass
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
if hasattr(os, 'register_at_fork'):
|
|
808
|
+
os.register_at_fork(after_in_child=_after_fork_in_child)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _close(loop: asyncio.AbstractEventLoop) -> None:
|
|
812
|
+
if not loop.is_running() and not loop.is_closed():
|
|
813
|
+
loop.close()
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
# Not in Mojolicious: the tasks that are left in a loop of its own, as a coroutine that waits for the next cycle of a loop
|
|
817
|
+
# that has stopped, are cancelled and run to their end before the loop closes, as asyncio.run does, so that none is
|
|
818
|
+
# destroyed while it is pending; not in another loop that runs, which cannot run this one, nor in the garbage collector
|
|
819
|
+
def _cancel(loop: asyncio.AbstractEventLoop) -> None:
|
|
820
|
+
if loop.is_running() or loop.is_closed():
|
|
821
|
+
return
|
|
822
|
+
try:
|
|
823
|
+
asyncio.get_running_loop()
|
|
824
|
+
return
|
|
825
|
+
except RuntimeError:
|
|
826
|
+
pass
|
|
827
|
+
if tasks := [task for task in asyncio.all_tasks(loop) if not task.done()]:
|
|
828
|
+
for task in tasks:
|
|
829
|
+
task.cancel()
|
|
830
|
+
loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _close_own() -> None:
|
|
834
|
+
for loop in list(_OWN):
|
|
835
|
+
_cancel(loop)
|
|
836
|
+
_close(loop)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
# Not in Mojolicious: the loops of their own are closed at the end of the program after their tasks, by a handler that is
|
|
840
|
+
# registered after the first finalizer, so that it runs before the exit function of weakref.finalize, as atexit runs
|
|
841
|
+
# the handlers in the reverse order, which would close the loops with their tasks pending
|
|
842
|
+
_registered = False
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _register() -> None:
|
|
846
|
+
global _registered
|
|
847
|
+
if not _registered:
|
|
848
|
+
_registered = True
|
|
849
|
+
atexit.register(_close_own)
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
new = Pyjo_Reactor_Asyncio.new
|
|
853
|
+
object = Pyjo_Reactor_Asyncio
|
|
854
|
+
Asyncio = Pyjo_Reactor_Asyncio
|