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
@@ -0,0 +1,560 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date, datetime, time
4
+ from decimal import Decimal
5
+ from typing import TYPE_CHECKING, ClassVar
6
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
7
+
8
+ from babel import Locale, UnknownLocaleError, dates, numbers, units
9
+ from dateutil import parser
10
+
11
+ if TYPE_CHECKING:
12
+ from .._filter import FilterContext
13
+
14
+
15
+ def _resolve_locale(
16
+ context: FilterContext,
17
+ locale_var: str,
18
+ default: Locale,
19
+ ) -> Locale:
20
+ locale_ = context.resolve(locale_var)
21
+
22
+ if context.is_nil(locale_):
23
+ locale = default
24
+
25
+ elif isinstance(locale_, str):
26
+ try:
27
+ locale = Locale.parse(locale_)
28
+ except UnknownLocaleError:
29
+ locale = default
30
+
31
+ else:
32
+ raise context.error(
33
+ f"expected a string argument, found {locale_.__class__.__name__}"
34
+ )
35
+
36
+ return locale
37
+
38
+
39
+ class Currency:
40
+ """A Liquid filter for formatting currency values.
41
+
42
+ Parameters
43
+ ----------
44
+ currency_code_var
45
+ The name of a render context variable that resolves to the current currency code.
46
+ Defaults to ``"currency_code"``.
47
+ default_currency_code
48
+ A fallback currency code if `currency_code_var` can not be resolved. Defaults to
49
+ ``"USD"``.
50
+ locale_var
51
+ The name of a render context variable that resolves to the current locale.
52
+ Defaults to ``"locale"``.
53
+ default_locale
54
+ A fallback locale to use if `locale_var` can not be resolved. Defaults to
55
+ ``"en_US"``.
56
+ format_var
57
+ The name of a render context variable that resolves to the current currency
58
+ format string. Defaults to ``"currency_format"``.
59
+ default_format
60
+ A fallback currency format that is used if `format_var` can not be resolved.
61
+ Defaults to `None`, which means the standard format for the current locale will
62
+ be used.
63
+ currency_digits
64
+ Indicates if the format should override locale specific trailing digit behavior.
65
+ Defaults to ``False``.
66
+ input_locale_var
67
+ The name of a render context variable that resolves to a locale suitable for
68
+ parsing input strings to decimals. Defaults to ``"input_locale"``.
69
+ default_input_locale
70
+ A fallback locale to use if `input_locale_var` can not be resolved. Defaults to
71
+ ``"en_US"``.
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ *,
77
+ currency_code_var: str = "currency_code",
78
+ default_currency_code: str = "USD",
79
+ locale_var: str = "locale",
80
+ default_locale: str = "en_US",
81
+ format_var: str = "currency_format",
82
+ default_format: str | None = None,
83
+ currency_digits: bool = True,
84
+ input_locale_var: str = "input_locale",
85
+ default_input_locale: str = "en_US",
86
+ ) -> None:
87
+ self.currency_code_var = currency_code_var
88
+ self.default_currency_code = default_currency_code
89
+ self.locale_var = locale_var
90
+ self.default_locale = Locale.parse(default_locale)
91
+ self.format_var = format_var
92
+ self.default_format = default_format
93
+ self.currency_digits = currency_digits
94
+ self.input_locale_var = input_locale_var
95
+ self.default_input_locale = Locale.parse(default_input_locale)
96
+
97
+ def __call__(
98
+ self,
99
+ context: FilterContext,
100
+ left: object,
101
+ *,
102
+ group_separator: bool = True,
103
+ ) -> str:
104
+ """Apply the filter and return the result."""
105
+ locale = _resolve_locale(
106
+ context,
107
+ self.locale_var,
108
+ default=self.default_locale,
109
+ )
110
+
111
+ input_locale = _resolve_locale(
112
+ context,
113
+ self.input_locale_var,
114
+ default=self.default_input_locale,
115
+ )
116
+
117
+ format_ = context.resolve(self.format_var)
118
+ if context.is_nil(format_):
119
+ format_ = self.default_format
120
+
121
+ if format_ is not None and not isinstance(format_, str):
122
+ raise context.error(f"expected a string argument, found {format_}")
123
+
124
+ currency_code = context.resolve(self.currency_code_var)
125
+ if context.is_nil(currency_code):
126
+ currency_code = self.default_currency_code
127
+
128
+ if not isinstance(currency_code, str):
129
+ raise context.error(f"expected a string argument, found {currency_code}")
130
+
131
+ return numbers.format_currency(
132
+ _parse_decimal(context, left, input_locale),
133
+ currency_code,
134
+ format=format_,
135
+ locale=locale,
136
+ group_separator=group_separator,
137
+ currency_digits=self.currency_digits,
138
+ )
139
+
140
+
141
+ def _parse_decimal(
142
+ context: FilterContext, val: object, locale: str | Locale
143
+ ) -> Decimal:
144
+ if isinstance(val, str):
145
+ try:
146
+ return numbers.parse_decimal(val, locale)
147
+ except numbers.NumberFormatError:
148
+ return Decimal(0)
149
+
150
+ if isinstance(val, (Decimal, float, int)):
151
+ return Decimal(val)
152
+
153
+ # Give objects that implement __int__ etc. a chance.
154
+ return Decimal(context.to_decimal(val, 0))
155
+
156
+
157
+ class DateTime:
158
+ """A Liquid filter for formatting datetime objects.
159
+
160
+ Parameters
161
+ ----------
162
+ timezone_var
163
+ The name of a render context variable that resolves to a timezone. Defaults to
164
+ ``"timezone"``.
165
+ default_timezone
166
+ A fallback timezone to use if `timezone_var` can not be resolved. Defaults to
167
+ ``"UTC"``.
168
+ locale_var
169
+ The name of a render context variable that resolves to the current locale.
170
+ Defaults to ``"locale"``.
171
+ default_locale
172
+ A fallback locale to use if `locale_var` can not be resolved. Defaults to
173
+ ``"en_US"``.
174
+ format_var
175
+ The name of a render context variable that resolves to the current datetime
176
+ format string. Defaults to ``"datetime_format"``.
177
+ default_format
178
+ A fallback datetime format that is used if `format_var` can not be resolved.
179
+ Defaults to ``"medium"``.
180
+ input_timezone_var
181
+ The name of a render context variable that resolves to a timezone for parsing
182
+ datetimes entered as strings. Defaults to ``"input_timezone"``.
183
+ default_input_timezone
184
+ A fallback timezone to use if `input_timezone_var` can not be resolved. Defaults
185
+ to ``"UTC"``.
186
+ """
187
+
188
+ formats: ClassVar[dict[str, str]] = {
189
+ "short": "short",
190
+ "medium": "medium",
191
+ "long": "long",
192
+ "full": "full",
193
+ }
194
+
195
+ def __init__(
196
+ self,
197
+ *,
198
+ timezone_var: str = "timezone",
199
+ default_timezone: str = "UTC",
200
+ locale_var: str = "locale",
201
+ default_locale: str = "en_US",
202
+ format_var: str = "datetime_format",
203
+ default_format: str = "medium",
204
+ input_timezone_var: str = "input_timezone",
205
+ default_input_timezone: str = "UTC",
206
+ ) -> None:
207
+ self.timezone_var = timezone_var
208
+ self.default_timezone = ZoneInfo(default_timezone)
209
+ self.locale_var = locale_var
210
+ self.default_locale = Locale.parse(default_locale)
211
+ self.format_var = format_var
212
+ self.default_format = self.formats.get(default_format, default_format)
213
+ self.input_timezone_var = input_timezone_var
214
+ self.default_input_timezone = ZoneInfo(default_input_timezone)
215
+
216
+ def __call__(
217
+ self,
218
+ context: FilterContext,
219
+ left: object,
220
+ *,
221
+ format: str | None = None,
222
+ ) -> str:
223
+ """Apply the filter and return the result."""
224
+ locale = _resolve_locale(
225
+ context,
226
+ self.locale_var,
227
+ default=self.default_locale,
228
+ )
229
+
230
+ if format:
231
+ _format = self.formats.get(format, format)
232
+ else:
233
+ format_string = context.resolve(self.format_var)
234
+
235
+ if context.is_nil(format_string):
236
+ _format = self.default_format
237
+ elif not isinstance(format_string, str):
238
+ raise context.error(
239
+ f"expected a string argument, found {format_string}"
240
+ )
241
+ else:
242
+ _format = self.formats.get(format_string, format_string)
243
+
244
+ tzinfo = self._resolve_timezone(
245
+ context,
246
+ self.timezone_var,
247
+ default=self.default_timezone,
248
+ )
249
+
250
+ input_tzinfo = self._resolve_timezone(
251
+ context,
252
+ self.input_timezone_var,
253
+ default=self.default_input_timezone,
254
+ )
255
+
256
+ return dates.format_datetime(
257
+ _parse_datetime(context, left, input_tzinfo),
258
+ format=_format,
259
+ locale=locale,
260
+ tzinfo=tzinfo,
261
+ )
262
+
263
+ def _resolve_timezone(
264
+ self,
265
+ context: FilterContext,
266
+ timezone_var: str,
267
+ default: ZoneInfo,
268
+ ) -> ZoneInfo:
269
+ _tz = context.resolve(timezone_var)
270
+
271
+ if context.is_nil(_tz):
272
+ timezone = default
273
+ elif isinstance(_tz, str):
274
+ try:
275
+ timezone = ZoneInfo(_tz)
276
+ except ZoneInfoNotFoundError:
277
+ timezone = default
278
+ else:
279
+ raise context.error(f"expected a string argument, found {_tz}")
280
+
281
+ return timezone
282
+
283
+
284
+ def _parse_number(val: str) -> int | float:
285
+ try:
286
+ return int(val)
287
+ except ValueError:
288
+ # Let the ValueError raise
289
+ return float(val)
290
+
291
+
292
+ def _parse_datetime(
293
+ context: FilterContext,
294
+ val: object,
295
+ default_timezone: ZoneInfo,
296
+ ) -> date | time | datetime | int | float | None:
297
+ if isinstance(val, str):
298
+ if val.lower() in ("now", "today"):
299
+ # `date.format_datetime` will use the current timestamp if
300
+ # given `None`.
301
+ return None
302
+
303
+ # String representations of ints and floats need to be cast
304
+ # to an int or float and not passed to the fuzzy parser.
305
+ try:
306
+ return _parse_number(val)
307
+ except ValueError:
308
+ pass
309
+
310
+ try:
311
+ date_ = parser.parse(val)
312
+ if date_.tzinfo is None:
313
+ return date_.replace(tzinfo=default_timezone)
314
+ return date_
315
+ except parser.ParserError as err:
316
+ raise context.error(str(err)) from err
317
+
318
+ if not isinstance(val, (date, time, datetime, int, float)):
319
+ raise context.error(f"date expected date or time, found {type(val).__name__}")
320
+
321
+ return val
322
+
323
+
324
+ class Number:
325
+ """A Liquid filter for formatting decimal values.
326
+
327
+ Parameters
328
+ ----------
329
+ decimal_quantization_var
330
+ The name of a render context variable that resolves to the decimal quantization
331
+ to be used. Defaults to ``"decimal_quantization"``.
332
+ default_decimal_quantization:
333
+ A fallback decimal quantization if `decimal_quantization_var` can not be
334
+ resolved. Defaults to ``False``.
335
+ locale_var
336
+ The name of a render context variable that resolves to the current locale.
337
+ Defaults to ``"locale"``.
338
+ default_locale
339
+ A fallback locale to use if `locale_var` can not be resolved. Defaults to
340
+ ``"en_US"``.
341
+ format_var
342
+ The name of a render context variable that resolves to the current decimal format
343
+ string. Defaults to ``"decimal_format"``.
344
+ default_format
345
+ A fallback decimal format that is used if `format_var` can not be resolved.
346
+ Defaults to ``None``, which means the standard format for the current locale will
347
+ be used.
348
+ input_locale_var
349
+ The name of a render context variable that resolves to a locale suitable for
350
+ parsing input strings to decimals. Defaults to ``"input_locale"``.
351
+ default_input_locale
352
+ A fallback locale to use if `input_locale_var` can not be resolved. Defaults to
353
+ ``"en_US"``.
354
+ """
355
+
356
+ def __init__(
357
+ self,
358
+ *,
359
+ decimal_quantization_var: str = "decimal_quantization",
360
+ default_decimal_quantization: bool = False,
361
+ locale_var: str = "locale",
362
+ default_locale: str = "en_US",
363
+ format_var: str = "decimal_format",
364
+ default_format: str | None = None,
365
+ input_locale_var: str = "input_locale",
366
+ default_input_locale: str = "en_US",
367
+ ) -> None:
368
+ self.decimal_quantization_var = decimal_quantization_var
369
+ self.default_decimal_quantization = default_decimal_quantization
370
+ self.locale_var = locale_var
371
+ self.default_locale = Locale.parse(default_locale)
372
+ self.format_var = format_var
373
+ self.default_format = default_format
374
+ self.input_locale_var = input_locale_var
375
+ self.default_input_locale = Locale.parse(default_input_locale)
376
+
377
+ def __call__(
378
+ self,
379
+ context: FilterContext,
380
+ left: object,
381
+ *,
382
+ group_separator: bool = True,
383
+ ) -> str:
384
+ """Apply the filter and return the result."""
385
+ decimal_quantization = context.resolve(self.decimal_quantization_var)
386
+ if context.is_nil(decimal_quantization):
387
+ decimal_quantization = self.default_decimal_quantization
388
+
389
+ if not isinstance(decimal_quantization, bool):
390
+ raise context.error(
391
+ f"expected a Boolean argument, found {decimal_quantization!r}"
392
+ )
393
+
394
+ locale = _resolve_locale(
395
+ context,
396
+ self.locale_var,
397
+ default=self.default_locale,
398
+ )
399
+
400
+ input_locale = _resolve_locale(
401
+ context,
402
+ self.input_locale_var,
403
+ default=self.default_input_locale,
404
+ )
405
+
406
+ format_ = context.resolve(self.format_var)
407
+ if context.is_nil(format_):
408
+ format_ = self.default_format
409
+
410
+ if format_ is not None and not isinstance(format_, str):
411
+ raise context.error(f"expected a string argument, found {format_}")
412
+
413
+ return numbers.format_decimal( # type: ignore
414
+ _parse_decimal(context, left, input_locale),
415
+ format=format_,
416
+ locale=locale,
417
+ group_separator=group_separator,
418
+ decimal_quantization=decimal_quantization,
419
+ )
420
+
421
+
422
+ class Unit:
423
+ """A Liquid filter for formatting units of measurement.
424
+
425
+ Parameters
426
+ ----------
427
+ locale_var
428
+ The name of a render context variable that resolves to the current locale.
429
+ Defaults to ``"locale"``.
430
+ default_locale
431
+ A fallback locale to use if `locale_var` can not be resolved. Defaults to
432
+ ``"en_US"``.
433
+ length_var
434
+ The name of a render context variable that resolves to a unit format length.
435
+ Should be one of ``"short"``, ``"long"`` or ``"narrow"``. Defaults to ``"long"``.
436
+ default_length
437
+ A fallback format length to use if `length_var` can not be resolved.
438
+ format_var
439
+ The name of a render context variable that resolves to a decimal format string.
440
+ Defaults to ``"unit_format"``.
441
+ default_format
442
+ A fallback decimal format to use if `format_var` can not be resolved. Defaults to
443
+ ``None``, meaning the locale's standard decimal format is used.
444
+ input_locale_var
445
+ The name of a render context variable that resolves to a locale suitable for
446
+ parsing input strings to decimals. Defaults to ``"input_locale"``.
447
+ default_input_locale
448
+ A fallback locale to use if `input_locale_var` can not be resolved. Defaults to
449
+ ``"en_US"``.
450
+ """
451
+
452
+ def __init__(
453
+ self,
454
+ *,
455
+ locale_var: str = "locale",
456
+ default_locale: str = "en_US",
457
+ length_var: str = "unit_length",
458
+ default_length: str = "long",
459
+ format_var: str = "unit_format",
460
+ default_format: str | None = None,
461
+ input_locale_var: str = "input_locale",
462
+ default_input_locale: str = "en_US",
463
+ ) -> None:
464
+ self.locale_var = locale_var
465
+ self.default_locale = Locale.parse(default_locale)
466
+ self.length_var = length_var
467
+ self.default_length = default_length
468
+ self.format_var = format_var
469
+ self.default_format = default_format
470
+ self.input_locale_var = input_locale_var
471
+ self.default_input_locale = Locale.parse(default_input_locale)
472
+
473
+ def __call__(
474
+ self,
475
+ context: FilterContext,
476
+ left: object,
477
+ measurement_unit: str,
478
+ *,
479
+ denominator: object = None,
480
+ denominator_unit: str | None = None,
481
+ length: str | None = None,
482
+ format: str | None = None,
483
+ ) -> str:
484
+ try:
485
+ return self._inner(
486
+ context,
487
+ left,
488
+ measurement_unit,
489
+ denominator=denominator,
490
+ denominator_unit=denominator_unit,
491
+ length=length,
492
+ format=format,
493
+ )
494
+ except TypeError as err:
495
+ raise context.error(str(err)) from err
496
+ except units.UnknownUnitError as err:
497
+ raise context.error(str(err)) from err
498
+
499
+ def _inner(
500
+ self,
501
+ context: FilterContext,
502
+ left: object,
503
+ measurement_unit: str,
504
+ *,
505
+ denominator: object = None,
506
+ denominator_unit: str | None = None,
507
+ length: str | None = None,
508
+ format: str | None = None,
509
+ ) -> str:
510
+ locale = _resolve_locale(
511
+ context,
512
+ self.locale_var,
513
+ default=self.default_locale,
514
+ )
515
+
516
+ input_locale = _resolve_locale(
517
+ context,
518
+ self.input_locale_var,
519
+ default=self.default_input_locale,
520
+ )
521
+
522
+ length_ = length if length else context.resolve(self.length_var)
523
+
524
+ if length_ not in ("short", "long", "narrow"):
525
+ length_ = self.default_length
526
+
527
+ assert isinstance(length_, str)
528
+
529
+ if format:
530
+ format_: str | None = format
531
+ else:
532
+ format_string = context.resolve(self.format_var)
533
+ if isinstance(format_string, str):
534
+ format_ = format_string
535
+ else:
536
+ format_ = self.default_format
537
+
538
+ if denominator is not None or denominator_unit is not None:
539
+ denominator_ = (
540
+ _parse_decimal(context, denominator, input_locale)
541
+ if denominator is not None
542
+ else 1
543
+ )
544
+ return units.format_compound_unit(
545
+ _parse_decimal(context, left, input_locale),
546
+ numerator_unit=measurement_unit,
547
+ denominator_value=denominator_, # type: ignore
548
+ denominator_unit=denominator_unit,
549
+ length=length_, # type: ignore
550
+ format=format_,
551
+ locale=locale,
552
+ )
553
+
554
+ return units.format_unit(
555
+ _parse_decimal(context, left, input_locale),
556
+ measurement_unit=measurement_unit,
557
+ length=length_, # type: ignore
558
+ format=format_,
559
+ locale=locale,
560
+ )
@@ -0,0 +1,26 @@
1
+ import base64
2
+ import binascii
3
+
4
+ from .._filter import FilterContext
5
+
6
+
7
+ def base64_encode(context: FilterContext, obj: object) -> str:
8
+ return base64.b64encode(context.to_str(obj, "").encode()).decode()
9
+
10
+
11
+ def base64_decode(context: FilterContext, obj: object) -> str:
12
+ try:
13
+ return base64.b64decode(context.to_str(obj, "")).decode()
14
+ except binascii.Error:
15
+ raise context.error("invalid base64-encoded string")
16
+
17
+
18
+ def base64_url_safe_encode(context: FilterContext, obj: object) -> str:
19
+ return base64.urlsafe_b64encode(context.to_str(obj, "").encode()).decode()
20
+
21
+
22
+ def base64_url_safe_decode(context: FilterContext, obj: object) -> str:
23
+ try:
24
+ return base64.urlsafe_b64decode(context.to_str(obj, "")).decode()
25
+ except binascii.Error:
26
+ raise context.error("invalid base64-encoded string")
@@ -0,0 +1,5 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def capitalize(context: FilterContext, left: object) -> str:
5
+ return context.to_str(left, "").capitalize()
liquid/filters/ceil.py ADDED
@@ -0,0 +1,8 @@
1
+ import math
2
+ from decimal import Decimal
3
+
4
+ from .._filter import FilterContext
5
+
6
+
7
+ def ceil(context: FilterContext, left: object) -> int | float | Decimal:
8
+ return math.ceil(context.to_numeric(left, 0))
@@ -0,0 +1,25 @@
1
+ from .._filter import FilterContext
2
+ from ..exceptions import LiquidFilterInterrupt
3
+
4
+
5
+ def compact(
6
+ context: FilterContext, left: object, prop: object = None
7
+ ) -> list[object] | None:
8
+ if prop is None:
9
+ return [item for item in context.input_array(left) if not context.is_nil(item)]
10
+
11
+ result: list[object] = []
12
+
13
+ for obj in context.input_array(left):
14
+ try:
15
+ val = context.get_item(obj, prop)
16
+ except LiquidFilterInterrupt:
17
+ return None
18
+
19
+ if val == context.missing:
20
+ raise context.error(f"can't read property {prop!r}")
21
+
22
+ if not context.is_nil(val):
23
+ result.append(obj)
24
+
25
+ return result
@@ -0,0 +1,11 @@
1
+ from .._filter import FilterContext
2
+
3
+
4
+ def concat(context: FilterContext, left: object, right: object) -> list[object]:
5
+ if not isinstance(right, (list, tuple)):
6
+ raise context.error("concat requires an array argument")
7
+
8
+ if context.is_nil(left):
9
+ return right # type: ignore
10
+
11
+ return context.input_array(left) + right # type: ignore
liquid/filters/date.py ADDED
@@ -0,0 +1,27 @@
1
+ import markupsafe
2
+
3
+ from .._filter import FilterContext
4
+ from .._strftime import ruby_strftime
5
+
6
+
7
+ def date(context: FilterContext, obj: object, fmt: str) -> object:
8
+ if context.is_nil(obj):
9
+ return ""
10
+
11
+ if context.is_nil(fmt) or fmt == "":
12
+ return context.to_str(obj, "")
13
+
14
+ date_ = context.to_date(obj)
15
+
16
+ if date_ is None:
17
+ return obj
18
+
19
+ try:
20
+ result = ruby_strftime(date_, fmt)
21
+ except (ValueError, TypeError) as err:
22
+ raise context.error(str(err))
23
+
24
+ if context.env.autoescape and isinstance(fmt, markupsafe.Markup):
25
+ return markupsafe.Markup(result)
26
+
27
+ return result
@@ -0,0 +1,25 @@
1
+ from decimal import Decimal
2
+
3
+ from .._filter import FilterContext
4
+ from ..drops import EMPTY, FalsyStrictUndefined
5
+
6
+
7
+ def default(
8
+ context: FilterContext,
9
+ left: object,
10
+ right: object = None,
11
+ allow_false: bool = False,
12
+ ) -> object:
13
+ if isinstance(left, FalsyStrictUndefined):
14
+ return right
15
+
16
+ if not isinstance(left, bool) and isinstance(left, (int, float, Decimal)):
17
+ return left
18
+
19
+ if allow_false and left is False:
20
+ return left
21
+
22
+ if not context.truthy(left) or left == EMPTY:
23
+ return right
24
+
25
+ return left