Skip to content

Commit

Permalink
Merge pull request #143 from jg-rp/caching-choice-loader
Browse files Browse the repository at this point in the history
Add caching choice loader
  • Loading branch information
jg-rp authored Jan 29, 2024
2 parents 303ab31 + 7c6b9f2 commit b7a6f7b
Show file tree
Hide file tree
Showing 9 changed files with 552 additions and 202 deletions.
2 changes: 2 additions & 0 deletions liquid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .token import Token
from .expression import Expression

from .loaders import CachingChoiceLoader
from .loaders import CachingFileSystemLoader
from .loaders import ChoiceLoader
from .loaders import DictLoader
Expand Down Expand Up @@ -47,6 +48,7 @@
__all__ = (
"AwareBoundTemplate",
"BoundTemplate",
"CachingChoiceLoader",
"CachingFileSystemLoader",
"ChoiceLoader",
"Context",
Expand Down
2 changes: 2 additions & 0 deletions liquid/builtin/loaders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .base_loader import TemplateSource
from .base_loader import UpToDate

from .choice_loader import CachingChoiceLoader
from .choice_loader import ChoiceLoader

from .file_system_loader import FileExtensionLoader
Expand All @@ -13,6 +14,7 @@

__all__ = (
"BaseLoader",
"CachingChoiceLoader",
"CachingFileSystemLoader",
"ChoiceLoader",
"DictLoader",
Expand Down
11 changes: 6 additions & 5 deletions liquid/builtin/loaders/base_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,12 @@ def load(
name: str,
globals: TemplateNamespace = None, # noqa: A002
) -> BoundTemplate:
"""Load and parse a template.
"""Find and parse template source code.
Used internally by `Environment` to load a template source. Delegates to
`get_source`. A custom loaders would typically implement `get_source` rather
than overriding `load`.
This is used internally by `liquid.Environment` to load template
source text. `load()` delegates to `BaseLoader.get_source()`. Custom
loaders would typically implement `get_source()` rather than overriding
`load()`.
"""
try:
source, filename, uptodate, matter = self.get_source(env, name)
Expand All @@ -156,7 +157,7 @@ async def load_async(
name: str,
globals: TemplateNamespace = None, # noqa: A002
) -> BoundTemplate:
"""An async version of `load`."""
"""An async version of `load()`."""
try:
template_source = await self.get_source_async(env, name)
source, filename, uptodate, matter = template_source
Expand Down
173 changes: 10 additions & 163 deletions liquid/builtin/loaders/caching_file_system_loader.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
"""A file system loader that caches parsed templates in memory."""
from __future__ import annotations

from functools import partial
from typing import TYPE_CHECKING
from typing import Awaitable
from typing import Callable
from typing import Iterable
from typing import Mapping
from typing import Union

from liquid.utils import LRUCache

from .file_system_loader import FileExtensionLoader
from .mixins import CachingLoaderMixin

if TYPE_CHECKING:
from pathlib import Path

from liquid import BoundTemplate
from liquid import Context
from liquid import Environment

from .base_loader import TemplateNamespace
from .base_loader import TemplateSource

# ruff: noqa: D102 D101
# ruff: noqa: D102


class CachingFileSystemLoader(FileExtensionLoader):
class CachingFileSystemLoader(CachingLoaderMixin, FileExtensionLoader):
"""A file system loader that caches parsed templates in memory.
Args:
Expand All @@ -45,8 +37,6 @@ class CachingFileSystemLoader(FileExtensionLoader):
the least recently used template.
"""

caching_loader = True

def __init__(
self,
search_path: Union[str, Path, Iterable[Union[str, Path]]],
Expand All @@ -58,160 +48,17 @@ def __init__(
cache_size: int = 300,
):
super().__init__(
auto_reload=auto_reload,
namespace_key=namespace_key,
cache_size=cache_size,
)

FileExtensionLoader.__init__(
self,
search_path=search_path,
encoding=encoding,
ext=ext,
)
self.auto_reload = auto_reload
self.cache = LRUCache(capacity=cache_size)
self.namespace_key = namespace_key

def load(
self,
env: Environment,
name: str,
globals: TemplateNamespace = None, # noqa: A002
) -> BoundTemplate:
return self.check_cache(
env,
name,
globals,
partial(super().load, env, name, globals),
)

async def load_async(
self,
env: Environment,
name: str,
globals: TemplateNamespace = None, # noqa: A002
) -> BoundTemplate:
return await self.check_cache_async(
env,
name,
globals,
partial(super().load_async, env, name, globals),
)

def load_with_args(
self,
env: Environment,
name: str,
globals: TemplateNamespace = None, # noqa: A002
**kwargs: object,
) -> BoundTemplate:
cache_key = self.cache_key(name, kwargs)
return self.check_cache(
env,
cache_key,
globals,
partial(super().load_with_args, env, cache_key, globals, **kwargs),
)

async def load_with_args_async(
self,
env: Environment,
name: str,
globals: TemplateNamespace = None, # noqa: A002
**kwargs: object,
) -> BoundTemplate:
cache_key = self.cache_key(name, kwargs)
return await self.check_cache_async(
env,
cache_key,
globals,
partial(super().load_with_args_async, env, cache_key, globals, **kwargs),
)

def load_with_context(
self, context: Context, name: str, **kwargs: str
) -> BoundTemplate:
cache_key = self.cache_key_with_context(name, context, **kwargs)
return self.check_cache(
context.env,
cache_key,
context.globals,
partial(super().load_with_context, context=context, name=name, **kwargs),
)

async def load_with_context_async(
self, context: Context, name: str, **kwargs: str
) -> BoundTemplate:
cache_key = self.cache_key_with_context(name, context, **kwargs)
return await self.check_cache_async(
context.env,
cache_key,
context.globals,
partial(super().load_with_context_async, context, name, **kwargs),
)

def check_cache(
self,
env: Environment, # noqa: ARG002
cache_key: str,
globals: TemplateNamespace, # noqa: A002
load_func: Callable[[], BoundTemplate],
) -> BoundTemplate:
try:
cached_template: BoundTemplate = self.cache[cache_key]
except KeyError:
template = load_func()
self.cache[cache_key] = template
return template

if self.auto_reload and not cached_template.is_up_to_date:
template = load_func()
self.cache[cache_key] = template
return template

if globals:
cached_template.globals.update(globals)
return cached_template

async def check_cache_async(
self,
env: Environment, # noqa: ARG002
cache_key: str,
globals: TemplateNamespace, # noqa: A002
load_func: Callable[[], Awaitable[BoundTemplate]],
) -> BoundTemplate:
try:
cached_template: BoundTemplate = self.cache[cache_key]
except KeyError:
template = await load_func()
self.cache[cache_key] = template
return template

if self.auto_reload and not await cached_template.is_up_to_date_async():
template = await load_func()
self.cache[cache_key] = template
return template

if globals:
cached_template.globals.update(globals)
return cached_template

def cache_key(self, name: str, args: Mapping[str, object]) -> str:
if not self.namespace_key:
return name

try:
return f"{args[self.namespace_key]}/{name}"
except KeyError:
return name

def cache_key_with_context(
self,
name: str,
context: Context,
**kwargs: str, # noqa: ARG002
) -> str:
if not self.namespace_key:
return name

try:
return f"{context.globals[self.namespace_key]}/{name}"
except KeyError:
return name

def get_source_with_context(
self, context: Context, template_name: str, **kwargs: str
Expand Down
92 changes: 87 additions & 5 deletions liquid/builtin/loaders/choice_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
from liquid.exceptions import TemplateNotFound

from .base_loader import BaseLoader
from .base_loader import TemplateSource
from .mixins import CachingLoaderMixin

if TYPE_CHECKING:
from liquid import Context
from liquid import Environment

from .base_loader import TemplateSource


class ChoiceLoader(BaseLoader):
"""A template loader that will try each of a list of loaders in turn.
Expand All @@ -24,9 +27,8 @@ def __init__(self, loaders: List[BaseLoader]):
super().__init__()
self.loaders = loaders

def get_source( # noqa: D102
self, env: Environment, template_name: str
) -> TemplateSource:
def get_source(self, env: Environment, template_name: str) -> TemplateSource:
"""Get source code for a template from one of the configured loaders."""
for loader in self.loaders:
try:
return loader.get_source(env, template_name)
Expand All @@ -35,15 +37,95 @@ def get_source( # noqa: D102

raise TemplateNotFound(template_name)

async def get_source_async( # noqa: D102
async def get_source_async(
self,
env: Environment,
template_name: str,
) -> TemplateSource:
"""An async version of `get_source`."""
for loader in self.loaders:
try:
return await loader.get_source_async(env, template_name)
except TemplateNotFound:
pass

raise TemplateNotFound(template_name)

def get_source_with_args(
self,
env: Environment,
template_name: str,
**kwargs: object,
) -> TemplateSource:
"""Get source code for a template from one of the configured loaders."""
for loader in self.loaders:
try:
return loader.get_source_with_args(env, template_name, **kwargs)
except TemplateNotFound:
pass

# TODO: include arguments in TemplateNotFound exception.
raise TemplateNotFound(template_name)

async def get_source_with_args_async(
self,
env: Environment,
template_name: str,
**kwargs: object,
) -> TemplateSource:
"""An async version of `get_source_with_args`."""
for loader in self.loaders:
try:
return await loader.get_source_with_args_async(
env, template_name, **kwargs
)
except TemplateNotFound:
pass

raise TemplateNotFound(template_name)

def get_source_with_context(
self, context: Context, template_name: str, **kwargs: str
) -> TemplateSource:
"""Get source code for a template from one of the configured loaders."""
for loader in self.loaders:
try:
return loader.get_source_with_context(context, template_name, **kwargs)
except TemplateNotFound:
pass

raise TemplateNotFound(template_name)

async def get_source_with_context_async(
self, context: Context, template_name: str, **kwargs: str
) -> TemplateSource:
"""Get source code for a template from one of the configured loaders."""
for loader in self.loaders:
try:
return await loader.get_source_with_context_async(
context, template_name, **kwargs
)
except TemplateNotFound:
pass

raise TemplateNotFound(template_name)


class CachingChoiceLoader(CachingLoaderMixin, ChoiceLoader):
"""A `ChoiceLoader` that caches parsed templates in memory."""

def __init__(
self,
loaders: List[BaseLoader],
*,
auto_reload: bool = True,
namespace_key: str = "",
cache_size: int = 300,
):
super().__init__(
auto_reload=auto_reload,
namespace_key=namespace_key,
cache_size=cache_size,
)

ChoiceLoader.__init__(self, loaders)
Loading

0 comments on commit b7a6f7b

Please sign in to comment.