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/Exception.py ADDED
@@ -0,0 +1,494 @@
1
+ r"""
2
+ Pyjo.Exception - Exception base class
3
+ =====================================
4
+ ::
5
+
6
+ import Pyjo.Exception
7
+
8
+ # Create exception classes
9
+ class MyApp_X_Foo(Pyjo.Exception.object):
10
+ pass
11
+
12
+ class MyApp_X_Bar(Pyjo.Exception.object):
13
+ pass
14
+
15
+ # Throw exceptions and handle them gracefully
16
+ from Pyjo.Exception import check
17
+ try:
18
+ MyApp_X_Foo.throw('Something went wrong!')
19
+ except Exception as e:
20
+ check(e, [
21
+ (MyApp_X_Foo, lambda e: print(f'Foo: {e}')),
22
+ (MyApp_X_Bar, lambda e: print(f'Bar: {e}')),
23
+ ])
24
+
25
+ # Generate exception classes on demand
26
+ from Pyjo.Exception import check, throw
27
+ try:
28
+ throw('MyApp.X.Name', 'The name Minion is already taken')
29
+ except Exception as e:
30
+ check(e, [
31
+ ('MyApp.X.Name', lambda e: print(f'Name error: {e}')),
32
+ ('default', lambda e: print(f'Error: {e}')),
33
+ ])
34
+
35
+ :mod:`Pyjo.Exception` is a container for exceptions with context
36
+ information. It is an :class:`Exception`, so it is raised and caught as the
37
+ exceptions of Python are.
38
+
39
+ See also
40
+ --------
41
+
42
+ :mod:`Pyjoyment`, https://mojolicious.org.
43
+
44
+ Ported from `Mojo::Exception <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Exception.pm>`_
45
+ of Mojolicious 9.49.
46
+
47
+ Functions
48
+ ---------
49
+ """
50
+
51
+ import Pyjo.Base
52
+ import Pyjo.String.Mixin
53
+
54
+ from Pyjo.Util import decode
55
+
56
+ import builtins
57
+ import importlib
58
+ import os
59
+ import re
60
+ import sys
61
+ import traceback
62
+
63
+ from collections.abc import Callable
64
+ from typing import Any, NoReturn, Self, cast
65
+
66
+
67
+ type Anything = builtins.object
68
+
69
+ # A pattern of check(): "default", a class, its name or a regular expression
70
+ type Pattern = str | type[BaseException] | re.Pattern[str]
71
+
72
+
73
+ class Pyjo_Exception(Exception, Pyjo.Base.object, Pyjo.String.Mixin.object):
74
+ """
75
+ :mod:`Pyjo.Exception` inherits all methods from :class:`Exception`,
76
+ :mod:`Pyjo.Base` and :mod:`Pyjo.String.Mixin` and implements the following
77
+ new ones.
78
+ """
79
+
80
+ def __init__(self, message: Anything = None) -> None:
81
+ """::
82
+
83
+ e = Pyjo.Exception.new()
84
+ e = Pyjo.Exception.new('Died at test.py line 3.')
85
+ e = Pyjo.Exception.new(ValueError('Something went wrong!'))
86
+
87
+ Construct a new :mod:`Pyjo.Exception` object and assign
88
+ :attr:`message` if necessary. An exception of Python gives its message
89
+ and the :attr:`frames` of its traceback.
90
+ """
91
+ self.frames: list[traceback.FrameSummary] = []
92
+ """::
93
+
94
+ frames = e.frames
95
+ e.frames = [frame1, frame2]
96
+
97
+ Stack trace if available, as a list of
98
+ :class:`traceback.FrameSummary`, the most recent first. ::
99
+
100
+ # Extract information from the last frame
101
+ filename, lineno, name, line = e.frames[-1]
102
+ """
103
+
104
+ self.line: list[int | str] = []
105
+ """::
106
+
107
+ line = e.line
108
+ e.line = [3, 'raise Exception()']
109
+
110
+ The line where the exception occurred if available.
111
+ """
112
+
113
+ self.lines_after: list[list[int | str]] = []
114
+ """::
115
+
116
+ lines = e.lines_after
117
+ e.lines_after = [[4, 'print(foo)'], [5, 'print(bar)']]
118
+
119
+ Lines after the line where the exception occurred if available.
120
+ """
121
+
122
+ self.lines_before: list[list[int | str]] = []
123
+ """::
124
+
125
+ lines = e.lines_before
126
+ e.lines_before = [[1, 'foo = 23'], [2, 'bar = 24']]
127
+
128
+ Lines before the line where the exception occurred if available.
129
+ """
130
+
131
+ self.message: str = 'Exception!'
132
+ """::
133
+
134
+ msg = e.message
135
+ e.message = 'Died at test.py line 3.'
136
+
137
+ Exception message, defaults to ``Exception!``.
138
+ """
139
+
140
+ self._verbose: bool | None = None
141
+
142
+ # Not in Mojolicious: an exception of Python
143
+ if isinstance(message, BaseException):
144
+ self.message = str(message)
145
+ if message.__traceback__ is not None:
146
+ self.frames = list(reversed(traceback.extract_tb(message.__traceback__)))
147
+ elif message is not None:
148
+ self.message = str(message)
149
+
150
+ super().__init__(self.message)
151
+
152
+ @property
153
+ def verbose(self) -> bool:
154
+ """::
155
+
156
+ boolean = e.verbose
157
+ e.verbose = boolean
158
+
159
+ Show more information with :meth:`to_str`, such as :attr:`frames`,
160
+ defaults to the value of the ``PYJO_EXCEPTION_VERBOSE`` environment
161
+ variable.
162
+ """
163
+ if self._verbose is None:
164
+ self._verbose = bool(os.environ.get('PYJO_EXCEPTION_VERBOSE'))
165
+ return self._verbose
166
+
167
+ @verbose.setter
168
+ def verbose(self, value: bool) -> None:
169
+ self._verbose = value
170
+
171
+ def __bool__(self) -> bool:
172
+ """::
173
+
174
+ boolean = bool(e)
175
+
176
+ Always true.
177
+ """
178
+ return True
179
+
180
+ def __str__(self) -> str:
181
+ """::
182
+
183
+ string = str(e)
184
+
185
+ Alias for :meth:`to_str`.
186
+ """
187
+ return self.to_str()
188
+
189
+ def inspect(self, *sources: str) -> Self:
190
+ """::
191
+
192
+ e = e.inspect()
193
+ e = e.inspect(source1, source2)
194
+
195
+ Inspect :attr:`message`, :attr:`frames` and optional additional sources
196
+ to fill :attr:`lines_before`, :attr:`line` and :attr:`lines_after` with
197
+ context information.
198
+ """
199
+ if len(self.line):
200
+ return self
201
+
202
+ # Extract file and line from message
203
+ files = [(m[1], int(m[2])) for m in re.finditer(r'at\s+(.+?)\s+line\s+(\d+)', self.message)][::-1]
204
+
205
+ # Extract file and line from stack trace
206
+ if len(self.frames) and (zero := self.frames[0]).lineno is not None:
207
+ files.append((zero.filename, zero.lineno))
208
+
209
+ # Search for context in files
210
+ for file, num in files:
211
+ try:
212
+ with open(file, 'rb') as handle:
213
+ data = handle.read()
214
+ except OSError:
215
+ continue
216
+ self._context(num, [re.findall(rb'[^\n]*\n|[^\n]+$', data)])
217
+ return self
218
+
219
+ # Search for context in sources
220
+ if len(sources) and len(files):
221
+ self._context(files[-1][1], [_split(s) for s in sources])
222
+
223
+ return self
224
+
225
+ @classmethod
226
+ def throw(cls, message: Anything = None) -> NoReturn:
227
+ """::
228
+
229
+ Pyjo.Exception.object.throw('Something went wrong!')
230
+
231
+ Throw exception from the current execution context. ::
232
+
233
+ # Longer version
234
+ raise Pyjo.Exception.new('Something went wrong!').trace()
235
+ """
236
+ raise cls.new(message).trace(2)
237
+
238
+ # Renamed from Mojolicious: to_string
239
+ def to_str(self) -> str:
240
+ """::
241
+
242
+ string = e.to_str()
243
+
244
+ Render exception. Note that the output format may change as more
245
+ features are added, only the error message at the beginning is
246
+ guaranteed not to be modified to allow regex matching.
247
+ """
248
+ string = self.message
249
+
250
+ frames = self.frames
251
+ if not string.endswith('\n'):
252
+ string += f' at {frames[0].filename} line {frames[0].lineno}.\n' if len(frames) else '\n'
253
+ if not self.verbose:
254
+ return string
255
+
256
+ line = self.line
257
+ if len(line):
258
+ string += 'Context:\n'
259
+ string += ''.join(f' {n[0]}: {n[1]}\n' for n in self.lines_before)
260
+ string += f' {line[0]}: {line[1]}\n'
261
+ string += ''.join(f' {n[0]}: {n[1]}\n' for n in self.lines_after)
262
+
263
+ if len(frames):
264
+ string += 'Traceback (most recent call first):\n'
265
+ string += ''.join(f' File "{f.filename}", line {f.lineno}, in "{f.name}"\n' for f in frames)
266
+
267
+ return string
268
+
269
+ def trace(self, start: int = 1) -> Self:
270
+ """::
271
+
272
+ e = e.trace()
273
+ e = e.trace(skip)
274
+
275
+ Generate stack trace and store all :attr:`frames`, defaults to skipping
276
+ ``1`` call frame. ::
277
+
278
+ # Skip 3 call frames
279
+ e.trace(3)
280
+
281
+ # Skip no call frames
282
+ e.trace(0)
283
+ """
284
+ try:
285
+ frame = sys._getframe(start) # pyright: ignore[reportPrivateUsage]
286
+ except ValueError:
287
+ self.frames = []
288
+ else:
289
+ self.frames = list(reversed(traceback.extract_stack(frame)))
290
+ return self
291
+
292
+ # Left out from Mojolicious: with_roles, as Pyjoyment has no roles
293
+
294
+ def _context(self, num: int, sources: list[list[bytes]] | list[list[str]]) -> None:
295
+
296
+ # Line
297
+ if not 0 <= num - 1 < len(sources[0]):
298
+ return
299
+ self.line = [num]
300
+ for s in sources:
301
+ self.line.append(_line(s, num - 1))
302
+
303
+ # Before
304
+ for i in range(2, 7):
305
+ if (previous := num - i) < 0:
306
+ break
307
+ self.lines_before.insert(0, [previous + 1, *(_line(s, previous) for s in sources)])
308
+
309
+ # After
310
+ for i in range(5):
311
+ if (following := num + i) < 0 or following >= len(sources[0]):
312
+ continue
313
+ self.lines_after.append([following + 1, *(_line(s, following) for s in sources)])
314
+
315
+
316
+ def check(
317
+ err: Anything, spec: list[tuple[Pattern | list[Pattern] | tuple[Pattern, ...], Callable[[Any], Anything]]]
318
+ ) -> bool | None:
319
+ """::
320
+
321
+ boolean = check(err, [(MyApp_X_Foo, lambda e: ...)])
322
+
323
+ Process exceptions by dispatching them to handlers with one or more
324
+ matching conditions. Exceptions that could not be handled will be raised
325
+ again automatically. ::
326
+
327
+ # Handle various types of exceptions
328
+ try:
329
+ dangerous_code()
330
+ except Exception as e:
331
+ check(e, [
332
+ (MyApp_X_Foo, lambda e: print(f'Foo: {e}')),
333
+ (re.compile(r'^Could not open'), lambda e: print(f'Open error: {e}')),
334
+ ('default', lambda e: print(f'Something went wrong: {e}')),
335
+ ])
336
+
337
+ Matching conditions can be classes, or their names, for checks with
338
+ :func:`isinstance` on exception objects, or regular expressions to match
339
+ string exceptions and stringified exception objects. The matching
340
+ exception will be the argument passed to the callback. ::
341
+
342
+ # Catch MyApp_X_Foo object or a specific string exception
343
+ try:
344
+ dangerous_code()
345
+ except Exception as e:
346
+ check(e, [
347
+ (MyApp_X_Foo, lambda e: print(f'Foo: {e}')),
348
+ (re.compile(r'^Could not open'), lambda e: print(f'Open error: {e}')),
349
+ ])
350
+
351
+ A list can be used to share the same handler with multiple conditions, of
352
+ which only one needs to match. And since exception handlers are just
353
+ callbacks, they can also throw their own exceptions. ::
354
+
355
+ # Handle MyApp_X_Foo and MyApp_X_Bar the same
356
+ try:
357
+ dangerous_code()
358
+ except Exception as e:
359
+ check(e, [
360
+ ([MyApp_X_Foo, MyApp_X_Bar], lambda e: Pyjo.Exception.object.throw(f'Foo/Bar: {e}')),
361
+ ])
362
+
363
+ There is currently only one keyword you can use to set special handlers.
364
+ The ``default`` handler is used when no other handler matched. ::
365
+
366
+ # Use "default" to catch everything
367
+ try:
368
+ dangerous_code()
369
+ except Exception as e:
370
+ check(e, [
371
+ ('default', lambda e: print(f'Error: {e}')),
372
+ ])
373
+ """
374
+ if not err:
375
+ return None
376
+
377
+ # The spec is checked as Mojo checks it, for the caller without type hints
378
+ pairs = cast(Anything, spec)
379
+ if not isinstance(pairs, list) or not all(
380
+ isinstance(p, tuple) and len(cast(tuple[Any, ...], p)) == 2 for p in cast(list[Any], pairs)
381
+ ):
382
+ # Unlike Mojolicious: Mojo asks for an array reference
383
+ raise TypeError('List of pattern/handler pairs required to dispatch exceptions')
384
+
385
+ default = handler = None
386
+ is_obj, string = not isinstance(err, str), str(err)
387
+ for checks, cb in spec:
388
+ if checks == 'default':
389
+ default = cb
390
+ continue
391
+
392
+ for c in checks if isinstance(checks, (list, tuple)) else [checks]:
393
+ is_re = isinstance(c, re.Pattern)
394
+ if is_obj and not is_re and _isa(err, c):
395
+ handler = cb
396
+ break
397
+ if is_re and c.search(string):
398
+ handler = cb
399
+ break
400
+ if handler is not None:
401
+ break
402
+
403
+ # Rethrow if no handler could be found
404
+ if (handler := handler or default) is None:
405
+ raise err if isinstance(err, BaseException) else Pyjo_Exception(err)
406
+ handler(err)
407
+
408
+ return True
409
+
410
+
411
+ def throw(first: Anything, *rest: Anything) -> NoReturn:
412
+ """::
413
+
414
+ throw('Something went wrong!')
415
+ throw('MyApp.X.Foo', 'Something went wrong!')
416
+ throw(MyApp_X_Foo, 'Something went wrong!')
417
+
418
+ Raise a :mod:`Pyjo.Exception`, if the class does not exist yet, one is
419
+ created as a :mod:`Pyjo.Exception` subclass on demand. ``raise_`` is an
420
+ alias, the name of Mojolicious with an underscore.
421
+ """
422
+ # Renamed from Mojolicious: raise, a keyword of Python
423
+ name, err = (first, rest[0]) if len(rest) else (Pyjo_Exception, first)
424
+ cls = _class(name) if isinstance(name, str) else name
425
+ if not (isinstance(cls, type) and issubclass(cls, Pyjo_Exception)):
426
+ raise TypeError(f'{_name(name)} is not a Pyjo.Exception subclass')
427
+ raise cls.new(err).trace(2)
428
+
429
+
430
+ # The name of Mojolicious, with an underscore as raise is a keyword of Python
431
+ raise_ = throw
432
+
433
+
434
+ # Mojo makes the class with eval
435
+ def _class(name: str) -> Anything:
436
+ if name in _CLASSES:
437
+ return _CLASSES[name]
438
+
439
+ # The longest prefix that is a module, and the rest its attributes
440
+ parts = name.split('.')
441
+ for i in range(len(parts) - 1, 0, -1):
442
+ try:
443
+ found: Anything = importlib.import_module('.'.join(parts[:i]))
444
+ except ImportError:
445
+ continue
446
+ try:
447
+ for attr in parts[i:]:
448
+ found = getattr(found, attr)
449
+ except AttributeError:
450
+ break
451
+ return found
452
+
453
+ module, _, qualname = name.rpartition('.')
454
+ cls = type(qualname, (Pyjo_Exception,), {'__module__': module or __name__, '__qualname__': qualname})
455
+ _CLASSES[name] = cls
456
+ return cls
457
+
458
+
459
+ _CLASSES: dict[str, type[Pyjo_Exception]] = {}
460
+
461
+
462
+ def _isa(err: Anything, cls: Pattern) -> bool:
463
+ if isinstance(cls, type):
464
+ return isinstance(err, cls)
465
+ return any(cls in (c.__qualname__, f'{c.__module__}.{c.__qualname__}') for c in type(err).__mro__)
466
+
467
+
468
+ # A line of a source, which is empty where an additional source is shorter, as Perl reads undef there
469
+ def _line(source: list[bytes] | list[str], i: int) -> str:
470
+ if i >= len(source):
471
+ return ''
472
+ line = source[i]
473
+ if isinstance(line, bytes):
474
+ decoded = decode('UTF-8', line)
475
+ line = decoded if decoded is not None else line.decode('latin-1')
476
+ return line[:-1] if line.endswith('\n') else line
477
+
478
+
479
+ def _name(cls: Anything) -> str:
480
+ if isinstance(cls, type):
481
+ return f'{cls.__module__}.{cls.__qualname__}'
482
+ return str(cls)
483
+
484
+
485
+ # Perl's split /\n/, which drops the empty lines at the end
486
+ def _split(source: str) -> list[str]:
487
+ lines = source.split('\n')
488
+ while len(lines) and lines[-1] == '':
489
+ lines.pop()
490
+ return lines
491
+
492
+
493
+ new = Pyjo_Exception.new
494
+ object = Pyjo_Exception