python-liquid 1.13.0__py3-none-any.whl → 2.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 (135) hide show
  1. liquid/__init__.py +192 -31
  2. liquid/analyze_tags.py +28 -23
  3. liquid/ast.py +153 -153
  4. liquid/builtin/__init__.py +34 -5
  5. liquid/builtin/content.py +41 -0
  6. liquid/builtin/expressions/__init__.py +47 -0
  7. liquid/builtin/expressions/_tokenize.py +186 -0
  8. liquid/builtin/expressions/arguments.py +207 -0
  9. liquid/builtin/expressions/filtered.py +387 -0
  10. liquid/builtin/expressions/logical.py +642 -0
  11. liquid/builtin/expressions/loop.py +278 -0
  12. liquid/builtin/expressions/path.py +163 -0
  13. liquid/builtin/expressions/primitive.py +417 -0
  14. liquid/builtin/filters/array.py +144 -65
  15. liquid/builtin/filters/extra.py +2 -5
  16. liquid/builtin/filters/math.py +7 -12
  17. liquid/builtin/filters/misc.py +5 -8
  18. liquid/builtin/filters/string.py +37 -25
  19. liquid/builtin/illegal.py +10 -6
  20. liquid/builtin/loaders/__init__.py +0 -133
  21. liquid/builtin/loaders/caching_file_system_loader.py +9 -27
  22. liquid/builtin/loaders/choice_loader.py +27 -76
  23. liquid/builtin/loaders/dict_loader.py +64 -0
  24. liquid/builtin/loaders/file_system_loader.py +36 -53
  25. liquid/builtin/loaders/mixins.py +63 -118
  26. liquid/builtin/loaders/package_loader.py +30 -20
  27. liquid/builtin/output.py +74 -0
  28. liquid/builtin/tags/assign_tag.py +42 -58
  29. liquid/builtin/tags/capture_tag.py +45 -55
  30. liquid/builtin/tags/case_tag.py +218 -148
  31. liquid/builtin/tags/comment_tag.py +32 -50
  32. liquid/builtin/tags/cycle_tag.py +72 -86
  33. liquid/builtin/tags/decrement_tag.py +28 -40
  34. liquid/builtin/tags/doc_tag.py +68 -0
  35. liquid/builtin/tags/echo_tag.py +24 -25
  36. liquid/builtin/tags/for_tag.py +177 -213
  37. liquid/builtin/tags/if_tag.py +100 -151
  38. liquid/builtin/tags/ifchanged_tag.py +33 -33
  39. liquid/builtin/tags/include_tag.py +132 -137
  40. liquid/builtin/tags/increment_tag.py +29 -36
  41. liquid/builtin/tags/inline_comment_tag.py +24 -11
  42. liquid/builtin/tags/liquid_tag.py +128 -38
  43. liquid/builtin/tags/render_tag.py +136 -143
  44. liquid/builtin/tags/tablerow_tag.py +57 -54
  45. liquid/builtin/tags/unless_tag.py +109 -145
  46. liquid/context.py +238 -397
  47. liquid/environment.py +182 -370
  48. liquid/exceptions.py +129 -86
  49. liquid/expression.py +23 -1154
  50. liquid/extra/__init__.py +44 -57
  51. liquid/extra/filters/__init__.py +20 -0
  52. liquid/extra/filters/_json.py +1 -2
  53. liquid/extra/filters/array.py +3 -4
  54. liquid/extra/filters/babel.py +552 -0
  55. liquid/extra/filters/translate.py +448 -0
  56. liquid/extra/tags/__init__.py +8 -20
  57. liquid/extra/tags/_with.py +39 -50
  58. liquid/extra/tags/extends_tag.py +577 -0
  59. liquid/extra/tags/macro_tag.py +268 -0
  60. liquid/extra/tags/translate_tag.py +388 -0
  61. liquid/filter.py +24 -18
  62. liquid/future/environment.py +10 -15
  63. liquid/future/filters/__init__.py +1 -3
  64. liquid/future/tags/__init__.py +1 -11
  65. liquid/golden/__init__.py +10 -0
  66. liquid/golden/case.py +3 -3
  67. liquid/golden/comment_tag.py +92 -0
  68. liquid/golden/doc_tag.py +61 -0
  69. liquid/golden/find_filter.py +90 -0
  70. liquid/golden/find_index_filter.py +96 -0
  71. liquid/golden/has_filter.py +139 -0
  72. liquid/golden/if_tag.py +135 -0
  73. liquid/golden/output_statement.py +57 -2
  74. liquid/golden/range_objects.py +43 -0
  75. liquid/golden/reject_filter.py +268 -0
  76. liquid/golden/split_filter.py +31 -0
  77. liquid/lex.py +115 -388
  78. liquid/limits.py +2 -1
  79. liquid/loader.py +141 -0
  80. liquid/messages.py +357 -0
  81. liquid/output.py +10 -1
  82. liquid/parser.py +113 -0
  83. liquid/span.py +35 -0
  84. liquid/static_analysis.py +286 -826
  85. liquid/stream.py +127 -64
  86. liquid/tag.py +12 -11
  87. liquid/template.py +295 -145
  88. liquid/token.py +29 -69
  89. liquid/undefined.py +78 -18
  90. liquid/utils/__init__.py +5 -1
  91. liquid/utils/html.py +2 -4
  92. liquid/utils/lru_cache.py +129 -0
  93. python_liquid-2.0.0.dist-info/METADATA +169 -0
  94. python_liquid-2.0.0.dist-info/RECORD +182 -0
  95. liquid/builtin/literal.py +0 -47
  96. liquid/builtin/loaders/base_loader.py +0 -302
  97. liquid/builtin/statement.py +0 -65
  98. liquid/expressions/__init__.py +0 -29
  99. liquid/expressions/arguments/__init__.py +0 -11
  100. liquid/expressions/arguments/lex.py +0 -105
  101. liquid/expressions/arguments/parse.py +0 -140
  102. liquid/expressions/boolean/__init__.py +0 -11
  103. liquid/expressions/boolean/lex.py +0 -188
  104. liquid/expressions/boolean/parse.py +0 -290
  105. liquid/expressions/common.py +0 -402
  106. liquid/expressions/conditional/__init__.py +0 -11
  107. liquid/expressions/conditional/lex.py +0 -196
  108. liquid/expressions/conditional/parse.py +0 -223
  109. liquid/expressions/filtered/__init__.py +0 -7
  110. liquid/expressions/filtered/lex.py +0 -109
  111. liquid/expressions/filtered/parse.py +0 -263
  112. liquid/expressions/include/__init__.py +0 -3
  113. liquid/expressions/include/lex.py +0 -109
  114. liquid/expressions/loop/__init__.py +0 -7
  115. liquid/expressions/loop/lex.py +0 -105
  116. liquid/expressions/loop/parse.py +0 -138
  117. liquid/expressions/stream.py +0 -106
  118. liquid/extra/tags/extends.py +0 -558
  119. liquid/extra/tags/if_expressions.py +0 -85
  120. liquid/extra/tags/if_not.py +0 -24
  121. liquid/extra/tags/macro.py +0 -326
  122. liquid/future/filters/_split.py +0 -23
  123. liquid/future/tags/_case_tag.py +0 -182
  124. liquid/future/tags/_if_tag.py +0 -8
  125. liquid/future/tags/_tablerow_tag.py +0 -14
  126. liquid/future/tags/_unless_tag.py +0 -8
  127. liquid/loaders.py +0 -30
  128. liquid/parse.py +0 -791
  129. liquid/utils/cache.py +0 -194
  130. liquid/utils/cache.pyi +0 -13
  131. python_liquid-1.13.0.dist-info/METADATA +0 -186
  132. python_liquid-1.13.0.dist-info/RECORD +0 -191
  133. /liquid/{chain_map.py → utils/chain_map.py} +0 -0
  134. {python_liquid-1.13.0.dist-info → python_liquid-2.0.0.dist-info}/WHEEL +0 -0
  135. {python_liquid-1.13.0.dist-info → python_liquid-2.0.0.dist-info}/licenses/LICENSE +0 -0
