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
@@ -1,20 +1,12 @@
1
1
  """An LRU cache with a mapping interface implemented using an ordered dict."""
2
2
 
3
3
  from collections import OrderedDict
4
+ from collections.abc import Iterator
4
5
  from threading import Lock
5
- from typing import Generic
6
- from typing import Iterator
7
- from typing import Optional
8
- from typing import TypeVar
9
- from typing import Union
10
6
  from typing import overload
11
7
 
12
- _KT = TypeVar("_KT")
13
- _VT = TypeVar("_VT")
14
- _T = TypeVar("_T")
15
8
 
16
-
17
- class LRUCache(Generic[_KT, _VT]):
9
+ class LRUCache[KT, VT]:
18
10
  """An LRU cache with a mapping interface."""
19
11
 
20
12
  def __init__(self, capacity: int):
@@ -22,14 +14,14 @@ class LRUCache(Generic[_KT, _VT]):
22
14
  raise ValueError("cache capacity must be greater than zero")
23
15
 
24
16
  self.capacity = capacity
25
- self._cache: OrderedDict[_KT, _VT] = OrderedDict()
17
+ self._cache: OrderedDict[KT, VT] = OrderedDict()
26
18
 
27
- def __getitem__(self, key: _KT) -> _VT:
19
+ def __getitem__(self, key: KT) -> VT:
28
20
  value = self._cache[key] # This will raise a KeyError if key is not cached
29
21
  self._cache.move_to_end(key)
30
22
  return value
31
23
 
32
- def __setitem__(self, key: _KT, value: _VT) -> None:
24
+ def __setitem__(self, key: KT, value: VT) -> None:
33
25
  try:
34
26
  self._cache.move_to_end(key)
35
27
  except KeyError:
@@ -38,74 +30,74 @@ class LRUCache(Generic[_KT, _VT]):
38
30
 
39
31
  self._cache[key] = value
40
32
 
41
- def __delitem__(self, key: _KT) -> None:
33
+ def __delitem__(self, key: KT) -> None:
42
34
  del self._cache[key]
43
35
 
44
36
  def __len__(self) -> int:
45
37
  return len(self._cache)
46
38
 
47
- def __iter__(self) -> Iterator[_KT]:
39
+ def __iter__(self) -> Iterator[KT]:
48
40
  return reversed(self._cache)
49
41
 
50
- def __contains__(self, key: _KT) -> bool:
42
+ def __contains__(self, key: KT) -> bool:
51
43
  return key in self._cache
52
44
 
53
45
  @overload
54
- def get(self, key: _KT) -> Optional[_VT]: ...
46
+ def get(self, key: KT) -> VT | None: ...
55
47
  @overload
56
- def get(self, key: _KT, default: _VT) -> _VT: ...
48
+ def get(self, key: KT, default: VT) -> VT: ...
57
49
  @overload
58
- def get(self, key: _KT, default: _T) -> Union[_VT, _T]: ...
59
- def get(self, key: _KT, default: object = None) -> object:
50
+ def get[T](self, key: KT, default: T) -> VT | T: ...
51
+ def get(self, key: KT, default: object = None) -> object:
60
52
  """Return the cached value for _key_ if _key_ is in the cache, else default."""
61
53
  try:
62
54
  return self[key]
63
55
  except KeyError:
64
56
  return default
65
57
 
66
- def keys(self) -> Iterator[_KT]:
58
+ def keys(self) -> Iterator[KT]:
67
59
  """Return an iterator over this cache's keys."""
68
60
  return reversed(self._cache.keys())
69
61
 
70
- def values(self) -> Iterator[_VT]:
62
+ def values(self) -> Iterator[VT]:
71
63
  """Return an iterator over this cache's values."""
72
64
  return reversed(self._cache.values())
73
65
 
74
- def items(self) -> Iterator[tuple[_KT, _VT]]:
66
+ def items(self) -> Iterator[tuple[KT, VT]]:
75
67
  """Return an iterator over this cache's key/value pairs."""
76
68
  return reversed(self._cache.items())
77
69
 
78
70
 
79
- class ThreadSafeLRUCache(LRUCache[_KT, _VT]):
71
+ class ThreadSafeLRUCache[KT, VT](LRUCache[KT, VT]):
80
72
  """A thread safe LRU cache."""
81
73
 
