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.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. 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)