python-liquid 1.10.2__py3-none-any.whl → 1.12.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/__init__.py +9 -1
- liquid/builtin/loaders/__init__.py +109 -0
- liquid/builtin/loaders/base_loader.py +6 -5
- liquid/builtin/loaders/caching_file_system_loader.py +10 -163
- liquid/builtin/loaders/choice_loader.py +99 -5
- liquid/builtin/loaders/mixins.py +240 -0
- liquid/builtin/loaders/package_loader.py +108 -0
- liquid/exceptions.py +4 -0
- liquid/expression.py +68 -47
- liquid/golden/if_tag.py +48 -0
- liquid/loaders.py +8 -0
- liquid/utils/cache.py +5 -0
- {python_liquid-1.10.2.dist-info → python_liquid-1.12.0.dist-info}/METADATA +8 -2
- {python_liquid-1.10.2.dist-info → python_liquid-1.12.0.dist-info}/RECORD +16 -14
- {python_liquid-1.10.2.dist-info → python_liquid-1.12.0.dist-info}/WHEEL +1 -1
- {python_liquid-1.10.2.dist-info → python_liquid-1.12.0.dist-info}/licenses/LICENSE +0 -0
liquid/__init__.py
CHANGED
|
@@ -12,11 +12,15 @@ from .mode import Mode
|
|
|
12
12
|
from .token import Token
|
|
13
13
|
from .expression import Expression
|
|
14
14
|
|
|
15
|
+
from .loaders import CachingChoiceLoader
|
|
15
16
|
from .loaders import CachingFileSystemLoader
|
|
16
17
|
from .loaders import ChoiceLoader
|
|
17
18
|
from .loaders import DictLoader
|
|
18
19
|
from .loaders import FileExtensionLoader
|
|
19
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
|
|
20
24
|
|
|
21
25
|
from .context import Context
|
|
22
26
|
from .context import DebugUndefined
|
|
@@ -42,11 +46,12 @@ from .static_analysis import ContextualTemplateAnalysis
|
|
|
42
46
|
|
|
43
47
|
from . import future
|
|
44
48
|
|
|
45
|
-
__version__ = "1.
|
|
49
|
+
__version__ = "1.12.0"
|
|
46
50
|
|
|
47
51
|
__all__ = (
|
|
48
52
|
"AwareBoundTemplate",
|
|
49
53
|
"BoundTemplate",
|
|
54
|
+
"CachingChoiceLoader",
|
|
50
55
|
"CachingFileSystemLoader",
|
|
51
56
|
"ChoiceLoader",
|
|
52
57
|
"Context",
|
|
@@ -64,8 +69,11 @@ __all__ = (
|
|
|
64
69
|
"FutureBoundTemplate",
|
|
65
70
|
"FutureContext",
|
|
66
71
|
"is_undefined",
|
|
72
|
+
"make_choice_loader",
|
|
73
|
+
"make_file_system_loader",
|
|
67
74
|
"Markup",
|
|
68
75
|
"Mode",
|
|
76
|
+
"PackageLoader",
|
|
69
77
|
"soft_str",
|
|
70
78
|
"StrictDefaultUndefined",
|
|
71
79
|
"StrictUndefined",
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
from typing import List
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
1
6
|
from .base_loader import BaseLoader
|
|
2
7
|
from .base_loader import DictLoader
|
|
3
8
|
from .base_loader import TemplateNamespace
|
|
4
9
|
from .base_loader import TemplateSource
|
|
5
10
|
from .base_loader import UpToDate
|
|
6
11
|
|
|
12
|
+
from .choice_loader import CachingChoiceLoader
|
|
7
13
|
from .choice_loader import ChoiceLoader
|
|
8
14
|
|
|
9
15
|
from .file_system_loader import FileExtensionLoader
|
|
@@ -11,14 +17,117 @@ from .file_system_loader import FileSystemLoader
|
|
|
11
17
|
|
|
12
18
|
from .caching_file_system_loader import CachingFileSystemLoader
|
|
13
19
|
|
|
20
|
+
from .package_loader import PackageLoader
|
|
21
|
+
|
|
14
22
|
__all__ = (
|
|
15
23
|
"BaseLoader",
|
|
24
|
+
"CachingChoiceLoader",
|
|
16
25
|
"CachingFileSystemLoader",
|
|
17
26
|
"ChoiceLoader",
|
|
18
27
|
"DictLoader",
|
|
19
28
|
"FileExtensionLoader",
|
|
20
29
|
"FileSystemLoader",
|
|
30
|
+
"make_choice_loader",
|
|
31
|
+
"make_file_system_loader",
|
|
32
|
+
"PackageLoader",
|
|
21
33
|
"TemplateNamespace",
|
|
22
34
|
"TemplateSource",
|
|
23
35
|
"UpToDate",
|
|
24
36
|
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_file_system_loader(
|
|
40
|
+
search_path: Union[str, Path, Iterable[Union[str, Path]]],
|
|
41
|
+
*,
|
|
42
|
+
encoding: str = "utf-8",
|
|
43
|
+
ext: str = ".liquid",
|
|
44
|
+
auto_reload: bool = True,
|
|
45
|
+
namespace_key: str = "",
|
|
46
|
+
cache_size: int = 300,
|
|
47
|
+
) -> BaseLoader:
|
|
48
|
+
"""A _file system_ template loader factory.
|
|
49
|
+
|
|
50
|
+
Returns one of `CachingFileSystemLoader`, `FileExtensionLoader` or
|
|
51
|
+
`FileSystemLoader` depending in the given arguments.
|
|
52
|
+
|
|
53
|
+
A `CachingFileSystemLoader` is returned if _cache_size_ is greater than 0.
|
|
54
|
+
Otherwise a `FileExtensionLoader` is returned if _ext_ is not empty.
|
|
55
|
+
If _ext_ is empty, a `FileSystemLoader` is returned.
|
|
56
|
+
|
|
57
|
+
_auto_reload_ and _namespace_key_ are ignored if _cache_key_ is less than 1.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
search_path: One or more paths to search.
|
|
61
|
+
encoding: Open template files with the given encoding.
|
|
62
|
+
ext: A default file extension. Should include a leading period.
|
|
63
|
+
auto_reload: If `True`, automatically reload a cached template if it has been
|
|
64
|
+
updated.
|
|
65
|
+
namespace_key: The name of a global render context variable or loader keyword
|
|
66
|
+
argument that resolves to the current loader "namespace" or "scope".
|
|
67
|
+
|
|
68
|
+
If you're developing a multi-user application, a good namespace might be
|
|
69
|
+
`uid`, where `uid` is a unique identifier for a user and templates are
|
|
70
|
+
arranged in folders named for each `uid` inside the search path.
|
|
71
|
+
cache_size: The maximum number of templates to hold in the cache before removing
|
|
72
|
+
the least recently used template.
|
|
73
|
+
|
|
74
|
+
_New in version 1.12.0_
|
|
75
|
+
"""
|
|
76
|
+
if cache_size > 0:
|
|
77
|
+
return CachingFileSystemLoader(
|
|
78
|
+
search_path=search_path,
|
|
79
|
+
encoding=encoding,
|
|
80
|
+
ext=ext,
|
|
81
|
+
auto_reload=auto_reload,
|
|
82
|
+
namespace_key=namespace_key,
|
|
83
|
+
cache_size=cache_size,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if ext:
|
|
87
|
+
return FileExtensionLoader(
|
|
88
|
+
search_path=search_path,
|
|
89
|
+
encoding=encoding,
|
|
90
|
+
ext=ext,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return FileSystemLoader(search_path=search_path, encoding=encoding)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def make_choice_loader(
|
|
97
|
+
loaders: List[BaseLoader],
|
|
98
|
+
*,
|
|
99
|
+
auto_reload: bool = True,
|
|
100
|
+
namespace_key: str = "",
|
|
101
|
+
cache_size: int = 300,
|
|
102
|
+
) -> BaseLoader:
|
|
103
|
+
"""A _choice loader_ factory.
|
|
104
|
+
|
|
105
|
+
Returns one of `CachingChoiceLoader` or `ChoiceLoader` depending on the
|
|
106
|
+
given arguments.
|
|
107
|
+
|
|
108
|
+
A `CachingChoiceLoader` is returned if _cache_size_ > 0, otherwise a
|
|
109
|
+
`ChoiceLoader` is returned.
|
|
110
|
+
|
|
111
|
+
_auto_reload_ and _namespace_key_ are ignored if _cache_key_ is less than 1.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
loaders: A list of loaders implementing `liquid.loaders.BaseLoader`.
|
|
115
|
+
auto_reload: If `True`, automatically reload a cached template if it
|
|
116
|
+
has been updated.
|
|
117
|
+
namespace_key: The name of a global render context variable or loader
|
|
118
|
+
keyword argument that resolves to the current loader "namespace" or
|
|
119
|
+
"scope".
|
|
120
|
+
cache_size: The maximum number of templates to hold in the cache before
|
|
121
|
+
removing the least recently used template.
|
|
122
|
+
|
|
123
|
+
_New in version 1.12.0_
|
|
124
|
+
"""
|
|
125
|
+
if cache_size > 0:
|
|
126
|
+
return CachingChoiceLoader(
|
|
127
|
+
loaders=loaders,
|
|
128
|
+
auto_reload=auto_reload,
|
|
129
|
+
namespace_key=namespace_key,
|
|
130
|
+
cache_size=cache_size,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return ChoiceLoader(loaders=loaders)
|
|
@@ -129,11 +129,12 @@ class BaseLoader(ABC): # noqa: B024
|
|
|
129
129
|
name: str,
|
|
130
130
|
globals: TemplateNamespace = None, # noqa: A002
|
|
131
131
|
) -> BoundTemplate:
|
|
132
|
-
"""
|
|
132
|
+
"""Find and parse template source code.
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
than overriding
|
|
134
|
+
This is used internally by `liquid.Environment` to load template
|
|
135
|
+
source text. `load()` delegates to `BaseLoader.get_source()`. Custom
|
|
136
|
+
loaders would typically implement `get_source()` rather than overriding
|
|
137
|
+
`load()`.
|
|
137
138
|
"""
|
|
138
139
|
try:
|
|
139
140
|
source, filename, uptodate, matter = self.get_source(env, name)
|
|
@@ -156,7 +157,7 @@ class BaseLoader(ABC): # noqa: B024
|
|
|
156
157
|
name: str,
|
|
157
158
|
globals: TemplateNamespace = None, # noqa: A002
|
|
158
159
|
) -> BoundTemplate:
|
|
159
|
-
"""An async version of `load`."""
|
|
160
|
+
"""An async version of `load()`."""
|
|
160
161
|
try:
|
|
161
162
|
template_source = await self.get_source_async(env, name)
|
|
162
163
|
source, filename, uptodate, matter = template_source
|
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
"""A file system loader that caches parsed templates in memory."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from functools import partial
|
|
5
4
|
from typing import TYPE_CHECKING
|
|
6
|
-
from typing import Awaitable
|
|
7
|
-
from typing import Callable
|
|
8
5
|
from typing import Iterable
|
|
9
|
-
from typing import Mapping
|
|
10
6
|
from typing import Union
|
|
11
7
|
|
|
12
|
-
from liquid.utils import LRUCache
|
|
13
|
-
|
|
14
8
|
from .file_system_loader import FileExtensionLoader
|
|
9
|
+
from .mixins import CachingLoaderMixin
|
|
15
10
|
|
|
16
11
|
if TYPE_CHECKING:
|
|
17
12
|
from pathlib import Path
|
|
18
13
|
|
|
19
|
-
from liquid import BoundTemplate
|
|
20
14
|
from liquid import Context
|
|
21
|
-
from liquid import Environment
|
|
22
15
|
|
|
23
|
-
from .base_loader import TemplateNamespace
|
|
24
16
|
from .base_loader import TemplateSource
|
|
25
17
|
|
|
26
|
-
# ruff: noqa: D102
|
|
18
|
+
# ruff: noqa: D102
|
|
27
19
|
|
|
28
20
|
|
|
29
|
-
class CachingFileSystemLoader(FileExtensionLoader):
|
|
21
|
+
class CachingFileSystemLoader(CachingLoaderMixin, FileExtensionLoader):
|
|
30
22
|
"""A file system loader that caches parsed templates in memory.
|
|
31
23
|
|
|
32
24
|
Args:
|
|
@@ -45,8 +37,6 @@ class CachingFileSystemLoader(FileExtensionLoader):
|
|
|
45
37
|
the least recently used template.
|
|
46
38
|
"""
|
|
47
39
|
|
|
48
|
-
caching_loader = True
|
|
49
|
-
|
|
50
40
|
def __init__(
|
|
51
41
|
self,
|
|
52
42
|
search_path: Union[str, Path, Iterable[Union[str, Path]]],
|
|
@@ -58,160 +48,17 @@ class CachingFileSystemLoader(FileExtensionLoader):
|
|
|
58
48
|
cache_size: int = 300,
|
|
59
49
|
):
|
|
60
50
|
super().__init__(
|
|
51
|
+
auto_reload=auto_reload,
|
|
52
|
+
namespace_key=namespace_key,
|
|
53
|
+
cache_size=cache_size,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
FileExtensionLoader.__init__(
|
|
57
|
+
self,
|
|
61
58
|
search_path=search_path,
|
|
62
59
|
encoding=encoding,
|
|
63
60
|
ext=ext,
|
|
64
61
|
)
|
|
65
|
-
self.auto_reload = auto_reload
|
|
66
|
-
self.cache = LRUCache(capacity=cache_size)
|
|
67
|
-
self.namespace_key = namespace_key
|
|
68
|
-
|
|
69
|
-
def load(
|
|
70
|
-
self,
|
|
71
|
-
env: Environment,
|
|
72
|
-
name: str,
|
|
73
|
-
globals: TemplateNamespace = None, # noqa: A002
|
|
74
|
-
) -> BoundTemplate:
|
|
75
|
-
return self.check_cache(
|
|
76
|
-
env,
|
|
77
|
-
name,
|
|
78
|
-
globals,
|
|
79
|
-
partial(super().load, env, name, globals),
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
async def load_async(
|
|
83
|
-
self,
|
|
84
|
-
env: Environment,
|
|
85
|
-
name: str,
|
|
86
|
-
globals: TemplateNamespace = None, # noqa: A002
|
|
87
|
-
) -> BoundTemplate:
|
|
88
|
-
return await self.check_cache_async(
|
|
89
|
-
env,
|
|
90
|
-
name,
|
|
91
|
-
globals,
|
|
92
|
-
partial(super().load_async, env, name, globals),
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
def load_with_args(
|
|
96
|
-
self,
|
|
97
|
-
env: Environment,
|
|
98
|
-
name: str,
|
|
99
|
-
globals: TemplateNamespace = None, # noqa: A002
|
|
100
|
-
**kwargs: object,
|
|
101
|
-
) -> BoundTemplate:
|
|
102
|
-
cache_key = self.cache_key(name, kwargs)
|
|
103
|
-
return self.check_cache(
|
|
104
|
-
env,
|
|
105
|
-
cache_key,
|
|
106
|
-
globals,
|
|
107
|
-
partial(super().load_with_args, env, cache_key, globals, **kwargs),
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
async def load_with_args_async(
|
|
111
|
-
self,
|
|
112
|
-
env: Environment,
|
|
113
|
-
name: str,
|
|
114
|
-
globals: TemplateNamespace = None, # noqa: A002
|
|
115
|
-
**kwargs: object,
|
|
116
|
-
) -> BoundTemplate:
|
|
117
|
-
cache_key = self.cache_key(name, kwargs)
|
|
118
|
-
return await self.check_cache_async(
|
|
119
|
-
env,
|
|
120
|
-
cache_key,
|
|
121
|
-
globals,
|
|
122
|
-
partial(super().load_with_args_async, env, cache_key, globals, **kwargs),
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
def load_with_context(
|
|
126
|
-
self, context: Context, name: str, **kwargs: str
|
|
127
|
-
) -> BoundTemplate:
|
|
128
|
-
cache_key = self.cache_key_with_context(name, context, **kwargs)
|
|
129
|
-
return self.check_cache(
|
|
130
|
-
context.env,
|
|
131
|
-
cache_key,
|
|
132
|
-
context.globals,
|
|
133
|
-
partial(super().load_with_context, context=context, name=name, **kwargs),
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
async def load_with_context_async(
|
|
137
|
-
self, context: Context, name: str, **kwargs: str
|
|
138
|
-
) -> BoundTemplate:
|
|
139
|
-
cache_key = self.cache_key_with_context(name, context, **kwargs)
|
|
140
|
-
return await self.check_cache_async(
|
|
141
|
-
context.env,
|
|
142
|
-
cache_key,
|
|
143
|
-
context.globals,
|
|
144
|
-
partial(super().load_with_context_async, context, name, **kwargs),
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
def check_cache(
|
|
148
|
-
self,
|
|
149
|
-
env: Environment, # noqa: ARG002
|
|
150
|
-
cache_key: str,
|
|
151
|
-
globals: TemplateNamespace, # noqa: A002
|
|
152
|
-
load_func: Callable[[], BoundTemplate],
|
|
153
|
-
) -> BoundTemplate:
|
|
154
|
-
try:
|
|
155
|
-
cached_template: BoundTemplate = self.cache[cache_key]
|
|
156
|
-
except KeyError:
|
|
157
|
-
template = load_func()
|
|
158
|
-
self.cache[cache_key] = template
|
|
159
|
-
return template
|
|
160
|
-
|
|
161
|
-
if self.auto_reload and not cached_template.is_up_to_date:
|
|
162
|
-
template = load_func()
|
|
163
|
-
self.cache[cache_key] = template
|
|
164
|
-
return template
|
|
165
|
-
|
|
166
|
-
if globals:
|
|
167
|
-
cached_template.globals.update(globals)
|
|
168
|
-
return cached_template
|
|
169
|
-
|
|
170
|
-
async def check_cache_async(
|
|
171
|
-
self,
|
|
172
|
-
env: Environment, # noqa: ARG002
|
|
173
|
-
cache_key: str,
|
|
174
|
-
globals: TemplateNamespace, # noqa: A002
|
|
175
|
-
load_func: Callable[[], Awaitable[BoundTemplate]],
|
|
176
|
-
) -> BoundTemplate:
|
|
177
|
-
try:
|
|
178
|
-
cached_template: BoundTemplate = self.cache[cache_key]
|
|
179
|
-
except KeyError:
|
|
180
|
-
template = await load_func()
|
|
181
|
-
self.cache[cache_key] = template
|
|
182
|
-
return template
|
|
183
|
-
|
|
184
|
-
if self.auto_reload and not await cached_template.is_up_to_date_async():
|
|
185
|
-
template = await load_func()
|
|
186
|
-
self.cache[cache_key] = template
|
|
187
|
-
return template
|
|
188
|
-
|
|
189
|
-
if globals:
|
|
190
|
-
cached_template.globals.update(globals)
|
|
191
|
-
return cached_template
|
|
192
|
-
|
|
193
|
-
def cache_key(self, name: str, args: Mapping[str, object]) -> str:
|
|
194
|
-
if not self.namespace_key:
|
|
195
|
-
return name
|
|
196
|
-
|
|
197
|
-
try:
|
|
198
|
-
return f"{args[self.namespace_key]}/{name}"
|
|
199
|
-
except KeyError:
|
|
200
|
-
return name
|
|
201
|
-
|
|
202
|
-
def cache_key_with_context(
|
|
203
|
-
self,
|
|
204
|
-
name: str,
|
|
205
|
-
context: Context,
|
|
206
|
-
**kwargs: str, # noqa: ARG002
|
|
207
|
-
) -> str:
|
|
208
|
-
if not self.namespace_key:
|
|
209
|
-
return name
|
|
210
|
-
|
|
211
|
-
try:
|
|
212
|
-
return f"{context.globals[self.namespace_key]}/{name}"
|
|
213
|
-
except KeyError:
|
|
214
|
-
return name
|
|
215
62
|
|
|
216
63
|
def get_source_with_context(
|
|
217
64
|
self, context: Context, template_name: str, **kwargs: str
|
|
@@ -7,11 +7,14 @@ from typing import List
|
|
|
7
7
|
from liquid.exceptions import TemplateNotFound
|
|
8
8
|
|
|
9
9
|
from .base_loader import BaseLoader
|
|
10
|
-
from .
|
|
10
|
+
from .mixins import CachingLoaderMixin
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
+
from liquid import Context
|
|
13
14
|
from liquid import Environment
|
|
14
15
|
|
|
16
|
+
from .base_loader import TemplateSource
|
|
17
|
+
|
|
15
18
|
|
|
16
19
|
class ChoiceLoader(BaseLoader):
|
|
17
20
|
"""A template loader that will try each of a list of loaders in turn.
|
|
@@ -24,9 +27,8 @@ class ChoiceLoader(BaseLoader):
|
|
|
24
27
|
super().__init__()
|
|
25
28
|
self.loaders = loaders
|
|
26
29
|
|
|
27
|
-
def get_source(
|
|
28
|
-
|
|
29
|
-
) -> TemplateSource:
|
|
30
|
+
def get_source(self, env: Environment, template_name: str) -> TemplateSource:
|
|
31
|
+
"""Get source code for a template from one of the configured loaders."""
|
|
30
32
|
for loader in self.loaders:
|
|
31
33
|
try:
|
|
32
34
|
return loader.get_source(env, template_name)
|
|
@@ -35,11 +37,12 @@ class ChoiceLoader(BaseLoader):
|
|
|
35
37
|
|
|
36
38
|
raise TemplateNotFound(template_name)
|
|
37
39
|
|
|
38
|
-
async def get_source_async(
|
|
40
|
+
async def get_source_async(
|
|
39
41
|
self,
|
|
40
42
|
env: Environment,
|
|
41
43
|
template_name: str,
|
|
42
44
|
) -> TemplateSource:
|
|
45
|
+
"""An async version of `get_source`."""
|
|
43
46
|
for loader in self.loaders:
|
|
44
47
|
try:
|
|
45
48
|
return await loader.get_source_async(env, template_name)
|
|
@@ -47,3 +50,94 @@ class ChoiceLoader(BaseLoader):
|
|
|
47
50
|
pass
|
|
48
51
|
|
|
49
52
|
raise TemplateNotFound(template_name)
|
|
53
|
+
|
|
54
|
+
def get_source_with_args(
|
|
55
|
+
self,
|
|
56
|
+
env: Environment,
|
|
57
|
+
template_name: str,
|
|
58
|
+
**kwargs: object,
|
|
59
|
+
) -> TemplateSource:
|
|
60
|
+
"""Get source code for a template from one of the configured loaders."""
|
|
61
|
+
for loader in self.loaders:
|
|
62
|
+
try:
|
|
63
|
+
return loader.get_source_with_args(env, template_name, **kwargs)
|
|
64
|
+
except TemplateNotFound:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
# TODO: include arguments in TemplateNotFound exception.
|
|
68
|
+
raise TemplateNotFound(template_name)
|
|
69
|
+
|
|
70
|
+
async def get_source_with_args_async(
|
|
71
|
+
self,
|
|
72
|
+
env: Environment,
|
|
73
|
+
template_name: str,
|
|
74
|
+
**kwargs: object,
|
|
75
|
+
) -> TemplateSource:
|
|
76
|
+
"""An async version of `get_source_with_args`."""
|
|
77
|
+
for loader in self.loaders:
|
|
78
|
+
try:
|
|
79
|
+
return await loader.get_source_with_args_async(
|
|
80
|
+
env, template_name, **kwargs
|
|
81
|
+
)
|
|
82
|
+
except TemplateNotFound:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
raise TemplateNotFound(template_name)
|
|
86
|
+
|
|
87
|
+
def get_source_with_context(
|
|
88
|
+
self, context: Context, template_name: str, **kwargs: str
|
|
89
|
+
) -> TemplateSource:
|
|
90
|
+
"""Get source code for a template from one of the configured loaders."""
|
|
91
|
+
for loader in self.loaders:
|
|
92
|
+
try:
|
|
93
|
+
return loader.get_source_with_context(context, template_name, **kwargs)
|
|
94
|
+
except TemplateNotFound:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
raise TemplateNotFound(template_name)
|
|
98
|
+
|
|
99
|
+
async def get_source_with_context_async(
|
|
100
|
+
self, context: Context, template_name: str, **kwargs: str
|
|
101
|
+
) -> TemplateSource:
|
|
102
|
+
"""Get source code for a template from one of the configured loaders."""
|
|
103
|
+
for loader in self.loaders:
|
|
104
|
+
try:
|
|
105
|
+
return await loader.get_source_with_context_async(
|
|
106
|
+
context, template_name, **kwargs
|
|
107
|
+
)
|
|
108
|
+
except TemplateNotFound:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
raise TemplateNotFound(template_name)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class CachingChoiceLoader(CachingLoaderMixin, ChoiceLoader):
|
|
115
|
+
"""A `ChoiceLoader` that caches parsed templates in memory.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
loaders: A list of loaders implementing `liquid.loaders.BaseLoader`.
|
|
119
|
+
auto_reload: If `True`, automatically reload a cached template if it has been
|
|
120
|
+
updated.
|
|
121
|
+
namespace_key: The name of a global render context variable or loader keyword
|
|
122
|
+
argument that resolves to the current loader "namespace" or "scope".
|
|
123
|
+
cache_size: The maximum number of templates to hold in the cache before removing
|
|
124
|
+
the least recently used template.
|
|
125
|
+
|
|
126
|
+
_New in version 1.11.0._
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
loaders: List[BaseLoader],
|
|
132
|
+
*,
|
|
133
|
+
auto_reload: bool = True,
|
|
134
|
+
namespace_key: str = "",
|
|
135
|
+
cache_size: int = 300,
|
|
136
|
+
):
|
|
137
|
+
super().__init__(
|
|
138
|
+
auto_reload=auto_reload,
|
|
139
|
+
namespace_key=namespace_key,
|
|
140
|
+
cache_size=cache_size,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
ChoiceLoader.__init__(self, loaders)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Mixin classes that can be used to add common functions to a template loader."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from abc import ABC
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
from typing import Awaitable
|
|
8
|
+
from typing import Callable
|
|
9
|
+
from typing import Mapping
|
|
10
|
+
|
|
11
|
+
from typing_extensions import Protocol
|
|
12
|
+
|
|
13
|
+
from liquid.utils import LRUCache
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from liquid import BoundTemplate
|
|
17
|
+
from liquid import Context
|
|
18
|
+
from liquid import Environment
|
|
19
|
+
|
|
20
|
+
from .base_loader import TemplateNamespace
|
|
21
|
+
|
|
22
|
+
# ruff: noqa: D102
|
|
23
|
+
|
|
24
|
+
# ignoring "safe-super" type errors due to https://github.com/python/mypy/issues/14757
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _CachingLoaderProtocol(Protocol):
|
|
28
|
+
def load(
|
|
29
|
+
self,
|
|
30
|
+
env: Environment,
|
|
31
|
+
name: str,
|
|
32
|
+
globals: TemplateNamespace = None, # noqa: A002
|
|
33
|
+
) -> BoundTemplate:
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
async def load_async(
|
|
37
|
+
self,
|
|
38
|
+
env: Environment,
|
|
39
|
+
name: str,
|
|
40
|
+
globals: TemplateNamespace = None, # noqa: A002
|
|
41
|
+
) -> BoundTemplate:
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def load_with_args(
|
|
45
|
+
self,
|
|
46
|
+
env: Environment,
|
|
47
|
+
name: str,
|
|
48
|
+
globals: TemplateNamespace = None, # noqa: A002
|
|
49
|
+
**kwargs: object,
|
|
50
|
+
) -> BoundTemplate:
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
async def load_with_args_async(
|
|
54
|
+
self,
|
|
55
|
+
env: Environment,
|
|
56
|
+
name: str,
|
|
57
|
+
globals: TemplateNamespace = None, # noqa: A002
|
|
58
|
+
**kwargs: object,
|
|
59
|
+
) -> BoundTemplate:
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def load_with_context(
|
|
63
|
+
self,
|
|
64
|
+
context: Context,
|
|
65
|
+
name: str,
|
|
66
|
+
**kwargs: str,
|
|
67
|
+
) -> BoundTemplate:
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
async def load_with_context_async(
|
|
71
|
+
self,
|
|
72
|
+
context: Context,
|
|
73
|
+
name: str,
|
|
74
|
+
**kwargs: str,
|
|
75
|
+
) -> BoundTemplate:
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CachingLoaderMixin(ABC, _CachingLoaderProtocol):
|
|
80
|
+
"""A mixin class that adds caching to a template loader."""
|
|
81
|
+
|
|
82
|
+
caching_loader = True
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
auto_reload: bool = True,
|
|
88
|
+
namespace_key: str = "",
|
|
89
|
+
cache_size: int = 300,
|
|
90
|
+
):
|
|
91
|
+
self.auto_reload = auto_reload
|
|
92
|
+
self.cache = LRUCache(capacity=cache_size)
|
|
93
|
+
self.namespace_key = namespace_key
|
|
94
|
+
|
|
95
|
+
def _check_cache(
|
|
96
|
+
self,
|
|
97
|
+
env: Environment, # noqa: ARG002
|
|
98
|
+
cache_key: str,
|
|
99
|
+
globals: TemplateNamespace, # noqa: A002
|
|
100
|
+
load_func: Callable[[], BoundTemplate],
|
|
101
|
+
) -> BoundTemplate:
|
|
102
|
+
try:
|
|
103
|
+
cached_template: BoundTemplate = self.cache[cache_key]
|
|
104
|
+
except KeyError:
|
|
105
|
+
template = load_func()
|
|
106
|
+
self.cache[cache_key] = template
|
|
107
|
+
return template
|
|
108
|
+
|
|
109
|
+
if self.auto_reload and not cached_template.is_up_to_date:
|
|
110
|
+
template = load_func()
|
|
111
|
+
self.cache[cache_key] = template
|
|
112
|
+
return template
|
|
113
|
+
|
|
114
|
+
if globals:
|
|
115
|
+
cached_template.globals.update(globals)
|
|
116
|
+
return cached_template
|
|
117
|
+
|
|
118
|
+
async def _check_cache_async(
|
|
119
|
+
self,
|
|
120
|
+
env: Environment, # noqa: ARG002
|
|
121
|
+
cache_key: str,
|
|
122
|
+
globals: TemplateNamespace, # noqa: A002
|
|
123
|
+
load_func: Callable[[], Awaitable[BoundTemplate]],
|
|
124
|
+
) -> BoundTemplate:
|
|
125
|
+
try:
|
|
126
|
+
cached_template: BoundTemplate = self.cache[cache_key]
|
|
127
|
+
except KeyError:
|
|
128
|
+
template = await load_func()
|
|
129
|
+
self.cache[cache_key] = template
|
|
130
|
+
return template
|
|
131
|
+
|
|
132
|
+
if self.auto_reload and not await cached_template.is_up_to_date_async():
|
|
133
|
+
template = await load_func()
|
|
134
|
+
self.cache[cache_key] = template
|
|
135
|
+
return template
|
|
136
|
+
|
|
137
|
+
if globals:
|
|
138
|
+
cached_template.globals.update(globals)
|
|
139
|
+
return cached_template
|
|
140
|
+
|
|
141
|
+
def load(
|
|
142
|
+
self,
|
|
143
|
+
env: Environment,
|
|
144
|
+
name: str,
|
|
145
|
+
globals: TemplateNamespace = None, # noqa: A002
|
|
146
|
+
) -> BoundTemplate:
|
|
147
|
+
return self._check_cache(
|
|
148
|
+
env,
|
|
149
|
+
name,
|
|
150
|
+
globals,
|
|
151
|
+
partial(super().load, env, name, globals), # type: ignore
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def load_async(
|
|
155
|
+
self,
|
|
156
|
+
env: Environment,
|
|
157
|
+
name: str,
|
|
158
|
+
globals: TemplateNamespace = None, # noqa: A002
|
|
159
|
+
) -> BoundTemplate:
|
|
160
|
+
return await self._check_cache_async(
|
|
161
|
+
env,
|
|
162
|
+
name,
|
|
163
|
+
globals,
|
|
164
|
+
partial(super().load_async, env, name, globals), # type: ignore
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def load_with_args(
|
|
168
|
+
self,
|
|
169
|
+
env: Environment,
|
|
170
|
+
name: str,
|
|
171
|
+
globals: TemplateNamespace = None, # noqa: A002
|
|
172
|
+
**kwargs: object,
|
|
173
|
+
) -> BoundTemplate:
|
|
174
|
+
cache_key = self.cache_key(name, kwargs)
|
|
175
|
+
return self._check_cache(
|
|
176
|
+
env,
|
|
177
|
+
cache_key,
|
|
178
|
+
globals,
|
|
179
|
+
partial(super().load_with_args, env, cache_key, globals, **kwargs), # type: ignore
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
async def load_with_args_async(
|
|
183
|
+
self,
|
|
184
|
+
env: Environment,
|
|
185
|
+
name: str,
|
|
186
|
+
globals: TemplateNamespace = None, # noqa: A002
|
|
187
|
+
**kwargs: object,
|
|
188
|
+
) -> BoundTemplate:
|
|
189
|
+
cache_key = self.cache_key(name, kwargs)
|
|
190
|
+
return await self._check_cache_async(
|
|
191
|
+
env,
|
|
192
|
+
cache_key,
|
|
193
|
+
globals,
|
|
194
|
+
partial(super().load_with_args_async, env, cache_key, globals, **kwargs), # type: ignore
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def load_with_context(
|
|
198
|
+
self, context: Context, name: str, **kwargs: str
|
|
199
|
+
) -> BoundTemplate:
|
|
200
|
+
cache_key = self.cache_key_with_context(name, context, **kwargs)
|
|
201
|
+
return self._check_cache(
|
|
202
|
+
context.env,
|
|
203
|
+
cache_key,
|
|
204
|
+
context.globals,
|
|
205
|
+
partial(super().load_with_context, context=context, name=name, **kwargs), # type: ignore
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
async def load_with_context_async(
|
|
209
|
+
self, context: Context, name: str, **kwargs: str
|
|
210
|
+
) -> BoundTemplate:
|
|
211
|
+
cache_key = self.cache_key_with_context(name, context, **kwargs)
|
|
212
|
+
return await self._check_cache_async(
|
|
213
|
+
context.env,
|
|
214
|
+
cache_key,
|
|
215
|
+
context.globals,
|
|
216
|
+
partial(super().load_with_context_async, context, name, **kwargs), # type: ignore
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def cache_key(self, name: str, args: Mapping[str, object]) -> str:
|
|
220
|
+
if not self.namespace_key:
|
|
221
|
+
return name
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
return f"{args[self.namespace_key]}/{name}"
|
|
225
|
+
except KeyError:
|
|
226
|
+
return name
|
|
227
|
+
|
|
228
|
+
def cache_key_with_context(
|
|
229
|
+
self,
|
|
230
|
+
name: str,
|
|
231
|
+
context: Context,
|
|
232
|
+
**kwargs: str, # noqa: ARG002
|
|
233
|
+
) -> str:
|
|
234
|
+
if not self.namespace_key:
|
|
235
|
+
return name
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
return f"{context.globals[self.namespace_key]}/{name}"
|
|
239
|
+
except KeyError:
|
|
240
|
+
return name
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""A template loader that reads templates from Python packages."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import Iterable
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
from importlib_resources import files
|
|
12
|
+
|
|
13
|
+
from liquid.exceptions import TemplateNotFound
|
|
14
|
+
|
|
15
|
+
from .base_loader import BaseLoader
|
|
16
|
+
from .base_loader import TemplateSource
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from types import ModuleType
|
|
20
|
+
|
|
21
|
+
from importlib_resources.abc import Traversable
|
|
22
|
+
|
|
23
|
+
from liquid import Environment
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PackageLoader(BaseLoader):
|
|
27
|
+
"""A template loader that reads templates from Python packages.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
package: Import name of a package containing Liquid templates.
|
|
31
|
+
package_path: One or more directories in the package containing Liquid
|
|
32
|
+
templates.
|
|
33
|
+
encoding: Encoding of template files.
|
|
34
|
+
ext: A default file extension to use if one is not provided. Should
|
|
35
|
+
include a leading period.
|
|
36
|
+
|
|
37
|
+
_New in version 1.11.0._
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
package: Union[str, ModuleType],
|
|
43
|
+
*,
|
|
44
|
+
package_path: Union[str, Iterable[str]] = "templates",
|
|
45
|
+
encoding: str = "utf-8",
|
|
46
|
+
ext: str = ".liquid",
|
|
47
|
+
) -> None:
|
|
48
|
+
if isinstance(package_path, str):
|
|
49
|
+
self.paths = [files(package).joinpath(package_path)]
|
|
50
|
+
else:
|
|
51
|
+
_package = files(package)
|
|
52
|
+
self.paths = [_package.joinpath(path) for path in package_path]
|
|
53
|
+
|
|
54
|
+
self.encoding = encoding
|
|
55
|
+
self.ext = ext
|
|
56
|
+
|
|
57
|
+
def _resolve_path(self, template_name: str) -> Traversable:
|
|
58
|
+
template_path = Path(template_name)
|
|
59
|
+
|
|
60
|
+
# Don't build a path that escapes package/package_path.
|
|
61
|
+
# Does ".." appear in template_name?
|
|
62
|
+
if os.path.pardir in template_path.parts:
|
|
63
|
+
raise TemplateNotFound(template_name)
|
|
64
|
+
|
|
65
|
+
# Add suffix self.ext if template name does not have a suffix.
|
|
66
|
+
if not template_path.suffix:
|
|
67
|
+
template_path = template_path.with_suffix(self.ext)
|
|
68
|
+
|
|
69
|
+
for path in self.paths:
|
|
70
|
+
source_path = path.joinpath(template_path)
|
|
71
|
+
if source_path.is_file():
|
|
72
|
+
# MyPy seems to think source_path has `Any` type :(
|
|
73
|
+
return source_path # type: ignore
|
|
74
|
+
|
|
75
|
+
raise TemplateNotFound(template_name)
|
|
76
|
+
|
|
77
|
+
def get_source( # noqa: D102
|
|
78
|
+
self,
|
|
79
|
+
_: Environment,
|
|
80
|
+
template_name: str,
|
|
81
|
+
) -> TemplateSource:
|
|
82
|
+
source_path = self._resolve_path(template_name)
|
|
83
|
+
return TemplateSource(
|
|
84
|
+
source=source_path.read_text(self.encoding),
|
|
85
|
+
filename=str(source_path),
|
|
86
|
+
uptodate=None,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def get_source_async( # noqa: D102
|
|
90
|
+
self, _: Environment, template_name: str
|
|
91
|
+
) -> TemplateSource:
|
|
92
|
+
loop = asyncio.get_running_loop()
|
|
93
|
+
|
|
94
|
+
source_path = await loop.run_in_executor(
|
|
95
|
+
None,
|
|
96
|
+
self._resolve_path,
|
|
97
|
+
template_name,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
source_text = await loop.run_in_executor(
|
|
101
|
+
None,
|
|
102
|
+
source_path.read_text,
|
|
103
|
+
self.encoding,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return TemplateSource(
|
|
107
|
+
source=source_text, filename=str(source_path), uptodate=None
|
|
108
|
+
)
|
liquid/exceptions.py
CHANGED
liquid/expression.py
CHANGED
|
@@ -16,6 +16,7 @@ from typing import Iterable
|
|
|
16
16
|
from typing import Iterator
|
|
17
17
|
from typing import List
|
|
18
18
|
from typing import Mapping
|
|
19
|
+
from typing import NoReturn
|
|
19
20
|
from typing import Optional
|
|
20
21
|
from typing import Tuple
|
|
21
22
|
from typing import TypeVar
|
|
@@ -29,6 +30,7 @@ from liquid.exceptions import FilterValueError
|
|
|
29
30
|
from liquid.exceptions import LiquidTypeError
|
|
30
31
|
from liquid.exceptions import NoSuchFilterFunc
|
|
31
32
|
from liquid.limits import to_int
|
|
33
|
+
from liquid.undefined import Undefined
|
|
32
34
|
|
|
33
35
|
# ruff: noqa: D102 D101
|
|
34
36
|
|
|
@@ -1079,7 +1081,7 @@ def eval_number_expression(left: Number, operator: str, right: Number) -> bool:
|
|
|
1079
1081
|
|
|
1080
1082
|
|
|
1081
1083
|
def _is_py_falsy_number(obj: object) -> bool:
|
|
1082
|
-
# Liquid 0, 0.0,
|
|
1084
|
+
# Liquid 0, 0.0, and Decimal("0") are not falsy.
|
|
1083
1085
|
return not isinstance(obj, bool) and isinstance(obj, (int, float, Decimal))
|
|
1084
1086
|
|
|
1085
1087
|
|
|
@@ -1090,67 +1092,86 @@ def is_truthy(obj: Any) -> bool:
|
|
|
1090
1092
|
return _is_py_falsy_number(obj) or obj not in (False, None)
|
|
1091
1093
|
|
|
1092
1094
|
|
|
1093
|
-
def
|
|
1094
|
-
"""Compare
|
|
1095
|
-
if
|
|
1096
|
-
isinstance(right, bool) and _is_py_falsy_number(left)
|
|
1097
|
-
):
|
|
1098
|
-
if operator in ("==", "<", ">", "<=", ">="):
|
|
1099
|
-
return False
|
|
1100
|
-
if operator in ("!=", "<>"):
|
|
1101
|
-
return True
|
|
1102
|
-
raise LiquidTypeError(
|
|
1103
|
-
f"unknown operator: {type(left)} {operator} {type(right)}"
|
|
1104
|
-
)
|
|
1105
|
-
|
|
1106
|
-
if operator == "==":
|
|
1107
|
-
return bool(left == right)
|
|
1108
|
-
if operator in ("!=", "<>"):
|
|
1109
|
-
return bool(left != right)
|
|
1110
|
-
if operator in ("<", ">", "<=", ">="):
|
|
1111
|
-
return False
|
|
1112
|
-
|
|
1113
|
-
raise LiquidTypeError(f"unknown operator: {type(left)} {operator} {type(right)}")
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
def compare(left: Any, operator: str, right: Any) -> bool: # noqa: PLR0911, PLR0912
|
|
1117
|
-
"""Return the result of a comparison operation between two objects."""
|
|
1118
|
-
if operator == "and":
|
|
1095
|
+
def compare(left: object, op: str, right: object) -> bool: # noqa: PLR0911, PLR0912
|
|
1096
|
+
"""Compare _left_ with _right_ according to Liquid semantics."""
|
|
1097
|
+
if op == "and":
|
|
1119
1098
|
return is_truthy(left) and is_truthy(right)
|
|
1120
|
-
if
|
|
1099
|
+
if op == "or":
|
|
1121
1100
|
return is_truthy(left) or is_truthy(right)
|
|
1122
1101
|
|
|
1123
1102
|
if hasattr(left, "__liquid__"):
|
|
1124
1103
|
left = left.__liquid__()
|
|
1104
|
+
|
|
1125
1105
|
if hasattr(right, "__liquid__"):
|
|
1126
1106
|
right = right.__liquid__()
|
|
1127
1107
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if isinstance(left, bool) or isinstance(right, bool):
|
|
1132
|
-
return compare_bool(left, operator, right)
|
|
1108
|
+
def _type_error(_left: object, _right: object) -> NoReturn:
|
|
1109
|
+
if type(_left) != type(_right):
|
|
1110
|
+
raise LiquidTypeError(f"invalid operator for types '{_left} {op} {_right}'")
|
|
1133
1111
|
|
|
1134
|
-
|
|
1135
|
-
return bool(left == right)
|
|
1136
|
-
if operator in ("!=", "<>"):
|
|
1137
|
-
return bool(left != right)
|
|
1112
|
+
raise LiquidTypeError(f"unknown operator: {type(_left)} {op} {type(_right)}")
|
|
1138
1113
|
|
|
1139
|
-
if
|
|
1114
|
+
if op == "==":
|
|
1115
|
+
return _eq(left, right)
|
|
1116
|
+
if op == "!=":
|
|
1117
|
+
return not _eq(left, right)
|
|
1118
|
+
if op == "<>":
|
|
1119
|
+
return not _eq(left, right)
|
|
1120
|
+
if op == "<":
|
|
1121
|
+
try:
|
|
1122
|
+
return _lt(left, right)
|
|
1123
|
+
except TypeError:
|
|
1124
|
+
_type_error(left, right)
|
|
1125
|
+
if op == ">":
|
|
1126
|
+
try:
|
|
1127
|
+
return _lt(right, left)
|
|
1128
|
+
except TypeError:
|
|
1129
|
+
_type_error(right, left)
|
|
1130
|
+
if op == ">=":
|
|
1131
|
+
try:
|
|
1132
|
+
return _lt(right, left) or _eq(left, right)
|
|
1133
|
+
except TypeError:
|
|
1134
|
+
_type_error(right, left)
|
|
1135
|
+
if op == "<=":
|
|
1136
|
+
try:
|
|
1137
|
+
return _lt(left, right) or _eq(left, right)
|
|
1138
|
+
except TypeError:
|
|
1139
|
+
_type_error(left, right)
|
|
1140
|
+
if op == "contains":
|
|
1140
1141
|
if isinstance(left, str):
|
|
1141
1142
|
return str(right) in left
|
|
1142
1143
|
if isinstance(left, (list, dict)):
|
|
1143
1144
|
return right in left
|
|
1145
|
+
if isinstance(left, Undefined):
|
|
1146
|
+
return False
|
|
1144
1147
|
|
|
1145
|
-
|
|
1146
|
-
return False
|
|
1148
|
+
return _type_error(left, right)
|
|
1147
1149
|
|
|
1148
|
-
if type(left) in (int, float) and type(right) in (int, float):
|
|
1149
|
-
return eval_number_expression(left, operator, right)
|
|
1150
1150
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1151
|
+
def _eq(left: object, right: object) -> bool:
|
|
1152
|
+
if isinstance(right, (Empty, Blank)):
|
|
1153
|
+
left, right = right, left
|
|
1154
|
+
|
|
1155
|
+
# Remember 1 == True and 0 == False in Python
|
|
1156
|
+
if isinstance(right, bool):
|
|
1157
|
+
left, right = right, left
|
|
1158
|
+
|
|
1159
|
+
if isinstance(left, bool):
|
|
1160
|
+
return isinstance(right, bool) and left == right
|
|
1161
|
+
|
|
1162
|
+
return left == right
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def _lt(left: object, right: object) -> bool:
|
|
1166
|
+
if isinstance(left, str) and isinstance(right, str):
|
|
1167
|
+
return left < right
|
|
1168
|
+
|
|
1169
|
+
if isinstance(left, bool) or isinstance(right, bool):
|
|
1170
|
+
return False
|
|
1171
|
+
|
|
1172
|
+
if isinstance(left, (int, float, Decimal)) and isinstance(
|
|
1173
|
+
right, (int, float, Decimal)
|
|
1174
|
+
):
|
|
1175
|
+
return left < right
|
|
1155
1176
|
|
|
1156
|
-
raise
|
|
1177
|
+
raise TypeError
|
liquid/golden/if_tag.py
CHANGED
|
@@ -219,4 +219,52 @@ cases = [
|
|
|
219
219
|
expect="false",
|
|
220
220
|
error=True,
|
|
221
221
|
),
|
|
222
|
+
Case(
|
|
223
|
+
description="string is less than string",
|
|
224
|
+
template="{% if 'abc' < 'acb' %}true{% else %}false{% endif %}",
|
|
225
|
+
globals={},
|
|
226
|
+
expect="true",
|
|
227
|
+
),
|
|
228
|
+
Case(
|
|
229
|
+
description="string is not less than string",
|
|
230
|
+
template="{% if 'bbb' < 'aaa' %}true{% else %}false{% endif %}",
|
|
231
|
+
globals={},
|
|
232
|
+
expect="false",
|
|
233
|
+
),
|
|
234
|
+
Case(
|
|
235
|
+
description="string is less than or equal to string",
|
|
236
|
+
template="{% if 'abc' <= 'acb' %}true{% else %}false{% endif %}",
|
|
237
|
+
globals={},
|
|
238
|
+
expect="true",
|
|
239
|
+
),
|
|
240
|
+
Case(
|
|
241
|
+
description="string is not less than or equal to string",
|
|
242
|
+
template="{% if 'bbb' <= 'aaa' %}true{% else %}false{% endif %}",
|
|
243
|
+
globals={},
|
|
244
|
+
expect="false",
|
|
245
|
+
),
|
|
246
|
+
Case(
|
|
247
|
+
description="string is greater than string",
|
|
248
|
+
template="{% if 'abc' > 'acb' %}true{% else %}false{% endif %}",
|
|
249
|
+
globals={},
|
|
250
|
+
expect="false",
|
|
251
|
+
),
|
|
252
|
+
Case(
|
|
253
|
+
description="string is not greater than string",
|
|
254
|
+
template="{% if 'bbb' > 'aaa' %}true{% else %}false{% endif %}",
|
|
255
|
+
globals={},
|
|
256
|
+
expect="true",
|
|
257
|
+
),
|
|
258
|
+
Case(
|
|
259
|
+
description="string is greater than or equal to string",
|
|
260
|
+
template="{% if 'abc' >= 'acb' %}true{% else %}false{% endif %}",
|
|
261
|
+
globals={},
|
|
262
|
+
expect="false",
|
|
263
|
+
),
|
|
264
|
+
Case(
|
|
265
|
+
description="string is not greater than or equal to string",
|
|
266
|
+
template="{% if 'bbb' >= 'aaa' %}true{% else %}false{% endif %}",
|
|
267
|
+
globals={},
|
|
268
|
+
expect="true",
|
|
269
|
+
),
|
|
222
270
|
]
|
liquid/loaders.py
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
"""Built-in loaders."""
|
|
2
2
|
from .builtin.loaders import BaseLoader
|
|
3
|
+
from .builtin.loaders import CachingChoiceLoader
|
|
3
4
|
from .builtin.loaders import CachingFileSystemLoader
|
|
4
5
|
from .builtin.loaders import ChoiceLoader
|
|
5
6
|
from .builtin.loaders import DictLoader
|
|
6
7
|
from .builtin.loaders import FileExtensionLoader
|
|
7
8
|
from .builtin.loaders import FileSystemLoader
|
|
9
|
+
from .builtin.loaders import PackageLoader
|
|
8
10
|
from .builtin.loaders import TemplateNamespace
|
|
9
11
|
from .builtin.loaders import TemplateSource
|
|
10
12
|
from .builtin.loaders import UpToDate
|
|
13
|
+
from .builtin.loaders import make_choice_loader
|
|
14
|
+
from .builtin.loaders import make_file_system_loader
|
|
11
15
|
|
|
12
16
|
__all__ = (
|
|
13
17
|
"BaseLoader",
|
|
18
|
+
"CachingChoiceLoader",
|
|
14
19
|
"CachingFileSystemLoader",
|
|
15
20
|
"ChoiceLoader",
|
|
16
21
|
"DictLoader",
|
|
17
22
|
"FileExtensionLoader",
|
|
18
23
|
"FileSystemLoader",
|
|
24
|
+
"make_choice_loader",
|
|
25
|
+
"make_file_system_loader",
|
|
26
|
+
"PackageLoader",
|
|
19
27
|
"TemplateNamespace",
|
|
20
28
|
"TemplateSource",
|
|
21
29
|
"UpToDate",
|
liquid/utils/cache.py
CHANGED
|
@@ -36,6 +36,8 @@ from collections import abc
|
|
|
36
36
|
from collections import deque
|
|
37
37
|
from threading import Lock
|
|
38
38
|
|
|
39
|
+
from liquid.exceptions import CacheCapacityValueError
|
|
40
|
+
|
|
39
41
|
|
|
40
42
|
class LRUCache(abc.MutableMapping):
|
|
41
43
|
"""A simple LRU Cache implementation."""
|
|
@@ -45,6 +47,9 @@ class LRUCache(abc.MutableMapping):
|
|
|
45
47
|
# won't do any harm.
|
|
46
48
|
|
|
47
49
|
def __init__(self, capacity: int):
|
|
50
|
+
if capacity < 1:
|
|
51
|
+
raise CacheCapacityValueError("cache size must be greater than 1")
|
|
52
|
+
|
|
48
53
|
self.capacity = capacity
|
|
49
54
|
self._mapping = {}
|
|
50
55
|
self._queue = deque()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-liquid
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.12.0
|
|
4
4
|
Summary: A Python engine for the Liquid template language.
|
|
5
5
|
Project-URL: Change Log, https://github.com/jg-rp/liquid/blob/main/CHANGES.md
|
|
6
6
|
Project-URL: Documentation, https://jg-rp.github.io/liquid/
|
|
@@ -19,9 +19,11 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.9
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
23
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
23
24
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
24
25
|
Requires-Python: >=3.7
|
|
26
|
+
Requires-Dist: importlib-resources>=5.10.0
|
|
25
27
|
Requires-Dist: python-dateutil>=2.8.1
|
|
26
28
|
Requires-Dist: typing-extensions>=4.2.0
|
|
27
29
|
Provides-Extra: autoescape
|
|
@@ -32,8 +34,8 @@ Description-Content-Type: text/markdown
|
|
|
32
34
|
|
|
33
35
|
<p align="center">
|
|
34
36
|
A Python engine for <a href="https://shopify.github.io/liquid/">Liquid</a>, the safe customer-facing template language for flexible web apps.
|
|
37
|
+
<br>We follow <a href="https://github.com/Shopify/liquid">Shopify/Liquid</a> closely and test against the <a href="https://github.com/jg-rp/golden-liquid">Golden Liquid</a> test suite.
|
|
35
38
|
</p>
|
|
36
|
-
|
|
37
39
|
<p align="center">
|
|
38
40
|
<a href="https://github.com/jg-rp/liquid/blob/main/LICENSE">
|
|
39
41
|
<img src="https://img.shields.io/pypi/l/python-liquid.svg?style=flat-square" alt="License">
|
|
@@ -59,6 +61,10 @@ A Python engine for <a href="https://shopify.github.io/liquid/">Liquid</a>, the
|
|
|
59
61
|
<a href="https://github.com/jg-rp/liquid/actions/workflows/coverage.yaml">
|
|
60
62
|
<img src="https://img.shields.io/github/actions/workflow/status/jg-rp/liquid/coverage.yaml?branch=main&label=coverage&style=flat-square" alt="Coverage">
|
|
61
63
|
</a>
|
|
64
|
+
<br>
|
|
65
|
+
<a href="https://pypi.org/project/python-liquid/">
|
|
66
|
+
<img alt="PyPI - Downloads" src="https://img.shields.io/pypi/dm/python-liquid?style=flat-square">
|
|
67
|
+
</a>
|
|
62
68
|
</p>
|
|
63
69
|
|
|
64
70
|
---
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
liquid/__init__.py,sha256=
|
|
1
|
+
liquid/__init__.py,sha256=0nYsF01jFXyXrxYCLnglEp0H4H7fHG7_tyh91X4td5o,2173
|
|
2
2
|
liquid/analyze_tags.py,sha256=PKMBk4lFWNwkfuMY9ulou6QeSq676IgQmC88JqfeO0k,7503
|
|
3
3
|
liquid/ast.py,sha256=zawW4ryxo_0ExGKpMYUifbtD7F5SlmlMrDJvCVTfWCM,8313
|
|
4
4
|
liquid/chain_map.py,sha256=nxkw3wwF6ddlGarIuL7Ii2elm4dU80LySgdQx1oift0,1517
|
|
5
5
|
liquid/context.py,sha256=cHn0IYhtOM3xlujn1M0fY0KO9WMS8sMNj9AWjTxicZg,26261
|
|
6
6
|
liquid/environment.py,sha256=9PhcexMoKW30pmFzfo8so7pZhweO_WCk6SRQp0w-jdo,30780
|
|
7
|
-
liquid/exceptions.py,sha256=
|
|
8
|
-
liquid/expression.py,sha256=
|
|
7
|
+
liquid/exceptions.py,sha256=gbqQcwJmChp6FCGQdxTWo0mstQtAqmp3-MkUT-evlFI,6691
|
|
8
|
+
liquid/expression.py,sha256=NwI8ZIbfqY8HejYZJiNTRCdu_KvEkPtDD9_PIepsmsk,35252
|
|
9
9
|
liquid/filter.py,sha256=AOBC4cU4eLLiiUvLfb9L0zZvR38ln0BSKqlIy41AUd4,6315
|
|
10
10
|
liquid/lex.py,sha256=pizKyPdIqRNT6TN5NolRhBI3oBpTyoYwJS_DCyyQhg8,15137
|
|
11
11
|
liquid/limits.py,sha256=N3InvvDMg9lTVc6vUhKftV206Oz8D3Pmg_a_jZtgyro,1675
|
|
12
|
-
liquid/loaders.py,sha256=
|
|
12
|
+
liquid/loaders.py,sha256=aXh-hiDyYV6gaBEWRkUORawkhJ4hbBwXP8Sbv9c4j7c,937
|
|
13
13
|
liquid/mode.py,sha256=nYm-oYRkcZk1j-pmYPXMBqfQMMfYYB13SHI2OWHHFr0,207
|
|
14
14
|
liquid/output.py,sha256=QEL_dg4Opb63W0pv1P-4IpUX36uAtuJf9sKxZbVLGxk,773
|
|
15
15
|
liquid/parse.py,sha256=50BT569pjW1sYAofiJ2SVzZgzKlma9lKPStF7EPfE1w,27131
|
|
@@ -33,11 +33,13 @@ liquid/builtin/filters/extra.py,sha256=7_8mUD6DlNLBti-TVCeqzI2NpCmua_OJrwYa5re6c
|
|
|
33
33
|
liquid/builtin/filters/math.py,sha256=ZEzeX5Rw9gTTtIEnR951MXl_1Fv9j2ntfM9EDzmSYlU,3804
|
|
34
34
|
liquid/builtin/filters/misc.py,sha256=z4DXSqidAqZ0F3W5Kr__mebAghDmzYzGr4BzE5-tsFY,3436
|
|
35
35
|
liquid/builtin/filters/string.py,sha256=GJMl2nFcygAz73snVsud4lX0Gk0VhwGbhldeX558SSo,9964
|
|
36
|
-
liquid/builtin/loaders/__init__.py,sha256=
|
|
37
|
-
liquid/builtin/loaders/base_loader.py,sha256=
|
|
38
|
-
liquid/builtin/loaders/caching_file_system_loader.py,sha256=
|
|
39
|
-
liquid/builtin/loaders/choice_loader.py,sha256=
|
|
36
|
+
liquid/builtin/loaders/__init__.py,sha256=9B7KAc-HoNd-pUNwJ-Syzr_8JsBD2S7feYwulUWtrtk,4309
|
|
37
|
+
liquid/builtin/loaders/base_loader.py,sha256=ks_tfzx1t0Dg2gW0Ewr-jHn4KKd5oUWXdREh_n2zHEY,9509
|
|
38
|
+
liquid/builtin/loaders/caching_file_system_loader.py,sha256=3_rGFWv2A3caYmGiaXqjJF-kqEFnU5oZj9R95y1Qc3w,2681
|
|
39
|
+
liquid/builtin/loaders/choice_loader.py,sha256=89rXKA6LR66BqA5ZQEOM9JwRIjPdLUtb3_DyJel8C4I,4566
|
|
40
40
|
liquid/builtin/loaders/file_system_loader.py,sha256=z6nSZih0fDn5aUCWaPlBMqKgAPaGj2NxyZOPHCfTVrg,4396
|
|
41
|
+
liquid/builtin/loaders/mixins.py,sha256=lIlKcjhekb8aoSCuGjJR7mquNP9VjxKUaMu86ONcOSg,6668
|
|
42
|
+
liquid/builtin/loaders/package_loader.py,sha256=xqkl1neXhEPlfjGUlcRzHi3OUpFH4Ht596WOKt04MSw,3234
|
|
41
43
|
liquid/builtin/tags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
44
|
liquid/builtin/tags/assign_tag.py,sha256=0TujfsRE8N8hSjuVpmNQ0HQmmMUbFY89K_mnK2ogdfw,2554
|
|
43
45
|
liquid/builtin/tags/capture_tag.py,sha256=m9Mo7bxarA-Xq_nsrc70aUZJXRVr3WBvoKmCXepy6wk,3136
|
|
@@ -122,7 +124,7 @@ liquid/golden/first_filter.py,sha256=BzyMgvUVZYqDp-qGIXDlncbocXmO4zpnz2gxKCejOfU
|
|
|
122
124
|
liquid/golden/floor_filter.py,sha256=hL7aonQiADdMhvIYRi5NY90MV4GjTSPEVk3ju3xudGQ,1474
|
|
123
125
|
liquid/golden/for_tag.py,sha256=FGUwzLQef_ibBwSszddA-DIMNAesSfyO6CC8TuqYbHE,20459
|
|
124
126
|
liquid/golden/identifiers.py,sha256=XyGUFaMHdvW_q13JrloLaY8AwhSFGIpM78YHeqkeOUc,7364
|
|
125
|
-
liquid/golden/if_tag.py,sha256=
|
|
127
|
+
liquid/golden/if_tag.py,sha256=YmdlIHVGRYZq0lbqFqZtnt8gieZfbr5jh9S-m6TiGkU,8177
|
|
126
128
|
liquid/golden/ifchanged_tag.py,sha256=I048sTPf6p4XHy-RQs6rTVrFkZR1YW9juQxBw7l3i2g,1375
|
|
127
129
|
liquid/golden/illegal.py,sha256=AyJIrNKf435NXT51KBqXlYTYP1t9WUia4e5Zg6iqY8Y,777
|
|
128
130
|
liquid/golden/include_tag.py,sha256=ukzCYwIm9SFA4ZOU7QML2JYB_WB_sF1JnWpNii5aIhk,6842
|
|
@@ -174,11 +176,11 @@ liquid/golden/url_encode_filter.py,sha256=pT6u0Ib44AgIKeHUdNGgpRZNjZzFwRwkvhjY8w
|
|
|
174
176
|
liquid/golden/where_filter.py,sha256=8myDb6AUCMU3qgBihU8-O-MehzeLEXUsXe7q9XGVckg,3858
|
|
175
177
|
liquid/golden/whitespace_control.py,sha256=uAcIIuE1DFWRQhP3fQmI0S3LumMGlIfvWQMIqbxbWMw,7809
|
|
176
178
|
liquid/utils/__init__.py,sha256=X8Dc_jIJKRCccurGqHGqin8IJswNUta-cV9XoRANmEQ,230
|
|
177
|
-
liquid/utils/cache.py,sha256=
|
|
179
|
+
liquid/utils/cache.py,sha256=9fgOrkocCSSQNHNjXmeh0EssK6ZfFmF6pB-0asogSLc,6457
|
|
178
180
|
liquid/utils/cache.pyi,sha256=4Vgtz5vk9IDPnD43AOnby1kWEJJgGH3fsHgQ-B7qa4M,543
|
|
179
181
|
liquid/utils/html.py,sha256=47ACnJLEMSRkDgqDLBujmT091pk0EjoapmlZxkzz3D8,1755
|
|
180
182
|
liquid/utils/text.py,sha256=1SwDECNMaqnnZ05je_AZZgxqzZd6U-mvq5jNU3W1-Qk,841
|
|
181
|
-
python_liquid-1.
|
|
182
|
-
python_liquid-1.
|
|
183
|
-
python_liquid-1.
|
|
184
|
-
python_liquid-1.
|
|
183
|
+
python_liquid-1.12.0.dist-info/METADATA,sha256=z32jYN1wGgKeuwpN8lzbgd1Ck3furve1KTkBD7FARPE,7925
|
|
184
|
+
python_liquid-1.12.0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
|
|
185
|
+
python_liquid-1.12.0.dist-info/licenses/LICENSE,sha256=yAFURzud5ERNHt1rZIPnTLJ92ep7q8y5yG9g5DUMR_E,1075
|
|
186
|
+
python_liquid-1.12.0.dist-info/RECORD,,
|
|
File without changes
|