82
74
  def __init__(self, capacity: int):
83
75
  super().__init__(capacity)
84
76
  self._lock = Lock()
85
77
 
86
- def __getitem__(self, key: _KT) -> _VT:
78
+ def __getitem__(self, key: KT) -> VT:
87
79
  with self._lock:
88
80
  return super().__getitem__(key)
89
81
 
90
- def __setitem__(self, key: _KT, value: _VT) -> None:
82
+ def __setitem__(self, key: KT, value: VT) -> None:
91
83
  with self._lock:
92
84
  return super().__setitem__(key, value)
93
85
 
94
- def __delitem__(self, key: _KT) -> None:
86
+ def __delitem__(self, key: KT) -> None:
95
87
  with self._lock:
96
88
  return super().__delitem__(key)
97
89
 
98
- def __contains__(self, key: _KT) -> bool:
90
+ def __contains__(self, key: KT) -> bool:
99
91
  with self._lock:
100
92
  return super().__contains__(key)
101
93
 
102
94
  @overload
103
- def get(self, key: _KT) -> Optional[_VT]: ...
95
+ def get(self, key: KT) -> VT | None: ...
104
96
  @overload
105
- def get(self, key: _KT, default: _VT) -> _VT: ...
97
+ def get(self, key: KT, default: VT) -> VT: ...
106
98
  @overload
107
- def get(self, key: _KT, default: _T) -> Union[_VT, _T]: ...
108
- def get(self, key: _KT, default: object = None) -> object:
99
+ def get[T](self, key: KT, default: T) -> VT | T: ...
100
+ def get(self, key: KT, default: object = None) -> object:
109
101
  """Return the cached value for _key_ if _key_ is in the cache, else default."""
110
102
  # NOTE: self.__getitem__ is already acquiring the lock.
111
103
  try:
@@ -113,17 +105,17 @@ class ThreadSafeLRUCache(LRUCache[_KT, _VT]):
113
105
  except KeyError:
114
106
  return default
115
107
 
116
- def keys(self) -> Iterator[_KT]:
108
+ def keys(self) -> Iterator[KT]:
117
109
  """Return an iterator over this cache's keys."""
118
110
  with self._lock:
119
111
  return super().keys()
120
112
 
121
- def values(self) -> Iterator[_VT]:
113
+ def values(self) -> Iterator[VT]:
122
114
  """Return an iterator over this cache's values."""
123
115
  with self._lock:
124
116
  return super().values()
125
117
 
126
- def items(self) -> Iterator[tuple[_KT, _VT]]:
118
+ def items(self) -> Iterator[tuple[KT, VT]]:
127
119
  """Return an iterator over this cache's key/value pairs."""
128
120
  with self._lock:
