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.
Files changed (212) hide show
  1. liquid/__about__.py +1 -0
  2. liquid/__init__.py +161 -187
  3. liquid/_base_tag.py +66 -0
  4. liquid/{utils/chain_map.py → _chain_map.py} +18 -19
  5. liquid/_context.py +465 -0
  6. liquid/_drop.py +108 -0
  7. liquid/_filter.py +174 -0
  8. liquid/_io.py +18 -0
  9. liquid/_lax_parser_mixin.py +100 -0
  10. liquid/_lexer.py +93 -0
  11. liquid/_lexer_extended.py +623 -0
  12. liquid/_lexer_legacy.py +536 -0
  13. liquid/_loader.py +84 -0
  14. liquid/{utils/lru_cache.py → _lru_cache.py} +28 -36
  15. liquid/_markup.py +253 -0
  16. liquid/_messages.py +292 -0
  17. liquid/_missing.py +3 -0
  18. liquid/_nothing.py +28 -0
  19. liquid/_parser.py +460 -0
  20. liquid/_parser_extended.py +293 -0
  21. liquid/_parser_legacy.py +819 -0
  22. liquid/_resource_limits.py +50 -0
  23. liquid/_slice.py +41 -0
  24. liquid/_static_analysis.py +503 -0
  25. liquid/_strftime.py +186 -0
  26. liquid/_template.py +536 -0
  27. liquid/drops/__init__.py +20 -0
  28. liquid/drops/forloop.py +78 -0
  29. liquid/drops/iterable.py +25 -0
  30. liquid/drops/range.py +76 -0
  31. liquid/drops/self_.py +45 -0
  32. liquid/drops/tablerowloop.py +142 -0
  33. liquid/drops/undefined.py +228 -0
  34. liquid/environment.py +717 -551
  35. liquid/exceptions.py +67 -214
  36. liquid/expressions.py +1184 -0
  37. liquid/filters/__init__.py +134 -0
  38. liquid/filters/abs.py +7 -0
  39. liquid/filters/append.py +5 -0
  40. liquid/filters/at_least.py +9 -0
  41. liquid/filters/at_most.py +9 -0
  42. liquid/filters/babel.py +560 -0
  43. liquid/filters/base64.py +26 -0
  44. liquid/filters/capitalize.py +5 -0
  45. liquid/filters/ceil.py +8 -0
  46. liquid/filters/compact.py +25 -0
  47. liquid/filters/concat.py +11 -0
  48. liquid/filters/date.py +27 -0
  49. liquid/filters/default.py +25 -0
  50. liquid/filters/divided_by.py +21 -0
  51. liquid/filters/downcase.py +5 -0
  52. liquid/filters/escape.py +56 -0
  53. liquid/filters/find.py +56 -0
  54. liquid/filters/first.py +19 -0
  55. liquid/filters/floor.py +8 -0
  56. liquid/filters/html.py +23 -0
  57. liquid/filters/index.py +12 -0
  58. liquid/filters/join.py +13 -0
  59. liquid/filters/json_.py +30 -0
  60. liquid/filters/last.py +17 -0
  61. liquid/filters/map.py +5 -0
  62. liquid/filters/minus.py +13 -0
  63. liquid/filters/modulo.py +18 -0
  64. liquid/filters/newline_to_br.py +16 -0
  65. liquid/filters/plus.py +13 -0
  66. liquid/filters/prepend.py +5 -0
  67. liquid/filters/reject.py +19 -0
  68. liquid/filters/remove.py +36 -0
  69. liquid/filters/replace.py +32 -0
  70. liquid/filters/reverse.py +5 -0
  71. liquid/filters/round.py +35 -0
  72. liquid/filters/size.py +25 -0
  73. liquid/filters/slice.py +39 -0
  74. liquid/filters/sort.py +148 -0
  75. liquid/filters/split.py +34 -0
  76. liquid/filters/squish.py +9 -0
  77. liquid/filters/strip.py +13 -0
  78. liquid/filters/strip_html.py +19 -0
  79. liquid/filters/strip_newlines.py +17 -0
  80. liquid/filters/sum.py +21 -0
  81. liquid/filters/times.py +13 -0
  82. liquid/filters/translate.py +380 -0
  83. liquid/filters/truncate.py +39 -0
  84. liquid/filters/uniq.py +11 -0
  85. liquid/filters/upcase.py +5 -0
  86. liquid/filters/url.py +18 -0
  87. liquid/filters/where.py +18 -0
  88. liquid/loaders/__init__.py +17 -0
  89. liquid/loaders/caching_file_system_loader.py +64 -0
  90. liquid/loaders/choice_loader.py +93 -0
  91. liquid/loaders/dict_loader.py +56 -0
  92. liquid/{builtin/loaders → loaders}/file_system_loader.py +51 -55
  93. liquid/{builtin/loaders → loaders}/mixins.py +39 -51
  94. liquid/loaders/package_loader.py +110 -0
  95. liquid/tags/__init__.py +61 -0
  96. liquid/tags/assign.py +68 -0
  97. liquid/tags/capture.py +83 -0
  98. liquid/tags/case.py +199 -0
  99. liquid/tags/comment.py +56 -0
  100. liquid/tags/cycle.py +131 -0
  101. liquid/tags/decrement.py +55 -0
  102. liquid/tags/doc.py +56 -0
  103. liquid/tags/echo.py +65 -0
  104. liquid/tags/else_.py +53 -0
  105. liquid/tags/error.py +51 -0
  106. liquid/tags/extends.py +448 -0
  107. liquid/tags/for_.py +421 -0
  108. liquid/tags/if_.py +156 -0
  109. liquid/tags/ifchanged.py +69 -0
  110. liquid/tags/include.py +223 -0
  111. liquid/tags/increment.py +55 -0
  112. liquid/tags/inline_comment.py +70 -0
  113. liquid/tags/liquid.py +56 -0
  114. liquid/tags/macro.py +281 -0
  115. liquid/tags/output.py +58 -0
  116. liquid/tags/raw.py +60 -0
  117. liquid/tags/render.py +245 -0
  118. liquid/tags/tablerow.py +254 -0
  119. liquid/tags/template_comment.py +56 -0
  120. liquid/tags/translate.py +316 -0
  121. liquid/tags/unless.py +121 -0
  122. liquid/tags/with_.py +83 -0
  123. liquid/tokens.py +370 -0
  124. {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/METADATA +32 -39
  125. python_liquid-3.0.0.dist-info/RECORD +128 -0
  126. {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
  127. {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/licenses/LICENSE +1 -1
  128. liquid/analyze_tags.py +0 -214
  129. liquid/ast.py +0 -252
  130. liquid/builtin/__init__.py +0 -203
  131. liquid/builtin/content.py +0 -41
  132. liquid/builtin/drops/__init__.py +0 -3
  133. liquid/builtin/drops/drops.py +0 -13
  134. liquid/builtin/expressions/__init__.py +0 -47
  135. liquid/builtin/expressions/_tokenize.py +0 -186
  136. liquid/builtin/expressions/arguments.py +0 -207
  137. liquid/builtin/expressions/filtered.py +0 -387
  138. liquid/builtin/expressions/logical.py +0 -672
  139. liquid/builtin/expressions/loop.py +0 -275
  140. liquid/builtin/expressions/path.py +0 -163
  141. liquid/builtin/expressions/primitive.py +0 -417
  142. liquid/builtin/filters/__init__.py +0 -0
  143. liquid/builtin/filters/array.py +0 -329
  144. liquid/builtin/filters/extra.py +0 -83
  145. liquid/builtin/filters/math.py +0 -129
  146. liquid/builtin/filters/misc.py +0 -137
  147. liquid/builtin/filters/string.py +0 -416
  148. liquid/builtin/illegal.py +0 -28
  149. liquid/builtin/loaders/__init__.py +0 -0
  150. liquid/builtin/loaders/caching_file_system_loader.py +0 -61
  151. liquid/builtin/loaders/choice_loader.py +0 -94
  152. liquid/builtin/loaders/dict_loader.py +0 -64
  153. liquid/builtin/loaders/package_loader.py +0 -132
  154. liquid/builtin/output.py +0 -74
  155. liquid/builtin/tags/__init__.py +0 -0
  156. liquid/builtin/tags/assign_tag.py +0 -75
  157. liquid/builtin/tags/capture_tag.py +0 -98
  158. liquid/builtin/tags/case_tag.py +0 -287
  159. liquid/builtin/tags/comment_tag.py +0 -77
  160. liquid/builtin/tags/cycle_tag.py +0 -132
  161. liquid/builtin/tags/decrement_tag.py +0 -59
  162. liquid/builtin/tags/doc_tag.py +0 -68
  163. liquid/builtin/tags/echo_tag.py +0 -45
  164. liquid/builtin/tags/for_tag.py +0 -337
  165. liquid/builtin/tags/if_tag.py +0 -209
  166. liquid/builtin/tags/ifchanged_tag.py +0 -94
  167. liquid/builtin/tags/include_tag.py +0 -251
  168. liquid/builtin/tags/increment_tag.py +0 -60
  169. liquid/builtin/tags/inline_comment_tag.py +0 -47
  170. liquid/builtin/tags/liquid_tag.py +0 -183
  171. liquid/builtin/tags/render_tag.py +0 -371
  172. liquid/builtin/tags/tablerow_tag.py +0 -320
  173. liquid/builtin/tags/unless_tag.py +0 -218
  174. liquid/context.py +0 -615
  175. liquid/expression.py +0 -41
  176. liquid/extra/__init__.py +0 -98
  177. liquid/extra/filters/__init__.py +0 -33
  178. liquid/extra/filters/_json.py +0 -33
  179. liquid/extra/filters/array.py +0 -60
  180. liquid/extra/filters/babel.py +0 -552
  181. liquid/extra/filters/html.py +0 -32
  182. liquid/extra/filters/translate.py +0 -448
  183. liquid/extra/tags/__init__.py +0 -17
  184. liquid/extra/tags/_with.py +0 -80
  185. liquid/extra/tags/extends_tag.py +0 -576
  186. liquid/extra/tags/macro_tag.py +0 -270
  187. liquid/extra/tags/snippet_tag.py +0 -107
  188. liquid/extra/tags/translate_tag.py +0 -388
  189. liquid/filter.py +0 -230
  190. liquid/future/__init__.py +0 -3
  191. liquid/future/environment.py +0 -30
  192. liquid/future/filters/__init__.py +0 -1
  193. liquid/future/tags/__init__.py +0 -1
  194. liquid/lex.py +0 -260
  195. liquid/limits.py +0 -66
  196. liquid/loader.py +0 -141
  197. liquid/messages.py +0 -357
  198. liquid/mode.py +0 -12
  199. liquid/output.py +0 -35
  200. liquid/parser.py +0 -119
  201. liquid/span.py +0 -35
  202. liquid/static_analysis.py +0 -443
  203. liquid/stream.py +0 -148
  204. liquid/stringify.py +0 -31
  205. liquid/tag.py +0 -47
  206. liquid/template.py +0 -658
  207. liquid/token.py +0 -145
  208. liquid/undefined.py +0 -218
  209. liquid/utils/__init__.py +0 -15
  210. liquid/utils/html.py +0 -57
  211. liquid/utils/text.py +0 -28
  212. 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
@@ -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
+ )
@@ -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
@@ -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