mstache 0.2.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.
- mstache-0.2.0.dist-info/LICENSE +21 -0
- mstache-0.2.0.dist-info/METADATA +175 -0
- mstache-0.2.0.dist-info/RECORD +8 -0
- mstache-0.2.0.dist-info/WHEEL +5 -0
- mstache-0.2.0.dist-info/entry_points.txt +3 -0
- mstache-0.2.0.dist-info/top_level.txt +1 -0
- mstache-0.2.0.dist-info/zip-safe +1 -0
- mstache.py +1147 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021-2024 Felipe A Hernandez <ergoithz@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of the software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions.
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mstache
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: mstache, Mustache for Python
|
|
5
|
+
Home-page: https://gitlab.com/ergoithz/mstache
|
|
6
|
+
Author: Felipe A Hernandez
|
|
7
|
+
Author-email: ergoithz@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: template,mustache
|
|
10
|
+
Platform: any
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.10.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: benchmark
|
|
21
|
+
Requires-Dist: ustache ==0.1.5 ; extra == 'benchmark'
|
|
22
|
+
Requires-Dist: chevron ; extra == 'benchmark'
|
|
23
|
+
Requires-Dist: tabulate ; extra == 'benchmark'
|
|
24
|
+
Provides-Extra: codestyle
|
|
25
|
+
Requires-Dist: pycodestyle ; extra == 'codestyle'
|
|
26
|
+
Requires-Dist: ruff ; extra == 'codestyle'
|
|
27
|
+
Requires-Dist: sphinx ; extra == 'codestyle'
|
|
28
|
+
Requires-Dist: rstcheck ; extra == 'codestyle'
|
|
29
|
+
Requires-Dist: mypy ; extra == 'codestyle'
|
|
30
|
+
Provides-Extra: coverage
|
|
31
|
+
Requires-Dist: coverage[toml] ; extra == 'coverage'
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pycodestyle ; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff ; extra == 'dev'
|
|
35
|
+
Requires-Dist: sphinx ; extra == 'dev'
|
|
36
|
+
Requires-Dist: rstcheck ; extra == 'dev'
|
|
37
|
+
Requires-Dist: mypy ; extra == 'dev'
|
|
38
|
+
Requires-Dist: coverage[toml] ; extra == 'dev'
|
|
39
|
+
Requires-Dist: recommonmark ; extra == 'dev'
|
|
40
|
+
Requires-Dist: sphinx-autobuild ; extra == 'dev'
|
|
41
|
+
Requires-Dist: sphinx-autodoc-typehints ; extra == 'dev'
|
|
42
|
+
Requires-Dist: build ; extra == 'dev'
|
|
43
|
+
Requires-Dist: twine ; extra == 'dev'
|
|
44
|
+
Requires-Dist: ustache ==0.1.5 ; extra == 'dev'
|
|
45
|
+
Requires-Dist: chevron ; extra == 'dev'
|
|
46
|
+
Requires-Dist: tabulate ; extra == 'dev'
|
|
47
|
+
Provides-Extra: docs
|
|
48
|
+
Requires-Dist: recommonmark ; extra == 'docs'
|
|
49
|
+
Requires-Dist: sphinx ; extra == 'docs'
|
|
50
|
+
Requires-Dist: sphinx-autobuild ; extra == 'docs'
|
|
51
|
+
Requires-Dist: sphinx-autodoc-typehints ; extra == 'docs'
|
|
52
|
+
Provides-Extra: release
|
|
53
|
+
Requires-Dist: build ; extra == 'release'
|
|
54
|
+
Requires-Dist: twine ; extra == 'release'
|
|
55
|
+
|
|
56
|
+
# mstache
|
|
57
|
+
|
|
58
|
+
Mustache for Python.
|
|
59
|
+
|
|
60
|
+
Documentation: [mstache.readthedocs.io](https://mstache.readthedocs.io)
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
[MIT License](https://gitlab.com/ergoithz/mstache/-/blob/master/LICENSE)
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
pip install mstache
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
Python:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import mstache
|
|
78
|
+
|
|
79
|
+
print(mstache.render('Hello {{v}}', {'v': 'World!'}))
|
|
80
|
+
# Hello World!
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Command line:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
$ mstache -j data.json -o output.html template.mustache
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Highlights
|
|
90
|
+
|
|
91
|
+
- The fastest pure-python Mustache implementation to this date.
|
|
92
|
+
- Command line interface.
|
|
93
|
+
- Spec compliant, but also highly compatible with `Mustache.js`.
|
|
94
|
+
- Small codebase, efficiently rendering to `str` or `bytes`,
|
|
95
|
+
either buffered or streaming.
|
|
96
|
+
- Fully customizable behavior: cache, property getter, partial resolver,
|
|
97
|
+
stringification, escaping and lambdas.
|
|
98
|
+
- No dynamic code generation, both jit and transpiler friendly.
|
|
99
|
+
|
|
100
|
+
## Considerations
|
|
101
|
+
|
|
102
|
+
For inter-compatibility with JavaScript (especially `Mustache.js`, enabling
|
|
103
|
+
client-side rendering with the same templates), **mstache** must behave
|
|
104
|
+
somewhat atypically for a `Python` template engine:
|
|
105
|
+
|
|
106
|
+
- Mustache blocks stick to JavaScript truthyness
|
|
107
|
+
(`Mustache.js` truthyness emulation):
|
|
108
|
+
- Empty mappings (such as `dict`) are unconditionally truthy.
|
|
109
|
+
- `NaN`/`-NaN` are falsy.
|
|
110
|
+
- Mustache blocks loop through any iterable other than mappings and strings
|
|
111
|
+
(`Mustache.js` `Array` handling).
|
|
112
|
+
- Non-mapping sized objects will expose a virtual `length` property
|
|
113
|
+
(JavaScript `Array.length` emulation, customizable via `getter` parameter).
|
|
114
|
+
- Mapping keys containing dot (`.`) or whitespace (` `) are unreachable,
|
|
115
|
+
(`Mustache.js` property limitation, customizable via `getter` parameter).
|
|
116
|
+
- Sequence elements are accessible by positive index in the same way mapping
|
|
117
|
+
integer-keyed items are also accessible when no string key conflicts, as
|
|
118
|
+
properties (JavaScript `Object` emulation, customizable via `getter` parameter).
|
|
119
|
+
|
|
120
|
+
## Syntax
|
|
121
|
+
|
|
122
|
+
Check out the [mustache(5) manual](https://mustache.github.io/mustache.5.html).
|
|
123
|
+
|
|
124
|
+
For quick reference, here is a quick overview of the Mustache syntax.
|
|
125
|
+
|
|
126
|
+
Template (`template.mustache`):
|
|
127
|
+
```handlebars
|
|
128
|
+
{{!comment}}
|
|
129
|
+
<ul>
|
|
130
|
+
{{#object}}<li>{{property}}</li>{{/object}}
|
|
131
|
+
{{^object}}<li>As <b>object</b> is truthy, this won't be shown</li>{{/object}}
|
|
132
|
+
{{^null}}<li><b>null</b> is falsy</li>{{/null}}
|
|
133
|
+
{{#array}}<li>{{property}}</li>
|
|
134
|
+
{{/array}}
|
|
135
|
+
{{^array}}<li>Array isn't empty, this won't be shown.</li>{{/array}}
|
|
136
|
+
{{#empty_array}}<li>Empty Array, this won't be shown</li>{{/empty_array}}
|
|
137
|
+
{{^empty_array}}<li>empty_array is empty</li>{{/empty_array}}
|
|
138
|
+
{{&unescaped_html}}
|
|
139
|
+
</ul>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Data (`data.json`):
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"object": {
|
|
146
|
+
"property": "Object property value"
|
|
147
|
+
},
|
|
148
|
+
"null": null,
|
|
149
|
+
"array": [
|
|
150
|
+
{"property": "Array item1 property"},
|
|
151
|
+
{"property": "Array item2 property"},
|
|
152
|
+
{"property": "Array item3 property"}
|
|
153
|
+
],
|
|
154
|
+
"empty_array": [],
|
|
155
|
+
"unescaped_html": "<li>this is unescaped html</li>"
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Command:
|
|
160
|
+
```sh
|
|
161
|
+
$ mstache -j data.json -o output.html template.mustache
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Output:
|
|
165
|
+
```html
|
|
166
|
+
<ul>
|
|
167
|
+
<li>Object property value</li>
|
|
168
|
+
<li><b>null</b> is falsy</li>
|
|
169
|
+
<li>Array item1 property</li>
|
|
170
|
+
<li>Array item2 property</li>
|
|
171
|
+
<li>Array item3 property</li>
|
|
172
|
+
<li>empty_array is empty</li>
|
|
173
|
+
<li>this is unescaped html</li>
|
|
174
|
+
</ul>
|
|
175
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mstache.py,sha256=Fyb7XLb-uoPskeArd_Dl1hF8ACaSXEl0TjZyQlK2piY,35598
|
|
2
|
+
mstache-0.2.0.dist-info/LICENSE,sha256=Dm6iE3DBTvHjFk7lmBlxltSnL_cniKTuDGimSeYnILw,1100
|
|
3
|
+
mstache-0.2.0.dist-info/METADATA,sha256=Sokx2kaCTNqoS97LHCmWsfzSykrrlRJvXE7CR4_VmLY,5341
|
|
4
|
+
mstache-0.2.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
5
|
+
mstache-0.2.0.dist-info/entry_points.txt,sha256=Og2tE2b_-ptIXcCpFPseKLtRaa1EE2ixpAGiZlNsg_4,62
|
|
6
|
+
mstache-0.2.0.dist-info/top_level.txt,sha256=Xoa-62eFM8ceW2fl0WB_tFJJXPuGxuyiVkGVNhkHBeM,8
|
|
7
|
+
mstache-0.2.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
|
+
mstache-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mstache
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
mstache.py
ADDED
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mstache module.
|
|
3
|
+
|
|
4
|
+
This module alone implements the entire mstache library, including its
|
|
5
|
+
minimal command line interface.
|
|
6
|
+
|
|
7
|
+
The main functions are considered to be :func:`cli`, :func:`render` and
|
|
8
|
+
:func:`stream`, and they expose different approaches to template rendering:
|
|
9
|
+
command line interface, buffered and streaming rendering API, respectively.
|
|
10
|
+
|
|
11
|
+
Other functionality will be considered **advanced**, exposing some
|
|
12
|
+
implementation-specific complexity and potentially non-standard behavior
|
|
13
|
+
which could reduce compatibility with other mustache implementations and
|
|
14
|
+
future major mstache revisions.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
"""
|
|
18
|
+
mstache, Mustache for Python
|
|
19
|
+
============================
|
|
20
|
+
|
|
21
|
+
See `README.md`_, `project documentation`_ and `project repository`_.
|
|
22
|
+
|
|
23
|
+
.. _README.md: https://mstache.readthedocs.io/en/latest/README.html
|
|
24
|
+
.. _project documentation: https://mstache.readthedocs.io
|
|
25
|
+
.. _project repository: https://gitlab.com/ergoithz/mstache
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
License
|
|
29
|
+
-------
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2021-2024, Felipe A Hernandez.
|
|
32
|
+
|
|
33
|
+
MIT License (see `LICENSE`_).
|
|
34
|
+
|
|
35
|
+
.. _LICENSE: https://gitlab.com/ergoithz/mstache/-/blob/master/LICENSE
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import codecs
|
|
40
|
+
import collections
|
|
41
|
+
import collections.abc
|
|
42
|
+
import math
|
|
43
|
+
import types
|
|
44
|
+
import typing
|
|
45
|
+
|
|
46
|
+
__author__ = 'Felipe A Hernandez'
|
|
47
|
+
__email__ = 'ergoithz@gmail.com'
|
|
48
|
+
__license__ = 'MIT'
|
|
49
|
+
__version__ = '0.2.0'
|
|
50
|
+
__all__ = (
|
|
51
|
+
# api
|
|
52
|
+
'tokenize',
|
|
53
|
+
'stream',
|
|
54
|
+
'render',
|
|
55
|
+
'cli',
|
|
56
|
+
# exceptions
|
|
57
|
+
'TokenException',
|
|
58
|
+
'ClosingTokenException',
|
|
59
|
+
'UnclosedTokenException',
|
|
60
|
+
'DelimiterTokenException',
|
|
61
|
+
'TemplateRecursionError',
|
|
62
|
+
# defaults
|
|
63
|
+
'default_recursion_limit',
|
|
64
|
+
'default_resolver',
|
|
65
|
+
'default_getter',
|
|
66
|
+
'default_stringify',
|
|
67
|
+
'default_escape',
|
|
68
|
+
'default_lambda_render',
|
|
69
|
+
'default_tags',
|
|
70
|
+
'default_cache',
|
|
71
|
+
'default_cache_make_key',
|
|
72
|
+
'default_virtuals',
|
|
73
|
+
# types
|
|
74
|
+
'TString',
|
|
75
|
+
'PartialResolver',
|
|
76
|
+
'PropertyGetter',
|
|
77
|
+
'StringifyFunction',
|
|
78
|
+
'EscapeFunction',
|
|
79
|
+
'LambdaRenderFunctionConstructor',
|
|
80
|
+
'VirtualPropertyFunction',
|
|
81
|
+
'VirtualPropertyMapping',
|
|
82
|
+
'TagsTuple',
|
|
83
|
+
'TagsByteTuple',
|
|
84
|
+
'CompiledToken',
|
|
85
|
+
'CompiledTemplate',
|
|
86
|
+
'CompiledTemplateCache',
|
|
87
|
+
'CacheMakeKeyFunction',
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
K = typing.TypeVar('K')
|
|
91
|
+
"""Generic."""
|
|
92
|
+
|
|
93
|
+
T = typing.TypeVar('T')
|
|
94
|
+
"""Generic."""
|
|
95
|
+
|
|
96
|
+
D = typing.TypeVar('D')
|
|
97
|
+
"""Generic."""
|
|
98
|
+
|
|
99
|
+
TString = typing.TypeVar('TString', str, bytes)
|
|
100
|
+
"""String/bytes generic."""
|
|
101
|
+
|
|
102
|
+
PartialResolver = collections.abc.Callable[[typing.AnyStr], typing.AnyStr]
|
|
103
|
+
"""Template partial tag resolver function interface."""
|
|
104
|
+
|
|
105
|
+
PropertyGetter = collections.abc.Callable[
|
|
106
|
+
[typing.Any, collections.abc.Sequence, typing.AnyStr, typing.Any],
|
|
107
|
+
typing.Any,
|
|
108
|
+
]
|
|
109
|
+
"""Template property getter function interface."""
|
|
110
|
+
|
|
111
|
+
StringifyFunction = collections.abc.Callable[[bytes, bool], bytes]
|
|
112
|
+
"""Template variable general stringification function interface."""
|
|
113
|
+
|
|
114
|
+
EscapeFunction = collections.abc.Callable[[bytes], bytes]
|
|
115
|
+
"""Template variable value escape function interface."""
|
|
116
|
+
|
|
117
|
+
LambdaRenderFunctionConstructor = collections.abc.Callable[
|
|
118
|
+
...,
|
|
119
|
+
collections.abc.Callable[..., typing.AnyStr],
|
|
120
|
+
]
|
|
121
|
+
"""Lambda render function constructor interface."""
|
|
122
|
+
|
|
123
|
+
LambdaRenderFunctionFactory = LambdaRenderFunctionConstructor
|
|
124
|
+
"""
|
|
125
|
+
.. deprecated:: 0.1.3
|
|
126
|
+
Use :attr:`LambdaRenderFunctionConstructor` instead.
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
VirtualPropertyFunction = collections.abc.Callable[[typing.Any], typing.Any]
|
|
131
|
+
"""Virtual property implementation callable interface."""
|
|
132
|
+
|
|
133
|
+
VirtualPropertyMapping = collections.abc.Mapping[str, VirtualPropertyFunction]
|
|
134
|
+
"""Virtual property mapping interface."""
|
|
135
|
+
|
|
136
|
+
TagsTuple = tuple[typing.AnyStr, typing.AnyStr]
|
|
137
|
+
"""Mustache tag tuple interface."""
|
|
138
|
+
|
|
139
|
+
TagsByteTuple = tuple[bytes, bytes]
|
|
140
|
+
"""Mustache tag byte tuple interface."""
|
|
141
|
+
|
|
142
|
+
CompiledToken = tuple[bool, bool, bool, bytes, bytes, int]
|
|
143
|
+
"""
|
|
144
|
+
Compiled template token.
|
|
145
|
+
|
|
146
|
+
Tokens are tuples containing a renderer decision path, key, content and flags.
|
|
147
|
+
|
|
148
|
+
``type: bool``
|
|
149
|
+
Decision for rendering path node `a`.
|
|
150
|
+
|
|
151
|
+
``type: bool``
|
|
152
|
+
Decision for rendering path node `b`.
|
|
153
|
+
|
|
154
|
+
``type: bool``
|
|
155
|
+
Decision for rendering path node `c`
|
|
156
|
+
|
|
157
|
+
``key: bytes``
|
|
158
|
+
Template substring with token scope key.
|
|
159
|
+
|
|
160
|
+
``content: bytes`
|
|
161
|
+
Template substring with token content data.
|
|
162
|
+
|
|
163
|
+
``flags: int``
|
|
164
|
+
Token flags.
|
|
165
|
+
|
|
166
|
+
- Unused: ``-1`` (default)
|
|
167
|
+
- Variable flags:
|
|
168
|
+
- ``0`` - escaped
|
|
169
|
+
- ``1`` - unescaped
|
|
170
|
+
- Block start flags:
|
|
171
|
+
- ``0`` - falsy
|
|
172
|
+
- ``1`` - truthy
|
|
173
|
+
- Block end value: block content index.
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
CompiledTemplate = tuple[CompiledToken, ...]
|
|
178
|
+
"""
|
|
179
|
+
Compiled template interface.
|
|
180
|
+
|
|
181
|
+
.. seealso::
|
|
182
|
+
|
|
183
|
+
:py:attr:`mstache.CompiledToken`
|
|
184
|
+
Item type.
|
|
185
|
+
|
|
186
|
+
:py:attr:`mstache.CompiledTemplateCache`
|
|
187
|
+
Interface exposing this type.
|
|
188
|
+
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class CompiledTemplateCache(typing.Protocol):
|
|
193
|
+
"""
|
|
194
|
+
Cache object protocol.
|
|
195
|
+
|
|
196
|
+
.. seealso::
|
|
197
|
+
|
|
198
|
+
:py:attr:`mstache.CompiledTemplate`
|
|
199
|
+
Item type.
|
|
200
|
+
|
|
201
|
+
:py:attr:`mstache.CacheMakeKeyFunction`
|
|
202
|
+
Cache key function interface.
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def get(self, key: K) -> CompiledTemplate | None:
|
|
207
|
+
"""Get compiled template from key, if any."""
|
|
208
|
+
|
|
209
|
+
def __setitem__(self, key: K, value: CompiledTemplate) -> None:
|
|
210
|
+
"""Assign compiled template to key."""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
CacheMakeKeyFunction = typing.Callable[
|
|
214
|
+
[tuple[bytes, bytes, bytes, bool]],
|
|
215
|
+
collections.abc.Hashable,
|
|
216
|
+
]
|
|
217
|
+
"""
|
|
218
|
+
Cache mapping key function interface.
|
|
219
|
+
|
|
220
|
+
.. seealso::
|
|
221
|
+
|
|
222
|
+
:py:attr:`mstache.CompiledTemplateCache`
|
|
223
|
+
Interface exposing this type.
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class LRUCache(collections.OrderedDict, typing.Generic[T]):
|
|
229
|
+
"""Capped mapping discarding least recently used elements."""
|
|
230
|
+
|
|
231
|
+
def __init__(self, maxsize: int, *args, **kwargs) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Initialize.
|
|
234
|
+
|
|
235
|
+
:param maxsize: maximum number of elements will be kept
|
|
236
|
+
|
|
237
|
+
Any parameter excess will be passed straight to dict constructor.
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
self.maxsize = maxsize
|
|
241
|
+
super().__init__(*args, **kwargs)
|
|
242
|
+
|
|
243
|
+
def get(self, key: collections.abc.Hashable, default: D = None) -> T | D:
|
|
244
|
+
"""
|
|
245
|
+
Get value for given key or default if not present.
|
|
246
|
+
|
|
247
|
+
:param key: hashable
|
|
248
|
+
:param default: value will be returned if key is not present
|
|
249
|
+
:returns: value if key is present, default if not
|
|
250
|
+
|
|
251
|
+
"""
|
|
252
|
+
try:
|
|
253
|
+
return self[key]
|
|
254
|
+
except KeyError:
|
|
255
|
+
return default # type: ignore
|
|
256
|
+
|
|
257
|
+
def __getitem__(self, key: collections.abc.Hashable) -> T:
|
|
258
|
+
"""
|
|
259
|
+
Get value for given key.
|
|
260
|
+
|
|
261
|
+
:param key: hashable
|
|
262
|
+
:returns: value if key is present
|
|
263
|
+
:raises KeyError: if key is not present
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
self.move_to_end(key)
|
|
267
|
+
return super().__getitem__(key)
|
|
268
|
+
|
|
269
|
+
def __setitem__(self, key: collections.abc.Hashable, value: T) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Set value for given key.
|
|
272
|
+
|
|
273
|
+
:param key: hashable will be used to retrieve values later on
|
|
274
|
+
:param value: value for given key
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
super().__setitem__(key, value)
|
|
278
|
+
try:
|
|
279
|
+
self.move_to_end(key)
|
|
280
|
+
while len(self) > self.maxsize:
|
|
281
|
+
self.popitem(last=False)
|
|
282
|
+
except KeyError: # race condition
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
default_recursion_limit = 1024
|
|
287
|
+
"""
|
|
288
|
+
Default number of maximum nested renders.
|
|
289
|
+
|
|
290
|
+
A nested render happens whenener the lambda render parameter is used or when
|
|
291
|
+
a partial template is injected.
|
|
292
|
+
|
|
293
|
+
Meant to avoid recursion bombs on templates.
|
|
294
|
+
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
default_tags = b'{{', b'}}'
|
|
298
|
+
"""Tuple of default mustache tags as in `tuple[bytes, bytes]`."""
|
|
299
|
+
|
|
300
|
+
default_cache: CompiledTemplateCache = LRUCache(1024)
|
|
301
|
+
"""
|
|
302
|
+
Default template cache mapping, keeping the 1024 most recently used
|
|
303
|
+
compiled templates (LRU expiration).
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
default_cache_make_key: CacheMakeKeyFunction = tuple
|
|
307
|
+
"""
|
|
308
|
+
Defaut template cache key function, :py:class:`tuple`, so keys would be
|
|
309
|
+
as in `tuple[bytes, bytes, bytes, bool]` containing the relevant
|
|
310
|
+
:py:func:`mstache.tokenizer` parameters.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def virtual_length(ref: collections.abc.Sized) -> int:
|
|
315
|
+
"""
|
|
316
|
+
Resolve virtual length property.
|
|
317
|
+
|
|
318
|
+
:param ref: any non-mapping object implementing `__len__`
|
|
319
|
+
:returns: number of items
|
|
320
|
+
:raises TypeError: if ref is mapping or has no `__len__` method
|
|
321
|
+
|
|
322
|
+
"""
|
|
323
|
+
if isinstance(ref, collections.abc.Mapping):
|
|
324
|
+
raise TypeError
|
|
325
|
+
return len(ref)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
default_virtuals: VirtualPropertyMapping = types.MappingProxyType({
|
|
329
|
+
'length': virtual_length,
|
|
330
|
+
})
|
|
331
|
+
"""
|
|
332
|
+
Immutable mapping with default virtual properties.
|
|
333
|
+
|
|
334
|
+
The following virtual properties are implemented:
|
|
335
|
+
|
|
336
|
+
- **length**, for non-mapping sized objects, returning ``len(ref)``.
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
TOKEN_TYPES = tuple({
|
|
341
|
+
**dict.fromkeys(range(0x100), (True, False, True, False)),
|
|
342
|
+
0x21: (False, False, False, False), # '!'
|
|
343
|
+
0x23: (False, True, True, False), # '#'
|
|
344
|
+
0x26: (True, True, True, True), # '&'
|
|
345
|
+
0x2F: (False, True, False, False), # '/'
|
|
346
|
+
0x3D: (False, False, False, True), # '='
|
|
347
|
+
0x3E: (False, False, True, False), # '>'
|
|
348
|
+
0x5E: (False, True, True, True), # '^'
|
|
349
|
+
0x7B: (True, True, False, True), # '{'
|
|
350
|
+
}.values())
|
|
351
|
+
"""ASCII-indexed tokenizer decision matrix."""
|
|
352
|
+
|
|
353
|
+
COMMON_FALSY: tuple = None, False, 0, '', b'', (), [], frozenset()
|
|
354
|
+
NON_LOOPING_TYPES: tuple = str, bytes, collections.abc.Mapping
|
|
355
|
+
EMPTY = b''
|
|
356
|
+
SELF_SCOPE = frozenset((b'.', '.'))
|
|
357
|
+
MISSING = object()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class TokenException(SyntaxError):
|
|
361
|
+
"""Invalid token found during tokenization."""
|
|
362
|
+
|
|
363
|
+
_fmt = 'Invalid tag {tag} at line {row} column {column}'
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def from_template(
|
|
367
|
+
cls,
|
|
368
|
+
template: bytes,
|
|
369
|
+
start: int,
|
|
370
|
+
end: int,
|
|
371
|
+
) -> 'TokenException':
|
|
372
|
+
"""
|
|
373
|
+
Create exception instance from parsing data.
|
|
374
|
+
|
|
375
|
+
:param template: template bytes
|
|
376
|
+
:param start: character position where the offending tag starts at
|
|
377
|
+
:param end: character position where the offending tag ends at
|
|
378
|
+
:returns: exception instance
|
|
379
|
+
|
|
380
|
+
"""
|
|
381
|
+
tag = template[start:end].decode(errors='replace')
|
|
382
|
+
row = 1 + template[:start].count(b'\n')
|
|
383
|
+
column = 1 + start - max(0, template.rfind(b'\n', 0, start))
|
|
384
|
+
return cls(cls._fmt.format(tag=tag, row=row, column=column))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class ClosingTokenException(TokenException):
|
|
388
|
+
"""Non-matching closing token found during tokenization."""
|
|
389
|
+
|
|
390
|
+
_fmt = 'Non-matching tag {tag} at line {row} column {column}'
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class UnclosedTokenException(ClosingTokenException):
|
|
394
|
+
"""Unclosed token found during tokenization."""
|
|
395
|
+
|
|
396
|
+
_fmt = 'Unclosed tag {tag} at line {row} column {column}'
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class DelimiterTokenException(TokenException):
|
|
400
|
+
"""
|
|
401
|
+
Invalid delimiters token found during tokenization.
|
|
402
|
+
|
|
403
|
+
.. versionadded:: 0.1.1
|
|
404
|
+
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
_fmt = 'Invalid delimiters {tag} at line {row} column {column}'
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class TemplateRecursionError(RecursionError):
|
|
411
|
+
"""Template rendering exceeded maximum render recursion."""
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def default_stringify(data: typing.Any, text: bool) -> bytes:
|
|
415
|
+
"""
|
|
416
|
+
Convert arbitrary data to bytes.
|
|
417
|
+
|
|
418
|
+
:param data: value will be serialized
|
|
419
|
+
:param text: whether running in text mode or not (bytes mode)
|
|
420
|
+
:returns: template bytes
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
return (
|
|
424
|
+
data if isinstance(data, bytes) and not text else
|
|
425
|
+
f'{data}'.encode()
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def default_escape(data: bytes) -> bytes:
|
|
430
|
+
"""
|
|
431
|
+
Convert bytes conflicting with HTML to their escape sequences.
|
|
432
|
+
|
|
433
|
+
:param data: bytes containing text
|
|
434
|
+
:returns: escaped text bytes
|
|
435
|
+
|
|
436
|
+
"""
|
|
437
|
+
return (
|
|
438
|
+
data
|
|
439
|
+
.replace(b'&', b'&')
|
|
440
|
+
.replace(b'<', b'<')
|
|
441
|
+
.replace(b'>', b'>')
|
|
442
|
+
.replace(b'"', b'"')
|
|
443
|
+
.replace(b"'", b'`')
|
|
444
|
+
.replace(b'`', b'=')
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def default_resolver(name: typing.AnyStr) -> bytes:
|
|
449
|
+
"""
|
|
450
|
+
Mustache partial resolver function (stub).
|
|
451
|
+
|
|
452
|
+
:param name: partial template name
|
|
453
|
+
:returns: empty bytes
|
|
454
|
+
|
|
455
|
+
"""
|
|
456
|
+
return EMPTY
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def default_getter(
|
|
460
|
+
scope: typing.Any,
|
|
461
|
+
scopes: collections.abc.Iterable,
|
|
462
|
+
key: typing.AnyStr,
|
|
463
|
+
default: typing.Any = None,
|
|
464
|
+
*,
|
|
465
|
+
virtuals: VirtualPropertyMapping = default_virtuals,
|
|
466
|
+
) -> typing.Any:
|
|
467
|
+
"""
|
|
468
|
+
Extract property value from scope hierarchy.
|
|
469
|
+
|
|
470
|
+
:param scope: uppermost scope (corresponding to key ``'.'``)
|
|
471
|
+
:param scopes: parent scope sequence
|
|
472
|
+
:param key: property key
|
|
473
|
+
:param default: value will be used as default when missing
|
|
474
|
+
:param virtuals: mapping of virtual property callables
|
|
475
|
+
:returns: value from scope or default
|
|
476
|
+
|
|
477
|
+
.. versionadded:: 0.1.3
|
|
478
|
+
*virtuals* parameter.
|
|
479
|
+
|
|
480
|
+
Both :class:`AttributeError` and :class:`TypeError` exceptions
|
|
481
|
+
raised by virtual property implementations will be handled as if
|
|
482
|
+
that property doesn't exist, which can be useful to filter out
|
|
483
|
+
incompatible types.
|
|
484
|
+
|
|
485
|
+
"""
|
|
486
|
+
if key in SELF_SCOPE:
|
|
487
|
+
return scope
|
|
488
|
+
|
|
489
|
+
binary_mode = not isinstance(key, str)
|
|
490
|
+
components = key.split(b'.' if binary_mode else '.') # type: ignore
|
|
491
|
+
for ref in (*scopes, scope)[::-1]:
|
|
492
|
+
for name in components:
|
|
493
|
+
if binary_mode:
|
|
494
|
+
try:
|
|
495
|
+
ref = ref[name]
|
|
496
|
+
continue
|
|
497
|
+
except (KeyError, TypeError, AttributeError):
|
|
498
|
+
pass
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
name = name.decode() # type: ignore
|
|
502
|
+
except UnicodeDecodeError:
|
|
503
|
+
break
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
ref = ref[name]
|
|
507
|
+
continue
|
|
508
|
+
except (KeyError, TypeError, AttributeError):
|
|
509
|
+
pass
|
|
510
|
+
|
|
511
|
+
if name.isdigit():
|
|
512
|
+
try:
|
|
513
|
+
ref = ref[int(name)]
|
|
514
|
+
continue
|
|
515
|
+
except (TypeError, KeyError, IndexError):
|
|
516
|
+
pass
|
|
517
|
+
else:
|
|
518
|
+
try:
|
|
519
|
+
ref = getattr(ref, name) # type: ignore
|
|
520
|
+
continue
|
|
521
|
+
except AttributeError:
|
|
522
|
+
pass
|
|
523
|
+
|
|
524
|
+
try:
|
|
525
|
+
ref = virtuals[name](ref) # type: ignore
|
|
526
|
+
continue
|
|
527
|
+
except (KeyError, TypeError, AttributeError):
|
|
528
|
+
pass
|
|
529
|
+
|
|
530
|
+
break
|
|
531
|
+
|
|
532
|
+
else:
|
|
533
|
+
return ref
|
|
534
|
+
|
|
535
|
+
return default
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def default_lambda_render(
|
|
539
|
+
scope: typing.Any,
|
|
540
|
+
**kwargs,
|
|
541
|
+
) -> collections.abc.Callable[[TString], TString]:
|
|
542
|
+
r"""
|
|
543
|
+
Generate a template-only render function with fixed parameters.
|
|
544
|
+
|
|
545
|
+
:param scope: current scope
|
|
546
|
+
:param \**kwargs: parameters forwarded to :func:`render`
|
|
547
|
+
:returns: template render function
|
|
548
|
+
|
|
549
|
+
"""
|
|
550
|
+
|
|
551
|
+
def lambda_render(template: TString) -> TString:
|
|
552
|
+
"""
|
|
553
|
+
Render given template to string/bytes.
|
|
554
|
+
|
|
555
|
+
:param template: template text
|
|
556
|
+
:returns: rendered string or bytes (depending on template type)
|
|
557
|
+
|
|
558
|
+
"""
|
|
559
|
+
return render(template, scope, **kwargs)
|
|
560
|
+
|
|
561
|
+
return lambda_render
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def tokenize(
|
|
565
|
+
template: bytes,
|
|
566
|
+
*,
|
|
567
|
+
tags: TagsByteTuple = default_tags,
|
|
568
|
+
comments: bool = False,
|
|
569
|
+
cache: CompiledTemplateCache = default_cache,
|
|
570
|
+
cache_make_key: CacheMakeKeyFunction = default_cache_make_key,
|
|
571
|
+
) -> CompiledTemplate:
|
|
572
|
+
"""
|
|
573
|
+
Compile mustache template as a tuple of token tuples.
|
|
574
|
+
|
|
575
|
+
:param template: template as utf-8 encoded bytes
|
|
576
|
+
:param tags: mustache tag tuple (open, close)
|
|
577
|
+
:param comments: whether yield comment tokens or not (ignore comments)
|
|
578
|
+
:param cache: mutable mapping for compiled template cache
|
|
579
|
+
:param cache_make_key: key function for compiled template cache
|
|
580
|
+
:returns: tuple of token tuples
|
|
581
|
+
|
|
582
|
+
:raises UnclosedTokenException: if token is left unclosed
|
|
583
|
+
:raises ClosingTokenException: if block closing token does not match
|
|
584
|
+
:raises DelimiterTokenException: if delimiter token syntax is invalid
|
|
585
|
+
|
|
586
|
+
"""
|
|
587
|
+
tokenization_key = cache_make_key((template, *tags, comments))
|
|
588
|
+
|
|
589
|
+
if cached := cache.get(tokenization_key):
|
|
590
|
+
return cached
|
|
591
|
+
|
|
592
|
+
template_find = template.find
|
|
593
|
+
|
|
594
|
+
stack: list[tuple[bytes | None, int, int, int]] = []
|
|
595
|
+
stack_append = stack.append
|
|
596
|
+
stack_pop = stack.pop
|
|
597
|
+
scope_label = None
|
|
598
|
+
scope_head = 0
|
|
599
|
+
scope_start = 0
|
|
600
|
+
scope_index = 0
|
|
601
|
+
|
|
602
|
+
empty = EMPTY
|
|
603
|
+
start_tag, end_tag = tags
|
|
604
|
+
end_literal = b'}' + end_tag
|
|
605
|
+
end_switch = b'=' + end_tag
|
|
606
|
+
start_len = len(start_tag)
|
|
607
|
+
end_len = len(end_tag)
|
|
608
|
+
|
|
609
|
+
token_types = TOKEN_TYPES
|
|
610
|
+
recording: list[CompiledToken] = []
|
|
611
|
+
record = recording.append
|
|
612
|
+
|
|
613
|
+
text_start, text_end = 0, template_find(start_tag)
|
|
614
|
+
while text_end != -1:
|
|
615
|
+
if text_start < text_end: # text
|
|
616
|
+
record((
|
|
617
|
+
False, True, False,
|
|
618
|
+
empty,
|
|
619
|
+
template[text_start:text_end],
|
|
620
|
+
-1,
|
|
621
|
+
))
|
|
622
|
+
|
|
623
|
+
tag_start = text_end + start_len
|
|
624
|
+
try:
|
|
625
|
+
a, b, c, d = token_types[template[tag_start]]
|
|
626
|
+
except IndexError:
|
|
627
|
+
raise UnclosedTokenException.from_template(
|
|
628
|
+
template=template,
|
|
629
|
+
start=text_end,
|
|
630
|
+
end=tag_start,
|
|
631
|
+
) from None
|
|
632
|
+
|
|
633
|
+
if a: # variables
|
|
634
|
+
tag_start += b
|
|
635
|
+
|
|
636
|
+
if c: # variable
|
|
637
|
+
tag_end = template_find(end_tag, tag_start)
|
|
638
|
+
text_start = tag_end + end_len
|
|
639
|
+
|
|
640
|
+
else: # triple-keyed variable
|
|
641
|
+
tag_end = template_find(end_literal, tag_start)
|
|
642
|
+
text_start = tag_end + end_len + 1
|
|
643
|
+
|
|
644
|
+
record((
|
|
645
|
+
False, True, True,
|
|
646
|
+
template[tag_start:tag_end].strip(),
|
|
647
|
+
empty,
|
|
648
|
+
d,
|
|
649
|
+
))
|
|
650
|
+
|
|
651
|
+
elif b:
|
|
652
|
+
tag_start += 1
|
|
653
|
+
tag_end = template_find(end_tag, tag_start)
|
|
654
|
+
text_start = tag_end + end_len
|
|
655
|
+
|
|
656
|
+
if c: # block
|
|
657
|
+
stack_append((scope_label, text_end, scope_start, scope_index))
|
|
658
|
+
scope_label = template[tag_start:tag_end].strip()
|
|
659
|
+
scope_head = text_end
|
|
660
|
+
scope_start = text_start
|
|
661
|
+
scope_index = len(recording) + 1
|
|
662
|
+
record((
|
|
663
|
+
True, False, True,
|
|
664
|
+
scope_label,
|
|
665
|
+
empty,
|
|
666
|
+
d,
|
|
667
|
+
))
|
|
668
|
+
|
|
669
|
+
elif scope_label != template[tag_start:tag_end].strip():
|
|
670
|
+
raise ClosingTokenException.from_template(
|
|
671
|
+
template=template,
|
|
672
|
+
start=text_end,
|
|
673
|
+
end=text_start,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
else: # close / loop
|
|
677
|
+
record((
|
|
678
|
+
True, True, True,
|
|
679
|
+
scope_label,
|
|
680
|
+
template[scope_start:text_end].strip(),
|
|
681
|
+
scope_index,
|
|
682
|
+
))
|
|
683
|
+
scope_label, scope_head, scope_start, scope_index = stack_pop()
|
|
684
|
+
|
|
685
|
+
elif c: # partial
|
|
686
|
+
tag_start += 1
|
|
687
|
+
tag_end = template_find(end_tag, tag_start)
|
|
688
|
+
text_start = tag_end + end_len
|
|
689
|
+
record((
|
|
690
|
+
True, False, False,
|
|
691
|
+
template[tag_start:tag_end].strip(),
|
|
692
|
+
empty,
|
|
693
|
+
-1,
|
|
694
|
+
))
|
|
695
|
+
|
|
696
|
+
elif d: # tags
|
|
697
|
+
tag_start += 1
|
|
698
|
+
tag_end = template_find(end_switch, tag_start)
|
|
699
|
+
text_start = tag_end + end_len + 1
|
|
700
|
+
|
|
701
|
+
try:
|
|
702
|
+
start_tag, end_tag = template[tag_start:tag_end].split(b' ')
|
|
703
|
+
except ValueError:
|
|
704
|
+
raise DelimiterTokenException.from_template(
|
|
705
|
+
template=template,
|
|
706
|
+
start=text_end,
|
|
707
|
+
end=text_start,
|
|
708
|
+
) from None
|
|
709
|
+
|
|
710
|
+
if not (start_tag and end_tag):
|
|
711
|
+
raise DelimiterTokenException.from_template(
|
|
712
|
+
template=template,
|
|
713
|
+
start=text_end,
|
|
714
|
+
end=text_start,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
end_literal = b'}' + end_tag
|
|
718
|
+
end_switch = b'=' + end_tag
|
|
719
|
+
start_len = len(start_tag)
|
|
720
|
+
end_len = len(end_tag)
|
|
721
|
+
start_end = tag_start + start_len
|
|
722
|
+
end_start = tag_end - end_len
|
|
723
|
+
|
|
724
|
+
record((
|
|
725
|
+
False, False, True,
|
|
726
|
+
template[tag_start:start_end].strip(),
|
|
727
|
+
template[end_start:tag_end].strip(),
|
|
728
|
+
-1,
|
|
729
|
+
))
|
|
730
|
+
|
|
731
|
+
else: # comment
|
|
732
|
+
tag_start += 1
|
|
733
|
+
tag_end = template_find(end_tag, tag_start)
|
|
734
|
+
text_start = tag_end + end_len
|
|
735
|
+
|
|
736
|
+
if comments:
|
|
737
|
+
record((
|
|
738
|
+
False, False, False,
|
|
739
|
+
empty,
|
|
740
|
+
template[tag_start:tag_end].strip(),
|
|
741
|
+
-1,
|
|
742
|
+
))
|
|
743
|
+
|
|
744
|
+
if tag_end < 0:
|
|
745
|
+
raise UnclosedTokenException.from_template(
|
|
746
|
+
template=template,
|
|
747
|
+
start=tag_start,
|
|
748
|
+
end=tag_end,
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
text_end = template_find(start_tag, text_start)
|
|
752
|
+
|
|
753
|
+
if stack:
|
|
754
|
+
raise UnclosedTokenException.from_template(
|
|
755
|
+
template=template,
|
|
756
|
+
start=scope_head,
|
|
757
|
+
end=scope_start,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
if (tail := template[text_start:]) or not recording:
|
|
761
|
+
record((False, True, False, empty, tail, -1)) # text
|
|
762
|
+
|
|
763
|
+
tokens = cache[tokenization_key] = tuple(recording)
|
|
764
|
+
return tokens
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def process(
|
|
768
|
+
template: TString,
|
|
769
|
+
scope: typing.Any,
|
|
770
|
+
*,
|
|
771
|
+
scopes: collections.abc.Iterable = (),
|
|
772
|
+
resolver: PartialResolver = default_resolver,
|
|
773
|
+
getter: PropertyGetter = default_getter,
|
|
774
|
+
stringify: StringifyFunction = default_stringify,
|
|
775
|
+
escape: EscapeFunction = default_escape,
|
|
776
|
+
lambda_render: LambdaRenderFunctionConstructor = default_lambda_render,
|
|
777
|
+
tags: TagsTuple = default_tags,
|
|
778
|
+
cache: CompiledTemplateCache = default_cache,
|
|
779
|
+
cache_make_key: CacheMakeKeyFunction = default_cache_make_key,
|
|
780
|
+
recursion_limit: int = default_recursion_limit,
|
|
781
|
+
) -> collections.abc.Generator[bytes, None, None]:
|
|
782
|
+
"""
|
|
783
|
+
Generate rendered mustache template byte chunks.
|
|
784
|
+
|
|
785
|
+
:param template: mustache template string
|
|
786
|
+
:param scope: root object used as root mustache scope
|
|
787
|
+
:param scopes: iterable of parent scopes
|
|
788
|
+
:param resolver: callable will be used to resolve partials (bytes)
|
|
789
|
+
:param getter: callable will be used to pick variables from scope
|
|
790
|
+
:param stringify: callable will be used to render python types (bytes)
|
|
791
|
+
:param escape: callable will be used to escape template (bytes)
|
|
792
|
+
:param lambda_render: lambda render function constructor
|
|
793
|
+
:param tags: mustache tag tuple (open, close)
|
|
794
|
+
:param cache: mutable mapping for compiled template cache
|
|
795
|
+
:param cache_make_key: key function for compiled template cache
|
|
796
|
+
:param recursion_limit: maximum number of nested render operations
|
|
797
|
+
:returns: byte chunk generator
|
|
798
|
+
|
|
799
|
+
:raises UnclosedTokenException: if token is left unclosed
|
|
800
|
+
:raises ClosingTokenException: if block closing token does not match
|
|
801
|
+
:raises DelimiterTokenException: if delimiter token syntax is invalid
|
|
802
|
+
:raises TemplateRecursionError: if rendering recursion limit is exceeded
|
|
803
|
+
|
|
804
|
+
"""
|
|
805
|
+
if recursion_limit < 0:
|
|
806
|
+
message = 'Template rendering exceeded maximum render recursion'
|
|
807
|
+
raise TemplateRecursionError(message)
|
|
808
|
+
|
|
809
|
+
text_mode = isinstance(template, str)
|
|
810
|
+
next_recursion_level = recursion_limit - 1
|
|
811
|
+
|
|
812
|
+
# encoding
|
|
813
|
+
data: bytes = (
|
|
814
|
+
template.encode() if text_mode else # type: ignore
|
|
815
|
+
template
|
|
816
|
+
)
|
|
817
|
+
current_tags: TagsByteTuple = tuple( # type: ignore
|
|
818
|
+
tag.encode() if isinstance(tag, str) else tag
|
|
819
|
+
for tag in tags[:2]
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
# current context
|
|
823
|
+
siblings: collections.abc.Iterator | None = None
|
|
824
|
+
callback = False
|
|
825
|
+
silent = False
|
|
826
|
+
|
|
827
|
+
# context stack
|
|
828
|
+
stack: list[tuple[collections.abc.Iterator | None, bool, bool]] = []
|
|
829
|
+
stack_append = stack.append
|
|
830
|
+
stack_pop = stack.pop
|
|
831
|
+
|
|
832
|
+
# scope stack
|
|
833
|
+
scopes = list(scopes)
|
|
834
|
+
scopes_append = scopes.append
|
|
835
|
+
scopes_pop = scopes.pop
|
|
836
|
+
|
|
837
|
+
# locals
|
|
838
|
+
decode: typing.Callable[[bytes], typing.AnyStr] = (
|
|
839
|
+
bytes.decode if text_mode else # type: ignore
|
|
840
|
+
bytes
|
|
841
|
+
)
|
|
842
|
+
isnan = math.isnan
|
|
843
|
+
missing = MISSING
|
|
844
|
+
falsies = COMMON_FALSY
|
|
845
|
+
non_looping = NON_LOOPING_TYPES
|
|
846
|
+
mappings = collections.abc.Mapping
|
|
847
|
+
|
|
848
|
+
start = 0
|
|
849
|
+
tokens = tokenized = tokenize(
|
|
850
|
+
data,
|
|
851
|
+
tags=current_tags,
|
|
852
|
+
cache=cache,
|
|
853
|
+
cache_make_key=cache_make_key,
|
|
854
|
+
)
|
|
855
|
+
while True:
|
|
856
|
+
for a, b, c, token_name, token_content, token_option in tokens:
|
|
857
|
+
if silent:
|
|
858
|
+
if a:
|
|
859
|
+
if b: # close / loop
|
|
860
|
+
closing_scope = scope
|
|
861
|
+
closing_callback = callback
|
|
862
|
+
|
|
863
|
+
scope = scopes_pop()
|
|
864
|
+
siblings, callback, silent = stack_pop()
|
|
865
|
+
|
|
866
|
+
if closing_callback and not silent:
|
|
867
|
+
yield stringify(
|
|
868
|
+
closing_scope(
|
|
869
|
+
decode(token_content),
|
|
870
|
+
lambda_render(
|
|
871
|
+
scope=scope,
|
|
872
|
+
scopes=scopes,
|
|
873
|
+
resolver=resolver,
|
|
874
|
+
getter=getter,
|
|
875
|
+
stringify=stringify,
|
|
876
|
+
escape=escape,
|
|
877
|
+
lambda_render=lambda_render,
|
|
878
|
+
tags=current_tags,
|
|
879
|
+
cache=cache,
|
|
880
|
+
cache_make_key=cache_make_key,
|
|
881
|
+
recursion_limit=next_recursion_level,
|
|
882
|
+
),
|
|
883
|
+
),
|
|
884
|
+
text_mode,
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
elif c: # block
|
|
888
|
+
scopes_append(scope)
|
|
889
|
+
stack_append((siblings, callback, silent))
|
|
890
|
+
|
|
891
|
+
elif a:
|
|
892
|
+
if b: # close / loop
|
|
893
|
+
if siblings:
|
|
894
|
+
try:
|
|
895
|
+
scope = next(siblings)
|
|
896
|
+
callback = callable(scope)
|
|
897
|
+
silent = callback
|
|
898
|
+
|
|
899
|
+
if start != token_option:
|
|
900
|
+
start = token_option
|
|
901
|
+
tokens = tokenized[start:]
|
|
902
|
+
|
|
903
|
+
break # restart
|
|
904
|
+
except StopIteration:
|
|
905
|
+
scope = scopes_pop()
|
|
906
|
+
siblings, callback, silent = stack_pop()
|
|
907
|
+
else:
|
|
908
|
+
scope = scopes_pop()
|
|
909
|
+
siblings, callback, silent = stack_pop()
|
|
910
|
+
|
|
911
|
+
elif c: # block
|
|
912
|
+
curr = scope
|
|
913
|
+
scope = getter(curr, scopes, decode(token_name), None)
|
|
914
|
+
|
|
915
|
+
scopes_append(curr)
|
|
916
|
+
stack_append((siblings, callback, silent))
|
|
917
|
+
|
|
918
|
+
falsy = (
|
|
919
|
+
hasattr(scope, '__float__') and isnan(scope)
|
|
920
|
+
if scope else
|
|
921
|
+
scope in falsies or not isinstance(scope, mappings)
|
|
922
|
+
)
|
|
923
|
+
if token_option: # falsy block
|
|
924
|
+
siblings = None
|
|
925
|
+
callback = False
|
|
926
|
+
silent = not falsy
|
|
927
|
+
elif falsy: # truthy block with falsy value
|
|
928
|
+
siblings = None
|
|
929
|
+
callback = False
|
|
930
|
+
silent = True
|
|
931
|
+
elif (hasattr(scope, '__iter__')
|
|
932
|
+
and not isinstance(scope, non_looping)): # loop
|
|
933
|
+
try:
|
|
934
|
+
siblings = iter(scope)
|
|
935
|
+
scope = next(siblings) # type: ignore
|
|
936
|
+
callback = callable(scope)
|
|
937
|
+
silent = callback
|
|
938
|
+
except StopIteration:
|
|
939
|
+
siblings = None
|
|
940
|
+
scope = None
|
|
941
|
+
callback = False
|
|
942
|
+
silent = True
|
|
943
|
+
else: # truthy block with truthy value
|
|
944
|
+
siblings = None
|
|
945
|
+
callback = callable(scope)
|
|
946
|
+
silent = callback
|
|
947
|
+
|
|
948
|
+
else: # partial
|
|
949
|
+
value = resolver(decode(token_name))
|
|
950
|
+
if value:
|
|
951
|
+
yield from process(
|
|
952
|
+
template=value,
|
|
953
|
+
scope=scope,
|
|
954
|
+
scopes=scopes,
|
|
955
|
+
resolver=resolver,
|
|
956
|
+
getter=getter,
|
|
957
|
+
stringify=stringify,
|
|
958
|
+
escape=escape,
|
|
959
|
+
lambda_render=lambda_render,
|
|
960
|
+
tags=current_tags,
|
|
961
|
+
cache=cache,
|
|
962
|
+
cache_make_key=cache_make_key,
|
|
963
|
+
recursion_limit=next_recursion_level,
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
elif b:
|
|
967
|
+
if c: # variable
|
|
968
|
+
value = getter(scope, scopes, decode(token_name), missing)
|
|
969
|
+
if value is not missing:
|
|
970
|
+
yield (
|
|
971
|
+
stringify(value, text_mode) if token_option else
|
|
972
|
+
escape(stringify(value, text_mode))
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
else: # text
|
|
976
|
+
yield token_content
|
|
977
|
+
|
|
978
|
+
elif c: # tags
|
|
979
|
+
current_tags = token_name, token_content
|
|
980
|
+
|
|
981
|
+
# else comment
|
|
982
|
+
|
|
983
|
+
else:
|
|
984
|
+
break
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def stream(
|
|
988
|
+
template: TString,
|
|
989
|
+
scope: typing.Any,
|
|
990
|
+
*,
|
|
991
|
+
scopes: collections.abc.Iterable = (),
|
|
992
|
+
resolver: PartialResolver = default_resolver,
|
|
993
|
+
getter: PropertyGetter = default_getter,
|
|
994
|
+
stringify: StringifyFunction = default_stringify,
|
|
995
|
+
escape: EscapeFunction = default_escape,
|
|
996
|
+
lambda_render: LambdaRenderFunctionConstructor = default_lambda_render,
|
|
997
|
+
tags: TagsTuple = default_tags,
|
|
998
|
+
cache: CompiledTemplateCache = default_cache,
|
|
999
|
+
cache_make_key: CacheMakeKeyFunction = default_cache_make_key,
|
|
1000
|
+
recursion_limit: int = default_recursion_limit,
|
|
1001
|
+
) -> collections.abc.Generator[TString, None, None]:
|
|
1002
|
+
"""
|
|
1003
|
+
Generate rendered mustache template chunks.
|
|
1004
|
+
|
|
1005
|
+
:param template: mustache template (str or bytes)
|
|
1006
|
+
:param scope: current rendering scope (data object)
|
|
1007
|
+
:param scopes: list of precedent scopes
|
|
1008
|
+
:param resolver: callable will be used to resolve partials (bytes)
|
|
1009
|
+
:param getter: callable will be used to pick variables from scope
|
|
1010
|
+
:param stringify: callable will be used to render python types (bytes)
|
|
1011
|
+
:param escape: callable will be used to escape template (bytes)
|
|
1012
|
+
:param lambda_render: lambda render function constructor
|
|
1013
|
+
:param tags: tuple (start, end) specifying the initial mustache delimiters
|
|
1014
|
+
:param cache: mutable mapping for compiled template cache
|
|
1015
|
+
:param cache_make_key: key function for compiled template cache
|
|
1016
|
+
:param recursion_limit: maximum number of nested render operations
|
|
1017
|
+
:returns: generator of bytes/str chunks (same type as template)
|
|
1018
|
+
|
|
1019
|
+
:raises UnclosedTokenException: if token is left unclosed
|
|
1020
|
+
:raises ClosingTokenException: if block closing token does not match
|
|
1021
|
+
:raises DelimiterTokenException: if delimiter token syntax is invalid
|
|
1022
|
+
:raises RenderingRecursionError: if rendering recursion limit is exceeded
|
|
1023
|
+
|
|
1024
|
+
"""
|
|
1025
|
+
chunks = process(
|
|
1026
|
+
template=template,
|
|
1027
|
+
scope=scope,
|
|
1028
|
+
scopes=scopes,
|
|
1029
|
+
resolver=resolver,
|
|
1030
|
+
getter=getter,
|
|
1031
|
+
stringify=stringify,
|
|
1032
|
+
escape=escape,
|
|
1033
|
+
lambda_render=lambda_render,
|
|
1034
|
+
tags=tags,
|
|
1035
|
+
cache=cache,
|
|
1036
|
+
cache_make_key=cache_make_key,
|
|
1037
|
+
recursion_limit=recursion_limit,
|
|
1038
|
+
)
|
|
1039
|
+
return (
|
|
1040
|
+
codecs.iterdecode(chunks, 'utf8') if isinstance(template, str) else
|
|
1041
|
+
chunks
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def render(
|
|
1046
|
+
template: TString,
|
|
1047
|
+
scope: typing.Any,
|
|
1048
|
+
*,
|
|
1049
|
+
scopes: collections.abc.Iterable = (),
|
|
1050
|
+
resolver: PartialResolver = default_resolver,
|
|
1051
|
+
getter: PropertyGetter = default_getter,
|
|
1052
|
+
stringify: StringifyFunction = default_stringify,
|
|
1053
|
+
escape: EscapeFunction = default_escape,
|
|
1054
|
+
lambda_render: LambdaRenderFunctionConstructor = default_lambda_render,
|
|
1055
|
+
tags: TagsTuple = default_tags,
|
|
1056
|
+
cache: CompiledTemplateCache = default_cache,
|
|
1057
|
+
cache_make_key: CacheMakeKeyFunction = default_cache_make_key,
|
|
1058
|
+
recursion_limit: int = default_recursion_limit,
|
|
1059
|
+
) -> TString:
|
|
1060
|
+
"""
|
|
1061
|
+
Render mustache template.
|
|
1062
|
+
|
|
1063
|
+
:param template: mustache template
|
|
1064
|
+
:param scope: current rendering scope (data object)
|
|
1065
|
+
:param scopes: list of precedent scopes
|
|
1066
|
+
:param resolver: callable will be used to resolve partials (bytes)
|
|
1067
|
+
:param getter: callable will be used to pick variables from scope
|
|
1068
|
+
:param stringify: callable will be used to render python types (bytes)
|
|
1069
|
+
:param escape: callable will be used to escape template (bytes)
|
|
1070
|
+
:param lambda_render: lambda render function constructor
|
|
1071
|
+
:param tags: tuple (start, end) specifying the initial mustache delimiters
|
|
1072
|
+
:param cache: mutable mapping for compiled template cache
|
|
1073
|
+
:param cache_make_key: key function for compiled template cache
|
|
1074
|
+
:param recursion_limit: maximum number of nested render operations
|
|
1075
|
+
:returns: rendered bytes/str (type depends on template)
|
|
1076
|
+
|
|
1077
|
+
:raises UnclosedTokenException: if token is left unclosed
|
|
1078
|
+
:raises ClosingTokenException: if block closing token does not match
|
|
1079
|
+
:raises DelimiterTokenException: if delimiter token syntax is invalid
|
|
1080
|
+
:raises TemplateRecursionError: if rendering recursion limit is exceeded
|
|
1081
|
+
|
|
1082
|
+
"""
|
|
1083
|
+
data = b''.join(process(
|
|
1084
|
+
template=template,
|
|
1085
|
+
scope=scope,
|
|
1086
|
+
scopes=scopes,
|
|
1087
|
+
resolver=resolver,
|
|
1088
|
+
getter=getter,
|
|
1089
|
+
stringify=stringify,
|
|
1090
|
+
escape=escape,
|
|
1091
|
+
lambda_render=lambda_render,
|
|
1092
|
+
tags=tags,
|
|
1093
|
+
cache=cache,
|
|
1094
|
+
cache_make_key=cache_make_key,
|
|
1095
|
+
recursion_limit=recursion_limit,
|
|
1096
|
+
))
|
|
1097
|
+
return data.decode() if isinstance(template, str) else data
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def cli(argv: collections.abc.Sequence[str] | None = None) -> None:
|
|
1101
|
+
"""
|
|
1102
|
+
Render template from command line.
|
|
1103
|
+
|
|
1104
|
+
Use `python -m mstache --help` to check available options.
|
|
1105
|
+
|
|
1106
|
+
:param argv: command line arguments, :attr:`sys.argv` when None
|
|
1107
|
+
|
|
1108
|
+
"""
|
|
1109
|
+
import argparse
|
|
1110
|
+
import json
|
|
1111
|
+
import sys
|
|
1112
|
+
|
|
1113
|
+
arguments = argparse.ArgumentParser(
|
|
1114
|
+
description='Render mustache template.',
|
|
1115
|
+
)
|
|
1116
|
+
arguments.add_argument(
|
|
1117
|
+
'template',
|
|
1118
|
+
metavar='PATH',
|
|
1119
|
+
type=argparse.FileType('r'),
|
|
1120
|
+
help='template file',
|
|
1121
|
+
)
|
|
1122
|
+
arguments.add_argument(
|
|
1123
|
+
'-j', '--json',
|
|
1124
|
+
metavar='PATH',
|
|
1125
|
+
type=argparse.FileType('r'),
|
|
1126
|
+
default=sys.stdin,
|
|
1127
|
+
help='JSON file, default: stdin',
|
|
1128
|
+
)
|
|
1129
|
+
arguments.add_argument(
|
|
1130
|
+
'-o', '--output',
|
|
1131
|
+
metavar='PATH',
|
|
1132
|
+
type=argparse.FileType('w'),
|
|
1133
|
+
default=sys.stdout,
|
|
1134
|
+
help='output file, default: stdout',
|
|
1135
|
+
)
|
|
1136
|
+
args = arguments.parse_args(argv)
|
|
1137
|
+
try:
|
|
1138
|
+
args.output.write(render(args.template.read(), json.load(args.json)))
|
|
1139
|
+
finally:
|
|
1140
|
+
args.template.close()
|
|
1141
|
+
for fd, std in ((args.json, sys.stdin), (args.output, sys.stdout)):
|
|
1142
|
+
if fd is not std:
|
|
1143
|
+
fd.close()
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
if __name__ == '__main__':
|
|
1147
|
+
cli()
|