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/expressions.py ADDED
@@ -0,0 +1,1184 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import InitVar, dataclass, field
4
+ from decimal import Decimal
5
+ from typing import TYPE_CHECKING
6
+
7
+ import markupsafe
8
+
9
+ from ._filter import FilterContext
10
+ from ._nothing import NOTHING
11
+ from .drops import BLANK, EMPTY, Range
12
+ from .exceptions import (
13
+ LiquidFilterInterrupt,
14
+ LiquidNameError,
15
+ LiquidSyntaxError,
16
+ LiquidTypeError,
17
+ LiquidZeroDivisionError,
18
+ )
19
+ from .tokens import span
20
+
21
+ if TYPE_CHECKING:
22
+ from ._context import RenderContext
23
+ from ._markup import Expression
24
+ from .tokens import Token
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class Filtered:
29
+ token: Token
30
+ left: Expression
31
+ name: Name
32
+ args: list[Expression | KeywordArgument]
33
+ span: Token = field(init=False)
34
+
35
+ def __post_init__(self) -> None:
36
+ self.span = (
37
+ span(self.left.token, self.name.token)
38
+ if not self.args
39
+ else span(self.left.token, self.args[-1].span)
40
+ )
41
+
42
+ def evaluate(self, context: RenderContext) -> object:
43
+ func = context.env.filters.get(self.name.value)
44
+
45
+ # Like Shopify/liquid, filter strictness is applied at render time,
46
+ # and only on rendered markup. Unknown filters in unreached markup will
47
+ # not throw an error.
48
+ if func is None:
49
+ if context.env.strict_filters:
50
+ raise LiquidNameError(
51
+ f"unknown filter {self.name.value!r}",
52
+ token=self.name.token,
53
+ source=context.template.source,
54
+ name=context.template.name,
55
+ )
56
+
57
+ # Pass the input value through.
58
+ return self.left.evaluate(context)
59
+
60
+ left = self.left.evaluate(context)
61
+
62
+ if not self.args:
63
+ try:
64
+ return func(FilterContext(context, self.span), left)
65
+ except LiquidFilterInterrupt:
66
+ return None
67
+ except (TypeError, ValueError) as err:
68
+ raise LiquidTypeError(
69
+ str(err),
70
+ token=self.span,
71
+ source=context.template.source,
72
+ name=context.template.name,
73
+ ) from err
74
+
75
+ args: list[object] = []
76
+ kwargs: dict[str, object] = {}
77
+
78
+ for arg in self.args:
79
+ if isinstance(arg, KeywordArgument):
80
+ kwargs[arg.name.value] = arg.expr.evaluate(context)
81
+ else:
82
+ args.append(arg.evaluate(context))
83
+
84
+ try:
85
+ return func(FilterContext(context, self.span), left, *args, **kwargs)
86
+ except LiquidFilterInterrupt:
87
+ return None
88
+ except (TypeError, ValueError) as err:
89
+ raise LiquidTypeError(
90
+ str(err),
91
+ token=self.span,
92
+ source=context.template.source,
93
+ name=context.template.name,
94
+ ) from err
95
+
96
+ async def evaluate_async(self, context: RenderContext) -> object:
97
+ func = context.env.filters.get(self.name.value)
98
+
99
+ # Like Shopify/liquid, filter strictness is applied at render time,
100
+ # and only on rendered markup. Unknown filters in unreached markup will
101
+ # not throw an error.
102
+ if func is None:
103
+ if context.env.strict_filters:
104
+ raise LiquidNameError(
105
+ f"unknown filter {self.name.value!r}",
106
+ token=self.name.token,
107
+ source=context.template.source,
108
+ name=context.template.name,
109
+ )
110
+
111
+ # Pass the input value through.
112
+ return await self.left.evaluate_async(context)
113
+
114
+ left = await self.left.evaluate_async(context)
115
+
116
+ if not self.args:
117
+ try:
118
+ return func(FilterContext(context, self.span), left)
119
+ except LiquidFilterInterrupt:
120
+ return None
121
+ except (TypeError, ValueError) as err:
122
+ raise LiquidTypeError(
123
+ str(err),
124
+ token=self.span,
125
+ source=context.template.source,
126
+ name=context.template.name,
127
+ ) from err
128
+
129
+ args: list[object] = []
130
+ kwargs: dict[str, object] = {}
131
+
132
+ for arg in self.args:
133
+ if isinstance(arg, KeywordArgument):
134
+ kwargs[arg.name.value] = await arg.expr.evaluate_async(context)
135
+ else:
136
+ args.append(await arg.evaluate_async(context))
137
+
138
+ try:
139
+ return func(FilterContext(context, self.span), left, *args, **kwargs)
140
+ except LiquidFilterInterrupt:
141
+ return None
142
+ except (TypeError, ValueError) as err:
143
+ raise LiquidTypeError(
144
+ str(err),
145
+ token=self.span,
146
+ source=context.template.source,
147
+ name=context.template.name,
148
+ ) from err
149
+
150
+ def children(self) -> list[Expression]:
151
+ return [
152
+ self.left,
153
+ *(a.expr if isinstance(a, KeywordArgument) else a for a in self.args),
154
+ ]
155
+
156
+ def __str__(self) -> str:
157
+ if self.args:
158
+ args = ": " + ", ".join(
159
+ f"{a.name.value}: {a.expr}"
160
+ if isinstance(a, KeywordArgument)
161
+ else str(a)
162
+ for a in self.args
163
+ )
164
+ else:
165
+ args = ""
166
+
167
+ return f"{self.left} | {self.name.value}{args}"
168
+
169
+
170
+ @dataclass(slots=True)
171
+ class Group:
172
+ token: Token
173
+ expr: Expression
174
+ segments: list[PathSegment] | None
175
+ end_token: InitVar[Token]
176
+ span: Token = field(init=False)
177
+
178
+ def __post_init__(self, end_token: Token) -> None:
179
+ if self.segments:
180
+ self.span = span(self.token, self.segments[-1].span)
181
+ else:
182
+ self.span = span(self.token, end_token)
183
+
184
+ def evaluate(self, context: RenderContext) -> object:
185
+ obj = self.expr.evaluate(context)
186
+
187
+ if not self.segments:
188
+ return obj
189
+
190
+ obj, index = context.resolve_path(
191
+ obj,
192
+ [s if isinstance(s, Name) else s.evaluate(context) for s in self.segments],
193
+ )
194
+
195
+ if obj is NOTHING:
196
+ return context.env.undefined(
197
+ self._path(self.segments[: index + 1]),
198
+ self.span,
199
+ context.template.source,
200
+ context.template.name,
201
+ )
202
+
203
+ return obj
204
+
205
+ async def evaluate_async(self, context: RenderContext) -> object:
206
+ obj = await self.expr.evaluate_async(context)
207
+
208
+ if not self.segments:
209
+ return obj
210
+
211
+ obj, index = context.resolve_path(
212
+ obj,
213
+ [
214
+ s if isinstance(s, Name) else await s.evaluate_async(context)
215
+ for s in self.segments
216
+ ],
217
+ )
218
+
219
+ if obj is NOTHING:
220
+ return context.env.undefined(
221
+ self._path(self.segments[: index + 1]),
222
+ self.span,
223
+ context.template.source,
224
+ context.template.name,
225
+ )
226
+
227
+ return obj
228
+
229
+ def __str__(self) -> str:
230
+ return f"({self.expr}){self._path(self.segments)}"
231
+
232
+ def children(self) -> list[Expression]:
233
+ if not self.segments:
234
+ return [self.expr]
235
+ return [
236
+ self.expr,
237
+ *(s.var for s in self.segments if isinstance(s, VariableSegment)),
238
+ ]
239
+
240
+ def _path(self, segments: list[PathSegment] | None) -> str:
241
+ if not segments:
242
+ return ""
243
+
244
+ return "".join(f".{s}" if isinstance(s, Name) else str(s) for s in segments)
245
+
246
+
247
+ @dataclass(slots=True)
248
+ class Ternary:
249
+ token: Token
250
+ consequence: Expression
251
+ condition: Expression
252
+ alternative: Expression | None
253
+ span: Token = field(init=False)
254
+
255
+ def __post_init__(self) -> None:
256
+ self.span = span(self.token, (self.alternative or self.condition).span)
257
+
258
+ def evaluate(self, context: RenderContext) -> object:
259
+ if context.env.truthy(self.condition.evaluate(context), context):
260
+ return self.consequence.evaluate(context)
261
+
262
+ if self.alternative:
263
+ return self.alternative.evaluate(context)
264
+
265
+ return NOTHING
266
+
267
+ async def evaluate_async(self, context: RenderContext) -> object:
268
+ if context.env.truthy(await self.condition.evaluate_async(context), context):
269
+ return await self.consequence.evaluate_async(context)
270
+
271
+ if self.alternative:
272
+ return await self.alternative.evaluate_async(context)
273
+
274
+ return NOTHING
275
+
276
+ def __str__(self) -> str:
277
+ if self.alternative:
278
+ return f"{self.consequence} if {self.condition} else {self.alternative}"
279
+ return f"{self.consequence} if {self.condition}"
280
+
281
+ def children(self) -> list[Expression]:
282
+ result: list[Expression] = [self.consequence, self.condition]
283
+ if self.alternative:
284
+ result.append(self.alternative)
285
+ return result
286
+
287
+
288
+ @dataclass(slots=True)
289
+ class PrefixExpression:
290
+ token: Token
291
+ right: Expression
292
+ span: Token = field(init=False)
293
+
294
+ def __post_init__(self) -> None:
295
+ self.span = span(self.token, self.right.span)
296
+
297
+ def children(self) -> list[Expression]:
298
+ return [self.right]
299
+
300
+
301
+ @dataclass(slots=True)
302
+ class LogicalNot(PrefixExpression):
303
+ def evaluate(self, context: RenderContext) -> object:
304
+ return not context.env.truthy(self.right.evaluate(context), context)
305
+
306
+ async def evaluate_async(self, context: RenderContext) -> object:
307
+ return not context.env.truthy(await self.right.evaluate_async(context), context)
308
+
309
+ def __str__(self) -> str:
310
+ return f"not {self.right}"
311
+
312
+
313
+ @dataclass(slots=True)
314
+ class Pos(PrefixExpression):
315
+ def evaluate(self, context: RenderContext) -> object:
316
+ return context.env.to_numeric(self.right.evaluate(context), context, 0)
317
+
318
+ async def evaluate_async(self, context: RenderContext) -> object:
319
+ return context.env.to_numeric(
320
+ await self.right.evaluate_async(context), context, 0
321
+ )
322
+
323
+ def __str__(self) -> str:
324
+ return f"+{self.right}"
325
+
326
+
327
+ @dataclass(slots=True)
328
+ class Neg(PrefixExpression):
329
+ def evaluate(self, context: RenderContext) -> object:
330
+ return -context.env.to_numeric(self.right.evaluate(context), context, 0)
331
+
332
+ async def evaluate_async(self, context: RenderContext) -> object:
333
+ return -context.env.to_numeric(
334
+ await self.right.evaluate_async(context), context, 0
335
+ )
336
+
337
+ def __str__(self) -> str:
338
+ return f"-{self.right}"
339
+
340
+
341
+ @dataclass(slots=True)
342
+ class InfixExpression:
343
+ token: Token
344
+ left: Expression
345
+ right: Expression
346
+ span: Token = field(init=False)
347
+
348
+ def __post_init__(self) -> None:
349
+ self.span = span(self.left.span, self.right.span)
350
+
351
+ def children(self) -> list[Expression]:
352
+ return [self.left, self.right]
353
+
354
+
355
+ @dataclass(slots=True)
356
+ class LogicalOr(InfixExpression):
357
+ def evaluate(self, context: RenderContext) -> object:
358
+ left = self.left.evaluate(context)
359
+ return (
360
+ left if context.env.truthy(left, context) else self.right.evaluate(context)
361
+ )
362
+
363
+ async def evaluate_async(self, context: RenderContext) -> object:
364
+ left = await self.left.evaluate_async(context)
365
+ return (
366
+ left
367
+ if context.env.truthy(left, context)
368
+ else await self.right.evaluate_async(context)
369
+ )
370
+
371
+ def __str__(self) -> str:
372
+ return f"{self.left} or {self.right}"
373
+
374
+
375
+ @dataclass(slots=True)
376
+ class LogicalAnd(InfixExpression):
377
+ def evaluate(self, context: RenderContext) -> object:
378
+ left = self.left.evaluate(context)
379
+ return (
380
+ self.right.evaluate(context) if context.env.truthy(left, context) else left
381
+ )
382
+
383
+ async def evaluate_async(self, context: RenderContext) -> object:
384
+ left = await self.left.evaluate_async(context)
385
+ return (
386
+ await self.right.evaluate_async(context)
387
+ if context.env.truthy(left, context)
388
+ else left
389
+ )
390
+
391
+ def __str__(self) -> str:
392
+ return f"{self.left} and {self.right}"
393
+
394
+
395
+ @dataclass(slots=True)
396
+ class Eq(InfixExpression):
397
+ def evaluate(self, context: RenderContext) -> object:
398
+ return context.env.eq(
399
+ self.left.evaluate(context),
400
+ self.right.evaluate(context),
401
+ context,
402
+ self.span,
403
+ )
404
+
405
+ async def evaluate_async(self, context: RenderContext) -> object:
406
+ return context.env.eq(
407
+ await self.left.evaluate_async(context),
408
+ await self.right.evaluate_async(context),
409
+ context,
410
+ self.span,
411
+ )
412
+
413
+ def __str__(self) -> str:
414
+ return f"{self.left} == {self.right}"
415
+
416
+
417
+ @dataclass(slots=True)
418
+ class Ne(InfixExpression):
419
+ def evaluate(self, context: RenderContext) -> object:
420
+ return not context.env.eq(
421
+ self.left.evaluate(context),
422
+ self.right.evaluate(context),
423
+ context,
424
+ self.span,
425
+ )
426
+
427
+ async def evaluate_async(self, context: RenderContext) -> object:
428
+ return not context.env.eq(
429
+ await self.left.evaluate_async(context),
430
+ await self.right.evaluate_async(context),
431
+ context,
432
+ self.span,
433
+ )
434
+
435
+ def __str__(self) -> str:
436
+ return f"{self.left} != {self.right}"
437
+
438
+
439
+ @dataclass(slots=True)
440
+ class Lt(InfixExpression):
441
+ def evaluate(self, context: RenderContext) -> object:
442
+ return context.env.lt(
443
+ self.left.evaluate(context),
444
+ self.right.evaluate(context),
445
+ context,
446
+ self.span,
447
+ )
448
+
449
+ async def evaluate_async(self, context: RenderContext) -> object:
450
+ return context.env.lt(
451
+ await self.left.evaluate_async(context),
452
+ await self.right.evaluate_async(context),
453
+ context,
454
+ self.span,
455
+ )
456
+
457
+ def __str__(self) -> str:
458
+ return f"{self.left} < {self.right}"
459
+
460
+
461
+ @dataclass(slots=True)
462
+ class Le(InfixExpression):
463
+ def evaluate(self, context: RenderContext) -> object:
464
+ return context.env.le(
465
+ self.left.evaluate(context),
466
+ self.right.evaluate(context),
467
+ context,
468
+ self.span,
469
+ )
470
+
471
+ async def evaluate_async(self, context: RenderContext) -> object:
472
+ return context.env.le(
473
+ await self.left.evaluate_async(context),
474
+ await self.right.evaluate_async(context),
475
+ context,
476
+ self.span,
477
+ )
478
+
479
+ def __str__(self) -> str:
480
+ return f"{self.left} <= {self.right}"
481
+
482
+
483
+ @dataclass(slots=True)
484
+ class Gt(InfixExpression):
485
+ def evaluate(self, context: RenderContext) -> object:
486
+ return context.env.lt(
487
+ self.right.evaluate(context),
488
+ self.left.evaluate(context),
489
+ context,
490
+ self.span,
491
+ )
492
+
493
+ async def evaluate_async(self, context: RenderContext) -> object:
494
+ return context.env.lt(
495
+ await self.right.evaluate_async(context),
496
+ await self.left.evaluate_async(context),
497
+ context,
498
+ self.span,
499
+ )
500
+
501
+ def __str__(self) -> str:
502
+ return f"{self.left} > {self.right}"
503
+
504
+
505
+ @dataclass(slots=True)
506
+ class Ge(InfixExpression):
507
+ def evaluate(self, context: RenderContext) -> object:
508
+ return context.env.le(
509
+ self.right.evaluate(context),
510
+ self.left.evaluate(context),
511
+ context,
512
+ self.span,
513
+ )
514
+
515
+ async def evaluate_async(self, context: RenderContext) -> object:
516
+ return context.env.le(
517
+ await self.right.evaluate_async(context),
518
+ await self.left.evaluate_async(context),
519
+ context,
520
+ self.span,
521
+ )
522
+
523
+ def __str__(self) -> str:
524
+ return f"{self.left} >= {self.right}"
525
+
526
+
527
+ @dataclass(slots=True)
528
+ class Contains(InfixExpression):
529
+ def evaluate(self, context: RenderContext) -> object:
530
+ return context.env.contains(
531
+ self.left.evaluate(context),
532
+ self.right.evaluate(context),
533
+ context,
534
+ self.span,
535
+ )
536
+
537
+ async def evaluate_async(self, context: RenderContext) -> object:
538
+ return context.env.contains(
539
+ await self.left.evaluate_async(context),
540
+ await self.right.evaluate_async(context),
541
+ context,
542
+ self.span,
543
+ )
544
+
545
+ def __str__(self) -> str:
546
+ return f"{self.left} contains {self.right}"
547
+
548
+
549
+ @dataclass(slots=True)
550
+ class Add(InfixExpression):
551
+ def evaluate(self, context: RenderContext) -> object:
552
+ left = context.env.to_numeric(self.left.evaluate(context), context, 0)
553
+ right = context.env.to_numeric(self.right.evaluate(context), context, 0)
554
+
555
+ if isinstance(left, int) and isinstance(right, int):
556
+ return left + right
557
+
558
+ return float(Decimal(str(left)) + Decimal(str(right)))
559
+
560
+ async def evaluate_async(self, context: RenderContext) -> object:
561
+ left = context.env.to_numeric(
562
+ await self.left.evaluate_async(context), context, 0
563
+ )
564
+ right = context.env.to_numeric(
565
+ await self.right.evaluate_async(context), context, 0
566
+ )
567
+
568
+ if isinstance(left, int) and isinstance(right, int):
569
+ return left + right
570
+
571
+ return float(Decimal(str(left)) + Decimal(str(right)))
572
+
573
+ def __str__(self) -> str:
574
+ return f"{self.left} + {self.right}"
575
+
576
+
577
+ @dataclass(slots=True)
578
+ class Sub(InfixExpression):
579
+ def evaluate(self, context: RenderContext) -> object:
580
+ left = context.env.to_numeric(self.left.evaluate(context), context, 0)
581
+ right = context.env.to_numeric(self.right.evaluate(context), context, 0)
582
+
583
+ if isinstance(left, int) and isinstance(right, int):
584
+ return left - right
585
+
586
+ return float(Decimal(str(left)) - Decimal(str(right)))
587
+
588
+ async def evaluate_async(self, context: RenderContext) -> object:
589
+ left = context.env.to_numeric(
590
+ await self.left.evaluate_async(context), context, 0
591
+ )
592
+ right = context.env.to_numeric(
593
+ await self.right.evaluate_async(context), context, 0
594
+ )
595
+
596
+ if isinstance(left, int) and isinstance(right, int):
597
+ return left - right
598
+
599
+ return float(Decimal(str(left)) - Decimal(str(right)))
600
+
601
+ def __str__(self) -> str:
602
+ return f"{self.left} - {self.right}"
603
+
604
+
605
+ @dataclass(slots=True)
606
+ class Mul(InfixExpression):
607
+ def evaluate(self, context: RenderContext) -> object:
608
+ left = context.env.to_numeric(self.left.evaluate(context), context, 0)
609
+ right = context.env.to_numeric(self.right.evaluate(context), context, 0)
610
+
611
+ if isinstance(left, int) and isinstance(right, int):
612
+ return left * right
613
+
614
+ return float(Decimal(str(left)) * Decimal(str(right)))
615
+
616
+ async def evaluate_async(self, context: RenderContext) -> object:
617
+ left = context.env.to_numeric(
618
+ await self.left.evaluate_async(context), context, 0
619
+ )
620
+ right = context.env.to_numeric(
621
+ await self.right.evaluate_async(context), context, 0
622
+ )
623
+
624
+ if isinstance(left, int) and isinstance(right, int):
625
+ return left * right
626
+
627
+ return float(Decimal(str(left)) * Decimal(str(right)))
628
+
629
+ def __str__(self) -> str:
630
+ return f"{self.left} * {self.right}"
631
+
632
+
633
+ @dataclass(slots=True)
634
+ class Div(InfixExpression):
635
+ def evaluate(self, context: RenderContext) -> object:
636
+ left = context.env.to_numeric(self.left.evaluate(context), context, 0)
637
+ right = context.env.to_numeric(self.right.evaluate(context), context, 0)
638
+
639
+ if right == 0:
640
+ raise LiquidZeroDivisionError(
641
+ "divide by zero",
642
+ token=self.right.span,
643
+ source=context.template.source,
644
+ name=context.template.name,
645
+ )
646
+
647
+ if isinstance(left, int) and isinstance(right, int):
648
+ return left // right
649
+
650
+ if isinstance(left, Decimal) or isinstance(right, Decimal):
651
+ return float(Decimal(str(left)) / Decimal(str(right)))
652
+
653
+ return left / right
654
+
655
+ async def evaluate_async(self, context: RenderContext) -> object:
656
+ left = context.env.to_numeric(
657
+ await self.left.evaluate_async(context), context, 0
658
+ )
659
+ right = context.env.to_numeric(
660
+ await self.right.evaluate_async(context), context, 0
661
+ )
662
+
663
+ if right == 0:
664
+ raise LiquidZeroDivisionError(
665
+ "divide by zero",
666
+ token=self.right.span,
667
+ source=context.template.source,
668
+ name=context.template.name,
669
+ )
670
+
671
+ if isinstance(left, int) and isinstance(right, int):
672
+ return left // right
673
+
674
+ if isinstance(left, Decimal) or isinstance(right, Decimal):
675
+ return float(Decimal(str(left)) / Decimal(str(right)))
676
+
677
+ return left / right
678
+
679
+ def __str__(self) -> str:
680
+ return f"{self.left} / {self.right}"
681
+
682
+
683
+ @dataclass(slots=True)
684
+ class Mod(InfixExpression):
685
+ def evaluate(self, context: RenderContext) -> object:
686
+ left = context.env.to_numeric(self.left.evaluate(context), context, 0)
687
+ right = context.env.to_numeric(self.right.evaluate(context), context, 0)
688
+
689
+ if right == 0:
690
+ raise LiquidZeroDivisionError(
691
+ "divide by zero",
692
+ token=self.right.span,
693
+ source=context.template.source,
694
+ name=context.template.name,
695
+ )
696
+
697
+ if isinstance(left, int) and isinstance(right, int):
698
+ return left % right
699
+
700
+ return float(Decimal(str(left)) % Decimal(str(right)))
701
+
702
+ async def evaluate_async(self, context: RenderContext) -> object:
703
+ left = context.env.to_numeric(
704
+ await self.left.evaluate_async(context), context, 0
705
+ )
706
+ right = context.env.to_numeric(
707
+ await self.right.evaluate_async(context), context, 0
708
+ )
709
+
710
+ if right == 0:
711
+ raise LiquidZeroDivisionError(
712
+ "divide by zero",
713
+ token=self.right.span,
714
+ source=context.template.source,
715
+ name=context.template.name,
716
+ )
717
+
718
+ if isinstance(left, int) and isinstance(right, int):
719
+ return left % right
720
+
721
+ return float(Decimal(str(left)) % Decimal(str(right)))
722
+
723
+ def __str__(self) -> str:
724
+ return f"{self.left} / {self.right}"
725
+
726
+
727
+ @dataclass(slots=True)
728
+ class Variable:
729
+ token: Token
730
+ root: Name | NameSegment | VariableSegment
731
+ selectors: list[PathSegment] | None
732
+ span: Token = field(init=False)
733
+
734
+ def __post_init__(self) -> None:
735
+ self.span = (
736
+ self.root.span
737
+ if not self.selectors
738
+ else span(self.root.span, self.selectors[-1].span)
739
+ )
740
+
741
+ def evaluate(self, context: RenderContext) -> object:
742
+ selector = (
743
+ self.root.evaluate(context)
744
+ if isinstance(self.root, VariableSegment)
745
+ else self.root.value
746
+ )
747
+
748
+ index = 0
749
+ obj = context.resolve(selector)
750
+
751
+ if self.selectors:
752
+ obj, index = context.resolve_path(
753
+ obj,
754
+ [
755
+ s if isinstance(s, (Name, IntegerLiteral)) else s.evaluate(context)
756
+ for s in self.selectors
757
+ ],
758
+ )
759
+
760
+ if obj is NOTHING:
761
+ return context.env.undefined(
762
+ self.path(self.selectors[: index + 1] if self.selectors else None),
763
+ self.span,
764
+ context.template.source,
765
+ context.template.name,
766
+ )
767
+
768
+ return obj
769
+
770
+ async def evaluate_async(self, context: RenderContext) -> object:
771
+ selector = (
772
+ await self.root.evaluate_async(context)
773
+ if isinstance(self.root, VariableSegment)
774
+ else self.root.value
775
+ )
776
+
777
+ index = 0
778
+ obj = context.resolve(selector)
779
+
780
+ if self.selectors:
781
+ obj, index = await context.resolve_path_async(
782
+ obj,
783
+ [
784
+ s
785
+ if isinstance(s, (Name, IntegerLiteral))
786
+ else await s.evaluate_async(context)
787
+ for s in self.selectors
788
+ ],
789
+ )
790
+
791
+ if obj is NOTHING:
792
+ return context.env.undefined(
793
+ self.path(self.selectors[: index + 1] if self.selectors else None),
794
+ self.span,
795
+ context.template.source,
796
+ context.template.name,
797
+ )
798
+
799
+ return obj
800
+
801
+ def children(self) -> list[Expression]:
802
+ exprs: list[Expression] = []
803
+
804
+ if isinstance(self.root, VariableSegment):
805
+ exprs.append(self.root.var)
806
+
807
+ if self.selectors:
808
+ exprs.extend(
809
+ s.var for s in self.selectors if isinstance(s, VariableSegment)
810
+ )
811
+
812
+ return exprs
813
+
814
+ def path(self, selectors: list[PathSegment] | None) -> str:
815
+ root = str(self.root)
816
+
817
+ if selectors:
818
+ _selectors = "".join(
819
+ f".{s}" if isinstance(s, (Name, IntegerLiteral)) else str(s)
820
+ for s in selectors
821
+ )
822
+ return f"{root}{_selectors}"
823
+
824
+ return root
825
+
826
+ def location(self) -> Location:
827
+ def serialize(s: PathSegment) -> int | str | Location:
828
+ if isinstance(s, (Name, NameSegment, IndexSegment, IntegerLiteral)):
829
+ return s.value
830
+ if isinstance(s, VariableSegment):
831
+ return s.var.location()
832
+ return str(s)
833
+
834
+ segments: Location = [serialize(self.root)]
835
+ if self.selectors:
836
+ segments.extend(serialize(s) for s in self.selectors)
837
+
838
+ return segments
839
+
840
+ def __str__(self) -> str:
841
+ return self.path(self.selectors)
842
+
843
+
844
+ @dataclass(slots=True)
845
+ class IndexSegment:
846
+ token: Token
847
+ value: int
848
+ span: Token
849
+
850
+ def evaluate(self, context: RenderContext) -> object:
851
+ return self.value
852
+
853
+ async def evaluate_async(self, context: RenderContext) -> object:
854
+ return self.value
855
+
856
+ def children(self) -> list[Expression]:
857
+ return []
858
+
859
+ def __str__(self) -> str:
860
+ return f"[{self.value}]"
861
+
862
+
863
+ @dataclass(slots=True)
864
+ class NameSegment:
865
+ token: Token
866
+ value: str
867
+ span: Token
868
+
869
+ def evaluate(self, context: RenderContext) -> object:
870
+ return self.value
871
+
872
+ async def evaluate_async(self, context: RenderContext) -> object:
873
+ return self.value
874
+
875
+ def children(self) -> list[Expression]:
876
+ return []
877
+
878
+ def __str__(self) -> str:
879
+ return f"[{self.value!r}]"
880
+
881
+
882
+ @dataclass(slots=True)
883
+ class VariableSegment:
884
+ token: Token
885
+ var: Variable
886
+ span: Token
887
+
888
+ def evaluate(self, context: RenderContext) -> object:
889
+ return self.var.evaluate(context)
890
+
891
+ async def evaluate_async(self, context: RenderContext) -> object:
892
+ return await self.var.evaluate_async(context)
893
+
894
+ def children(self) -> list[Expression]:
895
+ return [self.var]
896
+
897
+ def __str__(self) -> str:
898
+ return f"[{self.var}]"
899
+
900
+
901
+ @dataclass(slots=True)
902
+ class StringLiteral:
903
+ token: Token
904
+ value: str
905
+ span: Token
906
+
907
+ def evaluate(self, context: RenderContext) -> object:
908
+ return markupsafe.Markup(self.value) if context.env.autoescape else self.value
909
+
910
+ async def evaluate_async(self, context: RenderContext) -> object:
911
+ return markupsafe.Markup(self.value) if context.env.autoescape else self.value
912
+
913
+ def children(self) -> list[Expression]:
914
+ return []
915
+
916
+ def __str__(self) -> str:
917
+ return repr(self.value)
918
+
919
+
920
+ @dataclass(slots=True)
921
+ class IntegerLiteral:
922
+ token: Token
923
+ value: int
924
+ span: Token = field(init=False)
925
+
926
+ def __post_init__(self) -> None:
927
+ self.span = self.token
928
+
929
+ def evaluate(self, context: RenderContext) -> object:
930
+ return self.value
931
+
932
+ async def evaluate_async(self, context: RenderContext) -> object:
933
+ return self.value
934
+
935
+ def children(self) -> list[Expression]:
936
+ return []
937
+
938
+ def __str__(self) -> str:
939
+ return str(self.value)
940
+
941
+
942
+ @dataclass(slots=True)
943
+ class FloatLiteral:
944
+ token: Token
945
+ value: float
946
+ span: Token = field(init=False)
947
+
948
+ def __post_init__(self) -> None:
949
+ self.span = self.token
950
+
951
+ def evaluate(self, context: RenderContext) -> object:
952
+ return self.value
953
+
954
+ async def evaluate_async(self, context: RenderContext) -> object:
955
+ return self.value
956
+
957
+ def children(self) -> list[Expression]:
958
+ return []
959
+
960
+ def __str__(self) -> str:
961
+ return str(self.value)
962
+
963
+
964
+ @dataclass(slots=True)
965
+ class BooleanLiteral:
966
+ token: Token
967
+ value: bool
968
+ span: Token = field(init=False)
969
+
970
+ def __post_init__(self) -> None:
971
+ self.span = self.token
972
+
973
+ def evaluate(self, context: RenderContext) -> object:
974
+ return self.value
975
+
976
+ async def evaluate_async(self, context: RenderContext) -> object:
977
+ return self.value
978
+
979
+ def children(self) -> list[Expression]:
980
+ return []
981
+
982
+ def __str__(self) -> str:
983
+ return str(self.value).lower()
984
+
985
+
986
+ @dataclass(slots=True)
987
+ class NullLiteral:
988
+ token: Token
989
+ span: Token = field(init=False)
990
+
991
+ def __post_init__(self) -> None:
992
+ self.span = self.token
993
+
994
+ def evaluate(self, context: RenderContext) -> object:
995
+ return None
996
+
997
+ async def evaluate_async(self, context: RenderContext) -> object:
998
+ return None
999
+
1000
+ def children(self) -> list[Expression]:
1001
+ return []
1002
+
1003
+ def __str__(self) -> str:
1004
+ return "null"
1005
+
1006
+
1007
+ @dataclass(slots=True)
1008
+ class Blank:
1009
+ token: Token
1010
+ span: Token = field(init=False)
1011
+
1012
+ def __post_init__(self) -> None:
1013
+ self.span = self.token
1014
+
1015
+ def evaluate(self, context: RenderContext) -> object:
1016
+ return BLANK
1017
+
1018
+ async def evaluate_async(self, context: RenderContext) -> object:
1019
+ return BLANK
1020
+
1021
+ def children(self) -> list[Expression]:
1022
+ return []
1023
+
1024
+ def __str__(self) -> str:
1025
+ return ""
1026
+
1027
+
1028
+ @dataclass(slots=True)
1029
+ class Empty:
1030
+ token: Token
1031
+ span: Token = field(init=False)
1032
+
1033
+ def __post_init__(self) -> None:
1034
+ self.span = self.token
1035
+
1036
+ def evaluate(self, context: RenderContext) -> object:
1037
+ return EMPTY
1038
+
1039
+ async def evaluate_async(self, context: RenderContext) -> object:
1040
+ return EMPTY
1041
+
1042
+ def children(self) -> list[Expression]:
1043
+ return []
1044
+
1045
+ def __str__(self) -> str:
1046
+ return ""
1047
+
1048
+
1049
+ @dataclass(slots=True)
1050
+ class RangeLiteral:
1051
+ token: Token
1052
+ start: Expression
1053
+ stop: Expression
1054
+ span: Token = field(init=False)
1055
+ value: str = field(init=False)
1056
+
1057
+ def __post_init__(self) -> None:
1058
+ self.span = span(self.start.span, self.stop.span)
1059
+ self.value = ""
1060
+
1061
+ def evaluate(self, context: RenderContext) -> object:
1062
+ start = context.env.to_numeric(self.start.evaluate(context), context)
1063
+ stop = context.env.to_numeric(self.stop.evaluate(context), context)
1064
+ return Range(int(start), int(stop))
1065
+
1066
+ async def evaluate_async(self, context: RenderContext) -> object:
1067
+ start = context.env.to_numeric(
1068
+ await self.start.evaluate_async(context), context
1069
+ )
1070
+ stop = context.env.to_numeric(await self.stop.evaluate_async(context), context)
1071
+ return Range(int(start), int(stop))
1072
+
1073
+ def children(self) -> list[Expression]:
1074
+ return [self.start, self.stop]
1075
+
1076
+ def __str__(self) -> str:
1077
+ return f"({self.start}..{self.stop})"
1078
+
1079
+
1080
+ @dataclass(slots=True)
1081
+ class Name:
1082
+ token: Token
1083
+ value: str
1084
+ span: Token = field(init=False)
1085
+
1086
+ def __post_init__(self) -> None:
1087
+ self.span = self.token
1088
+
1089
+ def __str__(self) -> str:
1090
+ return self.value
1091
+
1092
+
1093
+ @dataclass(slots=True)
1094
+ class KeywordArgument:
1095
+ token: Token
1096
+ name: Name
1097
+ expr: Expression
1098
+ span: Token = field(init=False)
1099
+
1100
+ def __post_init__(self) -> None:
1101
+ self.span = span(self.token, self.expr.token)
1102
+
1103
+
1104
+ @dataclass(slots=True)
1105
+ class Parameter:
1106
+ token: Token
1107
+ name: Name
1108
+ expr: Expression | None
1109
+ span: Token = field(init=False)
1110
+
1111
+ def __post_init__(self) -> None:
1112
+ if self.expr:
1113
+ self.span = span(self.token, self.expr.token)
1114
+ else:
1115
+ self.span = self.token
1116
+
1117
+
1118
+ type PathSegment = (
1119
+ Name | NameSegment | IndexSegment | IntegerLiteral | VariableSegment | RangeLiteral
1120
+ )
1121
+
1122
+ type Location = list[int | str | Location]
1123
+
1124
+
1125
+ @dataclass(slots=True)
1126
+ class ErrorExpr:
1127
+ token: Token
1128
+ error: LiquidSyntaxError
1129
+ span: Token = field(init=False)
1130
+
1131
+ def __post_init__(self) -> None:
1132
+ self.span = self.token
1133
+
1134
+ def evaluate(self, context: RenderContext) -> object:
1135
+ if context.capture_errors:
1136
+ raise self.error
1137
+ return None
1138
+
1139
+ async def evaluate_async(self, context: RenderContext) -> object:
1140
+ if context.capture_errors:
1141
+ raise self.error
1142
+ return None
1143
+
1144
+ def children(self) -> list[Expression]:
1145
+ return []
1146
+
1147
+ def __str__(self) -> str:
1148
+ return self.error.detailed_message()
1149
+
1150
+
1151
+ def tree_view(expr: Expression) -> str:
1152
+ """Return an ASCII tree representation of `expr`."""
1153
+ # (prefix, connector, class_name, inspect_value)
1154
+ nodes: list[tuple[str, str, str, str]] = []
1155
+
1156
+ def visit(node: Expression, prefix: str, *, is_last: bool) -> None:
1157
+ if not prefix:
1158
+ connector = ""
1159
+ elif is_last:
1160
+ connector = "└── "
1161
+ else:
1162
+ connector = "├── "
1163
+
1164
+ nodes.append((prefix, connector, node.__class__.__name__, str(node)))
1165
+
1166
+ child_prefix = prefix + (" " if is_last else "│ ")
1167
+ for i, child in enumerate(node.children()):
1168
+ last = i == len(node.children()) - 1
1169
+ visit(child, child_prefix, is_last=last)
1170
+
1171
+ visit(expr, "", is_last=True)
1172
+
1173
+ widths = [len(prefix + connector + name) for prefix, connector, name, _ in nodes]
1174
+ max_width = max(widths) if widths else 0
1175
+
1176
+ lines: list[str] = []
1177
+
1178
+ for node, width in zip(nodes, widths, strict=True):
1179
+ prefix, connector, name, value = node
1180
+ left = prefix + connector + name
1181
+ padding = " " * (max_width - width + 4)
1182
+ lines.append(left + padding + value)
1183
+
1184
+ return "\n".join(lines)