python-liquid 2.3.5__py3-none-any.whl → 3.0.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.
- liquid/__about__.py +1 -0
- liquid/__init__.py +161 -187
- liquid/_base_tag.py +66 -0
- liquid/{utils/chain_map.py → _chain_map.py} +18 -19
- liquid/_context.py +465 -0
- liquid/_drop.py +108 -0
- liquid/_filter.py +174 -0
- liquid/_io.py +18 -0
- liquid/_lax_parser_mixin.py +100 -0
- liquid/_lexer.py +93 -0
- liquid/_lexer_extended.py +623 -0
- liquid/_lexer_legacy.py +536 -0
- liquid/_loader.py +84 -0
- liquid/{utils/lru_cache.py → _lru_cache.py} +28 -36
- liquid/_markup.py +253 -0
- liquid/_messages.py +292 -0
- liquid/_missing.py +3 -0
- liquid/_nothing.py +28 -0
- liquid/_parser.py +460 -0
- liquid/_parser_extended.py +293 -0
- liquid/_parser_legacy.py +819 -0
- liquid/_resource_limits.py +50 -0
- liquid/_slice.py +41 -0
- liquid/_static_analysis.py +503 -0
- liquid/_strftime.py +186 -0
- liquid/_template.py +536 -0
- liquid/drops/__init__.py +20 -0
- liquid/drops/forloop.py +78 -0
- liquid/drops/iterable.py +25 -0
- liquid/drops/range.py +76 -0
- liquid/drops/self_.py +45 -0
- liquid/drops/tablerowloop.py +142 -0
- liquid/drops/undefined.py +228 -0
- liquid/environment.py +717 -551
- liquid/exceptions.py +67 -214
- liquid/expressions.py +1184 -0
- liquid/filters/__init__.py +134 -0
- liquid/filters/abs.py +7 -0
- liquid/filters/append.py +5 -0
- liquid/filters/at_least.py +9 -0
- liquid/filters/at_most.py +9 -0
- liquid/filters/babel.py +560 -0
- liquid/filters/base64.py +26 -0
- liquid/filters/capitalize.py +5 -0
- liquid/filters/ceil.py +8 -0
- liquid/filters/compact.py +25 -0
- liquid/filters/concat.py +11 -0
- liquid/filters/date.py +27 -0
- liquid/filters/default.py +25 -0
- liquid/filters/divided_by.py +21 -0
- liquid/filters/downcase.py +5 -0
- liquid/filters/escape.py +56 -0
- liquid/filters/find.py +56 -0
- liquid/filters/first.py +19 -0
- liquid/filters/floor.py +8 -0
- liquid/filters/html.py +23 -0
- liquid/filters/index.py +12 -0
- liquid/filters/join.py +13 -0
- liquid/filters/json_.py +30 -0
- liquid/filters/last.py +17 -0
- liquid/filters/map.py +5 -0
- liquid/filters/minus.py +13 -0
- liquid/filters/modulo.py +18 -0
- liquid/filters/newline_to_br.py +16 -0
- liquid/filters/plus.py +13 -0
- liquid/filters/prepend.py +5 -0
- liquid/filters/reject.py +19 -0
- liquid/filters/remove.py +36 -0
- liquid/filters/replace.py +32 -0
- liquid/filters/reverse.py +5 -0
- liquid/filters/round.py +35 -0
- liquid/filters/size.py +25 -0
- liquid/filters/slice.py +39 -0
- liquid/filters/sort.py +148 -0
- liquid/filters/split.py +34 -0
- liquid/filters/squish.py +9 -0
- liquid/filters/strip.py +13 -0
- liquid/filters/strip_html.py +19 -0
- liquid/filters/strip_newlines.py +17 -0
- liquid/filters/sum.py +21 -0
- liquid/filters/times.py +13 -0
- liquid/filters/translate.py +380 -0
- liquid/filters/truncate.py +39 -0
- liquid/filters/uniq.py +11 -0
- liquid/filters/upcase.py +5 -0
- liquid/filters/url.py +18 -0
- liquid/filters/where.py +18 -0
- liquid/loaders/__init__.py +17 -0
- liquid/loaders/caching_file_system_loader.py +64 -0
- liquid/loaders/choice_loader.py +93 -0
- liquid/loaders/dict_loader.py +56 -0
- liquid/loaders/file_system_loader.py +143 -0
- liquid/{builtin/loaders → loaders}/mixins.py +40 -52
- liquid/loaders/package_loader.py +110 -0
- liquid/tags/__init__.py +61 -0
- liquid/tags/assign.py +68 -0
- liquid/tags/capture.py +83 -0
- liquid/tags/case.py +199 -0
- liquid/tags/comment.py +56 -0
- liquid/tags/cycle.py +131 -0
- liquid/tags/decrement.py +55 -0
- liquid/tags/doc.py +56 -0
- liquid/tags/echo.py +65 -0
- liquid/tags/else_.py +53 -0
- liquid/tags/error.py +51 -0
- liquid/tags/extends.py +448 -0
- liquid/tags/for_.py +421 -0
- liquid/tags/if_.py +156 -0
- liquid/tags/ifchanged.py +69 -0
- liquid/tags/include.py +223 -0
- liquid/tags/increment.py +55 -0
- liquid/tags/inline_comment.py +70 -0
- liquid/tags/liquid.py +56 -0
- liquid/tags/macro.py +281 -0
- liquid/tags/output.py +58 -0
- liquid/tags/raw.py +60 -0
- liquid/tags/render.py +245 -0
- liquid/tags/tablerow.py +254 -0
- liquid/tags/template_comment.py +56 -0
- liquid/tags/translate.py +316 -0
- liquid/tags/unless.py +121 -0
- liquid/tags/with_.py +83 -0
- liquid/tokens.py +370 -0
- {python_liquid-2.3.5.dist-info → python_liquid-3.0.0.dist-info}/METADATA +32 -39
- python_liquid-3.0.0.dist-info/RECORD +128 -0
- {python_liquid-2.3.5.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
- {python_liquid-2.3.5.dist-info → python_liquid-3.0.0.dist-info}/licenses/LICENSE +1 -1
- liquid/analyze_tags.py +0 -214
- liquid/ast.py +0 -252
- liquid/builtin/__init__.py +0 -203
- liquid/builtin/content.py +0 -41
- liquid/builtin/drops/__init__.py +0 -3
- liquid/builtin/drops/drops.py +0 -13
- liquid/builtin/expressions/__init__.py +0 -47
- liquid/builtin/expressions/_tokenize.py +0 -186
- liquid/builtin/expressions/arguments.py +0 -207
- liquid/builtin/expressions/filtered.py +0 -387
- liquid/builtin/expressions/logical.py +0 -672
- liquid/builtin/expressions/loop.py +0 -275
- liquid/builtin/expressions/path.py +0 -163
- liquid/builtin/expressions/primitive.py +0 -417
- liquid/builtin/filters/__init__.py +0 -0
- liquid/builtin/filters/array.py +0 -329
- liquid/builtin/filters/extra.py +0 -83
- liquid/builtin/filters/math.py +0 -129
- liquid/builtin/filters/misc.py +0 -137
- liquid/builtin/filters/string.py +0 -416
- liquid/builtin/illegal.py +0 -28
- liquid/builtin/loaders/__init__.py +0 -0
- liquid/builtin/loaders/caching_file_system_loader.py +0 -61
- liquid/builtin/loaders/choice_loader.py +0 -94
- liquid/builtin/loaders/dict_loader.py +0 -64
- liquid/builtin/loaders/file_system_loader.py +0 -146
- liquid/builtin/loaders/package_loader.py +0 -132
- liquid/builtin/output.py +0 -74
- liquid/builtin/tags/__init__.py +0 -0
- liquid/builtin/tags/assign_tag.py +0 -75
- liquid/builtin/tags/capture_tag.py +0 -98
- liquid/builtin/tags/case_tag.py +0 -287
- liquid/builtin/tags/comment_tag.py +0 -77
- liquid/builtin/tags/cycle_tag.py +0 -132
- liquid/builtin/tags/decrement_tag.py +0 -59
- liquid/builtin/tags/doc_tag.py +0 -68
- liquid/builtin/tags/echo_tag.py +0 -45
- liquid/builtin/tags/for_tag.py +0 -337
- liquid/builtin/tags/if_tag.py +0 -209
- liquid/builtin/tags/ifchanged_tag.py +0 -94
- liquid/builtin/tags/include_tag.py +0 -251
- liquid/builtin/tags/increment_tag.py +0 -60
- liquid/builtin/tags/inline_comment_tag.py +0 -47
- liquid/builtin/tags/liquid_tag.py +0 -183
- liquid/builtin/tags/render_tag.py +0 -371
- liquid/builtin/tags/tablerow_tag.py +0 -320
- liquid/builtin/tags/unless_tag.py +0 -218
- liquid/context.py +0 -615
- liquid/expression.py +0 -41
- liquid/extra/__init__.py +0 -98
- liquid/extra/filters/__init__.py +0 -33
- liquid/extra/filters/_json.py +0 -33
- liquid/extra/filters/array.py +0 -60
- liquid/extra/filters/babel.py +0 -552
- liquid/extra/filters/html.py +0 -32
- liquid/extra/filters/translate.py +0 -448
- liquid/extra/tags/__init__.py +0 -17
- liquid/extra/tags/_with.py +0 -80
- liquid/extra/tags/extends_tag.py +0 -576
- liquid/extra/tags/macro_tag.py +0 -270
- liquid/extra/tags/snippet_tag.py +0 -107
- liquid/extra/tags/translate_tag.py +0 -388
- liquid/filter.py +0 -230
- liquid/future/__init__.py +0 -3
- liquid/future/environment.py +0 -30
- liquid/future/filters/__init__.py +0 -1
- liquid/future/tags/__init__.py +0 -1
- liquid/lex.py +0 -260
- liquid/limits.py +0 -66
- liquid/loader.py +0 -141
- liquid/messages.py +0 -357
- liquid/mode.py +0 -12
- liquid/output.py +0 -35
- liquid/parser.py +0 -119
- liquid/span.py +0 -35
- liquid/static_analysis.py +0 -443
- liquid/stream.py +0 -148
- liquid/stringify.py +0 -31
- liquid/tag.py +0 -47
- liquid/template.py +0 -658
- liquid/token.py +0 -145
- liquid/undefined.py +0 -218
- liquid/utils/__init__.py +0 -15
- liquid/utils/html.py +0 -57
- liquid/utils/text.py +0 -28
- python_liquid-2.3.5.dist-info/RECORD +0 -96
liquid/_context.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# pyright: reportUnknownArgumentType=false
|
|
2
|
+
# pyright: reportUnknownVariableType=false
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import math
|
|
7
|
+
from collections.abc import Callable, Generator, Mapping, Sequence
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import TYPE_CHECKING, TypeGuard, cast
|
|
10
|
+
|
|
11
|
+
from ._chain_map import ReadOnlyChainMap
|
|
12
|
+
from ._drop import (
|
|
13
|
+
liquid_async_object,
|
|
14
|
+
liquid_object,
|
|
15
|
+
liquid_primitive,
|
|
16
|
+
)
|
|
17
|
+
from ._nothing import NOTHING
|
|
18
|
+
from .drops import Self
|
|
19
|
+
from .exceptions import DetailedLiquidError, ResourceLimitError
|
|
20
|
+
from .expressions import IntegerLiteral, Name
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ._template import Template
|
|
24
|
+
from .drops import ForLoop, TableRowLoop
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RenderContext:
|
|
28
|
+
__slots__ = (
|
|
29
|
+
"assign_score",
|
|
30
|
+
"assign_score_cumulative",
|
|
31
|
+
"capture_errors",
|
|
32
|
+
"context_depth",
|
|
33
|
+
"counters",
|
|
34
|
+
"disabled_tags",
|
|
35
|
+
"env",
|
|
36
|
+
"errors",
|
|
37
|
+
"globals",
|
|
38
|
+
"interrupts",
|
|
39
|
+
"limits",
|
|
40
|
+
"locals",
|
|
41
|
+
"loop_iteration_carry",
|
|
42
|
+
"loops",
|
|
43
|
+
"registers",
|
|
44
|
+
"render_score",
|
|
45
|
+
"render_score_cumulative",
|
|
46
|
+
"scope",
|
|
47
|
+
"template",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
template: Template,
|
|
53
|
+
*,
|
|
54
|
+
globals: Mapping[str, object] | None = None,
|
|
55
|
+
capture_errors: bool = False,
|
|
56
|
+
disabled_tags: Sequence[str] | None = None,
|
|
57
|
+
context_depth: int = 1,
|
|
58
|
+
assign_score_carry: int = 0,
|
|
59
|
+
render_score_carry: int = 0,
|
|
60
|
+
loop_iteration_carry: int = 1,
|
|
61
|
+
) -> None:
|
|
62
|
+
self.template = template
|
|
63
|
+
self.env = template.env
|
|
64
|
+
self.limits = self.env.resource_limits
|
|
65
|
+
|
|
66
|
+
self.globals = globals or {}
|
|
67
|
+
self.locals: dict[str, object] = {}
|
|
68
|
+
self.counters: dict[str, int] = {}
|
|
69
|
+
self.scope = ReadOnlyChainMap(
|
|
70
|
+
self.locals, self.counters, self.globals, {"self": Self(self)}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
self.registers: dict[str, object] = {}
|
|
74
|
+
|
|
75
|
+
self.loops: list[ForLoop | TableRowLoop] = []
|
|
76
|
+
self.interrupts: list[str] = []
|
|
77
|
+
self.disabled_tags = disabled_tags
|
|
78
|
+
|
|
79
|
+
self.context_depth = context_depth
|
|
80
|
+
self.assign_score = 0
|
|
81
|
+
self.render_score = 0
|
|
82
|
+
self.assign_score_cumulative = assign_score_carry
|
|
83
|
+
self.render_score_cumulative = render_score_carry
|
|
84
|
+
self.loop_iteration_carry = loop_iteration_carry
|
|
85
|
+
|
|
86
|
+
self.capture_errors = capture_errors
|
|
87
|
+
self.errors: list[DetailedLiquidError] = []
|
|
88
|
+
|
|
89
|
+
def assign(self, name: str, value: object) -> None:
|
|
90
|
+
if self.limits.max_assign_score or self.limits.max_assign_score_cumulative:
|
|
91
|
+
score = assign_score_of(value)
|
|
92
|
+
self.assign_score += score
|
|
93
|
+
self.assign_score_cumulative += score
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
self.limits.max_assign_score
|
|
97
|
+
and self.assign_score > self.limits.max_assign_score
|
|
98
|
+
) or (
|
|
99
|
+
self.limits.max_assign_score_cumulative
|
|
100
|
+
and self.assign_score_cumulative
|
|
101
|
+
> self.limits.max_assign_score_cumulative
|
|
102
|
+
):
|
|
103
|
+
raise ResourceLimitError("memory limit exceeded")
|
|
104
|
+
|
|
105
|
+
self.locals[name] = value
|
|
106
|
+
|
|
107
|
+
def resolve(self, name: object) -> object:
|
|
108
|
+
"""Lookup `name` in the current scope.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
The special value `NOTHING` if `name` can not be resolved.
|
|
113
|
+
"""
|
|
114
|
+
if not isinstance(name, str):
|
|
115
|
+
return NOTHING
|
|
116
|
+
|
|
117
|
+
return self.scope[name]
|
|
118
|
+
|
|
119
|
+
def resolve_path(self, obj: object, selectors: list[object]) -> tuple[object, int]:
|
|
120
|
+
index: int = -1
|
|
121
|
+
dotted: bool
|
|
122
|
+
|
|
123
|
+
for selector in selectors:
|
|
124
|
+
index += 1
|
|
125
|
+
|
|
126
|
+
if isinstance(selector, (Name, IntegerLiteral)):
|
|
127
|
+
selector = selector.value
|
|
128
|
+
dotted = True
|
|
129
|
+
else:
|
|
130
|
+
dotted = False
|
|
131
|
+
|
|
132
|
+
if liquid_object(obj):
|
|
133
|
+
if liquid_primitive(selector):
|
|
134
|
+
selector = selector.__liquid__("property", self)
|
|
135
|
+
|
|
136
|
+
if isinstance(selector, str) and obj.__liquid_hasattr__(selector, self):
|
|
137
|
+
obj = obj.__liquid_getattr__(selector, self)
|
|
138
|
+
else:
|
|
139
|
+
obj = NOTHING
|
|
140
|
+
|
|
141
|
+
elif isinstance(obj, str):
|
|
142
|
+
if self.env.iterable_strings:
|
|
143
|
+
obj = resolve_sequence_selector(obj, selector, dotted)
|
|
144
|
+
else:
|
|
145
|
+
obj = resolve_string_selector(obj, selector)
|
|
146
|
+
|
|
147
|
+
elif isinstance(obj, Sequence):
|
|
148
|
+
if liquid_primitive(selector):
|
|
149
|
+
selector = selector.__liquid__("index", self)
|
|
150
|
+
|
|
151
|
+
obj = resolve_sequence_selector(obj, selector, dotted)
|
|
152
|
+
|
|
153
|
+
elif isinstance(obj, Mapping):
|
|
154
|
+
if liquid_primitive(selector):
|
|
155
|
+
selector = selector.__liquid__("property", self)
|
|
156
|
+
obj = resolve_mapping_selector(obj, selector, dotted)
|
|
157
|
+
|
|
158
|
+
else:
|
|
159
|
+
obj = resolve_unknown_selector(obj, selector)
|
|
160
|
+
|
|
161
|
+
if obj is NOTHING:
|
|
162
|
+
return (NOTHING, index)
|
|
163
|
+
|
|
164
|
+
return (obj, index)
|
|
165
|
+
|
|
166
|
+
async def resolve_path_async(
|
|
167
|
+
self, obj: object, selectors: list[object]
|
|
168
|
+
) -> tuple[object, int]:
|
|
169
|
+
index: int = -1
|
|
170
|
+
dotted: bool
|
|
171
|
+
|
|
172
|
+
for selector in selectors:
|
|
173
|
+
index += 1
|
|
174
|
+
|
|
175
|
+
if isinstance(selector, (Name, IntegerLiteral)):
|
|
176
|
+
selector = selector.value
|
|
177
|
+
dotted = True
|
|
178
|
+
else:
|
|
179
|
+
dotted = False
|
|
180
|
+
|
|
181
|
+
if liquid_async_object(obj):
|
|
182
|
+
if liquid_primitive(selector):
|
|
183
|
+
selector = selector.__liquid__("property", self)
|
|
184
|
+
|
|
185
|
+
if isinstance(selector, str) and await obj.__liquid_hasattr_async__(
|
|
186
|
+
selector, self
|
|
187
|
+
):
|
|
188
|
+
obj = await obj.__liquid_getattr_async__(selector, self)
|
|
189
|
+
else:
|
|
190
|
+
obj = NOTHING
|
|
191
|
+
|
|
192
|
+
elif liquid_object(obj):
|
|
193
|
+
if liquid_primitive(selector):
|
|
194
|
+
selector = selector.__liquid__("property", self)
|
|
195
|
+
|
|
196
|
+
if isinstance(selector, str) and obj.__liquid_hasattr__(selector, self):
|
|
197
|
+
obj = obj.__liquid_getattr__(selector, self)
|
|
198
|
+
else:
|
|
199
|
+
obj = NOTHING
|
|
200
|
+
|
|
201
|
+
elif isinstance(obj, str):
|
|
202
|
+
obj = resolve_string_selector(obj, selector)
|
|
203
|
+
|
|
204
|
+
elif isinstance(obj, Sequence):
|
|
205
|
+
if liquid_primitive(selector):
|
|
206
|
+
selector = selector.__liquid__("index", self)
|
|
207
|
+
obj = resolve_sequence_selector(obj, selector, dotted)
|
|
208
|
+
|
|
209
|
+
elif isinstance(obj, Mapping):
|
|
210
|
+
if liquid_primitive(selector):
|
|
211
|
+
selector = selector.__liquid__("property", self)
|
|
212
|
+
obj = resolve_mapping_selector(obj, selector, dotted)
|
|
213
|
+
|
|
214
|
+
else:
|
|
215
|
+
obj = resolve_unknown_selector(obj, selector)
|
|
216
|
+
|
|
217
|
+
if obj is NOTHING:
|
|
218
|
+
return (NOTHING, index)
|
|
219
|
+
|
|
220
|
+
return (obj, index)
|
|
221
|
+
|
|
222
|
+
def get_register[T](self, key: str, default: Callable[[], T]) -> T:
|
|
223
|
+
if key not in self.registers:
|
|
224
|
+
self.registers[key] = default()
|
|
225
|
+
|
|
226
|
+
return cast("T", self.registers[key])
|
|
227
|
+
|
|
228
|
+
def decrement(self, name: str) -> int:
|
|
229
|
+
i = self.counters.get(name, 0)
|
|
230
|
+
i -= 1
|
|
231
|
+
self.counters[name] = i
|
|
232
|
+
return i
|
|
233
|
+
|
|
234
|
+
def increment(self, name: str) -> int:
|
|
235
|
+
i = self.counters.get(name, 0)
|
|
236
|
+
self.counters[name] = i + 1
|
|
237
|
+
return i
|
|
238
|
+
|
|
239
|
+
def copy(
|
|
240
|
+
self,
|
|
241
|
+
namespace: Mapping[str, object],
|
|
242
|
+
*,
|
|
243
|
+
template: Template | None = None,
|
|
244
|
+
disabled_tags: Sequence[str] | None = None,
|
|
245
|
+
carry_loop_iterations: bool = False,
|
|
246
|
+
block_scope: bool = False,
|
|
247
|
+
) -> RenderContext:
|
|
248
|
+
self.raise_for_context_depth()
|
|
249
|
+
|
|
250
|
+
if carry_loop_iterations:
|
|
251
|
+
loop_iteration_carry = (
|
|
252
|
+
math.prod(loop.length for loop in self.loops)
|
|
253
|
+
* self.loop_iteration_carry
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
else:
|
|
257
|
+
loop_iteration_carry = 1
|
|
258
|
+
|
|
259
|
+
globals_ = (
|
|
260
|
+
ReadOnlyChainMap(namespace, self.scope)
|
|
261
|
+
if block_scope
|
|
262
|
+
else ReadOnlyChainMap(namespace, self.globals)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
ctx = RenderContext(
|
|
266
|
+
template or self.template,
|
|
267
|
+
globals=globals_,
|
|
268
|
+
disabled_tags=disabled_tags,
|
|
269
|
+
context_depth=self.context_depth + 1,
|
|
270
|
+
assign_score_carry=self.assign_score_cumulative,
|
|
271
|
+
render_score_carry=self.render_score_cumulative,
|
|
272
|
+
loop_iteration_carry=loop_iteration_carry,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
for register in self.env.persistent_registers:
|
|
276
|
+
if register in self.registers:
|
|
277
|
+
ctx.registers[register] = self.registers[register]
|
|
278
|
+
|
|
279
|
+
return ctx
|
|
280
|
+
|
|
281
|
+
@contextmanager
|
|
282
|
+
def extend(
|
|
283
|
+
self, namespace: Mapping[str, object], template: Template | None = None
|
|
284
|
+
) -> Generator[RenderContext]:
|
|
285
|
+
self.raise_for_context_depth()
|
|
286
|
+
|
|
287
|
+
restore_assign_score = self.assign_score
|
|
288
|
+
restore_render_score = self.render_score
|
|
289
|
+
restore_template = self.template
|
|
290
|
+
|
|
291
|
+
if template:
|
|
292
|
+
self.template = template
|
|
293
|
+
|
|
294
|
+
self.scope.push(namespace)
|
|
295
|
+
self.context_depth += 1
|
|
296
|
+
self.assign_score = 0
|
|
297
|
+
self.render_score = 0
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
yield self
|
|
301
|
+
finally:
|
|
302
|
+
if template:
|
|
303
|
+
self.template = restore_template
|
|
304
|
+
self.scope.pop()
|
|
305
|
+
self.context_depth -= 1
|
|
306
|
+
self.assign_score = restore_assign_score
|
|
307
|
+
self.render_score = restore_render_score
|
|
308
|
+
|
|
309
|
+
@contextmanager
|
|
310
|
+
def loop(
|
|
311
|
+
self, namespace: Mapping[str, object], loop: ForLoop | TableRowLoop
|
|
312
|
+
) -> Generator[RenderContext]:
|
|
313
|
+
self.raise_for_loop_limit(loop.length)
|
|
314
|
+
self.loops.append(loop)
|
|
315
|
+
|
|
316
|
+
with self.extend(namespace) as context:
|
|
317
|
+
try:
|
|
318
|
+
yield context
|
|
319
|
+
finally:
|
|
320
|
+
self.loops.pop()
|
|
321
|
+
|
|
322
|
+
def parent_loop(self) -> ForLoop | None:
|
|
323
|
+
for loop in reversed(self.loops):
|
|
324
|
+
if is_forloop(loop):
|
|
325
|
+
return loop
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
def raise_for_context_depth(self) -> None:
|
|
329
|
+
if self.context_depth + 1 > self.limits.max_context_depth:
|
|
330
|
+
raise ResourceLimitError(
|
|
331
|
+
"maximum context depth reached, possible recursive render"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def raise_for_loop_limit(self, length: int) -> None:
|
|
335
|
+
if (
|
|
336
|
+
self.limits.max_loop_iterations
|
|
337
|
+
and math.prod(loop.length for loop in self.loops)
|
|
338
|
+
* (length * self.loop_iteration_carry)
|
|
339
|
+
> self.limits.max_loop_iterations
|
|
340
|
+
):
|
|
341
|
+
raise ResourceLimitError("loop iteration limit reached")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def is_forloop(loop: ForLoop | TableRowLoop) -> TypeGuard[ForLoop]:
|
|
345
|
+
return hasattr(loop, "parent_loop")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def assign_score_of(obj: object, max_nodes: int = 1_000_000) -> int:
|
|
349
|
+
stack: list[object] = [obj]
|
|
350
|
+
seen: set[int] = set()
|
|
351
|
+
count = 0
|
|
352
|
+
|
|
353
|
+
score = 0
|
|
354
|
+
|
|
355
|
+
while stack:
|
|
356
|
+
if count > max_nodes:
|
|
357
|
+
raise ResourceLimitError("maximum data depth reached")
|
|
358
|
+
|
|
359
|
+
node = stack.pop()
|
|
360
|
+
count += 1
|
|
361
|
+
|
|
362
|
+
if isinstance(node, str):
|
|
363
|
+
score += len(node)
|
|
364
|
+
|
|
365
|
+
elif isinstance(node, Sequence):
|
|
366
|
+
id_ = id(node)
|
|
367
|
+
if id_ in seen:
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
seen.add(id_)
|
|
371
|
+
score += 1
|
|
372
|
+
stack.extend(node)
|
|
373
|
+
|
|
374
|
+
elif isinstance(node, Mapping):
|
|
375
|
+
id_ = id(node)
|
|
376
|
+
if id_ in seen:
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
seen.add(id_)
|
|
380
|
+
score += 1
|
|
381
|
+
stack.extend(node.keys())
|
|
382
|
+
stack.extend(node.values())
|
|
383
|
+
|
|
384
|
+
else:
|
|
385
|
+
score += 1
|
|
386
|
+
|
|
387
|
+
return score
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def resolve_string_selector(obj: str, selector: object) -> object:
|
|
391
|
+
if selector == "size":
|
|
392
|
+
return len(obj)
|
|
393
|
+
|
|
394
|
+
if not obj:
|
|
395
|
+
return NOTHING
|
|
396
|
+
|
|
397
|
+
if selector == "first":
|
|
398
|
+
return obj[0]
|
|
399
|
+
|
|
400
|
+
if selector == "last":
|
|
401
|
+
return obj[-1]
|
|
402
|
+
|
|
403
|
+
return NOTHING
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def resolve_sequence_selector(
|
|
407
|
+
obj: Sequence[object], selector: object, dotted: bool
|
|
408
|
+
) -> object:
|
|
409
|
+
if not obj:
|
|
410
|
+
return 0 if selector == "size" else NOTHING
|
|
411
|
+
|
|
412
|
+
if isinstance(selector, int):
|
|
413
|
+
if selector < 0:
|
|
414
|
+
if len(obj) >= abs(selector):
|
|
415
|
+
return obj[selector]
|
|
416
|
+
return NOTHING
|
|
417
|
+
|
|
418
|
+
return obj[selector] if len(obj) > selector else NOTHING
|
|
419
|
+
|
|
420
|
+
# Only dotted path syntax "calls" "commands", like `.first` and `.size`.
|
|
421
|
+
if not dotted:
|
|
422
|
+
return NOTHING
|
|
423
|
+
|
|
424
|
+
if selector == "first":
|
|
425
|
+
return obj[0]
|
|
426
|
+
|
|
427
|
+
if selector == "last":
|
|
428
|
+
return obj[-1]
|
|
429
|
+
|
|
430
|
+
if selector == "size":
|
|
431
|
+
return len(obj)
|
|
432
|
+
|
|
433
|
+
return NOTHING
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def resolve_mapping_selector(
|
|
437
|
+
obj: Mapping[object, object], selector: object, dotted: bool
|
|
438
|
+
) -> object:
|
|
439
|
+
try:
|
|
440
|
+
if selector in obj:
|
|
441
|
+
return obj[selector]
|
|
442
|
+
except TypeError:
|
|
443
|
+
return NOTHING
|
|
444
|
+
|
|
445
|
+
# Only dotted path syntax "calls" "commands", like `.first` and `.size`.
|
|
446
|
+
if not dotted:
|
|
447
|
+
return NOTHING
|
|
448
|
+
|
|
449
|
+
if selector == "first":
|
|
450
|
+
return next(iter(obj.items()), NOTHING)
|
|
451
|
+
|
|
452
|
+
if selector == "size":
|
|
453
|
+
return len(obj)
|
|
454
|
+
|
|
455
|
+
return NOTHING
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def resolve_unknown_selector(obj: object, selector: object) -> object:
|
|
459
|
+
if selector == "size":
|
|
460
|
+
if isinstance(obj, bool):
|
|
461
|
+
return 0
|
|
462
|
+
|
|
463
|
+
# Close enough, most of the time.
|
|
464
|
+
return 8 if isinstance(obj, int) else NOTHING
|
|
465
|
+
return NOTHING
|
liquid/_drop.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Literal, Protocol, TypeGuard
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ._context import RenderContext
|
|
7
|
+
from .drops import AsyncIterableDrop, IterableDrop
|
|
8
|
+
|
|
9
|
+
type ContextHint = Literal[
|
|
10
|
+
"array", # to_array
|
|
11
|
+
"boolean", # to_bool
|
|
12
|
+
"index", # Sequence index during path resolution
|
|
13
|
+
"numeric", # to_numeric and right hand side of ordering
|
|
14
|
+
"object", # Right hand side of equality
|
|
15
|
+
"output", # serialize for output
|
|
16
|
+
"property", # Mapping key or object property during path resolution
|
|
17
|
+
"string", # to_str
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LiquidObject(Protocol):
|
|
22
|
+
def __liquid_hasattr__(self, name: str, context: RenderContext) -> bool:
|
|
23
|
+
"""Return whether a Liquid property with the given name exists.
|
|
24
|
+
|
|
25
|
+
The context is available for property-existence checks that depend on
|
|
26
|
+
the current render context. Implementations should distinguish between
|
|
27
|
+
a missing property and an existing property whose value is None.
|
|
28
|
+
"""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def __liquid_getattr__(self, name: str, context: RenderContext) -> object:
|
|
32
|
+
"""Return the value of a Liquid property.
|
|
33
|
+
|
|
34
|
+
The context is available for resolving values that depend on the
|
|
35
|
+
current render context. Implementations should raise an AttributeError
|
|
36
|
+
exception when a property cannot be resolved.
|
|
37
|
+
|
|
38
|
+
This synchronous method is used during synchronous rendering.
|
|
39
|
+
"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def liquid_object(obj: object) -> TypeGuard[LiquidObject]:
|
|
44
|
+
return hasattr(obj, "__liquid_hasattr__") and hasattr(obj, "__liquid_getattr__")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LiquidAsyncObject(Protocol):
|
|
48
|
+
async def __liquid_hasattr_async__(self, name: str, context: RenderContext) -> bool:
|
|
49
|
+
"""Asynchronously determine whether a Liquid property exists.
|
|
50
|
+
|
|
51
|
+
This method is used during asynchronous rendering. Implementations
|
|
52
|
+
may override it when checking property existence requires asynchronous
|
|
53
|
+
work.
|
|
54
|
+
"""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
async def __liquid_getattr_async__(
|
|
58
|
+
self, name: str, context: RenderContext
|
|
59
|
+
) -> object:
|
|
60
|
+
"""Asynchronously resolve a Liquid property.
|
|
61
|
+
|
|
62
|
+
This method is used during asynchronous rendering. Implementations may
|
|
63
|
+
override it when resolving a property requires asynchronous work.
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def liquid_async_object(obj: object) -> TypeGuard[LiquidAsyncObject]:
|
|
69
|
+
return hasattr(obj, "__liquid_hasattr_async__") and hasattr(
|
|
70
|
+
obj, "__liquid_getattr_async__"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class LiquidIterable(Protocol):
|
|
75
|
+
def __liquid_iter__(
|
|
76
|
+
self, offset: int, limit: int | None, reversed_: bool
|
|
77
|
+
) -> IterableDrop[object]:
|
|
78
|
+
"""Return a Liquid iterable."""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def liquid_iterable(obj: object) -> TypeGuard[LiquidIterable]:
|
|
83
|
+
return hasattr(obj, "__liquid_iter__")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class LiquidAsyncIterable(Protocol):
|
|
87
|
+
async def __liquid_iter_async__(
|
|
88
|
+
self, offset: int, limit: int | None, reversed_: bool
|
|
89
|
+
) -> IterableDrop[object] | AsyncIterableDrop[object]:
|
|
90
|
+
"""Return a asynchronous Liquid iterable."""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def liquid_async_iterable(obj: object) -> TypeGuard[LiquidAsyncIterable]:
|
|
95
|
+
return hasattr(obj, "__liquid_iter_async__")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class LiquidPrimitive(Protocol):
|
|
99
|
+
def __liquid__(self, hint: ContextHint, context: RenderContext) -> object:
|
|
100
|
+
"""Convert this Drop to a Liquid primitive type.
|
|
101
|
+
|
|
102
|
+
The hint identifies the requested representation:
|
|
103
|
+
"""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def liquid_primitive(obj: object) -> TypeGuard[LiquidPrimitive]:
|
|
108
|
+
return hasattr(obj, "__liquid__")
|