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.
- liquid/__about__.py +1 -0
- liquid/__init__.py +161 -187
- liquid/_base_tag.py +66 -0
- liquid/{utils/chain_map.py → _chain_map.py} +18 -19
- liquid/_context.py +465 -0
- liquid/_drop.py +108 -0
- liquid/_filter.py +174 -0
- liquid/_io.py +18 -0
- liquid/_lax_parser_mixin.py +100 -0
- liquid/_lexer.py +93 -0
- liquid/_lexer_extended.py +623 -0
- liquid/_lexer_legacy.py +536 -0
- liquid/_loader.py +84 -0
- liquid/{utils/lru_cache.py → _lru_cache.py} +28 -36
- liquid/_markup.py +253 -0
- liquid/_messages.py +292 -0
- liquid/_missing.py +3 -0
- liquid/_nothing.py +28 -0
- liquid/_parser.py +460 -0
- liquid/_parser_extended.py +293 -0
- liquid/_parser_legacy.py +819 -0
- liquid/_resource_limits.py +50 -0
- liquid/_slice.py +41 -0
- liquid/_static_analysis.py +503 -0
- liquid/_strftime.py +186 -0
- liquid/_template.py +536 -0
- liquid/drops/__init__.py +20 -0
- liquid/drops/forloop.py +78 -0
- liquid/drops/iterable.py +25 -0
- liquid/drops/range.py +76 -0
- liquid/drops/self_.py +45 -0
- liquid/drops/tablerowloop.py +142 -0
- liquid/drops/undefined.py +228 -0
- liquid/environment.py +717 -551
- liquid/exceptions.py +67 -214
- liquid/expressions.py +1184 -0
- liquid/filters/__init__.py +134 -0
- liquid/filters/abs.py +7 -0
- liquid/filters/append.py +5 -0
- liquid/filters/at_least.py +9 -0
- liquid/filters/at_most.py +9 -0
- liquid/filters/babel.py +560 -0
- liquid/filters/base64.py +26 -0
- liquid/filters/capitalize.py +5 -0
- liquid/filters/ceil.py +8 -0
- liquid/filters/compact.py +25 -0
- liquid/filters/concat.py +11 -0
- liquid/filters/date.py +27 -0
- liquid/filters/default.py +25 -0
- liquid/filters/divided_by.py +21 -0
- liquid/filters/downcase.py +5 -0
- liquid/filters/escape.py +56 -0
- liquid/filters/find.py +56 -0
- liquid/filters/first.py +19 -0
- liquid/filters/floor.py +8 -0
- liquid/filters/html.py +23 -0
- liquid/filters/index.py +12 -0
- liquid/filters/join.py +13 -0
- liquid/filters/json_.py +30 -0
- liquid/filters/last.py +17 -0
- liquid/filters/map.py +5 -0
- liquid/filters/minus.py +13 -0
- liquid/filters/modulo.py +18 -0
- liquid/filters/newline_to_br.py +16 -0
- liquid/filters/plus.py +13 -0
- liquid/filters/prepend.py +5 -0
- liquid/filters/reject.py +19 -0
- liquid/filters/remove.py +36 -0
- liquid/filters/replace.py +32 -0
- liquid/filters/reverse.py +5 -0
- liquid/filters/round.py +35 -0
- liquid/filters/size.py +25 -0
- liquid/filters/slice.py +39 -0
- liquid/filters/sort.py +148 -0
- liquid/filters/split.py +34 -0
- liquid/filters/squish.py +9 -0
- liquid/filters/strip.py +13 -0
- liquid/filters/strip_html.py +19 -0
- liquid/filters/strip_newlines.py +17 -0
- liquid/filters/sum.py +21 -0
- liquid/filters/times.py +13 -0
- liquid/filters/translate.py +380 -0
- liquid/filters/truncate.py +39 -0
- liquid/filters/uniq.py +11 -0
- liquid/filters/upcase.py +5 -0
- liquid/filters/url.py +18 -0
- liquid/filters/where.py +18 -0
- liquid/loaders/__init__.py +17 -0
- liquid/loaders/caching_file_system_loader.py +64 -0
- liquid/loaders/choice_loader.py +93 -0
- liquid/loaders/dict_loader.py +56 -0
- liquid/{builtin/loaders → loaders}/file_system_loader.py +51 -55
- liquid/{builtin/loaders → loaders}/mixins.py +39 -51
- liquid/loaders/package_loader.py +110 -0
- liquid/tags/__init__.py +61 -0
- liquid/tags/assign.py +68 -0
- liquid/tags/capture.py +83 -0
- liquid/tags/case.py +199 -0
- liquid/tags/comment.py +56 -0
- liquid/tags/cycle.py +131 -0
- liquid/tags/decrement.py +55 -0
- liquid/tags/doc.py +56 -0
- liquid/tags/echo.py +65 -0
- liquid/tags/else_.py +53 -0
- liquid/tags/error.py +51 -0
- liquid/tags/extends.py +448 -0
- liquid/tags/for_.py +421 -0
- liquid/tags/if_.py +156 -0
- liquid/tags/ifchanged.py +69 -0
- liquid/tags/include.py +223 -0
- liquid/tags/increment.py +55 -0
- liquid/tags/inline_comment.py +70 -0
- liquid/tags/liquid.py +56 -0
- liquid/tags/macro.py +281 -0
- liquid/tags/output.py +58 -0
- liquid/tags/raw.py +60 -0
- liquid/tags/render.py +245 -0
- liquid/tags/tablerow.py +254 -0
- liquid/tags/template_comment.py +56 -0
- liquid/tags/translate.py +316 -0
- liquid/tags/unless.py +121 -0
- liquid/tags/with_.py +83 -0
- liquid/tokens.py +370 -0
- {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/METADATA +32 -39
- python_liquid-3.0.0.dist-info/RECORD +128 -0
- {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/WHEEL +1 -1
- {python_liquid-2.3.6.dist-info → python_liquid-3.0.0.dist-info}/licenses/LICENSE +1 -1
- liquid/analyze_tags.py +0 -214
- liquid/ast.py +0 -252
- liquid/builtin/__init__.py +0 -203
- liquid/builtin/content.py +0 -41
- liquid/builtin/drops/__init__.py +0 -3
- liquid/builtin/drops/drops.py +0 -13
- liquid/builtin/expressions/__init__.py +0 -47
- liquid/builtin/expressions/_tokenize.py +0 -186
- liquid/builtin/expressions/arguments.py +0 -207
- liquid/builtin/expressions/filtered.py +0 -387
- liquid/builtin/expressions/logical.py +0 -672
- liquid/builtin/expressions/loop.py +0 -275
- liquid/builtin/expressions/path.py +0 -163
- liquid/builtin/expressions/primitive.py +0 -417
- liquid/builtin/filters/__init__.py +0 -0
- liquid/builtin/filters/array.py +0 -329
- liquid/builtin/filters/extra.py +0 -83
- liquid/builtin/filters/math.py +0 -129
- liquid/builtin/filters/misc.py +0 -137
- liquid/builtin/filters/string.py +0 -416
- liquid/builtin/illegal.py +0 -28
- liquid/builtin/loaders/__init__.py +0 -0
- liquid/builtin/loaders/caching_file_system_loader.py +0 -61
- liquid/builtin/loaders/choice_loader.py +0 -94
- liquid/builtin/loaders/dict_loader.py +0 -64
- liquid/builtin/loaders/package_loader.py +0 -132
- liquid/builtin/output.py +0 -74
- liquid/builtin/tags/__init__.py +0 -0
- liquid/builtin/tags/assign_tag.py +0 -75
- liquid/builtin/tags/capture_tag.py +0 -98
- liquid/builtin/tags/case_tag.py +0 -287
- liquid/builtin/tags/comment_tag.py +0 -77
- liquid/builtin/tags/cycle_tag.py +0 -132
- liquid/builtin/tags/decrement_tag.py +0 -59
- liquid/builtin/tags/doc_tag.py +0 -68
- liquid/builtin/tags/echo_tag.py +0 -45
- liquid/builtin/tags/for_tag.py +0 -337
- liquid/builtin/tags/if_tag.py +0 -209
- liquid/builtin/tags/ifchanged_tag.py +0 -94
- liquid/builtin/tags/include_tag.py +0 -251
- liquid/builtin/tags/increment_tag.py +0 -60
- liquid/builtin/tags/inline_comment_tag.py +0 -47
- liquid/builtin/tags/liquid_tag.py +0 -183
- liquid/builtin/tags/render_tag.py +0 -371
- liquid/builtin/tags/tablerow_tag.py +0 -320
- liquid/builtin/tags/unless_tag.py +0 -218
- liquid/context.py +0 -615
- liquid/expression.py +0 -41
- liquid/extra/__init__.py +0 -98
- liquid/extra/filters/__init__.py +0 -33
- liquid/extra/filters/_json.py +0 -33
- liquid/extra/filters/array.py +0 -60
- liquid/extra/filters/babel.py +0 -552
- liquid/extra/filters/html.py +0 -32
- liquid/extra/filters/translate.py +0 -448
- liquid/extra/tags/__init__.py +0 -17
- liquid/extra/tags/_with.py +0 -80
- liquid/extra/tags/extends_tag.py +0 -576
- liquid/extra/tags/macro_tag.py +0 -270
- liquid/extra/tags/snippet_tag.py +0 -107
- liquid/extra/tags/translate_tag.py +0 -388
- liquid/filter.py +0 -230
- liquid/future/__init__.py +0 -3
- liquid/future/environment.py +0 -30
- liquid/future/filters/__init__.py +0 -1
- liquid/future/tags/__init__.py +0 -1
- liquid/lex.py +0 -260
- liquid/limits.py +0 -66
- liquid/loader.py +0 -141
- liquid/messages.py +0 -357
- liquid/mode.py +0 -12
- liquid/output.py +0 -35
- liquid/parser.py +0 -119
- liquid/span.py +0 -35
- liquid/static_analysis.py +0 -443
- liquid/stream.py +0 -148
- liquid/stringify.py +0 -31
- liquid/tag.py +0 -47
- liquid/template.py +0 -658
- liquid/token.py +0 -145
- liquid/undefined.py +0 -218
- liquid/utils/__init__.py +0 -15
- liquid/utils/html.py +0 -57
- liquid/utils/text.py +0 -28
- 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[
|
|
17
|
+
self._cache: OrderedDict[KT, VT] = OrderedDict()
|
|
26
18
|
|
|
27
|
-
def __getitem__(self, key:
|
|
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:
|
|
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:
|
|
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[
|
|
39
|
+
def __iter__(self) -> Iterator[KT]:
|
|
48
40
|
return reversed(self._cache)
|
|
49
41
|
|
|
50
|
-
def __contains__(self, key:
|
|
42
|
+
def __contains__(self, key: KT) -> bool:
|
|
51
43
|
return key in self._cache
|
|
52
44
|
|
|
53
45
|
@overload
|
|
54
|
-
def get(self, key:
|
|
46
|
+
def get(self, key: KT) -> VT | None: ...
|
|
55
47
|
@overload
|
|
56
|
-
def get(self, key:
|
|
48
|
+
def get(self, key: KT, default: VT) -> VT: ...
|
|
57
49
|
@overload
|
|
58
|
-
def get(self, key:
|
|
59
|
-
def get(self, key:
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
95
|
+
def get(self, key: KT) -> VT | None: ...
|
|
104
96
|
@overload
|
|
105
|
-
def get(self, key:
|
|
97
|
+
def get(self, key: KT, default: VT) -> VT: ...
|
|
106
98
|
@overload
|
|
107
|
-
def get(self, key:
|
|
108
|
-
def get(self, key:
|
|
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[
|
|
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[
|
|
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[
|
|
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