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/Test/Pyjo.py
ADDED
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Test.Pyjo - Testing Pyjo
|
|
3
|
+
=============================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Test import *
|
|
7
|
+
|
|
8
|
+
import Pyjo.Test.Pyjo
|
|
9
|
+
|
|
10
|
+
t = Pyjo.Test.Pyjo.new('MyApp')
|
|
11
|
+
|
|
12
|
+
# HTML/XML
|
|
13
|
+
t.get_ok('/welcome').status_is(200).text_is('div#message', 'Hello!')
|
|
14
|
+
|
|
15
|
+
# JSON
|
|
16
|
+
(
|
|
17
|
+
t.post_ok('/search.json', form={'q': 'Python'})
|
|
18
|
+
.status_is(200)
|
|
19
|
+
.header_is('Server', 'Pyjoyment (Python)')
|
|
20
|
+
.header_isnt('X-Bender', 'Bite my shiny metal ass!')
|
|
21
|
+
.json_is('/results/4/title', 'Python rocks!')
|
|
22
|
+
.json_like('/results/7/title', re.compile(r'Python'))
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# WebSocket
|
|
26
|
+
t.websocket_ok('/echo').send_ok('hello').message_ok().message_is('echo: hello').finish_ok()
|
|
27
|
+
|
|
28
|
+
# Server-Sent Events (SSE)
|
|
29
|
+
(
|
|
30
|
+
t.get_sse_ok('/events')
|
|
31
|
+
.status_is(200)
|
|
32
|
+
.sse_ok()
|
|
33
|
+
.sse_type_is('message')
|
|
34
|
+
.sse_text_is('hello pyjo')
|
|
35
|
+
.sse_finish_ok()
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
done_testing()
|
|
39
|
+
|
|
40
|
+
:mod:`Pyjo.Test.Pyjo` is a test user agent based on :mod:`Pyjo.UserAgent`, it
|
|
41
|
+
is usually used together with :mod:`Pyjo.Test` to test :mod:`Pyjoyment`
|
|
42
|
+
applications. Just run your tests with :mod:`Pyjo.Prove`. ::
|
|
43
|
+
|
|
44
|
+
$ python -m Pyjo.Prove -l -v
|
|
45
|
+
$ python -m Pyjo.Prove -l -v t/foo.py
|
|
46
|
+
|
|
47
|
+
If it is not already defined, the ``PYJO_LOG_LEVEL`` environment variable
|
|
48
|
+
will be set to ``trace`` or ``fatal``, depending on the value of the
|
|
49
|
+
``PYJO_HARNESS_IS_VERBOSE`` environment variable, which
|
|
50
|
+
``python -m Pyjo.Prove -v`` sets, or without it of ``HARNESS_IS_VERBOSE``,
|
|
51
|
+
which Perl's ``prove -v`` sets. And to make it easier to test HTTPS/WSS web
|
|
52
|
+
services :attr:`Pyjo.UserAgent.Pyjo_UserAgent.insecure` will be activated by
|
|
53
|
+
default for :attr:`~Pyjo_Test_Pyjo.ua`.
|
|
54
|
+
|
|
55
|
+
The end of a ``with`` block closes the connections and the servers of its
|
|
56
|
+
:attr:`~Pyjo_Test_Pyjo.ua`.
|
|
57
|
+
|
|
58
|
+
See also
|
|
59
|
+
--------
|
|
60
|
+
|
|
61
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
62
|
+
|
|
63
|
+
Ported from `Test::Mojo <https://github.com/mojolicious/mojo/blob/v9.49/lib/Test/Mojo.pm>`_ of Mojolicious 9.49.
|
|
64
|
+
|
|
65
|
+
Classes
|
|
66
|
+
-------
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
import Pyjo.Base
|
|
70
|
+
import Pyjo.File
|
|
71
|
+
import Pyjo.IOLoop
|
|
72
|
+
import Pyjo.JSON.Pointer
|
|
73
|
+
import Pyjo.Server
|
|
74
|
+
import Pyjo.Test
|
|
75
|
+
import Pyjo.UserAgent
|
|
76
|
+
|
|
77
|
+
from Pyjo.JSON import j
|
|
78
|
+
from Pyjo.Util import decode
|
|
79
|
+
|
|
80
|
+
import os
|
|
81
|
+
import re
|
|
82
|
+
|
|
83
|
+
from collections.abc import Callable
|
|
84
|
+
from types import TracebackType
|
|
85
|
+
from typing import TYPE_CHECKING, Any, Self, cast
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if TYPE_CHECKING:
|
|
89
|
+
import Pyjo.Transaction.HTTP
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Silent or loud tests
|
|
93
|
+
# Unlike Mojolicious: PYJO_HARNESS_IS_VERBOSE of Pyjo.Prove, and HARNESS_IS_VERBOSE of prove without it
|
|
94
|
+
if not os.environ.get('PYJO_LOG_LEVEL'):
|
|
95
|
+
verbose = os.environ.get('PYJO_HARNESS_IS_VERBOSE', os.environ.get('HARNESS_IS_VERBOSE'))
|
|
96
|
+
os.environ['PYJO_LOG_LEVEL'] = 'trace' if verbose else 'fatal'
|
|
97
|
+
|
|
98
|
+
# Not in Mojolicious: pytest leaves the frames of this module out of the traceback of a failed test, as those of
|
|
99
|
+
# Pyjo.Test
|
|
100
|
+
__tracebackhide__ = Pyjo.Test.__tracebackhide__
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Pyjo_Test_Pyjo(Pyjo.Base.object):
|
|
104
|
+
"""
|
|
105
|
+
:mod:`Pyjo.Test.Pyjo` inherits all methods from :mod:`Pyjo.Base` and
|
|
106
|
+
implements the following new ones.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, app: Any = None, config: dict[str, Any] | None = None, /) -> None:
|
|
110
|
+
"""::
|
|
111
|
+
|
|
112
|
+
t = Pyjo.Test.Pyjo.new()
|
|
113
|
+
t = Pyjo.Test.Pyjo.new('MyApp')
|
|
114
|
+
t = Pyjo.Test.Pyjo.new('MyApp', {'foo': 'bar'})
|
|
115
|
+
t = Pyjo.Test.Pyjo.new(Pyjo.File.new('/path/to/myapp.py'))
|
|
116
|
+
t = Pyjo.Test.Pyjo.new(Pyjo.File.new('/path/to/myapp.py'), {'foo': 'bar'})
|
|
117
|
+
t = Pyjo.Test.Pyjo.new(MyApp.new())
|
|
118
|
+
t = Pyjo.Test.Pyjo.new(MyApp.new(), {'foo': 'bar'})
|
|
119
|
+
|
|
120
|
+
Construct a new :mod:`Pyjo.Test.Pyjo` object. In addition to a class
|
|
121
|
+
name or :mod:`Pyjo.File` object pointing to the application script,
|
|
122
|
+
you can pass along a dict with configuration values that will be used
|
|
123
|
+
to override the application configuration. The special configuration
|
|
124
|
+
value ``config_override`` will be set in
|
|
125
|
+
:meth:`Pyjoyment.Pyjoyment_App.config` as well, which is used to
|
|
126
|
+
disable configuration plugins for tests. ::
|
|
127
|
+
|
|
128
|
+
# Load application script relative to the "t" directory
|
|
129
|
+
from Pyjo.File import curfile
|
|
130
|
+
|
|
131
|
+
t = Pyjo.Test.Pyjo.new(curfile().dirname().sibling('myapp.py'))
|
|
132
|
+
"""
|
|
133
|
+
super().__init__()
|
|
134
|
+
|
|
135
|
+
self.handler: Callable[..., Any] = _handler
|
|
136
|
+
"""::
|
|
137
|
+
|
|
138
|
+
cb = t.handler
|
|
139
|
+
t.handler = lambda name, *args: ...
|
|
140
|
+
|
|
141
|
+
A callback to connect :mod:`Pyjo.Test.Pyjo` with :mod:`Pyjo.Test`,
|
|
142
|
+
which gets the name of a function of ``Test::More``, as ``is``, and
|
|
143
|
+
its arguments. ::
|
|
144
|
+
|
|
145
|
+
def handler(name, *args):
|
|
146
|
+
return {'is': is_ok, 'like': like_ok, 'ok': ok}[name](*args)
|
|
147
|
+
|
|
148
|
+
t.handler = handler
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
self.message: list[Any] | None = None
|
|
152
|
+
"""::
|
|
153
|
+
|
|
154
|
+
msg = t.message
|
|
155
|
+
t.message = ['text', b'bytes']
|
|
156
|
+
|
|
157
|
+
Current WebSocket message represented as a list containing the frame
|
|
158
|
+
type and payload. ::
|
|
159
|
+
|
|
160
|
+
# More specific tests
|
|
161
|
+
from Pyjo.JSON import decode_json
|
|
162
|
+
|
|
163
|
+
hash = decode_json(t.message[1])
|
|
164
|
+
is_ok(type(hash), dict, 'right reference')
|
|
165
|
+
is_ok(hash['foo'], 'bar', 'right value')
|
|
166
|
+
|
|
167
|
+
# Test custom message
|
|
168
|
+
t.message = ['binary', b'bytes']
|
|
169
|
+
t.json_message_has('/foo/bar').json_message_hasnt('/bar').json_message_is('/foo/baz', {'yada': [1, 2, 3]})
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
self.sse: dict[str, Any] | None = None
|
|
173
|
+
"""::
|
|
174
|
+
|
|
175
|
+
event = t.sse
|
|
176
|
+
t.sse = {'type': 'message', 'text': 'working!'}
|
|
177
|
+
|
|
178
|
+
Current Server-Sent Event (SSE) represented as a dict. Note that this
|
|
179
|
+
attribute is **EXPERIMENTAL** and may change without warning!
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
self.success: bool | None = None
|
|
183
|
+
"""::
|
|
184
|
+
|
|
185
|
+
boolean = t.success
|
|
186
|
+
t.success = boolean
|
|
187
|
+
|
|
188
|
+
True if the last test was successful. ::
|
|
189
|
+
|
|
190
|
+
# Build custom tests
|
|
191
|
+
def location_is(t, value, desc=''):
|
|
192
|
+
desc = desc or f'Location: {value}'
|
|
193
|
+
t.success = bool(is_ok(t.tx.res.headers.location, value, desc))
|
|
194
|
+
return t
|
|
195
|
+
|
|
196
|
+
t.get_ok('/').status_is(302)
|
|
197
|
+
location_is(t, 'https://mojolicious.org').otherwise(lambda t: diag('Must have been Joel!'))
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
self.tx: Pyjo.Transaction.HTTP.object | None = None
|
|
201
|
+
"""::
|
|
202
|
+
|
|
203
|
+
tx = t.tx
|
|
204
|
+
t.tx = Pyjo.Transaction.HTTP.new()
|
|
205
|
+
|
|
206
|
+
Current transaction, usually a :mod:`Pyjo.Transaction.HTTP` or
|
|
207
|
+
:mod:`Pyjo.Transaction.WebSocket` object. ::
|
|
208
|
+
|
|
209
|
+
# More specific tests
|
|
210
|
+
is_ok(t.tx.res.json()['foo'], 'bar', 'right value')
|
|
211
|
+
ok(t.tx.res.content.is_multipart, 'multipart content')
|
|
212
|
+
is_ok(t.tx.previous.res.code, 302, 'right status')
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
self._ua: Pyjo.UserAgent.object | None = None
|
|
216
|
+
self._finished: list[Any] | None = None
|
|
217
|
+
self._messages: list[list[Any]] = []
|
|
218
|
+
self._sse_events: list[dict[str, Any]] = []
|
|
219
|
+
self._sse_finished: bool | None = None
|
|
220
|
+
|
|
221
|
+
if app is None:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
cfg: dict[str, Any] = {'config': {'config_override': 1, **config}} if config is not None else {}
|
|
225
|
+
if isinstance(app, (str, type)):
|
|
226
|
+
self.app = Pyjo.Server.new().build_app(app, **cfg)
|
|
227
|
+
elif isinstance(app, (Pyjo.File.object, os.PathLike)):
|
|
228
|
+
self.app = Pyjo.Server.new().load_app(app, **cfg) # pyright: ignore[reportUnknownArgumentType]
|
|
229
|
+
else:
|
|
230
|
+
if cfg:
|
|
231
|
+
app.config(cfg['config'])
|
|
232
|
+
self.app = app
|
|
233
|
+
|
|
234
|
+
# Not in Mojolicious: a context manager, which closes the connections and the servers of its user agent where the
|
|
235
|
+
# block ends, as PyPy frees the object only when its garbage collector runs
|
|
236
|
+
def __enter__(self) -> Self:
|
|
237
|
+
return self
|
|
238
|
+
|
|
239
|
+
def __exit__(
|
|
240
|
+
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
|
241
|
+
) -> None:
|
|
242
|
+
self.close()
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def ua(self) -> Pyjo.UserAgent.object:
|
|
246
|
+
"""::
|
|
247
|
+
|
|
248
|
+
ua = t.ua
|
|
249
|
+
t.ua = Pyjo.UserAgent.new()
|
|
250
|
+
|
|
251
|
+
User agent used for testing, defaults to a :mod:`Pyjo.UserAgent`
|
|
252
|
+
object. ::
|
|
253
|
+
|
|
254
|
+
# Allow redirects
|
|
255
|
+
t.ua.max_redirects = 10
|
|
256
|
+
t.get_ok('/redirect').status_is(200).content_like(re.compile(r'redirected'))
|
|
257
|
+
|
|
258
|
+
# Switch protocol from HTTP to HTTPS
|
|
259
|
+
t.ua.server.url('https')
|
|
260
|
+
t.get_ok('/secure').status_is(200).content_like(re.compile(r'secure'))
|
|
261
|
+
|
|
262
|
+
# Use absolute URL for request with Basic authentication
|
|
263
|
+
url = t.ua.server.url().set(userinfo='sri:secr3t').set(path='/secrets.json')
|
|
264
|
+
t.post_ok(url, json={'limit': 10}).status_is(200).json_is('/1/content', 'Pyjo rocks!')
|
|
265
|
+
|
|
266
|
+
# Customize all transactions (including followed redirects)
|
|
267
|
+
t.ua.on('start', lambda ua, tx: tx.req.headers.set(accept_language='en-US'))
|
|
268
|
+
t.get_ok('/hello').status_is(200).content_like(re.compile(r'Howdy'))
|
|
269
|
+
"""
|
|
270
|
+
if self._ua is None:
|
|
271
|
+
self._ua = Pyjo.UserAgent.new(insecure=True, ioloop=Pyjo.IOLoop.singleton)
|
|
272
|
+
return self._ua
|
|
273
|
+
|
|
274
|
+
@ua.setter
|
|
275
|
+
def ua(self, value: Pyjo.UserAgent.object) -> None:
|
|
276
|
+
self._ua = value
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def app(self) -> Any:
|
|
280
|
+
"""::
|
|
281
|
+
|
|
282
|
+
app = t.app
|
|
283
|
+
t.app = Pyjoyment.new()
|
|
284
|
+
|
|
285
|
+
Access application with :attr:`Pyjo.UserAgent.Server.Pyjo_UserAgent_Server.app`. ::
|
|
286
|
+
|
|
287
|
+
# Change log level
|
|
288
|
+
t.app.log.level = 'fatal'
|
|
289
|
+
|
|
290
|
+
# Test application directly
|
|
291
|
+
is_ok(t.app.defaults('foo'), 'bar', 'right value')
|
|
292
|
+
ok(t.app.routes.find('echo').is_websocket, 'WebSocket route')
|
|
293
|
+
c = t.app.build_controller()
|
|
294
|
+
ok(c.render(template='foo'), 'rendering was successful')
|
|
295
|
+
is_ok(c.res.code, 200, 'right status')
|
|
296
|
+
is_ok(c.res.body, b'Foo!', 'right content')
|
|
297
|
+
|
|
298
|
+
# Change application behavior
|
|
299
|
+
@t.app.hook('before_dispatch')
|
|
300
|
+
def before_dispatch(c):
|
|
301
|
+
if c.req.url.path.contains('/user'):
|
|
302
|
+
c.render(text='This request did not reach the router.')
|
|
303
|
+
|
|
304
|
+
t.get_ok('/user').status_is(200).content_like(re.compile(r'not reach the router'))
|
|
305
|
+
|
|
306
|
+
# Extract additional information
|
|
307
|
+
stash = {}
|
|
308
|
+
t.app.hook('after_dispatch', lambda c: stash.update(c.stash()))
|
|
309
|
+
t.get_ok('/hello').status_is(200)
|
|
310
|
+
is_ok(stash['foo'], 'bar', 'right value')
|
|
311
|
+
"""
|
|
312
|
+
return self.ua.server.app
|
|
313
|
+
|
|
314
|
+
@app.setter
|
|
315
|
+
def app(self, value: Any) -> None:
|
|
316
|
+
self.ua.server.app = value
|
|
317
|
+
|
|
318
|
+
def attr_is(self, selector: str, attr: str, value: Any, desc: str | None = None) -> Self:
|
|
319
|
+
"""::
|
|
320
|
+
|
|
321
|
+
t = t.attr_is('img.cat', 'alt', 'Grumpy cat')
|
|
322
|
+
t = t.attr_is('img.cat', 'alt', 'Grumpy cat', 'right alt text')
|
|
323
|
+
|
|
324
|
+
Checks text content of attribute with :meth:`Pyjo.DOM.Pyjo_DOM.attr` at
|
|
325
|
+
the CSS selectors first matching HTML/XML element for exact match with
|
|
326
|
+
:meth:`Pyjo.DOM.Pyjo_DOM.at`.
|
|
327
|
+
"""
|
|
328
|
+
desc = _desc(desc, f'exact match for attribute "{attr}" at selector "{selector}"')
|
|
329
|
+
return self.test('is', self._attr(selector, attr), value, desc)
|
|
330
|
+
|
|
331
|
+
def attr_isnt(self, selector: str, attr: str, value: Any, desc: str | None = None) -> Self:
|
|
332
|
+
"""::
|
|
333
|
+
|
|
334
|
+
t = t.attr_isnt('img.cat', 'alt', 'Calm cat')
|
|
335
|
+
t = t.attr_isnt('img.cat', 'alt', 'Calm cat', 'different alt text')
|
|
336
|
+
|
|
337
|
+
Opposite of :meth:`attr_is`.
|
|
338
|
+
"""
|
|
339
|
+
desc = _desc(desc, f'no match for attribute "{attr}" at selector "{selector}"')
|
|
340
|
+
return self.test('isnt', self._attr(selector, attr), value, desc)
|
|
341
|
+
|
|
342
|
+
def attr_like(self, selector: str, attr: str, regex: Any, desc: str | None = None) -> Self:
|
|
343
|
+
"""::
|
|
344
|
+
|
|
345
|
+
t = t.attr_like('img.cat', 'alt', re.compile(r'Grumpy'))
|
|
346
|
+
t = t.attr_like('img.cat', 'alt', re.compile(r'Grumpy'), 'right alt text')
|
|
347
|
+
|
|
348
|
+
Checks text content of attribute with :meth:`Pyjo.DOM.Pyjo_DOM.attr` at
|
|
349
|
+
the CSS selectors first matching HTML/XML element for similar match
|
|
350
|
+
with :meth:`Pyjo.DOM.Pyjo_DOM.at`.
|
|
351
|
+
"""
|
|
352
|
+
desc = _desc(desc, f'similar match for attribute "{attr}" at selector "{selector}"')
|
|
353
|
+
return self.test('like', self._attr(selector, attr), regex, desc)
|
|
354
|
+
|
|
355
|
+
def attr_unlike(self, selector: str, attr: str, regex: Any, desc: str | None = None) -> Self:
|
|
356
|
+
"""::
|
|
357
|
+
|
|
358
|
+
t = t.attr_unlike('img.cat', 'alt', re.compile(r'Calm'))
|
|
359
|
+
t = t.attr_unlike('img.cat', 'alt', re.compile(r'Calm'), 'different alt text')
|
|
360
|
+
|
|
361
|
+
Opposite of :meth:`attr_like`.
|
|
362
|
+
"""
|
|
363
|
+
desc = _desc(desc, f'no similar match for attribute "{attr}" at selector "{selector}"')
|
|
364
|
+
return self.test('unlike', self._attr(selector, attr), regex, desc)
|
|
365
|
+
|
|
366
|
+
# Not in Mojolicious: the release of the user agent, which Perl does in DESTROY, also at the end of a with block
|
|
367
|
+
def close(self) -> None:
|
|
368
|
+
"""::
|
|
369
|
+
|
|
370
|
+
t.close()
|
|
371
|
+
|
|
372
|
+
Close the connections and the servers of :attr:`ua`, as the end of a
|
|
373
|
+
``with`` block does.
|
|
374
|
+
"""
|
|
375
|
+
if self._ua is not None:
|
|
376
|
+
self._ua.close()
|
|
377
|
+
|
|
378
|
+
def content_is(self, value: Any, desc: str | None = None) -> Self:
|
|
379
|
+
"""::
|
|
380
|
+
|
|
381
|
+
t = t.content_is('working!')
|
|
382
|
+
t = t.content_is('working!', 'right content')
|
|
383
|
+
|
|
384
|
+
Check response content for exact match after retrieving it from
|
|
385
|
+
:attr:`Pyjo.Message.Pyjo_Message.text`.
|
|
386
|
+
"""
|
|
387
|
+
return self.test('is', self._tx.res.text, value, _desc(desc, 'exact match for content'))
|
|
388
|
+
|
|
389
|
+
def content_isnt(self, value: Any, desc: str | None = None) -> Self:
|
|
390
|
+
"""::
|
|
391
|
+
|
|
392
|
+
t = t.content_isnt('working!')
|
|
393
|
+
t = t.content_isnt('working!', 'different content')
|
|
394
|
+
|
|
395
|
+
Opposite of :meth:`content_is`.
|
|
396
|
+
"""
|
|
397
|
+
return self.test('isnt', self._tx.res.text, value, _desc(desc, 'no match for content'))
|
|
398
|
+
|
|
399
|
+
def content_like(self, regex: Any, desc: str | None = None) -> Self:
|
|
400
|
+
"""::
|
|
401
|
+
|
|
402
|
+
t = t.content_like(re.compile(r'working!'))
|
|
403
|
+
t = t.content_like(re.compile(r'working!'), 'right content')
|
|
404
|
+
|
|
405
|
+
Check response content for similar match after retrieving it from
|
|
406
|
+
:attr:`Pyjo.Message.Pyjo_Message.text`.
|
|
407
|
+
"""
|
|
408
|
+
return self.test('like', self._tx.res.text, regex, _desc(desc, 'content is similar'))
|
|
409
|
+
|
|
410
|
+
def content_type_is(self, type: str, desc: str | None = None) -> Self:
|
|
411
|
+
"""::
|
|
412
|
+
|
|
413
|
+
t = t.content_type_is('text/html')
|
|
414
|
+
t = t.content_type_is('text/html', 'right content type')
|
|
415
|
+
|
|
416
|
+
Check response ``Content-Type`` header for exact match.
|
|
417
|
+
"""
|
|
418
|
+
return self.test('is', self._tx.res.headers.content_type, type, _desc(desc, f'Content-Type: {type}'))
|
|
419
|
+
|
|
420
|
+
def content_type_isnt(self, type: str, desc: str | None = None) -> Self:
|
|
421
|
+
"""::
|
|
422
|
+
|
|
423
|
+
t = t.content_type_isnt('text/html')
|
|
424
|
+
t = t.content_type_isnt('text/html', 'different content type')
|
|
425
|
+
|
|
426
|
+
Opposite of :meth:`content_type_is`.
|
|
427
|
+
"""
|
|
428
|
+
return self.test('isnt', self._tx.res.headers.content_type, type, _desc(desc, f'not Content-Type: {type}'))
|
|
429
|
+
|
|
430
|
+
def content_type_like(self, regex: Any, desc: str | None = None) -> Self:
|
|
431
|
+
"""::
|
|
432
|
+
|
|
433
|
+
t = t.content_type_like(re.compile(r'text'))
|
|
434
|
+
t = t.content_type_like(re.compile(r'text'), 'right content type')
|
|
435
|
+
|
|
436
|
+
Check response ``Content-Type`` header for similar match.
|
|
437
|
+
"""
|
|
438
|
+
return self.test('like', self._tx.res.headers.content_type, regex, _desc(desc, 'Content-Type is similar'))
|
|
439
|
+
|
|
440
|
+
def content_type_unlike(self, regex: Any, desc: str | None = None) -> Self:
|
|
441
|
+
"""::
|
|
442
|
+
|
|
443
|
+
t = t.content_type_unlike(re.compile(r'text'))
|
|
444
|
+
t = t.content_type_unlike(re.compile(r'text'), 'different content type')
|
|
445
|
+
|
|
446
|
+
Opposite of :meth:`content_type_like`.
|
|
447
|
+
"""
|
|
448
|
+
desc = _desc(desc, 'Content-Type is not similar')
|
|
449
|
+
return self.test('unlike', self._tx.res.headers.content_type, regex, desc)
|
|
450
|
+
|
|
451
|
+
def content_unlike(self, regex: Any, desc: str | None = None) -> Self:
|
|
452
|
+
"""::
|
|
453
|
+
|
|
454
|
+
t = t.content_unlike(re.compile(r'working!'))
|
|
455
|
+
t = t.content_unlike(re.compile(r'working!'), 'different content')
|
|
456
|
+
|
|
457
|
+
Opposite of :meth:`content_like`.
|
|
458
|
+
"""
|
|
459
|
+
return self.test('unlike', self._tx.res.text, regex, _desc(desc, 'content is not similar'))
|
|
460
|
+
|
|
461
|
+
def delete_ok(self, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
462
|
+
"""::
|
|
463
|
+
|
|
464
|
+
t = t.delete_ok('http://example.com/foo')
|
|
465
|
+
t = t.delete_ok('/foo')
|
|
466
|
+
t = t.delete_ok('/foo', {'Accept': '*/*'}, 'Content!')
|
|
467
|
+
t = t.delete_ok('/foo', {'Accept': '*/*'}, form={'a': 'b'})
|
|
468
|
+
t = t.delete_ok('/foo', {'Accept': '*/*'}, json={'a': 'b'})
|
|
469
|
+
|
|
470
|
+
Perform a ``DELETE`` request and check for transport errors, takes the
|
|
471
|
+
same arguments as :meth:`Pyjo.UserAgent.Pyjo_UserAgent.delete`, except
|
|
472
|
+
for the callback.
|
|
473
|
+
"""
|
|
474
|
+
return self._build_ok('DELETE', url, *args, **kwargs)
|
|
475
|
+
|
|
476
|
+
def element_count_is(self, selector: str, count: int, desc: str | None = None) -> Self:
|
|
477
|
+
"""::
|
|
478
|
+
|
|
479
|
+
t = t.element_count_is('div.foo[x=y]', 5)
|
|
480
|
+
t = t.element_count_is('html body div', 30, 'thirty elements')
|
|
481
|
+
|
|
482
|
+
Checks the number of HTML/XML elements matched by the CSS selector
|
|
483
|
+
with :meth:`Pyjo.DOM.Pyjo_DOM.find`.
|
|
484
|
+
"""
|
|
485
|
+
size = len(self._dom().find(selector))
|
|
486
|
+
return self.test('is', size, count, _desc(desc, f'element count for selector "{selector}"'))
|
|
487
|
+
|
|
488
|
+
def element_exists(self, selector: str, desc: str | None = None) -> Self:
|
|
489
|
+
"""::
|
|
490
|
+
|
|
491
|
+
t = t.element_exists('div.foo[x=y]')
|
|
492
|
+
t = t.element_exists('html head title', 'has a title')
|
|
493
|
+
|
|
494
|
+
Checks for existence of the CSS selectors first matching HTML/XML
|
|
495
|
+
element with :meth:`Pyjo.DOM.Pyjo_DOM.at`. ::
|
|
496
|
+
|
|
497
|
+
# Check attribute values
|
|
498
|
+
(
|
|
499
|
+
t.get_ok('/login')
|
|
500
|
+
.element_exists('label[for=email]')
|
|
501
|
+
.element_exists('input[name=email][type=text][value*="example.com"]')
|
|
502
|
+
.element_exists('label[for=pass]')
|
|
503
|
+
.element_exists('input[name=pass][type=password]')
|
|
504
|
+
.element_exists('input[type=submit][value]')
|
|
505
|
+
)
|
|
506
|
+
"""
|
|
507
|
+
desc = _desc(desc, f'element for selector "{selector}" exists')
|
|
508
|
+
return self.test('ok', self._dom().at(selector), desc)
|
|
509
|
+
|
|
510
|
+
def element_exists_not(self, selector: str, desc: str | None = None) -> Self:
|
|
511
|
+
"""::
|
|
512
|
+
|
|
513
|
+
t = t.element_exists_not('div.foo[x=y]')
|
|
514
|
+
t = t.element_exists_not('html head title', 'has no title')
|
|
515
|
+
|
|
516
|
+
Opposite of :meth:`element_exists`.
|
|
517
|
+
"""
|
|
518
|
+
return self.test('ok', not self._dom().at(selector), _desc(desc, f'no element for selector "{selector}"'))
|
|
519
|
+
|
|
520
|
+
def finish_ok(self, *args: Any) -> Self:
|
|
521
|
+
"""::
|
|
522
|
+
|
|
523
|
+
t = t.finish_ok()
|
|
524
|
+
t = t.finish_ok(1000)
|
|
525
|
+
t = t.finish_ok(1003, 'Cannot accept data!')
|
|
526
|
+
|
|
527
|
+
Close WebSocket connection gracefully.
|
|
528
|
+
"""
|
|
529
|
+
if self._tx.is_websocket:
|
|
530
|
+
self._tx.finish(*args)
|
|
531
|
+
while self._finished is None:
|
|
532
|
+
Pyjo.IOLoop.one_tick()
|
|
533
|
+
return self.test('ok', 1, 'closed WebSocket')
|
|
534
|
+
|
|
535
|
+
def finished_ok(self, code: int) -> Self:
|
|
536
|
+
"""::
|
|
537
|
+
|
|
538
|
+
t = t.finished_ok(1000)
|
|
539
|
+
|
|
540
|
+
Wait for WebSocket connection to be closed gracefully and check status.
|
|
541
|
+
"""
|
|
542
|
+
while self._finished is None:
|
|
543
|
+
Pyjo.IOLoop.one_tick()
|
|
544
|
+
status = self._finished[0] if len(self._finished) else None
|
|
545
|
+
if not (ok := status == code):
|
|
546
|
+
Pyjo.Test.diag(f'WebSocket closed with status {status}')
|
|
547
|
+
return self.test('ok', ok, f'WebSocket closed with status {code}')
|
|
548
|
+
|
|
549
|
+
def get_ok(self, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
550
|
+
"""::
|
|
551
|
+
|
|
552
|
+
t = t.get_ok('http://example.com/foo')
|
|
553
|
+
t = t.get_ok('/foo')
|
|
554
|
+
t = t.get_ok('/foo', {'Accept': '*/*'}, 'Content!')
|
|
555
|
+
t = t.get_ok('/foo', {'Accept': '*/*'}, form={'a': 'b'})
|
|
556
|
+
t = t.get_ok('/foo', {'Accept': '*/*'}, json={'a': 'b'})
|
|
557
|
+
|
|
558
|
+
Perform a ``GET`` request and check for transport errors, takes the
|
|
559
|
+
same arguments as :meth:`Pyjo.UserAgent.Pyjo_UserAgent.get`, except
|
|
560
|
+
for the callback. ::
|
|
561
|
+
|
|
562
|
+
# Run tests against remote host
|
|
563
|
+
t.get_ok('https://github.com').status_is(200)
|
|
564
|
+
|
|
565
|
+
# Use relative URL for request with Basic authentication
|
|
566
|
+
t.get_ok('//sri:secr3t@/secrets.json').status_is(200).json_is('/1/content', 'Pyjo rocks!')
|
|
567
|
+
|
|
568
|
+
# Run additional tests on the transaction
|
|
569
|
+
t.get_ok('/foo').status_is(200)
|
|
570
|
+
is_ok(t.tx.res.dom().at('input').val, 'whatever', 'right value')
|
|
571
|
+
"""
|
|
572
|
+
return self._build_ok('GET', url, *args, **kwargs)
|
|
573
|
+
|
|
574
|
+
def get_sse_ok(self, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
575
|
+
"""::
|
|
576
|
+
|
|
577
|
+
t = t.get_sse_ok('http://example.com/events')
|
|
578
|
+
t = t.get_sse_ok('/events')
|
|
579
|
+
t = t.get_sse_ok('/events', {'Accept': 'text/event-stream'}, 'Content!')
|
|
580
|
+
t = t.get_sse_ok('/events', {'Accept': 'text/event-stream'}, form={'a': 'b'})
|
|
581
|
+
t = t.get_sse_ok('/events', {'Accept': 'text/event-stream'}, json={'a': 'b'})
|
|
582
|
+
|
|
583
|
+
Perform a ``GET`` request to establish a Server-Sent Events (SSE)
|
|
584
|
+
connection, takes the same arguments as
|
|
585
|
+
:meth:`Pyjo.UserAgent.Pyjo_UserAgent.get`, except for the callback.
|
|
586
|
+
Note that this method is **EXPERIMENTAL** and may change without
|
|
587
|
+
warning!
|
|
588
|
+
"""
|
|
589
|
+
return self._build_sse_ok('GET', url, *args, **kwargs)
|
|
590
|
+
|
|
591
|
+
def head_ok(self, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
592
|
+
"""::
|
|
593
|
+
|
|
594
|
+
t = t.head_ok('http://example.com/foo')
|
|
595
|
+
t = t.head_ok('/foo')
|
|
596
|
+
t = t.head_ok('/foo', {'Accept': '*/*'}, 'Content!')
|
|
597
|
+
t = t.head_ok('/foo', {'Accept': '*/*'}, form={'a': 'b'})
|
|
598
|
+
t = t.head_ok('/foo', {'Accept': '*/*'}, json={'a': 'b'})
|
|
599
|
+
|
|
600
|
+
Perform a ``HEAD`` request and check for transport errors, takes the
|
|
601
|
+
same arguments as :meth:`Pyjo.UserAgent.Pyjo_UserAgent.head`, except
|
|
602
|
+
for the callback.
|
|
603
|
+
"""
|
|
604
|
+
return self._build_ok('HEAD', url, *args, **kwargs)
|
|
605
|
+
|
|
606
|
+
def header_exists(self, name: str, desc: str | None = None) -> Self:
|
|
607
|
+
"""::
|
|
608
|
+
|
|
609
|
+
t = t.header_exists('ETag')
|
|
610
|
+
t = t.header_exists('ETag', 'header exists')
|
|
611
|
+
|
|
612
|
+
Check if response header exists.
|
|
613
|
+
"""
|
|
614
|
+
exists = bool(len(self._tx.res.headers.every_header(name)))
|
|
615
|
+
return self.test('ok', exists, _desc(desc, f'header "{name}" exists'))
|
|
616
|
+
|
|
617
|
+
def header_exists_not(self, name: str, desc: str | None = None) -> Self:
|
|
618
|
+
"""::
|
|
619
|
+
|
|
620
|
+
t = t.header_exists_not('ETag')
|
|
621
|
+
t = t.header_exists_not('ETag', 'header is missing')
|
|
622
|
+
|
|
623
|
+
Opposite of :meth:`header_exists`.
|
|
624
|
+
"""
|
|
625
|
+
missing = not len(self._tx.res.headers.every_header(name))
|
|
626
|
+
return self.test('ok', missing, _desc(desc, f'no "{name}" header'))
|
|
627
|
+
|
|
628
|
+
def header_is(self, name: str, value: Any, desc: str | None = None) -> Self:
|
|
629
|
+
"""::
|
|
630
|
+
|
|
631
|
+
t = t.header_is('ETag', '"abc321"')
|
|
632
|
+
t = t.header_is('ETag', '"abc321"', 'right header')
|
|
633
|
+
|
|
634
|
+
Check response header for exact match.
|
|
635
|
+
"""
|
|
636
|
+
desc = _desc(desc, f'{name}: {"" if value is None else value}')
|
|
637
|
+
return self.test('is', self._tx.res.headers.header(name), value, desc)
|
|
638
|
+
|
|
639
|
+
def header_isnt(self, name: str, value: Any, desc: str | None = None) -> Self:
|
|
640
|
+
"""::
|
|
641
|
+
|
|
642
|
+
t = t.header_isnt('Etag', '"abc321"')
|
|
643
|
+
t = t.header_isnt('ETag', '"abc321"', 'different header')
|
|
644
|
+
|
|
645
|
+
Opposite of :meth:`header_is`.
|
|
646
|
+
"""
|
|
647
|
+
desc = _desc(desc, f'not {name}: {"" if value is None else value}')
|
|
648
|
+
return self.test('isnt', self._tx.res.headers.header(name), value, desc)
|
|
649
|
+
|
|
650
|
+
def header_like(self, name: str, regex: Any, desc: str | None = None) -> Self:
|
|
651
|
+
"""::
|
|
652
|
+
|
|
653
|
+
t = t.header_like('ETag', re.compile(r'abc'))
|
|
654
|
+
t = t.header_like('ETag', re.compile(r'abc'), 'right header')
|
|
655
|
+
|
|
656
|
+
Check response header for similar match.
|
|
657
|
+
"""
|
|
658
|
+
return self.test('like', self._tx.res.headers.header(name), regex, _desc(desc, f'{name} is similar'))
|
|
659
|
+
|
|
660
|
+
def header_unlike(self, name: str, regex: Any, desc: str | None = None) -> Self:
|
|
661
|
+
"""::
|
|
662
|
+
|
|
663
|
+
t = t.header_unlike('ETag', re.compile(r'abc'))
|
|
664
|
+
t = t.header_unlike('ETag', re.compile(r'abc'), 'different header')
|
|
665
|
+
|
|
666
|
+
Opposite of :meth:`header_like`.
|
|
667
|
+
"""
|
|
668
|
+
return self.test('unlike', self._tx.res.headers.header(name), regex, _desc(desc, f'{name} is not similar'))
|
|
669
|
+
|
|
670
|
+
def json_has(self, p: str, desc: str | None = None) -> Self:
|
|
671
|
+
"""::
|
|
672
|
+
|
|
673
|
+
t = t.json_has('/foo')
|
|
674
|
+
t = t.json_has('/minibar', 'has a minibar')
|
|
675
|
+
|
|
676
|
+
Check if JSON response contains a value that can be identified using
|
|
677
|
+
the given JSON Pointer with :mod:`Pyjo.JSON.Pointer`.
|
|
678
|
+
"""
|
|
679
|
+
desc = _desc(desc, f'has value for JSON Pointer "{p}"')
|
|
680
|
+
return self.test('ok', bool(Pyjo.JSON.Pointer.new(self._tx.res.json()).contains(p)), desc)
|
|
681
|
+
|
|
682
|
+
def json_hasnt(self, p: str, desc: str | None = None) -> Self:
|
|
683
|
+
"""::
|
|
684
|
+
|
|
685
|
+
t = t.json_hasnt('/foo')
|
|
686
|
+
t = t.json_hasnt('/minibar', 'no minibar')
|
|
687
|
+
|
|
688
|
+
Opposite of :meth:`json_has`.
|
|
689
|
+
"""
|
|
690
|
+
desc = _desc(desc, f'has no value for JSON Pointer "{p}"')
|
|
691
|
+
return self.test('ok', not Pyjo.JSON.Pointer.new(self._tx.res.json()).contains(p), desc)
|
|
692
|
+
|
|
693
|
+
def json_is(self, *args: Any) -> Self:
|
|
694
|
+
"""::
|
|
695
|
+
|
|
696
|
+
t = t.json_is({'foo': [1, 2, 3]})
|
|
697
|
+
t = t.json_is('/foo', [1, 2, 3])
|
|
698
|
+
t = t.json_is('/foo/1', 2, 'right value')
|
|
699
|
+
|
|
700
|
+
Check the value extracted from JSON response using the given JSON
|
|
701
|
+
Pointer with :mod:`Pyjo.JSON.Pointer`, which defaults to the root value
|
|
702
|
+
if it is omitted. ::
|
|
703
|
+
|
|
704
|
+
# Use an empty JSON Pointer to test the whole JSON response with a test description
|
|
705
|
+
t.json_is('', {'foo': [1, 2, 3]}, 'right object')
|
|
706
|
+
"""
|
|
707
|
+
p, data, desc = _json_args(args)
|
|
708
|
+
return self.test('is_deeply', self._tx.res.json(p), data, _desc(desc, f'exact match for JSON Pointer "{p}"'))
|
|
709
|
+
|
|
710
|
+
def json_like(self, p: str, regex: Any, desc: str | None = None) -> Self:
|
|
711
|
+
"""::
|
|
712
|
+
|
|
713
|
+
t = t.json_like('/foo/1', re.compile(r'^\\d+$'))
|
|
714
|
+
t = t.json_like('/foo/1', re.compile(r'^\\d+$'), 'right value')
|
|
715
|
+
|
|
716
|
+
Check the value extracted from JSON response using the given JSON
|
|
717
|
+
Pointer with :mod:`Pyjo.JSON.Pointer` for similar match.
|
|
718
|
+
"""
|
|
719
|
+
desc = _desc(desc, f'similar match for JSON Pointer "{p}"')
|
|
720
|
+
return self.test('like', self._tx.res.json(p), regex, desc)
|
|
721
|
+
|
|
722
|
+
def json_message_has(self, p: str, desc: str | None = None) -> Self:
|
|
723
|
+
"""::
|
|
724
|
+
|
|
725
|
+
t = t.json_message_has('/foo')
|
|
726
|
+
t = t.json_message_has('/minibar', 'has a minibar')
|
|
727
|
+
|
|
728
|
+
Check if JSON WebSocket message contains a value that can be
|
|
729
|
+
identified using the given JSON Pointer with :mod:`Pyjo.JSON.Pointer`.
|
|
730
|
+
"""
|
|
731
|
+
return self.test('ok', self._json('contains', p), _desc(desc, f'has value for JSON Pointer "{p}"'))
|
|
732
|
+
|
|
733
|
+
def json_message_hasnt(self, p: str, desc: str | None = None) -> Self:
|
|
734
|
+
"""::
|
|
735
|
+
|
|
736
|
+
t = t.json_message_hasnt('/foo')
|
|
737
|
+
t = t.json_message_hasnt('/minibar', 'no minibar')
|
|
738
|
+
|
|
739
|
+
Opposite of :meth:`json_message_has`.
|
|
740
|
+
"""
|
|
741
|
+
return self.test('ok', not self._json('contains', p), _desc(desc, f'has no value for JSON Pointer "{p}"'))
|
|
742
|
+
|
|
743
|
+
def json_message_is(self, *args: Any) -> Self:
|
|
744
|
+
"""::
|
|
745
|
+
|
|
746
|
+
t = t.json_message_is({'foo': [1, 2, 3]})
|
|
747
|
+
t = t.json_message_is('/foo', [1, 2, 3])
|
|
748
|
+
t = t.json_message_is('/foo/1', 2, 'right value')
|
|
749
|
+
|
|
750
|
+
Check the value extracted from JSON WebSocket message using the given
|
|
751
|
+
JSON Pointer with :mod:`Pyjo.JSON.Pointer`, which defaults to the root
|
|
752
|
+
value if it is omitted.
|
|
753
|
+
"""
|
|
754
|
+
p, data, desc = _json_args(args)
|
|
755
|
+
return self.test('is_deeply', self._json('get', p), data, _desc(desc, f'exact match for JSON Pointer "{p}"'))
|
|
756
|
+
|
|
757
|
+
def json_message_like(self, p: str, regex: Any, desc: str | None = None) -> Self:
|
|
758
|
+
"""::
|
|
759
|
+
|
|
760
|
+
t = t.json_message_like('/foo/1', re.compile(r'^\\d+$'))
|
|
761
|
+
t = t.json_message_like('/foo/1', re.compile(r'^\\d+$'), 'right value')
|
|
762
|
+
|
|
763
|
+
Check the value extracted from JSON WebSocket message using the given
|
|
764
|
+
JSON Pointer with :mod:`Pyjo.JSON.Pointer` for similar match.
|
|
765
|
+
"""
|
|
766
|
+
desc = _desc(desc, f'similar match for JSON Pointer "{p}"')
|
|
767
|
+
return self.test('like', self._json('get', p), regex, desc)
|
|
768
|
+
|
|
769
|
+
def json_message_unlike(self, p: str, regex: Any, desc: str | None = None) -> Self:
|
|
770
|
+
"""::
|
|
771
|
+
|
|
772
|
+
t = t.json_message_unlike('/foo/1', re.compile(r'^\\d+$'))
|
|
773
|
+
t = t.json_message_unlike('/foo/1', re.compile(r'^\\d+$'), 'different value')
|
|
774
|
+
|
|
775
|
+
Opposite of :meth:`json_message_like`.
|
|
776
|
+
"""
|
|
777
|
+
desc = _desc(desc, f'no similar match for JSON Pointer "{p}"')
|
|
778
|
+
return self.test('unlike', self._json('get', p), regex, desc)
|
|
779
|
+
|
|
780
|
+
def json_unlike(self, p: str, regex: Any, desc: str | None = None) -> Self:
|
|
781
|
+
"""::
|
|
782
|
+
|
|
783
|
+
t = t.json_unlike('/foo/1', re.compile(r'^\\d+$'))
|
|
784
|
+
t = t.json_unlike('/foo/1', re.compile(r'^\\d+$'), 'different value')
|
|
785
|
+
|
|
786
|
+
Opposite of :meth:`json_like`.
|
|
787
|
+
"""
|
|
788
|
+
desc = _desc(desc, f'no similar match for JSON Pointer "{p}"')
|
|
789
|
+
return self.test('unlike', self._tx.res.json(p), regex, desc)
|
|
790
|
+
|
|
791
|
+
def message_is(self, value: Any, desc: str | None = None) -> Self:
|
|
792
|
+
"""::
|
|
793
|
+
|
|
794
|
+
t = t.message_is({'binary': b'bytes'})
|
|
795
|
+
t = t.message_is({'text': b'bytes'})
|
|
796
|
+
t = t.message_is('working!')
|
|
797
|
+
t = t.message_is('working!', 'right message')
|
|
798
|
+
|
|
799
|
+
Check WebSocket message for exact match.
|
|
800
|
+
"""
|
|
801
|
+
return self._message('is', value, _desc(desc, 'exact match for message'))
|
|
802
|
+
|
|
803
|
+
def message_isnt(self, value: Any, desc: str | None = None) -> Self:
|
|
804
|
+
"""::
|
|
805
|
+
|
|
806
|
+
t = t.message_isnt({'binary': b'bytes'})
|
|
807
|
+
t = t.message_isnt({'text': b'bytes'})
|
|
808
|
+
t = t.message_isnt('working!')
|
|
809
|
+
t = t.message_isnt('working!', 'different message')
|
|
810
|
+
|
|
811
|
+
Opposite of :meth:`message_is`.
|
|
812
|
+
"""
|
|
813
|
+
return self._message('isnt', value, _desc(desc, 'no match for message'))
|
|
814
|
+
|
|
815
|
+
def message_like(self, regex: Any, desc: str | None = None) -> Self:
|
|
816
|
+
"""::
|
|
817
|
+
|
|
818
|
+
t = t.message_like({'binary': re.compile(rb'bytes')})
|
|
819
|
+
t = t.message_like({'text': re.compile(rb'bytes')})
|
|
820
|
+
t = t.message_like(re.compile(r'working!'))
|
|
821
|
+
t = t.message_like(re.compile(r'working!'), 'right message')
|
|
822
|
+
|
|
823
|
+
Check WebSocket message for similar match.
|
|
824
|
+
"""
|
|
825
|
+
return self._message('like', regex, _desc(desc, 'message is similar'))
|
|
826
|
+
|
|
827
|
+
def message_ok(self, desc: str | None = None) -> Self:
|
|
828
|
+
"""::
|
|
829
|
+
|
|
830
|
+
t = t.message_ok()
|
|
831
|
+
t = t.message_ok('got a message')
|
|
832
|
+
|
|
833
|
+
Wait for next WebSocket message to arrive. ::
|
|
834
|
+
|
|
835
|
+
# Wait for message and perform multiple tests on it
|
|
836
|
+
(
|
|
837
|
+
t.websocket_ok('/time')
|
|
838
|
+
.message_ok()
|
|
839
|
+
.message_like(re.compile(r'\\d+'))
|
|
840
|
+
.message_unlike(re.compile(r'\\w+'))
|
|
841
|
+
.finish_ok()
|
|
842
|
+
)
|
|
843
|
+
"""
|
|
844
|
+
return self.test('ok', bool(self._wait()), _desc(desc, 'message received'))
|
|
845
|
+
|
|
846
|
+
def message_unlike(self, regex: Any, desc: str | None = None) -> Self:
|
|
847
|
+
"""::
|
|
848
|
+
|
|
849
|
+
t = t.message_unlike({'binary': re.compile(rb'bytes')})
|
|
850
|
+
t = t.message_unlike({'text': re.compile(rb'bytes')})
|
|
851
|
+
t = t.message_unlike(re.compile(r'working!'))
|
|
852
|
+
t = t.message_unlike(re.compile(r'working!'), 'different message')
|
|
853
|
+
|
|
854
|
+
Opposite of :meth:`message_like`.
|
|
855
|
+
"""
|
|
856
|
+
return self._message('unlike', regex, _desc(desc, 'message is not similar'))
|
|
857
|
+
|
|
858
|
+
def options_ok(self, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
859
|
+
"""::
|
|
860
|
+
|
|
861
|
+
t = t.options_ok('http://example.com/foo')
|
|
862
|
+
t = t.options_ok('/foo')
|
|
863
|
+
t = t.options_ok('/foo', {'Accept': '*/*'}, 'Content!')
|
|
864
|
+
t = t.options_ok('/foo', {'Accept': '*/*'}, form={'a': 'b'})
|
|
865
|
+
t = t.options_ok('/foo', {'Accept': '*/*'}, json={'a': 'b'})
|
|
866
|
+
|
|
867
|
+
Perform a ``OPTIONS`` request and check for transport errors, takes the
|
|
868
|
+
same arguments as :meth:`Pyjo.UserAgent.Pyjo_UserAgent.options`, except
|
|
869
|
+
for the callback.
|
|
870
|
+
"""
|
|
871
|
+
return self._build_ok('OPTIONS', url, *args, **kwargs)
|
|
872
|
+
|
|
873
|
+
def otherwise(self, cb: Callable[[Self], Any]) -> Self:
|
|
874
|
+
"""::
|
|
875
|
+
|
|
876
|
+
t = t.otherwise(lambda t: ...)
|
|
877
|
+
|
|
878
|
+
Execute callback if the value of :attr:`success` is false. ::
|
|
879
|
+
|
|
880
|
+
# Diagnostics
|
|
881
|
+
(
|
|
882
|
+
t.get_ok('/bad')
|
|
883
|
+
.otherwise(lambda t: diag('Must have been Glen!'))
|
|
884
|
+
.status_is(200)
|
|
885
|
+
.otherwise(lambda t: diag(t.tx.res.dom().at('title').text))
|
|
886
|
+
)
|
|
887
|
+
"""
|
|
888
|
+
if not self.success:
|
|
889
|
+
cb(self)
|
|
890
|
+
return self
|
|
891
|
+
|
|
892
|
+
# Renamed from Mojolicious: or is otherwise, as or is a keyword of Python, and or_ is its alias
|
|
893
|
+
or_ = otherwise
|
|
894
|
+
|
|
895
|
+
def patch_ok(self, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
896
|
+
"""::
|
|
897
|
+
|
|
898
|
+
t = t.patch_ok('http://example.com/foo')
|
|
899
|
+
t = t.patch_ok('/foo')
|
|
900
|
+
t = t.patch_ok('/foo', {'Accept': '*/*'}, 'Content!')
|
|
901
|
+
t = t.patch_ok('/foo', {'Accept': '*/*'}, form={'a': 'b'})
|
|
902
|
+
t = t.patch_ok('/foo', {'Accept': '*/*'}, json={'a': 'b'})
|
|
903
|
+
|
|
904
|
+
Perform a ``PATCH`` request and check for transport errors, takes the
|
|
905
|
+
same arguments as :meth:`Pyjo.UserAgent.Pyjo_UserAgent.patch`, except
|
|
906
|
+
for the callback.
|
|
907
|
+
"""
|
|
908
|
+
return self._build_ok('PATCH', url, *args, **kwargs)
|
|
909
|
+
|
|
910
|
+
def post_ok(self, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
911
|
+
"""::
|
|
912
|
+
|
|
913
|
+
t = t.post_ok('http://example.com/foo')
|
|
914
|
+
t = t.post_ok('/foo')
|
|
915
|
+
t = t.post_ok('/foo', {'Accept': '*/*'}, 'Content!')
|
|
916
|
+
t = t.post_ok('/foo', {'Accept': '*/*'}, form={'a': 'b'})
|
|
917
|
+
t = t.post_ok('/foo', {'Accept': '*/*'}, json={'a': 'b'})
|
|
918
|
+
|
|
919
|
+
Perform a ``POST`` request and check for transport errors, takes the
|
|
920
|
+
same arguments as :meth:`Pyjo.UserAgent.Pyjo_UserAgent.post`, except
|
|
921
|
+
for the callback. ::
|
|
922
|
+
|
|
923
|
+
# Test file upload
|
|
924
|
+
upload = {'foo': {'content': 'bar', 'filename': 'baz.txt'}}
|
|
925
|
+
t.post_ok('/upload', form=upload).status_is(200)
|
|
926
|
+
|
|
927
|
+
# Test JSON API
|
|
928
|
+
t.post_ok('/hello.json', json={'hello': 'world'}).status_is(200).json_is({'bye': 'world'})
|
|
929
|
+
"""
|
|
930
|
+
return self._build_ok('POST', url, *args, **kwargs)
|
|
931
|
+
|
|
932
|
+
def post_sse_ok(self, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
933
|
+
"""::
|
|
934
|
+
|
|
935
|
+
t = t.post_sse_ok('http://example.com/events')
|
|
936
|
+
t = t.post_sse_ok('/events')
|
|
937
|
+
t = t.post_sse_ok('/events', {'Accept': 'text/event-stream'}, 'Content!')
|
|
938
|
+
t = t.post_sse_ok('/events', {'Accept': 'text/event-stream'}, form={'a': 'b'})
|
|
939
|
+
t = t.post_sse_ok('/events', {'Accept': 'text/event-stream'}, json={'a': 'b'})
|
|
940
|
+
|
|
941
|
+
Perform a ``POST`` request to establish a Server-Sent Events (SSE)
|
|
942
|
+
connection, takes the same arguments as
|
|
943
|
+
:meth:`Pyjo.UserAgent.Pyjo_UserAgent.post`, except for the callback.
|
|
944
|
+
Note that this method is **EXPERIMENTAL** and may change without
|
|
945
|
+
warning!
|
|
946
|
+
"""
|
|
947
|
+
return self._build_sse_ok('POST', url, *args, **kwargs)
|
|
948
|
+
|
|
949
|
+
def put_ok(self, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
950
|
+
"""::
|
|
951
|
+
|
|
952
|
+
t = t.put_ok('http://example.com/foo')
|
|
953
|
+
t = t.put_ok('/foo')
|
|
954
|
+
t = t.put_ok('/foo', {'Accept': '*/*'}, 'Content!')
|
|
955
|
+
t = t.put_ok('/foo', {'Accept': '*/*'}, form={'a': 'b'})
|
|
956
|
+
t = t.put_ok('/foo', {'Accept': '*/*'}, json={'a': 'b'})
|
|
957
|
+
|
|
958
|
+
Perform a ``PUT`` request and check for transport errors, takes the
|
|
959
|
+
same arguments as :meth:`Pyjo.UserAgent.Pyjo_UserAgent.put`, except
|
|
960
|
+
for the callback.
|
|
961
|
+
"""
|
|
962
|
+
return self._build_ok('PUT', url, *args, **kwargs)
|
|
963
|
+
|
|
964
|
+
def query_ok(self, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
965
|
+
"""::
|
|
966
|
+
|
|
967
|
+
t = t.query_ok('http://example.com/foo')
|
|
968
|
+
t = t.query_ok('/foo')
|
|
969
|
+
t = t.query_ok('/foo', {'Accept': '*/*'}, 'Content!')
|
|
970
|
+
t = t.query_ok('/foo', {'Accept': '*/*'}, form={'a': 'b'})
|
|
971
|
+
t = t.query_ok('/foo', {'Accept': '*/*'}, json={'a': 'b'})
|
|
972
|
+
|
|
973
|
+
Perform a ``QUERY`` request and check for transport errors, takes the
|
|
974
|
+
same arguments as :meth:`Pyjo.UserAgent.Pyjo_UserAgent.query`, except
|
|
975
|
+
for the callback.
|
|
976
|
+
"""
|
|
977
|
+
return self._build_ok('QUERY', url, *args, **kwargs)
|
|
978
|
+
|
|
979
|
+
def request_ok(self, tx: 'Pyjo.Transaction.HTTP.object') -> Self:
|
|
980
|
+
"""::
|
|
981
|
+
|
|
982
|
+
t = t.request_ok(Pyjo.Transaction.HTTP.new())
|
|
983
|
+
|
|
984
|
+
Perform request and check for transport errors. ::
|
|
985
|
+
|
|
986
|
+
# Request with custom method
|
|
987
|
+
tx = t.ua.build_tx('FOO', '/test.json', json={'foo': 1})
|
|
988
|
+
t.request_ok(tx).status_is(200).json_is({'success': 1})
|
|
989
|
+
|
|
990
|
+
# Request with custom cookie
|
|
991
|
+
tx = t.ua.build_tx('GET', '/account')
|
|
992
|
+
tx.req.cookies({'name': 'user', 'value': 'sri'})
|
|
993
|
+
t.request_ok(tx).status_is(200).text_is('head > title', 'Hello sri')
|
|
994
|
+
|
|
995
|
+
# Custom WebSocket handshake
|
|
996
|
+
tx = t.ua.build_websocket_tx('/foo')
|
|
997
|
+
tx.req.headers.remove('User-Agent')
|
|
998
|
+
t.request_ok(tx).message_ok().message_is('bar').finish_ok()
|
|
999
|
+
"""
|
|
1000
|
+
return self._request_ok(tx, tx.req.url.to_str())
|
|
1001
|
+
|
|
1002
|
+
def reset_session(self) -> Self:
|
|
1003
|
+
"""::
|
|
1004
|
+
|
|
1005
|
+
t = t.reset_session()
|
|
1006
|
+
|
|
1007
|
+
Reset user agent session.
|
|
1008
|
+
"""
|
|
1009
|
+
self.ua.cookie_jar.empty()
|
|
1010
|
+
self.tx = None
|
|
1011
|
+
return self
|
|
1012
|
+
|
|
1013
|
+
def send_ok(self, msg: Any, desc: str | None = None) -> Self:
|
|
1014
|
+
"""::
|
|
1015
|
+
|
|
1016
|
+
t = t.send_ok({'binary': b'bytes'})
|
|
1017
|
+
t = t.send_ok({'text': b'bytes'})
|
|
1018
|
+
t = t.send_ok({'json': {'test': [1, 2, 3]}})
|
|
1019
|
+
t = t.send_ok([fin, rsv1, rsv2, rsv3, op, payload])
|
|
1020
|
+
t = t.send_ok('chars')
|
|
1021
|
+
t = t.send_ok('chars', 'sent successfully')
|
|
1022
|
+
|
|
1023
|
+
Send message or frame via WebSocket. ::
|
|
1024
|
+
|
|
1025
|
+
# Send JSON object as "Text" message
|
|
1026
|
+
(
|
|
1027
|
+
t.websocket_ok('/echo.json')
|
|
1028
|
+
.send_ok({'json': {'test': 'I ♥ Pyjoyment!'}})
|
|
1029
|
+
.message_ok()
|
|
1030
|
+
.json_message_is('/test', 'I ♥ Pyjoyment!')
|
|
1031
|
+
.finish_ok()
|
|
1032
|
+
)
|
|
1033
|
+
"""
|
|
1034
|
+
desc = _desc(desc, 'send message')
|
|
1035
|
+
if not self._tx.is_websocket:
|
|
1036
|
+
return self.test('ok', 0, desc)
|
|
1037
|
+
|
|
1038
|
+
self._tx.send(msg, _stop)
|
|
1039
|
+
Pyjo.IOLoop.start()
|
|
1040
|
+
return self.test('ok', 1, desc)
|
|
1041
|
+
|
|
1042
|
+
def sse_finish_ok(self) -> Self:
|
|
1043
|
+
"""::
|
|
1044
|
+
|
|
1045
|
+
t = t.sse_finish_ok()
|
|
1046
|
+
|
|
1047
|
+
Close Server-Sent Events connection. Note that this method is
|
|
1048
|
+
**EXPERIMENTAL** and may change without warning!
|
|
1049
|
+
"""
|
|
1050
|
+
# Unlike Mojolicious: the error is interrupted by Pyjo.Test.Pyjo, where Mojo names Test::Mojo
|
|
1051
|
+
self._tx.res.error = {'message': 'Interrupted by Pyjo.Test.Pyjo'}
|
|
1052
|
+
|
|
1053
|
+
# Unlike Mojolicious: the connection is closed too, as the user agent finds the error only when it reads, so that
|
|
1054
|
+
# a stream without events, which Mojo waits for until its next event or its inactivity timeout, ends at once
|
|
1055
|
+
if (id := self._tx.connection) is not None and (stream := Pyjo.IOLoop.singleton.stream(id)) is not None:
|
|
1056
|
+
stream.close_gracefully()
|
|
1057
|
+
while not self._sse_finished:
|
|
1058
|
+
Pyjo.IOLoop.one_tick()
|
|
1059
|
+
return self.test('ok', 1, 'closed SSE connection')
|
|
1060
|
+
|
|
1061
|
+
def sse_finished_ok(self) -> Self:
|
|
1062
|
+
"""::
|
|
1063
|
+
|
|
1064
|
+
t = t.sse_finished_ok()
|
|
1065
|
+
|
|
1066
|
+
Wait for Server-Sent Events connection to be closed. Note that this
|
|
1067
|
+
method is **EXPERIMENTAL** and may change without warning!
|
|
1068
|
+
"""
|
|
1069
|
+
while not self._sse_finished:
|
|
1070
|
+
Pyjo.IOLoop.one_tick()
|
|
1071
|
+
return self.test('ok', 1, 'SSE connection has been closed')
|
|
1072
|
+
|
|
1073
|
+
def sse_ok(self, desc: str | None = None) -> Self:
|
|
1074
|
+
"""::
|
|
1075
|
+
|
|
1076
|
+
t = t.sse_ok()
|
|
1077
|
+
t = t.sse_ok('got an event')
|
|
1078
|
+
|
|
1079
|
+
Wait for next Server-Sent Event to arrive. Note that this method is
|
|
1080
|
+
**EXPERIMENTAL** and may change without warning!
|
|
1081
|
+
"""
|
|
1082
|
+
return self.test('ok', bool(self._sse_wait()), _desc(desc, 'event received'))
|
|
1083
|
+
|
|
1084
|
+
def sse_id_is(self, id: Any, desc: str | None = None) -> Self:
|
|
1085
|
+
"""::
|
|
1086
|
+
|
|
1087
|
+
t = t.sse_id_is(123)
|
|
1088
|
+
t = t.sse_id_is(123, 'right id')
|
|
1089
|
+
|
|
1090
|
+
Check Server-Sent Event id for exact match. Note that this method is
|
|
1091
|
+
**EXPERIMENTAL** and may change without warning!
|
|
1092
|
+
"""
|
|
1093
|
+
return self.test('is', self._event('id'), id, _desc(desc, 'exact match for SSE event id'))
|
|
1094
|
+
|
|
1095
|
+
def sse_id_isnt(self, id: Any, desc: str | None = None) -> Self:
|
|
1096
|
+
"""::
|
|
1097
|
+
|
|
1098
|
+
t = t.sse_id_isnt(123)
|
|
1099
|
+
t = t.sse_id_isnt(123, 'different id')
|
|
1100
|
+
|
|
1101
|
+
Opposite of :meth:`sse_id_is`. Note that this method is
|
|
1102
|
+
**EXPERIMENTAL** and may change without warning!
|
|
1103
|
+
"""
|
|
1104
|
+
return self.test('isnt', self._event('id'), id, _desc(desc, 'no match for SSE event id'))
|
|
1105
|
+
|
|
1106
|
+
def sse_text_is(self, text: Any, desc: str | None = None) -> Self:
|
|
1107
|
+
"""::
|
|
1108
|
+
|
|
1109
|
+
t = t.sse_text_is('working!')
|
|
1110
|
+
t = t.sse_text_is('working!', 'right text')
|
|
1111
|
+
|
|
1112
|
+
Check Server-Sent Event text for exact match. Note that this method is
|
|
1113
|
+
**EXPERIMENTAL** and may change without warning!
|
|
1114
|
+
"""
|
|
1115
|
+
return self.test('is', self._event('text'), text, _desc(desc, 'exact match for SSE event text'))
|
|
1116
|
+
|
|
1117
|
+
def sse_text_isnt(self, text: Any, desc: str | None = None) -> Self:
|
|
1118
|
+
"""::
|
|
1119
|
+
|
|
1120
|
+
t = t.sse_text_isnt('working!')
|
|
1121
|
+
t = t.sse_text_isnt('working!', 'different text')
|
|
1122
|
+
|
|
1123
|
+
Opposite of :meth:`sse_text_is`. Note that this method is
|
|
1124
|
+
**EXPERIMENTAL** and may change without warning!
|
|
1125
|
+
"""
|
|
1126
|
+
return self.test('isnt', self._event('text'), text, _desc(desc, 'no match for SSE event text'))
|
|
1127
|
+
|
|
1128
|
+
def sse_text_like(self, regex: Any, desc: str | None = None) -> Self:
|
|
1129
|
+
"""::
|
|
1130
|
+
|
|
1131
|
+
t = t.sse_text_like(re.compile(r'working'))
|
|
1132
|
+
t = t.sse_text_like(re.compile(r'working'), 'right text')
|
|
1133
|
+
|
|
1134
|
+
Check Server-Sent Event text for exact match. Note that this method is
|
|
1135
|
+
**EXPERIMENTAL** and may change without warning!
|
|
1136
|
+
"""
|
|
1137
|
+
return self.test('like', self._event('text'), regex, _desc(desc, 'similar match for SSE event text'))
|
|
1138
|
+
|
|
1139
|
+
def sse_text_unlike(self, regex: Any, desc: str | None = None) -> Self:
|
|
1140
|
+
"""::
|
|
1141
|
+
|
|
1142
|
+
t = t.sse_text_unlike(re.compile(r'working'))
|
|
1143
|
+
t = t.sse_text_unlike(re.compile(r'working'), 'different text')
|
|
1144
|
+
|
|
1145
|
+
Opposite of :meth:`sse_text_like`. Note that this method is
|
|
1146
|
+
**EXPERIMENTAL** and may change without warning!
|
|
1147
|
+
"""
|
|
1148
|
+
return self.test('unlike', self._event('text'), regex, _desc(desc, 'no similar match for SSE event text'))
|
|
1149
|
+
|
|
1150
|
+
def sse_type_is(self, type: Any, desc: str | None = None) -> Self:
|
|
1151
|
+
"""::
|
|
1152
|
+
|
|
1153
|
+
t = t.sse_type_is('message')
|
|
1154
|
+
t = t.sse_type_is('message', 'right type')
|
|
1155
|
+
|
|
1156
|
+
Check Server-Sent Event type for exact match. Note that this method is
|
|
1157
|
+
**EXPERIMENTAL** and may change without warning!
|
|
1158
|
+
"""
|
|
1159
|
+
return self.test('is', self._event('type'), type, _desc(desc, 'exact match for SSE event type'))
|
|
1160
|
+
|
|
1161
|
+
def sse_type_isnt(self, type: Any, desc: str | None = None) -> Self:
|
|
1162
|
+
"""::
|
|
1163
|
+
|
|
1164
|
+
t = t.sse_type_isnt('message')
|
|
1165
|
+
t = t.sse_type_isnt('message', 'different type')
|
|
1166
|
+
|
|
1167
|
+
Opposite of :meth:`sse_type_is`. Note that this method is
|
|
1168
|
+
**EXPERIMENTAL** and may change without warning!
|
|
1169
|
+
"""
|
|
1170
|
+
return self.test('isnt', self._event('type'), type, _desc(desc, 'no match for SSE event type'))
|
|
1171
|
+
|
|
1172
|
+
def status_is(self, status: int, desc: str | None = None) -> Self:
|
|
1173
|
+
"""::
|
|
1174
|
+
|
|
1175
|
+
t = t.status_is(200)
|
|
1176
|
+
t = t.status_is(200, 'right status')
|
|
1177
|
+
|
|
1178
|
+
Check response status for exact match.
|
|
1179
|
+
"""
|
|
1180
|
+
res = self._tx.res
|
|
1181
|
+
desc = _desc(desc, f'{status} {res.default_message(status)}')
|
|
1182
|
+
return self.test('is', res.code, status, desc)
|
|
1183
|
+
|
|
1184
|
+
def status_isnt(self, status: int, desc: str | None = None) -> Self:
|
|
1185
|
+
"""::
|
|
1186
|
+
|
|
1187
|
+
t = t.status_isnt(200)
|
|
1188
|
+
t = t.status_isnt(200, 'different status')
|
|
1189
|
+
|
|
1190
|
+
Opposite of :meth:`status_is`.
|
|
1191
|
+
"""
|
|
1192
|
+
res = self._tx.res
|
|
1193
|
+
desc = _desc(desc, f'not {status} {res.default_message(status)}')
|
|
1194
|
+
return self.test('isnt', res.code, status, desc)
|
|
1195
|
+
|
|
1196
|
+
def test(self, name: str, *args: Any) -> Self:
|
|
1197
|
+
"""::
|
|
1198
|
+
|
|
1199
|
+
t = t.test('is', 'first value', 'second value', 'right value')
|
|
1200
|
+
|
|
1201
|
+
Call :mod:`Pyjo.Test` functions through :attr:`handler`, by the names
|
|
1202
|
+
of the functions of ``Test::More`` (``ok``, ``is``, ``isnt``,
|
|
1203
|
+
``like``, ``unlike`` and ``is_deeply``), used to implement
|
|
1204
|
+
:mod:`Pyjo.Test.Pyjo` extensions. The result will be stored in
|
|
1205
|
+
:attr:`success`.
|
|
1206
|
+
"""
|
|
1207
|
+
self.success = bool(self.handler(name, *args))
|
|
1208
|
+
return self
|
|
1209
|
+
|
|
1210
|
+
def text_is(self, selector: str, value: Any, desc: str | None = None) -> Self:
|
|
1211
|
+
"""::
|
|
1212
|
+
|
|
1213
|
+
t = t.text_is('div.foo[x=y]', 'Hello!')
|
|
1214
|
+
t = t.text_is('html head title', 'Hello!', 'right title')
|
|
1215
|
+
|
|
1216
|
+
Checks text content of the CSS selectors first matching HTML/XML
|
|
1217
|
+
element for exact match with :meth:`Pyjo.DOM.Pyjo_DOM.at`.
|
|
1218
|
+
"""
|
|
1219
|
+
return self.test('is', self._text(selector), value, _desc(desc, f'exact match for selector "{selector}"'))
|
|
1220
|
+
|
|
1221
|
+
def text_isnt(self, selector: str, value: Any, desc: str | None = None) -> Self:
|
|
1222
|
+
"""::
|
|
1223
|
+
|
|
1224
|
+
t = t.text_isnt('div.foo[x=y]', 'Hello!')
|
|
1225
|
+
t = t.text_isnt('html head title', 'Hello!', 'different title')
|
|
1226
|
+
|
|
1227
|
+
Opposite of :meth:`text_is`.
|
|
1228
|
+
"""
|
|
1229
|
+
return self.test('isnt', self._text(selector), value, _desc(desc, f'no match for selector "{selector}"'))
|
|
1230
|
+
|
|
1231
|
+
def text_like(self, selector: str, regex: Any, desc: str | None = None) -> Self:
|
|
1232
|
+
"""::
|
|
1233
|
+
|
|
1234
|
+
t = t.text_like('div.foo[x=y]', re.compile(r'Hello'))
|
|
1235
|
+
t = t.text_like('html head title', re.compile(r'Hello'), 'right title')
|
|
1236
|
+
|
|
1237
|
+
Checks text content of the CSS selectors first matching HTML/XML
|
|
1238
|
+
element for similar match with :meth:`Pyjo.DOM.Pyjo_DOM.at`.
|
|
1239
|
+
"""
|
|
1240
|
+
desc = _desc(desc, f'similar match for selector "{selector}"')
|
|
1241
|
+
return self.test('like', self._text(selector), regex, desc)
|
|
1242
|
+
|
|
1243
|
+
def text_unlike(self, selector: str, regex: Any, desc: str | None = None) -> Self:
|
|
1244
|
+
"""::
|
|
1245
|
+
|
|
1246
|
+
t = t.text_unlike('div.foo[x=y]', re.compile(r'Hello'))
|
|
1247
|
+
t = t.text_unlike('html head title', re.compile(r'Hello'), 'different title')
|
|
1248
|
+
|
|
1249
|
+
Opposite of :meth:`text_like`.
|
|
1250
|
+
"""
|
|
1251
|
+
desc = _desc(desc, f'no similar match for selector "{selector}"')
|
|
1252
|
+
return self.test('unlike', self._text(selector), regex, desc)
|
|
1253
|
+
|
|
1254
|
+
def websocket_ok(self, url: Any, *args: Any) -> Self:
|
|
1255
|
+
"""::
|
|
1256
|
+
|
|
1257
|
+
t = t.websocket_ok('http://example.com/echo')
|
|
1258
|
+
t = t.websocket_ok('/echo')
|
|
1259
|
+
t = t.websocket_ok('/echo', {'DNT': 1}, ['v1.proto'])
|
|
1260
|
+
|
|
1261
|
+
Open a WebSocket connection with transparent handshake, takes the same
|
|
1262
|
+
arguments as :meth:`Pyjo.UserAgent.Pyjo_UserAgent.websocket`, except
|
|
1263
|
+
for the callback. ::
|
|
1264
|
+
|
|
1265
|
+
# WebSocket with permessage-deflate compression
|
|
1266
|
+
(
|
|
1267
|
+
t.websocket_ok('/', {'Sec-WebSocket-Extensions': 'permessage-deflate'})
|
|
1268
|
+
.send_ok('y' * 50000)
|
|
1269
|
+
.message_ok()
|
|
1270
|
+
.message_is('z' * 50000)
|
|
1271
|
+
.finish_ok()
|
|
1272
|
+
)
|
|
1273
|
+
"""
|
|
1274
|
+
return self._request_ok(self.ua.build_websocket_tx(url, *args), url)
|
|
1275
|
+
|
|
1276
|
+
@property
|
|
1277
|
+
def _tx(self) -> Any:
|
|
1278
|
+
if self.tx is None:
|
|
1279
|
+
raise AttributeError('No transaction to test, make a request first')
|
|
1280
|
+
return self.tx
|
|
1281
|
+
|
|
1282
|
+
def _attr(self, selector: str, attr: str) -> str | None:
|
|
1283
|
+
if (e := self._dom().at(selector)) is None:
|
|
1284
|
+
return None
|
|
1285
|
+
value = e.attr(attr)
|
|
1286
|
+
return value if value is not None else ''
|
|
1287
|
+
|
|
1288
|
+
def _build_ok(self, method: str, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
1289
|
+
return self._request_ok(self.ua.build_tx(method, url, *args, **kwargs), url)
|
|
1290
|
+
|
|
1291
|
+
def _build_sse_ok(self, method: str, url: Any, *args: Any, **kwargs: Any) -> Self:
|
|
1292
|
+
return self._sse_ok(self.ua.build_tx(method, url, *args, **kwargs), url)
|
|
1293
|
+
|
|
1294
|
+
def _dom(self) -> Any:
|
|
1295
|
+
return self._tx.res.dom()
|
|
1296
|
+
|
|
1297
|
+
def _event(self, name: str) -> Any:
|
|
1298
|
+
return (self.sse or {}).get(name)
|
|
1299
|
+
|
|
1300
|
+
def _json(self, method: str, p: str) -> Any:
|
|
1301
|
+
message = self.message or []
|
|
1302
|
+
payload = message[1] if len(message) > 1 else None
|
|
1303
|
+
return getattr(Pyjo.JSON.Pointer.new(j(payload) if payload is not None else None), method)(p)
|
|
1304
|
+
|
|
1305
|
+
def _message(self, name: str, value: Any, desc: str) -> Self:
|
|
1306
|
+
type, msg = (self.message or [None, None])[:2]
|
|
1307
|
+
|
|
1308
|
+
# Type check
|
|
1309
|
+
if isinstance(value, dict):
|
|
1310
|
+
expect = 'text' if 'text' in value else 'binary'
|
|
1311
|
+
value = cast(dict[str, Any], value)[expect]
|
|
1312
|
+
if (type or '') != expect:
|
|
1313
|
+
msg = ''
|
|
1314
|
+
|
|
1315
|
+
# Decode text frame if there is no type check
|
|
1316
|
+
elif (type or '') == 'text':
|
|
1317
|
+
msg = decode('UTF-8', msg)
|
|
1318
|
+
|
|
1319
|
+
return self.test(name, msg if msg is not None else '', value, desc)
|
|
1320
|
+
|
|
1321
|
+
def _request_ok(self, tx: Any, url: Any) -> Self:
|
|
1322
|
+
# Establish WebSocket connection
|
|
1323
|
+
if tx.req.is_handshake:
|
|
1324
|
+
self._finished, self._messages = None, []
|
|
1325
|
+
|
|
1326
|
+
def handshake(ua: Any, tx: Any) -> None:
|
|
1327
|
+
self.tx = tx
|
|
1328
|
+
if not tx.is_websocket:
|
|
1329
|
+
self._finished = []
|
|
1330
|
+
|
|
1331
|
+
def finish(tx: Any, *args: Any) -> None:
|
|
1332
|
+
self._finished = list(args)
|
|
1333
|
+
|
|
1334
|
+
def binary(tx: Any, msg: bytes) -> None:
|
|
1335
|
+
self._messages.append(['binary', msg])
|
|
1336
|
+
|
|
1337
|
+
def text(tx: Any, msg: bytes) -> None:
|
|
1338
|
+
self._messages.append(['text', msg])
|
|
1339
|
+
|
|
1340
|
+
tx.on('finish', finish)
|
|
1341
|
+
tx.on('binary', binary)
|
|
1342
|
+
tx.on('text', text)
|
|
1343
|
+
Pyjo.IOLoop.stop()
|
|
1344
|
+
|
|
1345
|
+
self.ua.start(tx, handshake)
|
|
1346
|
+
Pyjo.IOLoop.start()
|
|
1347
|
+
|
|
1348
|
+
return self.test('ok', self._tx.is_websocket, _desc(None, f'WebSocket handshake with {url}'))
|
|
1349
|
+
|
|
1350
|
+
# Perform request
|
|
1351
|
+
self.tx = self.ua.start(tx)
|
|
1352
|
+
err = self._tx.error
|
|
1353
|
+
if not (ok := not err or not err.get('message') or err.get('code')) and err:
|
|
1354
|
+
Pyjo.Test.diag(err['message'])
|
|
1355
|
+
return self.test('ok', bool(ok), _desc(None, f'{tx.req.method.upper()} {url}'))
|
|
1356
|
+
|
|
1357
|
+
def _sse_ok(self, tx: Any, url: Any) -> Self:
|
|
1358
|
+
ok = None
|
|
1359
|
+
self._sse_finished, self._sse_events = None, []
|
|
1360
|
+
|
|
1361
|
+
def sse(content: Any, event: dict[str, Any] | None) -> None:
|
|
1362
|
+
nonlocal ok
|
|
1363
|
+
if event:
|
|
1364
|
+
self._sse_events.append(event)
|
|
1365
|
+
else:
|
|
1366
|
+
ok = 1
|
|
1367
|
+
Pyjo.IOLoop.stop()
|
|
1368
|
+
|
|
1369
|
+
tx.res.content.on('sse', sse)
|
|
1370
|
+
|
|
1371
|
+
def start(ua: Any, tx: Any) -> None:
|
|
1372
|
+
self.tx = tx
|
|
1373
|
+
|
|
1374
|
+
cb = self.ua.on('start', start)
|
|
1375
|
+
|
|
1376
|
+
def finish(ua: Any, tx: Any) -> None:
|
|
1377
|
+
self._sse_finished = True
|
|
1378
|
+
self.ua.unsubscribe('start', cb)
|
|
1379
|
+
Pyjo.IOLoop.stop()
|
|
1380
|
+
|
|
1381
|
+
self.ua.start(tx, finish)
|
|
1382
|
+
Pyjo.IOLoop.start()
|
|
1383
|
+
|
|
1384
|
+
return self.test('ok', ok, _desc(None, f'SSE connection established: {tx.req.method.upper()} {url}'))
|
|
1385
|
+
|
|
1386
|
+
def _sse_wait(self) -> dict[str, Any] | None:
|
|
1387
|
+
while not self._sse_finished and not len(self._sse_events):
|
|
1388
|
+
Pyjo.IOLoop.one_tick()
|
|
1389
|
+
self.sse = self._sse_events.pop(0) if len(self._sse_events) else None
|
|
1390
|
+
return self.sse
|
|
1391
|
+
|
|
1392
|
+
def _text(self, selector: str) -> str | None:
|
|
1393
|
+
if (e := self._dom().at(selector)) is None:
|
|
1394
|
+
return None
|
|
1395
|
+
return e.text
|
|
1396
|
+
|
|
1397
|
+
def _wait(self) -> list[Any] | None:
|
|
1398
|
+
while self._finished is None and not len(self._messages):
|
|
1399
|
+
Pyjo.IOLoop.one_tick()
|
|
1400
|
+
self.message = self._messages.pop(0) if len(self._messages) else None
|
|
1401
|
+
return self.message
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
# Unlike Mojolicious: the strings are characters, which Mojo encodes to UTF-8 for Test::More
|
|
1405
|
+
def _desc(desc: str | None, default: str) -> str:
|
|
1406
|
+
return desc or default
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
# A regular expression of Pyjo.Test
|
|
1410
|
+
type _Pattern = str | bytes | re.Pattern[str] | re.Pattern[bytes] | tuple[str | bytes, str]
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
# like and unlike of Test::More, which match the string of a value, as a number, and match bytes, the characters
|
|
1414
|
+
# 0-255 of Perl, with a pattern of characters
|
|
1415
|
+
def _like(function: Callable[..., Pyjo.Test.Result]) -> Callable[..., Pyjo.Test.Result]:
|
|
1416
|
+
def like(got: Any, regex: _Pattern, desc: str | None = None) -> Pyjo.Test.Result:
|
|
1417
|
+
pattern = regex.pattern if isinstance(regex, re.Pattern) else regex[0] if isinstance(regex, tuple) else regex
|
|
1418
|
+
if isinstance(got, bytes) and not isinstance(pattern, bytes):
|
|
1419
|
+
got = got.decode('latin-1')
|
|
1420
|
+
elif got is not None and not isinstance(got, str | bytes):
|
|
1421
|
+
got = str(got)
|
|
1422
|
+
return function(got, regex, desc)
|
|
1423
|
+
|
|
1424
|
+
return like
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
# The functions of Pyjo.Test by the names of the functions of Test::More
|
|
1428
|
+
_FUNCTIONS: dict[str, Callable[..., Any]] = {
|
|
1429
|
+
'is': Pyjo.Test.is_ok,
|
|
1430
|
+
'is_deeply': Pyjo.Test.is_deeply_ok,
|
|
1431
|
+
'isnt': Pyjo.Test.isnt_ok,
|
|
1432
|
+
'like': _like(Pyjo.Test.like_ok),
|
|
1433
|
+
'ok': Pyjo.Test.ok,
|
|
1434
|
+
'unlike': _like(Pyjo.Test.unlike_ok),
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
def _handler(name: str, *args: Any) -> Any:
|
|
1439
|
+
return _FUNCTIONS[name](*args)
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def _json_args(args: tuple[Any, ...]) -> tuple[str, Any, str | None]:
|
|
1443
|
+
values = list(args)
|
|
1444
|
+
if len(values) > 1:
|
|
1445
|
+
return values[0], values[1], values[2] if len(values) > 2 else None
|
|
1446
|
+
return '', values[0] if len(values) else None, None
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def _stop(tx: Any) -> None:
|
|
1450
|
+
Pyjo.IOLoop.stop()
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
new = Pyjo_Test_Pyjo.new
|
|
1454
|
+
object = Pyjo_Test_Pyjo
|