Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Caching file system loader #116

Merged
merged 4 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

- Added context-aware template loader bootstrapping methods. See [#109](https://github.com/jg-rp/liquid/pull/109).
- Added support for async class-based filters. See [#115](https://github.com/jg-rp/liquid/pull/115).
- Added `CachingFileSystemLoader`, a template loader that handles its own cache rather than relying on the `Environment` template cache, which can't handle context-aware loaders. See [#116](https://github.com/jg-rp/liquid/pull/116).

## Version 1.8.1

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<h1 align="center">Python Liquid</h1>

<p align="center">
A Python implementation of <a href="https://shopify.github.io/liquid/">Liquid</a>, the safe customer-facing template language for flexible web apps.
A Python engine for <a href="https://shopify.github.io/liquid/">Liquid</a>, the safe customer-facing template language for flexible web apps.
</p>

<p align="center">
Expand Down
3 changes: 3 additions & 0 deletions docs/loaders.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Template Loaders

::: liquid.loaders.CachingFileSystemLoader
handler: python

::: liquid.loaders.FileSystemLoader
handler: python

Expand Down
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 CachingFileSystemLoader
from .loaders import ChoiceLoader
from .loaders import DictLoader
from .loaders import FileExtensionLoader
Expand Down Expand Up @@ -44,6 +45,7 @@
__all__ = (
"AwareBoundTemplate",
"BoundTemplate",
"CachingFileSystemLoader",
"ChoiceLoader",
"Context",
"ContextualTemplateAnalysis",
Expand Down
24 changes: 24 additions & 0 deletions liquid/builtin/loaders/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from .base_loader import BaseLoader
from .base_loader import DictLoader
from .base_loader import TemplateNamespace
from .base_loader import TemplateSource
from .base_loader import UpToDate

from .choice_loader import ChoiceLoader

from .file_system_loader import FileExtensionLoader
from .file_system_loader import FileSystemLoader

from .caching_file_system_loader import CachingFileSystemLoader

__all__ = (
"BaseLoader",
"CachingFileSystemLoader",
"ChoiceLoader",
"DictLoader",
"FileExtensionLoader",
"FileSystemLoader",
"TemplateNamespace",
"TemplateSource",
"UpToDate",
)
300 changes: 300 additions & 0 deletions liquid/builtin/loaders/base_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
"""Base template loader."""
from __future__ import annotations

from abc import ABC
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Awaitable
from typing import Callable
from typing import Dict
from typing import Mapping
from typing import NamedTuple
from typing import Optional
from typing import Union

from liquid.exceptions import TemplateNotFound

if TYPE_CHECKING:
from liquid import Context
from liquid import Environment
from liquid.template import BoundTemplate

# ruff: noqa: D102 D101

TemplateNamespace = Optional[Mapping[str, object]]
UpToDate = Union[Callable[[], bool], Callable[[], Awaitable[bool]], None]


class TemplateSource(NamedTuple):
"""A Liquid template source as returned by the `get_source` method of a `loader`.

Attributes:
source: The liquid template source code.
filename: The liquid template file name or other string identifying its origin.
uptodate: Optional callable that will return `True` if the template is up to
date, or `False` if it needs to be reloaded.
matter: Optional mapping containing variables associated with the template.
Could be "front matter" or other meta data.
"""

source: str
filename: str
uptodate: UpToDate
matter: TemplateNamespace = None


class BaseLoader(ABC): # noqa: B024
"""Base template loader from which all template loaders are derived.

Attributes:
caching_loader (bool): Indicates if this loader implements its own cache.
Setting this sto `True` will cause the `Environment` to disable its cache
when initialized with a caching loader.
"""

caching_loader = False

def get_source(
self,
env: Environment,
template_name: str,
) -> TemplateSource:
"""Get the template source, filename and reload helper for a template.

Args:
env: The `Environment` attempting to load the template source text.
template_name: A name or identifier for a template's source text.
"""
raise NotImplementedError("template loaders must implement a get_source method")

async def get_source_async(
self,
env: Environment,
template_name: str,
) -> TemplateSource:
"""An async version of `get_source`.

The default implementation delegates to `get_source()`.
"""
return self.get_source(env, template_name)

def get_source_with_args(
self,
env: Environment,
template_name: str,
**kwargs: object, # noqa: ARG002
) -> TemplateSource:
"""Get template source text, optionally referencing arbitrary keyword arguments.

Keyword arguments can be useful for multi-user environments where you need to
modify a template loader's search space for a given user.

By default, this method delegates to `get_source`, ignoring any keyword
arguments.

_New in version 1.9.0._
"""
return self.get_source(env, 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`.

_New in version 1.9.0._
"""
return self.get_source_with_args(env, template_name, **kwargs)

def get_source_with_context(
self,
context: Context,
template_name: str,
**kwargs: str, # noqa: ARG002
) -> TemplateSource:
"""Get a template's source, optionally referencing a render context."""
return self.get_source(context.env, template_name)

async def get_source_with_context_async(
self,
context: Context,
template_name: str,
**kwargs: str, # noqa: ARG002
) -> TemplateSource:
"""An async version of `get_source_with_context`."""
return await self.get_source_async(context.env, template_name)

def load(
self,
env: Environment,
name: str,
globals: TemplateNamespace = None, # noqa: A002
) -> BoundTemplate:
"""Load and parse a template.

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`.
"""
try:
source, filename, uptodate, matter = self.get_source(env, name)
except Exception as err: # noqa: BLE001
raise TemplateNotFound(name) from err

template = env.from_string(
source,
globals=globals,
name=name,
path=Path(filename),
matter=matter,
)
template.uptodate = uptodate
return template

async def load_async(
self,
env: Environment,
name: str,
globals: TemplateNamespace = None, # noqa: A002
) -> BoundTemplate:
"""An async version of `load`."""
try:
template_source = await self.get_source_async(env, name)
source, filename, uptodate, matter = template_source
except Exception as err: # noqa: BLE001
raise TemplateNotFound(name) from err

template = env.from_string(
source,
globals=globals,
name=name,
path=Path(filename),
matter=matter,
)
template.uptodate = uptodate
return template

def load_with_args(
self,
env: Environment,
name: str,
globals: TemplateNamespace = None, # noqa: A002
**kwargs: object,
) -> BoundTemplate:
"""Load template source text, optionally referencing extra keyword arguments.

Most custom loaders will want to override `get_source_with_args()` rather than
this method. For example, you might want to override `load_with_args()` and
`get_source_with_args()` when implementing a custom caching loader. Where cache
handling happens in `load_*` methods.
"""
try:
source, filename, uptodate, matter = self.get_source_with_args(
env, name, **kwargs
)
except Exception as err: # noqa: BLE001
raise TemplateNotFound(name) from err

template = env.from_string(
source,
globals=globals,
name=name,
path=Path(filename),
matter=matter,
)
template.uptodate = uptodate
return template

async def load_with_args_async(
self,
env: Environment,
name: str,
globals: TemplateNamespace = None, # noqa: A002
**kwargs: object,
) -> BoundTemplate:
"""An async version of `load_with_args()`."""
try:
template_source = await self.get_source_with_args_async(env, name, **kwargs)
source, filename, uptodate, matter = template_source
except Exception as err: # noqa: BLE001
raise TemplateNotFound(name) from err

template = env.from_string(
source,
globals=globals,
name=name,
path=Path(filename),
matter=matter,
)
template.uptodate = uptodate
return template

def load_with_context(
self,
context: Context,
name: str,
**kwargs: str,
) -> BoundTemplate:
"""Load and parse a template, optionally referencing a render context."""
try:
source, filename, uptodate, matter = self.get_source_with_context(
context, name, **kwargs
)
except Exception as err: # noqa: BLE001
raise TemplateNotFound(name) from err

template = context.env.from_string(
source,
globals=context.globals,
name=name,
path=Path(filename),
matter=matter,
)
template.uptodate = uptodate
return template

async def load_with_context_async(
self,
context: Context,
name: str,
**kwargs: str,
) -> BoundTemplate:
"""An async version of `load_with_context`."""
try:
(
source,
filename,
uptodate,
matter,
) = await self.get_source_with_context_async(context, name, **kwargs)
except Exception as err: # noqa: BLE001
raise TemplateNotFound(name) from err

template = context.env.from_string(
source,
globals=context.globals,
name=name,
path=Path(filename),
matter=matter,
)
template.uptodate = uptodate
return template


class DictLoader(BaseLoader):
"""A loader that loads templates from a dictionary.

Args:
templates: A dictionary mapping template names to template source strings.
"""

def __init__(self, templates: Dict[str, str]):
super().__init__()
self.templates = templates

def get_source(self, _: Environment, template_name: str) -> TemplateSource:
try:
source = self.templates[template_name]
except KeyError as err:
raise TemplateNotFound(template_name) from err

return TemplateSource(source, template_name, None)
Loading