liquid/__init__.py CHANGED
@@ -1,34 +1,26 @@
1
- try:
2
- from markupsafe import Markup
3
- from markupsafe import escape
4
- from markupsafe import soft_str
5
- except ImportError:
6
- from liquid.exceptions import Markup # type: ignore
7
- from liquid.exceptions import escape # type: ignore
1
+ from pathlib import Path
2
+ from typing import Iterable
3
+ from typing import Iterator
4
+ from typing import Optional
5
+ from typing import TextIO
6
+ from typing import Union
8
7
 
9
- soft_str = str # type: ignore
8
+ from markupsafe import Markup
9
+ from markupsafe import escape
10
+ from markupsafe import soft_str
10
11
 
11
12
  from .mode import Mode
12
13
  from .token import Token
13
14
  from .expression import Expression
14
15
 
15
- from .loaders import CachingChoiceLoader
16
- from .loaders import CachingFileSystemLoader
17
- from .loaders import ChoiceLoader
18
- from .loaders import DictLoader
19
- from .loaders import FileExtensionLoader
20
- from .loaders import FileSystemLoader
21
- from .loaders import PackageLoader
22
- from .loaders import make_choice_loader
23
- from .loaders import make_file_system_loader
24
-
25
- from .context import Context
26
- from .context import DebugUndefined
27
- from .context import is_undefined
16
+ from .context import RenderContext
28
17
  from .context import FutureContext
