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/__init__.py
ADDED
|
@@ -0,0 +1,1233 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Pyjo.Test - Writing test scripts
|
|
3
|
+
================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Test import *
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
if not os.getenv('TEST_ONLINE'):
|
|
11
|
+
plan(skip_all='set TEST_ONLINE to enable this test')
|
|
12
|
+
|
|
13
|
+
ok(got == expected, 'test name')
|
|
14
|
+
|
|
15
|
+
is_ok(got, expected, 'test name')
|
|
16
|
+
isnt_ok(got, unexpected, 'test name')
|
|
17
|
+
like_ok(got, r'expected', 'test name')
|
|
18
|
+
unlike_ok(got, r'unexpected', 'test name')
|
|
19
|
+
cmp_ok(got, '==', expected, 'test name')
|
|
20
|
+
is_deeply_ok(got_complex_structure, expected_complex_structure, 'test name')
|
|
21
|
+
isa_ok(obj, cls)
|
|
22
|
+
throws_ok(lambda: int('x'), ValueError, 'test name')
|
|
23
|
+
that(got).is_(expected, 'test name').unlike(r'unexpected', 'test name')
|
|
24
|
+
|
|
25
|
+
with subtest('Parse'):
|
|
26
|
+
is_ok(url.host, 'example.com', 'right host')
|
|
27
|
+
|
|
28
|
+
@subtest('Format')
|
|
29
|
+
def _():
|
|
30
|
+
is_ok(str(url), 'http://example.com', 'right format')
|
|
31
|
+
|
|
32
|
+
diag("here's what went wrong")
|
|
33
|
+
note('what happens next')
|
|
34
|
+
|
|
35
|
+
done_testing()
|
|
36
|
+
|
|
37
|
+
:mod:`Pyjo.Test` is a module for writing test scripts which print their results
|
|
38
|
+
in the `Test Anything Protocol <https://testanything.org/>`_ (TAP). It is
|
|
39
|
+
inspired by Perl's ``Test::More``, which the tests of Mojolicious are written
|
|
40
|
+
with, but it is not a port of it.
|
|
41
|
+
|
|
42
|
+
Each test script is a separate Python program, usually placed in the ``t``
|
|
43
|
+
directory.
|
|
44
|
+
|
|
45
|
+
Running tests
|
|
46
|
+
-------------
|
|
47
|
+
|
|
48
|
+
The test scripts run with :mod:`Pyjo.Prove`: ::
|
|
49
|
+
|
|
50
|
+
$ python -m Pyjo.Prove -I . -r t
|
|
51
|
+
|
|
52
|
+
or with Perl's ``prove``, as they print standard TAP: ::
|
|
53
|
+
|
|
54
|
+
$ PYTHONPATH=. prove --ext=py --exec=python -r t
|
|
55
|
+
|
|
56
|
+
pytest runs them too, each script as a test of pytest, through
|
|
57
|
+
:mod:`Pyjo.Test.pytest_plugin`.
|
|
58
|
+
|
|
59
|
+
A single script runs on its own as well: ::
|
|
60
|
+
|
|
61
|
+
$ PYTHONPATH=. python t/Pyjo/URL.py
|
|
62
|
+
|
|
63
|
+
Plans
|
|
64
|
+
-----
|
|
65
|
+
|
|
66
|
+
A test script declares how many tests it runs, either up front with
|
|
67
|
+
:func:`plan` or at the end with :func:`done_testing`. A script that runs tests
|
|
68
|
+
without a plan fails.
|
|
69
|
+
|
|
70
|
+
:func:`plan` is a context manager too, which issues the plan at the end of its
|
|
71
|
+
block, as :func:`done_testing` does, unless the block raises an exception: ::
|
|
72
|
+
|
|
73
|
+
with plan():
|
|
74
|
+
ok(True, 'first')
|
|
75
|
+
ok(True, 'second')
|
|
76
|
+
|
|
77
|
+
Subtests
|
|
78
|
+
--------
|
|
79
|
+
|
|
80
|
+
A subtest groups tests under a name. Its tests are numbered on their own and
|
|
81
|
+
printed indented, and the subtest as a whole counts as one test of its parent,
|
|
82
|
+
which passes when all of its tests pass. :func:`subtest` works as a context
|
|
83
|
+
manager, as a decorator which runs the function at once, and as a function
|
|
84
|
+
which runs a callback: ::
|
|
85
|
+
|
|
86
|
+
with subtest('Simple'):
|
|
87
|
+
ok(True, 'first')
|
|
88
|
+
|
|
89
|
+
@subtest('Decorated')
|
|
90
|
+
def _():
|
|
91
|
+
ok(True, 'second')
|
|
92
|
+
|
|
93
|
+
subtest('Callback', lambda: ok(True, 'third'))
|
|
94
|
+
|
|
95
|
+
A subtest has an implicit :func:`done_testing`. ``plan(skip_all=...)`` skips
|
|
96
|
+
the rest of the subtest, and a subtest that runs no tests fails.
|
|
97
|
+
|
|
98
|
+
Chains
|
|
99
|
+
------
|
|
100
|
+
|
|
101
|
+
The functions which test a value return a :class:`Result`, which is true
|
|
102
|
+
when the test passed, so that ``if not is_ok(...):`` works, and whose
|
|
103
|
+
methods test the same value again, with the names of Perl's ``Test::More``,
|
|
104
|
+
so that the tests of a value chain as the assertions of
|
|
105
|
+
:mod:`Pyjo.Test.Pyjo` do: ::
|
|
106
|
+
|
|
107
|
+
is_ok(url.host, 'example.com', 'right host').like(r'\.com$', 'top-level domain')
|
|
108
|
+
|
|
109
|
+
:func:`that` makes one of a value without a test, so that a chain starts
|
|
110
|
+
with the first of its methods: ::
|
|
111
|
+
|
|
112
|
+
(
|
|
113
|
+
that(answer)
|
|
114
|
+
.isa(int)
|
|
115
|
+
.cmp('>', 0, 'positive')
|
|
116
|
+
.is_(42, 'meaning of life')
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
Each method is a test of its own, which runs also after a test of the chain
|
|
120
|
+
failed, and a failure shows the line of the method.
|
|
121
|
+
|
|
122
|
+
Extensions
|
|
123
|
+
----------
|
|
124
|
+
|
|
125
|
+
A failed test shows the line of the first frame outside of :mod:`Pyjo.Test`
|
|
126
|
+
and of the modules that leave their frames out of the traceback of pytest
|
|
127
|
+
with ``__tracebackhide__``, so that the assertions of a module of their own,
|
|
128
|
+
as a subclass of :mod:`Pyjo.Test.Pyjo`, show the line of the test that calls
|
|
129
|
+
them: ::
|
|
130
|
+
|
|
131
|
+
# MyTest.py
|
|
132
|
+
import Pyjo.Test
|
|
133
|
+
|
|
134
|
+
__tracebackhide__ = Pyjo.Test.__tracebackhide__
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def host_is(url, host, desc=None):
|
|
138
|
+
return Pyjo.Test.is_ok(url.host, host, desc or f'host is {host}')
|
|
139
|
+
|
|
140
|
+
Exit status
|
|
141
|
+
-----------
|
|
142
|
+
|
|
143
|
+
The exit status of a test script is ``0`` when every test passed, the number of
|
|
144
|
+
failed tests (up to 254), ``254`` when tests ran without a plan, and ``255``
|
|
145
|
+
when the number of tests does not match the plan, when the script died with an
|
|
146
|
+
exception, or after :func:`bail_out`.
|
|
147
|
+
|
|
148
|
+
Exceptions
|
|
149
|
+
----------
|
|
150
|
+
|
|
151
|
+
After :func:`raise_failures`, the functions do not print TAP, so that they
|
|
152
|
+
work in the tests of another test runner, as pytest or unittest, where
|
|
153
|
+
:mod:`Pyjo.Test.pytest_plugin` calls it. A failed test raises
|
|
154
|
+
:exc:`AssertionError` with the name of the test and the diagnostic message,
|
|
155
|
+
``plan(skip_all=...)`` raises :exc:`unittest.SkipTest`, which both runners
|
|
156
|
+
report as a skipped test, or in a subtest leaves the subtest, and
|
|
157
|
+
:func:`bail_out` raises :exc:`BailOut`. A subtest is a block, the plans are
|
|
158
|
+
not checked, :func:`skip` does nothing, and :func:`diag` and :func:`note`
|
|
159
|
+
print their messages, which the runner captures. ::
|
|
160
|
+
|
|
161
|
+
import unittest
|
|
162
|
+
|
|
163
|
+
import Pyjo.Test
|
|
164
|
+
import Pyjo.URL
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def setUpModule():
|
|
168
|
+
Pyjo.Test.raise_failures()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def tearDownModule():
|
|
172
|
+
Pyjo.Test.raise_failures(False)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestURL(unittest.TestCase):
|
|
176
|
+
def test_host(self):
|
|
177
|
+
url = Pyjo.URL.new('HtTp://Example.Com')
|
|
178
|
+
Pyjo.Test.is_ok(url.host, 'Example.Com', 'right host')
|
|
179
|
+
|
|
180
|
+
Functions
|
|
181
|
+
---------
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
import atexit
|
|
185
|
+
import inspect
|
|
186
|
+
import os
|
|
187
|
+
import re
|
|
188
|
+
import sys
|
|
189
|
+
|
|
190
|
+
from collections.abc import Callable
|
|
191
|
+
from dataclasses import dataclass
|
|
192
|
+
from types import TracebackType
|
|
193
|
+
from typing import Any, NoReturn, Self, TextIO, cast, overload
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
__all__ = [
|
|
197
|
+
'bail_out',
|
|
198
|
+
'cmp_ok',
|
|
199
|
+
'diag',
|
|
200
|
+
'done_testing',
|
|
201
|
+
'fail_ok',
|
|
202
|
+
'in_ok',
|
|
203
|
+
'is_deeply_ok',
|
|
204
|
+
'is_ok',
|
|
205
|
+
'isa_ok',
|
|
206
|
+
'isnt_ok',
|
|
207
|
+
'like_ok',
|
|
208
|
+
'none_ok',
|
|
209
|
+
'not_in_ok',
|
|
210
|
+
'note',
|
|
211
|
+
'ok',
|
|
212
|
+
'pass_ok',
|
|
213
|
+
'plan',
|
|
214
|
+
'plan_skip_all',
|
|
215
|
+
'plan_tests',
|
|
216
|
+
'raise_failures',
|
|
217
|
+
'skip',
|
|
218
|
+
'subtest',
|
|
219
|
+
'that',
|
|
220
|
+
'throws_ok',
|
|
221
|
+
'unlike_ok',
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class Error(Exception):
|
|
226
|
+
"""
|
|
227
|
+
Exception raised for a misuse of the test functions, for example planning
|
|
228
|
+
twice.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class BailOut(Exception):
|
|
233
|
+
"""
|
|
234
|
+
Exception raised by :func:`bail_out` after :func:`raise_failures`, with
|
|
235
|
+
the reason.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class SkipAll(Exception):
|
|
240
|
+
"""
|
|
241
|
+
Exception raised by ``plan(skip_all=...)`` inside a subtest, to leave it.
|
|
242
|
+
It is caught by :func:`subtest`.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class _State:
|
|
248
|
+
name: str | None = None
|
|
249
|
+
depth: int = 0
|
|
250
|
+
count: int = 0
|
|
251
|
+
failed: int = 0
|
|
252
|
+
planned: int | None = None
|
|
253
|
+
done: bool = False
|
|
254
|
+
skip_all: str | None = None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass
|
|
258
|
+
class _Step:
|
|
259
|
+
kind: str | None
|
|
260
|
+
idx: object
|
|
261
|
+
got: object
|
|
262
|
+
expected: object
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# Any: comparisons and regular expressions take whatever the tested code
|
|
266
|
+
# returns, str or bytes, and a mismatch is a failed test, not a type error.
|
|
267
|
+
type _Pattern = str | bytes | re.Pattern[Any] | tuple[str | bytes, str]
|
|
268
|
+
|
|
269
|
+
_stack: list[_State] = [_State()]
|
|
270
|
+
_used = False
|
|
271
|
+
_died = False
|
|
272
|
+
_bailed_out = False
|
|
273
|
+
_raising = False
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# pytest leaves the frames of this module out of the traceback of a failed test, which ends at the call of the function
|
|
277
|
+
def __tracebackhide__(excinfo: Any) -> bool:
|
|
278
|
+
return excinfo is not None and isinstance(excinfo.value, AssertionError | BailOut)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def bail_out(reason: str = '') -> NoReturn:
|
|
282
|
+
"""::
|
|
283
|
+
|
|
284
|
+
if not connected:
|
|
285
|
+
bail_out('No database connection')
|
|
286
|
+
|
|
287
|
+
Stop all testing at once, here and in the test harness, which shows the
|
|
288
|
+
reason. The exit status is ``255``.
|
|
289
|
+
"""
|
|
290
|
+
global _bailed_out
|
|
291
|
+
if _raising:
|
|
292
|
+
raise BailOut(reason)
|
|
293
|
+
_use()
|
|
294
|
+
_bailed_out = True
|
|
295
|
+
_print(f'Bail out! {reason}', depth=0)
|
|
296
|
+
sys.exit(255)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def cmp_ok(got: Any, operator: str, expected: Any, test_name: str | None = None) -> 'Result':
|
|
300
|
+
"""::
|
|
301
|
+
|
|
302
|
+
# ok(got == expected)
|
|
303
|
+
cmp_ok(got, '==', expected, 'this == that')
|
|
304
|
+
|
|
305
|
+
Compare two values with a binary operator: ``==``, ``!=``, ``<``, ``<=``,
|
|
306
|
+
``>``, ``>=``, ``is``, ``is not``, ``in`` or ``not in``. The test passes if
|
|
307
|
+
the comparison is true.
|
|
308
|
+
"""
|
|
309
|
+
operators: dict[str, Callable[[Any, Any], bool]] = {
|
|
310
|
+
'==': lambda a, b: a == b,
|
|
311
|
+
'!=': lambda a, b: a != b,
|
|
312
|
+
'<': lambda a, b: a < b,
|
|
313
|
+
'<=': lambda a, b: a <= b,
|
|
314
|
+
'>': lambda a, b: a > b,
|
|
315
|
+
'>=': lambda a, b: a >= b,
|
|
316
|
+
'is': lambda a, b: a is b,
|
|
317
|
+
'is not': lambda a, b: a is not b,
|
|
318
|
+
'in': lambda a, b: a in b,
|
|
319
|
+
'not in': lambda a, b: a not in b,
|
|
320
|
+
}
|
|
321
|
+
if operator not in operators:
|
|
322
|
+
raise Error(f'Unknown operator {operator!r}')
|
|
323
|
+
check = operators[operator](got, expected)
|
|
324
|
+
_ok(check, test_name, None if check else [f' {got!r}', f' {operator}', f' {expected!r}'])
|
|
325
|
+
return Result(got, check)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def diag(*messages: object) -> bool:
|
|
329
|
+
"""::
|
|
330
|
+
|
|
331
|
+
if not in_ok(users, 'foo', "There's a foo user"):
|
|
332
|
+
diag("Since there's no foo, check that /etc/bar is set up right")
|
|
333
|
+
|
|
334
|
+
Print a diagnostic message to the standard error, which does not interfere
|
|
335
|
+
with the test output. Returns ``False``, so a failure is preserved.
|
|
336
|
+
"""
|
|
337
|
+
_use()
|
|
338
|
+
_diag_lines(''.join(str(m) for m in messages).split('\n'))
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def done_testing(how_many: int | None = None) -> bool:
|
|
343
|
+
"""::
|
|
344
|
+
|
|
345
|
+
ok(True)
|
|
346
|
+
done_testing()
|
|
347
|
+
|
|
348
|
+
ok(True)
|
|
349
|
+
done_testing(1)
|
|
350
|
+
|
|
351
|
+
Issue the plan when the tests are done, instead of declaring it up front
|
|
352
|
+
with :func:`plan`. The number of tests is optional; when it is given, it
|
|
353
|
+
has to match the number of tests run.
|
|
354
|
+
"""
|
|
355
|
+
if _raising:
|
|
356
|
+
return True
|
|
357
|
+
_use()
|
|
358
|
+
state = _current()
|
|
359
|
+
|
|
360
|
+
if state.done:
|
|
361
|
+
return fail_ok('done_testing() was already called')
|
|
362
|
+
|
|
363
|
+
if state.planned is not None and how_many is not None and how_many != state.planned:
|
|
364
|
+
fail_ok(f'planned to run {state.planned} but done_testing() expects {how_many}')
|
|
365
|
+
|
|
366
|
+
if state.planned is None:
|
|
367
|
+
state.planned = state.count if how_many is None else how_many
|
|
368
|
+
_print(f'1..{state.planned}')
|
|
369
|
+
|
|
370
|
+
state.done = True
|
|
371
|
+
return _finish(state)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def fail_ok(test_name: str | None = None) -> bool:
|
|
375
|
+
"""::
|
|
376
|
+
|
|
377
|
+
fail_ok('This should not happen')
|
|
378
|
+
|
|
379
|
+
A test that always fails, the same as ``ok(False)``.
|
|
380
|
+
"""
|
|
381
|
+
return _ok(False, test_name)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def in_ok(got: Any, elem: object, test_name: str | None = None) -> 'Result':
|
|
385
|
+
"""::
|
|
386
|
+
|
|
387
|
+
in_ok(['foo', 'bar'], 'foo', 'foo is in the list')
|
|
388
|
+
|
|
389
|
+
Check if the element is in the container. The same as
|
|
390
|
+
``ok(elem in got)``, with a better diagnostic message.
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
check = elem in got
|
|
394
|
+
except TypeError:
|
|
395
|
+
check = False
|
|
396
|
+
_ok(check, test_name, None if check else [f' got: {got!r}', f' expected: to contain {elem!r}'])
|
|
397
|
+
return Result(got, check)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def is_deeply_ok(got: object, expected: object, test_name: str | None = None) -> 'Result':
|
|
401
|
+
"""::
|
|
402
|
+
|
|
403
|
+
is_deeply_ok([[1, 2], [3, [4, 5]]], [[1, 2], [3]], 'deep structure')
|
|
404
|
+
|
|
405
|
+
Compare two data structures, walking through lists, tuples, sets and
|
|
406
|
+
dicts. If they differ, the diagnostic message shows where they start
|
|
407
|
+
differing.
|
|
408
|
+
"""
|
|
409
|
+
if _is_container(got) and _is_container(expected):
|
|
410
|
+
stack: list[_Step] = []
|
|
411
|
+
check = _deep_check(stack, got, expected)
|
|
412
|
+
_ok(check, test_name, None if check else _format_stack(stack))
|
|
413
|
+
else:
|
|
414
|
+
check = got == expected
|
|
415
|
+
_ok(check, test_name, None if check else [f' got: {got!r}', f' expected: {expected!r}'])
|
|
416
|
+
return Result(got, check)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def is_ok(got: object, expected: object, test_name: str | None = None) -> 'Result':
|
|
420
|
+
"""::
|
|
421
|
+
|
|
422
|
+
is_ok(ultimate_answer(), 42, 'meaning of life')
|
|
423
|
+
|
|
424
|
+
Compare two values with ``==``.
|
|
425
|
+
"""
|
|
426
|
+
check = got == expected
|
|
427
|
+
_ok(check, test_name, None if check else [f' got: {got!r}', f' expected: {expected!r}'])
|
|
428
|
+
return Result(got, check)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def isa_ok(got: object, cls: type | tuple[type, ...], test_name: str | None = None) -> 'Result':
|
|
432
|
+
"""::
|
|
433
|
+
|
|
434
|
+
obj = SomeClass()
|
|
435
|
+
isa_ok(obj, SomeClass)
|
|
436
|
+
isa_ok(123, (int, float))
|
|
437
|
+
|
|
438
|
+
Check if the object is an instance of the class, or of one of the classes.
|
|
439
|
+
"""
|
|
440
|
+
if test_name is None:
|
|
441
|
+
test_name = f'An object of class {type(got).__name__!r} isa {_class_names(cls)!r}'
|
|
442
|
+
check = isinstance(got, cls)
|
|
443
|
+
_ok(
|
|
444
|
+
check,
|
|
445
|
+
test_name,
|
|
446
|
+
None if check else [f' The object of class {type(got).__name__!r} is not a {_class_names(cls)!r}'],
|
|
447
|
+
)
|
|
448
|
+
return Result(got, check)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def isnt_ok(got: object, expected: object, test_name: str | None = None) -> 'Result':
|
|
452
|
+
"""::
|
|
453
|
+
|
|
454
|
+
isnt_ok(foo, '', 'got some foo')
|
|
455
|
+
|
|
456
|
+
Compare two values with ``!=``.
|
|
457
|
+
"""
|
|
458
|
+
check = got != expected
|
|
459
|
+
_ok(check, test_name, None if check else [f' got: {got!r}', ' expected: anything else'])
|
|
460
|
+
return Result(got, check)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def like_ok(got: Any, expected: _Pattern, test_name: str | None = None) -> 'Result':
|
|
464
|
+
"""::
|
|
465
|
+
|
|
466
|
+
like_ok(got, r'expected', 'test name')
|
|
467
|
+
like_ok(got, (r'expected', 'i'), 'test name')
|
|
468
|
+
like_ok(got, re.compile(r'expected'), 'test name')
|
|
469
|
+
|
|
470
|
+
Check if the value matches the regular expression. The expression can be
|
|
471
|
+
compiled already, or given with flags as a tuple: ``a``, ``i``, ``m``,
|
|
472
|
+
``s`` and ``x``.
|
|
473
|
+
"""
|
|
474
|
+
regex = _regex(expected)
|
|
475
|
+
try:
|
|
476
|
+
check = regex.search(got) is not None
|
|
477
|
+
except TypeError:
|
|
478
|
+
check = False
|
|
479
|
+
_ok(check, test_name, None if check else [f' {got!r}', f" doesn't match {regex.pattern!r}"])
|
|
480
|
+
return Result(got, check)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def none_ok(got: object, test_name: str | None = None) -> 'Result':
|
|
484
|
+
"""::
|
|
485
|
+
|
|
486
|
+
none_ok(req.headers.transfer_encoding, 'no "Transfer-Encoding" value')
|
|
487
|
+
|
|
488
|
+
Check if the value is ``None``.
|
|
489
|
+
"""
|
|
490
|
+
check = got is None
|
|
491
|
+
_ok(check, test_name, None if check else [f' got: {got!r}', ' expected: None'])
|
|
492
|
+
return Result(got, check)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def not_in_ok(got: Any, elem: object, test_name: str | None = None) -> 'Result':
|
|
496
|
+
"""::
|
|
497
|
+
|
|
498
|
+
not_in_ok(os.environ, 'DISPLAY', 'DISPLAY is not set')
|
|
499
|
+
|
|
500
|
+
The opposite of :func:`in_ok`: check if the element is not in the
|
|
501
|
+
container.
|
|
502
|
+
"""
|
|
503
|
+
try:
|
|
504
|
+
check = elem not in got
|
|
505
|
+
except TypeError:
|
|
506
|
+
check = False
|
|
507
|
+
_ok(check, test_name, None if check else [f' got: {got!r}', f' expected: not to contain {elem!r}'])
|
|
508
|
+
return Result(got, check)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def note(*messages: object) -> None:
|
|
512
|
+
"""::
|
|
513
|
+
|
|
514
|
+
note(f'Tempfile is {tempfile}')
|
|
515
|
+
|
|
516
|
+
Print a message to the standard output as a TAP comment, which the test
|
|
517
|
+
harness hides unless asked to show it.
|
|
518
|
+
"""
|
|
519
|
+
_use()
|
|
520
|
+
for line in ''.join(str(m) for m in messages).split('\n'):
|
|
521
|
+
_print(f'# {line}' if line else '#')
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def ok(check: object, test_name: str | None = None) -> 'Result':
|
|
525
|
+
"""::
|
|
526
|
+
|
|
527
|
+
ok(3**4 == 81, 'simple exponential')
|
|
528
|
+
ok(isinstance(film, Film), 'set_db()')
|
|
529
|
+
ok(None not in items, 'all items defined')
|
|
530
|
+
|
|
531
|
+
The basic test: it passes if the value is true and fails otherwise.
|
|
532
|
+
Returns whether the test passed.
|
|
533
|
+
"""
|
|
534
|
+
return Result(check, _ok(check, test_name))
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def pass_ok(test_name: str | None = None) -> bool:
|
|
538
|
+
"""::
|
|
539
|
+
|
|
540
|
+
pass_ok('step 1')
|
|
541
|
+
|
|
542
|
+
A test that always passes, the same as ``ok(True)``.
|
|
543
|
+
"""
|
|
544
|
+
return _ok(True, test_name)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def plan(tests: int | None = None, skip_all: str | None = None) -> 'Plan':
|
|
548
|
+
"""::
|
|
549
|
+
|
|
550
|
+
plan(tests=3)
|
|
551
|
+
|
|
552
|
+
if not os.getenv('TEST_ONLINE'):
|
|
553
|
+
plan(skip_all='set TEST_ONLINE to enable this test')
|
|
554
|
+
|
|
555
|
+
with plan():
|
|
556
|
+
ok(True)
|
|
557
|
+
|
|
558
|
+
with plan(tests=1):
|
|
559
|
+
ok(True)
|
|
560
|
+
|
|
561
|
+
Declare how many tests the script is going to run, which protects against
|
|
562
|
+
a premature exit, or skip all of them with a reason. In a subtest,
|
|
563
|
+
``skip_all`` skips the rest of the subtest.
|
|
564
|
+
|
|
565
|
+
It returns a :class:`Plan`, a context manager which calls
|
|
566
|
+
:func:`done_testing` at the end of its block, so that the number of tests
|
|
567
|
+
can be left out.
|
|
568
|
+
"""
|
|
569
|
+
if _raising:
|
|
570
|
+
if skip_all is not None:
|
|
571
|
+
# The exception of a skipped test that pytest knows too, imported only by the tests that raise it
|
|
572
|
+
import unittest
|
|
573
|
+
|
|
574
|
+
raise SkipAll(skip_all) if _current().depth else unittest.SkipTest(skip_all)
|
|
575
|
+
return Plan()
|
|
576
|
+
|
|
577
|
+
_use()
|
|
578
|
+
state = _current()
|
|
579
|
+
|
|
580
|
+
if state.planned is not None or state.done:
|
|
581
|
+
raise Error('You tried to plan twice')
|
|
582
|
+
|
|
583
|
+
if skip_all is not None:
|
|
584
|
+
state.skip_all = skip_all
|
|
585
|
+
state.planned = 0
|
|
586
|
+
state.done = True
|
|
587
|
+
_print(f'1..0 # SKIP {skip_all}')
|
|
588
|
+
if state.depth:
|
|
589
|
+
raise SkipAll(skip_all)
|
|
590
|
+
sys.exit(0)
|
|
591
|
+
|
|
592
|
+
if tests is not None:
|
|
593
|
+
state.planned = tests
|
|
594
|
+
_print(f'1..{tests}')
|
|
595
|
+
return Plan()
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def plan_skip_all(why: str) -> 'Plan':
|
|
599
|
+
"""::
|
|
600
|
+
|
|
601
|
+
plan_skip_all('set TEST_ONLINE to enable this test')
|
|
602
|
+
|
|
603
|
+
The same as ``plan(skip_all=why)``.
|
|
604
|
+
"""
|
|
605
|
+
return plan(skip_all=why)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def plan_tests(how_many: int) -> 'Plan':
|
|
609
|
+
"""::
|
|
610
|
+
|
|
611
|
+
plan_tests(3)
|
|
612
|
+
|
|
613
|
+
The same as ``plan(tests=how_many)``.
|
|
614
|
+
"""
|
|
615
|
+
return plan(tests=how_many)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def raise_failures(enable: bool = True) -> None:
|
|
619
|
+
"""::
|
|
620
|
+
|
|
621
|
+
raise_failures()
|
|
622
|
+
raise_failures(False)
|
|
623
|
+
|
|
624
|
+
Make the functions raise exceptions instead of printing TAP, as the
|
|
625
|
+
section `Exceptions`_ describes, or print TAP again with ``False``.
|
|
626
|
+
"""
|
|
627
|
+
global _raising
|
|
628
|
+
_raising = enable
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def skip(why: str | None = None, how_many: int = 1) -> bool:
|
|
632
|
+
"""::
|
|
633
|
+
|
|
634
|
+
if os.getenv('TEST_ONLINE'):
|
|
635
|
+
ok(Pyjo.UserAgent.new().get('https://mojolicious.org').result.is_success, 'online request')
|
|
636
|
+
else:
|
|
637
|
+
skip('set TEST_ONLINE to enable this test', 1)
|
|
638
|
+
|
|
639
|
+
Skip the given number of tests, which still count for the plan.
|
|
640
|
+
"""
|
|
641
|
+
if _raising:
|
|
642
|
+
return True
|
|
643
|
+
_use()
|
|
644
|
+
state = _current()
|
|
645
|
+
for _ in range(how_many):
|
|
646
|
+
state.count += 1
|
|
647
|
+
_print(f'ok {state.count} # skip {why}' if why is not None else f'ok {state.count} # skip')
|
|
648
|
+
return True
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class Plan:
|
|
652
|
+
"""
|
|
653
|
+
The plan of :func:`plan`, which is true. It is a context manager: at the
|
|
654
|
+
end of its block, it calls :func:`done_testing`, unless the tests are done
|
|
655
|
+
already or the block raises an exception, which ends the script without a
|
|
656
|
+
plan.
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
def __bool__(self) -> bool:
|
|
660
|
+
return True
|
|
661
|
+
|
|
662
|
+
def __enter__(self) -> Self:
|
|
663
|
+
return self
|
|
664
|
+
|
|
665
|
+
def __exit__(
|
|
666
|
+
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
667
|
+
) -> None:
|
|
668
|
+
if exc_type is None and not _current().done:
|
|
669
|
+
done_testing()
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
class Result:
|
|
673
|
+
"""
|
|
674
|
+
The result of a test of a value, returned by :func:`ok`, :func:`is_ok`
|
|
675
|
+
and the other functions which test one, or of no test, returned by
|
|
676
|
+
:func:`that`. Its methods test the same value again, and it is true when
|
|
677
|
+
all the tests of the chain passed: ::
|
|
678
|
+
|
|
679
|
+
isa_ok(answer, str).is_('42', 'meaning of life') or diag('the answer is lost')
|
|
680
|
+
"""
|
|
681
|
+
|
|
682
|
+
def __init__(self, got: object, passed: bool) -> None:
|
|
683
|
+
self.got: Any = got
|
|
684
|
+
"""::
|
|
685
|
+
|
|
686
|
+
got = result.got
|
|
687
|
+
|
|
688
|
+
The value that the tests check: the first argument of the function,
|
|
689
|
+
or the exception that the callback of :func:`throws_ok` raised, without
|
|
690
|
+
its traceback.
|
|
691
|
+
"""
|
|
692
|
+
|
|
693
|
+
self.passed = passed
|
|
694
|
+
"""::
|
|
695
|
+
|
|
696
|
+
boolean = result.passed
|
|
697
|
+
|
|
698
|
+
True if all the tests of the chain passed.
|
|
699
|
+
"""
|
|
700
|
+
|
|
701
|
+
def __bool__(self) -> bool:
|
|
702
|
+
"""::
|
|
703
|
+
|
|
704
|
+
boolean = bool(result)
|
|
705
|
+
|
|
706
|
+
The value of :attr:`passed`.
|
|
707
|
+
"""
|
|
708
|
+
return self.passed
|
|
709
|
+
|
|
710
|
+
def __repr__(self) -> str:
|
|
711
|
+
return f'<{type(self).__module__}.{type(self).__qualname__} passed={self.passed}>'
|
|
712
|
+
|
|
713
|
+
def cmp(self, operator: str, expected: Any, test_name: str | None = None) -> Self:
|
|
714
|
+
"""::
|
|
715
|
+
|
|
716
|
+
ok(count, 'some').cmp('<', 10, 'small enough')
|
|
717
|
+
|
|
718
|
+
The same as :func:`cmp_ok`.
|
|
719
|
+
"""
|
|
720
|
+
return self._chain(cmp_ok(self.got, operator, expected, test_name))
|
|
721
|
+
|
|
722
|
+
def in_(self, elem: object, test_name: str | None = None) -> Self:
|
|
723
|
+
"""::
|
|
724
|
+
|
|
725
|
+
isa_ok(names, list).in_('foo', "there's a foo")
|
|
726
|
+
|
|
727
|
+
The same as :func:`in_ok`.
|
|
728
|
+
"""
|
|
729
|
+
return self._chain(in_ok(self.got, elem, test_name))
|
|
730
|
+
|
|
731
|
+
def is_(self, expected: object, test_name: str | None = None) -> Self:
|
|
732
|
+
"""::
|
|
733
|
+
|
|
734
|
+
isa_ok(answer, int).is_(42, 'meaning of life')
|
|
735
|
+
|
|
736
|
+
The same as :func:`is_ok`.
|
|
737
|
+
"""
|
|
738
|
+
return self._chain(is_ok(self.got, expected, test_name))
|
|
739
|
+
|
|
740
|
+
def is_deeply(self, expected: object, test_name: str | None = None) -> Self:
|
|
741
|
+
"""::
|
|
742
|
+
|
|
743
|
+
isa_ok(got, dict).is_deeply({'a': [1, 2]}, 'deep structure')
|
|
744
|
+
|
|
745
|
+
The same as :func:`is_deeply_ok`.
|
|
746
|
+
"""
|
|
747
|
+
return self._chain(is_deeply_ok(self.got, expected, test_name))
|
|
748
|
+
|
|
749
|
+
def isa(self, cls: type | tuple[type, ...], test_name: str | None = None) -> Self:
|
|
750
|
+
"""::
|
|
751
|
+
|
|
752
|
+
throws_ok(lambda: {}['x'], KeyError).isa(LookupError)
|
|
753
|
+
|
|
754
|
+
The same as :func:`isa_ok`.
|
|
755
|
+
"""
|
|
756
|
+
return self._chain(isa_ok(self.got, cls, test_name))
|
|
757
|
+
|
|
758
|
+
def isnt(self, expected: object, test_name: str | None = None) -> Self:
|
|
759
|
+
"""::
|
|
760
|
+
|
|
761
|
+
isa_ok(foo, str).isnt('', 'got some foo')
|
|
762
|
+
|
|
763
|
+
The same as :func:`isnt_ok`.
|
|
764
|
+
"""
|
|
765
|
+
return self._chain(isnt_ok(self.got, expected, test_name))
|
|
766
|
+
|
|
767
|
+
def like(self, expected: _Pattern, test_name: str | None = None) -> Self:
|
|
768
|
+
"""::
|
|
769
|
+
|
|
770
|
+
isa_ok(got, str).like(r'expected', 'test name')
|
|
771
|
+
|
|
772
|
+
The same as :func:`like_ok`.
|
|
773
|
+
"""
|
|
774
|
+
return self._chain(like_ok(self.got, expected, test_name))
|
|
775
|
+
|
|
776
|
+
def none(self, test_name: str | None = None) -> Self:
|
|
777
|
+
"""::
|
|
778
|
+
|
|
779
|
+
isnt_ok(req.headers.transfer_encoding, 'chunked').none('no "Transfer-Encoding" value')
|
|
780
|
+
|
|
781
|
+
The same as :func:`none_ok`.
|
|
782
|
+
"""
|
|
783
|
+
return self._chain(none_ok(self.got, test_name))
|
|
784
|
+
|
|
785
|
+
def not_in(self, elem: object, test_name: str | None = None) -> Self:
|
|
786
|
+
"""::
|
|
787
|
+
|
|
788
|
+
in_ok(names, 'foo').not_in('bar', 'no bar')
|
|
789
|
+
|
|
790
|
+
The same as :func:`not_in_ok`.
|
|
791
|
+
"""
|
|
792
|
+
return self._chain(not_in_ok(self.got, elem, test_name))
|
|
793
|
+
|
|
794
|
+
def ok(self, test_name: str | None = None) -> Self:
|
|
795
|
+
"""::
|
|
796
|
+
|
|
797
|
+
isa_ok(items, list).ok('not empty')
|
|
798
|
+
|
|
799
|
+
The same as :func:`ok` of the value.
|
|
800
|
+
"""
|
|
801
|
+
return self._chain(ok(self.got, test_name))
|
|
802
|
+
|
|
803
|
+
def unlike(self, expected: _Pattern, test_name: str | None = None) -> Self:
|
|
804
|
+
"""::
|
|
805
|
+
|
|
806
|
+
like_ok(got, r'expected').unlike(r'unexpected', 'test name')
|
|
807
|
+
|
|
808
|
+
The same as :func:`unlike_ok`.
|
|
809
|
+
"""
|
|
810
|
+
return self._chain(unlike_ok(self.got, expected, test_name))
|
|
811
|
+
|
|
812
|
+
def _chain(self, result: 'Result') -> Self:
|
|
813
|
+
self.passed = self.passed and result.passed
|
|
814
|
+
return self
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
class Subtest:
|
|
818
|
+
"""
|
|
819
|
+
A subtest, returned by :func:`subtest` without a callback. It is a context
|
|
820
|
+
manager and a decorator; after it ends, :attr:`passed` tells whether all
|
|
821
|
+
of its tests passed.
|
|
822
|
+
"""
|
|
823
|
+
|
|
824
|
+
def __init__(self, name: str) -> None:
|
|
825
|
+
self.name = name
|
|
826
|
+
self.passed: bool | None = None
|
|
827
|
+
|
|
828
|
+
def __call__[F: Callable[[], object]](self, fn: F) -> F:
|
|
829
|
+
with self:
|
|
830
|
+
fn()
|
|
831
|
+
return fn
|
|
832
|
+
|
|
833
|
+
def __enter__(self) -> Self:
|
|
834
|
+
_use()
|
|
835
|
+
if not _raising:
|
|
836
|
+
_print(f'# Subtest: {self.name}')
|
|
837
|
+
_stack.append(_State(self.name, _current().depth + 1))
|
|
838
|
+
return self
|
|
839
|
+
|
|
840
|
+
def __exit__(
|
|
841
|
+
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
842
|
+
) -> bool:
|
|
843
|
+
if _raising:
|
|
844
|
+
_stack.pop()
|
|
845
|
+
skipped = exc_type is not None and issubclass(exc_type, SkipAll)
|
|
846
|
+
self.passed = exc_type is None or skipped
|
|
847
|
+
return skipped
|
|
848
|
+
|
|
849
|
+
state = _current()
|
|
850
|
+
|
|
851
|
+
if exc_type is not None and issubclass(exc_type, SystemExit):
|
|
852
|
+
_stack.pop()
|
|
853
|
+
return False
|
|
854
|
+
|
|
855
|
+
if exc_type is not None and issubclass(exc_type, SkipAll):
|
|
856
|
+
_stack.pop()
|
|
857
|
+
self.passed = True
|
|
858
|
+
skip(state.skip_all)
|
|
859
|
+
return True
|
|
860
|
+
|
|
861
|
+
name = self.name
|
|
862
|
+
if exc_type is not None:
|
|
863
|
+
if state.planned is None:
|
|
864
|
+
state.planned = state.count
|
|
865
|
+
_print(f'1..{state.planned}')
|
|
866
|
+
self.passed = False
|
|
867
|
+
elif state.count == 0 and state.planned is None and not state.done:
|
|
868
|
+
_print('1..0')
|
|
869
|
+
_diag_lines(['No tests run!'], stream=sys.stdout)
|
|
870
|
+
name = f'No tests run for subtest "{self.name}"'
|
|
871
|
+
self.passed = False
|
|
872
|
+
elif state.done:
|
|
873
|
+
self.passed = _passed(state)
|
|
874
|
+
else:
|
|
875
|
+
state.done = True
|
|
876
|
+
if state.planned is None:
|
|
877
|
+
state.planned = state.count
|
|
878
|
+
_print(f'1..{state.planned}')
|
|
879
|
+
self.passed = _finish(state)
|
|
880
|
+
|
|
881
|
+
_stack.pop()
|
|
882
|
+
_ok(self.passed, name)
|
|
883
|
+
return False
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
@overload
|
|
887
|
+
def subtest(name: str, fn: None = None) -> Subtest: ...
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
@overload
|
|
891
|
+
def subtest(name: str, fn: Callable[[], object]) -> bool: ...
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def subtest(name: str, fn: Callable[[], object] | None = None) -> Subtest | bool:
|
|
895
|
+
"""::
|
|
896
|
+
|
|
897
|
+
with subtest('Parse'):
|
|
898
|
+
ok(True)
|
|
899
|
+
|
|
900
|
+
@subtest('Parse')
|
|
901
|
+
def _():
|
|
902
|
+
ok(True)
|
|
903
|
+
|
|
904
|
+
passed = subtest('Parse', lambda: ok(True))
|
|
905
|
+
|
|
906
|
+
Run a group of tests as a single test of the parent. Without a callback it
|
|
907
|
+
returns a :class:`Subtest`, which works as a context manager and as a
|
|
908
|
+
decorator; with a callback it runs it at once and returns whether the
|
|
909
|
+
subtest passed.
|
|
910
|
+
"""
|
|
911
|
+
test = Subtest(name)
|
|
912
|
+
if fn is None:
|
|
913
|
+
return test
|
|
914
|
+
test(fn)
|
|
915
|
+
return bool(test.passed)
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def that(got: object) -> Result:
|
|
919
|
+
"""::
|
|
920
|
+
|
|
921
|
+
that(answer).isa(int).is_(42, 'meaning of life')
|
|
922
|
+
|
|
923
|
+
Make a :class:`Result` of the value without a test, which is true, so
|
|
924
|
+
that a chain of tests of the value starts with the first of its methods.
|
|
925
|
+
"""
|
|
926
|
+
return Result(got, True)
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def throws_ok(
|
|
930
|
+
cb: Callable[[], object], expected: type[BaseException] | str | re.Pattern[str], test_name: str | None = None
|
|
931
|
+
) -> 'Result':
|
|
932
|
+
"""::
|
|
933
|
+
|
|
934
|
+
throws_ok(lambda: [1, 2, 3][3], IndexError, 'no more elements')
|
|
935
|
+
throws_ok(lambda: int('x'), "invalid literal for int() with base 10: 'x'", 'right message')
|
|
936
|
+
throws_ok(lambda: int('x'), re.compile(r'invalid literal'), 'right message')
|
|
937
|
+
|
|
938
|
+
Check if the callback raises an exception: of the given class, with the
|
|
939
|
+
given message, or with a message matching the regular expression.
|
|
940
|
+
"""
|
|
941
|
+
error = None
|
|
942
|
+
got = None
|
|
943
|
+
check = False
|
|
944
|
+
|
|
945
|
+
# The expected class, also one that is not an Exception, as SystemExit, but no other BaseException
|
|
946
|
+
catch = (Exception, expected) if isinstance(expected, type) else Exception
|
|
947
|
+
try:
|
|
948
|
+
cb()
|
|
949
|
+
except catch as e:
|
|
950
|
+
# Without its traceback, whose frames would keep what the callback had, as a socket, in a cycle with this frame
|
|
951
|
+
error = e.with_traceback(None)
|
|
952
|
+
if isinstance(expected, type):
|
|
953
|
+
got = type(e).__name__
|
|
954
|
+
check = isinstance(e, expected)
|
|
955
|
+
elif isinstance(expected, re.Pattern):
|
|
956
|
+
got = str(e)
|
|
957
|
+
check = bool(expected.search(got))
|
|
958
|
+
else:
|
|
959
|
+
got = str(e)
|
|
960
|
+
check = got == expected
|
|
961
|
+
|
|
962
|
+
if isinstance(expected, type):
|
|
963
|
+
expected = expected.__name__
|
|
964
|
+
elif isinstance(expected, re.Pattern):
|
|
965
|
+
expected = f'a message matching {expected.pattern!r}'
|
|
966
|
+
|
|
967
|
+
_ok(
|
|
968
|
+
check,
|
|
969
|
+
test_name,
|
|
970
|
+
None if check else [f' got: {got if got is not None else "no exception"}', f' expected: {expected}'],
|
|
971
|
+
)
|
|
972
|
+
return Result(error, check)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def unlike_ok(got: Any, expected: _Pattern, test_name: str | None = None) -> 'Result':
|
|
976
|
+
"""::
|
|
977
|
+
|
|
978
|
+
unlike_ok(got, r'unexpected', 'test name')
|
|
979
|
+
|
|
980
|
+
The opposite of :func:`like_ok`: check if the value does not match the
|
|
981
|
+
regular expression.
|
|
982
|
+
"""
|
|
983
|
+
regex = _regex(expected)
|
|
984
|
+
try:
|
|
985
|
+
check = regex.search(got) is None
|
|
986
|
+
except TypeError:
|
|
987
|
+
check = True
|
|
988
|
+
_ok(check, test_name, None if check else [f' {got!r}', f' matches {regex.pattern!r}'])
|
|
989
|
+
return Result(got, check)
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
class _DoesNotExist:
|
|
993
|
+
def __repr__(self) -> str:
|
|
994
|
+
return 'Does not exist'
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
_DOES_NOT_EXIST = _DoesNotExist()
|
|
998
|
+
|
|
999
|
+
_FLAGS: dict[str, re.RegexFlag] = {
|
|
1000
|
+
'a': re.ASCII,
|
|
1001
|
+
'i': re.IGNORECASE,
|
|
1002
|
+
'm': re.MULTILINE,
|
|
1003
|
+
's': re.DOTALL,
|
|
1004
|
+
'x': re.VERBOSE,
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def _at_exit() -> None:
|
|
1009
|
+
if not _used:
|
|
1010
|
+
return
|
|
1011
|
+
|
|
1012
|
+
state = _stack[0]
|
|
1013
|
+
|
|
1014
|
+
if _bailed_out:
|
|
1015
|
+
code = 255
|
|
1016
|
+
elif state.skip_all is not None:
|
|
1017
|
+
code = 0
|
|
1018
|
+
elif _died:
|
|
1019
|
+
_diag_lines([f'Looks like your test exited with 255 just after {state.count}.'], depth=0)
|
|
1020
|
+
code = 255
|
|
1021
|
+
elif state.planned is None:
|
|
1022
|
+
_diag_lines(['Tests were run but no plan was declared and done_testing() was not seen.'], depth=0)
|
|
1023
|
+
code = 254
|
|
1024
|
+
else:
|
|
1025
|
+
if not state.done:
|
|
1026
|
+
_finish(state)
|
|
1027
|
+
if state.count != state.planned:
|
|
1028
|
+
code = 255
|
|
1029
|
+
else:
|
|
1030
|
+
code = min(state.failed, 254)
|
|
1031
|
+
|
|
1032
|
+
if code:
|
|
1033
|
+
sys.stdout.flush()
|
|
1034
|
+
sys.stderr.flush()
|
|
1035
|
+
os._exit(code)
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _class_names(cls: type | tuple[type, ...]) -> str:
|
|
1039
|
+
if isinstance(cls, tuple):
|
|
1040
|
+
return ', '.join(c.__name__ for c in cls)
|
|
1041
|
+
return cls.__name__
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
# The first frame outside of the package and of the modules that pytest leaves out of a traceback, as an extension of
|
|
1045
|
+
# Pyjo.Test.Pyjo in a package of its own, as Pyjo.Test.Pyjo raises $Test::Builder::Level of Test::Mojo
|
|
1046
|
+
def _caller() -> str | None:
|
|
1047
|
+
frame = inspect.currentframe()
|
|
1048
|
+
while frame is not None and (
|
|
1049
|
+
os.path.dirname(frame.f_code.co_filename) == os.path.dirname(__file__)
|
|
1050
|
+
or frame.f_globals.get('__tracebackhide__')
|
|
1051
|
+
):
|
|
1052
|
+
frame = frame.f_back
|
|
1053
|
+
if frame is None:
|
|
1054
|
+
return None
|
|
1055
|
+
return f'{frame.f_code.co_filename} line {frame.f_lineno}'
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def _current() -> _State:
|
|
1059
|
+
return _stack[-1]
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def _deep_check(stack: list[_Step], e1: object, e2: object) -> bool:
|
|
1063
|
+
step = _Step(None, None, e1, e2)
|
|
1064
|
+
|
|
1065
|
+
if e1 is e2:
|
|
1066
|
+
return True
|
|
1067
|
+
|
|
1068
|
+
if isinstance(e1, list) and isinstance(e2, list):
|
|
1069
|
+
return _eq_sequence(stack, 'list', cast(list[object], e1), cast(list[object], e2))
|
|
1070
|
+
|
|
1071
|
+
if isinstance(e1, tuple) and isinstance(e2, tuple):
|
|
1072
|
+
return _eq_sequence(stack, 'tuple', cast(tuple[object, ...], e1), cast(tuple[object, ...], e2))
|
|
1073
|
+
|
|
1074
|
+
if isinstance(e1, set) and isinstance(e2, set):
|
|
1075
|
+
return _eq_sequence(
|
|
1076
|
+
stack, 'set', sorted(cast(set[object], e1), key=repr), sorted(cast(set[object], e2), key=repr)
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
if isinstance(e1, dict) and isinstance(e2, dict):
|
|
1080
|
+
return _eq_dict(stack, cast(dict[object, object], e1), cast(dict[object, object], e2))
|
|
1081
|
+
|
|
1082
|
+
if e1 is not None and e2 is not None and e1 == e2:
|
|
1083
|
+
return True
|
|
1084
|
+
|
|
1085
|
+
stack.append(step)
|
|
1086
|
+
return False
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
def _diag_lines(lines: list[str], stream: TextIO | None = None, depth: int | None = None) -> None:
|
|
1090
|
+
_print(*[f'# {line}' if line else '#' for line in lines], stream=stream or sys.stderr, depth=depth)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _eq_dict(stack: list[_Step], h1: dict[object, object], h2: dict[object, object]) -> bool:
|
|
1094
|
+
for k in sorted(set(h1) | set(h2), key=repr):
|
|
1095
|
+
e1 = h1.get(k, _DOES_NOT_EXIST)
|
|
1096
|
+
e2 = h2.get(k, _DOES_NOT_EXIST)
|
|
1097
|
+
stack.append(_Step('dict', k, e1, e2))
|
|
1098
|
+
if not _deep_check(stack, e1, e2):
|
|
1099
|
+
return False
|
|
1100
|
+
stack.pop()
|
|
1101
|
+
return True
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def _eq_sequence(
|
|
1105
|
+
stack: list[_Step], kind: str, a1: list[object] | tuple[object, ...], a2: list[object] | tuple[object, ...]
|
|
1106
|
+
) -> bool:
|
|
1107
|
+
for i in range(max(len(a1), len(a2))):
|
|
1108
|
+
e1 = a1[i] if i < len(a1) else _DOES_NOT_EXIST
|
|
1109
|
+
e2 = a2[i] if i < len(a2) else _DOES_NOT_EXIST
|
|
1110
|
+
stack.append(_Step(kind, i, e1, e2))
|
|
1111
|
+
if not _deep_check(stack, e1, e2):
|
|
1112
|
+
return False
|
|
1113
|
+
stack.pop()
|
|
1114
|
+
return True
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def _except_hook(exc_type: type[BaseException], exc_value: BaseException, traceback: TracebackType | None) -> None:
|
|
1118
|
+
global _died
|
|
1119
|
+
_died = True
|
|
1120
|
+
_original_except_hook(exc_type, exc_value, traceback)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _finish(state: _State) -> bool:
|
|
1124
|
+
if state.skip_all is None:
|
|
1125
|
+
if state.count != state.planned:
|
|
1126
|
+
_diag_lines([f'Looks like you planned {_tests(state.planned)} but ran {state.count}.'])
|
|
1127
|
+
elif state.failed:
|
|
1128
|
+
_diag_lines([f'Looks like you failed {_tests(state.failed)} of {state.count}.'])
|
|
1129
|
+
elif not state.count:
|
|
1130
|
+
_diag_lines(['No tests run!'])
|
|
1131
|
+
return _passed(state)
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def _format_stack(stack: list[_Step]) -> list[str]:
|
|
1135
|
+
path = ''
|
|
1136
|
+
for step in stack:
|
|
1137
|
+
if step.kind in ('list', 'tuple', 'dict'):
|
|
1138
|
+
path += f'[{step.idx!r}]'
|
|
1139
|
+
elif step.kind == 'set':
|
|
1140
|
+
path = f'sorted({path or "x"})[{step.idx!r}]'
|
|
1141
|
+
|
|
1142
|
+
got, expected = stack[-1].got, stack[-1].expected
|
|
1143
|
+
return [
|
|
1144
|
+
' Structures begin differing at:',
|
|
1145
|
+
f' got{path} = {got!r}',
|
|
1146
|
+
f' expected{path} = {expected!r}',
|
|
1147
|
+
]
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def _is_container(value: object) -> bool:
|
|
1151
|
+
return isinstance(value, (list, tuple, set, dict))
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _ok(check: object, test_name: str | None = None, diagnostics: list[str] | None = None) -> bool:
|
|
1155
|
+
check = bool(check)
|
|
1156
|
+
if _raising:
|
|
1157
|
+
if not check:
|
|
1158
|
+
failed = f"Failed test '{test_name}'" if test_name is not None else 'Failed test'
|
|
1159
|
+
raise AssertionError('\n'.join([failed, *(diagnostics or [])]))
|
|
1160
|
+
return True
|
|
1161
|
+
|
|
1162
|
+
_use()
|
|
1163
|
+
state = _current()
|
|
1164
|
+
state.count += 1
|
|
1165
|
+
|
|
1166
|
+
line = f'ok {state.count}' if check else f'not ok {state.count}'
|
|
1167
|
+
if test_name is not None:
|
|
1168
|
+
if str(test_name).isdigit():
|
|
1169
|
+
_diag_lines(
|
|
1170
|
+
[
|
|
1171
|
+
f" You named your test '{test_name}'. You shouldn't use numbers for your test names.",
|
|
1172
|
+
' Very confusing.',
|
|
1173
|
+
]
|
|
1174
|
+
)
|
|
1175
|
+
line += ' - ' + str(test_name).replace('#', '\\#')
|
|
1176
|
+
_print(line)
|
|
1177
|
+
|
|
1178
|
+
if not check:
|
|
1179
|
+
state.failed += 1
|
|
1180
|
+
where = _caller()
|
|
1181
|
+
failed = f" Failed test '{test_name}'" if test_name is not None else ' Failed test'
|
|
1182
|
+
_diag_lines([failed, f' at {where}.'] if where else [failed])
|
|
1183
|
+
if diagnostics:
|
|
1184
|
+
_diag_lines(diagnostics)
|
|
1185
|
+
|
|
1186
|
+
return check
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def _passed(state: _State) -> bool:
|
|
1190
|
+
if state.skip_all is not None:
|
|
1191
|
+
return True
|
|
1192
|
+
return state.count == state.planned and not state.failed and state.count > 0
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def _print(*lines: str, stream: TextIO | None = None, depth: int | None = None) -> None:
|
|
1196
|
+
stream = stream or sys.stdout
|
|
1197
|
+
if stream is None:
|
|
1198
|
+
return
|
|
1199
|
+
indent = ' ' * (_current().depth if depth is None else depth)
|
|
1200
|
+
text = ''.join(f'{indent}{line}\n' for line in lines)
|
|
1201
|
+
encoding = getattr(stream, 'encoding', None)
|
|
1202
|
+
if not encoding or encoding.lower().replace('-', '') != 'utf8':
|
|
1203
|
+
text = text.encode('ascii', 'backslashreplace').decode('ascii')
|
|
1204
|
+
if stream is sys.stderr:
|
|
1205
|
+
sys.stdout.flush()
|
|
1206
|
+
stream.write(text)
|
|
1207
|
+
stream.flush()
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def _regex(expected: _Pattern) -> re.Pattern[Any]:
|
|
1211
|
+
if isinstance(expected, re.Pattern):
|
|
1212
|
+
return expected
|
|
1213
|
+
flags = 0
|
|
1214
|
+
if isinstance(expected, (list, tuple)):
|
|
1215
|
+
expected, letters = expected
|
|
1216
|
+
for letter in letters:
|
|
1217
|
+
flags |= _FLAGS[letter]
|
|
1218
|
+
return re.compile(expected, flags)
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _tests(n: int | None) -> str:
|
|
1222
|
+
return f'{n} test' if n == 1 else f'{n} tests'
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _use() -> None:
|
|
1226
|
+
global _used
|
|
1227
|
+
if not _raising:
|
|
1228
|
+
_used = True
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
_original_except_hook = sys.excepthook
|
|
1232
|
+
sys.excepthook = _except_hook
|
|
1233
|
+
atexit.register(_at_exit)
|