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/Promise.py
ADDED
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Promise - Promises/A+
|
|
3
|
+
==========================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
import Pyjo.UserAgent
|
|
7
|
+
|
|
8
|
+
from Pyjo.Promise import Promise
|
|
9
|
+
|
|
10
|
+
# Wrap continuation-passing style APIs with promises
|
|
11
|
+
ua = Pyjo.UserAgent.new()
|
|
12
|
+
|
|
13
|
+
def get_p(url):
|
|
14
|
+
promise = Promise()
|
|
15
|
+
|
|
16
|
+
def cb(ua, tx):
|
|
17
|
+
err = tx.error
|
|
18
|
+
if not err or err.get('code'):
|
|
19
|
+
promise.resolve(tx)
|
|
20
|
+
else:
|
|
21
|
+
promise.reject(err['message'])
|
|
22
|
+
|
|
23
|
+
ua.get(url, cb)
|
|
24
|
+
return promise
|
|
25
|
+
|
|
26
|
+
# Or import the module, as in Google Python Style Guide
|
|
27
|
+
|
|
28
|
+
import Pyjo.Promise
|
|
29
|
+
import Pyjo.UserAgent
|
|
30
|
+
|
|
31
|
+
# Wrap continuation-passing style APIs with promises
|
|
32
|
+
ua = Pyjo.UserAgent.new()
|
|
33
|
+
|
|
34
|
+
def get_p(url):
|
|
35
|
+
promise = Pyjo.Promise.new()
|
|
36
|
+
|
|
37
|
+
def cb(ua, tx):
|
|
38
|
+
err = tx.error
|
|
39
|
+
if not err or err.get('code'):
|
|
40
|
+
promise.resolve(tx)
|
|
41
|
+
else:
|
|
42
|
+
promise.reject(err['message'])
|
|
43
|
+
|
|
44
|
+
ua.get(url, cb)
|
|
45
|
+
return promise
|
|
46
|
+
|
|
47
|
+
# Perform non-blocking operations sequentially
|
|
48
|
+
def mojo_cb(mojo):
|
|
49
|
+
print(mojo.res.code)
|
|
50
|
+
return get_p('https://metacpan.org')
|
|
51
|
+
|
|
52
|
+
get_p('https://mojolicious.org').then(mojo_cb).then(
|
|
53
|
+
lambda cpan: print(cpan.res.code)
|
|
54
|
+
).catch(lambda err: warn(f'Something went wrong: {err}')).wait()
|
|
55
|
+
|
|
56
|
+
# Synchronize non-blocking operations (all)
|
|
57
|
+
mojo = get_p('https://mojolicious.org')
|
|
58
|
+
cpan = get_p('https://metacpan.org')
|
|
59
|
+
Pyjo.Promise.all(mojo, cpan).then(
|
|
60
|
+
lambda mojo, cpan: print(mojo[0].res.code, cpan[0].res.code)
|
|
61
|
+
).catch(lambda err: warn(f'Something went wrong: {err}')).wait()
|
|
62
|
+
|
|
63
|
+
# Synchronize non-blocking operations (race)
|
|
64
|
+
mojo = get_p('https://mojolicious.org')
|
|
65
|
+
cpan = get_p('https://metacpan.org')
|
|
66
|
+
Pyjo.Promise.race(mojo, cpan).then(
|
|
67
|
+
lambda tx: print(tx.req.url, 'won!')
|
|
68
|
+
).catch(lambda err: warn(f'Something went wrong: {err}')).wait()
|
|
69
|
+
|
|
70
|
+
# Await a promise in a coroutine
|
|
71
|
+
@Pyjo.Promise.coroutine
|
|
72
|
+
async def code(url):
|
|
73
|
+
tx = await get_p(url)
|
|
74
|
+
return tx.res.code
|
|
75
|
+
|
|
76
|
+
code('https://mojolicious.org').then(print).wait()
|
|
77
|
+
|
|
78
|
+
:mod:`Pyjo.Promise` is a Python implementation of
|
|
79
|
+
`Promises/A+ <https://promisesaplus.com>`_ and a superset of
|
|
80
|
+
`ES6 Promises <https://duckduckgo.com/?q=\\\\mdn%20Promise>`_.
|
|
81
|
+
|
|
82
|
+
States
|
|
83
|
+
------
|
|
84
|
+
|
|
85
|
+
A promise is an object representing the eventual completion or failure of a
|
|
86
|
+
non-blocking operation. It allows non-blocking functions to return values,
|
|
87
|
+
like blocking functions. But instead of immediately returning the final value,
|
|
88
|
+
the non-blocking function returns a promise to supply the value at some point
|
|
89
|
+
in the future.
|
|
90
|
+
|
|
91
|
+
A promise can be in one of three states:
|
|
92
|
+
|
|
93
|
+
pending
|
|
94
|
+
Initial state, neither fulfilled nor rejected.
|
|
95
|
+
|
|
96
|
+
fulfilled
|
|
97
|
+
Meaning that the operation completed successfully.
|
|
98
|
+
|
|
99
|
+
rejected
|
|
100
|
+
Meaning that the operation failed.
|
|
101
|
+
|
|
102
|
+
A pending promise can either be fulfilled with a value or rejected with a
|
|
103
|
+
reason. When either happens, the associated handlers queued up by a promise's
|
|
104
|
+
:meth:`~Pyjo_Promise.then` method are called.
|
|
105
|
+
|
|
106
|
+
Values
|
|
107
|
+
------
|
|
108
|
+
|
|
109
|
+
A promise is fulfilled or rejected with any number of values, and the
|
|
110
|
+
handlers get them as arguments. What a handler returns is the value of the new
|
|
111
|
+
promise, ``None`` is no value, and a promise is adopted, so a handler that
|
|
112
|
+
passes several values on returns a promise of them. ::
|
|
113
|
+
|
|
114
|
+
promise.then(lambda *values: Pyjo.Promise.resolve(*values))
|
|
115
|
+
|
|
116
|
+
Coroutines
|
|
117
|
+
----------
|
|
118
|
+
|
|
119
|
+
``await promise`` waits for the promise in a coroutine, and gives its value,
|
|
120
|
+
a tuple of several values, or ``None`` for none. A rejection raises the
|
|
121
|
+
exception the promise was rejected with, or a :mod:`Pyjo.Exception` of the
|
|
122
|
+
first reason. A coroutine is a promise with :meth:`~Pyjo_Promise.new` or
|
|
123
|
+
:func:`coroutine`. The reactor of the event loop runs it: a coroutine awaits the
|
|
124
|
+
promises of Pyjoyment with any reactor, and with :mod:`Pyjo.Reactor.Asyncio`
|
|
125
|
+
anything of asyncio too, and a promise can be awaited in any asyncio task.
|
|
126
|
+
Another awaitable, as a future of asyncio, is a promise with
|
|
127
|
+
:meth:`~Pyjo_Promise.new` too, and :meth:`~Pyjo_Promise.to_future` turns a
|
|
128
|
+
promise into a future of the reactor. ::
|
|
129
|
+
|
|
130
|
+
async def main():
|
|
131
|
+
title = (await get_p('https://mojolicious.org')).res.dom().at('title').text
|
|
132
|
+
|
|
133
|
+
Debugging
|
|
134
|
+
---------
|
|
135
|
+
|
|
136
|
+
You can set the ``PYJO_PROMISE_DEBUG`` environment variable to get some
|
|
137
|
+
advanced diagnostics information printed to ``sys.stderr``. ::
|
|
138
|
+
|
|
139
|
+
PYJO_PROMISE_DEBUG=1
|
|
140
|
+
|
|
141
|
+
See also
|
|
142
|
+
--------
|
|
143
|
+
|
|
144
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
145
|
+
|
|
146
|
+
Ported from `Mojo::Promise <https://github.com/mojolicious/mojo/blob/v9.52/lib/Mojo/Promise.pm>`_
|
|
147
|
+
of Mojolicious 9.52.
|
|
148
|
+
|
|
149
|
+
Functions
|
|
150
|
+
---------
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
import Pyjo.Base
|
|
154
|
+
import Pyjo.Exception
|
|
155
|
+
import Pyjo.IOLoop
|
|
156
|
+
import Pyjo.Reactor
|
|
157
|
+
|
|
158
|
+
from Pyjo.Util import warn
|
|
159
|
+
|
|
160
|
+
import atexit
|
|
161
|
+
import builtins
|
|
162
|
+
import collections
|
|
163
|
+
import concurrent.futures
|
|
164
|
+
import functools
|
|
165
|
+
import os
|
|
166
|
+
import socket
|
|
167
|
+
import warnings
|
|
168
|
+
import weakref
|
|
169
|
+
|
|
170
|
+
from collections.abc import Awaitable, Callable, Coroutine, Generator
|
|
171
|
+
from typing import TYPE_CHECKING, Any, Concatenate, cast
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
type Anything = builtins.object
|
|
175
|
+
|
|
176
|
+
# A handler gets the values of the promise
|
|
177
|
+
type Handler = Callable[..., Any]
|
|
178
|
+
|
|
179
|
+
DEBUG = bool(os.environ.get('PYJO_PROMISE_DEBUG'))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _hybridmethod[**P, R]:
|
|
183
|
+
"""A method that is called on the class or on an object, as Perl's can be."""
|
|
184
|
+
|
|
185
|
+
def __init__(self, func: Callable[Concatenate[Any, P], R]) -> None:
|
|
186
|
+
self.func = func
|
|
187
|
+
self.__doc__ = func.__doc__
|
|
188
|
+
|
|
189
|
+
def __get__(self, obj: Anything, objtype: type | None = None) -> Callable[P, R]:
|
|
190
|
+
def bound(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
191
|
+
return self.func(obj if obj is not None else objtype, *args, **kwargs)
|
|
192
|
+
|
|
193
|
+
bound.__doc__ = self.__doc__
|
|
194
|
+
return bound
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Pyjo_Promise(Pyjo.Base.object):
|
|
198
|
+
"""
|
|
199
|
+
:mod:`Pyjo.Promise` inherits all methods from :mod:`Pyjo.Base` and
|
|
200
|
+
implements the following new ones.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
def __init__(
|
|
204
|
+
self, cb: Callable[[Handler, Handler], Anything] | Awaitable[Any] | concurrent.futures.Future[Any] | None = None
|
|
205
|
+
) -> None:
|
|
206
|
+
"""::
|
|
207
|
+
|
|
208
|
+
promise = Pyjo.Promise.new()
|
|
209
|
+
promise = Pyjo.Promise.new(lambda resolve, reject: ...)
|
|
210
|
+
promise = Pyjo.Promise.new(coroutine)
|
|
211
|
+
promise = Pyjo.Promise.new(future)
|
|
212
|
+
|
|
213
|
+
Construct a new :mod:`Pyjo.Promise` object. ::
|
|
214
|
+
|
|
215
|
+
# Wrap a continuation-passing style API
|
|
216
|
+
def cb(resolve, reject):
|
|
217
|
+
Pyjo.IOLoop.timer(5, lambda loop: resolve('Lucky!') if random.randint(0, 1) else reject('Unlucky!'))
|
|
218
|
+
|
|
219
|
+
promise = Pyjo.Promise.new(cb)
|
|
220
|
+
|
|
221
|
+
A coroutine is run by the reactor of :attr:`ioloop`, and the promise
|
|
222
|
+
is fulfilled with what it returns, or rejected with the exception it
|
|
223
|
+
raises. ::
|
|
224
|
+
|
|
225
|
+
# Wrap a coroutine
|
|
226
|
+
async def lucky():
|
|
227
|
+
await Pyjo.Promise.timer(5)
|
|
228
|
+
return 'Lucky!'
|
|
229
|
+
|
|
230
|
+
promise = Pyjo.Promise.new(lucky())
|
|
231
|
+
|
|
232
|
+
Another awaitable, like an :class:`asyncio.Future` or an
|
|
233
|
+
:class:`asyncio.Task`, is awaited in a coroutine, so with a reactor
|
|
234
|
+
that can await it, as :mod:`Pyjo.Reactor.Asyncio`. ::
|
|
235
|
+
|
|
236
|
+
# Wrap a future of asyncio
|
|
237
|
+
future = asyncio.get_running_loop().run_in_executor(None, time.sleep, 1)
|
|
238
|
+
promise = Pyjo.Promise.new(future)
|
|
239
|
+
|
|
240
|
+
A :class:`concurrent.futures.Future`, which is done in another thread
|
|
241
|
+
or process, is waited for with any reactor, which the thread that
|
|
242
|
+
completes the future wakes up. ::
|
|
243
|
+
|
|
244
|
+
# Wrap a future of a pool of threads
|
|
245
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
246
|
+
promise = Pyjo.Promise.new(executor.submit(time.sleep, 1))
|
|
247
|
+
promise.wait()
|
|
248
|
+
"""
|
|
249
|
+
self._debug: Pyjo.Exception.object | None = None
|
|
250
|
+
self._handled = False
|
|
251
|
+
self._ioloop: weakref.ref[Pyjo.IOLoop.object] | None = None
|
|
252
|
+
self._reject: list[Handler] = []
|
|
253
|
+
self._resolve: list[Handler] = []
|
|
254
|
+
self._results: tuple[Any, ...] | None = None
|
|
255
|
+
self._status = ''
|
|
256
|
+
|
|
257
|
+
if DEBUG:
|
|
258
|
+
self._debug = Pyjo.Exception.new().trace()
|
|
259
|
+
|
|
260
|
+
# Not in Mojolicious: a future of concurrent.futures, done in another thread
|
|
261
|
+
if isinstance(cb, concurrent.futures.Future):
|
|
262
|
+
_wait_future(self, cast(concurrent.futures.Future[Any], cb))
|
|
263
|
+
|
|
264
|
+
# Not in Mojolicious: a coroutine, or another awaitable in one, as a future of asyncio
|
|
265
|
+
elif isinstance(cb, Awaitable):
|
|
266
|
+
coro = cast(Coroutine[Any, Any, Any], cb) if isinstance(cb, Coroutine) else _await(cast(Awaitable[Any], cb))
|
|
267
|
+
|
|
268
|
+
def resolve(value: Any) -> None:
|
|
269
|
+
self.resolve() if value is None else self.resolve(value)
|
|
270
|
+
|
|
271
|
+
self.ioloop.reactor.run_coroutine(coro, resolve, lambda e: self.reject(e))
|
|
272
|
+
|
|
273
|
+
elif cb is not None:
|
|
274
|
+
cb(lambda *values: self.resolve(*values), lambda *values: self.reject(*values))
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def ioloop(self) -> Pyjo.IOLoop.object:
|
|
278
|
+
"""::
|
|
279
|
+
|
|
280
|
+
loop = promise.ioloop
|
|
281
|
+
promise.ioloop = Pyjo.IOLoop.new()
|
|
282
|
+
|
|
283
|
+
Event loop object to control, defaults to the global
|
|
284
|
+
:mod:`Pyjo.IOLoop` singleton. Note that this attribute is weakened.
|
|
285
|
+
"""
|
|
286
|
+
if self._ioloop is None:
|
|
287
|
+
self._ioloop = weakref.ref(Pyjo.IOLoop.singleton)
|
|
288
|
+
if (loop := self._ioloop()) is None:
|
|
289
|
+
raise ReferenceError('weakly-referenced object no longer exists')
|
|
290
|
+
return loop
|
|
291
|
+
|
|
292
|
+
@ioloop.setter
|
|
293
|
+
def ioloop(self, value: Pyjo.IOLoop.object) -> None:
|
|
294
|
+
self._ioloop = weakref.ref(value)
|
|
295
|
+
|
|
296
|
+
def __await__(self) -> Generator[Any, Any, Any]:
|
|
297
|
+
"""::
|
|
298
|
+
|
|
299
|
+
value = await promise
|
|
300
|
+
|
|
301
|
+
Wait for the promise in a coroutine and return its value, a tuple of
|
|
302
|
+
several values, or ``None`` for none, or raise the exception it was
|
|
303
|
+
rejected with, or a :mod:`Pyjo.Exception` of the first reason.
|
|
304
|
+
"""
|
|
305
|
+
# Mojo's AWAIT_* methods
|
|
306
|
+
self._handled = True
|
|
307
|
+
yield from self.ioloop.reactor.awaitable(self).__await__()
|
|
308
|
+
|
|
309
|
+
results: tuple[Any, ...] = self._results or ()
|
|
310
|
+
if self._status == 'resolve':
|
|
311
|
+
return _result(results)
|
|
312
|
+
raise _exception(results)
|
|
313
|
+
|
|
314
|
+
if not TYPE_CHECKING:
|
|
315
|
+
# Not in Mojolicious: an attribute of the value taken from the promise itself, as of the result of a request of
|
|
316
|
+
# get_p without await; out of the sight of type checkers, which would take any attribute of a promise for one
|
|
317
|
+
def __getattr__(self, name: str) -> Any:
|
|
318
|
+
message = f"'{type(self).__name__}' object has no attribute '{name}'"
|
|
319
|
+
if not name.startswith('_'):
|
|
320
|
+
message += ', a promise gives its value to await or to a callback of then'
|
|
321
|
+
raise AttributeError(message, name=name, obj=self)
|
|
322
|
+
|
|
323
|
+
def __del__(self) -> None:
|
|
324
|
+
if getattr(self, '_handled', True) or getattr(self, '_status', '') != 'reject' or self._results is None:
|
|
325
|
+
return
|
|
326
|
+
try:
|
|
327
|
+
warnings.warn(f'Unhandled rejected promise: {" ".join(str(r) for r in self._results)}', stacklevel=2)
|
|
328
|
+
if DEBUG and (debug := self._debug) is not None:
|
|
329
|
+
debug.message = '-- Destroyed promise\n'
|
|
330
|
+
debug.verbose = True
|
|
331
|
+
warn(debug.to_str())
|
|
332
|
+
except Exception:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
@classmethod
|
|
336
|
+
def all(cls, *promises: 'Pyjo_Promise') -> 'Pyjo_Promise':
|
|
337
|
+
"""::
|
|
338
|
+
|
|
339
|
+
new = Pyjo.Promise.all(*promises)
|
|
340
|
+
|
|
341
|
+
Returns a new :mod:`Pyjo.Promise` object that either fulfills when all
|
|
342
|
+
of the passed :mod:`Pyjo.Promise` objects have fulfilled or rejects as
|
|
343
|
+
soon as one of them rejects. If the returned promise fulfills, it is
|
|
344
|
+
fulfilled with the values from the fulfilled promises in the same order
|
|
345
|
+
as the passed promises, a list of the values of each.
|
|
346
|
+
"""
|
|
347
|
+
return _all(2, promises)
|
|
348
|
+
|
|
349
|
+
@classmethod
|
|
350
|
+
def all_settled(cls, *promises: 'Pyjo_Promise') -> 'Pyjo_Promise':
|
|
351
|
+
"""::
|
|
352
|
+
|
|
353
|
+
new = Pyjo.Promise.all_settled(*promises)
|
|
354
|
+
|
|
355
|
+
Returns a new :mod:`Pyjo.Promise` object that fulfills when all of the
|
|
356
|
+
passed :mod:`Pyjo.Promise` objects have fulfilled or rejected, with
|
|
357
|
+
dicts that describe the outcome of each promise.
|
|
358
|
+
"""
|
|
359
|
+
return _all(0, promises)
|
|
360
|
+
|
|
361
|
+
@classmethod
|
|
362
|
+
def any(cls, *promises: 'Pyjo_Promise') -> 'Pyjo_Promise':
|
|
363
|
+
"""::
|
|
364
|
+
|
|
365
|
+
new = Pyjo.Promise.any(*promises)
|
|
366
|
+
|
|
367
|
+
Returns a new :mod:`Pyjo.Promise` object that fulfills as soon as one
|
|
368
|
+
of the passed :mod:`Pyjo.Promise` objects fulfills, with the value from
|
|
369
|
+
that promise.
|
|
370
|
+
"""
|
|
371
|
+
return _all(3, promises)
|
|
372
|
+
|
|
373
|
+
def catch(self, cb: Handler) -> 'Pyjo_Promise':
|
|
374
|
+
"""::
|
|
375
|
+
|
|
376
|
+
new = promise.catch(lambda *reason: ...)
|
|
377
|
+
|
|
378
|
+
Appends a rejection handler callback to the promise, and returns a new
|
|
379
|
+
:mod:`Pyjo.Promise` object resolving to the return value of the
|
|
380
|
+
callback if it is called, or to its original fulfillment value if the
|
|
381
|
+
promise is instead fulfilled. ::
|
|
382
|
+
|
|
383
|
+
# Longer version
|
|
384
|
+
new = promise.then(None, lambda *reason: ...)
|
|
385
|
+
|
|
386
|
+
# Pass along the rejection reason
|
|
387
|
+
def cb(*reason):
|
|
388
|
+
warn(f'Something went wrong: {reason[0]}')
|
|
389
|
+
return Pyjo.Promise.reject(*reason)
|
|
390
|
+
|
|
391
|
+
promise.catch(cb)
|
|
392
|
+
|
|
393
|
+
# Change the rejection reason
|
|
394
|
+
promise.catch(lambda *reason: f'This is bad: {reason[0]}')
|
|
395
|
+
"""
|
|
396
|
+
return self.then(None, cb)
|
|
397
|
+
|
|
398
|
+
def clone(self) -> 'Pyjo_Promise':
|
|
399
|
+
"""::
|
|
400
|
+
|
|
401
|
+
new = promise.clone()
|
|
402
|
+
|
|
403
|
+
Return a new :mod:`Pyjo.Promise` object cloned from this promise that
|
|
404
|
+
is still pending.
|
|
405
|
+
"""
|
|
406
|
+
new = type(self)()
|
|
407
|
+
new.ioloop = self.ioloop
|
|
408
|
+
return new
|
|
409
|
+
|
|
410
|
+
def lastly(self, cb: Callable[[], Any]) -> 'Pyjo_Promise':
|
|
411
|
+
"""::
|
|
412
|
+
|
|
413
|
+
new = promise.lastly(lambda: ...)
|
|
414
|
+
|
|
415
|
+
Appends a fulfillment and rejection handler to the promise, and returns
|
|
416
|
+
a new :mod:`Pyjo.Promise` object resolving to the original fulfillment
|
|
417
|
+
value or rejection reason. ``finally_`` is an alias, the name of
|
|
418
|
+
Mojolicious with an underscore. ::
|
|
419
|
+
|
|
420
|
+
# Do something on fulfillment and rejection
|
|
421
|
+
promise.lastly(lambda: print('We are done!'))
|
|
422
|
+
"""
|
|
423
|
+
# Renamed from Mojolicious: finally, a keyword of Python, as Bluebird names it
|
|
424
|
+
return self._finally(True, cb)
|
|
425
|
+
|
|
426
|
+
# The name of Mojolicious, with an underscore as finally is a keyword of Python
|
|
427
|
+
finally_ = lastly
|
|
428
|
+
|
|
429
|
+
@classmethod
|
|
430
|
+
def map(cls, cb: Callable[[Any], 'Pyjo_Promise'], *items: Any, concurrency: int = 0) -> 'Pyjo_Promise':
|
|
431
|
+
"""::
|
|
432
|
+
|
|
433
|
+
new = Pyjo.Promise.map(lambda item: ..., *items)
|
|
434
|
+
new = Pyjo.Promise.map(lambda item: ..., *items, concurrency=3)
|
|
435
|
+
|
|
436
|
+
Apply a function that returns a :mod:`Pyjo.Promise` to each item in a
|
|
437
|
+
list of items while optionally limiting concurrency. Returns a
|
|
438
|
+
:mod:`Pyjo.Promise` that collects the results in the same manner as
|
|
439
|
+
:meth:`all`. If any item's promise is rejected, any remaining items
|
|
440
|
+
which have not yet been mapped will not be. ::
|
|
441
|
+
|
|
442
|
+
# Perform 3 requests at a time concurrently
|
|
443
|
+
Pyjo.Promise.map(lambda url: ua.get_p(url), *urls, concurrency=3).then(
|
|
444
|
+
lambda *results: [print(r[0].res.dom().at('title').text) for r in results]
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
These options are currently available:
|
|
448
|
+
|
|
449
|
+
concurrency
|
|
450
|
+
::
|
|
451
|
+
|
|
452
|
+
concurrency=3
|
|
453
|
+
|
|
454
|
+
The maximum number of items that are in progress at the same time.
|
|
455
|
+
"""
|
|
456
|
+
if not concurrency or len(items) <= concurrency:
|
|
457
|
+
return cls.all(*(cb(item) for item in items))
|
|
458
|
+
|
|
459
|
+
# Deques, whose popleft takes the first item as shift of Perl does, where pop(0) of a list moves all the others
|
|
460
|
+
rest = collections.deque(items[concurrency:])
|
|
461
|
+
start = [cb(item) for item in items[:concurrency]]
|
|
462
|
+
wait = [start[0].clone() for _ in rest]
|
|
463
|
+
waiting = collections.deque(wait)
|
|
464
|
+
|
|
465
|
+
def start_next(*values: Any) -> None:
|
|
466
|
+
if not len(rest):
|
|
467
|
+
return
|
|
468
|
+
item = rest.popleft()
|
|
469
|
+
chain = waiting.popleft()
|
|
470
|
+
|
|
471
|
+
def resolve(*values: Any) -> None:
|
|
472
|
+
chain.resolve(*values)
|
|
473
|
+
start_next()
|
|
474
|
+
|
|
475
|
+
def reject(*values: Any) -> None:
|
|
476
|
+
chain.reject(*values)
|
|
477
|
+
rest.clear()
|
|
478
|
+
|
|
479
|
+
cb(item).then(resolve, reject)
|
|
480
|
+
|
|
481
|
+
for promise in start:
|
|
482
|
+
promise.then(start_next, lambda *values: None)
|
|
483
|
+
|
|
484
|
+
return cls.all(*start, *wait)
|
|
485
|
+
|
|
486
|
+
@classmethod
|
|
487
|
+
def race(cls, *promises: 'Pyjo_Promise') -> 'Pyjo_Promise':
|
|
488
|
+
"""::
|
|
489
|
+
|
|
490
|
+
new = Pyjo.Promise.race(*promises)
|
|
491
|
+
|
|
492
|
+
Returns a new :mod:`Pyjo.Promise` object that fulfills or rejects as
|
|
493
|
+
soon as one of the passed :mod:`Pyjo.Promise` objects fulfills or
|
|
494
|
+
rejects, with the value or reason from that promise.
|
|
495
|
+
"""
|
|
496
|
+
return _all(1, promises)
|
|
497
|
+
|
|
498
|
+
@_hybridmethod
|
|
499
|
+
def reject(self_or_cls: 'Pyjo_Promise | type[Pyjo_Promise]', *reason: Any) -> 'Pyjo_Promise': # noqa: N805
|
|
500
|
+
"""::
|
|
501
|
+
|
|
502
|
+
new = Pyjo.Promise.reject(*reason)
|
|
503
|
+
promise = promise.reject(*reason)
|
|
504
|
+
|
|
505
|
+
Build rejected :mod:`Pyjo.Promise` object or reject the promise with one
|
|
506
|
+
or more rejection reasons. ::
|
|
507
|
+
|
|
508
|
+
# Longer version
|
|
509
|
+
promise = Pyjo.Promise.new().reject(*reason)
|
|
510
|
+
"""
|
|
511
|
+
return Pyjo_Promise._settle(self_or_cls, 'reject', reason)
|
|
512
|
+
|
|
513
|
+
@_hybridmethod
|
|
514
|
+
def resolve(self_or_cls: 'Pyjo_Promise | type[Pyjo_Promise]', *value: Any) -> 'Pyjo_Promise': # noqa: N805
|
|
515
|
+
"""::
|
|
516
|
+
|
|
517
|
+
new = Pyjo.Promise.resolve(*value)
|
|
518
|
+
promise = promise.resolve(*value)
|
|
519
|
+
|
|
520
|
+
Build resolved :mod:`Pyjo.Promise` object or resolve the promise with
|
|
521
|
+
one or more fulfillment values. ::
|
|
522
|
+
|
|
523
|
+
# Longer version
|
|
524
|
+
promise = Pyjo.Promise.new().resolve(*value)
|
|
525
|
+
"""
|
|
526
|
+
return Pyjo_Promise._settle(self_or_cls, 'resolve', value)
|
|
527
|
+
|
|
528
|
+
def then(self, resolve: Handler | None = None, reject: Handler | None = None) -> 'Pyjo_Promise':
|
|
529
|
+
"""::
|
|
530
|
+
|
|
531
|
+
new = promise.then(lambda *value: ...)
|
|
532
|
+
new = promise.then(lambda *value: ..., lambda *reason: ...)
|
|
533
|
+
new = promise.then(None, lambda *reason: ...)
|
|
534
|
+
|
|
535
|
+
Appends fulfillment and rejection handlers to the promise, and returns
|
|
536
|
+
a new :mod:`Pyjo.Promise` object resolving to the return value of the
|
|
537
|
+
called handler. ::
|
|
538
|
+
|
|
539
|
+
# Pass along the fulfillment value or rejection reason
|
|
540
|
+
def value_cb(*value):
|
|
541
|
+
print(f'The result is {value[0]}')
|
|
542
|
+
return Pyjo.Promise.resolve(*value)
|
|
543
|
+
|
|
544
|
+
def reason_cb(*reason):
|
|
545
|
+
warn(f'Something went wrong: {reason[0]}')
|
|
546
|
+
return Pyjo.Promise.reject(*reason)
|
|
547
|
+
|
|
548
|
+
promise.then(value_cb, reason_cb)
|
|
549
|
+
|
|
550
|
+
# Change the fulfillment value or rejection reason
|
|
551
|
+
promise.then(lambda *value: f'This is good: {value[0]}', lambda *reason: f'This is bad: {reason[0]}')
|
|
552
|
+
"""
|
|
553
|
+
new = self.clone()
|
|
554
|
+
self._handled = True
|
|
555
|
+
self._resolve.append(lambda *results: _then_cb(new, resolve, 'resolve', results))
|
|
556
|
+
self._reject.append(lambda *results: _then_cb(new, reject, 'reject', results))
|
|
557
|
+
|
|
558
|
+
if self._results is not None:
|
|
559
|
+
self._defer()
|
|
560
|
+
|
|
561
|
+
return new
|
|
562
|
+
|
|
563
|
+
@_hybridmethod
|
|
564
|
+
def timer(self_or_cls: 'Pyjo_Promise | type[Pyjo_Promise]', after: float, *value: Any) -> 'Pyjo_Promise': # noqa: N805
|
|
565
|
+
"""::
|
|
566
|
+
|
|
567
|
+
new = Pyjo.Promise.timer(5, 'Success!')
|
|
568
|
+
promise = promise.timer(5, 'Success!')
|
|
569
|
+
promise = promise.timer(5)
|
|
570
|
+
|
|
571
|
+
Create a new :mod:`Pyjo.Promise` object with a timer or attach a timer
|
|
572
|
+
to an existing promise. The promise will be resolved after the given
|
|
573
|
+
amount of time in seconds with or without a value, unless it has already
|
|
574
|
+
been settled. Note that this does not delay the resolution of an existing
|
|
575
|
+
promise, to add a delay to a promise chain you have to return a new timer
|
|
576
|
+
promise instead. ::
|
|
577
|
+
|
|
578
|
+
# Wait 5 seconds between two steps
|
|
579
|
+
promise.then(lambda *_: Pyjo.Promise.timer(5)).then(lambda *_: print('Five seconds later'))
|
|
580
|
+
"""
|
|
581
|
+
return _timer(self_or_cls, 'resolve', after, value)
|
|
582
|
+
|
|
583
|
+
@_hybridmethod
|
|
584
|
+
def timeout(self_or_cls: 'Pyjo_Promise | type[Pyjo_Promise]', after: float, *reason: Any) -> 'Pyjo_Promise': # noqa: N805
|
|
585
|
+
"""::
|
|
586
|
+
|
|
587
|
+
new = Pyjo.Promise.timeout(5, 'Timeout!')
|
|
588
|
+
promise = promise.timeout(5, 'Timeout!')
|
|
589
|
+
promise = promise.timeout(5)
|
|
590
|
+
|
|
591
|
+
Create a new :mod:`Pyjo.Promise` object with a timeout or attach a
|
|
592
|
+
timeout to an existing promise. The promise will be rejected after the
|
|
593
|
+
given amount of time in seconds with a reason, which defaults to
|
|
594
|
+
``Promise timeout``, unless it has already been settled.
|
|
595
|
+
"""
|
|
596
|
+
return _timer(self_or_cls, 'reject', after, reason)
|
|
597
|
+
|
|
598
|
+
# Not in Mojolicious: a future of the reactor
|
|
599
|
+
def to_future(self) -> Pyjo.Reactor.Future[Any]:
|
|
600
|
+
"""::
|
|
601
|
+
|
|
602
|
+
future = promise.to_future()
|
|
603
|
+
|
|
604
|
+
Turn promise into a future of the reactor of :attr:`ioloop`, an
|
|
605
|
+
:class:`asyncio.Future` of its loop with :mod:`Pyjo.Reactor.Asyncio`,
|
|
606
|
+
or a :class:`concurrent.futures.Future` with a reactor without asyncio,
|
|
607
|
+
as :mod:`Pyjo.Reactor.Poll`. The future is done when the promise is
|
|
608
|
+
fulfilled or rejected, with what ``await promise`` gives: its value, a
|
|
609
|
+
tuple of several values, or ``None`` for none, or the exception it was
|
|
610
|
+
rejected with, or a :mod:`Pyjo.Exception` of the first reason. ::
|
|
611
|
+
|
|
612
|
+
# Print the value when the future is done
|
|
613
|
+
future = Pyjo.Promise.timer(1, 'Done!').to_future()
|
|
614
|
+
future.add_done_callback(lambda future: print(future.result()))
|
|
615
|
+
"""
|
|
616
|
+
future = self.ioloop.reactor.future()
|
|
617
|
+
|
|
618
|
+
def resolve(*values: Any) -> None:
|
|
619
|
+
if not future.done():
|
|
620
|
+
future.set_result(_result(values))
|
|
621
|
+
|
|
622
|
+
def reject(*reason: Any) -> None:
|
|
623
|
+
if not future.done():
|
|
624
|
+
future.set_exception(_exception(reason))
|
|
625
|
+
|
|
626
|
+
self.then(resolve, reject)
|
|
627
|
+
return future
|
|
628
|
+
|
|
629
|
+
def wait(self) -> None:
|
|
630
|
+
"""::
|
|
631
|
+
|
|
632
|
+
promise.wait()
|
|
633
|
+
|
|
634
|
+
Start :attr:`ioloop` and stop it again once the promise has been
|
|
635
|
+
fulfilled or rejected, does nothing when :attr:`ioloop` is already
|
|
636
|
+
running.
|
|
637
|
+
"""
|
|
638
|
+
if (loop := self.ioloop).is_running:
|
|
639
|
+
return
|
|
640
|
+
done = False
|
|
641
|
+
|
|
642
|
+
def finish() -> None:
|
|
643
|
+
nonlocal done
|
|
644
|
+
done = True
|
|
645
|
+
loop.stop()
|
|
646
|
+
|
|
647
|
+
self._finally(False, finish).catch(lambda *reason: None)
|
|
648
|
+
while not done:
|
|
649
|
+
loop.start()
|
|
650
|
+
|
|
651
|
+
@staticmethod
|
|
652
|
+
def _settle(
|
|
653
|
+
self_or_cls: 'Pyjo_Promise | type[Pyjo_Promise]', status: str, results: tuple[Any, ...]
|
|
654
|
+
) -> 'Pyjo_Promise':
|
|
655
|
+
first = results[0] if len(results) else None
|
|
656
|
+
thenable = first is not None and not isinstance(first, type) and callable(getattr(first, 'then', None))
|
|
657
|
+
if isinstance(self_or_cls, type):
|
|
658
|
+
if thenable and status == 'resolve' and isinstance(first, Pyjo_Promise):
|
|
659
|
+
return first
|
|
660
|
+
promise = self_or_cls.new()
|
|
661
|
+
else:
|
|
662
|
+
promise = self_or_cls
|
|
663
|
+
|
|
664
|
+
if thenable and status == 'resolve':
|
|
665
|
+
|
|
666
|
+
def resolve(*values: Any) -> None:
|
|
667
|
+
promise.resolve(*values)
|
|
668
|
+
|
|
669
|
+
def reject(*values: Any) -> None:
|
|
670
|
+
promise.reject(*values)
|
|
671
|
+
|
|
672
|
+
cast(Any, first).then(resolve, reject)
|
|
673
|
+
|
|
674
|
+
elif promise._results is None:
|
|
675
|
+
promise._results, promise._status = results, status
|
|
676
|
+
promise._defer()
|
|
677
|
+
|
|
678
|
+
return promise
|
|
679
|
+
|
|
680
|
+
def _defer(self) -> None:
|
|
681
|
+
if (results := self._results) is None:
|
|
682
|
+
return
|
|
683
|
+
cbs = self._resolve if self._status == 'resolve' else self._reject
|
|
684
|
+
self._resolve, self._reject = [], []
|
|
685
|
+
|
|
686
|
+
def run(loop: Any) -> None:
|
|
687
|
+
for cb in cbs:
|
|
688
|
+
cb(*results)
|
|
689
|
+
|
|
690
|
+
self.ioloop.next_tick(run)
|
|
691
|
+
|
|
692
|
+
def _finally(self, handled: bool, cb: Callable[[], Any]) -> 'Pyjo_Promise':
|
|
693
|
+
new = self.clone()
|
|
694
|
+
|
|
695
|
+
def finally_cb(*results: Any) -> 'Pyjo_Promise':
|
|
696
|
+
value = cb()
|
|
697
|
+
return (new.resolve() if value is None else new.resolve(value)).then(lambda *values: _values(results))
|
|
698
|
+
|
|
699
|
+
before = self._handled
|
|
700
|
+
self.catch(finally_cb)
|
|
701
|
+
then = self.then(finally_cb)
|
|
702
|
+
if not before and not handled:
|
|
703
|
+
self._handled = False
|
|
704
|
+
|
|
705
|
+
return then
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
# Not in Mojolicious: a decorator of a coroutine function, as Mojo's async sub
|
|
709
|
+
def coroutine[**P](func: Callable[P, Coroutine[Any, Any, Any]]) -> Callable[P, Pyjo_Promise]:
|
|
710
|
+
"""::
|
|
711
|
+
|
|
712
|
+
@Pyjo.Promise.coroutine
|
|
713
|
+
async def title(url):
|
|
714
|
+
tx = await ua.get_p(url)
|
|
715
|
+
return tx.res.dom().at('title').text
|
|
716
|
+
|
|
717
|
+
title('https://mojolicious.org').then(print).wait()
|
|
718
|
+
|
|
719
|
+
Turn a coroutine function into a function that returns a
|
|
720
|
+
:mod:`Pyjo.Promise` of the coroutine, as ``async sub`` does in Mojo.
|
|
721
|
+
"""
|
|
722
|
+
|
|
723
|
+
@functools.wraps(func)
|
|
724
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Pyjo_Promise:
|
|
725
|
+
return Pyjo_Promise.new(func(*args, **kwargs))
|
|
726
|
+
|
|
727
|
+
return wrapper
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _all(type: int, promises: tuple[Pyjo_Promise, ...]) -> Pyjo_Promise:
|
|
731
|
+
all = promises[0].clone()
|
|
732
|
+
results: list[Any] = [None] * len(promises)
|
|
733
|
+
remaining = len(promises)
|
|
734
|
+
|
|
735
|
+
def resolve_all(*values: Any) -> None:
|
|
736
|
+
all.resolve(*values)
|
|
737
|
+
|
|
738
|
+
def reject_all(*values: Any) -> None:
|
|
739
|
+
all.reject(*values)
|
|
740
|
+
|
|
741
|
+
for i, promise in enumerate(promises):
|
|
742
|
+
# "race"
|
|
743
|
+
if type == 1:
|
|
744
|
+
promise.then(resolve_all, reject_all)
|
|
745
|
+
|
|
746
|
+
# "all"
|
|
747
|
+
elif type == 2:
|
|
748
|
+
|
|
749
|
+
def fulfilled(*values: Any, i: int = i) -> None:
|
|
750
|
+
nonlocal remaining
|
|
751
|
+
results[i] = list(values)
|
|
752
|
+
remaining -= 1
|
|
753
|
+
if remaining <= 0:
|
|
754
|
+
all.resolve(*results)
|
|
755
|
+
|
|
756
|
+
promise.then(fulfilled, reject_all)
|
|
757
|
+
|
|
758
|
+
# "any"
|
|
759
|
+
elif type == 3:
|
|
760
|
+
|
|
761
|
+
def rejected(*values: Any, i: int = i) -> None:
|
|
762
|
+
nonlocal remaining
|
|
763
|
+
results[i] = list(values)
|
|
764
|
+
remaining -= 1
|
|
765
|
+
if remaining <= 0:
|
|
766
|
+
all.reject(*results)
|
|
767
|
+
|
|
768
|
+
promise.then(resolve_all, rejected)
|
|
769
|
+
|
|
770
|
+
# "all_settled"
|
|
771
|
+
else:
|
|
772
|
+
|
|
773
|
+
def settled_fulfilled(*values: Any, i: int = i) -> None:
|
|
774
|
+
nonlocal remaining
|
|
775
|
+
results[i] = {'status': 'fulfilled', 'value': list(values)}
|
|
776
|
+
remaining -= 1
|
|
777
|
+
if remaining <= 0:
|
|
778
|
+
all.resolve(*results)
|
|
779
|
+
|
|
780
|
+
def settled_rejected(*values: Any, i: int = i) -> None:
|
|
781
|
+
nonlocal remaining
|
|
782
|
+
results[i] = {'status': 'rejected', 'reason': list(values)}
|
|
783
|
+
remaining -= 1
|
|
784
|
+
if remaining <= 0:
|
|
785
|
+
all.resolve(*results)
|
|
786
|
+
|
|
787
|
+
promise.then(settled_fulfilled, settled_rejected)
|
|
788
|
+
|
|
789
|
+
return all
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _then_cb(new: Pyjo_Promise, cb: Handler | None, method: str, results: tuple[Any, ...]) -> Pyjo_Promise:
|
|
793
|
+
if cb is None:
|
|
794
|
+
return new.resolve(*results) if method == 'resolve' else new.reject(*results)
|
|
795
|
+
|
|
796
|
+
try:
|
|
797
|
+
res = cb(*results)
|
|
798
|
+
except Exception as e:
|
|
799
|
+
return new.reject(e)
|
|
800
|
+
# Not in Mojolicious: a callback of Python returns one value, None is no value
|
|
801
|
+
return new.resolve() if res is None else new.resolve(res)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def _timer(
|
|
805
|
+
self_or_cls: Pyjo_Promise | type[Pyjo_Promise], method: str, after: float, results: tuple[Any, ...]
|
|
806
|
+
) -> Pyjo_Promise:
|
|
807
|
+
promise = self_or_cls.new() if isinstance(self_or_cls, type) else self_or_cls
|
|
808
|
+
if method == 'reject' and not len(results):
|
|
809
|
+
results = ('Promise timeout',)
|
|
810
|
+
|
|
811
|
+
def timer(loop: Any) -> None:
|
|
812
|
+
promise.resolve(*results) if method == 'resolve' else promise.reject(*results)
|
|
813
|
+
|
|
814
|
+
promise.ioloop.timer(after, timer)
|
|
815
|
+
return promise
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
# A future of concurrent.futures settles the promise in the thread of the reactor, which a socket pair wakes up from the
|
|
819
|
+
# thread that completes the future, as the resolver of Pyjo.IOLoop.Client does, so that it works with any reactor
|
|
820
|
+
def _wait_future(promise: Pyjo_Promise, future: concurrent.futures.Future[Any]) -> None:
|
|
821
|
+
reactor = promise.ioloop.reactor
|
|
822
|
+
reader, writer = socket.socketpair()
|
|
823
|
+
reader.setblocking(False)
|
|
824
|
+
_pairs.update((reader, writer))
|
|
825
|
+
|
|
826
|
+
def ready(reactor: Any, writable: bool) -> None:
|
|
827
|
+
reactor.remove(reader)
|
|
828
|
+
_pairs.difference_update((reader, writer))
|
|
829
|
+
reader.close()
|
|
830
|
+
writer.close()
|
|
831
|
+
if future.cancelled():
|
|
832
|
+
promise.reject(concurrent.futures.CancelledError())
|
|
833
|
+
elif (e := future.exception()) is not None:
|
|
834
|
+
promise.reject(e)
|
|
835
|
+
elif (value := future.result()) is None:
|
|
836
|
+
promise.resolve()
|
|
837
|
+
else:
|
|
838
|
+
promise.resolve(value)
|
|
839
|
+
|
|
840
|
+
reactor.io(reader, ready).watch(reader, True, False)
|
|
841
|
+
future.add_done_callback(lambda future: writer.send(b'\x00'))
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
# The socket pairs of the futures that are not done when the program ends, after the threads of concurrent.futures, whose
|
|
845
|
+
# event loop does not run again to see them done
|
|
846
|
+
_pairs: set[socket.socket] = set()
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
@atexit.register
|
|
850
|
+
def _close_pairs() -> None:
|
|
851
|
+
for handle in list(_pairs):
|
|
852
|
+
handle.close()
|
|
853
|
+
_pairs.clear()
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
# An awaitable in a coroutine, which the reactor runs
|
|
857
|
+
async def _await(awaitable: Awaitable[Any]) -> Any:
|
|
858
|
+
return await awaitable
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
# The exception of a rejection: the reason, or an exception of it
|
|
862
|
+
def _exception(reason: tuple[Any, ...]) -> BaseException:
|
|
863
|
+
if len(reason) and isinstance(reason[0], BaseException):
|
|
864
|
+
return reason[0]
|
|
865
|
+
return Pyjo.Exception.new(reason[0] if len(reason) else None)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
# The value of a fulfillment: none, one, or a tuple of several
|
|
869
|
+
def _result(values: tuple[Any, ...]) -> Any:
|
|
870
|
+
return None if not len(values) else values[0] if len(values) == 1 else values
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
# The value of a handler that passes values on: none, one, or a promise of several
|
|
874
|
+
def _values(values: tuple[Any, ...]) -> Any:
|
|
875
|
+
if not len(values):
|
|
876
|
+
return None
|
|
877
|
+
if len(values) == 1:
|
|
878
|
+
return values[0]
|
|
879
|
+
return Pyjo_Promise.resolve(*values)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
new = Pyjo_Promise.new
|
|
883
|
+
object = Pyjo_Promise
|
|
884
|
+
Promise = Pyjo_Promise
|
|
885
|
+
|
|
886
|
+
all = Pyjo_Promise.all
|
|
887
|
+
all_settled = Pyjo_Promise.all_settled
|
|
888
|
+
any = Pyjo_Promise.any
|
|
889
|
+
map = Pyjo_Promise.map
|
|
890
|
+
race = Pyjo_Promise.race
|
|
891
|
+
reject = Pyjo_Promise.reject
|
|
892
|
+
resolve = Pyjo_Promise.resolve
|
|
893
|
+
timer = Pyjo_Promise.timer
|
|
894
|
+
timeout = Pyjo_Promise.timeout
|