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