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/Template.py ADDED
@@ -0,0 +1,1290 @@
1
+ r"""
2
+ Pyjo.Template - Python-ish templates
3
+ ====================================
4
+ ::
5
+
6
+ from Pyjo.Template import Template
7
+
8
+ # Use Python modules
9
+ mt = Template()
10
+ print(mt.render('''
11
+ % import time
12
+ <div>
13
+ % now = time.gmtime(0)
14
+ Time: <%= time.strftime('%H:%M:%S', now) %>
15
+ </div>
16
+ '''))
17
+
18
+ # Or import the module, as in Google Python Style Guide
19
+
20
+ import Pyjo.Template
21
+
22
+ # Use Python modules
23
+ mt = Pyjo.Template.new()
24
+ print(mt.render('''
25
+ % import time
26
+ <div>
27
+ % now = time.gmtime(0)
28
+ Time: <%= time.strftime('%H:%M:%S', now) %>
29
+ </div>
30
+ '''))
31
+
32
+ # Render with arguments
33
+ print(mt.render('''
34
+ % numbers, title = args
35
+ <div>
36
+ <h1><%= title %></h1>
37
+ % for i in numbers {
38
+ Test <%= i %>
39
+ % }
40
+ </div>
41
+ ''', range(1, 14), 'Hello World!'))
42
+
43
+ # Render with named variables
44
+ print(Pyjo.Template.new(vars=True).render('''
45
+ <div>
46
+ <h1><%= title %></h1>
47
+ %= 5 + 5
48
+ </div>
49
+ ''', {'title': 'Hello World!'}))
50
+
51
+ :mod:`Pyjo.Template` is a minimalistic, fast, and very Python-ish template
52
+ engine, designed specifically for all those small tasks that come up during
53
+ big projects. Like preprocessing a configuration file, generating text from
54
+ heredocs and stuff like that.
55
+
56
+ Syntax
57
+ ------
58
+
59
+ ::
60
+
61
+ <% Python code %>
62
+ <%= Python expression, replaced with result %>
63
+ <%== Python expression, replaced with XML escaped result %>
64
+ <%# Comment, useful for debugging %>
65
+ <%% Replaced with "<%", useful for generating templates %>
66
+ % Python code line, treated as "<% line =%>" (explained later)
67
+ %= Python expression line, treated as "<%= line %>"
68
+ %== Python expression line, treated as "<%== line %>"
69
+ %# Comment line, useful for debugging
70
+ %% Replaced with "%", useful for generating templates
71
+
72
+ Escaping behavior can be reversed with the :attr:`~Pyjo_Template.auto_escape`
73
+ attribute. ::
74
+
75
+ <%= Python expression, replaced with XML escaped result %>
76
+ <%== Python expression, replaced with result %>
77
+
78
+ Markup, an object with the method ``__html__``, as :mod:`Pyjo.String.HTML`,
79
+ is always excluded from automatic escaping, and a value is rendered as an
80
+ interpolation of :mod:`Pyjo.String.Template`: ``None`` is nothing, a template
81
+ is markup, and a list is rendered item by item. ::
82
+
83
+ % from Pyjo.String.HTML import html
84
+ <%= html('<div>excluded!</div>') %>
85
+
86
+ The indentation of the template does not matter: a code line that ends with
87
+ ``:`` opens a block, which ``end`` closes, and a line that starts with
88
+ ``elif``, ``else``, ``except``, ``finally`` or ``case`` closes the block
89
+ before it and opens the next one. ``end`` closes the whole ``match`` after its
90
+ last ``case``. Braces can open and close a block too, which makes
91
+ one-liners easy: ``{`` at the end of a line that starts with a keyword of a
92
+ block opens it, and ``}`` at the start of a line closes it, as in ``} else
93
+ {``. Any other brace is the one of Python, so a ``dict`` or a ``set`` that
94
+ starts at the end of such a line is put in parentheses. ::
95
+
96
+ % for name in ['Baerbel', 'Wolfgang']:
97
+ % if name.startswith('B'):
98
+ <b><%= name %></b>
99
+ % else:
100
+ <i><%= name %></i>
101
+ % end
102
+ % end
103
+ <% for i in range(3) { %><%= i %><% } %>
104
+
105
+ Whitespace characters around tags can be trimmed by adding an additional equal
106
+ sign to the end of a tag. ::
107
+
108
+ <% for i in range(1, 4) { %>
109
+ <%= 'Trim all whitespace characters around this expression' =%>
110
+ <% } %>
111
+
112
+ Newline characters can be escaped with a backslash. ::
113
+
114
+ This is <%= 1 + 1 %> a\
115
+ single line
116
+
117
+ And a backslash in front of a newline character can be escaped with another
118
+ backslash. ::
119
+
120
+ This will <%= 1 + 1 %> result\\
121
+ in multiple\\
122
+ lines
123
+
124
+ A newline character gets appended automatically to every template, unless the
125
+ last character is a backslash. And empty lines at the end of a template are
126
+ ignored. ::
127
+
128
+ There is <%= 1 + 1 %> no newline at the end here\
129
+
130
+ You can capture whole template blocks for reuse later with the ``begin`` and
131
+ ``end`` keywords. Just be aware that both keywords are part of the surrounding
132
+ tag and not actual Python code, so there can only be whitespace after
133
+ ``begin`` and before ``end``. ::
134
+
135
+ <% block = begin %>
136
+ <% name = args[0] =%>
137
+ Hello <%= name %>.
138
+ <% end %>
139
+ <%= block('Baerbel') %>
140
+ <%= block('Wolfgang') %>
141
+
142
+ Python lines can also be indented freely. ::
143
+
144
+ % block = begin
145
+ % name = args[0]
146
+ Hello <%= name %>.
147
+ % end
148
+ %= block('Baerbel')
149
+ %= block('Wolfgang')
150
+
151
+ :mod:`Pyjo.Template` templates get compiled to a Python function, that means
152
+ you can access arguments simply via ``args``. ::
153
+
154
+ % foo, bar = args
155
+ % x = args[0]
156
+ test 123 <%= foo %>
157
+
158
+ The compilation of templates to Python code can make debugging a bit tricky,
159
+ but :mod:`Pyjo.Template` will return :mod:`Pyjo.Exception` objects that
160
+ stringify to error messages with context. ::
161
+
162
+ name 'xx' is not defined at template line 4.
163
+ Context:
164
+ 1: <html>
165
+ 2: </head>
166
+ 3: <body>
167
+ 4: % i = 2; xx
168
+ 5: %= i * 2
169
+ 6: </body>
170
+ Traceback (most recent call first):
171
+ File "template", line 4, in "_template"
172
+ File "path/to/Pyjo/Template.py", line 123, in "process"
173
+ File "path/to/myapp.py", line 123, in "<module>"
174
+
175
+ Debugging
176
+ ---------
177
+
178
+ You can set the ``PYJO_TEMPLATE_DEBUG`` environment variable to get some
179
+ advanced diagnostics information printed to ``STDERR``. ::
180
+
181
+ PYJO_TEMPLATE_DEBUG=1
182
+
183
+ See also
184
+ --------
185
+
186
+ :mod:`Pyjoyment`, https://mojolicious.org.
187
+
188
+ Ported from `Mojo::Template <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Template.pm>`_
189
+ of Mojolicious 9.49.
190
+
191
+ Classes
192
+ -------
193
+ """
194
+
195
+ import Pyjo.Base
196
+ import Pyjo.Exception
197
+ import Pyjo.File
198
+ import Pyjo.String.HTML
199
+ import Pyjo.String.Template
200
+
201
+ from Pyjo.String.Unicode import split
202
+ from Pyjo.Util import decode
203
+
204
+ import builtins
205
+ import inspect
206
+ import keyword
207
+ import os
208
+ import re
209
+ import sys
210
+ import traceback
211
+ import types
212
+
213
+ from collections.abc import Callable, Iterator
214
+ from dataclasses import dataclass
215
+ from typing import Any, Self, cast
216
+
217
+
218
+ type Anything = builtins.object
219
+
220
+ DEBUG = bool(os.environ.get('PYJO_TEMPLATE_DEBUG'))
221
+
222
+ # The keywords that open a block with a brace, and those that continue the block before
223
+ BLOCK_KEYWORDS = {
224
+ 'async',
225
+ 'class',
226
+ 'case',
227
+ 'def',
228
+ 'elif',
229
+ 'else',
230
+ 'except',
231
+ 'finally',
232
+ 'for',
233
+ 'if',
234
+ 'match',
235
+ 'try',
236
+ 'while',
237
+ 'with',
238
+ }
239
+ CONTINUE_KEYWORDS = {'case', 'elif', 'else', 'except', 'finally'}
240
+
241
+ RE_KEYWORD = re.compile(r'\s*(?:\}\s*)?([A-Za-z_]\w*)')
242
+ RE_STRING_PREFIX = re.compile(r'(?<![\w])([rRbBfFtTuU]{1,2})\Z')
243
+
244
+ _SANDBOXES: dict[str, types.ModuleType] = {}
245
+
246
+
247
+ class Pyjo_Template(Pyjo.Base.object):
248
+ """
249
+ :mod:`Pyjo.Template` inherits all methods from :mod:`Pyjo.Base` and
250
+ implements the following new ones.
251
+ """
252
+
253
+ def __init__(
254
+ self,
255
+ *,
256
+ append: str = '',
257
+ auto_escape: bool | None = None,
258
+ capture_end: str = 'end',
259
+ capture_start: str = 'begin',
260
+ code: str = '',
261
+ comment_mark: str = '#',
262
+ compiled: Callable[..., Any] | None = None,
263
+ encoding: str | None = 'UTF-8',
264
+ escape: Callable[[Any], str] | None = None,
265
+ escape_mark: str = '=',
266
+ expression_mark: str = '=',
267
+ line_start: str = '%',
268
+ name: str | None = None,
269
+ namespace: str = 'Pyjo.Template.Sandbox',
270
+ prepend: str = '',
271
+ replace_mark: str = '%',
272
+ tag_end: str = '%>',
273
+ tag_start: str = '<%',
274
+ tree: list[list[str]] | None = None,
275
+ trim_mark: str = '=',
276
+ unparsed: str = '',
277
+ vars: bool | None = None,
278
+ ) -> None:
279
+ """::
280
+
281
+ mt = Pyjo.Template.new()
282
+ mt = Pyjo.Template.new(auto_escape=True)
283
+
284
+ Construct a new :mod:`Pyjo.Template` object.
285
+ """
286
+ self.auto_escape = auto_escape
287
+ """::
288
+
289
+ boolean = mt.auto_escape
290
+ mt.auto_escape = boolean
291
+
292
+ Activate automatic escaping. ::
293
+
294
+ # "&lt;html&gt;"
295
+ Pyjo.Template.new(auto_escape=True).render("<%= '<html>' %>")
296
+ """
297
+
298
+ self.append = append
299
+ """::
300
+
301
+ code = mt.append
302
+ mt.append = 'print("Processed template")'
303
+
304
+ Append Python code to compiled template, as lines of their own.
305
+ """
306
+
307
+ self.capture_end = capture_end
308
+ """::
309
+
310
+ end = mt.capture_end
311
+ mt.capture_end = 'end'
312
+
313
+ Keyword indicating the end of a capture block, defaults to ``end``. ::
314
+
315
+ <% block = begin %>
316
+ Some data!
317
+ <% end %>
318
+ """
319
+
320
+ self.capture_start = capture_start
321
+ """::
322
+
323
+ start = mt.capture_start
324
+ mt.capture_start = 'begin'
325
+
326
+ Keyword indicating the start of a capture block, defaults to
327
+ ``begin``. ::
328
+
329
+ <% block = begin %>
330
+ Some data!
331
+ <% end %>
332
+ """
333
+
334
+ self.code = code
335
+ """::
336
+
337
+ code = mt.code
338
+ mt.code = code
339
+
340
+ Python code for template if available.
341
+ """
342
+
343
+ self.comment_mark = comment_mark
344
+ """::
345
+
346
+ mark = mt.comment_mark
347
+ mt.comment_mark = '#'
348
+
349
+ Character indicating the start of a comment, defaults to ``#``. ::
350
+
351
+ <%# This is a comment %>
352
+ """
353
+
354
+ self.compiled = compiled
355
+ """::
356
+
357
+ compiled = mt.compiled
358
+ mt.compiled = compiled
359
+
360
+ Compiled template code if available.
361
+ """
362
+
363
+ self.encoding = encoding
364
+ """::
365
+
366
+ encoding = mt.encoding
367
+ mt.encoding = 'UTF-8'
368
+
369
+ Encoding used for template files, defaults to ``UTF-8``.
370
+ """
371
+
372
+ # Unlike Mojolicious: the values are escaped as the interpolations of a template, as markup is what has
373
+ # __html__, not every Mojo::ByteStream
374
+ self.escape: Callable[[Any], str] = escape or Pyjo.String.Template.escape
375
+ """::
376
+
377
+ cb = mt.escape
378
+ mt.escape = lambda value: ...
379
+
380
+ A callback used to escape the results of escaped expressions, defaults
381
+ to :func:`Pyjo.String.Template.escape`. ::
382
+
383
+ mt.escape = lambda value: str(value)[::-1]
384
+ """
385
+
386
+ self.escape_mark = escape_mark
387
+ """::
388
+
389
+ mark = mt.escape_mark
390
+ mt.escape_mark = '='
391
+
392
+ Character indicating the start of an escaped expression, defaults to
393
+ ``=``. ::
394
+
395
+ <%== foo %>
396
+ """
397
+
398
+ self.expression_mark = expression_mark
399
+ """::
400
+
401
+ mark = mt.expression_mark
402
+ mt.expression_mark = '='
403
+
404
+ Character indicating the start of an expression, defaults to ``=``. ::
405
+
406
+ <%= foo %>
407
+ """
408
+
409
+ self.line_start = line_start
410
+ """::
411
+
412
+ start = mt.line_start
413
+ mt.line_start = '%'
414
+
415
+ Character indicating the start of a code line, defaults to ``%``. ::
416
+
417
+ % foo = 23
418
+ """
419
+
420
+ self._name = name
421
+
422
+ # Unlike Mojolicious: a namespace is a module, which Python has as Perl has a package
423
+ self.namespace = namespace
424
+ """::
425
+
426
+ namespace = mt.namespace
427
+ mt.namespace = 'main'
428
+
429
+ Namespace used to compile templates, defaults to
430
+ ``Pyjo.Template.Sandbox``: the module of the name, if it is loaded, or
431
+ a module of its own. Note that namespaces should only be shared very
432
+ carefully between templates, since the globals of the module will not
433
+ be cleared automatically.
434
+ """
435
+
436
+ self.prepend = prepend
437
+ """::
438
+
439
+ code = mt.prepend
440
+ mt.prepend = 'this = args[0]'
441
+
442
+ Prepend Python code to compiled template, as lines of their own.
443
+ """
444
+
445
+ self.replace_mark = replace_mark
446
+ """::
447
+
448
+ mark = mt.replace_mark
449
+ mt.replace_mark = '%'
450
+
451
+ Character used for escaping the start of a tag or line, defaults to
452
+ ``%``. ::
453
+
454
+ <%% foo = 23 %>
455
+ """
456
+
457
+ self.tag_start = tag_start
458
+ """::
459
+
460
+ start = mt.tag_start
461
+ mt.tag_start = '<%'
462
+
463
+ Characters indicating the start of a tag, defaults to ``<%``. ::
464
+
465
+ <% foo = 23 %>
466
+ """
467
+
468
+ self.tag_end = tag_end
469
+ """::
470
+
471
+ end = mt.tag_end
472
+ mt.tag_end = '%>'
473
+
474
+ Characters indicating the end of a tag, defaults to ``%>``. ::
475
+
476
+ <%= foo %>
477
+ """
478
+
479
+ self.tree: list[list[str]] = tree if tree is not None else []
480
+ """::
481
+
482
+ tree = mt.tree
483
+ mt.tree = [['text', 'foo'], ['line']]
484
+
485
+ Template in parsed form if available. Note that this structure should
486
+ only be used very carefully since it is very dynamic.
487
+ """
488
+
489
+ self.trim_mark = trim_mark
490
+ """::
491
+
492
+ mark = mt.trim_mark
493
+ mt.trim_mark = '-'
494
+
495
+ Character activating automatic whitespace trimming, defaults to
496
+ ``=``. ::
497
+
498
+ <%= foo =%>
499
+ """
500
+
501
+ self.unparsed = unparsed
502
+ """::
503
+
504
+ unparsed = mt.unparsed
505
+ mt.unparsed = '<%= 1 + 1 %>'
506
+
507
+ Raw unparsed template if available.
508
+ """
509
+
510
+ self.vars = vars
511
+ """::
512
+
513
+ boolean = mt.vars
514
+ mt.vars = boolean
515
+
516
+ Instead of a list of values, use a dict with named variables to pass
517
+ data to templates. ::
518
+
519
+ # "works!"
520
+ Pyjo.Template.new(vars=True).render('<%= test %>!', {'test': 'works'})
521
+ """
522
+
523
+ # Not in Mojolicious: the template lines of the lines of the compiled code, and its code objects, which find
524
+ # the frames of the template in a traceback
525
+ self._body: list[int] = []
526
+ self._lines: list[int] = []
527
+ self._codes: set[types.CodeType] = set()
528
+ self._projection: list[str] = []
529
+
530
+ @property
531
+ def name(self) -> str:
532
+ """::
533
+
534
+ name = mt.name
535
+ mt.name = 'foo.ep'
536
+
537
+ Name of template currently being processed, defaults to ``template``.
538
+ """
539
+ if self._name is None:
540
+ self._name = 'template'
541
+ return self._name
542
+
543
+ @name.setter
544
+ def name(self, value: str) -> None:
545
+ self._name = value
546
+
547
+ def parse(self, template: str) -> Self:
548
+ """::
549
+
550
+ mt = mt.parse('<%= 1 + 1 %>')
551
+
552
+ Parse template into :attr:`tree`.
553
+ """
554
+
555
+ # Clean start
556
+ tree: list[list[str]] = []
557
+ self.unparsed = template
558
+ self.tree = tree
559
+ self.compiled = None
560
+
561
+ tag = self.tag_start
562
+ replace = self.replace_mark
563
+ expr = self.expression_mark
564
+ escp = self.escape_mark
565
+ cpen = self.capture_end
566
+ cmnt = self.comment_mark
567
+ cpst = self.capture_start
568
+ trim = self.trim_mark
569
+ end = self.tag_end
570
+ start = self.line_start
571
+
572
+ q = re.escape
573
+ line_re = re.compile(rf'^(\s*){q(start)}(?:({q(replace)})|({q(cmnt)})|({q(expr)}))?(.*)$')
574
+ token_re = re.compile(
575
+ '('
576
+ # Replace
577
+ rf'{q(tag)}(?:{q(replace)}|{q(cmnt)})'
578
+ # Expression
579
+ rf'|{q(tag + expr)}(?:{q(escp)})?(?:\s*{q(cpen)}(?!\w))?'
580
+ # Code
581
+ rf'|{q(tag)}(?:\s*{q(cpen)}(?!\w))?'
582
+ # End
583
+ rf'|(?:(?<!\w){q(cpst)}\s*)?(?:{q(trim)})?{q(end)}'
584
+ ')'
585
+ )
586
+ cpen_re = re.compile(rf'^{q(tag)}(?:{q(expr)})?(?:{q(escp)})?\s*{q(cpen)}(.*)$')
587
+ end_re = re.compile(rf'^(?:({q(cpst)})\s*)?({q(trim)})?{q(end)}$')
588
+
589
+ # Split lines
590
+ op = 'text'
591
+ trimming: str | None = None
592
+ capture = False
593
+ for line in split('\n', template):
594
+ line = line or ''
595
+
596
+ # Turn Python line into mixed line
597
+ if op == 'text' and (m := line_re.match(line)):
598
+ # Escaped start
599
+ if m[2]:
600
+ line = f'{m[1]}{start}{m[5]}'
601
+
602
+ # Comment
603
+ elif m[3]:
604
+ line = f'{tag}{m[3]} {trim}{end}'
605
+
606
+ # Expression or code
607
+ else:
608
+ line = f'{m[1]}{tag}{m[4]}{m[5]} {end}' if m[4] else f'{tag}{m[5]} {trim}{end}'
609
+
610
+ # Escaped line ending
611
+ if line.endswith('\\\\'):
612
+ line = line[:-2] + '\\\n'
613
+ elif line.endswith('\\'):
614
+ line = line[:-1]
615
+ else:
616
+ line += '\n'
617
+
618
+ # Mixed line
619
+ for token in split(token_re, line):
620
+ token = token or ''
621
+
622
+ # Capture end
623
+ if m := cpen_re.match(token):
624
+ token, capture = f'{tag}{m[1]}', True
625
+
626
+ # End
627
+ if op != 'text' and (m := end_re.match(token)):
628
+ # Capture start
629
+ if m[1]:
630
+ tree.insert(len(tree) - 1, ['cpst'])
631
+
632
+ # Trim left side
633
+ if (trimming := m[2]) and len(tree) > 1:
634
+ _trim(tree)
635
+
636
+ # Hint at end
637
+ op = 'text'
638
+ tree.append([op, ''])
639
+
640
+ # Code
641
+ elif token == tag:
642
+ op = 'code'
643
+
644
+ # Expression
645
+ elif token == f'{tag}{expr}':
646
+ op = 'expr'
647
+
648
+ # Expression that needs to be escaped
649
+ elif token == f'{tag}{expr}{escp}':
650
+ op = 'escp'
651
+
652
+ # Comment
653
+ elif token == f'{tag}{cmnt}':
654
+ op = 'cmnt'
655
+
656
+ # Text (comments are just ignored)
657
+ elif op != 'cmnt':
658
+ # Replace
659
+ if token == f'{tag}{replace}':
660
+ token = tag
661
+
662
+ # Trim right side (convert whitespace to line noise)
663
+ if trimming and (m := re.match(r'\s+', token)):
664
+ tree.append(['code', m[0]])
665
+ token = token[m.end() :]
666
+ trimming = None
667
+
668
+ # Token (with optional capture end)
669
+ if capture:
670
+ tree.append(['cpen'])
671
+ tree.append([op, token])
672
+ capture = False
673
+
674
+ # Optimize successive text lines separated by a newline
675
+ if (
676
+ (_at(tree, -4) and tree[-4][0] != 'line')
677
+ or (not _at(tree, -3) or tree[-3][0] != 'text' or not tree[-3][1].endswith('\n'))
678
+ or ((_at(tree, -2) or [''])[0] != 'line' or tree[-1][0] != 'text')
679
+ ):
680
+ tree.append(['line'])
681
+ continue
682
+ tree[-3][1] += tree.pop()[1]
683
+
684
+ return self
685
+
686
+ def process(self, *args: Anything) -> Any:
687
+ """::
688
+
689
+ output = mt.process()
690
+ output = mt.process(*args)
691
+ output = mt.process({'foo': 'bar'})
692
+
693
+ Process previously parsed template and return the result, or a
694
+ :mod:`Pyjo.Exception` object if rendering failed. ::
695
+
696
+ # Parse and process
697
+ print(Pyjo.Template.new().parse('Hello <%= args[0] %>').process('Bender'))
698
+
699
+ # Reuse template (for much better performance)
700
+ mt = Pyjo.Template.new()
701
+ print(mt.render('Hello <%= args[0] %>!', 'Bender'))
702
+ print(mt.process('Fry'))
703
+ print(mt.process('Leela'))
704
+ """
705
+
706
+ # Use a local stack trace for compile exceptions
707
+ compiled = self.compiled
708
+ if compiled is None:
709
+ try:
710
+ compiled = self._compile()._eval(args)
711
+ except _CompileError as e:
712
+ exception = Pyjo.Exception.new(e.message).inspect(self.unparsed, '\n'.join(self._projection))
713
+ exception.trace().verbose = True
714
+ return exception
715
+ self.compiled = compiled
716
+
717
+ # Unlike Mojolicious: the result is markup, as a template renders its own markup
718
+ try:
719
+ return Pyjo.String.HTML.new(compiled(*args))
720
+
721
+ # An exception of Pyjoyment passes through, as Mojo passes on an object
722
+ except Pyjo.Exception.object as e:
723
+ return e
724
+
725
+ # Use a real stack trace for normal exceptions
726
+ except Exception as e:
727
+ exception = Pyjo.Exception.new(e)
728
+ caller = inspect.currentframe()
729
+ exception.frames = self._frames(e) + traceback.extract_stack(caller and caller.f_back)[::-1]
730
+ del caller
731
+ exception.inspect(self.unparsed, '\n'.join(self._projection)).verbose = True
732
+ return exception
733
+
734
+ def render(self, template: str, *args: Anything) -> Any:
735
+ """::
736
+
737
+ output = mt.render('<%= 1 + 1 %>')
738
+ output = mt.render('<%= args[0] + args[1] %>', *args)
739
+ output = mt.render('<%= foo %>', {'foo': 'bar'})
740
+
741
+ Render template and return the result, or a :mod:`Pyjo.Exception`
742
+ object if rendering failed. ::
743
+
744
+ # Longer version
745
+ output = mt.parse('<%= 1 + 1 %>').process()
746
+
747
+ # Render with arguments
748
+ print(Pyjo.Template.new().render('<%= args[0] %>', 'bar'))
749
+
750
+ # Render with named variables
751
+ print(Pyjo.Template.new(vars=True).render('<%= foo %>', {'foo': 'bar'}))
752
+ """
753
+ return self.parse(template).process(*args)
754
+
755
+ def render_file(self, path: Anything, *args: Anything) -> Any:
756
+ """::
757
+
758
+ output = mt.render_file('/tmp/foo.ep')
759
+ output = mt.render_file('/tmp/foo.ep', *args)
760
+ output = mt.render_file('/tmp/bar.ep', {'foo': 'bar'})
761
+
762
+ Same as :meth:`render`, but renders a template file.
763
+ """
764
+ if self._name is None:
765
+ self.name = str(path)
766
+ data = Pyjo.File.path(path).slurp()
767
+ assert isinstance(data, bytes)
768
+ if (encoding := self.encoding) is None:
769
+ template = data.decode('latin-1')
770
+ elif (template := decode(encoding, data)) is None:
771
+ raise Pyjo.Exception.new(f'Template "{path}" has invalid encoding')
772
+
773
+ return self.render(template, *args)
774
+
775
+ # Unlike Mojolicious: Python needs the indentation of its blocks, so the code is not line for line the template,
776
+ # and the template lines of the code are kept for the tracebacks
777
+ def _compile(self) -> Self:
778
+ compiler = _Compiler(self.auto_escape)
779
+ try:
780
+ compiler.compile(self.tree)
781
+ finally:
782
+ self._projection = compiler.projection
783
+ self._body = [line for _, line in compiler.out]
784
+ self.code = '\n'.join(text for text, _ in compiler.out)
785
+ self.tree = []
786
+ return self
787
+
788
+ def _eval(self, args: tuple[Anything, ...]) -> Callable[..., Any]:
789
+ name = self._filename()
790
+
791
+ # Variables
792
+ variables: list[str] = []
793
+ if self.vars and len(args) and isinstance(args[0], dict):
794
+ keys = cast(dict[Anything, Anything], args[0]).keys()
795
+ names = [k for k in keys if isinstance(k, str) and k.isidentifier() and not keyword.iskeyword(k)]
796
+ if len(names):
797
+ variables = [f'{k} = args[0].get({k!r})' for k in names] + ['args = args[1:]']
798
+
799
+ # Wrap lines, the code of prepend and append as lines of their own, and the output as a list, which the markup
800
+ # of Pyjo.String.HTML does not turn into markup as a string would
801
+ head = [
802
+ 'def _wrap(_escape, _str, _html):',
803
+ ' def _template(*args):',
804
+ ' _O = []',
805
+ *(f' {line}' for line in [*self.prepend.split('\n'), *variables] if line.strip()),
806
+ ]
807
+ tail = [
808
+ *(f' {line}' for line in self.append.split('\n') if line.strip()),
809
+ " return ''.join(_O)",
810
+ ' return _template',
811
+ ]
812
+ body = self.code.split('\n') if len(self._body) else []
813
+ self._lines = [1] * len(head) + self._body + [max(self._body, default=1) + 1] * len(tail)
814
+ code = '\n'.join([*head, *body, *tail])
815
+
816
+ if DEBUG:
817
+ sys.stderr.write(f'-- Code for {name}\n{code}\n\n')
818
+
819
+ try:
820
+ compiled = compile(code, name, 'exec')
821
+ except SyntaxError as e:
822
+ raise _CompileError(f'{e.msg} at {name} line {self._line(e.lineno)}.\n') from None
823
+ namespace: dict[str, Any] = {}
824
+ exec(compiled, _sandbox(self.namespace), namespace)
825
+ self._codes = set(_code_objects(compiled))
826
+ return namespace['_wrap'](self.escape, Pyjo.String.Template.raw, Pyjo.String.HTML.new)
827
+
828
+ def _filename(self) -> str:
829
+ return self.name.replace('"', '')
830
+
831
+ def _frames(self, e: BaseException) -> list[traceback.FrameSummary]:
832
+ frames: list[traceback.FrameSummary] = []
833
+ lines = _split(self.unparsed)
834
+ for frame, lineno in traceback.walk_tb(e.__traceback__):
835
+ code = frame.f_code
836
+ if code in self._codes:
837
+ num = self._line(lineno)
838
+ line = lines[num - 1] if 0 < num <= len(lines) else ''
839
+ frames.append(traceback.FrameSummary(self._filename(), num, code.co_name, lookup_line=False, line=line))
840
+ else:
841
+ frames.append(traceback.FrameSummary(code.co_filename, lineno, code.co_name))
842
+ return frames[::-1]
843
+
844
+ def _line(self, lineno: int | None) -> int:
845
+ if lineno is None or not len(self._lines):
846
+ return 1
847
+ return self._lines[min(max(lineno, 1), len(self._lines)) - 1]
848
+
849
+
850
+ # A block of the compiled code: a capture of begin and end, or a block of Python
851
+ @dataclass
852
+ class _Block:
853
+ keyword: str
854
+ line: int
855
+ head: tuple[str, str] | None = None
856
+ count: int = 0
857
+
858
+
859
+ class _CompileError(Exception):
860
+ def __init__(self, message: str) -> None:
861
+ super().__init__(message)
862
+ self.message = message
863
+
864
+
865
+ # A logical line of Python code, as the template has it
866
+ @dataclass
867
+ class _Logical:
868
+ text: str
869
+ start: int
870
+ brace_open: int = -1
871
+ brace_close: int = -1
872
+ colon: bool = False
873
+
874
+
875
+ class _Compiler:
876
+ def __init__(self, escape: bool | None) -> None:
877
+ self.escape = escape
878
+ self.out: list[tuple[str, int]] = []
879
+ self.projection: list[str] = []
880
+ self.blocks: list[_Block] = []
881
+ self.depth = 2
882
+ self.line = 1
883
+ self.closed = False
884
+
885
+ # Code lines of the template that are not compiled yet, as lines with their numbers
886
+ self.buffer: list[tuple[int, str]] = []
887
+
888
+ # An expression of several lines
889
+ self.expr: list[tuple[int, str]] | None = None
890
+ self.expr_op = ''
891
+
892
+ # The statement that continues after the end of a capture
893
+ self.continuation: tuple[str, str] | None = None
894
+ self.captures = 0
895
+
896
+ def compile(self, tree: list[list[str]]) -> None:
897
+ capture = multi = False
898
+ i = 0
899
+ while (i := i + 1) <= len(tree) and (following := _at(tree, i)) is not None:
900
+ op, value = (tree[i - 1] + [''])[:2]
901
+ if op == 'line':
902
+ self.line += 1
903
+ if len(self.buffer):
904
+ self.buffer.append((self.line, ''))
905
+ if self.expr is not None:
906
+ self.expr.append((self.line, ''))
907
+ continue
908
+ newline = value.endswith('\n')
909
+ if newline:
910
+ value = value[:-1]
911
+
912
+ # Capture start before a token that is not code or an expression
913
+ if capture and op not in ('code', 'expr', 'escp', 'cpst'):
914
+ self.begin('code', '')
915
+ capture = False
916
+
917
+ # Text (quote and fix line ending), and the code before it unless it is the hint at the end of a tag
918
+ if op == 'text':
919
+ if value or newline:
920
+ self.flush()
921
+ self.text(value, newline)
922
+
923
+ # Multi-line expression
924
+ elif multi and op in ('code', 'expr', 'escp'):
925
+ assert self.expr is not None
926
+ self._project(value)
927
+ self.expr[-1] = (self.expr[-1][0], self.expr[-1][1] + value)
928
+
929
+ # Code
930
+ elif op == 'code':
931
+ self._project(value)
932
+ if capture:
933
+ self.begin('code', value)
934
+ capture = False
935
+ elif self.continuation is not None:
936
+ kind, head = self.continuation
937
+ self.continuation = None
938
+ if following[0] == 'cpst':
939
+ self.continuation = (kind, head + value)
940
+ elif kind == 'code':
941
+ self.code(head + value)
942
+ else:
943
+ self.expression(kind, [(self.line, head + value)])
944
+ else:
945
+ self.code(value)
946
+
947
+ # Capture end
948
+ elif op == 'cpen':
949
+ self.flush()
950
+ self.end()
951
+
952
+ # Expression
953
+ if op in ('expr', 'escp') and not multi:
954
+ self.flush()
955
+ self._project(value)
956
+ escaped = op == 'escp' and not self.escape or op == 'expr' and self.escape
957
+ kind = 'escp' if escaped else 'expr'
958
+ if capture:
959
+ self.begin(kind, value)
960
+ capture = False
961
+ else:
962
+ self.expr, self.expr_op = [(self.line, value)], kind
963
+ if op in ('expr', 'escp', 'code') and self.expr is not None:
964
+ # Multi-line
965
+ multi = following[0] != 'text'
966
+ if not multi:
967
+ self.expression(self.expr_op, self.expr)
968
+ self.expr = None
969
+
970
+ # Capture start
971
+ if op == 'cpst':
972
+ capture = True
973
+
974
+ self.flush()
975
+ if len(self.blocks):
976
+ block = self.blocks[-1]
977
+ what = 'begin' if block.keyword == 'begin' else f'block "{block.keyword}"'
978
+ raise _CompileError(f'Missing end of {what} from line {block.line} at template line {self.line}.\n')
979
+
980
+ def begin(self, kind: str, value: str) -> None:
981
+ if self.continuation is not None:
982
+ kind, head = self.continuation
983
+ self.continuation = None
984
+ else:
985
+ head = self.flush(final=False)
986
+ self.captures += 1
987
+ name = f'_begin{self.captures}'
988
+ self.emit(f'def {name}(*args):')
989
+ self.open(_Block('begin', self.line, head=(kind, head + value + name)))
990
+ self.emit('_O = []')
991
+
992
+ def close(self) -> _Block:
993
+ block = self.blocks.pop()
994
+ if not block.count:
995
+ self.emit('pass', self.depth)
996
+ self.depth -= 1
997
+ return block
998
+
999
+ def code(self, value: str) -> None:
1000
+ if not value.strip():
1001
+ return
1002
+ if not len(self.buffer):
1003
+ self.buffer.append((self.line, ''))
1004
+ first, *rest = value.split('\n')
1005
+ num, text = self.buffer[-1]
1006
+ self.buffer[-1] = (num, text + first)
1007
+ self.buffer.extend((self.line, line) for line in rest)
1008
+
1009
+ def emit(self, text: str, depth: int | None = None, line: int | None = None) -> None:
1010
+ self.out.append(
1011
+ (' ' * 4 * (self.depth if depth is None else depth) + text, self.line if line is None else line)
1012
+ )
1013
+ if len(self.blocks):
1014
+ self.blocks[-1].count += 1
1015
+ self.closed = False
1016
+
1017
+ def end(self) -> None:
1018
+ if not len(self.blocks):
1019
+ raise _CompileError(f'Unexpected end at template line {self.line}.\n')
1020
+ if self.blocks[-1].keyword == 'begin':
1021
+ self.emit("return _html(''.join(_O))")
1022
+ block = self.close()
1023
+ if block.keyword == 'begin':
1024
+ self.continuation = block.head
1025
+ elif block.keyword == 'case' and len(self.blocks) and self.blocks[-1].keyword == 'match':
1026
+ self.close()
1027
+ self.closed = True
1028
+
1029
+ def expression(self, kind: str, parts: list[tuple[int, str]]) -> None:
1030
+ call = '_escape' if kind == 'escp' else '_str'
1031
+ (first_line, first), *rest = parts
1032
+ if not len(rest):
1033
+ self.emit(f'_O.append({call}({first.strip()}))', line=first_line)
1034
+ return
1035
+ self.emit(f'_O.append({call}(({first}', line=first_line)
1036
+ for num, part in rest:
1037
+ self.out.append((part, num))
1038
+ self.out.append((')))', rest[-1][0]))
1039
+
1040
+ def flush(self, final: bool = True) -> str:
1041
+ if not len(self.buffer):
1042
+ return ''
1043
+ logicals, tail = _scan('\n'.join(text for _, text in self.buffer), final)
1044
+ starts = [num for num, _ in self.buffer]
1045
+ self.buffer = []
1046
+ for logical in logicals:
1047
+ self.statement(logical, starts)
1048
+ return tail
1049
+
1050
+ def open(self, block: _Block) -> None:
1051
+ self.blocks.append(block)
1052
+ self.depth += 1
1053
+
1054
+ def statement(self, logical: _Logical, starts: list[int]) -> None:
1055
+ text, line = logical.text, starts[logical.start]
1056
+
1057
+ # Brace that opens a block
1058
+ brace = logical.brace_open >= 0
1059
+ if brace:
1060
+ text = text[: logical.brace_open].rstrip() + ':' + text[logical.brace_open + 1 :]
1061
+
1062
+ # Brace that closes a block
1063
+ if logical.brace_close >= 0:
1064
+ if not len(self.blocks) or self.blocks[-1].keyword == 'begin':
1065
+ raise _CompileError(f'Unexpected "}}" at template line {line}.\n')
1066
+ self.close()
1067
+ self.closed = True
1068
+ text = text[logical.brace_close + 1 :]
1069
+ if not text.strip() or text.lstrip().startswith('#'):
1070
+ return
1071
+
1072
+ # Keyword that continues the block before
1073
+ word = m[1] if (m := RE_KEYWORD.match(text)) else ''
1074
+ if word in CONTINUE_KEYWORDS and not self.closed and len(self.blocks) and self.blocks[-1].keyword != 'begin':
1075
+ if not (word == 'case' and self.blocks[-1].keyword == 'match'):
1076
+ self.close()
1077
+
1078
+ first, *rest = text.lstrip().split('\n')
1079
+ self.emit(first if len(rest) else first.rstrip(), line=line)
1080
+ for i, continued in enumerate(rest, 1):
1081
+ self.out.append((continued, starts[logical.start + i]))
1082
+ if brace or logical.colon:
1083
+ self.open(_Block(word, line))
1084
+
1085
+ def text(self, value: str, newline: bool) -> None:
1086
+ pieces = value.split('\n')
1087
+ if newline:
1088
+ pieces[-1] += '\n'
1089
+ pieces = [p + '\n' for p in pieces[:-1]] + [pieces[-1]]
1090
+ if len(pieces) == 1:
1091
+ if pieces[0]:
1092
+ self.emit(f'_O.append({pieces[0]!r})')
1093
+ self._project(f'_O.append({pieces[0]!r})')
1094
+ return
1095
+ self.emit(f'_O.append({pieces[0]!r}')
1096
+ self._project(f'_O.append({pieces[0]!r}')
1097
+ for piece in pieces[1:-1]:
1098
+ self.line += 1
1099
+ self.out.append((repr(piece), self.line))
1100
+ self._project(repr(piece))
1101
+ self.line += 1
1102
+ self.out.append((f'{pieces[-1]!r})', self.line))
1103
+ self._project(f'{pieces[-1]!r})')
1104
+
1105
+ def _project(self, fragment: str) -> None:
1106
+ while len(self.projection) < self.line:
1107
+ self.projection.append('')
1108
+ self.projection[self.line - 1] += fragment
1109
+
1110
+
1111
+ def _at(tree: list[list[str]], i: int) -> list[str] | None:
1112
+ return tree[i] if -len(tree) <= i < len(tree) else None
1113
+
1114
+
1115
+ def _code_objects(code: types.CodeType) -> Iterator[types.CodeType]:
1116
+ yield code
1117
+ for const in code.co_consts:
1118
+ if isinstance(const, types.CodeType):
1119
+ yield from _code_objects(const)
1120
+
1121
+
1122
+ def _sandbox(name: str) -> dict[str, Any]:
1123
+ if (module := sys.modules.get(name)) is None and (module := _SANDBOXES.get(name)) is None:
1124
+ module = _SANDBOXES[name] = types.ModuleType(name)
1125
+
1126
+ # No spec, as the module has no source that linecache could read, which warns of a spec without a loader
1127
+ del module.__spec__
1128
+ return module.__dict__
1129
+
1130
+
1131
+ # Logical lines of Python code, the brace and the colon that open a block, the brace that closes one, and the end of
1132
+ # the code that is not a complete logical line yet
1133
+ def _scan(code: str, final: bool) -> tuple[list[_Logical], str]:
1134
+ logicals: list[_Logical] = []
1135
+ i = start = line = first_line = 0
1136
+ n = len(code)
1137
+ depth = 0
1138
+ brace = close = last = -1
1139
+ first = -1
1140
+
1141
+ def finish(end: int) -> None:
1142
+ nonlocal start, depth, brace, close, last, first, first_line
1143
+ text = code[start:end]
1144
+ if text.strip():
1145
+ lead = len(text) - len(text.lstrip())
1146
+ begin = start + lead
1147
+ logicals.append(
1148
+ _Logical(
1149
+ text[lead:],
1150
+ first_line + text.count('\n', 0, lead),
1151
+ brace - begin if brace >= 0 and depth == 1 else -1,
1152
+ close - begin if close >= 0 else -1,
1153
+ depth == 0 and last >= 0 and code[last] == ':',
1154
+ )
1155
+ )
1156
+ start = end + 1
1157
+ depth = 0
1158
+ brace = close = last = first = -1
1159
+ first_line = line + 1
1160
+
1161
+ while i < n:
1162
+ c = code[i]
1163
+ if c == '#':
1164
+ while i < n and code[i] != '\n':
1165
+ i += 1
1166
+ continue
1167
+ if c == '\\' and i + 1 < n and code[i + 1] == '\n':
1168
+ i += 2
1169
+ line += 1
1170
+ continue
1171
+ if c in '"\'':
1172
+ m = RE_STRING_PREFIX.search(code, start, i)
1173
+ end = _skip_string(code, i, m[1] if m else '')
1174
+ if end < 0:
1175
+ break
1176
+ line += code.count('\n', i, end)
1177
+ i = end
1178
+ last = i - 1
1179
+ if first < 0:
1180
+ first = i
1181
+ continue
1182
+ if c == '\n':
1183
+ if depth == 0 or (depth == 1 and brace >= 0 and last == brace and _block_keyword(code[start:i])):
1184
+ finish(i)
1185
+ line += 1
1186
+ i += 1
1187
+ continue
1188
+ if c in ' \t\r\f':
1189
+ i += 1
1190
+ continue
1191
+ if c in '([{':
1192
+ if c == '{' and depth == 0:
1193
+ brace = i
1194
+ depth += 1
1195
+ elif c in ')]}':
1196
+ if depth == 0 and c == '}' and first < 0 and close < 0:
1197
+ close = i
1198
+ i += 1
1199
+ continue
1200
+ depth = max(depth - 1, 0)
1201
+ if depth == 0:
1202
+ brace = -1
1203
+ last = i
1204
+ if first < 0:
1205
+ first = i
1206
+ i += 1
1207
+
1208
+ if final and start < n:
1209
+ if not (depth == 1 and brace >= 0 and last == brace and _block_keyword(code[start:n])):
1210
+ brace = -1
1211
+ finish(n)
1212
+ return logicals, code[start:] if start < n else ''
1213
+
1214
+
1215
+ def _block_keyword(text: str) -> bool:
1216
+ return bool(m := RE_KEYWORD.match(text)) and m[1] in BLOCK_KEYWORDS
1217
+
1218
+
1219
+ # The end of a string literal, or -1 for a string that is not terminated
1220
+ def _skip_string(code: str, i: int, prefix: str) -> int:
1221
+ quote = code[i : i + 3] if code[i : i + 3] in ('"""', "'''") else code[i]
1222
+ formatted = any(p in prefix.lower() for p in 'ft')
1223
+ i += len(quote)
1224
+ n = len(code)
1225
+ while i < n:
1226
+ if code.startswith(quote, i):
1227
+ return i + len(quote)
1228
+ c = code[i]
1229
+ if c == '\\':
1230
+ i += 2
1231
+ continue
1232
+ if c == '\n' and len(quote) == 1:
1233
+ return -1
1234
+ if formatted and c == '{':
1235
+ if code.startswith('{{', i):
1236
+ i += 2
1237
+ continue
1238
+ if (i := _skip_field(code, i + 1)) < 0:
1239
+ return -1
1240
+ continue
1241
+ i += 1
1242
+ return -1
1243
+
1244
+
1245
+ # The end of a replacement field of an f-string or a t-string
1246
+ def _skip_field(code: str, i: int) -> int:
1247
+ depth = 0
1248
+ n = len(code)
1249
+ while i < n:
1250
+ c = code[i]
1251
+ if c in '"\'':
1252
+ m = RE_STRING_PREFIX.search(code, 0, i)
1253
+ if (i := _skip_string(code, i, m[1] if m else '')) < 0:
1254
+ return -1
1255
+ continue
1256
+ if c in '([{':
1257
+ depth += 1
1258
+ elif c in ')]}':
1259
+ if depth == 0 and c == '}':
1260
+ return i + 1
1261
+ depth -= 1
1262
+ i += 1
1263
+ return -1
1264
+
1265
+
1266
+ def _split(source: str) -> list[str]:
1267
+ lines = source.split('\n')
1268
+ while len(lines) and lines[-1] == '':
1269
+ lines.pop()
1270
+ return lines
1271
+
1272
+
1273
+ def _trim(tree: list[list[str]]) -> None:
1274
+
1275
+ # Skip captures
1276
+ i = -3 if tree[-2][0] in ('cpst', 'cpen') else -2
1277
+
1278
+ # Only trim text
1279
+ if (node := _at(tree, i)) is None or node[0] != 'text':
1280
+ return
1281
+
1282
+ # Convert whitespace text to line noise
1283
+ if m := re.search(r'\s+\Z', node[1]):
1284
+ node[1] = node[1][: m.start()]
1285
+ tree.insert(len(tree) + i, ['code', m[0]])
1286
+
1287
+
1288
+ new = Pyjo_Template.new
1289
+ object = Pyjo_Template
1290
+ Template = Pyjo_Template