29
- from .context import StrictDefaultUndefined
30
- from .context import StrictUndefined
31
- from .context import Undefined
18
+ from .undefined import DebugUndefined
19
+ from .undefined import is_undefined
20
+ from .undefined import FalsyStrictUndefined
21
+ from .undefined import StrictDefaultUndefined
22
+ from .undefined import StrictUndefined
23
+ from .undefined import Undefined
32
24
 
33
25
  from .environment import Environment
34
26
  from .environment import Template
@@ -38,31 +30,52 @@ from .template import BoundTemplate
38
30
  from .template import FutureAwareBoundTemplate
39
31
  from .template import FutureBoundTemplate
40
32
 
33
+ from .builtin import CachingDictLoader
34
+ from .builtin import DictLoader
35
+ from .builtin import ChoiceLoader
36
+ from .builtin import CachingChoiceLoader
37
+ from .builtin import CachingFileSystemLoader
38
+ from .builtin import FileSystemLoader
39
+ from .builtin import PackageLoader
40
+ from .builtin import CachingLoaderMixin
41
+ from .loader import BaseLoader
42
+
43
+ from .ast import Node
44
+ from .ast import BlockNode
45
+ from .ast import ConditionalBlockNode
46
+
41
47
  from .analyze_tags import TagAnalysis
42
48
  from .analyze_tags import DEFAULT_INNER_TAG_MAP
43
49
 
44
- from .static_analysis import TemplateAnalysis
45
- from .static_analysis import ContextualTemplateAnalysis
50
+ from .messages import MessageTuple
51
+ from .messages import Translations
52
+ from .messages import extract_from_template
53
+
54
+ from .stream import TokenStream
55
+ from .tag import Tag
46
56
 
47
57
  from . import future
48
58
 
49
- __version__ = "1.13.0"
59
+ __version__ = "2.0.0"
50
60
 