129
- return super().items()
121
+ return super().items()
liquid/_markup.py ADDED
@@ -0,0 +1,253 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Literal, Protocol
4
+
5
+ from .exceptions import DetailedLiquidError, DisabledTagError, ResourceLimitError
6
+
7
+ if TYPE_CHECKING:
8
+ from ._context import RenderContext
9
+ from ._io import StringBuffer
10
+ from ._parser import Parser
11
+ from ._template import Template
12
+ from .expressions import Name
13
+ from .tokens import Token
14
+
15
+ type Node = str | Markup
16
+ """A node in a template syntax tree."""
17
+
18
+ type Block = list[Node]
19
+ """A sequence of template syntax tree nodes."""
20
+
21
+
22
+ class Markup(Protocol):
23
+ """The interface used for *markup nodes* in a template syntax tree.
24
+
25
+ All tags and the output statement must implement this interface.
26
+ """
27
+
28
+ @property
29
+ def blank(self) -> bool:
30
+ """Return ``True`` if this node renders to an empty string or a string containing
31
+ whitespace only.
32
+
33
+ This is used to suppress control flow blocks that don't contain text or markup
34
+ that contributes to the output.
35
+ """
36
+ ...
37
+
38
+ @property
39
+ def token(self) -> Token:
40
+ """Return a representative token for this node.
41
+
42
+ In the absence of a more specific token from an enclosed expression, this token
43
+ will be used for reporting errors.
44
+ """
45
+ ...
46
+
47
+ @property
48
+ def tag(self) -> str:
49
+ """Return the name of this tag, or the empty string if this node is not a tag."""
50
+ ...
51
+
52
+ def render(self, context: RenderContext, buffer: StringBuffer) -> None:
53
+ """Render this node to `buffer` using data from `context`."""
54
+ ...
55
+
56
+ async def render_async(self, context: RenderContext, buffer: StringBuffer) -> None:
57
+ """Render this node to `buffer` asynchronously using data from `context`."""
58
+ ...
59
+
60
+ def children(self) -> list[Markup]:
61
+ """Returns markup nodes that are direct children of this node in the syntax tree.
62
+
63
+ This is used to traverse template syntax trees during static analysis.
64
+ """
65
+ ...
66
+
67
+ def expressions(self) -> list[Expression]:
68
+ """Return expression owned by this node."""
69
+ ...
70
+
71
+ def block_scope(self) -> list[Name]:
72
+ """Return variable names that are in scope for the duration of this node's block.
73
+
74
+ This is used during static analysis to isolate and report global variables.
75
+ """
76
+ ...
77
+
78
+ def template_scope(self) -> list[Name]:
79
+ """Return variable names that this tag adds to the template scope."""
80
+ ...
81
+
82
+ def partials(self, context: RenderContext) -> list[Partial]:
83
+ """Return meta data about partial templates rendered from this node."""
84
+ ...
85
+
86
+ async def partials_async(self, context: RenderContext) -> list[Partial]:
87
+ """Return meta data about partial templates rendered from this node."""
88
+ ...
89
+
90
+
91
+ class Tag(Protocol):
92
+ """The interface used for parsing tokens into markup nodes."""
93
+
94
+ @classmethod
95
+ def parse(cls, token: Token, parser: Parser) -> Markup:
96
+ """Parse tokens or text from `parser` into a markup node."""
97
+ ...
98
+
99
+
100
+ class Expression(Protocol):
101
+ @property
102
+ def token(self) -> Token:
103
+ """Return a representative token for this expression.
104
+
105
+ It could be the expression's first token or the token for this expression's
106
+ operator, for example.
107
+ """
108
+ ...
109
+
110
+ @property
111
+ def span(self) -> Token:
112
+ """Return a token spanning the entire expression."""
113
+ ...
114
+
115
+ def evaluate(self, context: RenderContext) -> object:
116
+ """Evaluate this expression using data from `context`."""
117
+ ...
118
+
119
+ async def evaluate_async(self, context: RenderContext) -> object:
120
+ """Evaluate this expression asynchronously using data from `context`."""
121
+ ...
122
+
123
+ def children(self) -> list[Expression]:
124
+ """Return this expressions direct child expressions."""
125
+ ...
126
+
127
+
128
+ type Scope = Literal[1, 2, 3]
129
+
130
+ SCOPE_SHARED: Literal[1] = 1
131
+ SCOPE_ISOLATED: Literal[2] = 2
132
+ SCOPE_INHERITED: Literal[3] = 3
133
+
134
+
135
+ class Partial:
136
+ def __init__(
137
+ self,
138
+ *,
139
+ template: Template,
140
+ scope: Scope,
141
+ in_scope: list[Name],
142
+ key: int,
143
+ ) -> None:
144
+ self.template = template
145
+ self.scope = scope
146
+ self.in_scope = in_scope
147
+ self.key = key
148
+
149
+
150
+ def render_block(block: Block, context: RenderContext, buffer: StringBuffer) -> None:
151
+ if context.limits.max_render_score or context.limits.max_render_score_cumulative:
152
+ context.render_score += len(block)
153
+ context.render_score_cumulative += len(block)
154
+
155
+ if (
156
+ context.limits.max_render_score
157
+ and context.render_score > context.limits.max_render_score
158
+ ) or (
159
+ context.limits.max_render_score_cumulative
160
+ and context.render_score_cumulative
161
+ > context.limits.max_render_score_cumulative
162
+ ):
163
+ raise ResourceLimitError("memory limits reached")
164
+
165
+ for node in block:
166
+ if isinstance(node, str):
167
+ buffer.write(node)
168
+
169
+ else:
170
+ if context.disabled_tags and node.tag in context.disabled_tags:
171
+ raise DisabledTagError(
172
+ f"{node.tag!r} is not allowed in this context",
173
+ token=node.token,
174
+ source=context.template.source,
175
+ name=context.template.name,
176
+ )
177
+
178
+ try:
179
+ node.render(context, buffer)
180
+ except DetailedLiquidError as err:
181
+ if context.capture_errors:
182
+ buffer.write(f"\n{err}\n")
183
+ context.errors.append(err)
184
+ else:
185
+ raise
186
+
187
+ if context.interrupts:
188
+ if context.interrupts[-1] == "stop":
189
+ context.interrupts.pop()
190
+ break
191
+
192
+ if (
193
+ context.limits.max_render_size
194
+ and len(buffer) > context.limits.max_render_size
195
+ ):
196
+ raise ResourceLimitError("memory limits exceeded")
197
+
198
+
199
+ async def render_block_async(
200
+ block: Block, context: RenderContext, buffer: StringBuffer
201
+ ) -> None:
202
+ if context.limits.max_render_score or context.limits.max_render_score_cumulative:
203
+ context.render_score += len(block)
204
+ context.render_score_cumulative += len(block)
205
+
206
+ if (
207
+ context.limits.max_render_score
208
+ and context.render_score > context.limits.max_render_score
209
+ ) or (
210
+ context.limits.max_render_score_cumulative
211
+ and context.render_score_cumulative
212
+ > context.limits.max_render_score_cumulative
213
+ ):
214
+ raise ResourceLimitError("memory limits reached")
215
+
216
+ for node in block:
217
+ if isinstance(node, str):
218
+ buffer.write(node)
219
+
220
+ else:
221
+ if context.disabled_tags and node.tag in context.disabled_tags:
222
+ raise DisabledTagError(
223
+ f"{node.tag!r} is not allowed in this context",
224
+ token=node.token,
225
+ source=context.template.source,
226
+ name=context.template.name,
227
+ )
228
+
229
+ try:
230
+ await node.render_async(context, buffer)
231
+ except DetailedLiquidError as err:
232
+ if context.capture_errors:
233
+ buffer.write(f"\n{err}\n")
234
+ context.errors.append(err)
235
+ else:
236
+ raise
237
+
238
+ if context.interrupts:
239
+ if context.interrupts[-1] == "stop":
240
+ context.interrupts.pop()
241
+ break
242
+
243
+ if (
244
+ context.limits.max_render_size
245
+ and len(buffer) > context.limits.max_render_size
246
+ ):
247
+ raise ResourceLimitError("memory limits exceeded")
248
+
249
+
250
+ def is_blank_block(block: Block) -> bool:
251
+ return all(
252
+ node.isspace() if isinstance(node, str) else node.blank for node in block
253
+ )
liquid/_messages.py ADDED
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ NamedTuple,
9
+ Protocol,
10
+ TypeGuard,
11
+ )
12
+
13
+ from babel.messages import Catalog
14
+
15
+ from . import expressions
16
+ from .tags import CommentTag, InlineCommentTag, TemplateComment
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Iterable, Iterator
20
+
21
+ from ._markup import Expression, Markup
22
+ from ._template import Template
23
+ from .environment import Environment
24
+
25
+
26
+ DEFAULT_KEYWORDS: dict[str, Any] = {
27
+ "t": None,
28
+ "trans": None,
29
+ "translate": None,
30
+ "gettext": None,
31
+ "ngettext": (1, 2),
32
+ "pgettext": ((1, "c"), 2),
33
+ "npgettext": ((1, "c"), 2, 3),
34
+ }
35
+
36
+ DEFAULT_COMMENT_TAGS = [
37
+ "Translators:",
38
+ ]
39
+
40
+
41
+ class Translations(Protocol):
42
+ """Message catalog interface.
43
+
44
+ An object implementing this protocol is expected to be available in a render context.
45
+
46
+ Could be a ``GNUTranslations`` instance from the ``gettext`` module, a Babel
47
+ ``Translations`` object, or any object implementing ``gettext``, ``ngettext``,
48
+ ``pgettext`` and ``npgettext`` methods.
49
+ """
50
+
51
+ def gettext(self, message: str) -> str:
52
+ """Lookup the message in the catalog."""
53
+ ...
54
+
55
+ def ngettext(self, singular: str, plural: str, n: int) -> str:
56
+ """Do plural-forms message lookup."""
57
+ ...
58
+
59
+ def pgettext(self, context: str, message: str) -> str:
60
+ """Lookup the context and message in the catalog."""
61
+ ...
62
+
63
+ def npgettext(self, context: str, singular: str, plural: str, n: int) -> str:
64
+ """Do plural-forms context and message lookup."""
65
+ ...
66
+
67
+
68
+ type MESSAGES = (
69
+ str
70
+ | tuple[str, ...]
71
+ | tuple[tuple[str, str], str]
72
+ | tuple[tuple[str, str], str, str]
73
+ )
74
+
75
+ type SPEC = (
76
+ tuple[int, ...] | tuple[tuple[int, str], int] | tuple[tuple[int, str], int, int]
77
+ )
78
+
79
+
80
+ class MessageTuple(NamedTuple):
81
+ """The tuple expected to be returned from babel extraction methods."""
82
+
83
+ lineno: int
84
+ funcname: str
85
+ message: MESSAGES
86
+ comments: list[str]
87
+
88
+
89
+ class MessageText(NamedTuple):
90
+ """Partial message tuple returned from translatable tags."""
91
+
92
+ lineno: int
93
+ funcname: str
94
+ message: MESSAGES
95
+
96
+
97
+ class TranslatableTag(Protocol):
98
+ def messages(self, template: Template) -> Iterable[MessageText]:
99
+ """Generate a sequence of translation messages."""
100
+ ...
101
+
102
+ def children(self) -> list[Markup]: ...
103
+
104
+
105
+ def translatable_tag(node: Markup) -> TypeGuard[TranslatableTag]:
106
+ return hasattr(node, "messages")
107
+
108
+
109
+ class TranslatableFilter(Protocol):
110
+ def message(
111
+ self,
112
+ left: Expression,
113
+ filter_: expressions.Filtered,
114
+ lineno: int,
115
+ ) -> MessageText | None:
116
+ """Return a translation message."""
117
+
118
+
119
+ def translatable_filter(node: object) -> TypeGuard[TranslatableFilter]:
120
+ return hasattr(node, "message")
121
+
122
+
123
+ def extract_from_templates(
124
+ *templates: Template,
125
+ keywords: dict[str, Any] | None = None,
126
+ comment_tags: list[str] | None = None,
127
+ strip_comment_tags: bool = False,
128
+ ) -> Catalog:
129
+ """Extract messages from one or more templates.
130
+
131
+ This function returns a single `babel.messages.Catalog` containing messages from all
132
+ the given templates.
133
+
134
+ Parameters
135
+ ----------
136
+ templates
137
+ Templates to extract messages from.
138
+ keywords
139
+ A Babel compatible mapping of translatable "function" names to argument specs.
140
+ The included translation filters and tag transform their messages into typical
141
+ \\*gettext format, regardless of their names.
142
+ comment_tags
143
+ A list of translator tags to search for and include in extracted messages.
144
+ strip_comment_tags
145
+ If `True`, remove comment tags from collected message comments.
146
+ """
147
+ keywords = keywords or DEFAULT_KEYWORDS
148
+ comment_tags = comment_tags or DEFAULT_COMMENT_TAGS
149
+ catalog = Catalog()
150
+ for template in templates:
151
+ for lineno, funcname, messages, comments in extract_from_template(
152
+ template, keywords, comment_tags
153
+ ):
154
+ # A partial re-implementation of Babel's messages.extract function.
155
+ # See https://github.com/python-babel/babel/blob/master/babel/messages/extract.py#L262
156
+ spec: SPEC = keywords[funcname] or (1,)
157
+ if not isinstance(messages, (list, tuple)):
158
+ messages = (messages,)
159
+ if not messages:
160
+ continue
161
+ if len(spec) != len(messages):
162
+ continue
163
+
164
+ # Assumes context is the first item in the spec
165
+ if isinstance(spec[0], tuple):
166
+ # context aware message
167
+ context = messages[spec[0][0] - 1][0]
168
+ message = [messages[i - 1] for i in spec[1:]]
169
+ else:
170
+ context = None
171
+ message = [messages[i - 1] for i in spec if isinstance(i, int)]
172
+
173
+ if not message[0]:
174
+ # Empty message
175
+ continue
176
+
177
+ if strip_comment_tags:
178
+ comments = _strip_comment_tags(comments, comment_tags)
179
+
180
+ # Use the template's path if it has one
181
+ template_name = template.name
182
+ if isinstance(template.path, Path):
183
+ template_name = str(template.path.joinpath(template_name))
184
+ elif isinstance(template.path, str):
185
+ template_name = os.path.join(template.path, template_name)
186
+
187
+ catalog.add(
188
+ message[0] if len(message) == 1 else message, # type: ignore
189
+ "",
190
+ [(template_name, lineno)],
191
+ auto_comments=comments,
192
+ context=context,
193
+ )
194
+
195
+ return catalog
196
+
197
+
198
+ def extract_from_template(
199
+ template: Template,
200
+ keywords: list[str] | dict[str, Any] | None = None,
201
+ comment_tags: list[str] | None = None,
202
+ ) -> Iterator[MessageTuple]:
203
+ """Extract translation messages from a Liquid template."""
204
+ comment_tags_ = comment_tags or DEFAULT_COMMENT_TAGS
205
+ comments_: list[tuple[int, str]] = []
206
+ keywords_ = keywords or DEFAULT_KEYWORDS
207
+
208
+ def visit_expression(expr: Expression, lineno: int) -> Iterator[MessageTuple]:
209
+ if isinstance(expr, expressions.Filtered):
210
+ for _lineno, funcname, message in _extract_from_filters(
211
+ template.env, expr, lineno, keywords_
212
+ ):
213
+ if comments_ and comments_[-1][0] < lineno - 1:
214
+ comments_.clear()
215
+
216
+ yield MessageTuple(
217
+ lineno=_lineno,
218
+ funcname=funcname,
219
+ message=message,
220
+ comments=[text for _, text in comments_],
221
+ )
222
+ comments_.clear()
223
+
224
+ for expression in expr.children():
225
+ yield from visit_expression(expression, lineno)
226
+
227
+ def visit(node: Markup) -> Iterator[MessageTuple]:
228
+ lineno, _ = template.line_col(node.token)
229
+ if (
230
+ isinstance(node, (CommentTag, InlineCommentTag, TemplateComment))
231
+ and node.text
232
+ ):
233
+ comment_text = node.text.strip()
234
+
235
+ for comment_tag in comment_tags_:
236
+ if comment_text.startswith(comment_tag):
237
+ # Our multi-line comments are wrapped in a tag, so we're
238
+ # only ever going to have one comment text object to deal
239
+ # with.
240
+ comments_.clear()
241
+ comments_.append((lineno, comment_text))
242
+ break
243
+
244
+ elif translatable_tag(node):
245
+ for lineno, funcname, message in node.messages(template):
246
+ if comments_ and comments_[-1][0] < lineno - 1:
247
+ comments_.clear()
248
+
249
+ yield MessageTuple(
250
+ lineno=lineno,
251
+ funcname=funcname,
252
+ message=message,
253
+ comments=[text for _, text in comments_],
254
+ )
255
+ comments_.clear()
256
+
257
+ for child in node.children():
258
+ for expr in child.expressions():
259
+ yield from visit_expression(expr, template.line_col(expr.token)[0])
260
+ yield from visit(child)
261
+
262
+ for node in template.nodes:
263
+ if not isinstance(node, str):
264
+ for expr in node.expressions():
265
+ yield from visit_expression(expr, template.line_col(expr.token)[0])
266
+ yield from visit(node)
267
+
268
+
269
+ def _extract_from_filters(
270
+ environment: Environment,
271
+ expression: expressions.Filtered,
272
+ lineno: int,
273
+ keywords: list[str] | dict[str, Any],
274
+ ) -> Iterable[MessageText]:
275
+ if expression.name.value in keywords:
276
+ filter_callable = environment.filters.get(expression.name.value)
277
+ if translatable_filter(filter_callable):
278
+ msg = filter_callable.message(expression.left, expression, lineno)
279
+ if msg:
280
+ yield msg
281
+
282
+
283
+ def _strip_comment_tags(comments: list[str], tags: list[str]) -> list[str]:
284
+ """Similar to Babel's messages.extract._strip_comment_tags."""
285
+
286
+ def _strip(line: str) -> str:
287
+ for tag in tags:
288
+ if line.startswith(tag):
289
+ return line[len(tag) :].strip()
290
+ return line
291
+
292
+ return [_strip(comment) for comment in comments]
liquid/_missing.py ADDED
@@ -0,0 +1,3 @@
1
+ from ._nothing import NOTHING
2
+
3
+ MISSING = NOTHING