python-liquid 2.3.6__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/{builtin/loaders → loaders}/file_system_loader.py +51 -55
- liquid/{builtin/loaders → loaders}/mixins.py +39 -51
- 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.6.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.6.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
- {python_liquid-2.3.6.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/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.6.dist-info/RECORD +0 -96
liquid/context.py
DELETED
|
@@ -1,615 +0,0 @@
|
|
|
1
|
-
"""Liquid template render context."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import datetime
|
|
6
|
-
import itertools
|
|
7
|
-
import re
|
|
8
|
-
import sys
|
|
9
|
-
import warnings
|
|
10
|
-
from collections import defaultdict
|
|
11
|
-
from contextlib import contextmanager
|
|
12
|
-
from functools import partial
|
|
13
|
-
from functools import reduce
|
|
14
|
-
from io import StringIO
|
|
15
|
-
from operator import mul
|
|
16
|
-
from typing import TYPE_CHECKING
|
|
17
|
-
from typing import Any
|
|
18
|
-
from typing import Callable
|
|
19
|
-
from typing import Iterator
|
|
20
|
-
from typing import Mapping
|
|
21
|
-
from typing import MutableMapping
|
|
22
|
-
from typing import Optional
|
|
23
|
-
from typing import Sequence
|
|
24
|
-
from typing import Sized
|
|
25
|
-
from typing import TextIO
|
|
26
|
-
from typing import Union
|
|
27
|
-
|
|
28
|
-
from .exceptions import ContextDepthError
|
|
29
|
-
from .exceptions import LiquidError
|
|
30
|
-
from .exceptions import LocalNamespaceLimitError
|
|
31
|
-
from .exceptions import LoopIterationLimitError
|
|
32
|
-
from .exceptions import UnknownFilterError
|
|
33
|
-
from .exceptions import lookup_warning
|
|
34
|
-
from .mode import Mode
|
|
35
|
-
from .output import LimitedStringIO
|
|
36
|
-
from .undefined import UNDEFINED
|
|
37
|
-
from .undefined import Undefined
|
|
38
|
-
from .utils import ReadOnlyChainMap
|
|
39
|
-
|
|
40
|
-
if TYPE_CHECKING:
|
|
41
|
-
from .builtin.tags.for_tag import ForLoop
|
|
42
|
-
from .builtin.tags.tablerow_tag import TableRow
|
|
43
|
-
from .environment import Environment
|
|
44
|
-
from .template import BoundTemplate
|
|
45
|
-
from .token import Token
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class RenderContext:
|
|
49
|
-
"""A template render context.
|
|
50
|
-
|
|
51
|
-
A new render context is created automatically each time `BoundTemplate.render`
|
|
52
|
-
is called, which includes `globals` set on the bound `liquid.Environment` and
|
|
53
|
-
`liquid.template.BoundTemplate`.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
__slots__ = (
|
|
57
|
-
"_copy_depth",
|
|
58
|
-
"autoescape",
|
|
59
|
-
"counters",
|
|
60
|
-
"disabled_tags",
|
|
61
|
-
"env",
|
|
62
|
-
"globals",
|
|
63
|
-
"local_namespace_size_carry",
|
|
64
|
-
"locals",
|
|
65
|
-
"loop_iteration_carry",
|
|
66
|
-
"loops",
|
|
67
|
-
"parent_context",
|
|
68
|
-
"scope",
|
|
69
|
-
"tag_namespace",
|
|
70
|
-
"template",
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
def __init__(
|
|
74
|
-
self,
|
|
75
|
-
template: BoundTemplate,
|
|
76
|
-
*,
|
|
77
|
-
globals: Optional[Mapping[str, object]] = None, # noqa: A002
|
|
78
|
-
disabled_tags: Optional[list[str]] = None,
|
|
79
|
-
copy_depth: int = 0,
|
|
80
|
-
parent_context: Optional[RenderContext] = None,
|
|
81
|
-
loop_iteration_carry: int = 1,
|
|
82
|
-
local_namespace_size_carry: int = 0,
|
|
83
|
-
):
|
|
84
|
-
self.env: Environment = template.env
|
|
85
|
-
self.template = template
|
|
86
|
-
|
|
87
|
-
# A namespace for template local variables. Those that are bound with
|
|
88
|
-
# `assign` or `capture`.
|
|
89
|
-
self.locals: MutableMapping[str, object] = {}
|
|
90
|
-
|
|
91
|
-
# A read-only namespace containing globally available variables. Usually
|
|
92
|
-
# passed down from the environment.
|
|
93
|
-
self.globals: Mapping[str, object] = globals or {}
|
|
94
|
-
|
|
95
|
-
# A namespace for `increment` and `decrement` counters.
|
|
96
|
-
self.counters: dict[str, int] = {}
|
|
97
|
-
|
|
98
|
-
# Namespaces are searched in this order. When a context is extended, the
|
|
99
|
-
# temporary namespace is pushed to the front of this chain.
|
|
100
|
-
self.scope = ReadOnlyChainMap(self.locals, self.globals, builtin, self.counters)
|
|
101
|
-
|
|
102
|
-
# A namespace supporting stateful tags. Such as `cycle`, `increment`,
|
|
103
|
-
# `decrement` and `ifchanged`.
|
|
104
|
-
self.tag_namespace: dict[str, Any] = {
|
|
105
|
-
"cycles": {},
|
|
106
|
-
"ifchanged": "",
|
|
107
|
-
"stopindex": {},
|
|
108
|
-
"extends": defaultdict(list),
|
|
109
|
-
"macros": {},
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
# As stack of loop objects. Used for populating forloop.parentloop and
|
|
113
|
-
# calculating the effective loop count for the loop iteration resource
|
|
114
|
-
# limit.
|
|
115
|
-
self.loops: list[Union[ForLoop, TableRow]] = []
|
|
116
|
-
|
|
117
|
-
# A list of tags names that are disallowed in this context. For example,
|
|
118
|
-
# partial templates rendered using the "render" tag are not allowed to
|
|
119
|
-
# use "include" tags.
|
|
120
|
-
self.disabled_tags: list[str] = disabled_tags or []
|
|
121
|
-
|
|
122
|
-
# Indicates if HTML auto-escaping is enabled.
|
|
123
|
-
self.autoescape = self.env.autoescape
|
|
124
|
-
|
|
125
|
-
# Count of the number of times `copy` was called in creating this context. This
|
|
126
|
-
# helps us guard against recursive use of the "render" tag, or at least fail
|
|
127
|
-
# gracefully.
|
|
128
|
-
self._copy_depth = copy_depth
|
|
129
|
-
|
|
130
|
-
# A reference to a parent render context, if this one has been copied.
|
|
131
|
-
self.parent_context = parent_context
|
|
132
|
-
|
|
133
|
-
# A loop iteration count carried over from a parent context, if this one has
|
|
134
|
-
# been copied. This helps us adhere to loop iteration limits in templates
|
|
135
|
-
# rendered with the `render` tag.
|
|
136
|
-
self.loop_iteration_carry = loop_iteration_carry
|
|
137
|
-
|
|
138
|
-
# The cumulative number of names in parent render context local namespaces. We
|
|
139
|
-
# use this to adhere to local namespace limits when copying a render context.
|
|
140
|
-
self.local_namespace_size_carry = local_namespace_size_carry
|
|
141
|
-
|
|
142
|
-
def assign(self, key: str, val: Any) -> None:
|
|
143
|
-
"""Add or replace the context variable named _key_ with the value _val_."""
|
|
144
|
-
self.locals[key] = val
|
|
145
|
-
if (
|
|
146
|
-
self.env.local_namespace_limit
|
|
147
|
-
and self.get_size_of_locals() > self.env.local_namespace_limit
|
|
148
|
-
):
|
|
149
|
-
raise LocalNamespaceLimitError("local namespace limit reached", token=None)
|
|
150
|
-
|
|
151
|
-
def get_size_of_locals(self) -> int:
|
|
152
|
-
"""Return the "size" or a "score" for the current local namespace.
|
|
153
|
-
|
|
154
|
-
This is used by the optional local namespace resource limit. Override
|
|
155
|
-
`get_size_of_locals` to customize how the limit is calculated. Be sure
|
|
156
|
-
to consider `self.local_namespace_size_carry` when writing a custom
|
|
157
|
-
implementation of `get_size_of_locals`.
|
|
158
|
-
|
|
159
|
-
The default implementation uses `sys.getsizeof()` on each of the local
|
|
160
|
-
namespace's values. It is not a reliable measure of size in bytes.
|
|
161
|
-
"""
|
|
162
|
-
if not self.env.local_namespace_limit:
|
|
163
|
-
return 0
|
|
164
|
-
return (
|
|
165
|
-
sum(sys.getsizeof(obj, default=1) for obj in self.locals.values())
|
|
166
|
-
+ self.local_namespace_size_carry
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
def get( # noqa: PLR0911
|
|
170
|
-
self, path: list[object], *, token: Optional[Token], default: object = UNDEFINED
|
|
171
|
-
) -> object:
|
|
172
|
-
"""Resolve and return _path_ in the current scope.
|
|
173
|
-
|
|
174
|
-
If _token_ is not None, it will be used to give error messages extra contextual
|
|
175
|
-
information.
|
|
176
|
-
|
|
177
|
-
Returns _default_ is the path is not in scope.
|
|
178
|
-
"""
|
|
179
|
-
it = iter(path)
|
|
180
|
-
root = next(it)
|
|
181
|
-
|
|
182
|
-
if not isinstance(root, str):
|
|
183
|
-
if default == UNDEFINED:
|
|
184
|
-
hint = f"{root} is undefined"
|
|
185
|
-
return self.env.undefined(str(root), hint=hint, token=token)
|
|
186
|
-
return default
|
|
187
|
-
|
|
188
|
-
try:
|
|
189
|
-
obj = self.scope[root]
|
|
190
|
-
except (KeyError, TypeError, IndexError):
|
|
191
|
-
if default == UNDEFINED:
|
|
192
|
-
hint = f"{root!r} is undefined"
|
|
193
|
-
return self.env.undefined(root, hint=hint, token=token)
|
|
194
|
-
return default
|
|
195
|
-
|
|
196
|
-
for i, segment in enumerate(it):
|
|
197
|
-
try:
|
|
198
|
-
obj = self.get_item(obj, segment)
|
|
199
|
-
except (KeyError, TypeError):
|
|
200
|
-
if default == UNDEFINED:
|
|
201
|
-
hint = f"{_segments_str(path[: i + 2])} is undefined"
|
|
202
|
-
return self.env.undefined(root, hint=hint, token=token)
|
|
203
|
-
return default
|
|
204
|
-
except IndexError:
|
|
205
|
-
if default == UNDEFINED:
|
|
206
|
-
hint = "index out of range"
|
|
207
|
-
return self.env.undefined(root, hint=hint, token=token)
|
|
208
|
-
return default
|
|
209
|
-
|
|
210
|
-
return obj
|
|
211
|
-
|
|
212
|
-
async def get_async( # noqa: PLR0911
|
|
213
|
-
self,
|
|
214
|
-
path: list[object],
|
|
215
|
-
*,
|
|
216
|
-
token: Optional[Token],
|
|
217
|
-
default: object = UNDEFINED,
|
|
218
|
-
) -> object:
|
|
219
|
-
"""Resolve and return _path_ in the current scope.
|
|
220
|
-
|
|
221
|
-
If _token_ is not None, it will be used to give error messages extra contextual
|
|
222
|
-
information.
|
|
223
|
-
|
|
224
|
-
Returns _default_ is the path is not in scope.
|
|
225
|
-
"""
|
|
226
|
-
it = iter(path)
|
|
227
|
-
root = next(it)
|
|
228
|
-
assert isinstance(root, str)
|
|
229
|
-
|
|
230
|
-
try:
|
|
231
|
-
obj = self.scope[root]
|
|
232
|
-
except (KeyError, TypeError, IndexError):
|
|
233
|
-
if default == UNDEFINED:
|
|
234
|
-
hint = f"{root!r} is undefined"
|
|
235
|
-
return self.env.undefined(root, hint=hint, token=token)
|
|
236
|
-
return default
|
|
237
|
-
|
|
238
|
-
for i, segment in enumerate(it):
|
|
239
|
-
try:
|
|
240
|
-
obj = await self.get_item_async(obj, segment)
|
|
241
|
-
except (KeyError, TypeError):
|
|
242
|
-
if default == UNDEFINED:
|
|
243
|
-
hint = f"{_segments_str(path[: i + 2])} is undefined"
|
|
244
|
-
return self.env.undefined(root, hint=hint, token=token)
|
|
245
|
-
return default
|
|
246
|
-
except IndexError:
|
|
247
|
-
if default == UNDEFINED:
|
|
248
|
-
hint = "index out of range"
|
|
249
|
-
return self.env.undefined(root, hint=hint, token=token)
|
|
250
|
-
return default
|
|
251
|
-
|
|
252
|
-
return obj
|
|
253
|
-
|
|
254
|
-
def resolve(
|
|
255
|
-
self, name: str, *, token: Optional[Token] = None, default: object = UNDEFINED
|
|
256
|
-
) -> Any:
|
|
257
|
-
"""Return the object/value at `name` in the current scope.
|
|
258
|
-
|
|
259
|
-
This is like `get`, but does a single, top-level lookup rather than a
|
|
260
|
-
chained lookup from a sequence of keys.
|
|
261
|
-
"""
|
|
262
|
-
return self._resolve(name, token=token, default=default)
|
|
263
|
-
|
|
264
|
-
def _resolve(
|
|
265
|
-
self, name: str, *, token: Optional[Token], default: object = UNDEFINED
|
|
266
|
-
) -> Any:
|
|
267
|
-
try:
|
|
268
|
-
return self.scope[name]
|
|
269
|
-
except KeyError:
|
|
270
|
-
if default == UNDEFINED:
|
|
271
|
-
return self.env.undefined(name, token=token)
|
|
272
|
-
return default
|
|
273
|
-
|
|
274
|
-
def filter(self, name: str, token: Optional[Token]) -> Callable[..., object]: # noqa: A003
|
|
275
|
-
"""Return the filter function with given name."""
|
|
276
|
-
try:
|
|
277
|
-
filter_func = self.env.filters[name]
|
|
278
|
-
except KeyError as err:
|
|
279
|
-
if not self.env.strict_filters:
|
|
280
|
-
return _no_op_filter
|
|
281
|
-
raise UnknownFilterError(f"unknown filter '{name}'", token=token) from err
|
|
282
|
-
|
|
283
|
-
kwargs: dict[str, Any] = {}
|
|
284
|
-
|
|
285
|
-
if getattr(filter_func, "with_context", False):
|
|
286
|
-
kwargs["context"] = self
|
|
287
|
-
|
|
288
|
-
if getattr(filter_func, "with_environment", False):
|
|
289
|
-
kwargs["environment"] = self.env
|
|
290
|
-
|
|
291
|
-
if kwargs:
|
|
292
|
-
if hasattr(filter_func, "filter_async"):
|
|
293
|
-
_filter_func = partial(filter_func, **kwargs)
|
|
294
|
-
_filter_func.filter_async = partial( # type: ignore
|
|
295
|
-
filter_func.filter_async,
|
|
296
|
-
**kwargs,
|
|
297
|
-
)
|
|
298
|
-
return _filter_func
|
|
299
|
-
return partial(filter_func, **kwargs)
|
|
300
|
-
|
|
301
|
-
return filter_func
|
|
302
|
-
|
|
303
|
-
def get_template(self, name: str) -> BoundTemplate:
|
|
304
|
-
"""Load a template from the environment."""
|
|
305
|
-
return self.env.get_template(name)
|
|
306
|
-
|
|
307
|
-
async def get_template_async(self, name: str) -> BoundTemplate:
|
|
308
|
-
"""Load a template from the environment asynchronously."""
|
|
309
|
-
return await self.env.get_template_async(name)
|
|
310
|
-
|
|
311
|
-
def increment(self, name: str) -> int:
|
|
312
|
-
"""Increment the named counter and return its value."""
|
|
313
|
-
val: int = self.counters.get(name, 0)
|
|
314
|
-
self.counters[name] = val + 1
|
|
315
|
-
return val
|
|
316
|
-
|
|
317
|
-
def decrement(self, name: str) -> int:
|
|
318
|
-
"""Decrement the named counter and return its value."""
|
|
319
|
-
val: int = self.counters.get(name, 0) - 1
|
|
320
|
-
self.counters[name] = val
|
|
321
|
-
return val
|
|
322
|
-
|
|
323
|
-
def cycle(self, key: object, length: int) -> int:
|
|
324
|
-
"""Return the index of the next item in the cycle."""
|
|
325
|
-
namespace: dict[object, int] = self.tag_namespace["cycles"]
|
|
326
|
-
idx = namespace.setdefault(key, 0)
|
|
327
|
-
namespace[key] = (idx + 1) % (length or 1) # Just in case.
|
|
328
|
-
return idx
|
|
329
|
-
|
|
330
|
-
def ifchanged(self, val: str) -> bool:
|
|
331
|
-
"""Return True if the `ifchanged` value has changed."""
|
|
332
|
-
if val != self.tag_namespace["ifchanged"]:
|
|
333
|
-
self.tag_namespace["ifchanged"] = val
|
|
334
|
-
return True
|
|
335
|
-
|
|
336
|
-
return False
|
|
337
|
-
|
|
338
|
-
def stopindex(self, key: str, index: Optional[int] = None) -> int:
|
|
339
|
-
"""Set or return the stop index of a for loop."""
|
|
340
|
-
if index is not None:
|
|
341
|
-
self.tag_namespace["stopindex"][key] = index
|
|
342
|
-
return index
|
|
343
|
-
|
|
344
|
-
idx: int = self.tag_namespace["stopindex"].get(key, 0)
|
|
345
|
-
return idx
|
|
346
|
-
|
|
347
|
-
@contextmanager
|
|
348
|
-
def extend(
|
|
349
|
-
self, namespace: Mapping[str, object], template: Optional[BoundTemplate] = None
|
|
350
|
-
) -> Iterator[RenderContext]:
|
|
351
|
-
"""Extend this context with the given read-only namespace."""
|
|
352
|
-
if self.scope.size() > self.env.context_depth_limit:
|
|
353
|
-
raise ContextDepthError(
|
|
354
|
-
"maximum context depth reached, possible recursive include", token=None
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
# Remember the current template so we can restore it upon exiting the
|
|
358
|
-
# context manager.
|
|
359
|
-
_template = self.template
|
|
360
|
-
if template:
|
|
361
|
-
self.template = template
|
|
362
|
-
|
|
363
|
-
self.scope.push(namespace)
|
|
364
|
-
|
|
365
|
-
try:
|
|
366
|
-
yield self
|
|
367
|
-
finally:
|
|
368
|
-
if template:
|
|
369
|
-
self.template = _template
|
|
370
|
-
self.scope.pop()
|
|
371
|
-
|
|
372
|
-
@contextmanager
|
|
373
|
-
def loop(
|
|
374
|
-
self, namespace: Mapping[str, object], forloop: ForLoop | TableRow
|
|
375
|
-
) -> Iterator[RenderContext]:
|
|
376
|
-
"""Just like `Context.extend`, but keeps track of ForLoop objects too."""
|
|
377
|
-
self.raise_for_loop_limit(forloop.length)
|
|
378
|
-
self.loops.append(forloop)
|
|
379
|
-
with self.extend(namespace) as context:
|
|
380
|
-
try:
|
|
381
|
-
yield context
|
|
382
|
-
finally:
|
|
383
|
-
self.loops.pop()
|
|
384
|
-
|
|
385
|
-
def parentloop(self) -> Union[Undefined, object]:
|
|
386
|
-
"""Return the last ForLoop object from the loop stack."""
|
|
387
|
-
for loop in reversed(self.loops):
|
|
388
|
-
if hasattr(loop, "parentloop"):
|
|
389
|
-
return loop
|
|
390
|
-
|
|
391
|
-
return self.env.undefined("parentloop", token=None)
|
|
392
|
-
|
|
393
|
-
def raise_for_loop_limit(self, length: int = 1) -> None:
|
|
394
|
-
"""Raise a `LoopIterationLimitError` if loop stack is bigger than the limit."""
|
|
395
|
-
if (
|
|
396
|
-
self.env.loop_iteration_limit
|
|
397
|
-
and reduce(
|
|
398
|
-
mul,
|
|
399
|
-
(loop.length for loop in self.loops),
|
|
400
|
-
length * self.loop_iteration_carry,
|
|
401
|
-
)
|
|
402
|
-
> self.env.loop_iteration_limit
|
|
403
|
-
):
|
|
404
|
-
raise LoopIterationLimitError("loop iteration limit reached", token=None)
|
|
405
|
-
|
|
406
|
-
def copy(
|
|
407
|
-
self,
|
|
408
|
-
namespace: Mapping[str, object],
|
|
409
|
-
disabled_tags: Optional[list[str]] = None,
|
|
410
|
-
carry_loop_iterations: bool = False,
|
|
411
|
-
template: Optional[BoundTemplate] = None,
|
|
412
|
-
block_scope: bool = False,
|
|
413
|
-
) -> RenderContext:
|
|
414
|
-
"""Return a copy of this render context.
|
|
415
|
-
|
|
416
|
-
Local variables and other state for stateful tags are not copied.
|
|
417
|
-
"""
|
|
418
|
-
if self._copy_depth > self.env.context_depth_limit:
|
|
419
|
-
raise ContextDepthError(
|
|
420
|
-
"maximum context depth reached, possible recursive render", token=None
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
if carry_loop_iterations:
|
|
424
|
-
loop_iteration_carry = reduce(
|
|
425
|
-
mul,
|
|
426
|
-
(loop.length for loop in self.loops),
|
|
427
|
-
self.loop_iteration_carry,
|
|
428
|
-
)
|
|
429
|
-
else:
|
|
430
|
-
loop_iteration_carry = 1
|
|
431
|
-
|
|
432
|
-
if block_scope:
|
|
433
|
-
ctx = self.__class__(
|
|
434
|
-
template or self.template,
|
|
435
|
-
globals=ReadOnlyChainMap(namespace, self.scope),
|
|
436
|
-
disabled_tags=disabled_tags,
|
|
437
|
-
copy_depth=self._copy_depth + 1,
|
|
438
|
-
parent_context=self,
|
|
439
|
-
loop_iteration_carry=loop_iteration_carry,
|
|
440
|
-
local_namespace_size_carry=self.get_size_of_locals(),
|
|
441
|
-
)
|
|
442
|
-
# This might need to be generalized so the caller can specify which
|
|
443
|
-
# tag namespaces need to be copied.
|
|
444
|
-
ctx.tag_namespace["extends"] = self.tag_namespace["extends"]
|
|
445
|
-
else:
|
|
446
|
-
ctx = self.__class__(
|
|
447
|
-
template or self.template,
|
|
448
|
-
globals=ReadOnlyChainMap(namespace, self.globals),
|
|
449
|
-
disabled_tags=disabled_tags,
|
|
450
|
-
copy_depth=self._copy_depth + 1,
|
|
451
|
-
parent_context=self,
|
|
452
|
-
loop_iteration_carry=loop_iteration_carry,
|
|
453
|
-
local_namespace_size_carry=self.get_size_of_locals(),
|
|
454
|
-
)
|
|
455
|
-
|
|
456
|
-
return ctx
|
|
457
|
-
|
|
458
|
-
def error(self, exc: LiquidError) -> None:
|
|
459
|
-
"""Ignore, raise or convert the given exception to a warning."""
|
|
460
|
-
if self.env.mode == Mode.STRICT:
|
|
461
|
-
raise exc
|
|
462
|
-
if self.env.mode == Mode.WARN:
|
|
463
|
-
warnings.warn(
|
|
464
|
-
str(exc), category=lookup_warning(exc.__class__), stacklevel=2
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
def get_buffer(self, buf: Optional[TextIO] = None) -> StringIO:
|
|
468
|
-
"""Return a new StringIO object that respects the configured stream limit."""
|
|
469
|
-
if self.env.output_stream_limit is None:
|
|
470
|
-
return StringIO()
|
|
471
|
-
|
|
472
|
-
carry = buf.size if isinstance(buf, LimitedStringIO) else 0
|
|
473
|
-
return LimitedStringIO(limit=self.env.output_stream_limit - carry)
|
|
474
|
-
|
|
475
|
-
def get_item(self, obj: Any, key: Any) -> Any: # noqa: PLR0911, PLR0912
|
|
476
|
-
"""An item getter used when resolving a Liquid path.
|
|
477
|
-
|
|
478
|
-
Override this to change the behavior of `.first`, `.last` and `.size`.
|
|
479
|
-
"""
|
|
480
|
-
if hasattr(key, "__liquid__"):
|
|
481
|
-
key = key.__liquid__()
|
|
482
|
-
|
|
483
|
-
if key == "size":
|
|
484
|
-
try:
|
|
485
|
-
return obj["size"]
|
|
486
|
-
except (KeyError, IndexError, TypeError):
|
|
487
|
-
if isinstance(obj, Sized):
|
|
488
|
-
return len(obj)
|
|
489
|
-
raise
|
|
490
|
-
|
|
491
|
-
if key == "first":
|
|
492
|
-
try:
|
|
493
|
-
return obj["first"]
|
|
494
|
-
except (KeyError, IndexError, TypeError):
|
|
495
|
-
if isinstance(obj, Mapping) and obj:
|
|
496
|
-
return next(itertools.islice(obj.items(), 1))
|
|
497
|
-
if isinstance(obj, str) and not self.env.string_first_and_last:
|
|
498
|
-
raise TypeError("string object has no method 'first'") from None
|
|
499
|
-
if isinstance(obj, Sequence):
|
|
500
|
-
return obj[0]
|
|
501
|
-
raise
|
|
502
|
-
|
|
503
|
-
if key == "last":
|
|
504
|
-
try:
|
|
505
|
-
return obj["last"]
|
|
506
|
-
except (KeyError, IndexError, TypeError):
|
|
507
|
-
if isinstance(obj, str) and not self.env.string_first_and_last:
|
|
508
|
-
raise TypeError("string object has no method 'last'") from None
|
|
509
|
-
if isinstance(obj, Sequence):
|
|
510
|
-
return obj[-1]
|
|
511
|
-
raise
|
|
512
|
-
|
|
513
|
-
if (
|
|
514
|
-
not self.env.string_sequences
|
|
515
|
-
and isinstance(key, int)
|
|
516
|
-
and isinstance(obj, str)
|
|
517
|
-
):
|
|
518
|
-
raise TypeError("string object is not subscriptable")
|
|
519
|
-
|
|
520
|
-
return obj[key]
|
|
521
|
-
|
|
522
|
-
async def get_item_async(self, obj: Any, key: Any) -> Any: # noqa: PLR0911, PLR0912
|
|
523
|
-
"""An async item getter for resolving paths."""
|
|
524
|
-
|
|
525
|
-
async def _get_item(obj: Any, key: Any) -> object:
|
|
526
|
-
if hasattr(obj, "__getitem_async__"):
|
|
527
|
-
return await obj.__getitem_async__(key)
|
|
528
|
-
return obj[key]
|
|
529
|
-
|
|
530
|
-
if hasattr(key, "__liquid__"):
|
|
531
|
-
key = key.__liquid__()
|
|
532
|
-
|
|
533
|
-
if key == "size":
|
|
534
|
-
try:
|
|
535
|
-
return await _get_item(obj, "size")
|
|
536
|
-
except (KeyError, IndexError, TypeError):
|
|
537
|
-
if isinstance(obj, Sized):
|
|
538
|
-
return len(obj)
|
|
539
|
-
raise
|
|
540
|
-
|
|
541
|
-
if key == "first":
|
|
542
|
-
try:
|
|
543
|
-
return await _get_item(obj, "first")
|
|
544
|
-
except (KeyError, IndexError, TypeError):
|
|
545
|
-
if isinstance(obj, Mapping) and obj:
|
|
546
|
-
return next(itertools.islice(obj.items(), 1))
|
|
547
|
-
if isinstance(obj, str) and not self.env.string_first_and_last:
|
|
548
|
-
raise TypeError("string object has no method 'first'") from None
|
|
549
|
-
if isinstance(obj, Sequence):
|
|
550
|
-
return obj[0]
|
|
551
|
-
raise
|
|
552
|
-
|
|
553
|
-
if key == "last":
|
|
554
|
-
try:
|
|
555
|
-
return await _get_item(obj, "last")
|
|
556
|
-
except (KeyError, IndexError, TypeError):
|
|
557
|
-
if isinstance(obj, str) and not self.env.string_first_and_last:
|
|
558
|
-
raise TypeError("string object has no method 'last'") from None
|
|
559
|
-
if isinstance(obj, Sequence):
|
|
560
|
-
return obj[-1]
|
|
561
|
-
raise
|
|
562
|
-
|
|
563
|
-
if (
|
|
564
|
-
not self.env.string_sequences
|
|
565
|
-
and isinstance(key, int)
|
|
566
|
-
and isinstance(obj, str)
|
|
567
|
-
):
|
|
568
|
-
raise TypeError("string object is not subscriptable")
|
|
569
|
-
|
|
570
|
-
return await _get_item(obj, key)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
class FutureContext(RenderContext):
|
|
574
|
-
"""A render context configured for maximum compatibility with the Ruby liquid."""
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
class BuiltIn(Mapping[str, object]):
|
|
578
|
-
"""Mapping-like object for resolving built-in, dynamic objects."""
|
|
579
|
-
|
|
580
|
-
def __contains__(self, item: object) -> bool:
|
|
581
|
-
return item in ("now", "today")
|
|
582
|
-
|
|
583
|
-
def __getitem__(self, key: str) -> object:
|
|
584
|
-
if key == "now":
|
|
585
|
-
return datetime.datetime.now()
|
|
586
|
-
if key == "today":
|
|
587
|
-
return datetime.date.today()
|
|
588
|
-
raise KeyError(str(key))
|
|
589
|
-
|
|
590
|
-
def __len__(self) -> int:
|
|
591
|
-
return 2
|
|
592
|
-
|
|
593
|
-
def __iter__(self) -> Iterator[str]:
|
|
594
|
-
return iter(["now", "today"])
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
builtin = BuiltIn()
|
|
598
|
-
|
|
599
|
-
RE_PROPERTY = re.compile(r"[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*")
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
def _segments_str(segments: list[object]) -> str:
|
|
603
|
-
it = iter(segments)
|
|
604
|
-
buf = [str(next(it))]
|
|
605
|
-
for segment in it:
|
|
606
|
-
if isinstance(segment, str):
|
|
607
|
-
if RE_PROPERTY.fullmatch(segment):
|
|
608
|
-
buf.append(f".{segment}")
|
|
609
|
-
else:
|
|
610
|
-
buf.append(f"[{segment!r}]")
|
|
611
|
-
return "".join(buf)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def _no_op_filter(left: object, *_args: object, **_kwargs: object) -> object:
|
|
615
|
-
return left
|
liquid/expression.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
"""The base class for all built-in expressions."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from abc import ABC
|
|
6
|
-
from abc import abstractmethod
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
8
|
-
from typing import Iterable
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from .builtin.expressions import Identifier
|
|
12
|
-
from .context import RenderContext
|
|
13
|
-
from .token import Token
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Expression(ABC):
|
|
17
|
-
"""The base class for all built-in expressions."""
|
|
18
|
-
|
|
19
|
-
__slots__ = ("token",)
|
|
20
|
-
|
|
21
|
-
def __init__(self, token: Token):
|
|
22
|
-
self.token = token
|
|
23
|
-
|
|
24
|
-
@abstractmethod
|
|
25
|
-
def evaluate(self, context: RenderContext) -> object:
|
|
26
|
-
"""Evaluate this expression an return the result."""
|
|
27
|
-
|
|
28
|
-
async def evaluate_async(self, context: RenderContext) -> object:
|
|
29
|
-
"""Evaluate this expression asynchronously."""
|
|
30
|
-
return self.evaluate(context)
|
|
31
|
-
|
|
32
|
-
@abstractmethod
|
|
33
|
-
def children(self) -> Iterable[Expression]:
|
|
34
|
-
"""Return this expression's child expressions."""
|
|
35
|
-
|
|
36
|
-
def scope(self) -> Iterable[Identifier]:
|
|
37
|
-
"""Return variables this expression adds the scope of any child expressions.
|
|
38
|
-
|
|
39
|
-
Used by lambda expressions only.
|
|
40
|
-
"""
|
|
41
|
-
return []
|