51
61
  __all__ = (
52
62
  "AwareBoundTemplate",
63
+ "BlockNode",
53
64
  "BoundTemplate",
54
65
  "CachingChoiceLoader",
66
+ "CachingDictLoader",
55
67
  "CachingFileSystemLoader",
68
+ "CachingLoaderMixin",
56
69
  "ChoiceLoader",
57
- "Context",
58
- "ContextualTemplateAnalysis",
70
+ "ConditionalBlockNode",
59
71
  "DebugUndefined",
60
72
  "DEFAULT_INNER_TAG_MAP",
61
73
  "DictLoader",
62
74
  "Environment",
63
75
  "escape",
64
76
  "Expression",
65
- "FileExtensionLoader",
77
+ "extract_from_template",
78
+ "FalsyStrictUndefined",
66
79
  "FileSystemLoader",
67
80
  "future",
68
81
  "FutureAwareBoundTemplate",
@@ -72,14 +85,162 @@ __all__ = (
72
85
  "make_choice_loader",
73
86
  "make_file_system_loader",
74
87
  "Markup",
88
+ "MessageTuple",
75
89
  "Mode",
90
+ "Node",
76
91
  "PackageLoader",
92
+ "parse",
93
+ "render_async",
94
+ "render",
95
+ "RenderContext",
77
96
  "soft_str",
78
97
  "StrictDefaultUndefined",
79
98
  "StrictUndefined",
99
+ "Tag",
80
100
  "TagAnalysis",
81
101
  "Template",
82
- "TemplateAnalysis",
83
102
  "Token",
103
+ "TokenStream",
104
+ "Translations",
84
105
  "Undefined",
85
106
  )
107
+
108
+ DEFAULT_ENVIRONMENT = Environment()
109
+
110
+
111
+ def parse(source: str) -> BoundTemplate:
112
+ """Parse template source text using the default environment."""
113
+ return DEFAULT_ENVIRONMENT.from_string(source)
114
+
115
+
116
+ def render(source: str, **data: object) -> str:
117
+ """Parse and render source text using the default environment."""
118
+ return DEFAULT_ENVIRONMENT.render(source, **data)
119
+
120
+
121
+ async def render_async(source: str, **data: object) -> str:
122
+ """Parse and render source text using the default environment."""
123
+ return await DEFAULT_ENVIRONMENT.render_async(source, **data)
124
+
125
+
126
+ def make_file_system_loader(
127
+ search_path: Union[str, Path, Iterable[Union[str, Path]]],
128
+ *,
129
+ encoding: str = "utf-8",
130
+ ext: str = ".liquid",
131
+ auto_reload: bool = True,
132
+ namespace_key: str = "",
133
+ cache_size: int = 300,
134
+ ) -> BaseLoader:
135
+ """A _file system_ template loader factory.
136
+
137
+ Returns one of `CachingFileSystemLoader` or `FileSystemLoader` depending in
138
+ the given arguments.
139
+
140
+ A `CachingFileSystemLoader` is returned if _cache_size_ is greater than 0.
141
+ Otherwise a `FileExtensionLoader` is returned if _ext_ is not empty.
142
+ If _ext_ is empty, a `FileSystemLoader` is returned.
143
+
144
+ _auto_reload_ and _namespace_key_ are ignored if _cache_key_ is less than 1.
145
+
146
+ Args:
147
+ search_path: One or more paths to search.
148
+ encoding: Open template files with the given encoding.
149
+ ext: A default file extension. Should include a leading period.
150
+ auto_reload: If `True`, automatically reload a cached template if it has been
151
+ updated.
152
+ namespace_key: The name of a global render context variable or loader keyword
153
+ argument that resolves to the current loader "namespace" or "scope".
154
+
155
+ If you're developing a multi-user application, a good namespace might be
156
+ `uid`, where `uid` is a unique identifier for a user and templates are
157
+ arranged in folders named for each `uid` inside the search path.
158
+ cache_size: The maximum number of templates to hold in the cache before removing
159
+ the least recently used template.
160
+
161
+ _New in version 1.12.0_
162
+ """
163
+ if cache_size > 0:
164
+ return CachingFileSystemLoader(
165
+ search_path=search_path,
166
+ encoding=encoding,
167
+ ext=ext,
168
+ auto_reload=auto_reload,
169
+ namespace_key=namespace_key,
170
+ capacity=cache_size,
171
+ )
172
+
173
+ return FileSystemLoader(search_path=search_path, encoding=encoding, ext=ext)
174
+
175
+
176
+ def make_choice_loader(
177
+ loaders: list[BaseLoader],
178
+ *,
179
+ auto_reload: bool = True,
180
+ namespace_key: str = "",
181
+ cache_size: int = 300,
182
+ ) -> BaseLoader:
183
+ """A _choice loader_ factory.
184
+
185
+ Returns one of `CachingChoiceLoader` or `ChoiceLoader` depending on the
186
+ given arguments.
187
+
188
+ A `CachingChoiceLoader` is returned if _cache_size_ > 0, otherwise a
189
+ `ChoiceLoader` is returned.
190
+
191
+ _auto_reload_ and _namespace_key_ are ignored if _cache_key_ is less than 1.
192
+
193
+ Args:
194
+ loaders: A list of loaders implementing `liquid.loaders.BaseLoader`.
195
+ auto_reload: If `True`, automatically reload a cached template if it
196
+ has been updated.
197
+ namespace_key: The name of a global render context variable or loader
198
+ keyword argument that resolves to the current loader "namespace" or
199
+ "scope".
200
+ cache_size: The maximum number of templates to hold in the cache before
201
+ removing the least recently used template.
202
+
203
+ _New in version 1.12.0_
204
+ """
205
+ if cache_size > 0:
206
+ return CachingChoiceLoader(
207
+ loaders=loaders,
208
+ auto_reload=auto_reload,
209
+ namespace_key=namespace_key,
210
+ capacity=cache_size,
211
+ )
212
+
213
+ return ChoiceLoader(loaders=loaders)
214
+
215
+
216
+ def extract_liquid(
217
+ fileobj: TextIO,
218
+ keywords: list[str],
219
+ comment_tags: Optional[list[str]] = None,
220
+ options: Optional[dict[object, object]] = None, # noqa: ARG001
221
+ ) -> Iterator[MessageTuple]:
222
+ """A babel compatible translation message extraction method for Liquid templates.
223
+
224
+ See https://babel.pocoo.org/en/latest/messages.html
225
+
226
+ Keywords are the names of Liquid filters or tags operating on translatable
227
+ strings. For a filter to contribute to message extraction, it must also
228
+ appear as a child of a `FilteredExpression` and be a `TranslatableFilter`.
229
+ Similarly, tags must produce a node that is a `TranslatableTag`.
230
+
231
+ Where a Liquid comment contains a prefix in `comment_tags`, the comment
232
+ will be attached to the translatable filter or tag immediately following
233
+ the comment. Python Liquid's non-standard shorthand comments are not
234
+ supported.
235
+
236
+ Options are arguments passed to the `liquid.Template` constructor with the
237
+ contents of `fileobj` as the template's source. Use `extract_from_template`
238
+ to extract messages from an existing template bound to an existing
239
+ environment.
240
+ """
241
+ template = Environment(extra=True).parse(fileobj.read())
242
+ return extract_from_template(
243
+ template=template,
244
+ keywords=keywords,
245
+ comment_tags=comment_tags,
246
+ )
liquid/analyze_tags.py CHANGED
@@ -1,20 +1,20 @@
1
1
  """Analyze template tags from tokenized source text."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from collections import defaultdict
5
6
  from typing import TYPE_CHECKING
6
- from typing import Dict
7
+ from typing import DefaultDict
7
8
  from typing import Iterable
8
- from typing import List
9
9
  from typing import Mapping
10
10
  from typing import Optional
11
- from typing import Tuple
12
11
 
13
- from liquid.token import TOKEN_TAG
12
+ from .span import Span
13
+ from .token import TOKEN_TAG
14
14
 
15
15
  if TYPE_CHECKING:
16
- from liquid import Environment
17
- from liquid.token import Token
16
+ from .environment import Environment
17
+ from .token import Token
18
18
 
19
19
 
20
20
  DEFAULT_INNER_TAG_MAP = {
@@ -24,15 +24,16 @@ DEFAULT_INNER_TAG_MAP = {
24
24
  "unless": ["else", "elsif"],
25
25
  }
26
26
 
27
+
27
28
  InnerTagMap = Mapping[str, Iterable[str]]
28
- TagMap = Dict[str, List[Tuple[str, int]]]
29
+ TagMap = dict[str, list[Span]]
29
30
 
30
31
 
31
32
  class TagAnalysis:
32
33
  """The result of analyzing a template's tags with `Environment.analyze_tags()`.
33
34
 
34
35
  Each of the following properties maps tag names to a list of their locations.
35
- Locations are (template_name, line_number) tuples.
36
+ Locations are (template_name, start_index) tuples.
36
37
 
37
38
  Note that `raw` tags are not included at all. The lexer converts them to text
38
39
  tokens before we get a chance to analyze them.
@@ -58,7 +59,7 @@ class TagAnalysis:
58
59
  *,
59
60
  env: Environment,
60
61
  name: str,
61
- tokens: List[Token],
62
+ tokens: list[Token],
62
63
  inner_tags: Optional[InnerTagMap] = None,
63
64
  ) -> None:
64
65
  self.template_name = name
@@ -86,20 +87,20 @@ class TagAnalysis:
86
87
  self.unknown_tags,
87
88
  ) = self._audit_tags(env, tokens)
88
89
 
89
- def _all_tags(self, tokens: List[Token]) -> TagMap:
90
+ def _all_tags(self, tokens: list[Token]) -> TagMap:
90
91
  """Map tag names to their locations, similar to `Template.analyze` etc."""
91
- tags = defaultdict(list)
92
+ tags: DefaultDict[str, list[Span]] = defaultdict(list)
92
93
  for token in tokens:
93
- if token.type == TOKEN_TAG:
94
- tags[token.value].append((self.template_name, token.linenum))
94
+ if token.kind == TOKEN_TAG:
95
+ tags[token.value].append(Span(self.template_name, token.start_index))
95
96
  return dict(tags)
96
97
 
97
98
  def _audit_tags( # noqa: PLR0912
98
99
  self,
99
100
  env: Environment,
100
- tokens: List[Token],
101
- ) -> Tuple[TagMap, TagMap, TagMap]:
102
- block_stack: List[_BlockStackItem] = []
101
+ tokens: list[Token],
102
+ ) -> tuple[TagMap, TagMap, TagMap]:
103
+ block_stack: list[_BlockStackItem] = []
103
104
  unclosed_tags: TagMap = defaultdict(list)
