python-liquid 2.1.0__py3-none-any.whl → 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- liquid/__init__.py +1 -1
- liquid/ast.py +17 -4
- liquid/builtin/__init__.py +2 -0
- liquid/builtin/filters/string.py +9 -0
- liquid/builtin/loaders/caching_file_system_loader.py +4 -0
- liquid/builtin/loaders/file_system_loader.py +21 -4
- liquid/builtin/tags/case_tag.py +3 -3
- liquid/builtin/tags/comment_tag.py +2 -2
- liquid/builtin/tags/doc_tag.py +1 -1
- liquid/builtin/tags/if_tag.py +1 -1
- liquid/builtin/tags/render_tag.py +85 -28
- liquid/exceptions.py +3 -2
- liquid/extra/__init__.py +2 -1
- liquid/extra/filters/babel.py +1 -1
- liquid/extra/tags/__init__.py +2 -0
- liquid/extra/tags/extends_tag.py +1 -2
- liquid/extra/tags/snippet_tag.py +107 -0
- liquid/filter.py +4 -1
- liquid/static_analysis.py +105 -32
- liquid/template.py +61 -6
- {python_liquid-2.1.0.dist-info → python_liquid-2.2.0.dist-info}/METADATA +3 -2
- {python_liquid-2.1.0.dist-info → python_liquid-2.2.0.dist-info}/RECORD +24 -23
- {python_liquid-2.1.0.dist-info → python_liquid-2.2.0.dist-info}/WHEEL +1 -1
- {python_liquid-2.1.0.dist-info → python_liquid-2.2.0.dist-info}/licenses/LICENSE +0 -0
liquid/__init__.py
CHANGED
liquid/ast.py
CHANGED
|
@@ -9,7 +9,9 @@ from enum import auto
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
from typing import Collection
|
|
11
11
|
from typing import Iterable
|
|
12
|
+
from typing import Optional
|
|
12
13
|
from typing import TextIO
|
|
14
|
+
from typing import Union
|
|
13
15
|
|
|
14
16
|
from .exceptions import DisabledTagError
|
|
15
17
|
from .output import NullIO
|
|
@@ -104,7 +106,7 @@ class Node(ABC):
|
|
|
104
106
|
"""Return variables this node adds to the node's block scope."""
|
|
105
107
|
return []
|
|
106
108
|
|
|
107
|
-
def partial_scope(self) -> Partial
|
|
109
|
+
def partial_scope(self) -> Optional[Partial]:
|
|
108
110
|
"""Return information about a partial template loaded by this node."""
|
|
109
111
|
return None
|
|
110
112
|
|
|
@@ -121,19 +123,30 @@ class Partial:
|
|
|
121
123
|
"""Partial template meta data.
|
|
122
124
|
|
|
123
125
|
Args:
|
|
124
|
-
name:
|
|
126
|
+
name: The name of the partial or an expression resolving to the name
|
|
127
|
+
associated with the partial template.
|
|
125
128
|
scope: The kind of scope the partial template should have when loaded.
|
|
126
129
|
in_scope: Names that will be added to the partial template scope.
|
|
130
|
+
key: A hash of the partial template name and any arguments the partial
|
|
131
|
+
template will be rendered with that might affect its scope. If a
|
|
132
|
+
key is provided, static analysis helpers will visit a partial
|
|
133
|
+
template once for each distinct key.
|
|
127
134
|
"""
|
|
128
135
|
|
|
129
|
-
__slots__ = ("name", "scope", "in_scope")
|
|
136
|
+
__slots__ = ("name", "scope", "in_scope", "key")
|
|
130
137
|
|
|
131
138
|
def __init__(
|
|
132
|
-
self,
|
|
139
|
+
self,
|
|
140
|
+
name: Union[Expression, str],
|
|
141
|
+
scope: PartialScope,
|
|
142
|
+
in_scope: Iterable[Identifier],
|
|
143
|
+
*,
|
|
144
|
+
key: Optional[int] = None,
|
|
133
145
|
) -> None:
|
|
134
146
|
self.name = name
|
|
135
147
|
self.scope = scope
|
|
136
148
|
self.in_scope = in_scope
|
|
149
|
+
self.key = key
|
|
137
150
|
|
|
138
151
|
|
|
139
152
|
class IllegalNode(Node):
|
liquid/builtin/__init__.py
CHANGED
|
@@ -58,6 +58,7 @@ from .filters.string import replace_last
|
|
|
58
58
|
from .filters.string import rstrip
|
|
59
59
|
from .filters.string import slice_
|
|
60
60
|
from .filters.string import split
|
|
61
|
+
from .filters.string import squish
|
|
61
62
|
from .filters.string import strip
|
|
62
63
|
from .filters.string import strip_html
|
|
63
64
|
from .filters.string import strip_newlines
|
|
@@ -175,6 +176,7 @@ def register(env: Environment) -> None: # noqa: PLR0915
|
|
|
175
176
|
env.add_filter("base64_decode", base64_decode)
|
|
176
177
|
env.add_filter("base64_url_safe_encode", base64_url_safe_encode)
|
|
177
178
|
env.add_filter("base64_url_safe_decode", base64_url_safe_decode)
|
|
179
|
+
env.add_filter("squish", squish)
|
|
178
180
|
|
|
179
181
|
env.add_filter("find", find)
|
|
180
182
|
env.add_filter("find_index", find_index)
|
liquid/builtin/filters/string.py
CHANGED
|
@@ -405,3 +405,12 @@ def base64_url_safe_decode(val: str) -> str:
|
|
|
405
405
|
return base64.urlsafe_b64decode(val).decode()
|
|
406
406
|
except binascii.Error as err:
|
|
407
407
|
raise FilterError("invalid base64-encoded string", token=None) from err
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
RE_WHITESPACE = re.compile(r"\s+")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@string_filter
|
|
414
|
+
def squish(val: str) -> str:
|
|
415
|
+
"""Strip whitespace and replace whitespace with a single space."""
|
|
416
|
+
return RE_WHITESPACE.sub(" ", val.strip())
|
|
@@ -21,6 +21,8 @@ class CachingFileSystemLoader(CachingLoaderMixin, FileSystemLoader):
|
|
|
21
21
|
search_path: One or more paths to search.
|
|
22
22
|
encoding: Open template files with the given encoding.
|
|
23
23
|
ext: A default file extension. Should include a leading period.
|
|
24
|
+
reject_symlinks: When `True`, reject paths to symlinks that resolve to files
|
|
25
|
+
outside the search path. Defaults to `False`.
|
|
24
26
|
auto_reload: If `True`, automatically reload a cached template if it has been
|
|
25
27
|
updated.
|
|
26
28
|
namespace_key: The name of a global render context variable or loader keyword
|
|
@@ -39,6 +41,7 @@ class CachingFileSystemLoader(CachingLoaderMixin, FileSystemLoader):
|
|
|
39
41
|
encoding: str = "utf-8",
|
|
40
42
|
ext: Optional[str] = None,
|
|
41
43
|
*,
|
|
44
|
+
reject_symlinks: bool = False,
|
|
42
45
|
auto_reload: bool = True,
|
|
43
46
|
namespace_key: str = "",
|
|
44
47
|
capacity: int = 300,
|
|
@@ -54,4 +57,5 @@ class CachingFileSystemLoader(CachingLoaderMixin, FileSystemLoader):
|
|
|
54
57
|
search_path=search_path,
|
|
55
58
|
encoding=encoding,
|
|
56
59
|
ext=ext,
|
|
60
|
+
reject_symlinks=reject_symlinks,
|
|
57
61
|
)
|
|
@@ -27,6 +27,8 @@ class FileSystemLoader(BaseLoader):
|
|
|
27
27
|
search_path: One or more paths to search.
|
|
28
28
|
encoding: Encoding to use when opening files.
|
|
29
29
|
ext: A default file extension. Should include a leading period.
|
|
30
|
+
reject_symlinks: When `True`, reject paths to symlinks that resolve to files
|
|
31
|
+
outside the search path. Defaults to `False`.
|
|
30
32
|
"""
|
|
31
33
|
|
|
32
34
|
def __init__(
|
|
@@ -35,6 +37,7 @@ class FileSystemLoader(BaseLoader):
|
|
|
35
37
|
*,
|
|
36
38
|
encoding: str = "utf-8",
|
|
37
39
|
ext: Optional[str] = None,
|
|
40
|
+
reject_symlinks: bool = False,
|
|
38
41
|
):
|
|
39
42
|
super().__init__()
|
|
40
43
|
if not isinstance(search_path, Iterable) or isinstance(search_path, str):
|
|
@@ -43,6 +46,7 @@ class FileSystemLoader(BaseLoader):
|
|
|
43
46
|
self.search_path = [Path(path) for path in search_path]
|
|
44
47
|
self.encoding = encoding
|
|
45
48
|
self.ext = ext
|
|
49
|
+
self.reject_symlinks = reject_symlinks
|
|
46
50
|
|
|
47
51
|
def resolve_path(self, template_name: str) -> Path:
|
|
48
52
|
"""Return a path to the template identified by _template_name_.
|
|
@@ -56,14 +60,27 @@ class FileSystemLoader(BaseLoader):
|
|
|
56
60
|
if self.ext and not template_path.suffix:
|
|
57
61
|
template_path = template_path.with_suffix(self.ext)
|
|
58
62
|
|
|
59
|
-
if os.path.pardir in template_path.parts:
|
|
63
|
+
if os.path.pardir in template_path.parts or template_path.is_absolute():
|
|
60
64
|
raise TemplateNotFoundError(template_name)
|
|
61
65
|
|
|
62
|
-
for
|
|
63
|
-
source_path =
|
|
64
|
-
|
|
66
|
+
for base in self.search_path:
|
|
67
|
+
source_path = base.joinpath(template_path)
|
|
68
|
+
|
|
69
|
+
if not source_path.exists() or not source_path.is_file():
|
|
65
70
|
continue
|
|
71
|
+
|
|
72
|
+
if self.reject_symlinks:
|
|
73
|
+
try:
|
|
74
|
+
resolved = source_path.resolve(strict=False)
|
|
75
|
+
base_resolved = base.resolve(strict=False)
|
|
76
|
+
except OSError:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if not resolved.is_relative_to(base_resolved):
|
|
80
|
+
continue
|
|
81
|
+
|
|
66
82
|
return source_path
|
|
83
|
+
|
|
67
84
|
raise TemplateNotFoundError(template_name)
|
|
68
85
|
|
|
69
86
|
def _read(self, source_path: Path) -> tuple[str, float]:
|
liquid/builtin/tags/case_tag.py
CHANGED
|
@@ -11,7 +11,7 @@ from typing import Union
|
|
|
11
11
|
from liquid.ast import BlockNode
|
|
12
12
|
from liquid.ast import Node
|
|
13
13
|
from liquid.builtin.expressions import parse_primitive
|
|
14
|
-
from liquid.builtin.expressions.logical import _eq
|
|
14
|
+
from liquid.builtin.expressions.logical import _eq # type: ignore
|
|
15
15
|
from liquid.exceptions import LiquidSyntaxError
|
|
16
16
|
from liquid.expression import Expression
|
|
17
17
|
from liquid.parser import get_parser
|
|
@@ -85,7 +85,7 @@ class CaseNode(Node):
|
|
|
85
85
|
count += count_
|
|
86
86
|
# Only render `else` blocks if all preceding `when` blocks are falsy.
|
|
87
87
|
# Multiple `else` blocks are OK.
|
|
88
|
-
elif
|
|
88
|
+
elif default:
|
|
89
89
|
count += block.render(context, buffer)
|
|
90
90
|
|
|
91
91
|
return count
|
|
@@ -106,7 +106,7 @@ class CaseNode(Node):
|
|
|
106
106
|
count += count_
|
|
107
107
|
# Only render `else` blocks if all preceding `when` blocks are falsy.
|
|
108
108
|
# Multiple `else` blocks are OK.
|
|
109
|
-
elif
|
|
109
|
+
elif default:
|
|
110
110
|
count += await block.render_async(context, buffer)
|
|
111
111
|
|
|
112
112
|
return count
|
|
@@ -37,7 +37,7 @@ class CommentNode(ast.Node):
|
|
|
37
37
|
def __str__(self) -> str:
|
|
38
38
|
return f"{{% comment %}}{self.text}{{% endcomment %}}"
|
|
39
39
|
|
|
40
|
-
def render_to_output(self,
|
|
40
|
+
def render_to_output(self, context: RenderContext, buffer: TextIO) -> int: # noqa: ARG002
|
|
41
41
|
"""Render the node to the output buffer."""
|
|
42
42
|
return 0
|
|
43
43
|
|
|
@@ -61,7 +61,7 @@ class CommentTag(Tag):
|
|
|
61
61
|
|
|
62
62
|
# A block `{% comment %}`
|
|
63
63
|
token = stream.eat(TOKEN_TAG)
|
|
64
|
-
text = []
|
|
64
|
+
text: list[str] = []
|
|
65
65
|
|
|
66
66
|
while True:
|
|
67
67
|
if stream.current.kind == TOKEN_EOF:
|
liquid/builtin/tags/doc_tag.py
CHANGED
|
@@ -50,7 +50,7 @@ class DocTag(Tag):
|
|
|
50
50
|
|
|
51
51
|
# This only happens if the doc tag is malformed
|
|
52
52
|
token = stream.eat(TOKEN_TAG)
|
|
53
|
-
text = []
|
|
53
|
+
text: list[str] = []
|
|
54
54
|
|
|
55
55
|
if stream.current.kind == TOKEN_EXPRESSION:
|
|
56
56
|
raise LiquidSyntaxError("unexpected expression", token=stream.current)
|
liquid/builtin/tags/if_tag.py
CHANGED
|
@@ -152,7 +152,7 @@ class IfTag(Tag):
|
|
|
152
152
|
condition = BooleanExpression.parse(self.env, tokens)
|
|
153
153
|
parse_block = get_parser(self.env).parse_block
|
|
154
154
|
consequence = parse_block(stream, ENDIFBLOCK)
|
|
155
|
-
alternatives = []
|
|
155
|
+
alternatives: list[ConditionalBlockNode] = []
|
|
156
156
|
|
|
157
157
|
while stream.current.is_tag(TAG_ELSIF):
|
|
158
158
|
# If the expression can't be parsed, eat the "elsif" block and
|
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
from typing import Iterable
|
|
8
8
|
from typing import Optional
|
|
9
9
|
from typing import TextIO
|
|
10
|
+
from typing import Union
|
|
10
11
|
|
|
11
12
|
from liquid.ast import Node
|
|
12
13
|
from liquid.ast import Partial
|
|
@@ -21,11 +22,12 @@ from liquid.builtin.expressions import parse_identifier
|
|
|
21
22
|
from liquid.builtin.expressions import parse_primitive
|
|
22
23
|
from liquid.builtin.tags.for_tag import ForLoop
|
|
23
24
|
from liquid.builtin.tags.include_tag import TAG_INCLUDE
|
|
25
|
+
from liquid.exceptions import LiquidSyntaxError
|
|
24
26
|
from liquid.exceptions import TemplateNotFoundError
|
|
25
27
|
from liquid.tag import Tag
|
|
28
|
+
from liquid.template import BoundTemplate
|
|
26
29
|
from liquid.token import TOKEN_AS
|
|
27
30
|
from liquid.token import TOKEN_FOR
|
|
28
|
-
from liquid.token import TOKEN_STRING
|
|
29
31
|
from liquid.token import TOKEN_TAG
|
|
30
32
|
from liquid.token import TOKEN_WITH
|
|
31
33
|
from liquid.token import TOKEN_WORD
|
|
@@ -50,7 +52,8 @@ class RenderNode(Node):
|
|
|
50
52
|
def __init__(
|
|
51
53
|
self,
|
|
52
54
|
token: Token,
|
|
53
|
-
name: StringLiteral,
|
|
55
|
+
name: Union[StringLiteral, Identifier],
|
|
56
|
+
*,
|
|
54
57
|
var: Optional[Expression] = None,
|
|
55
58
|
loop: bool = False,
|
|
56
59
|
alias: Optional[Identifier] = None,
|
|
@@ -77,14 +80,26 @@ class RenderNode(Node):
|
|
|
77
80
|
|
|
78
81
|
def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
|
|
79
82
|
"""Render the node to the output buffer."""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
if isinstance(self.name, Identifier):
|
|
84
|
+
# We're expecting an inline snippet.
|
|
85
|
+
template: Optional[BoundTemplate] = context.resolve(
|
|
86
|
+
self.name, token=self.token, default=None
|
|
83
87
|
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
if not isinstance(template, BoundTemplate):
|
|
89
|
+
raise TemplateNotFoundError(
|
|
90
|
+
self.name,
|
|
91
|
+
filename=context.template.full_name(),
|
|
92
|
+
token=self.name.token,
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
try:
|
|
96
|
+
template = context.env.get_template(
|
|
97
|
+
self.name.value, context=context, tag=self.tag
|
|
98
|
+
)
|
|
99
|
+
except TemplateNotFoundError as err:
|
|
100
|
+
err.token = self.name.token
|
|
101
|
+
err.template_name = context.template.full_name()
|
|
102
|
+
raise
|
|
88
103
|
|
|
89
104
|
# Evaluate keyword arguments once. Unlike 'include', 'render' can not
|
|
90
105
|
# mutate variables in the outer scope, so there's no need to re-evaluate
|
|
@@ -145,14 +160,26 @@ class RenderNode(Node):
|
|
|
145
160
|
self, context: RenderContext, buffer: TextIO
|
|
146
161
|
) -> int:
|
|
147
162
|
"""Render the node to the output buffer."""
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
if isinstance(self.name, Identifier):
|
|
164
|
+
# We're expecting an inline snippet.
|
|
165
|
+
template: Optional[BoundTemplate] = context.resolve(
|
|
166
|
+
self.name, token=self.token, default=None
|
|
151
167
|
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
if not isinstance(template, BoundTemplate):
|
|
169
|
+
raise TemplateNotFoundError(
|
|
170
|
+
self.name,
|
|
171
|
+
filename=context.template.full_name(),
|
|
172
|
+
token=self.name.token,
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
try:
|
|
176
|
+
template = await context.env.get_template_async(
|
|
177
|
+
self.name.value, context=context, tag=self.tag
|
|
178
|
+
)
|
|
179
|
+
except TemplateNotFoundError as err:
|
|
180
|
+
err.token = self.name.token
|
|
181
|
+
err.template_name = context.template.full_name()
|
|
182
|
+
raise
|
|
156
183
|
|
|
157
184
|
# Evaluate keyword arguments once. Unlike 'include', 'render' can not
|
|
158
185
|
# mutate variables in the outer scope, so there's no need to re-evaluate
|
|
@@ -215,7 +242,15 @@ class RenderNode(Node):
|
|
|
215
242
|
self, static_context: RenderContext, *, include_partials: bool = True
|
|
216
243
|
) -> Iterable[Node]:
|
|
217
244
|
"""Return this node's children."""
|
|
218
|
-
if
|
|
245
|
+
if isinstance(self.name, Identifier):
|
|
246
|
+
# We're expecting an inline snippet.
|
|
247
|
+
# Always visit inline snippets, even if include_partials is False.
|
|
248
|
+
template: Optional[BoundTemplate] = static_context.resolve(
|
|
249
|
+
self.name, token=self.token, default=None
|
|
250
|
+
)
|
|
251
|
+
if template:
|
|
252
|
+
yield from template.nodes
|
|
253
|
+
elif include_partials:
|
|
219
254
|
name = self.name.evaluate(static_context)
|
|
220
255
|
try:
|
|
221
256
|
template = static_context.env.get_template(
|
|
@@ -231,7 +266,14 @@ class RenderNode(Node):
|
|
|
231
266
|
self, static_context: RenderContext, *, include_partials: bool = True
|
|
232
267
|
) -> Iterable[Node]:
|
|
233
268
|
"""Return this node's children."""
|
|
234
|
-
if
|
|
269
|
+
if isinstance(self.name, Identifier):
|
|
270
|
+
# We're expecting an inline snippet.
|
|
271
|
+
template: Optional[BoundTemplate] = static_context.resolve(
|
|
272
|
+
self.name, token=self.token, default=None
|
|
273
|
+
)
|
|
274
|
+
if template:
|
|
275
|
+
return template.nodes
|
|
276
|
+
elif include_partials:
|
|
235
277
|
name = await self.name.evaluate_async(static_context)
|
|
236
278
|
try:
|
|
237
279
|
template = await static_context.env.get_template_async(
|
|
@@ -246,12 +288,11 @@ class RenderNode(Node):
|
|
|
246
288
|
|
|
247
289
|
def expressions(self) -> Iterable[Expression]:
|
|
248
290
|
"""Return this node's expressions."""
|
|
249
|
-
yield self.name
|
|
250
291
|
if self.var:
|
|
251
292
|
yield self.var
|
|
252
293
|
yield from (arg.value for arg in self.args)
|
|
253
294
|
|
|
254
|
-
def partial_scope(self) -> Partial
|
|
295
|
+
def partial_scope(self) -> Optional[Partial]:
|
|
255
296
|
"""Return information about a partial template loaded by this node."""
|
|
256
297
|
scope: list[Identifier] = [
|
|
257
298
|
Identifier(arg.name, token=arg.token) for arg in self.args
|
|
@@ -267,7 +308,17 @@ class RenderNode(Node):
|
|
|
267
308
|
)
|
|
268
309
|
)
|
|
269
310
|
|
|
270
|
-
|
|
311
|
+
partial_name = self.name.value if isinstance(self.name, StringLiteral) else ""
|
|
312
|
+
partial_key = hash((partial_name, *[arg.name for arg in self.args]))
|
|
313
|
+
|
|
314
|
+
# Static analysis will use the parent template name if Partial.name is
|
|
315
|
+
# empty. Which is what we want for inline snippets.
|
|
316
|
+
return Partial(
|
|
317
|
+
name=partial_name,
|
|
318
|
+
scope=PartialScope.ISOLATED,
|
|
319
|
+
in_scope=scope,
|
|
320
|
+
key=partial_key,
|
|
321
|
+
)
|
|
271
322
|
|
|
272
323
|
|
|
273
324
|
BIND_TAGS = frozenset((TOKEN_WITH, TOKEN_FOR))
|
|
@@ -284,12 +335,18 @@ class RenderTag(Tag):
|
|
|
284
335
|
"""Parse tokens from _stream_ into an AST node."""
|
|
285
336
|
token = stream.eat(TOKEN_TAG)
|
|
286
337
|
tokens = stream.into_inner(tag=token, eat=False)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
338
|
+
name: Union[Expression, Identifier] = parse_primitive(self.env, tokens)
|
|
339
|
+
|
|
340
|
+
if isinstance(name, Path):
|
|
341
|
+
head = name.head()
|
|
342
|
+
if len(name.path) != 1 or not isinstance(head, str):
|
|
343
|
+
raise LiquidSyntaxError(
|
|
344
|
+
"expected an identifier, found a path",
|
|
345
|
+
token=name.token,
|
|
346
|
+
)
|
|
347
|
+
name = Identifier(head, token=name.token)
|
|
348
|
+
elif not isinstance(name, StringLiteral):
|
|
349
|
+
raise LiquidSyntaxError("expected a string or identifier", token=name.token)
|
|
293
350
|
|
|
294
351
|
alias: Optional[Identifier] = None
|
|
295
352
|
var: Optional[Path] = None
|
|
@@ -311,4 +368,4 @@ class RenderTag(Tag):
|
|
|
311
368
|
|
|
312
369
|
# Zero or more keyword arguments
|
|
313
370
|
args = KeywordArgument.parse(self.env, tokens)
|
|
314
|
-
return self.node_class(token, name, var, loop, alias, args)
|
|
371
|
+
return self.node_class(token, name, var=var, loop=loop, alias=alias, args=args)
|
liquid/exceptions.py
CHANGED
|
@@ -169,9 +169,10 @@ class TemplateNotFoundError(LiquidError):
|
|
|
169
169
|
def __init__(
|
|
170
170
|
self,
|
|
171
171
|
*args: object,
|
|
172
|
-
filename:
|
|
172
|
+
filename: Optional[str] = None,
|
|
173
|
+
token: Optional[Token] = None,
|
|
173
174
|
):
|
|
174
|
-
super().__init__(*args, token=
|
|
175
|
+
super().__init__(*args, token=token, template_name=filename)
|
|
175
176
|
|
|
176
177
|
def __str__(self) -> str:
|
|
177
178
|
if not self.token:
|
liquid/extra/__init__.py
CHANGED
|
@@ -21,6 +21,7 @@ from .tags import BlockTag
|
|
|
21
21
|
from .tags import CallTag
|
|
22
22
|
from .tags import ExtendsTag
|
|
23
23
|
from .tags import MacroTag
|
|
24
|
+
from .tags import SnippetTag
|
|
24
25
|
from .tags import TranslateTag
|
|
25
26
|
from .tags import WithTag
|
|
26
27
|
|
|
@@ -38,7 +39,6 @@ __all__ = (
|
|
|
38
39
|
"DateTime",
|
|
39
40
|
"ExtendsTag",
|
|
40
41
|
"GetText",
|
|
41
|
-
"IfNotTag",
|
|
42
42
|
"index",
|
|
43
43
|
"JSON",
|
|
44
44
|
"MacroTag",
|
|
@@ -49,6 +49,7 @@ __all__ = (
|
|
|
49
49
|
"script_tag",
|
|
50
50
|
"sort_numeric",
|
|
51
51
|
"stylesheet_tag",
|
|
52
|
+
"SnippetTag",
|
|
52
53
|
"Translate",
|
|
53
54
|
"Unit",
|
|
54
55
|
"WithTag",
|
liquid/extra/filters/babel.py
CHANGED
|
@@ -66,7 +66,7 @@ class Currency:
|
|
|
66
66
|
can not be resolved. Defaults to `"USD"`.
|
|
67
67
|
locale_var: The name of a render context variable that resolves to the
|
|
68
68
|
current locale. Defaults to `"locale"`.
|
|
69
|
-
default_locale
|
|
69
|
+
default_locale: A fallback locale to use if `locale_var` can not be
|
|
70
70
|
resolved. Defaults to `"en_US"`.
|
|
71
71
|
format_var: The name of a render context variable that resolves to the
|
|
72
72
|
current currency format string. Defaults to `"currency_format"`.
|
liquid/extra/tags/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@ from .extends_tag import BlockTag
|
|
|
3
3
|
from .extends_tag import ExtendsTag
|
|
4
4
|
from .macro_tag import CallTag
|
|
5
5
|
from .macro_tag import MacroTag
|
|
6
|
+
from .snippet_tag import SnippetTag
|
|
6
7
|
from .translate_tag import TranslateTag
|
|
7
8
|
|
|
8
9
|
__all__ = (
|
|
@@ -11,5 +12,6 @@ __all__ = (
|
|
|
11
12
|
"ExtendsTag",
|
|
12
13
|
"CallTag",
|
|
13
14
|
"MacroTag",
|
|
15
|
+
"SnippetTag",
|
|
14
16
|
"TranslateTag",
|
|
15
17
|
)
|
liquid/extra/tags/extends_tag.py
CHANGED
|
@@ -19,7 +19,6 @@ from liquid.ast import Node
|
|
|
19
19
|
from liquid.ast import Partial
|
|
20
20
|
from liquid.ast import PartialScope
|
|
21
21
|
from liquid.builtin.expressions import Identifier
|
|
22
|
-
from liquid.builtin.expressions import StringLiteral
|
|
23
22
|
from liquid.builtin.expressions import parse_name
|
|
24
23
|
from liquid.exceptions import RequiredBlockError
|
|
25
24
|
from liquid.exceptions import StopRender
|
|
@@ -107,7 +106,7 @@ class ExtendsNode(Node):
|
|
|
107
106
|
def partial_scope(self) -> Optional[Partial]:
|
|
108
107
|
"""Return information about a partial template loaded by this node."""
|
|
109
108
|
return Partial(
|
|
110
|
-
name=
|
|
109
|
+
name=self.name,
|
|
111
110
|
scope=PartialScope.INHERITED,
|
|
112
111
|
in_scope=[],
|
|
113
112
|
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""The built-in _snippet_ tag.
|
|
2
|
+
|
|
3
|
+
New in Python Liquid version 2.2.0.
|
|
4
|
+
New in Shopify/liquid version 5.10.0.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
from typing import TextIO
|
|
11
|
+
|
|
12
|
+
from liquid.ast import BlockNode
|
|
13
|
+
from liquid.ast import Node
|
|
14
|
+
from liquid.builtin.expressions import parse_identifier
|
|
15
|
+
from liquid.parser import get_parser
|
|
16
|
+
from liquid.tag import Tag
|
|
17
|
+
from liquid.template import BoundTemplate
|
|
18
|
+
from liquid.token import TOKEN_EOF
|
|
19
|
+
from liquid.token import TOKEN_TAG
|
|
20
|
+
from liquid.token import Token
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Iterable
|
|
24
|
+
|
|
25
|
+
from liquid.builtin.expressions import Identifier
|
|
26
|
+
from liquid.context import RenderContext
|
|
27
|
+
from liquid.stream import TokenStream
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SnippetNode(Node):
|
|
31
|
+
"""The built-in _snippet_ tag."""
|
|
32
|
+
|
|
33
|
+
__slots__ = ("name", "block")
|
|
34
|
+
|
|
35
|
+
def __init__(self, token: Token, name: Identifier, block: BlockNode):
|
|
36
|
+
super().__init__(token)
|
|
37
|
+
self.name = name
|
|
38
|
+
self.block = block
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return f"{{% snippet {self.name} %}}{self.block}{{% endsnippet %}}"
|
|
42
|
+
|
|
43
|
+
def render_to_output(self, context: RenderContext, buffer: TextIO) -> int: # noqa: ARG002
|
|
44
|
+
"""Render the node to the output buffer."""
|
|
45
|
+
# Don't render anything, just bind the block to its name.
|
|
46
|
+
context.assign(
|
|
47
|
+
self.name,
|
|
48
|
+
SnippetDrop(
|
|
49
|
+
context.env,
|
|
50
|
+
self.block.nodes,
|
|
51
|
+
context.template.name,
|
|
52
|
+
context.template.path,
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
def children(
|
|
58
|
+
self,
|
|
59
|
+
static_context: RenderContext, # noqa: ARG002
|
|
60
|
+
*,
|
|
61
|
+
include_partials: bool = True, # noqa: ARG002
|
|
62
|
+
) -> Iterable[Node]:
|
|
63
|
+
"""Return this node's children."""
|
|
64
|
+
# Snippets are only visited when rendered so we get accurate variable
|
|
65
|
+
# scope analysis.
|
|
66
|
+
static_context.assign(
|
|
67
|
+
self.name,
|
|
68
|
+
SnippetDrop(
|
|
69
|
+
static_context.env,
|
|
70
|
+
self.block.nodes,
|
|
71
|
+
static_context.template.name,
|
|
72
|
+
static_context.template.path,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
def template_scope(self) -> Iterable[Identifier]:
|
|
78
|
+
"""Return variables this node adds to the template local scope."""
|
|
79
|
+
yield self.name
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SnippetTag(Tag):
|
|
83
|
+
"""The built-in _snippet_ tag."""
|
|
84
|
+
|
|
85
|
+
block = True
|
|
86
|
+
name = "snippet"
|
|
87
|
+
end = "endsnippet"
|
|
88
|
+
node_class = SnippetNode
|
|
89
|
+
|
|
90
|
+
ENDSNIPPETBLOCK = frozenset((end, TOKEN_EOF))
|
|
91
|
+
|
|
92
|
+
def parse(self, stream: TokenStream) -> SnippetNode:
|
|
93
|
+
"""Parse tokens from _stream_ into an AST node."""
|
|
94
|
+
token = stream.eat(TOKEN_TAG)
|
|
95
|
+
tokens = stream.into_inner(tag=token)
|
|
96
|
+
name = parse_identifier(self.env, tokens)
|
|
97
|
+
tokens.expect_eos()
|
|
98
|
+
block = get_parser(self.env).parse_block(stream, self.ENDSNIPPETBLOCK)
|
|
99
|
+
stream.expect(TOKEN_TAG, value=self.end)
|
|
100
|
+
return self.node_class(token, name, block)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SnippetDrop(BoundTemplate):
|
|
104
|
+
"""An template suitable for storing in a render context."""
|
|
105
|
+
|
|
106
|
+
def __str__(self) -> str:
|
|
107
|
+
return "SnippetDrop"
|
liquid/filter.py
CHANGED
|
@@ -54,8 +54,11 @@ def string_filter(_filter: FilterT) -> FilterT:
|
|
|
54
54
|
|
|
55
55
|
@wraps(_filter)
|
|
56
56
|
def wrapper(val: object, *args: Any, **kwargs: Any) -> Any:
|
|
57
|
-
if
|
|
57
|
+
if val is None:
|
|
58
|
+
val = ""
|
|
59
|
+
elif not isinstance(val, str):
|
|
58
60
|
val = str(val)
|
|
61
|
+
|
|
59
62
|
try:
|
|
60
63
|
return _filter(val, *args, **kwargs)
|
|
61
64
|
except TypeError as err:
|
liquid/static_analysis.py
CHANGED
|
@@ -8,6 +8,7 @@ from dataclasses import dataclass
|
|
|
8
8
|
from dataclasses import field
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
from typing import Iterable
|
|
11
|
+
from typing import Optional
|
|
11
12
|
from typing import Union
|
|
12
13
|
|
|
13
14
|
from .ast import BlockNode
|
|
@@ -126,7 +127,11 @@ class TemplateAnalysis:
|
|
|
126
127
|
tags: dict[str, list[Span]]
|
|
127
128
|
|
|
128
129
|
|
|
129
|
-
def
|
|
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
|
+
"""
|
|
130
135
|
variables = _VariableMap()
|
|
131
136
|
globals = _VariableMap() # noqa: A001
|
|
132
137
|
locals = _VariableMap() # noqa: A001
|
|
@@ -139,15 +144,26 @@ def _analyze(template: BoundTemplate, *, include_partials: bool) -> TemplateAnal
|
|
|
139
144
|
static_context = RenderContext(template)
|
|
140
145
|
|
|
141
146
|
# Names of partial templates that have already been analyzed.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
|
147
162
|
|
|
148
163
|
# Update tags from node.token
|
|
149
164
|
if (
|
|
150
|
-
not
|
|
165
|
+
not just_globals
|
|
166
|
+
and not isinstance(
|
|
151
167
|
node, (BlockNode, ConditionalBlockNode, MultiExpressionBlockNode)
|
|
152
168
|
)
|
|
153
169
|
and node.token.kind == TOKEN_TAG
|
|
@@ -156,11 +172,18 @@ def _analyze(template: BoundTemplate, *, include_partials: bool) -> TemplateAnal
|
|
|
156
172
|
|
|
157
173
|
# Update variables from node.expressions()
|
|
158
174
|
for expr in node.expressions():
|
|
159
|
-
_analyze_variables(
|
|
175
|
+
_analyze_variables(
|
|
176
|
+
expr,
|
|
177
|
+
template_name,
|
|
178
|
+
scope,
|
|
179
|
+
globals,
|
|
180
|
+
_VariableMap() if just_globals else variables,
|
|
181
|
+
)
|
|
160
182
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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)
|
|
164
187
|
|
|
165
188
|
# Update the template scope from node.template_scope()
|
|
166
189
|
for ident in node.template_scope():
|
|
@@ -173,11 +196,23 @@ def _analyze(template: BoundTemplate, *, include_partials: bool) -> TemplateAnal
|
|
|
173
196
|
)
|
|
174
197
|
|
|
175
198
|
if partial := node.partial_scope():
|
|
176
|
-
partial_name =
|
|
199
|
+
partial_name = (
|
|
200
|
+
partial.name
|
|
201
|
+
if isinstance(partial.name, str)
|
|
202
|
+
else str(partial.name.evaluate(static_context))
|
|
203
|
+
)
|
|
177
204
|
|
|
178
|
-
|
|
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.
|
|
179
211
|
return
|
|
180
212
|
|
|
213
|
+
seen[partial_name].add(partial.key)
|
|
214
|
+
partial_name = partial_name or template_name
|
|
215
|
+
|
|
181
216
|
partial_scope = (
|
|
182
217
|
_StaticScope(set(partial.in_scope))
|
|
183
218
|
if partial.scope == PartialScope.ISOLATED
|
|
@@ -187,8 +222,12 @@ def _analyze(template: BoundTemplate, *, include_partials: bool) -> TemplateAnal
|
|
|
187
222
|
for child in node.children(
|
|
188
223
|
static_context, include_partials=include_partials
|
|
189
224
|
):
|
|
190
|
-
|
|
191
|
-
|
|
225
|
+
_visit(
|
|
226
|
+
child,
|
|
227
|
+
partial_name,
|
|
228
|
+
partial_scope,
|
|
229
|
+
just_globals=just_globals or _just_globals,
|
|
230
|
+
)
|
|
192
231
|
|
|
193
232
|
partial_scope.pop()
|
|
194
233
|
else:
|
|
@@ -196,7 +235,7 @@ def _analyze(template: BoundTemplate, *, include_partials: bool) -> TemplateAnal
|
|
|
196
235
|
for child in node.children(
|
|
197
236
|
static_context, include_partials=include_partials
|
|
198
237
|
):
|
|
199
|
-
_visit(child, template_name, scope)
|
|
238
|
+
_visit(child, template_name, scope, just_globals=just_globals)
|
|
200
239
|
scope.pop()
|
|
201
240
|
|
|
202
241
|
for node in template.nodes:
|
|
@@ -211,9 +250,13 @@ def _analyze(template: BoundTemplate, *, include_partials: bool) -> TemplateAnal
|
|
|
211
250
|
)
|
|
212
251
|
|
|
213
252
|
|
|
214
|
-
async def
|
|
253
|
+
async def analyze_async(
|
|
215
254
|
template: BoundTemplate, *, include_partials: bool
|
|
216
255
|
) -> TemplateAnalysis:
|
|
256
|
+
"""Report variable, filters and tags used in the given template.
|
|
257
|
+
|
|
258
|
+
This is used by `BoundTemplate.analyze_async`.
|
|
259
|
+
"""
|
|
217
260
|
variables = _VariableMap()
|
|
218
261
|
globals = _VariableMap() # noqa: A001
|
|
219
262
|
locals = _VariableMap() # noqa: A001
|
|
@@ -226,15 +269,22 @@ async def _analyze_async(
|
|
|
226
269
|
static_context = RenderContext(template)
|
|
227
270
|
|
|
228
271
|
# Names of partial templates that have already been analyzed.
|
|
229
|
-
seen: set[
|
|
230
|
-
|
|
231
|
-
async def _visit(
|
|
232
|
-
|
|
233
|
-
|
|
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)
|
|
234
283
|
|
|
235
284
|
# Update tags from node.token
|
|
236
285
|
if (
|
|
237
|
-
not
|
|
286
|
+
not just_globals
|
|
287
|
+
and not isinstance(
|
|
238
288
|
node, (BlockNode, ConditionalBlockNode, MultiExpressionBlockNode)
|
|
239
289
|
)
|
|
240
290
|
and node.token.kind == TOKEN_TAG
|
|
@@ -243,11 +293,18 @@ async def _analyze_async(
|
|
|
243
293
|
|
|
244
294
|
# Update variables from node.expressions()
|
|
245
295
|
for expr in node.expressions():
|
|
246
|
-
_analyze_variables(
|
|
296
|
+
_analyze_variables(
|
|
297
|
+
expr,
|
|
298
|
+
template_name,
|
|
299
|
+
scope,
|
|
300
|
+
globals,
|
|
301
|
+
_VariableMap() if just_globals else variables,
|
|
302
|
+
)
|
|
247
303
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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)
|
|
251
308
|
|
|
252
309
|
# Update the template scope from node.template_scope()
|
|
253
310
|
for ident in node.template_scope():
|
|
@@ -260,11 +317,23 @@ async def _analyze_async(
|
|
|
260
317
|
)
|
|
261
318
|
|
|
262
319
|
if partial := node.partial_scope():
|
|
263
|
-
partial_name =
|
|
320
|
+
partial_name = (
|
|
321
|
+
partial.name
|
|
322
|
+
if isinstance(partial.name, str)
|
|
323
|
+
else str(partial.name.evaluate(static_context))
|
|
324
|
+
)
|
|
264
325
|
|
|
265
|
-
|
|
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.
|
|
266
332
|
return
|
|
267
333
|
|
|
334
|
+
seen[partial_name].add(partial.key)
|
|
335
|
+
partial_name = partial_name or template_name
|
|
336
|
+
|
|
268
337
|
partial_scope = (
|
|
269
338
|
_StaticScope(set(partial.in_scope))
|
|
270
339
|
if partial.scope == PartialScope.ISOLATED
|
|
@@ -274,8 +343,12 @@ async def _analyze_async(
|
|
|
274
343
|
for child in await node.children_async(
|
|
275
344
|
static_context, include_partials=include_partials
|
|
276
345
|
):
|
|
277
|
-
|
|
278
|
-
|
|
346
|
+
await _visit(
|
|
347
|
+
child,
|
|
348
|
+
partial_name,
|
|
349
|
+
partial_scope,
|
|
350
|
+
just_globals=just_globals or _just_globals,
|
|
351
|
+
)
|
|
279
352
|
|
|
280
353
|
partial_scope.pop()
|
|
281
354
|
else:
|
|
@@ -283,7 +356,7 @@ async def _analyze_async(
|
|
|
283
356
|
for child in await node.children_async(
|
|
284
357
|
static_context, include_partials=include_partials
|
|
285
358
|
):
|
|
286
|
-
await _visit(child, template_name, scope)
|
|
359
|
+
await _visit(child, template_name, scope, just_globals=just_globals)
|
|
287
360
|
scope.pop()
|
|
288
361
|
|
|
289
362
|
for node in template.nodes:
|
liquid/template.py
CHANGED
|
@@ -13,8 +13,11 @@ from typing import Mapping
|
|
|
13
13
|
from typing import Optional
|
|
14
14
|
from typing import TextIO
|
|
15
15
|
from typing import Type
|
|
16
|
+
from typing import TypedDict
|
|
16
17
|
from typing import Union
|
|
17
18
|
|
|
19
|
+
from .builtin.tags.comment_tag import CommentNode
|
|
20
|
+
from .builtin.tags.doc_tag import DocNode
|
|
18
21
|
from .context import FutureContext
|
|
19
22
|
from .context import RenderContext
|
|
20
23
|
from .exceptions import LiquidError
|
|
@@ -22,8 +25,8 @@ from .exceptions import LiquidInterrupt
|
|
|
22
25
|
from .exceptions import LiquidSyntaxError
|
|
23
26
|
from .exceptions import StopRender
|
|
24
27
|
from .output import LimitedStringIO
|
|
25
|
-
from .static_analysis import
|
|
26
|
-
from .static_analysis import
|
|
28
|
+
from .static_analysis import analyze
|
|
29
|
+
from .static_analysis import analyze_async
|
|
27
30
|
from .utils import ReadOnlyChainMap
|
|
28
31
|
|
|
29
32
|
if TYPE_CHECKING:
|
|
@@ -262,11 +265,11 @@ class BoundTemplate:
|
|
|
262
265
|
include_partials: If `True`, we will try to load partial templates and
|
|
263
266
|
analyze those templates too.
|
|
264
267
|
"""
|
|
265
|
-
return
|
|
268
|
+
return analyze(self, include_partials=include_partials)
|
|
266
269
|
|
|
267
270
|
async def analyze_async(self, *, include_partials: bool = True) -> TemplateAnalysis:
|
|
268
271
|
"""An async version of `analyze`."""
|
|
269
|
-
return await
|
|
272
|
+
return await analyze_async(self, include_partials=include_partials)
|
|
270
273
|
|
|
271
274
|
def variables(self, *, include_partials: bool = True) -> list[str]:
|
|
272
275
|
"""Return a list of variables used in this template without path segments.
|
|
@@ -520,6 +523,52 @@ class BoundTemplate:
|
|
|
520
523
|
"""Return a list of tag names used in this template."""
|
|
521
524
|
return list((await self.analyze_async(include_partials=include_partials)).tags)
|
|
522
525
|
|
|
526
|
+
def comments(self) -> list[CommentNode]:
|
|
527
|
+
"""Return a list of comment tag nodes found in this template.
|
|
528
|
+
|
|
529
|
+
Instances of `CommentNode` and `InlineCommentNode` have `token` and
|
|
530
|
+
`text` properties. Use `[node.text for node in template.comments()]` to
|
|
531
|
+
get a list of comment strings.
|
|
532
|
+
|
|
533
|
+
Note that this method does not try to load included or rendered
|
|
534
|
+
templates.
|
|
535
|
+
"""
|
|
536
|
+
context = RenderContext(self)
|
|
537
|
+
nodes: list[CommentNode] = []
|
|
538
|
+
|
|
539
|
+
def visit(node: Node) -> None:
|
|
540
|
+
if isinstance(node, CommentNode):
|
|
541
|
+
nodes.append(node)
|
|
542
|
+
|
|
543
|
+
for child in node.children(context, include_partials=False):
|
|
544
|
+
visit(child)
|
|
545
|
+
|
|
546
|
+
for child in self.nodes:
|
|
547
|
+
visit(child)
|
|
548
|
+
|
|
549
|
+
return nodes
|
|
550
|
+
|
|
551
|
+
def docs(self) -> list[DocNode]:
|
|
552
|
+
"""Return a list of doc tag nodes found in this template.
|
|
553
|
+
|
|
554
|
+
Instances of `DocNode` have `token` and `text` properties. Use
|
|
555
|
+
`[node.text for node in template.docs()]` to get a list of doc strings.
|
|
556
|
+
"""
|
|
557
|
+
context = RenderContext(self)
|
|
558
|
+
nodes: list[DocNode] = []
|
|
559
|
+
|
|
560
|
+
def visit(node: Node) -> None:
|
|
561
|
+
if isinstance(node, DocNode):
|
|
562
|
+
nodes.append(node)
|
|
563
|
+
|
|
564
|
+
for child in node.children(context, include_partials=False):
|
|
565
|
+
visit(child)
|
|
566
|
+
|
|
567
|
+
for child in self.nodes:
|
|
568
|
+
visit(child)
|
|
569
|
+
|
|
570
|
+
return nodes
|
|
571
|
+
|
|
523
572
|
|
|
524
573
|
class AwareBoundTemplate(BoundTemplate):
|
|
525
574
|
"""A `BoundTemplate` subclass with a `TemplateDrop` in the global namespace."""
|
|
@@ -559,6 +608,12 @@ class FutureAwareBoundTemplate(AwareBoundTemplate):
|
|
|
559
608
|
context_class = FutureContext
|
|
560
609
|
|
|
561
610
|
|
|
611
|
+
class _TemplateDropItems(TypedDict):
|
|
612
|
+
directory: str
|
|
613
|
+
name: str
|
|
614
|
+
suffix: str | None
|
|
615
|
+
|
|
616
|
+
|
|
562
617
|
class TemplateDrop(Mapping[str, Optional[str]]):
|
|
563
618
|
"""Template meta data mapping."""
|
|
564
619
|
|
|
@@ -575,7 +630,7 @@ class TemplateDrop(Mapping[str, Optional[str]]):
|
|
|
575
630
|
if "." in self.stem:
|
|
576
631
|
self.suffix = self.stem.split(".")[-1]
|
|
577
632
|
|
|
578
|
-
self._items = {
|
|
633
|
+
self._items: _TemplateDropItems = {
|
|
579
634
|
"directory": self.path.parent.name,
|
|
580
635
|
"name": self.path.name.split(".")[0],
|
|
581
636
|
"suffix": self.suffix,
|
|
@@ -594,7 +649,7 @@ class TemplateDrop(Mapping[str, Optional[str]]):
|
|
|
594
649
|
return item in self._items
|
|
595
650
|
|
|
596
651
|
def __getitem__(self, key: object) -> Optional[str]:
|
|
597
|
-
return self._items[str(key)]
|
|
652
|
+
return self._items[str(key)] # type: ignore
|
|
598
653
|
|
|
599
654
|
def __len__(self) -> int:
|
|
600
655
|
return len(self._items)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-liquid
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A Python engine for the Liquid template language.
|
|
5
5
|
Project-URL: Change Log, https://github.com/jg-rp/liquid/blob/main/CHANGES.md
|
|
6
6
|
Project-URL: Documentation, https://jg-rp.github.io/liquid/
|
|
@@ -19,9 +19,10 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
23
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
23
24
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
24
|
-
Requires-Python: >=3.
|
|
25
|
+
Requires-Python: >=3.9
|
|
25
26
|
Requires-Dist: babel>=2
|
|
26
27
|
Requires-Dist: markupsafe>=3
|
|
27
28
|
Requires-Dist: python-dateutil>=2.8.1
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
liquid/__init__.py,sha256=
|
|
1
|
+
liquid/__init__.py,sha256=xwTMWymqgCgXPk0wTZS6rKvS6GcM516JAPWOxwE53GY,7696
|
|
2
2
|
liquid/analyze_tags.py,sha256=Uc1nueKLRiIejs2JyQIC7u2pxp9l-5HJAMWlg-Qj1m8,7615
|
|
3
|
-
liquid/ast.py,sha256=
|
|
3
|
+
liquid/ast.py,sha256=4ZqSbuW4mv9IvoIMPk3OICUaOI1vPh8P8uFCrXI_3XA,8140
|
|
4
4
|
liquid/context.py,sha256=gSJI1mj7mdcUfiQ09_XCEj5JxAosCGEX85Uuzl9apeI,21505
|
|
5
5
|
liquid/environment.py,sha256=1X5WsOHcFvKAjljQcG3F-cQ9sfxzaiD-N3jGJOG5yHU,23940
|
|
6
|
-
liquid/exceptions.py,sha256=
|
|
6
|
+
liquid/exceptions.py,sha256=FOc4KAOijF4WHrBGepvi0ycAYT5hu0rhdpsVb2C-ZHs,8336
|
|
7
7
|
liquid/expression.py,sha256=Ozgajah2R5eg1yWA4ITFGaMFoOLt8GIjkE1NKqbBEzk,1143
|
|
8
|
-
liquid/filter.py,sha256
|
|
8
|
+
liquid/filter.py,sha256=3NtqGk6nHNwe6ux3X6ZIzzcEMJNA_SaMrBmMQA8hf-I,6735
|
|
9
9
|
liquid/lex.py,sha256=PrnnUWhXZO6novkO0KE-p4virh5cNJsg7m85sg2IUiw,8004
|
|
10
10
|
liquid/limits.py,sha256=BxqiC4Ax5iadmh6jDRcUNjFfOvbMkvZ1a046R1Cb8UA,1700
|
|
11
11
|
liquid/loader.py,sha256=R_RniSRszCXmRVZgXlu9WjV130cWvspVcC3vIVpf-qg,4381
|
|
@@ -15,14 +15,14 @@ liquid/output.py,sha256=1rgPmXGWUTut-aBBbHs-TCCZ2UGUhmbJYRmCsnqIOCk,1000
|
|
|
15
15
|
liquid/parser.py,sha256=F8aC__UiUzz39lxhPr3sl67l-V-mrxkdn-4MAaCTWRE,3824
|
|
16
16
|
liquid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
liquid/span.py,sha256=Z7Xb5V_h3WLvVmZUBebZeZkJFHKa33Bfcqq3S60SMSk,1122
|
|
18
|
-
liquid/static_analysis.py,sha256=
|
|
18
|
+
liquid/static_analysis.py,sha256=iPUqxiufXyK5EoPZQ9WNRKAzkqBYiX1HaSfX8oZhL9c,14537
|
|
19
19
|
liquid/stream.py,sha256=nYBSjdgs5SkjAM7TTTkmZtc_DF1_KOvQrTGsjZXSrYE,4882
|
|
20
20
|
liquid/stringify.py,sha256=TkRO0KbZVCY3usNV6EXfG6CbtWhuLoI7joxcrEArSNE,892
|
|
21
21
|
liquid/tag.py,sha256=h5jexl6NjUQnM2py4s5db1ksh0s8vi31YHud5kHJt1E,1189
|
|
22
|
-
liquid/template.py,sha256=
|
|
22
|
+
liquid/template.py,sha256=ne1abyNX8Tytl-d-vAROxtn-hHt_-O4nrn0eb2b_5fU,24062
|
|
23
23
|
liquid/token.py,sha256=gxqMGcGk1fWihz5gQ2BQJzqSEcY7tCAZiIjcdC34P1w,3995
|
|
24
24
|
liquid/undefined.py,sha256=Y57D69YR6yXSHV4x1rg1xtoTe-6M49h-mqlSO7pcRSE,5673
|
|
25
|
-
liquid/builtin/__init__.py,sha256=
|
|
25
|
+
liquid/builtin/__init__.py,sha256=YNQlg5dzSBnU4asR_1yX111l6Cl1Rj_JqwEnOdT7l5U,7090
|
|
26
26
|
liquid/builtin/content.py,sha256=HXo2ty2cQxsOkXJDhWmcAWC0ny73w9oABaer50zGp70,1122
|
|
27
27
|
liquid/builtin/illegal.py,sha256=RRzzlqo2_scdtbMt_hZq0pr-7khgbxrdub9XH-_goPs,844
|
|
28
28
|
liquid/builtin/output.py,sha256=4r_QJimFTbBdubhdIOG_nweROEOXiWHlp_xNmWXy564,2279
|
|
@@ -41,44 +41,45 @@ liquid/builtin/filters/array.py,sha256=dMuT15EASXU4ejmNBmawdi92u0f7y_JhqVaGoP48z
|
|
|
41
41
|
liquid/builtin/filters/extra.py,sha256=m13_1nbZyRBl3CZV9WWGLlUZueRPt8cKPEHRAioqM_o,2710
|
|
42
42
|
liquid/builtin/filters/math.py,sha256=A_K2wg5gxm5Ncj9zMGDX_Lt7jjx4bRr3CywbCiCtqxo,3925
|
|
43
43
|
liquid/builtin/filters/misc.py,sha256=t2ZsaDLKRc1-8XxZaiEOh7oAMqFJ4N5-tzfbpLHbmIQ,3466
|
|
44
|
-
liquid/builtin/filters/string.py,sha256=
|
|
44
|
+
liquid/builtin/filters/string.py,sha256=mWG-14HttAqyPA1atLmwdXjbxwDpAowookbadk_jijI,12164
|
|
45
45
|
liquid/builtin/loaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
-
liquid/builtin/loaders/caching_file_system_loader.py,sha256
|
|
46
|
+
liquid/builtin/loaders/caching_file_system_loader.py,sha256=V4OLa6UGT-RELGiV4eu02TeLL4P_JIE2kMYMrO3qy8U,2158
|
|
47
47
|
liquid/builtin/loaders/choice_loader.py,sha256=FE23RLMOiWmsRGZv9t6QmlG7bgMEUSblnucAv3j0SBM,2816
|
|
48
48
|
liquid/builtin/loaders/dict_loader.py,sha256=8TdMJqaYPLwllmIiEmuZNXqH6UOOsi4HuJ2t5sztOOw,1785
|
|
49
|
-
liquid/builtin/loaders/file_system_loader.py,sha256=
|
|
49
|
+
liquid/builtin/loaders/file_system_loader.py,sha256=6CzmJnktLE9n6y8YYEtUZFa4nDsZ6vsAWIDwCg6_vE0,4528
|
|
50
50
|
liquid/builtin/loaders/mixins.py,sha256=4Hj5wWGMOi3LqA9ZJYsUb4LFbG0a5DDb9H975G8nn58,5209
|
|
51
51
|
liquid/builtin/loaders/package_loader.py,sha256=XP7LT3aMClgfBlI8OMyHOERlXTXEWmqFH7HskCnY3cs,3659
|
|
52
52
|
liquid/builtin/tags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
53
|
liquid/builtin/tags/assign_tag.py,sha256=VTfkPbF-i0KLqMmaacBItFQHaBTP7ECWR-RvVh9Q6lY,2374
|
|
54
54
|
liquid/builtin/tags/capture_tag.py,sha256=7bOG7qVwcqRx1eStb96AZKQ9tKLBjmzaYAe27LJTIm4,3037
|
|
55
|
-
liquid/builtin/tags/case_tag.py,sha256=
|
|
56
|
-
liquid/builtin/tags/comment_tag.py,sha256=
|
|
55
|
+
liquid/builtin/tags/case_tag.py,sha256=AzY8nEq_JQvtljZxVzT5GCcabX-1N4IrVAor2sy0fQw,9023
|
|
56
|
+
liquid/builtin/tags/comment_tag.py,sha256=BMvee7nVBim_iDScKWbcUBCxNXzbBvguYfQUG3GM1y8,2250
|
|
57
57
|
liquid/builtin/tags/cycle_tag.py,sha256=tbgnEiwENXIfRAKRsVg18KRsZwaNLOLZeuLy-0Xm5c0,3789
|
|
58
58
|
liquid/builtin/tags/decrement_tag.py,sha256=WgkwWqjJgzjfEKffTKNw0ao6N0JxniadxGO_n1k8oZE,1660
|
|
59
|
-
liquid/builtin/tags/doc_tag.py,sha256=
|
|
59
|
+
liquid/builtin/tags/doc_tag.py,sha256=luc2QI-4N7KF49vWak6dNm5Hs9jwlXTAfUlEsr6ZCNM,2055
|
|
60
60
|
liquid/builtin/tags/echo_tag.py,sha256=VvYth9VuA-A9bsfGUOLlM4QxL_frsgWena3yBrrLKPI,1174
|
|
61
61
|
liquid/builtin/tags/for_tag.py,sha256=vVSc9WJ0ZeLW8zgDwNs_dVr8qe9lrg92Ny90GmpO_Jo,9440
|
|
62
|
-
liquid/builtin/tags/if_tag.py,sha256=
|
|
62
|
+
liquid/builtin/tags/if_tag.py,sha256=4hGDjQo33PXxsXui6pZ75Wx2BkEtt5SOiZoK3Yb2pL8,6588
|
|
63
63
|
liquid/builtin/tags/ifchanged_tag.py,sha256=VpJNDhT7MFXcNmRrYiWcJ9whhaRXh81oe_phatS5_Kg,2734
|
|
64
64
|
liquid/builtin/tags/include_tag.py,sha256=eAnjcbJ2-e0382TkrV2WJ9dACFWi_UNUeKG-QRuiOig,9220
|
|
65
65
|
liquid/builtin/tags/increment_tag.py,sha256=SV2nrbE90l4iz36WCROqGE0RkkzztWUpU1_Pyp2DDQo,1662
|
|
66
66
|
liquid/builtin/tags/inline_comment_tag.py,sha256=hLLtvaI-VpJxzFeoLTwDTj46g13A1ke6blqVo2MP0Cs,1357
|
|
67
67
|
liquid/builtin/tags/liquid_tag.py,sha256=vQh2T8_3uBfTTgp6PNqi_bOhbRNv45eoo9yoOXcNyz8,5465
|
|
68
|
-
liquid/builtin/tags/render_tag.py,sha256=
|
|
68
|
+
liquid/builtin/tags/render_tag.py,sha256=iYWjKYQBmGHQVQ9OZgPZvVhraygFdmPzITin7cKJI5E,14309
|
|
69
69
|
liquid/builtin/tags/tablerow_tag.py,sha256=qE5hIrlJXdKiLJ0u1YLgjnqt6GedWujfcfIO0b9x0d8,8947
|
|
70
70
|
liquid/builtin/tags/unless_tag.py,sha256=udlHvVxILJsYfDaWRkj6Ynmnz2-2FF5RfkP0eizXt7k,6932
|
|
71
|
-
liquid/extra/__init__.py,sha256=
|
|
71
|
+
liquid/extra/__init__.py,sha256=KrE9DAkwCAOLIaBHt73pnL6sb1tN5-UEKoQsX1b43Gg,2886
|
|
72
72
|
liquid/extra/filters/__init__.py,sha256=tsEfSmaJvd6xN8DfKISPOm_KcHOmBVc4RSQqViD8NvY,733
|
|
73
73
|
liquid/extra/filters/_json.py,sha256=Od7iJWKCWQzf1eZiIqOG1LGwx2JTiHCe95GO5DqgHYY,844
|
|
74
74
|
liquid/extra/filters/array.py,sha256=QvuP2jPg0G4xgIGXOcCRvjpX0O5h7U-WpayAaYmYDdc,1590
|
|
75
|
-
liquid/extra/filters/babel.py,sha256=
|
|
75
|
+
liquid/extra/filters/babel.py,sha256=K0kvw8r5Xy4NwvX4qv-lym14rZ25gB4fCnHqj-IVagI,19187
|
|
76
76
|
liquid/extra/filters/html.py,sha256=j6ghLorvxlVwAtyfVJlmB1tnroGJb8hD-uxUVALVSEQ,929
|
|
77
77
|
liquid/extra/filters/translate.py,sha256=PDEgvmdOvg45ChAKLsaqtamOqaBWOFh4XzQC8igXIyE,13019
|
|
78
|
-
liquid/extra/tags/__init__.py,sha256=
|
|
78
|
+
liquid/extra/tags/__init__.py,sha256=iqB5oOPYhZey-dyljrgAjGeni4lK8Q9-rW2cGRHa4hw,383
|
|
79
79
|
liquid/extra/tags/_with.py,sha256=QLlpY3oW8prCmutEXv1uUCMo_hloLd-sEbiMkMIbtXM,2627
|
|
80
|
-
liquid/extra/tags/extends_tag.py,sha256=
|
|
80
|
+
liquid/extra/tags/extends_tag.py,sha256=3PL1y1FHaeh6L4rA_Xz6wP6l5oHdiOY_u42ivS6c3xE,17893
|
|
81
81
|
liquid/extra/tags/macro_tag.py,sha256=ErjPxOVB50KKn2qi7-2Icmrbx5Uldv2vUSj49TCs4bY,8749
|
|
82
|
+
liquid/extra/tags/snippet_tag.py,sha256=8tFZWDvwhRId5IV0mNKhkFswJx3eTeAYs7-nAwiorNY,3155
|
|
82
83
|
liquid/extra/tags/translate_tag.py,sha256=wv2uK9QtWDrArVeAWmH2QQ7fkM5EAM4gK2bWjlstcnE,12515
|
|
83
84
|
liquid/future/__init__.py,sha256=5BUZi58WyWjPQDflh2C3sQJmuSKAZgNG_286ERyYKFE,79
|
|
84
85
|
liquid/future/environment.py,sha256=Zx4RbgIOMmH5N_VH9poOGRLcSI9DAA5_EkUlP-L6ZlA,986
|
|
@@ -89,7 +90,7 @@ liquid/utils/chain_map.py,sha256=nxkw3wwF6ddlGarIuL7Ii2elm4dU80LySgdQx1oift0,151
|
|
|
89
90
|
liquid/utils/html.py,sha256=TmqOOpRMsy7fqZLj7X5ybd_XQnWW2YDAVDwTP1Gwf40,1706
|
|
90
91
|
liquid/utils/lru_cache.py,sha256=p7bXOGaUwJpLsREI2lGSzK6lLna5W_k_zNXKWnPJdos,4022
|
|
91
92
|
liquid/utils/text.py,sha256=1SwDECNMaqnnZ05je_AZZgxqzZd6U-mvq5jNU3W1-Qk,841
|
|
92
|
-
python_liquid-2.
|
|
93
|
-
python_liquid-2.
|
|
94
|
-
python_liquid-2.
|
|
95
|
-
python_liquid-2.
|
|
93
|
+
python_liquid-2.2.0.dist-info/METADATA,sha256=q0pPKeKAtRV9p5BR0qee8DBDzZWCYlpTTzSt6oh2qrU,6745
|
|
94
|
+
python_liquid-2.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
95
|
+
python_liquid-2.2.0.dist-info/licenses/LICENSE,sha256=yAFURzud5ERNHt1rZIPnTLJ92ep7q8y5yG9g5DUMR_E,1075
|
|
96
|
+
python_liquid-2.2.0.dist-info/RECORD,,
|
|
File without changes
|