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/_template.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
5
|
+
from itertools import chain
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ._chain_map import ReadOnlyChainMap
|
|
9
|
+
from ._context import RenderContext
|
|
10
|
+
from ._io import StringBuffer
|
|
11
|
+
from ._markup import render_block, render_block_async
|
|
12
|
+
from ._static_analysis import Segments, TemplateAnalysis, analyze, analyze_async
|
|
13
|
+
from .exceptions import DetailedLiquidError, LiquidError
|
|
14
|
+
from .expressions import ErrorExpr
|
|
15
|
+
from .tags import CommentTag, DocTag, ErrorNode, InlineCommentTag, TemplateComment
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ._markup import Block, Expression, Node
|
|
19
|
+
from .environment import Environment
|
|
20
|
+
from .tokens import Token
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Template:
|
|
24
|
+
"""A compiled template ready to be rendered."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
env: Environment,
|
|
29
|
+
source: str,
|
|
30
|
+
name: str,
|
|
31
|
+
nodes: Block,
|
|
32
|
+
*,
|
|
33
|
+
path: str | None = None,
|
|
34
|
+
globals: Mapping[str, object] | None,
|
|
35
|
+
matter: Mapping[str, object] | None,
|
|
36
|
+
up_to_date: Callable[[], bool | Awaitable[bool]] | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.env = env
|
|
39
|
+
self.source = source
|
|
40
|
+
self.name = name
|
|
41
|
+
self.nodes = nodes
|
|
42
|
+
self.path = path
|
|
43
|
+
self.globals = globals
|
|
44
|
+
self.matter = matter
|
|
45
|
+
self.up_to_date = up_to_date
|
|
46
|
+
|
|
47
|
+
self._lines: list[str] | None = None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def lines(self) -> list[str]:
|
|
51
|
+
if self._lines is None:
|
|
52
|
+
lines = re.split(r"(?<=\n)", self.source)
|
|
53
|
+
if lines[-1] == "":
|
|
54
|
+
lines.pop()
|
|
55
|
+
self._lines = lines
|
|
56
|
+
|
|
57
|
+
return self._lines
|
|
58
|
+
|
|
59
|
+
def line_col(self, token: Token) -> tuple[int, int]:
|
|
60
|
+
"""Return the line and column number of `token` in this template."""
|
|
61
|
+
index = token[1]
|
|
62
|
+
cumulative_length = 0
|
|
63
|
+
target_line_index = -1
|
|
64
|
+
|
|
65
|
+
for i, line in enumerate(self.lines):
|
|
66
|
+
cumulative_length += len(line)
|
|
67
|
+
if index < cumulative_length:
|
|
68
|
+
target_line_index = i
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
if target_line_index == -1:
|
|
72
|
+
raise LiquidError("index out of bounds")
|
|
73
|
+
|
|
74
|
+
line_number = target_line_index + 1
|
|
75
|
+
line = self.lines[target_line_index]
|
|
76
|
+
column_number = index - (cumulative_length - len(line))
|
|
77
|
+
return line_number, column_number
|
|
78
|
+
|
|
79
|
+
def render(self, data: Mapping[str, object] | None = None) -> str:
|
|
80
|
+
"""Render this template with variables from `data`."""
|
|
81
|
+
buffer: StringBuffer = StringBuffer()
|
|
82
|
+
context = RenderContext(self, globals=self.make_globals(data))
|
|
83
|
+
self.render_with_context(context, buffer)
|
|
84
|
+
return buffer.getvalue()
|
|
85
|
+
|
|
86
|
+
async def render_async(self, data: Mapping[str, object] | None = None) -> str:
|
|
87
|
+
"""Render this template asynchronously with variables from `data`."""
|
|
88
|
+
buffer: StringBuffer = StringBuffer()
|
|
89
|
+
context = RenderContext(self, globals=self.make_globals(data))
|
|
90
|
+
await self.render_with_context_async(context, buffer)
|
|
91
|
+
return buffer.getvalue()
|
|
92
|
+
|
|
93
|
+
def render_with_context(self, context: RenderContext, buffer: StringBuffer) -> None:
|
|
94
|
+
render_block(self.nodes, context, buffer)
|
|
95
|
+
|
|
96
|
+
async def render_with_context_async(
|
|
97
|
+
self, context: RenderContext, buffer: StringBuffer
|
|
98
|
+
) -> None:
|
|
99
|
+
await render_block_async(self.nodes, context, buffer)
|
|
100
|
+
|
|
101
|
+
def render_with_errors(
|
|
102
|
+
self, data: Mapping[str, object] | None = None
|
|
103
|
+
) -> tuple[str, list[DetailedLiquidError]]:
|
|
104
|
+
"""Render this template with error capture and rendering.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
Rendered text and a list of :class:`liquid.exceptions.DetailedLiquidError`.
|
|
109
|
+
"""
|
|
110
|
+
buffer: StringBuffer = StringBuffer()
|
|
111
|
+
context = RenderContext(
|
|
112
|
+
self, globals=self.make_globals(data), capture_errors=True
|
|
113
|
+
)
|
|
114
|
+
self.render_with_context(context, buffer)
|
|
115
|
+
return buffer.getvalue(), context.errors
|
|
116
|
+
|
|
117
|
+
async def render_with_errors_async(
|
|
118
|
+
self, data: Mapping[str, object] | None = None
|
|
119
|
+
) -> tuple[str, list[DetailedLiquidError]]:
|
|
120
|
+
"""Render this template asynchronously with error capture and rendering.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
Rendered text and a list of :class:`liquid.exceptions.DetailedLiquidError`.
|
|
125
|
+
"""
|
|
126
|
+
buffer: StringBuffer = StringBuffer()
|
|
127
|
+
context = RenderContext(
|
|
128
|
+
self, globals=self.make_globals(data), capture_errors=True
|
|
129
|
+
)
|
|
130
|
+
await self.render_with_context_async(context, buffer)
|
|
131
|
+
return buffer.getvalue(), context.errors
|
|
132
|
+
|
|
133
|
+
def make_globals(
|
|
134
|
+
self, data: Mapping[str, object] | None
|
|
135
|
+
) -> Mapping[str, object] | None:
|
|
136
|
+
if not (data or self.matter or self.globals):
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
chain = ReadOnlyChainMap()
|
|
140
|
+
|
|
141
|
+
if self.globals:
|
|
142
|
+
chain.push(self.globals)
|
|
143
|
+
|
|
144
|
+
if self.matter:
|
|
145
|
+
chain.push(self.matter)
|
|
146
|
+
|
|
147
|
+
if data:
|
|
148
|
+
chain.push(data)
|
|
149
|
+
|
|
150
|
+
return chain
|
|
151
|
+
|
|
152
|
+
def is_up_to_date(self) -> bool:
|
|
153
|
+
"""Return ``False`` if the template has bee modified, ``True`` otherwise."""
|
|
154
|
+
if not self.up_to_date:
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
uptodate = self.up_to_date()
|
|
158
|
+
if not isinstance(uptodate, bool):
|
|
159
|
+
raise LiquidError(
|
|
160
|
+
f"expected a boolean from uptodate, found {type(uptodate).__name__}",
|
|
161
|
+
)
|
|
162
|
+
return uptodate
|
|
163
|
+
|
|
164
|
+
async def is_up_to_date_async(self) -> bool:
|
|
165
|
+
"""An async version of `is_up_to_date`.
|
|
166
|
+
|
|
167
|
+
If `template.up_to_date` is a coroutine, it wil be awaited. Otherwise it will be
|
|
168
|
+
called just like `is_up_to_date`
|
|
169
|
+
"""
|
|
170
|
+
if not self.up_to_date:
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
uptodate = self.up_to_date()
|
|
174
|
+
if isinstance(uptodate, Awaitable):
|
|
175
|
+
return await uptodate
|
|
176
|
+
return uptodate
|
|
177
|
+
|
|
178
|
+
def analyze(self, *, include_partials: bool = True) -> TemplateAnalysis:
|
|
179
|
+
"""Statically analyze this template and any included/rendered templates.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
include_partials
|
|
184
|
+
If ``True``, we will try to load partial templates and analyze those templates too.
|
|
185
|
+
"""
|
|
186
|
+
return analyze(self, include_partials=include_partials)
|
|
187
|
+
|
|
188
|
+
async def analyze_async(self, *, include_partials: bool = True) -> TemplateAnalysis:
|
|
189
|
+
"""An async version of `analyze`."""
|
|
190
|
+
return await analyze_async(self, include_partials=include_partials)
|
|
191
|
+
|
|
192
|
+
def variables(self, *, include_partials: bool = True) -> list[str]:
|
|
193
|
+
"""Return a list of variables used in this template without path segments.
|
|
194
|
+
|
|
195
|
+
Includes variables that are _local_ to the template, like those crated with
|
|
196
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
197
|
+
|
|
198
|
+
See also [global_variables][liquid.BoundTemplate.global_variables].
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
203
|
+
included/rendered templates too.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
A list of distinct root segments for variables in this template.
|
|
207
|
+
"""
|
|
208
|
+
return list(self.analyze(include_partials=include_partials).variables)
|
|
209
|
+
|
|
210
|
+
async def variables_async(self, *, include_partials: bool = True) -> list[str]:
|
|
211
|
+
"""Return a list of variables used in this template without path segments.
|
|
212
|
+
|
|
213
|
+
Includes variables that are _local_ to the template, like those crated with
|
|
214
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
215
|
+
|
|
216
|
+
See also [global_variables][liquid.BoundTemplate.global_variables].
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
221
|
+
included/rendered templates too.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
A list of distinct root segments for variables in this template.
|
|
225
|
+
"""
|
|
226
|
+
return list(
|
|
227
|
+
(await self.analyze_async(include_partials=include_partials)).variables
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def variable_paths(self, *, include_partials: bool = True) -> list[str]:
|
|
231
|
+
"""Return a list of variables used in this template including all path segments.
|
|
232
|
+
|
|
233
|
+
Includes variables that are _local_ to the template, like those crated with
|
|
234
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
235
|
+
|
|
236
|
+
See also [global_variable_paths][liquid.BoundTemplate.global_variable_paths].
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
241
|
+
included/rendered templates too.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
A list of distinct paths for variables in this template.
|
|
245
|
+
"""
|
|
246
|
+
analysis = self.analyze(include_partials=include_partials)
|
|
247
|
+
return list(
|
|
248
|
+
{v["path"] for v in chain.from_iterable(list(analysis.variables.values()))}
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
async def variable_paths_async(self, *, include_partials: bool = True) -> list[str]:
|
|
252
|
+
"""Return a list of variables used in this template including all path segments.
|
|
253
|
+
|
|
254
|
+
Includes variables that are _local_ to the template, like those crated with
|
|
255
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
256
|
+
|
|
257
|
+
See also [global_variable_paths][liquid.BoundTemplate.global_variable_paths].
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
262
|
+
included/rendered templates too.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
A list of distinct paths for variables in this template.
|
|
266
|
+
"""
|
|
267
|
+
analysis = await self.analyze_async(include_partials=include_partials)
|
|
268
|
+
return list(
|
|
269
|
+
{v["path"] for v in chain.from_iterable(list(analysis.variables.values()))}
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def variable_segments(self, *, include_partials: bool = True) -> list[Segments]:
|
|
273
|
+
"""Return a list of variables used in this template, each as a list of segments.
|
|
274
|
+
|
|
275
|
+
Includes variables that are _local_ to the template, like those crated with
|
|
276
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
277
|
+
|
|
278
|
+
See [global_variable_segments][liquid.BoundTemplate.global_variable_segments].
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
283
|
+
included/rendered templates too.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
A list of distinct paths for variables in this template.
|
|
287
|
+
"""
|
|
288
|
+
analysis = self.analyze(include_partials=include_partials)
|
|
289
|
+
return list(
|
|
290
|
+
{w["segments"] for w in chain.from_iterable(analysis.variables.values())}
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
async def variable_segments_async(
|
|
294
|
+
self, *, include_partials: bool = True
|
|
295
|
+
) -> list[Segments]:
|
|
296
|
+
"""Return a list of variables used in this template, each as a list of segments.
|
|
297
|
+
|
|
298
|
+
Includes variables that are _local_ to the template, like those crated with
|
|
299
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
300
|
+
|
|
301
|
+
See [global_variable_segments][liquid.BoundTemplate.global_variable_segments].
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
306
|
+
included/rendered templates too.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
A list of distinct paths for variables in this template.
|
|
310
|
+
"""
|
|
311
|
+
analysis = await self.analyze_async(include_partials=include_partials)
|
|
312
|
+
return list(
|
|
313
|
+
{w["segments"] for w in chain.from_iterable(analysis.variables.values())}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def global_variables(self, *, include_partials: bool = True) -> list[str]:
|
|
317
|
+
"""Return a list of variables used in this template without path segments.
|
|
318
|
+
|
|
319
|
+
Excludes variables that are _local_ to the template, like those crated with
|
|
320
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
325
|
+
included/rendered templates too.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
A list of distinct root segments for variables in this template.
|
|
329
|
+
"""
|
|
330
|
+
return list(self.analyze(include_partials=include_partials).globals)
|
|
331
|
+
|
|
332
|
+
async def global_variables_async(
|
|
333
|
+
self, *, include_partials: bool = True
|
|
334
|
+
) -> list[str]:
|
|
335
|
+
"""Return a list of variables used in this template without path segments.
|
|
336
|
+
|
|
337
|
+
Excludes variables that are _local_ to the template, like those crated with
|
|
338
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
339
|
+
|
|
340
|
+
Parameters
|
|
341
|
+
----------
|
|
342
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
343
|
+
included/rendered templates too.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
A list of distinct root segments for variables in this template.
|
|
347
|
+
"""
|
|
348
|
+
return list(
|
|
349
|
+
(await self.analyze_async(include_partials=include_partials)).globals
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def global_variable_paths(self, *, include_partials: bool = True) -> list[str]:
|
|
353
|
+
"""Return a list of variables used in this template including all path segments.
|
|
354
|
+
|
|
355
|
+
Excludes variables that are _local_ to the template, like those crated with
|
|
356
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
361
|
+
included/rendered templates too.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
A list of distinct paths for variables in this template.
|
|
365
|
+
"""
|
|
366
|
+
analysis = self.analyze(include_partials=include_partials)
|
|
367
|
+
return list(
|
|
368
|
+
{v["path"] for v in chain.from_iterable(list(analysis.globals.values()))}
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
async def global_variable_paths_async(
|
|
372
|
+
self, *, include_partials: bool = True
|
|
373
|
+
) -> list[str]:
|
|
374
|
+
"""Return a list of variables used in this template including all path segments.
|
|
375
|
+
|
|
376
|
+
Excludes variables that are _local_ to the template, like those crated with
|
|
377
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
381
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
382
|
+
included/rendered templates too.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
A list of distinct paths for variables in this template.
|
|
386
|
+
"""
|
|
387
|
+
analysis = await self.analyze_async(include_partials=include_partials)
|
|
388
|
+
return list(
|
|
389
|
+
{v["path"] for v in chain.from_iterable(list(analysis.globals.values()))}
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def global_variable_segments(
|
|
393
|
+
self, *, include_partials: bool = True
|
|
394
|
+
) -> list[Segments]:
|
|
395
|
+
"""Return a list of variables used in this template, each as a list of segments.
|
|
396
|
+
|
|
397
|
+
Excludes variables that are _local_ to the template, like those crated with
|
|
398
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
403
|
+
included/rendered templates too.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
A list of distinct paths for variables in this template.
|
|
407
|
+
"""
|
|
408
|
+
analysis = self.analyze(include_partials=include_partials)
|
|
409
|
+
return list(
|
|
410
|
+
{w["segments"] for w in chain.from_iterable(analysis.globals.values())}
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
async def global_variable_segments_async(
|
|
414
|
+
self, *, include_partials: bool = True
|
|
415
|
+
) -> list[Segments]:
|
|
416
|
+
"""Return a list of variables used in this template, each as a list of segments.
|
|
417
|
+
|
|
418
|
+
Excludes variables that are _local_ to the template, like those crated with
|
|
419
|
+
``{% assign %}`` and ``{% capture %}``.
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
include_partials: If ``True``, will try to load and find variables in
|
|
424
|
+
included/rendered templates too.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
A list of distinct paths for variables in this template.
|
|
428
|
+
"""
|
|
429
|
+
analysis = await self.analyze_async(include_partials=include_partials)
|
|
430
|
+
return list(
|
|
431
|
+
{w["segments"] for w in chain.from_iterable(analysis.globals.values())}
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def filter_names(self, *, include_partials: bool = True) -> list[str]:
|
|
435
|
+
"""Return a list of filter names used in this template."""
|
|
436
|
+
return list(self.analyze(include_partials=include_partials).filters)
|
|
437
|
+
|
|
438
|
+
async def filter_names_async(self, *, include_partials: bool = True) -> list[str]:
|
|
439
|
+
"""Return a list of filter names used in this template."""
|
|
440
|
+
return list(
|
|
441
|
+
(await self.analyze_async(include_partials=include_partials)).filters
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
def tag_names(self, *, include_partials: bool = True) -> list[str]:
|
|
445
|
+
"""Return a list of tag names used in this template."""
|
|
446
|
+
return list(self.analyze(include_partials=include_partials).tags)
|
|
447
|
+
|
|
448
|
+
async def tag_names_async(self, *, include_partials: bool = True) -> list[str]:
|
|
449
|
+
"""Return a list of tag names used in this template."""
|
|
450
|
+
return list((await self.analyze_async(include_partials=include_partials)).tags)
|
|
451
|
+
|
|
452
|
+
def comments(self) -> list[CommentTag | InlineCommentTag | TemplateComment]:
|
|
453
|
+
"""Return a list of comment tag nodes found in this template.
|
|
454
|
+
|
|
455
|
+
Instances of `CommentNode` and `InlineCommentNode` have `token` and
|
|
456
|
+
`text` properties. Use `[node.text for node in template.comments()]` to
|
|
457
|
+
get a list of comment strings.
|
|
458
|
+
|
|
459
|
+
Note that this method does not try to load included or rendered
|
|
460
|
+
templates.
|
|
461
|
+
"""
|
|
462
|
+
nodes: list[CommentTag | InlineCommentTag | TemplateComment] = []
|
|
463
|
+
|
|
464
|
+
def visit(node: Node) -> None:
|
|
465
|
+
if isinstance(node, str):
|
|
466
|
+
return
|
|
467
|
+
|
|
468
|
+
if isinstance(node, (CommentTag, InlineCommentTag, TemplateComment)):
|
|
469
|
+
nodes.append(node)
|
|
470
|
+
|
|
471
|
+
for child in node.children():
|
|
472
|
+
visit(child)
|
|
473
|
+
|
|
474
|
+
for child in self.nodes:
|
|
475
|
+
visit(child)
|
|
476
|
+
|
|
477
|
+
return nodes
|
|
478
|
+
|
|
479
|
+
def docs(self) -> list[DocTag]:
|
|
480
|
+
"""Return a list of doc tag nodes found in this template.
|
|
481
|
+
|
|
482
|
+
Instances of `DocNode` have `token` and `text` properties. Use
|
|
483
|
+
`[node.text for node in template.docs()]` to get a list of doc strings.
|
|
484
|
+
"""
|
|
485
|
+
nodes: list[DocTag] = []
|
|
486
|
+
|
|
487
|
+
def visit(node: Node) -> None:
|
|
488
|
+
if isinstance(node, str):
|
|
489
|
+
return
|
|
490
|
+
|
|
491
|
+
if isinstance(node, DocTag):
|
|
492
|
+
nodes.append(node)
|
|
493
|
+
|
|
494
|
+
for child in node.children():
|
|
495
|
+
visit(child)
|
|
496
|
+
|
|
497
|
+
for child in self.nodes:
|
|
498
|
+
visit(child)
|
|
499
|
+
|
|
500
|
+
return nodes
|
|
501
|
+
|
|
502
|
+
def errors(self) -> list[ErrorNode | ErrorExpr]:
|
|
503
|
+
"""Return a list of error nodes found in this template.
|
|
504
|
+
|
|
505
|
+
Error nodes are inserted by lax parsers via ``Environment.parser``. Strict
|
|
506
|
+
parsers raise errors before a template can be created.
|
|
507
|
+
"""
|
|
508
|
+
nodes: list[ErrorNode | ErrorExpr] = []
|
|
509
|
+
|
|
510
|
+
def visit_expr(expr: Expression) -> None:
|
|
511
|
+
if isinstance(expr, ErrorExpr):
|
|
512
|
+
nodes.append(expr)
|
|
513
|
+
|
|
514
|
+
for child in expr.children():
|
|
515
|
+
visit_expr(child)
|
|
516
|
+
|
|
517
|
+
def visit(node: Node) -> None:
|
|
518
|
+
if isinstance(node, str):
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
if isinstance(node, ErrorNode):
|
|
522
|
+
nodes.append(node)
|
|
523
|
+
|
|
524
|
+
for expr in node.expressions():
|
|
525
|
+
visit_expr(expr)
|
|
526
|
+
|
|
527
|
+
for child in node.children():
|
|
528
|
+
visit(child)
|
|
529
|
+
|
|
530
|
+
for child in self.nodes:
|
|
531
|
+
visit(child)
|
|
532
|
+
|
|
533
|
+
return nodes
|
|
534
|
+
|
|
535
|
+
def __str__(self) -> str:
|
|
536
|
+
return self.source
|
liquid/drops/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .forloop import ForLoop
|
|
2
|
+
from .iterable import AsyncIterableDrop, IterableDrop
|
|
3
|
+
from .range import Range
|
|
4
|
+
from .self_ import Self
|
|
5
|
+
from .tablerowloop import TableRowLoop
|
|
6
|
+
from .undefined import BLANK, EMPTY, FalsyStrictUndefined, StrictUndefined, Undefined
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"BLANK",
|
|
10
|
+
"EMPTY",
|
|
11
|
+
"AsyncIterableDrop",
|
|
12
|
+
"FalsyStrictUndefined",
|
|
13
|
+
"ForLoop",
|
|
14
|
+
"IterableDrop",
|
|
15
|
+
"Range",
|
|
16
|
+
"Self",
|
|
17
|
+
"StrictUndefined",
|
|
18
|
+
"TableRowLoop",
|
|
19
|
+
"Undefined",
|
|
20
|
+
)
|
liquid/drops/forloop.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .._nothing import NOTHING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .._context import RenderContext
|
|
9
|
+
from .._drop import ContextHint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ForLoop:
|
|
13
|
+
__slots__ = ("index", "length", "name", "parent_loop")
|
|
14
|
+
|
|
15
|
+
def __init__(self, name: str, length: int, parent_loop: ForLoop | None) -> None:
|
|
16
|
+
self.name = name
|
|
17
|
+
self.length = length
|
|
18
|
+
self.parent_loop = parent_loop or NOTHING
|
|
19
|
+
self.index = -1
|
|
20
|
+
|
|
21
|
+
def __liquid_hasattr__(self, name: str, context: RenderContext) -> bool:
|
|
22
|
+
return name in (
|
|
23
|
+
"name",
|
|
24
|
+
"length",
|
|
25
|
+
"index",
|
|
26
|
+
"index0",
|
|
27
|
+
"rindex",
|
|
28
|
+
"rindex0",
|
|
29
|
+
"first",
|
|
30
|
+
"last",
|
|
31
|
+
"parentloop",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def __liquid_getattr__(self, name: str, context: RenderContext) -> object:
|
|
35
|
+
if name == "first":
|
|
36
|
+
return self.index == 0
|
|
37
|
+
|
|
38
|
+
if name == "index":
|
|
39
|
+
return self.index + 1
|
|
40
|
+
|
|
41
|
+
if name == "index0":
|
|
42
|
+
return self.index
|
|
43
|
+
|
|
44
|
+
if name == "last":
|
|
45
|
+
return self.index == self.length - 1
|
|
46
|
+
|
|
47
|
+
if name == "length":
|
|
48
|
+
return self.length
|
|
49
|
+
|
|
50
|
+
if name == "name":
|
|
51
|
+
return self.name
|
|
52
|
+
|
|
53
|
+
if name == "parentloop":
|
|
54
|
+
return self.parent_loop
|
|
55
|
+
|
|
56
|
+
if name == "rindex":
|
|
57
|
+
return self.length - self.index
|
|
58
|
+
|
|
59
|
+
if name == "rindex0":
|
|
60
|
+
return self.length - self.index - 1
|
|
61
|
+
|
|
62
|
+
raise AttributeError
|
|
63
|
+
|
|
64
|
+
def step(self) -> None:
|
|
65
|
+
self.index += 1
|
|
66
|
+
|
|
67
|
+
def __str__(self) -> str:
|
|
68
|
+
return "Liquid::ForLoopDrop"
|
|
69
|
+
|
|
70
|
+
def __bool__(self) -> bool:
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
def __liquid__(self, hint: ContextHint, context: RenderContext) -> object:
|
|
74
|
+
match hint:
|
|
75
|
+
case "output" | "string" | "object":
|
|
76
|
+
return str(self)
|
|
77
|
+
case _:
|
|
78
|
+
return NOTHING
|
liquid/drops/iterable.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IterableDrop[T](Iterable[T]):
|
|
5
|
+
def __init__(self, it: Iterable[T], length: int) -> None:
|
|
6
|
+
self.it = it
|
|
7
|
+
self.length = length
|
|
8
|
+
|
|
9
|
+
def __iter__(self) -> Iterator[T]:
|
|
10
|
+
return iter(self.it)
|
|
11
|
+
|
|
12
|
+
def __len__(self) -> int:
|
|
13
|
+
return self.length
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncIterableDrop[T](AsyncIterable[T]):
|
|
17
|
+
def __init__(self, it: AsyncIterable[T], length: int) -> None:
|
|
18
|
+
self.it = it
|
|
19
|
+
self.length = length
|
|
20
|
+
|
|
21
|
+
def __aiter__(self) -> AsyncIterator[T]:
|
|
22
|
+
return aiter(self.it)
|
|
23
|
+
|
|
24
|
+
def __len__(self) -> int:
|
|
25
|
+
return self.length
|