Source code for duck.template.templatetags

"""
Module for creating user custom template tags and filters.
"""

from collections import defaultdict
from typing import Callable

from jinja2 import nodes
from jinja2.ext import Extension

from duck.exceptions.all import SettingsError
from duck.utils.importer import import_module_once
from duck.utils.safemarkup import MarkupSafeString


[docs] class TemplateTagError(Exception): """ Exception for errors related to the TemplateTag class. """
[docs] class TemplateFilterError(Exception): """ Exception for errors related to the TemplateFilter class. """
[docs] class TemplateTag: """ Base class for defining and registering template tags. This class provides a unified way to register template tags in templating engines like **Django** or **Jinja2**. It supports callable tags, allowing flexible logic to be associated with the tag. **Usage:** 1. Define a callable function to handle the tag logic: ```py def do_something(arg): # Perform some operations with the argument return f"Processed: {arg}" ``` 2. Register the callable as a template tag: ```py tag = TemplateTag("my_tag", tagcallable=do_something) ``` **Supported Syntax:** - Django: `{% my_tag arg1 arg2 %}` - Jinja2: `{{ my_tag(arg1, arg2) }}` """ __all_tags = defaultdict(str) # Dictionary for all created tags def __init__(self, tagname: str, tagcallable: Callable, takes_context: bool = False ): self.tagname = tagname self.tagcallable = tagcallable self.takes_context = takes_context assert isinstance(tagname, str), f"Argument 'tagname' should be an instance of string not {type(tagname)}" assert callable(tagcallable), f"Argument 'tagcallable' should be a callable not {type(tagcallable)}" assert isinstance(takes_context, bool), f"Argument 'takes_context' should be a boolean not {type(takes_context)}" if not self.__all_tags[tagname]: type(self).__all_tags[tagname] = self else: raise TemplateTagError(f"Repeated template tag '{tagname}', already exists")
[docs] @classmethod def get_tag(cls, tagname: str): """ Retreive a created Template Tag with provided name. Raises: TemplateTagError: If tag with provided name does'nt exist. """ tag = cls.__all_tags.get(tagname) if not tag: raise TemplateTagError(f"Template tag with name '{tagname}' was never created.") return tag
[docs] def register_in_django(self, library): """ Register this tag in a Django template library. Args: library: The Django template library to register the tag with. """ if self.takes_context: @library.simple_tag(name=self.tagname, takes_context=self.takes_context) def django_tag_wrapper(context, *args, **kwargs): return self.tagcallable(context, *args, **kwargs) else: @library.simple_tag(name=self.tagname, takes_context=self.takes_context) def django_tag_wrapper(*args, **kwargs): return self.tagcallable(*args, **kwargs)
[docs] def register_in_jinja2(self, environment): """ Register this tag in a Jinja2 environment. Args: environment: The Jinja2 environment to register the tag with. """ from jinja2 import pass_context if self.takes_context: @pass_context def jinja2_tag_wrapper(*args, **kwargs): return self.tagcallable(*args, **kwargs) environment.globals[self.tagname] = jinja2_tag_wrapper else: environment.globals[self.tagname] = self.tagcallable
[docs] def __repr__(self): """ Returns a string representation of the TemplateTag. Returns: str: String representation of the TemplateTag. """ return f'<{self.__class__.__name__} "{self.tagname}" takes_context={self.takes_context}>'
[docs] class BlockTemplateTag(TemplateTag): """ Template tag which acts as a block level template tag. To use this, name and tagcallable arguments should be provided. **Notes:** - The tagcallable should be a callable object accepting context as first argument if takes_context=True, and the second argument as the content wrapped between the tags. - The tagcallable should always return the new modified content after processing. **Example:** ```py def do_something(content): # process and modify content logic return content tag = BlockTemplateTag("some_name", tagcallable=do_something) ``` """
[docs] def register_in_django(self, library): """ Register this tag in a Django template library. Args: library: The Django template library to register the tag with. """ from django import template root_tag = self class CustomBlockNode(template.Node): """ CustomBlockNode class for all block level tags. """ def __init__(self, nodelist, *args, **kwargs): self.nodelist = nodelist self.args = args self.kwargs = kwargs def render(self, context): # Render the block content content = self.nodelist.render(context) if root_tag.takes_context: return root_tag.tagcallable(context, content) return root_tag.tagcallable(content) @library.tag(name=self.tagname) def django_tag_wrapper(parser, token): try: # Parse the tag's arguments and keyword arguments tokens = token.split_contents() tag_name = tokens.pop(0) # Remove the tag name args = [] kwargs = {} for token in tokens: if "=" in token: key, value = token.split("=", 1) kwargs[key] = parser.compile_filter(value) # Compile for runtime resolution else: args.append(parser.compile_filter(token)) # Compile for runtime resolution # Parse the block content nodelist = parser.parse((f"end{tag_name}",)) parser.delete_first_token() # Return the custom block node return CustomBlockNode(nodelist, *args, **kwargs) except Exception as e: raise template.TemplateSyntaxError(f"Error parsing custom block tag: {e}")
[docs] def register_in_jinja2(self, environment): """ Register this tag as a block-level tag in a Jinja2 environment. Args: environment: The Jinja2 environment to register the tag with. """ root_tag = self class CustomBlockExtension(Extension): # Define the tag name tags = {root_tag.tagname} def __init__(self, environment): super(CustomBlockExtension, self).__init__(environment) def parse(self, parser): lineno = next(parser.stream).lineno body = parser.parse_statements([f'name:end{root_tag.tagname}'], drop_needle=True) return nodes.CallBlock(self.call_method('_render_customblock', [nodes.ContextReference()]), [], [], body).set_lineno(lineno) def _render_customblock(self, context, caller): # Access context data here content = caller() # Process the content as needed if root_tag.takes_context: return root_tag.tagcallable(context, content) return root_tag.tagcallable(content) # Dynamically register the extension CustomBlockTagClass = type(f"CustomBlockTagExtension_{root_tag.tagname}", (CustomBlockExtension,), {}) environment.add_extension(CustomBlockTagClass)
[docs] class TemplateFilter: """ Base Class for template Filter. This will be used to register this filter to a template. **Notes:** - TemplateFilter does not support takes_context. You need to parse the context directly if you need it. Example: ```django {{ variable | myfilter:context }} ``` **Form:** ```django {{ variable | myfilter }} ``` """ __all_filters = defaultdict(str) # Dictionary for all created filters def __init__(self, filtername: str, filtercallable: Callable): self.filtername = filtername self.filtercallable = filtercallable # Improved assertions with clearer error messages and more explicit checks assert isinstance(filtername, str), f"Argument 'filtername' should be a string, but got {type(filtername).__name__} instead." assert filtername, "Argument 'filtername' cannot be an empty string." assert callable(filtercallable), f"Argument 'filtercallable' should be callable, but got {type(filtercallable).__name__} instead." if not self.__all_filters[filtername]: type(self).__all_filters[filtername] = self else: raise TemplateFilterError(f"Repeated template filter '{filtername}', already exists")
[docs] @classmethod def get_filter(cls, filtername: str): """ Retreive a created Template Filter with provided name. Raises: TemplateFilterError: If filter with provided name does'nt exist. """ filter = cls.__all_filters.get(filtername) if not filter: raise TemplateFilterError( f"Template Filter with name '{filtername}' was never created.") return filter
[docs] def register_in_django(self, library): """ Register this filter in a Django template library. Args: library: The Django template library to register the filter with. """ @library.filter(name=self.filtername) def django_filter_wrapper(_obj, *args, **kwargs) -> MarkupSafeString: return self.filtercallable(_obj, *args, **kwargs)
[docs] def register_in_jinja2(self, environment): """ Register this filter in a Jinja2 environment. Args: environment: The Jinja2 environment to register the filter with. """ environment.filters[self.filtername] = self.filtercallable
[docs] def __repr__(self): """ Returns a string representation of the TemplateFilter. Returns: str: String representation of the TemplateFilter. """ return f'<{self.__class__.__name__} "{self.filtername}">'