104
105
  unexpected_tags: TagMap = defaultdict(list)
105
106
  unknown_tags: TagMap = defaultdict(list)
@@ -108,7 +109,7 @@ class TagAnalysis:
108
109
  end_tags = {
109
110
  token.value
110
111
  for token in tokens
111
- if token.type == TOKEN_TAG and token.value.startswith("end")
112
+ if token.kind == TOKEN_TAG and token.value.startswith("end")
112
113
  }
113
114
 
114
115
  # Infer which tags are block tags. This may or may not match what the
@@ -131,7 +132,7 @@ class TagAnalysis:
131
132
  }
132
133
 
133
134
  for token in tokens:
134
- if token.type != TOKEN_TAG:
135
+ if token.kind != TOKEN_TAG:
135
136
  continue
136
137
 
137
138
  tag_name = token.value
@@ -143,7 +144,7 @@ class TagAnalysis:
143
144
  if start_block_tag != tag_name[3:]:
144
145
  # if start_block_tag.name not in inline_tags:
145
146
  unclosed_tags[start_block_tag.name].append(
146
- (self.template_name, start_block_tag.token.linenum)
147
+ Span(self.template_name, start_block_tag.token.start_index)
147
148
  )
148
149
  continue
149
150
 
@@ -152,18 +153,22 @@ class TagAnalysis:
152
153
  enclosing_tags: Iterable[str] = self._inner_tags.get(tag_name, [])
153
154
  if not enclosing_tags:
154
155
  # Not an inner tag for any block.
155
- unknown_tags[tag_name].append((self.template_name, token.linenum))
156
+ unknown_tags[tag_name].append(
157
+ Span(self.template_name, token.start_index)
158
+ )
156
159
  elif not self._valid_inner_tag(
157
160
  self._inner_tags.get(tag_name, []), block_stack
158
161
  ):
159
162
  # An inner tag, but not valid for any blocks currently on the stack.
160
163
  unexpected_tags[tag_name].append(
161
- (self.template_name, token.linenum)
164
+ Span(self.template_name, token.start_index)
162
165
  )
163
166
 
164
167
  # Catch any unclosed tags.
165
168
  for block in block_stack:
166
- unclosed_tags[block.name].append((self.template_name, block.token.linenum))
169
+ unclosed_tags[block.name].append(
170
+ Span(self.template_name, block.token.start_index)
171
+ )
167
172
 
168
173
  # Catch bad "end" tags.
169
174
  for tag_name, locations in self.all_tags.items():
@@ -184,7 +189,7 @@ class TagAnalysis:
184
189
  def _valid_inner_tag(
185
190
  self,
186
191
  tag_names: Iterable[str],
187
- block_stack: List[_BlockStackItem],
192
+ block_stack: list[_BlockStackItem],
188
193
  ) -> bool:
189
194
  return any(tag_name in block_stack for tag_name in tag_names)
190
195