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 ADDED
@@ -0,0 +1,465 @@
1
+ # pyright: reportUnknownArgumentType=false
2
+ # pyright: reportUnknownVariableType=false
3
+
4
+ from __future__ import annotations
5
+
6
+ import math
7
+ from collections.abc import Callable, Generator, Mapping, Sequence
8
+ from contextlib import contextmanager
9
+ from typing import TYPE_CHECKING, TypeGuard, cast
10
+
11
+ from ._chain_map import ReadOnlyChainMap
12
+ from ._drop import (
13
+ liquid_async_object,
14
+ liquid_object,
15
+ liquid_primitive,
16
+ )
17
+ from ._nothing import NOTHING
18
+ from .drops import Self
19
+ from .exceptions import DetailedLiquidError, ResourceLimitError
20
+ from .expressions import IntegerLiteral, Name
21
+
22
+ if TYPE_CHECKING:
23
+ from ._template import Template
24
+ from .drops import ForLoop, TableRowLoop
25
+
26
+
27
+ class RenderContext:
28
+ __slots__ = (
29
+ "assign_score",
30
+ "assign_score_cumulative",
31
+ "capture_errors",
32
+ "context_depth",
33
+ "counters",
34
+ "disabled_tags",
35
+ "env",
36
+ "errors",
37
+ "globals",
38
+ "interrupts",
39
+ "limits",
40
+ "locals",
41
+ "loop_iteration_carry",
42
+ "loops",
43
+ "registers",
44
+ "render_score",
45
+ "render_score_cumulative",
46
+ "scope",
47
+ "template",
48
+ )
49
+
50
+ def __init__(
51
+ self,
52
+ template: Template,
53
+ *,
54
+ globals: Mapping[str, object] | None = None,
55
+ capture_errors: bool = False,
56
+ disabled_tags: Sequence[str] | None = None,
57
+ context_depth: int = 1,
58
+ assign_score_carry: int = 0,
59
+ render_score_carry: int = 0,
60
+ loop_iteration_carry: int = 1,
61
+ ) -> None:
62
+ self.template = template
63
+ self.env = template.env
64
+ self.limits = self.env.resource_limits
65
+
66
+ self.globals = globals or {}
67
+ self.locals: dict[str, object] = {}
68
+ self.counters: dict[str, int] = {}
69
+ self.scope = ReadOnlyChainMap(
70
+ self.locals, self.counters, self.globals, {"self": Self(self)}
71
+ )
72
+
73
+ self.registers: dict[str, object] = {}
74
+
75
+ self.loops: list[ForLoop | TableRowLoop] = []
76
+ self.interrupts: list[str] = []
77
+ self.disabled_tags = disabled_tags
78
+
79
+ self.context_depth = context_depth
80
+ self.assign_score = 0
81
+ self.render_score = 0
82
+ self.assign_score_cumulative = assign_score_carry
83
+ self.render_score_cumulative = render_score_carry
84
+ self.loop_iteration_carry = loop_iteration_carry
85
+
86
+ self.capture_errors = capture_errors
87
+ self.errors: list[DetailedLiquidError] = []
88
+
89
+ def assign(self, name: str, value: object) -> None:
90
+ if self.limits.max_assign_score or self.limits.max_assign_score_cumulative:
91
+ score = assign_score_of(value)
92
+ self.assign_score += score
93
+ self.assign_score_cumulative += score
94
+
95
+ if (
96
+ self.limits.max_assign_score
97
+ and self.assign_score > self.limits.max_assign_score
98
+ ) or (
99
+ self.limits.max_assign_score_cumulative
100
+ and self.assign_score_cumulative
101
+ > self.limits.max_assign_score_cumulative
102
+ ):
103
+ raise ResourceLimitError("memory limit exceeded")
104
+
105
+ self.locals[name] = value
106
+
107
+ def resolve(self, name: object) -> object:
108
+ """Lookup `name` in the current scope.
109
+
110
+ Returns
111
+ -------
112
+ The special value `NOTHING` if `name` can not be resolved.
113
+ """
114
+ if not isinstance(name, str):
115
+ return NOTHING
116
+
117
+ return self.scope[name]
118
+
119
+ def resolve_path(self, obj: object, selectors: list[object]) -> tuple[object, int]:
120
+ index: int = -1
121
+ dotted: bool
122
+
123
+ for selector in selectors:
124
+ index += 1
125
+
126
+ if isinstance(selector, (Name, IntegerLiteral)):
127
+ selector = selector.value
128
+ dotted = True
129
+ else:
130
+ dotted = False
131
+
132
+ if liquid_object(obj):
133
+ if liquid_primitive(selector):
134
+ selector = selector.__liquid__("property", self)
135
+
136
+ if isinstance(selector, str) and obj.__liquid_hasattr__(selector, self):
137
+ obj = obj.__liquid_getattr__(selector, self)
138
+ else:
139
+ obj = NOTHING
140
+
141
+ elif isinstance(obj, str):
142
+ if self.env.iterable_strings:
143
+ obj = resolve_sequence_selector(obj, selector, dotted)
144
+ else:
145
+ obj = resolve_string_selector(obj, selector)
146
+
147
+ elif isinstance(obj, Sequence):
148
+ if liquid_primitive(selector):
149
+ selector = selector.__liquid__("index", self)
150
+
151
+ obj = resolve_sequence_selector(obj, selector, dotted)
152
+
153
+ elif isinstance(obj, Mapping):
154
+ if liquid_primitive(selector):
155
+ selector = selector.__liquid__("property", self)
156
+ obj = resolve_mapping_selector(obj, selector, dotted)
157
+
158
+ else:
159
+ obj = resolve_unknown_selector(obj, selector)
160
+
161
+ if obj is NOTHING:
162
+ return (NOTHING, index)
163
+
164
+ return (obj, index)
165
+
166
+ async def resolve_path_async(
167
+ self, obj: object, selectors: list[object]
168
+ ) -> tuple[object, int]:
169
+ index: int = -1
170
+ dotted: bool
171
+
172
+ for selector in selectors:
173
+ index += 1
174
+
175
+ if isinstance(selector, (Name, IntegerLiteral)):
176
+ selector = selector.value
177
+ dotted = True
178
+ else:
179
+ dotted = False
180
+
181
+ if liquid_async_object(obj):
182
+ if liquid_primitive(selector):
183
+ selector = selector.__liquid__("property", self)
184
+
185
+ if isinstance(selector, str) and await obj.__liquid_hasattr_async__(
186
+ selector, self
187
+ ):
188
+ obj = await obj.__liquid_getattr_async__(selector, self)
189
+ else:
190
+ obj = NOTHING
191
+
192
+ elif liquid_object(obj):
193
+ if liquid_primitive(selector):
194
+ selector = selector.__liquid__("property", self)
195
+
196
+ if isinstance(selector, str) and obj.__liquid_hasattr__(selector, self):
197
+ obj = obj.__liquid_getattr__(selector, self)
198
+ else:
199
+ obj = NOTHING
200
+
201
+ elif isinstance(obj, str):
202
+ obj = resolve_string_selector(obj, selector)
203
+
204
+ elif isinstance(obj, Sequence):
205
+ if liquid_primitive(selector):
206
+ selector = selector.__liquid__("index", self)
207
+ obj = resolve_sequence_selector(obj, selector, dotted)
208
+
209
+ elif isinstance(obj, Mapping):
210
+ if liquid_primitive(selector):
211
+ selector = selector.__liquid__("property", self)
212
+ obj = resolve_mapping_selector(obj, selector, dotted)
213
+
214
+ else:
215
+ obj = resolve_unknown_selector(obj, selector)
216
+
217
+ if obj is NOTHING:
218
+ return (NOTHING, index)
219
+
220
+ return (obj, index)
221
+
222
+ def get_register[T](self, key: str, default: Callable[[], T]) -> T:
223
+ if key not in self.registers:
224
+ self.registers[key] = default()
225
+
226
+ return cast("T", self.registers[key])
227
+
228
+ def decrement(self, name: str) -> int:
229
+ i = self.counters.get(name, 0)
230
+ i -= 1
231
+ self.counters[name] = i
232
+ return i
233
+
234
+ def increment(self, name: str) -> int:
235
+ i = self.counters.get(name, 0)
236
+ self.counters[name] = i + 1
237
+ return i
238
+
239
+ def copy(
240
+ self,
241
+ namespace: Mapping[str, object],
242
+ *,
243
+ template: Template | None = None,
244
+ disabled_tags: Sequence[str] | None = None,
245
+ carry_loop_iterations: bool = False,
246
+ block_scope: bool = False,
247
+ ) -> RenderContext:
248
+ self.raise_for_context_depth()
249
+
250
+ if carry_loop_iterations:
251
+ loop_iteration_carry = (
252
+ math.prod(loop.length for loop in self.loops)
253
+ * self.loop_iteration_carry
254
+ )
255
+
256
+ else:
257
+ loop_iteration_carry = 1
258
+
259
+ globals_ = (
260
+ ReadOnlyChainMap(namespace, self.scope)
261
+ if block_scope
262
+ else ReadOnlyChainMap(namespace, self.globals)
263
+ )
264
+
265
+ ctx = RenderContext(
266
+ template or self.template,
267
+ globals=globals_,
268
+ disabled_tags=disabled_tags,
269
+ context_depth=self.context_depth + 1,
270
+ assign_score_carry=self.assign_score_cumulative,
271
+ render_score_carry=self.render_score_cumulative,
272
+ loop_iteration_carry=loop_iteration_carry,
273
+ )
274
+
275
+ for register in self.env.persistent_registers:
276
+ if register in self.registers:
277
+ ctx.registers[register] = self.registers[register]
278
+
279
+ return ctx
280
+
281
+ @contextmanager
282
+ def extend(
283
+ self, namespace: Mapping[str, object], template: Template | None = None
284
+ ) -> Generator[RenderContext]:
285
+ self.raise_for_context_depth()
286
+
287
+ restore_assign_score = self.assign_score
288
+ restore_render_score = self.render_score
289
+ restore_template = self.template
290
+
291
+ if template:
292
+ self.template = template
293
+
294
+ self.scope.push(namespace)
295
+ self.context_depth += 1
296
+ self.assign_score = 0
297
+ self.render_score = 0
298
+
299
+ try:
300
+ yield self
301
+ finally:
302
+ if template:
303
+ self.template = restore_template
304
+ self.scope.pop()
305
+ self.context_depth -= 1
306
+ self.assign_score = restore_assign_score
307
+ self.render_score = restore_render_score
308
+
309
+ @contextmanager
310
+ def loop(
311
+ self, namespace: Mapping[str, object], loop: ForLoop | TableRowLoop
312
+ ) -> Generator[RenderContext]:
313
+ self.raise_for_loop_limit(loop.length)
314
+ self.loops.append(loop)
315
+
316
+ with self.extend(namespace) as context:
317
+ try:
318
+ yield context
319
+ finally:
320
+ self.loops.pop()
321
+
322
+ def parent_loop(self) -> ForLoop | None:
323
+ for loop in reversed(self.loops):
324
+ if is_forloop(loop):
325
+ return loop
326
+ return None
327
+
328
+ def raise_for_context_depth(self) -> None:
329
+ if self.context_depth + 1 > self.limits.max_context_depth:
330
+ raise ResourceLimitError(
331
+ "maximum context depth reached, possible recursive render"
332
+ )
333
+
334
+ def raise_for_loop_limit(self, length: int) -> None:
335
+ if (
336
+ self.limits.max_loop_iterations
337
+ and math.prod(loop.length for loop in self.loops)
338
+ * (length * self.loop_iteration_carry)
339
+ > self.limits.max_loop_iterations
340
+ ):
341
+ raise ResourceLimitError("loop iteration limit reached")
342
+
343
+
344
+ def is_forloop(loop: ForLoop | TableRowLoop) -> TypeGuard[ForLoop]:
345
+ return hasattr(loop, "parent_loop")
346
+
347
+
348
+ def assign_score_of(obj: object, max_nodes: int = 1_000_000) -> int:
349
+ stack: list[object] = [obj]
350
+ seen: set[int] = set()
351
+ count = 0
352
+
353
+ score = 0
354
+
355
+ while stack:
356
+ if count > max_nodes:
357
+ raise ResourceLimitError("maximum data depth reached")
358
+
359
+ node = stack.pop()
360
+ count += 1
361
+
362
+ if isinstance(node, str):
363
+ score += len(node)
364
+
365
+ elif isinstance(node, Sequence):
366
+ id_ = id(node)
367
+ if id_ in seen:
368
+ continue
369
+
370
+ seen.add(id_)
371
+ score += 1
372
+ stack.extend(node)
373
+
374
+ elif isinstance(node, Mapping):
375
+ id_ = id(node)
376
+ if id_ in seen:
377
+ continue
378
+
379
+ seen.add(id_)
380
+ score += 1
381
+ stack.extend(node.keys())
382
+ stack.extend(node.values())
383
+
384
+ else:
385
+ score += 1
386
+
387
+ return score
388
+
389
+
390
+ def resolve_string_selector(obj: str, selector: object) -> object:
391
+ if selector == "size":
392
+ return len(obj)
393
+
394
+ if not obj:
395
+ return NOTHING
396
+
397
+ if selector == "first":
398
+ return obj[0]
399
+
400
+ if selector == "last":
401
+ return obj[-1]
402
+
403
+ return NOTHING
404
+
405
+
406
+ def resolve_sequence_selector(
407
+ obj: Sequence[object], selector: object, dotted: bool
408
+ ) -> object:
409
+ if not obj:
410
+ return 0 if selector == "size" else NOTHING
411
+
412
+ if isinstance(selector, int):
413
+ if selector < 0:
414
+ if len(obj) >= abs(selector):
415
+ return obj[selector]
416
+ return NOTHING
417
+
418
+ return obj[selector] if len(obj) > selector else NOTHING
419
+
420
+ # Only dotted path syntax "calls" "commands", like `.first` and `.size`.
421
+ if not dotted:
422
+ return NOTHING
423
+
424
+ if selector == "first":
425
+ return obj[0]
426
+
427
+ if selector == "last":
428
+ return obj[-1]
429
+
430
+ if selector == "size":
431
+ return len(obj)
432
+
433
+ return NOTHING
434
+
435
+
436
+ def resolve_mapping_selector(
437
+ obj: Mapping[object, object], selector: object, dotted: bool
438
+ ) -> object:
439
+ try:
440
+ if selector in obj:
441
+ return obj[selector]
442
+ except TypeError:
443
+ return NOTHING
444
+
445
+ # Only dotted path syntax "calls" "commands", like `.first` and `.size`.
446
+ if not dotted:
447
+ return NOTHING
448
+
449
+ if selector == "first":
450
+ return next(iter(obj.items()), NOTHING)
451
+
452
+ if selector == "size":
453
+ return len(obj)
454
+
455
+ return NOTHING
456
+
457
+
458
+ def resolve_unknown_selector(obj: object, selector: object) -> object:
459
+ if selector == "size":
460
+ if isinstance(obj, bool):
461
+ return 0
462
+
463
+ # Close enough, most of the time.
464
+ return 8 if isinstance(obj, int) else NOTHING
465
+ return NOTHING
liquid/_drop.py ADDED
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Literal, Protocol, TypeGuard
4
+
5
+ if TYPE_CHECKING:
6
+ from ._context import RenderContext
7
+ from .drops import AsyncIterableDrop, IterableDrop
8
+
9
+ type ContextHint = Literal[
10
+ "array", # to_array
11
+ "boolean", # to_bool
12
+ "index", # Sequence index during path resolution
13
+ "numeric", # to_numeric and right hand side of ordering
14
+ "object", # Right hand side of equality
15
+ "output", # serialize for output
16
+ "property", # Mapping key or object property during path resolution
17
+ "string", # to_str
18
+ ]
19
+
20
+
21
+ class LiquidObject(Protocol):
22
+ def __liquid_hasattr__(self, name: str, context: RenderContext) -> bool:
23
+ """Return whether a Liquid property with the given name exists.
24
+
25
+ The context is available for property-existence checks that depend on
26
+ the current render context. Implementations should distinguish between
27
+ a missing property and an existing property whose value is None.
28
+ """
29
+ ...
30
+
31
+ def __liquid_getattr__(self, name: str, context: RenderContext) -> object:
32
+ """Return the value of a Liquid property.
33
+
34
+ The context is available for resolving values that depend on the
35
+ current render context. Implementations should raise an AttributeError
36
+ exception when a property cannot be resolved.
37
+
38
+ This synchronous method is used during synchronous rendering.
39
+ """
40
+ ...
41
+
42
+
43
+ def liquid_object(obj: object) -> TypeGuard[LiquidObject]:
44
+ return hasattr(obj, "__liquid_hasattr__") and hasattr(obj, "__liquid_getattr__")
45
+
46
+
47
+ class LiquidAsyncObject(Protocol):
48
+ async def __liquid_hasattr_async__(self, name: str, context: RenderContext) -> bool:
49
+ """Asynchronously determine whether a Liquid property exists.
50
+
51
+ This method is used during asynchronous rendering. Implementations
52
+ may override it when checking property existence requires asynchronous
53
+ work.
54
+ """
55
+ ...
56
+
57
+ async def __liquid_getattr_async__(
58
+ self, name: str, context: RenderContext
59
+ ) -> object:
60
+ """Asynchronously resolve a Liquid property.
61
+
62
+ This method is used during asynchronous rendering. Implementations may
63
+ override it when resolving a property requires asynchronous work.
64
+ """
65
+ ...
66
+
67
+
68
+ def liquid_async_object(obj: object) -> TypeGuard[LiquidAsyncObject]:
69
+ return hasattr(obj, "__liquid_hasattr_async__") and hasattr(
70
+ obj, "__liquid_getattr_async__"
71
+ )
72
+
73
+
74
+ class LiquidIterable(Protocol):
75
+ def __liquid_iter__(
76
+ self, offset: int, limit: int | None, reversed_: bool
77
+ ) -> IterableDrop[object]:
78
+ """Return a Liquid iterable."""
79
+ ...
80
+
81
+
82
+ def liquid_iterable(obj: object) -> TypeGuard[LiquidIterable]:
83
+ return hasattr(obj, "__liquid_iter__")
84
+
85
+
86
+ class LiquidAsyncIterable(Protocol):
87
+ async def __liquid_iter_async__(
88
+ self, offset: int, limit: int | None, reversed_: bool
89
+ ) -> IterableDrop[object] | AsyncIterableDrop[object]:
90
+ """Return a asynchronous Liquid iterable."""
91
+ ...
92
+
93
+
94
+ def liquid_async_iterable(obj: object) -> TypeGuard[LiquidAsyncIterable]:
95
+ return hasattr(obj, "__liquid_iter_async__")
96
+
97
+
98
+ class LiquidPrimitive(Protocol):
99
+ def __liquid__(self, hint: ContextHint, context: RenderContext) -> object:
100
+ """Convert this Drop to a Liquid primitive type.
101
+
102
+ The hint identifies the requested representation:
103
+ """
104
+ ...
105
+
106
+
107
+ def liquid_primitive(obj: object) -> TypeGuard[LiquidPrimitive]:
108
+ return hasattr(obj, "__liquid__")