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/static_analysis.py
DELETED
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
"""Analyze variable, tag and filter usage by traversing a template's syntax tree."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
from collections import defaultdict
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from dataclasses import field
|
|
9
|
-
from typing import TYPE_CHECKING
|
|
10
|
-
from typing import Iterable
|
|
11
|
-
from typing import Optional
|
|
12
|
-
from typing import Union
|
|
13
|
-
|
|
14
|
-
from .ast import BlockNode
|
|
15
|
-
from .ast import ConditionalBlockNode
|
|
16
|
-
from .ast import PartialScope
|
|
17
|
-
from .builtin.expressions import FilteredExpression
|
|
18
|
-
from .builtin.expressions import Path
|
|
19
|
-
from .builtin.expressions import TernaryFilteredExpression
|
|
20
|
-
from .builtin.tags.case_tag import MultiExpressionBlockNode
|
|
21
|
-
from .context import RenderContext
|
|
22
|
-
from .span import Span
|
|
23
|
-
from .token import TOKEN_TAG
|
|
24
|
-
|
|
25
|
-
if TYPE_CHECKING:
|
|
26
|
-
from .ast import Node
|
|
27
|
-
from .expression import Expression
|
|
28
|
-
from .template import BoundTemplate
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
RE_PROPERTY = re.compile(r"[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*")
|
|
32
|
-
Segments = list[Union[int, str, "Segments"]]
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@dataclass(frozen=True)
|
|
36
|
-
class Variable:
|
|
37
|
-
"""A variable as sequence of segments that make up its path and its location.
|
|
38
|
-
|
|
39
|
-
Variables with the same segments compare equal, regardless of span.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
segments: Segments
|
|
43
|
-
"""The variable's segments."""
|
|
44
|
-
|
|
45
|
-
span: Span = field(hash=False, compare=False)
|
|
46
|
-
"""The variable's location."""
|
|
47
|
-
|
|
48
|
-
def __str__(self) -> str:
|
|
49
|
-
return self._segments_str(self.segments)
|
|
50
|
-
|
|
51
|
-
def _segments_str(self, segments: Segments) -> str:
|
|
52
|
-
it = iter(segments)
|
|
53
|
-
buf = [str(next(it))]
|
|
54
|
-
|
|
55
|
-
for segment in it:
|
|
56
|
-
if isinstance(segment, list):
|
|
57
|
-
buf.append(f"[{self._segments_str(segment)}]")
|
|
58
|
-
elif isinstance(segment, str):
|
|
59
|
-
if RE_PROPERTY.fullmatch(segment):
|
|
60
|
-
buf.append(f".{segment}")
|
|
61
|
-
else:
|
|
62
|
-
buf.append(f"[{segment!r}]")
|
|
63
|
-
else:
|
|
64
|
-
buf.append(f"[{segment}]")
|
|
65
|
-
return "".join(buf)
|
|
66
|
-
|
|
67
|
-
def __hash__(self) -> int:
|
|
68
|
-
return hash(str(self))
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class _StaticScope:
|
|
72
|
-
def __init__(self, globals: set[str]) -> None: # noqa: A002
|
|
73
|
-
self.stack = [globals]
|
|
74
|
-
|
|
75
|
-
def __contains__(self, key: str) -> bool:
|
|
76
|
-
return any(key in scope for scope in self.stack)
|
|
77
|
-
|
|
78
|
-
def push(self, scope: set[str]) -> _StaticScope:
|
|
79
|
-
self.stack.append(scope)
|
|
80
|
-
return self
|
|
81
|
-
|
|
82
|
-
def pop(self) -> set[str]:
|
|
83
|
-
return self.stack.pop()
|
|
84
|
-
|
|
85
|
-
def add(self, name: str) -> None:
|
|
86
|
-
"""Add a name to the root/template scope."""
|
|
87
|
-
self.stack[0].add(name)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class _VariableMap:
|
|
91
|
-
def __init__(self) -> None:
|
|
92
|
-
self._data: dict[str, list[Variable]] = {}
|
|
93
|
-
|
|
94
|
-
def __getitem__(self, var: Variable) -> list[Variable]:
|
|
95
|
-
k = str(var.segments[0])
|
|
96
|
-
if k not in self._data:
|
|
97
|
-
self._data[k] = []
|
|
98
|
-
return self._data[k]
|
|
99
|
-
|
|
100
|
-
def add(self, var: Variable) -> None:
|
|
101
|
-
self[var].append(var)
|
|
102
|
-
|
|
103
|
-
def as_dict(self) -> dict[str, list[Variable]]:
|
|
104
|
-
return self._data
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@dataclass(frozen=True)
|
|
108
|
-
class TemplateAnalysis:
|
|
109
|
-
"""The result of analyzing a template using `Template.analyze()`.
|
|
110
|
-
|
|
111
|
-
Args:
|
|
112
|
-
variables: All referenced variables, whether they are in scope or not.
|
|
113
|
-
Including references to names such as `forloop` from the `for` tag.
|
|
114
|
-
locals: Template variables that are added to the template local
|
|
115
|
-
scope, whether they are subsequently used or not.
|
|
116
|
-
globals: Template variables that, on the given line number and
|
|
117
|
-
"file", are out of scope or are assumed to be "global". That is, expected to
|
|
118
|
-
be included by the application developer rather than a template author.
|
|
119
|
-
filters: All filters found during static analysis.
|
|
120
|
-
tags: All tags found during static analysis.
|
|
121
|
-
"""
|
|
122
|
-
|
|
123
|
-
variables: dict[str, list[Variable]]
|
|
124
|
-
globals: dict[str, list[Variable]]
|
|
125
|
-
locals: dict[str, list[Variable]]
|
|
126
|
-
filters: dict[str, list[Span]]
|
|
127
|
-
tags: dict[str, list[Span]]
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def analyze(template: BoundTemplate, *, include_partials: bool) -> TemplateAnalysis:
|
|
131
|
-
"""Report variable, filters and tags used in the given template.
|
|
132
|
-
|
|
133
|
-
This is used by `BoundTemplate.analyze`.
|
|
134
|
-
"""
|
|
135
|
-
variables = _VariableMap()
|
|
136
|
-
globals = _VariableMap() # noqa: A001
|
|
137
|
-
locals = _VariableMap() # noqa: A001
|
|
138
|
-
|
|
139
|
-
filters: defaultdict[str, list[Span]] = defaultdict(list)
|
|
140
|
-
tags: defaultdict[str, list[Span]] = defaultdict(list)
|
|
141
|
-
|
|
142
|
-
template_scope: set[str] = set()
|
|
143
|
-
root_scope = _StaticScope(template_scope)
|
|
144
|
-
static_context = RenderContext(template)
|
|
145
|
-
|
|
146
|
-
# Names of partial templates that have already been analyzed.
|
|
147
|
-
# Keys are hashes of partial template name and its arguments. If we've
|
|
148
|
-
# visited a template before but ith different arguments, later visits
|
|
149
|
-
# only record global variables so as not to double count locals, filters
|
|
150
|
-
# and tags.
|
|
151
|
-
seen: defaultdict[str, set[Optional[int]]] = defaultdict(set)
|
|
152
|
-
|
|
153
|
-
def _visit(
|
|
154
|
-
node: Node,
|
|
155
|
-
template_name: str,
|
|
156
|
-
scope: _StaticScope,
|
|
157
|
-
*,
|
|
158
|
-
just_globals: bool = False,
|
|
159
|
-
) -> None:
|
|
160
|
-
if template_name and not just_globals:
|
|
161
|
-
seen[template_name].add(None)
|
|
162
|
-
|
|
163
|
-
# Update tags from node.token
|
|
164
|
-
if (
|
|
165
|
-
not just_globals
|
|
166
|
-
and not isinstance(
|
|
167
|
-
node, (BlockNode, ConditionalBlockNode, MultiExpressionBlockNode)
|
|
168
|
-
)
|
|
169
|
-
and node.token.kind == TOKEN_TAG
|
|
170
|
-
):
|
|
171
|
-
tags[node.token.value].append(Span(template_name, node.token.start_index))
|
|
172
|
-
|
|
173
|
-
# Update variables from node.expressions()
|
|
174
|
-
for expr in node.expressions():
|
|
175
|
-
_analyze_variables(
|
|
176
|
-
expr,
|
|
177
|
-
template_name,
|
|
178
|
-
scope,
|
|
179
|
-
globals,
|
|
180
|
-
_VariableMap() if just_globals else variables,
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
if not just_globals:
|
|
184
|
-
# Update filters from expr
|
|
185
|
-
for name, span in _extract_filters(expr, template_name):
|
|
186
|
-
filters[name].append(span)
|
|
187
|
-
|
|
188
|
-
# Update the template scope from node.template_scope()
|
|
189
|
-
for ident in node.template_scope():
|
|
190
|
-
scope.add(ident)
|
|
191
|
-
locals.add(
|
|
192
|
-
Variable(
|
|
193
|
-
segments=[ident],
|
|
194
|
-
span=Span(template_name, ident.token.start_index),
|
|
195
|
-
)
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
if partial := node.partial_scope():
|
|
199
|
-
partial_name = (
|
|
200
|
-
partial.name
|
|
201
|
-
if isinstance(partial.name, str)
|
|
202
|
-
else str(partial.name.evaluate(static_context))
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
# If we've seen this partial before but with different arguments,
|
|
206
|
-
# we might want to visit it again but only capture globals.
|
|
207
|
-
_just_globals = partial_name in seen
|
|
208
|
-
if partial.key in seen[partial_name]:
|
|
209
|
-
# We've visited this partial template before with the same
|
|
210
|
-
# arguments.
|
|
211
|
-
return
|
|
212
|
-
|
|
213
|
-
seen[partial_name].add(partial.key)
|
|
214
|
-
partial_name = partial_name or template_name
|
|
215
|
-
|
|
216
|
-
partial_scope = (
|
|
217
|
-
_StaticScope(set(partial.in_scope))
|
|
218
|
-
if partial.scope == PartialScope.ISOLATED
|
|
219
|
-
else root_scope.push(set(partial.in_scope))
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
for child in node.children(
|
|
223
|
-
static_context, include_partials=include_partials
|
|
224
|
-
):
|
|
225
|
-
_visit(
|
|
226
|
-
child,
|
|
227
|
-
partial_name,
|
|
228
|
-
partial_scope,
|
|
229
|
-
just_globals=just_globals or _just_globals,
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
partial_scope.pop()
|
|
233
|
-
else:
|
|
234
|
-
scope.push(set(node.block_scope()))
|
|
235
|
-
for child in node.children(
|
|
236
|
-
static_context, include_partials=include_partials
|
|
237
|
-
):
|
|
238
|
-
_visit(child, template_name, scope, just_globals=just_globals)
|
|
239
|
-
scope.pop()
|
|
240
|
-
|
|
241
|
-
for node in template.nodes:
|
|
242
|
-
_visit(node, template.name, root_scope)
|
|
243
|
-
|
|
244
|
-
return TemplateAnalysis(
|
|
245
|
-
variables=variables.as_dict(),
|
|
246
|
-
globals=globals.as_dict(),
|
|
247
|
-
locals=locals.as_dict(),
|
|
248
|
-
filters=dict(filters),
|
|
249
|
-
tags=dict(tags),
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
async def analyze_async(
|
|
254
|
-
template: BoundTemplate, *, include_partials: bool
|
|
255
|
-
) -> TemplateAnalysis:
|
|
256
|
-
"""Report variable, filters and tags used in the given template.
|
|
257
|
-
|
|
258
|
-
This is used by `BoundTemplate.analyze_async`.
|
|
259
|
-
"""
|
|
260
|
-
variables = _VariableMap()
|
|
261
|
-
globals = _VariableMap() # noqa: A001
|
|
262
|
-
locals = _VariableMap() # noqa: A001
|
|
263
|
-
|
|
264
|
-
filters: defaultdict[str, list[Span]] = defaultdict(list)
|
|
265
|
-
tags: defaultdict[str, list[Span]] = defaultdict(list)
|
|
266
|
-
|
|
267
|
-
template_scope: set[str] = set()
|
|
268
|
-
root_scope = _StaticScope(template_scope)
|
|
269
|
-
static_context = RenderContext(template)
|
|
270
|
-
|
|
271
|
-
# Names of partial templates that have already been analyzed.
|
|
272
|
-
seen: defaultdict[str, set[Optional[int]]] = defaultdict(set)
|
|
273
|
-
|
|
274
|
-
async def _visit(
|
|
275
|
-
node: Node,
|
|
276
|
-
template_name: str,
|
|
277
|
-
scope: _StaticScope,
|
|
278
|
-
*,
|
|
279
|
-
just_globals: bool = False,
|
|
280
|
-
) -> None:
|
|
281
|
-
if template_name and not just_globals:
|
|
282
|
-
seen[template_name].add(None)
|
|
283
|
-
|
|
284
|
-
# Update tags from node.token
|
|
285
|
-
if (
|
|
286
|
-
not just_globals
|
|
287
|
-
and not isinstance(
|
|
288
|
-
node, (BlockNode, ConditionalBlockNode, MultiExpressionBlockNode)
|
|
289
|
-
)
|
|
290
|
-
and node.token.kind == TOKEN_TAG
|
|
291
|
-
):
|
|
292
|
-
tags[node.token.value].append(Span(template_name, node.token.start_index))
|
|
293
|
-
|
|
294
|
-
# Update variables from node.expressions()
|
|
295
|
-
for expr in node.expressions():
|
|
296
|
-
_analyze_variables(
|
|
297
|
-
expr,
|
|
298
|
-
template_name,
|
|
299
|
-
scope,
|
|
300
|
-
globals,
|
|
301
|
-
_VariableMap() if just_globals else variables,
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
if not just_globals:
|
|
305
|
-
# Update filters from expr
|
|
306
|
-
for name, span in _extract_filters(expr, template_name):
|
|
307
|
-
filters[name].append(span)
|
|
308
|
-
|
|
309
|
-
# Update the template scope from node.template_scope()
|
|
310
|
-
for ident in node.template_scope():
|
|
311
|
-
scope.add(ident)
|
|
312
|
-
locals.add(
|
|
313
|
-
Variable(
|
|
314
|
-
segments=[ident],
|
|
315
|
-
span=Span(template_name, ident.token.start_index),
|
|
316
|
-
)
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
if partial := node.partial_scope():
|
|
320
|
-
partial_name = (
|
|
321
|
-
partial.name
|
|
322
|
-
if isinstance(partial.name, str)
|
|
323
|
-
else str(partial.name.evaluate(static_context))
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
# If we've seen this partial before but with different arguments,
|
|
327
|
-
# we might want to visit it again but only capture globals.
|
|
328
|
-
_just_globals = partial_name in seen
|
|
329
|
-
if partial.key in seen[partial_name]:
|
|
330
|
-
# We've visited this partial template before with the same
|
|
331
|
-
# arguments.
|
|
332
|
-
return
|
|
333
|
-
|
|
334
|
-
seen[partial_name].add(partial.key)
|
|
335
|
-
partial_name = partial_name or template_name
|
|
336
|
-
|
|
337
|
-
partial_scope = (
|
|
338
|
-
_StaticScope(set(partial.in_scope))
|
|
339
|
-
if partial.scope == PartialScope.ISOLATED
|
|
340
|
-
else root_scope.push(set(partial.in_scope))
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
for child in await node.children_async(
|
|
344
|
-
static_context, include_partials=include_partials
|
|
345
|
-
):
|
|
346
|
-
await _visit(
|
|
347
|
-
child,
|
|
348
|
-
partial_name,
|
|
349
|
-
partial_scope,
|
|
350
|
-
just_globals=just_globals or _just_globals,
|
|
351
|
-
)
|
|
352
|
-
|
|
353
|
-
partial_scope.pop()
|
|
354
|
-
else:
|
|
355
|
-
scope.push(set(node.block_scope()))
|
|
356
|
-
for child in await node.children_async(
|
|
357
|
-
static_context, include_partials=include_partials
|
|
358
|
-
):
|
|
359
|
-
await _visit(child, template_name, scope, just_globals=just_globals)
|
|
360
|
-
scope.pop()
|
|
361
|
-
|
|
362
|
-
for node in template.nodes:
|
|
363
|
-
await _visit(node, template.name, root_scope)
|
|
364
|
-
|
|
365
|
-
return TemplateAnalysis(
|
|
366
|
-
variables=variables.as_dict(),
|
|
367
|
-
globals=globals.as_dict(),
|
|
368
|
-
locals=locals.as_dict(),
|
|
369
|
-
filters=dict(filters),
|
|
370
|
-
tags=dict(tags),
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
def _extract_filters(
|
|
375
|
-
expression: Expression, template_name: str
|
|
376
|
-
) -> Iterable[tuple[str, Span]]:
|
|
377
|
-
if isinstance(expression, TernaryFilteredExpression):
|
|
378
|
-
yield from _extract_filters(expression.left, template_name)
|
|
379
|
-
|
|
380
|
-
if expression.filters:
|
|
381
|
-
yield from (
|
|
382
|
-
(f.name, Span(template_name, f.token.start_index))
|
|
383
|
-
for f in expression.filters
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
if expression.tail_filters:
|
|
387
|
-
yield from (
|
|
388
|
-
(f.name, Span(template_name, f.token.start_index))
|
|
389
|
-
for f in expression.tail_filters
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
if isinstance(expression, FilteredExpression) and expression.filters:
|
|
393
|
-
yield from (
|
|
394
|
-
(f.name, Span(template_name, f.token.start_index))
|
|
395
|
-
for f in expression.filters
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
for expr in expression.children():
|
|
399
|
-
yield from _extract_filters(expr, template_name)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
def _analyze_variables(
|
|
403
|
-
expression: Expression,
|
|
404
|
-
template_name: str,
|
|
405
|
-
scope: _StaticScope,
|
|
406
|
-
globals: _VariableMap, # noqa: A002
|
|
407
|
-
variables: _VariableMap,
|
|
408
|
-
) -> None:
|
|
409
|
-
if isinstance(expression, Path):
|
|
410
|
-
var = Variable(
|
|
411
|
-
segments=_segments(expression, template_name),
|
|
412
|
-
span=Span(template_name, expression.token.start_index),
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
# NOTE: We're updating globals and variables here so we can manage scope while
|
|
416
|
-
# traversing the expression. This is for the benefit of lambda expressions that
|
|
417
|
-
# add names to the scope of their children.
|
|
418
|
-
variables.add(var)
|
|
419
|
-
|
|
420
|
-
root = str(var.segments[0])
|
|
421
|
-
if root not in scope:
|
|
422
|
-
globals.add(var)
|
|
423
|
-
|
|
424
|
-
if child_scope := expression.scope():
|
|
425
|
-
scope.push(set(child_scope))
|
|
426
|
-
for expr in expression.children():
|
|
427
|
-
_analyze_variables(expr, template_name, scope, globals, variables)
|
|
428
|
-
scope.pop()
|
|
429
|
-
else:
|
|
430
|
-
for expr in expression.children():
|
|
431
|
-
_analyze_variables(expr, template_name, scope, globals, variables)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def _segments(path: Path, template_name: str) -> Segments:
|
|
435
|
-
segments: Segments = []
|
|
436
|
-
|
|
437
|
-
for segment in path.path:
|
|
438
|
-
if isinstance(segment, Path):
|
|
439
|
-
segments.append(_segments(segment, template_name))
|
|
440
|
-
else:
|
|
441
|
-
segments.append(segment)
|
|
442
|
-
|
|
443
|
-
return segments
|
liquid/stream.py
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"""Step through a sequence of tokens."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Iterator
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
from .builtin.expressions import tokenize
|
|
9
|
-
from .exceptions import LiquidSyntaxError
|
|
10
|
-
from .token import TOKEN_EOF
|
|
11
|
-
from .token import TOKEN_EXPRESSION
|
|
12
|
-
from .token import Token
|
|
13
|
-
from .token import reverse_operators
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class TokenStream:
|
|
17
|
-
"""Step through a sequence of tokens."""
|
|
18
|
-
|
|
19
|
-
eof = Token(TOKEN_EOF, TOKEN_EOF, -1, "")
|
|
20
|
-
|
|
21
|
-
def __init__(self, tokens: Iterator[Token], block_depth_carry: int = 0):
|
|
22
|
-
self.tokens = list(tokens)
|
|
23
|
-
self.pos = 0
|
|
24
|
-
self.block_depth = block_depth_carry
|
|
25
|
-
|
|
26
|
-
def __next__(self) -> Token:
|
|
27
|
-
return self.next_token()
|
|
28
|
-
|
|
29
|
-
def next(self) -> Token:
|
|
30
|
-
"""Return the next token and advance the iterator."""
|
|
31
|
-
try:
|
|
32
|
-
token = self.tokens[self.pos]
|
|
33
|
-
self.pos += 1
|
|
34
|
-
return token
|
|
35
|
-
except IndexError:
|
|
36
|
-
return self.eof
|
|
37
|
-
|
|
38
|
-
def next_token(self) -> Token:
|
|
39
|
-
"""Return the next token and advance the iterator."""
|
|
40
|
-
try:
|
|
41
|
-
token = self.tokens[self.pos]
|
|
42
|
-
self.pos += 1
|
|
43
|
-
return token
|
|
44
|
-
except IndexError:
|
|
45
|
-
return self.eof
|
|
46
|
-
|
|
47
|
-
@property
|
|
48
|
-
def current(self) -> Token:
|
|
49
|
-
"""Return the token at the head of the stream without advancing."""
|
|
50
|
-
try:
|
|
51
|
-
return self.tokens[self.pos]
|
|
52
|
-
except IndexError:
|
|
53
|
-
return self.eof
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def peek(self) -> Token:
|
|
57
|
-
"""Look at the next token."""
|
|
58
|
-
try:
|
|
59
|
-
return self.tokens[self.pos + 1]
|
|
60
|
-
except IndexError:
|
|
61
|
-
return self.eof
|
|
62
|
-
|
|
63
|
-
def _expect(self, tok: Token, typ: str, value: Optional[str] = None) -> Token:
|
|
64
|
-
if tok.kind != typ or (value is not None and tok.value != value):
|
|
65
|
-
_typ = self._operator(tok.kind)
|
|
66
|
-
_expected_typ = self._operator(typ)
|
|
67
|
-
if value is not None:
|
|
68
|
-
msg = f"expected {_expected_typ} {value}, found {_typ}"
|
|
69
|
-
else:
|
|
70
|
-
msg = f"expected {_expected_typ}, found {_typ}"
|
|
71
|
-
raise LiquidSyntaxError(msg, token=tok)
|
|
72
|
-
return tok
|
|
73
|
-
|
|
74
|
-
def expect(self, typ: str, value: Optional[str] = None) -> Token:
|
|
75
|
-
"""Check the current token in the stream matches the given type and value.
|
|
76
|
-
|
|
77
|
-
Returns the current token if its type matches _typ_.
|
|
78
|
-
Raises a `LiquidSyntaxError` if it doesn't.
|
|
79
|
-
"""
|
|
80
|
-
return self._expect(self.current, typ, value)
|
|
81
|
-
|
|
82
|
-
def expect_peek(self, typ: str, value: Optional[str] = None) -> Token:
|
|
83
|
-
"""Check the next token in the stream matches the given type and value.
|
|
84
|
-
|
|
85
|
-
Returns the next token if its type matches _typ_.
|
|
86
|
-
Raises a `LiquidSyntaxError` it doesn't.
|
|
87
|
-
"""
|
|
88
|
-
return self._expect(self.peek, typ, value)
|
|
89
|
-
|
|
90
|
-
def eat(self, typ: str) -> Token:
|
|
91
|
-
"""Consume and return the next token.
|
|
92
|
-
|
|
93
|
-
If the type of the next token is equal to _typ_, raise an exceptions.
|
|
94
|
-
|
|
95
|
-
This is equivalent to `stream.expect(typ)` followed by `next(stream)`.
|
|
96
|
-
"""
|
|
97
|
-
tok = self.current
|
|
98
|
-
if tok.kind != typ:
|
|
99
|
-
msg = f"expected {self._operator(typ)}, found {self._operator(tok.kind)}"
|
|
100
|
-
raise LiquidSyntaxError(msg, token=tok)
|
|
101
|
-
return next(self)
|
|
102
|
-
|
|
103
|
-
def eat_one_of(self, *typ: str) -> Token:
|
|
104
|
-
"""Consume and return the next token.
|
|
105
|
-
|
|
106
|
-
If the type of the next token is equal to _typ_, raise an exceptions.
|
|
107
|
-
|
|
108
|
-
This is equivalent to `stream.expect(typ)` followed by `next(stream)`.
|
|
109
|
-
"""
|
|
110
|
-
tok = self.current
|
|
111
|
-
if tok.kind not in typ:
|
|
112
|
-
msg = f"expected one of {typ!r}, found {self._operator(tok.kind)}"
|
|
113
|
-
raise LiquidSyntaxError(msg, token=tok)
|
|
114
|
-
return next(self)
|
|
115
|
-
|
|
116
|
-
def into_inner(
|
|
117
|
-
self, *, tag: Optional[Token] = None, eat: bool = True
|
|
118
|
-
) -> TokenStream:
|
|
119
|
-
"""Return a stream of tokens for the current expression token.
|
|
120
|
-
|
|
121
|
-
If the current token is not an expression, a `LiquidSyntaxError` is
|
|
122
|
-
raised.
|
|
123
|
-
|
|
124
|
-
If _tag_ is given, it will be used to add context information to the
|
|
125
|
-
syntax error, should one be raised.
|
|
126
|
-
|
|
127
|
-
If _eat_ is true (the default), the current token is consumed.
|
|
128
|
-
"""
|
|
129
|
-
token = self.current
|
|
130
|
-
if token.kind != TOKEN_EXPRESSION:
|
|
131
|
-
raise LiquidSyntaxError("missing expression", token=tag or token)
|
|
132
|
-
|
|
133
|
-
if eat:
|
|
134
|
-
next(self)
|
|
135
|
-
return TokenStream(tokenize(token.value, parent_token=token))
|
|
136
|
-
|
|
137
|
-
def expect_eos(self) -> None:
|
|
138
|
-
"""Raise a syntax error if we're not at the end of the stream."""
|
|
139
|
-
if self.current.kind != TOKEN_EOF:
|
|
140
|
-
raise LiquidSyntaxError(
|
|
141
|
-
f"unexpected token {self.current.kind}", token=self.current
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
def _operator(self, kind: str) -> str:
|
|
145
|
-
try:
|
|
146
|
-
return repr(reverse_operators[kind])
|
|
147
|
-
except KeyError:
|
|
148
|
-
return kind
|
liquid/stringify.py
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"""Stringify a Python object ready for output in a Liquid template."""
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from liquid import Markup
|
|
5
|
-
from liquid import escape
|
|
6
|
-
from liquid import soft_str
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def to_liquid_string(val: Any, autoescape: bool) -> str:
|
|
10
|
-
"""Stringify a Python object ready for output in a Liquid template."""
|
|
11
|
-
if isinstance(val, str) or (autoescape and hasattr(val, "__html__")):
|
|
12
|
-
pass
|
|
13
|
-
elif isinstance(val, bool):
|
|
14
|
-
val = str(val).lower()
|
|
15
|
-
elif val is None:
|
|
16
|
-
val = ""
|
|
17
|
-
elif isinstance(val, list):
|
|
18
|
-
if autoescape:
|
|
19
|
-
val = Markup("").join(soft_str(itm) for itm in val)
|
|
20
|
-
else:
|
|
21
|
-
val = "".join(soft_str(itm) for itm in val)
|
|
22
|
-
elif isinstance(val, range):
|
|
23
|
-
val = f"{val.start}..{val.stop - 1}"
|
|
24
|
-
else:
|
|
25
|
-
val = str(val)
|
|
26
|
-
|
|
27
|
-
if autoescape:
|
|
28
|
-
val = escape(val)
|
|
29
|
-
|
|
30
|
-
assert isinstance(val, str)
|
|
31
|
-
return val
|
liquid/tag.py
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"""Liquid tag base class."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from abc import ABC
|
|
6
|
-
from abc import abstractmethod
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
8
|
-
|
|
9
|
-
from .ast import IllegalNode
|
|
10
|
-
from .ast import Node
|
|
11
|
-
from .exceptions import LiquidError
|
|
12
|
-
from .parser import eat_block
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from .environment import Environment
|
|
16
|
-
from .stream import TokenStream
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class Tag(ABC):
|
|
20
|
-
"""Base class for all built-in and custom template tags."""
|
|
21
|
-
|
|
22
|
-
block = True
|
|
23
|
-
name = ""
|
|
24
|
-
end = ""
|
|
25
|
-
|
|
26
|
-
def __init__(self, env: Environment):
|
|
27
|
-
self.env = env
|
|
28
|
-
|
|
29
|
-
def get_node(self, stream: TokenStream) -> Node:
|
|
30
|
-
"""Wraps `Tag.parse`, possibly returning an `IllegalNode`."""
|
|
31
|
-
try:
|
|
32
|
-
return self.parse(stream)
|
|
33
|
-
except LiquidError as err:
|
|
34
|
-
token = stream.current
|
|
35
|
-
if not err.token:
|
|
36
|
-
err.token = token
|
|
37
|
-
|
|
38
|
-
self.env.error(err)
|
|
39
|
-
|
|
40
|
-
if self.block and hasattr(self, "end"):
|
|
41
|
-
eat_block(stream, (self.end,))
|
|
42
|
-
|
|
43
|
-
return IllegalNode(token)
|
|
44
|
-
|
|
45
|
-
@abstractmethod
|
|
46
|
-
def parse(self, stream: TokenStream) -> Node:
|
|
47
|
-
"""Return a parse tree node by parsing tokens from the given stream."""
|