Pyjoyment 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjoyment/Renderer.py
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjoyment.Renderer - Generate dynamic content
|
|
3
|
+
=============================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjoyment.Renderer import Renderer
|
|
7
|
+
|
|
8
|
+
renderer = Renderer()
|
|
9
|
+
renderer.classes.append('MyApp.Controller.Foo')
|
|
10
|
+
renderer.paths.append('/home/sri/templates')
|
|
11
|
+
|
|
12
|
+
# Or import the module, as in Google Python Style Guide
|
|
13
|
+
|
|
14
|
+
import Pyjoyment.Renderer
|
|
15
|
+
|
|
16
|
+
renderer = Pyjoyment.Renderer.new()
|
|
17
|
+
renderer.classes.append('MyApp.Controller.Foo')
|
|
18
|
+
renderer.paths.append('/home/sri/templates')
|
|
19
|
+
|
|
20
|
+
:mod:`Pyjoyment.Renderer` is the standard :mod:`Pyjoyment` renderer.
|
|
21
|
+
|
|
22
|
+
See also
|
|
23
|
+
--------
|
|
24
|
+
|
|
25
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
26
|
+
|
|
27
|
+
Ported from `Mojolicious::Renderer <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Renderer.pm>`_
|
|
28
|
+
of Mojolicious 9.49.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import Pyjo.Base
|
|
32
|
+
import Pyjo.Cache
|
|
33
|
+
import Pyjo.Exception
|
|
34
|
+
import Pyjo.File
|
|
35
|
+
import Pyjo.Loader
|
|
36
|
+
|
|
37
|
+
from Pyjo.JSON import encode_json
|
|
38
|
+
from Pyjo.String.Template import is_template
|
|
39
|
+
from Pyjo.TOML import dump as dump_toml
|
|
40
|
+
from Pyjo.Util import _suggest, decamelize, deprecated, encode, gzip, md5_sum # pyright: ignore[reportPrivateUsage]
|
|
41
|
+
from Pyjo.YAML import dump
|
|
42
|
+
|
|
43
|
+
import io
|
|
44
|
+
import os
|
|
45
|
+
import re
|
|
46
|
+
import sys
|
|
47
|
+
import weakref
|
|
48
|
+
|
|
49
|
+
from collections.abc import Callable
|
|
50
|
+
from typing import TYPE_CHECKING, Any, Self, cast, overload
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if TYPE_CHECKING:
|
|
54
|
+
import Pyjoyment.Controller
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# A handler gets the renderer, the controller, the file object to write the output to and the options
|
|
58
|
+
type Handler = Callable[[Pyjoyment_Renderer, Any, io.StringIO, dict[str, Any]], Any]
|
|
59
|
+
|
|
60
|
+
# Bundled templates
|
|
61
|
+
_TEMPLATES = Pyjo.File.path(__file__).sibling('resources', 'templates')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Pyjoyment_Renderer(Pyjo.Base.object):
|
|
65
|
+
"""
|
|
66
|
+
:mod:`Pyjoyment.Renderer` inherits all methods from :mod:`Pyjo.Base` and
|
|
67
|
+
implements the following new ones.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self) -> None:
|
|
71
|
+
"""::
|
|
72
|
+
|
|
73
|
+
renderer = Pyjoyment.Renderer.new()
|
|
74
|
+
|
|
75
|
+
Construct a new :mod:`Pyjoyment.Renderer` object.
|
|
76
|
+
"""
|
|
77
|
+
self.cache: Pyjo.Cache.object[Any, Any] = Pyjo.Cache.new()
|
|
78
|
+
"""::
|
|
79
|
+
|
|
80
|
+
cache = renderer.cache
|
|
81
|
+
renderer.cache = Pyjo.Cache.new()
|
|
82
|
+
|
|
83
|
+
Renderer cache, defaults to a :mod:`Pyjo.Cache` object.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
self.classes = ['__main__']
|
|
87
|
+
"""::
|
|
88
|
+
|
|
89
|
+
classes = renderer.classes
|
|
90
|
+
renderer.classes = ['__main__']
|
|
91
|
+
|
|
92
|
+
Classes to use for finding templates in ``DATA`` sections with
|
|
93
|
+
:mod:`Pyjo.Loader`, first one has the highest precedence, defaults to
|
|
94
|
+
``__main__``. Only files with exactly two extensions will be used, like
|
|
95
|
+
``index.html.ep``. Note that for templates to be detected, these
|
|
96
|
+
classes need to have already been loaded and added before
|
|
97
|
+
:meth:`warmup` is called, which usually happens automatically during
|
|
98
|
+
application startup. ::
|
|
99
|
+
|
|
100
|
+
# Add another class with templates in DATA section
|
|
101
|
+
renderer.classes.append('Pyjoyment.Plugin.Fun')
|
|
102
|
+
|
|
103
|
+
# Add another class with templates in DATA section and higher precedence
|
|
104
|
+
renderer.classes.insert(0, 'Pyjoyment.Plugin.MoreFun')
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
self.compress = True
|
|
108
|
+
"""::
|
|
109
|
+
|
|
110
|
+
boolean = renderer.compress
|
|
111
|
+
renderer.compress = boolean
|
|
112
|
+
|
|
113
|
+
Try to negotiate compression for dynamically generated response content
|
|
114
|
+
and ``gzip`` compress it automatically, defaults to true.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
self.default_format = 'html'
|
|
118
|
+
"""::
|
|
119
|
+
|
|
120
|
+
default = renderer.default_format
|
|
121
|
+
renderer.default_format = 'html'
|
|
122
|
+
|
|
123
|
+
The default format to render if ``format`` is not set in the stash,
|
|
124
|
+
defaults to ``html``. Note that changing the default away from ``html``
|
|
125
|
+
is not recommended, as it has the potential to break, for example,
|
|
126
|
+
plugins with bundled templates.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
self.default_handler: str | None = None
|
|
130
|
+
"""::
|
|
131
|
+
|
|
132
|
+
default = renderer.default_handler
|
|
133
|
+
renderer.default_handler = 'ep'
|
|
134
|
+
|
|
135
|
+
The default template handler to use for rendering in cases where
|
|
136
|
+
auto-detection doesn't work, like for ``inline`` templates. An
|
|
137
|
+
``inline`` template that is a template string of
|
|
138
|
+
:mod:`Pyjo.String.Template` is rendered by the handler ``py`` of
|
|
139
|
+
:mod:`Pyjoyment.Plugin.PyRenderer`.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
self.encoding: str | None = 'UTF-8'
|
|
143
|
+
"""::
|
|
144
|
+
|
|
145
|
+
encoding = renderer.encoding
|
|
146
|
+
renderer.encoding = 'koi8-r'
|
|
147
|
+
|
|
148
|
+
Will encode generated content if set, defaults to ``UTF-8``. Note that
|
|
149
|
+
many renderers also use this value to determine if template files
|
|
150
|
+
should be decoded before processing.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
self.handlers: dict[str, Handler] = {}
|
|
154
|
+
"""::
|
|
155
|
+
|
|
156
|
+
handlers = renderer.handlers
|
|
157
|
+
renderer.handlers = {'epl': lambda renderer, c, output, options: ...}
|
|
158
|
+
|
|
159
|
+
Registered handlers.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
self.helpers: dict[str, Callable[..., Any]] = {}
|
|
163
|
+
"""::
|
|
164
|
+
|
|
165
|
+
helpers = renderer.helpers
|
|
166
|
+
renderer.helpers = {'url_for': lambda c, *args: ...}
|
|
167
|
+
|
|
168
|
+
Registered helpers.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
self.min_compress_size = 860
|
|
172
|
+
"""::
|
|
173
|
+
|
|
174
|
+
size = renderer.min_compress_size
|
|
175
|
+
renderer.min_compress_size = 1024
|
|
176
|
+
|
|
177
|
+
Minimum output size in bytes required for compression to be used if
|
|
178
|
+
enabled, defaults to ``860``.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
self.paths: list[str] = []
|
|
182
|
+
"""::
|
|
183
|
+
|
|
184
|
+
paths = renderer.paths
|
|
185
|
+
renderer.paths = ['/home/sri/templates']
|
|
186
|
+
|
|
187
|
+
Directories to look for templates in, first one has the highest
|
|
188
|
+
precedence. ::
|
|
189
|
+
|
|
190
|
+
# Add another "templates" directory
|
|
191
|
+
renderer.paths.append('/home/sri/templates')
|
|
192
|
+
|
|
193
|
+
# Add another "templates" directory with higher precedence
|
|
194
|
+
renderer.paths.insert(0, '/home/sri/themes/blue/templates')
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
# Not in Mojolicious: the templates of functions, for the handler "py"
|
|
198
|
+
self.templates: dict[str, Callable[..., Any]] = {}
|
|
199
|
+
"""::
|
|
200
|
+
|
|
201
|
+
templates = renderer.templates
|
|
202
|
+
renderer.templates = {'index.html': lambda c: t('<p>Hello!</p>')}
|
|
203
|
+
|
|
204
|
+
Registered templates of functions, by the name of the template with
|
|
205
|
+
its format, as ``index.html``, which the handler ``py`` of
|
|
206
|
+
:mod:`Pyjoyment.Plugin.PyRenderer` renders, before the templates of
|
|
207
|
+
:attr:`paths` and :attr:`classes`.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
self._index: dict[str, str] = {}
|
|
211
|
+
self._namespaces: list[type[Pyjoyment_Renderer_Helpers]] = []
|
|
212
|
+
self._proxy: dict[str, type[Pyjoyment_Renderer_Helpers]] = {}
|
|
213
|
+
self._templates: dict[str, list[str]] | None = None
|
|
214
|
+
|
|
215
|
+
# The methods of the proxies go with the renderer, as Mojo removes them in DESTROY
|
|
216
|
+
weakref.finalize(self, _teardown, self._namespaces)
|
|
217
|
+
|
|
218
|
+
def accepts(self, c: 'Pyjoyment.Controller.object', *formats: str) -> list[str] | str | None:
|
|
219
|
+
"""::
|
|
220
|
+
|
|
221
|
+
all = renderer.accepts(Pyjoyment.Controller.new())
|
|
222
|
+
best = renderer.accepts(Pyjoyment.Controller.new(), 'html', 'json')
|
|
223
|
+
|
|
224
|
+
Select best possible representation for :mod:`Pyjoyment.Controller`
|
|
225
|
+
object from ``format`` ``GET``/``POST`` parameter, ``format`` stash
|
|
226
|
+
value, or ``Accept`` request header, defaults to returning the first
|
|
227
|
+
extension if no preference could be detected.
|
|
228
|
+
"""
|
|
229
|
+
# DEPRECATED!
|
|
230
|
+
req = c.req
|
|
231
|
+
param = req.param('format')
|
|
232
|
+
if param is not None:
|
|
233
|
+
deprecated('The ?format=* parameter is deprecated in favor of ?_format=* for content negotiation')
|
|
234
|
+
|
|
235
|
+
# List representations
|
|
236
|
+
fmt = (param if param is not None else req.param('_format')) or c.stash().get('format')
|
|
237
|
+
exts = [fmt] if fmt else []
|
|
238
|
+
exts.extend(c.app.types.detect(req.headers.accept))
|
|
239
|
+
if not formats:
|
|
240
|
+
return exts
|
|
241
|
+
|
|
242
|
+
# Find best representation
|
|
243
|
+
for ext in exts:
|
|
244
|
+
if ext in formats:
|
|
245
|
+
return ext
|
|
246
|
+
return None if exts else formats[0]
|
|
247
|
+
|
|
248
|
+
def add_handler(self, name: str, cb: Handler) -> Self:
|
|
249
|
+
"""::
|
|
250
|
+
|
|
251
|
+
renderer = renderer.add_handler('epl', lambda renderer, c, output, options: ...)
|
|
252
|
+
|
|
253
|
+
Register a handler. ::
|
|
254
|
+
|
|
255
|
+
def foo(renderer, c, output, options):
|
|
256
|
+
...
|
|
257
|
+
output.write('Hello World!')
|
|
258
|
+
|
|
259
|
+
renderer.add_handler('foo', foo)
|
|
260
|
+
"""
|
|
261
|
+
self.handlers[name] = cb
|
|
262
|
+
return self
|
|
263
|
+
|
|
264
|
+
def add_helper(self, name: str, cb: Callable[..., Any]) -> Self:
|
|
265
|
+
"""::
|
|
266
|
+
|
|
267
|
+
renderer = renderer.add_helper('url_for', lambda c, *args: ...)
|
|
268
|
+
|
|
269
|
+
Register a helper. ::
|
|
270
|
+
|
|
271
|
+
def foo(c, *args):
|
|
272
|
+
...
|
|
273
|
+
|
|
274
|
+
renderer.add_helper('foo', foo)
|
|
275
|
+
"""
|
|
276
|
+
self.helpers[name] = cb
|
|
277
|
+
self._proxy.clear()
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
@overload
|
|
281
|
+
def add_template[F: Callable[..., Any]](self, name: str, cb: None = None) -> Callable[[F], F]: ...
|
|
282
|
+
|
|
283
|
+
@overload
|
|
284
|
+
def add_template(self, name: str, cb: Callable[..., Any]) -> Self: ...
|
|
285
|
+
|
|
286
|
+
# Not in Mojolicious: the templates of functions, for the handler "py"
|
|
287
|
+
def add_template(self, name: str, cb: Callable[..., Any] | None = None) -> Any:
|
|
288
|
+
"""::
|
|
289
|
+
|
|
290
|
+
renderer = renderer.add_template('index.html', lambda c: t('<p>Hello!</p>'))
|
|
291
|
+
|
|
292
|
+
@renderer.add_template('index.html')
|
|
293
|
+
def index(c): ...
|
|
294
|
+
|
|
295
|
+
Register a template of a function, which gets the controller and
|
|
296
|
+
returns a template string, see :mod:`Pyjoyment.Plugin.PyRenderer`. ::
|
|
297
|
+
|
|
298
|
+
def foo(c, name):
|
|
299
|
+
return t('<p>Hello {name}!</p>')
|
|
300
|
+
|
|
301
|
+
renderer.add_template('foo.html', foo)
|
|
302
|
+
"""
|
|
303
|
+
if cb is None:
|
|
304
|
+
|
|
305
|
+
def decorator(cb: Callable[..., Any]) -> Callable[..., Any]:
|
|
306
|
+
self.add_template(name, cb)
|
|
307
|
+
return cb
|
|
308
|
+
|
|
309
|
+
return decorator
|
|
310
|
+
self.templates[name] = cb
|
|
311
|
+
if self._templates is not None:
|
|
312
|
+
self.warmup()
|
|
313
|
+
return self
|
|
314
|
+
|
|
315
|
+
def get_data_template(self, options: dict[str, Any]) -> str | bytes | None:
|
|
316
|
+
"""::
|
|
317
|
+
|
|
318
|
+
template = renderer.get_data_template({'template': 'foo/bar', 'format': 'html', 'handler': 'epl'})
|
|
319
|
+
|
|
320
|
+
Return a ``DATA`` section template from :attr:`classes` for an options
|
|
321
|
+
dict with ``template``, ``format``, ``variant`` and ``handler`` values,
|
|
322
|
+
or ``None`` if no template could be found, usually used by handlers.
|
|
323
|
+
"""
|
|
324
|
+
if not (template := self.template_name(options)):
|
|
325
|
+
return None
|
|
326
|
+
return cast('str | bytes | None', Pyjo.Loader.data_section(self._index.get(template), template))
|
|
327
|
+
|
|
328
|
+
def get_helper(self, name: str) -> 'Callable[..., Any] | type[Pyjoyment_Renderer_Helpers] | None':
|
|
329
|
+
"""::
|
|
330
|
+
|
|
331
|
+
helper = renderer.get_helper('url_for')
|
|
332
|
+
|
|
333
|
+
Get a helper by full name, generate a helper dynamically for a prefix,
|
|
334
|
+
or return ``None`` if no helper or prefix could be found. Generated
|
|
335
|
+
helpers return a proxy object containing the current controller object
|
|
336
|
+
and on which nested helpers can be called.
|
|
337
|
+
"""
|
|
338
|
+
if h := self._proxy.get(name) or self.helpers.get(name):
|
|
339
|
+
return h
|
|
340
|
+
|
|
341
|
+
# The proxy of a prefix is a class with the nested helpers, as Mojo makes it, a helper as a method and a prefix
|
|
342
|
+
# as a property
|
|
343
|
+
found = False
|
|
344
|
+
attrs: dict[str, Any] = {}
|
|
345
|
+
regex = re.compile(rf'^({re.escape(name)}\.([^.]+))' if name else r'^(([^.]+))')
|
|
346
|
+
for key in list(self.helpers):
|
|
347
|
+
if not (m := regex.match(key)):
|
|
348
|
+
continue
|
|
349
|
+
found = True
|
|
350
|
+
sub = self.get_helper(m[1])
|
|
351
|
+
attrs[m[2]] = _attribute(sub)
|
|
352
|
+
|
|
353
|
+
if not found:
|
|
354
|
+
return None
|
|
355
|
+
md5 = md5_sum(f'{name}:{id(self)}'.encode())
|
|
356
|
+
cls = type(f'Pyjoyment_Renderer_Helpers_{md5}', (Pyjoyment_Renderer_Helpers,), attrs)
|
|
357
|
+
self._namespaces.append(cls)
|
|
358
|
+
self._proxy[name] = cls
|
|
359
|
+
return cls
|
|
360
|
+
|
|
361
|
+
def render(self, c: 'Pyjoyment.Controller.object') -> tuple[str | bytes, str | None] | None:
|
|
362
|
+
"""::
|
|
363
|
+
|
|
364
|
+
output, format = renderer.render(Pyjoyment.Controller.new())
|
|
365
|
+
|
|
366
|
+
Render output through one of the renderers. See
|
|
367
|
+
:meth:`Pyjoyment.Controller.Pyjoyment_Controller.render` for a more
|
|
368
|
+
user-friendly interface.
|
|
369
|
+
"""
|
|
370
|
+
stash = c.stash()
|
|
371
|
+
options: dict[str, Any] = {
|
|
372
|
+
'encoding': self.encoding,
|
|
373
|
+
'handler': stash.get('handler'),
|
|
374
|
+
'template': stash.pop('template', None),
|
|
375
|
+
'variant': stash.get('variant'),
|
|
376
|
+
}
|
|
377
|
+
inline = options['inline'] = stash.pop('inline', None)
|
|
378
|
+
if inline is not None and options['handler'] is None:
|
|
379
|
+
# Not in Mojolicious: an inline template string, which the handler "py" renders
|
|
380
|
+
options['handler'] = 'py' if is_template(inline) else self.default_handler
|
|
381
|
+
options['format'] = stash.get('format') or self.default_format
|
|
382
|
+
|
|
383
|
+
# Data
|
|
384
|
+
if stash.get('data') is not None:
|
|
385
|
+
return stash.pop('data'), options['format']
|
|
386
|
+
|
|
387
|
+
# Text, which is a string as Perl stringifies a number or an object
|
|
388
|
+
if stash.get('text') is not None:
|
|
389
|
+
text = stash.pop('text')
|
|
390
|
+
return _maybe(options.get('encoding'), text if isinstance(text, str) else str(text)), options['format']
|
|
391
|
+
|
|
392
|
+
# JSON
|
|
393
|
+
if 'json' in stash:
|
|
394
|
+
return encode_json(stash.pop('json')), 'json'
|
|
395
|
+
|
|
396
|
+
# Not in Mojolicious: YAML, in the style of the documents that tools write, as index.yaml of a chart repository
|
|
397
|
+
# of Helm: the items of a sequence at the column of its key, no anchors, and the quotes of YAML 1.1
|
|
398
|
+
if 'yaml' in stash:
|
|
399
|
+
return encode('UTF-8', dump(stash.pop('yaml'), indent_sequences=False, anchors=False)), 'yaml'
|
|
400
|
+
|
|
401
|
+
# Not in Mojolicious: TOML, of a table
|
|
402
|
+
if 'toml' in stash:
|
|
403
|
+
return encode('UTF-8', dump_toml(stash.pop('toml'))), 'toml'
|
|
404
|
+
|
|
405
|
+
# Template or templateless handler
|
|
406
|
+
if options['template'] is None:
|
|
407
|
+
options['template'] = self.template_for(c)
|
|
408
|
+
if (output := self._render_template(c, options)) is None:
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
# Inheritance
|
|
412
|
+
content = stash.setdefault('mojo.content', {})
|
|
413
|
+
saved = _MISSING
|
|
414
|
+
if local := bool(stash.get('extends') or stash.get('layout')):
|
|
415
|
+
saved = content.get('content', _MISSING)
|
|
416
|
+
content['content'] = output if _visible(output) else None
|
|
417
|
+
try:
|
|
418
|
+
while (next := _next(stash)) and inline is None:
|
|
419
|
+
options['handler'], options['template'] = stash.get('handler'), next
|
|
420
|
+
options['format'] = stash.get('format') or self.default_format
|
|
421
|
+
if (tmp := self._render_template(c, options)) is not None:
|
|
422
|
+
output = tmp
|
|
423
|
+
if content.get('content') is None and _visible(output):
|
|
424
|
+
content['content'] = output
|
|
425
|
+
|
|
426
|
+
if stash.get('mojo.string'):
|
|
427
|
+
return output, None
|
|
428
|
+
return _maybe(options.get('encoding'), output), options['format']
|
|
429
|
+
|
|
430
|
+
finally:
|
|
431
|
+
if local and saved is _MISSING:
|
|
432
|
+
content.pop('content', None)
|
|
433
|
+
elif local:
|
|
434
|
+
content['content'] = saved
|
|
435
|
+
|
|
436
|
+
def respond(
|
|
437
|
+
self, c: 'Pyjoyment.Controller.object', output: str | bytes, format: str | None, status: int | str | None = None
|
|
438
|
+
) -> bool:
|
|
439
|
+
"""::
|
|
440
|
+
|
|
441
|
+
boolean = renderer.respond(Pyjoyment.Controller.new(), output, format)
|
|
442
|
+
boolean = renderer.respond(Pyjoyment.Controller.new(), output, format, status)
|
|
443
|
+
|
|
444
|
+
Finalize dynamically generated response content and :attr:`compress`
|
|
445
|
+
it if possible.
|
|
446
|
+
"""
|
|
447
|
+
stash = c.stash()
|
|
448
|
+
respond = stash.get('mojo.respond')
|
|
449
|
+
stash['mojo.respond'] = (respond or 0) + 1
|
|
450
|
+
if respond:
|
|
451
|
+
raise Pyjo.Exception.new('A response has already been rendered')
|
|
452
|
+
|
|
453
|
+
# Gzip compression
|
|
454
|
+
res = c.res
|
|
455
|
+
if self.compress and len(output) >= self.min_compress_size:
|
|
456
|
+
headers = res.headers
|
|
457
|
+
headers.append('Vary', 'Accept-Encoding')
|
|
458
|
+
accept = re.search(r'gzip', c.req.headers.accept_encoding or '', re.IGNORECASE)
|
|
459
|
+
if accept and not headers.content_encoding:
|
|
460
|
+
headers.content_encoding = 'gzip'
|
|
461
|
+
output = gzip(output if isinstance(output, bytes) else output.encode())
|
|
462
|
+
|
|
463
|
+
res.body = output if isinstance(output, bytes) else output.encode()
|
|
464
|
+
c.app.types.content_type(c, ext=format)
|
|
465
|
+
return bool(c.rendered(status))
|
|
466
|
+
|
|
467
|
+
def template_for(self, c: 'Pyjoyment.Controller.object') -> str | None:
|
|
468
|
+
"""::
|
|
469
|
+
|
|
470
|
+
name = renderer.template_for(Pyjoyment.Controller.new())
|
|
471
|
+
|
|
472
|
+
Return default template name for :mod:`Pyjoyment.Controller` object, or
|
|
473
|
+
``None`` if no name could be generated.
|
|
474
|
+
"""
|
|
475
|
+
# Normal default template
|
|
476
|
+
stash = c.stash()
|
|
477
|
+
controller, action = stash.get('controller'), stash.get('action')
|
|
478
|
+
if controller and action:
|
|
479
|
+
return '/'.join([*decamelize(controller.replace('.', '::')).split('-'), action])
|
|
480
|
+
|
|
481
|
+
# Try the route name if we don't have controller and action
|
|
482
|
+
if (route := c.match.endpoint) is None:
|
|
483
|
+
return None
|
|
484
|
+
return route.name
|
|
485
|
+
|
|
486
|
+
def template_handler(self, options: dict[str, Any]) -> str | None:
|
|
487
|
+
"""::
|
|
488
|
+
|
|
489
|
+
handler = renderer.template_handler({'template': 'foo/bar', 'format': 'html'})
|
|
490
|
+
|
|
491
|
+
Return handler for an options dict with ``template``, ``format`` and
|
|
492
|
+
``variant`` values, or ``None`` if no handler could be found.
|
|
493
|
+
"""
|
|
494
|
+
if not (file := self.template_name(options)):
|
|
495
|
+
return None
|
|
496
|
+
if not (handlers := (self._templates or {}).get(file)):
|
|
497
|
+
return self.default_handler
|
|
498
|
+
return handlers[0]
|
|
499
|
+
|
|
500
|
+
def template_name(self, options: dict[str, Any]) -> str | None:
|
|
501
|
+
"""::
|
|
502
|
+
|
|
503
|
+
template = renderer.template_name({'template': 'foo/bar', 'format': 'html', 'handler': 'epl'})
|
|
504
|
+
|
|
505
|
+
Return a template name for an options dict with ``template``,
|
|
506
|
+
``format``, ``variant`` and ``handler`` values, or ``None`` if no
|
|
507
|
+
template could be found, usually used by handlers.
|
|
508
|
+
"""
|
|
509
|
+
if (template := options.get('template')) is None:
|
|
510
|
+
return None
|
|
511
|
+
if not (format := options.get('format')):
|
|
512
|
+
return None
|
|
513
|
+
template = f'{template}.{format}'
|
|
514
|
+
|
|
515
|
+
if self._templates is None:
|
|
516
|
+
self.warmup()
|
|
517
|
+
templates = self._templates or {}
|
|
518
|
+
|
|
519
|
+
# Variants
|
|
520
|
+
handler = options.get('handler')
|
|
521
|
+
if (variant := options.get('variant')) is not None:
|
|
522
|
+
variant = f'{template}+{variant}'
|
|
523
|
+
handlers = templates.get(variant, [])
|
|
524
|
+
if (handlers and handler is None) or handler in handlers:
|
|
525
|
+
template = variant
|
|
526
|
+
|
|
527
|
+
return f'{template}.{handler}' if handler is not None else template
|
|
528
|
+
|
|
529
|
+
def template_path(self, options: dict[str, Any]) -> str | None:
|
|
530
|
+
"""::
|
|
531
|
+
|
|
532
|
+
path = renderer.template_path({'template': 'foo/bar', 'format': 'html', 'handler': 'epl'})
|
|
533
|
+
|
|
534
|
+
Return the full template path for an options dict with ``template``,
|
|
535
|
+
``format``, ``variant`` and ``handler`` values, or ``None`` if the file
|
|
536
|
+
does not exist in :attr:`paths`, usually used by handlers.
|
|
537
|
+
"""
|
|
538
|
+
if not (name := self.template_name(options)):
|
|
539
|
+
return None
|
|
540
|
+
parts = name.split('/')
|
|
541
|
+
for path in [*self.paths, _TEMPLATES]:
|
|
542
|
+
if os.access(file := Pyjo.File.path(path, *parts).to_str(), os.R_OK):
|
|
543
|
+
return file
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
def warmup(self) -> None:
|
|
547
|
+
"""::
|
|
548
|
+
|
|
549
|
+
renderer.warmup()
|
|
550
|
+
|
|
551
|
+
Prepare templates from :attr:`classes` and :attr:`templates` for
|
|
552
|
+
future use.
|
|
553
|
+
"""
|
|
554
|
+
index: dict[str, str] = {}
|
|
555
|
+
templates: dict[str, list[str]] = {}
|
|
556
|
+
self._index, self._templates = index, templates
|
|
557
|
+
|
|
558
|
+
# Handlers for templates
|
|
559
|
+
for path in [*self.paths, _TEMPLATES]:
|
|
560
|
+
for file in Pyjo.File.path(path).list_tree():
|
|
561
|
+
name = '/'.join(file.to_rel(path).to_list())
|
|
562
|
+
if m := re.search(r'\.(\w+)$', name):
|
|
563
|
+
templates.setdefault(name[: m.start()], []).append(m[1])
|
|
564
|
+
|
|
565
|
+
# Handlers and classes for DATA templates
|
|
566
|
+
for cls in reversed(self.classes):
|
|
567
|
+
keys = sorted(cast(dict[str, Any], Pyjo.Loader.data_section(cls) or {}))
|
|
568
|
+
for key in keys:
|
|
569
|
+
index[key] = cls
|
|
570
|
+
for key in reversed(keys):
|
|
571
|
+
if m := re.search(r'\.(\w+)$', key):
|
|
572
|
+
templates.setdefault(key[: m.start()], []).insert(0, m[1])
|
|
573
|
+
|
|
574
|
+
# Not in Mojolicious: the handler of the templates of functions
|
|
575
|
+
for key in self.templates:
|
|
576
|
+
templates.setdefault(key, []).insert(0, 'py')
|
|
577
|
+
|
|
578
|
+
def _render_template(self, c: 'Pyjoyment.Controller.object', options: dict[str, Any]) -> str | bytes | None:
|
|
579
|
+
if not options.get('handler'):
|
|
580
|
+
options['handler'] = self.template_handler(options)
|
|
581
|
+
if not (handler := options['handler']):
|
|
582
|
+
return None
|
|
583
|
+
if (renderer := self.handlers.get(handler)) is None:
|
|
584
|
+
c.helpers.log().error(f'No handler for "{handler}" found')
|
|
585
|
+
return None
|
|
586
|
+
|
|
587
|
+
output = _Output()
|
|
588
|
+
renderer(self, c, output, options)
|
|
589
|
+
return output.value() if output.defined else None
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
class Pyjoyment_Renderer_Helpers:
|
|
593
|
+
"""
|
|
594
|
+
The proxy object of :meth:`Pyjoyment_Renderer.get_helper`, which contains
|
|
595
|
+
the current controller object and on which nested helpers can be called.
|
|
596
|
+
"""
|
|
597
|
+
|
|
598
|
+
def __init__(self, c: Any) -> None:
|
|
599
|
+
self._c = c
|
|
600
|
+
|
|
601
|
+
def __getattr__(self, name: str) -> Any:
|
|
602
|
+
package = f'{type(self).__module__}.{type(self).__qualname__}'
|
|
603
|
+
|
|
604
|
+
# The location of the caller, which croak adds to the message
|
|
605
|
+
caller = sys._getframe(1) # pyright: ignore[reportPrivateUsage]
|
|
606
|
+
at = f'at {caller.f_code.co_filename} line {caller.f_lineno}'
|
|
607
|
+
|
|
608
|
+
# Not in Mojolicious: the nested helper that the name most likely meant
|
|
609
|
+
suggestion = None if name.startswith('_') else _suggest(name, dir(type(self)))
|
|
610
|
+
mean = f' Did you mean "{suggestion}"?' if suggestion is not None else ''
|
|
611
|
+
raise AttributeError(
|
|
612
|
+
f'Can\'t locate object method "{name}" via package "{package}" {at}.{mean}\n', name=name, obj=self
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
# The output of a handler, which tells whether the handler wrote any, as Mojo checks that it is defined, and which
|
|
617
|
+
# holds bytes instead of characters for a handler that writes them, as one that removes the encoding of the options
|
|
618
|
+
class _Output(io.StringIO):
|
|
619
|
+
defined = False
|
|
620
|
+
data: bytearray | None = None
|
|
621
|
+
|
|
622
|
+
def write(self, s: str | bytes, /) -> int:
|
|
623
|
+
self.defined = True
|
|
624
|
+
if isinstance(s, bytes):
|
|
625
|
+
self.data = (self.data or bytearray()) + s
|
|
626
|
+
return len(s)
|
|
627
|
+
return super().write(s)
|
|
628
|
+
|
|
629
|
+
def value(self) -> str | bytes:
|
|
630
|
+
return bytes(self.data) if self.data is not None else self.getvalue()
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
_MISSING: Any = Pyjo.Base.object()
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
# A nested helper is a method of the proxy, a nested prefix a property that makes its proxy
|
|
637
|
+
def _attribute(sub: Any) -> Any:
|
|
638
|
+
if isinstance(sub, type):
|
|
639
|
+
proxy = cast(type[Pyjoyment_Renderer_Helpers], sub)
|
|
640
|
+
return property(lambda helpers: proxy(helpers._c)) # pyright: ignore[reportPrivateUsage]
|
|
641
|
+
|
|
642
|
+
def method(helpers: Pyjoyment_Renderer_Helpers, *args: Any, **kwargs: Any) -> Any:
|
|
643
|
+
return sub(helpers._c, *args, **kwargs) # pyright: ignore[reportPrivateUsage]
|
|
644
|
+
|
|
645
|
+
return method
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _maybe(encoding: str | None, output: str | bytes) -> str | bytes:
|
|
649
|
+
return encode(encoding, output) if encoding and isinstance(output, str) else output
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
# Content that is not only whitespace, of characters or of the bytes of a handler
|
|
653
|
+
def _visible(output: str | bytes) -> bool:
|
|
654
|
+
return bool(re.search(rb'\S', output) if isinstance(output, bytes) else re.search(r'\S', output))
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _next(stash: dict[str, Any]) -> str | None:
|
|
658
|
+
if stash.get('extends'):
|
|
659
|
+
return stash.pop('extends')
|
|
660
|
+
if not (layout := stash.pop('layout', None)):
|
|
661
|
+
return None
|
|
662
|
+
return f'layouts/{layout}'
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _teardown(namespaces: list[type[Pyjoyment_Renderer_Helpers]]) -> None:
|
|
666
|
+
for cls in namespaces:
|
|
667
|
+
for name in [name for name in vars(cls) if not name.startswith('__')]:
|
|
668
|
+
delattr(cls, name)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
new = Pyjoyment_Renderer.new
|
|
672
|
+
object = Pyjoyment_Renderer
|
|
673
|
+
Renderer = Pyjoyment_Renderer
|