mirror of https://github.com/estheruary/subjugate
Actually kinda working
parent
b2562d59fe
commit
63b26ca186
@ -1 +1,54 @@
|
|||||||
__version__ = "0.0.1"
|
__version__ = "0.0.1"
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.apps import apps
|
||||||
|
from django.utils.encoding import iri_to_uri
|
||||||
|
from urllib.parse import quote, urljoin
|
||||||
|
from django.utils.lorem_ipsum import paragraphs, words
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
|
||||||
|
class SubjugateTemplate(ABC):
|
||||||
|
def __init__(self, engine, context, request):
|
||||||
|
self.engine = engine
|
||||||
|
self.context = context
|
||||||
|
self.request = request
|
||||||
|
self.vars = SimpleNamespace(**self.context.flatten())
|
||||||
|
|
||||||
|
def extend(self, template_name, **kwargs):
|
||||||
|
return self.engine.get_template(template_name).render(self.context, self.request, **kwargs)
|
||||||
|
|
||||||
|
def csrf_token(self):
|
||||||
|
return self.context.get("csrf_token", "") if self.context else ""
|
||||||
|
|
||||||
|
def url(self, path):
|
||||||
|
return reverse(path)
|
||||||
|
|
||||||
|
def static(self, path):
|
||||||
|
# Damn, I thought doing this was janky as hell but it's official!
|
||||||
|
if apps.is_installed("django.contrib.staticfiles"):
|
||||||
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
|
|
||||||
|
return staticfiles_storage.url(path)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from django.conf import settings
|
||||||
|
except ImportError:
|
||||||
|
prefix = ""
|
||||||
|
else:
|
||||||
|
prefix = iri_to_uri(getattr(settings, "STATIC_URL", ""))
|
||||||
|
return urljoin(prefix, quote(path))
|
||||||
|
|
||||||
|
def filter(self, name, value):
|
||||||
|
return self.engine.filters[name](value)
|
||||||
|
|
||||||
|
def lorem_words(self, count=1, common=True):
|
||||||
|
return words(count, common).split(" ")
|
||||||
|
|
||||||
|
def lorem_paragraphs(self, count=1, common=True):
|
||||||
|
return paragraphs(count, common)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def render(**kwargs):
|
||||||
|
...
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
from importlib.util import module_from_spec
|
||||||
|
from django.template.context import Context, make_context
|
||||||
|
from django.template.exceptions import TemplateSyntaxError
|
||||||
|
|
||||||
|
from subjugate import SubjugateTemplate as SubjugateUserTemplate
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import re
|
||||||
|
import importlib.machinery
|
||||||
|
|
||||||
|
|
||||||
|
class SubjugateTemplate:
|
||||||
|
def find_template_class(self, module, base_class):
|
||||||
|
userclass = next(
|
||||||
|
(cls for _, cls in inspect.getmembers(module, inspect.isclass)
|
||||||
|
if cls.__module__ == module.__name__ and issubclass(cls, base_class)),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not userclass:
|
||||||
|
raise TemplateSyntaxError("Templates must have a subjugate of SubjugateTemplate")
|
||||||
|
|
||||||
|
return userclass
|
||||||
|
|
||||||
|
def code_to_module(self, code, template_name):
|
||||||
|
module_name = self.modulify(template_name)
|
||||||
|
spec = importlib.machinery.ModuleSpec(
|
||||||
|
name=module_name,
|
||||||
|
loader=None,
|
||||||
|
origin=self.origin
|
||||||
|
)
|
||||||
|
module = module_from_spec(spec)
|
||||||
|
exec(code, module.__dict__)
|
||||||
|
return module
|
||||||
|
|
||||||
|
def modulify(self, template_name: str):
|
||||||
|
# Remove any extensions.
|
||||||
|
noext = re.sub(r"\..*$", "", template_name)
|
||||||
|
|
||||||
|
# Split on any non asciibetical characters.
|
||||||
|
words = re.split(r"\W+", noext)
|
||||||
|
|
||||||
|
# Capitalize each word and join them back together.
|
||||||
|
base = ".".join((w.lower() for w in words))
|
||||||
|
|
||||||
|
# Prefix the class to ensure it doesn't start with a number.
|
||||||
|
return f"subjugate.usertemplates.{base}"
|
||||||
|
|
||||||
|
def classify(self, template_name: str):
|
||||||
|
# Remove any extensions.
|
||||||
|
noext = re.sub(r"\..*$", "", template_name)
|
||||||
|
|
||||||
|
# Split on any non asciibetical characters.
|
||||||
|
words = re.split(r"\W+", noext)
|
||||||
|
|
||||||
|
# Capitalize each word and join them back together.
|
||||||
|
base = "".join((w.capitalize() for w in words))
|
||||||
|
|
||||||
|
# Prefix the class to ensure it doesn't start with a number.
|
||||||
|
return f"SubjugateTemplate{base}"
|
||||||
|
|
||||||
|
def __init__(self, contents, origin, template_name, engine):
|
||||||
|
self.contents= contents
|
||||||
|
self.origin = origin
|
||||||
|
self.template_name = template_name
|
||||||
|
self.engine = engine
|
||||||
|
self.module = self.code_to_module(self.contents, template_name)
|
||||||
|
self.userclass = self.find_template_class(self.module, SubjugateUserTemplate)
|
||||||
|
|
||||||
|
def render(self, context, request, **kwargs):
|
||||||
|
if isinstance(context, dict) or context is None:
|
||||||
|
context = make_context(context, request, autoescape=self.engine.autoescape)
|
||||||
|
|
||||||
|
return self.userclass(self.engine, context, request).render(**kwargs)
|
||||||
|
|
||||||
|
def html(self, context=None, request=None):
|
||||||
|
return self.render(context, request).render()
|
@ -0,0 +1,14 @@
|
|||||||
|
from subjugate.template.engine import GenericEngine
|
||||||
|
from subjugate.template import SubjugateTemplate
|
||||||
|
|
||||||
|
class SubjugateTemplates(GenericEngine):
|
||||||
|
template_cls = SubjugateTemplate
|
||||||
|
app_dirname = "subjugate"
|
||||||
|
|
||||||
|
def __init__(self, params):
|
||||||
|
params = params.copy()
|
||||||
|
self.dirs = list(params.pop('DIRS')) or []
|
||||||
|
self.app_dirs = bool(params.pop('APP_DIRS'))
|
||||||
|
options = params.pop('OPTIONS')
|
||||||
|
|
||||||
|
super().__init__(self.dirs, self.app_dirs, **options)
|
@ -0,0 +1,111 @@
|
|||||||
|
from typing import List, Tuple, Union
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
from django.template import Template
|
||||||
|
from django.template.engine import Engine
|
||||||
|
from django.template.backends.django import DjangoTemplates
|
||||||
|
|
||||||
|
from subjugate.template.loaders import GenericLoader
|
||||||
|
|
||||||
|
|
||||||
|
def derive_from_generic(loader_class, template_cls):
|
||||||
|
generic_loader = type(
|
||||||
|
f"{GenericLoader.__name__}{template_cls.__name__}",
|
||||||
|
(GenericLoader,),
|
||||||
|
{"template_cls": template_cls},
|
||||||
|
)
|
||||||
|
|
||||||
|
return type(f"Generic{loader_class.__name__}", (loader_class, generic_loader), {})
|
||||||
|
|
||||||
|
|
||||||
|
# This might seem weird but the engine class MUST be a subclass of
|
||||||
|
# of DjangoTemplates to support cache clearing on modification.
|
||||||
|
#
|
||||||
|
# See autoreload.py
|
||||||
|
class GenericEngine(Engine, DjangoTemplates):
|
||||||
|
template_cls = Template
|
||||||
|
app_dirname = "template"
|
||||||
|
dirs_loader = "django.template.loaders.filesystem.Loader"
|
||||||
|
appdirs_loader = "subjugate.template.loaders.app_directories.Loader"
|
||||||
|
cache_loader = "django.template.loaders.cached.Loader"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dirs=[],
|
||||||
|
app_dirs=False,
|
||||||
|
builtins=None,
|
||||||
|
context_processors=[],
|
||||||
|
file_charset="UTF-8",
|
||||||
|
debug=False,
|
||||||
|
loaders=None,
|
||||||
|
libraries=None,
|
||||||
|
autoescape=True,
|
||||||
|
**_,
|
||||||
|
):
|
||||||
|
self.app_dirs = app_dirs
|
||||||
|
self.context_processors = context_processors
|
||||||
|
self.dirs = dirs
|
||||||
|
self.file_charset = file_charset
|
||||||
|
self.debug = debug
|
||||||
|
self.autoescape = autoescape
|
||||||
|
self.engine = self
|
||||||
|
|
||||||
|
if loaders is None:
|
||||||
|
self.loaders = self.get_default_loaders()
|
||||||
|
else:
|
||||||
|
self.loaders = loaders
|
||||||
|
|
||||||
|
if libraries is None:
|
||||||
|
libraries = {}
|
||||||
|
if builtins is None:
|
||||||
|
builtins = []
|
||||||
|
|
||||||
|
self.libraries = libraries
|
||||||
|
self.template_libraries = self.get_template_libraries(libraries)
|
||||||
|
self.template_builtins = self.get_template_builtins(builtins + self.default_builtins)
|
||||||
|
|
||||||
|
self.filters = {}
|
||||||
|
for lib in self.template_builtins:
|
||||||
|
self.filters.update(lib.filters)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__class__.__qualname__}>"
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def template_context_processors(self):
|
||||||
|
return tuple([import_string(path) for path in self.context_processors])
|
||||||
|
|
||||||
|
def find_template_loader(self, loader):
|
||||||
|
if isinstance(loader, (tuple, list)):
|
||||||
|
loader, *args = loader
|
||||||
|
else:
|
||||||
|
args = []
|
||||||
|
|
||||||
|
if isinstance(loader, str):
|
||||||
|
loader_class = import_string(loader)
|
||||||
|
derived_clss = derive_from_generic(loader_class, self.template_cls)
|
||||||
|
return derived_clss(self, *args)
|
||||||
|
else:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"Invalid value in template loaders configuration: %r" % loader
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_default_loaders(self):
|
||||||
|
loaders: List[Union[str, List, Tuple]] = [self.dirs_loader]
|
||||||
|
if self.app_dirs:
|
||||||
|
loaders.append((self.appdirs_loader, self.app_dirname))
|
||||||
|
|
||||||
|
return [(self.cache_loader, loaders)]
|
||||||
|
|
||||||
|
def get_template_loaders(self, template_loaders):
|
||||||
|
return [ld for tl in template_loaders if (ld := self.find_template_loader(tl))]
|
||||||
|
|
||||||
|
def from_string(self, template_code):
|
||||||
|
self.template_cls(template_code, engine=self)
|
||||||
|
|
||||||
|
def get_template(self, template_name):
|
||||||
|
template, origin = self.find_template(template_name)
|
||||||
|
if not hasattr(template, "render"):
|
||||||
|
template = self.template_cls(template, origin, template_name, engine=self)
|
||||||
|
return template
|
@ -0,0 +1,5 @@
|
|||||||
|
from .base import GenericLoader
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"GenericLoader",
|
||||||
|
]
|
@ -0,0 +1,11 @@
|
|||||||
|
from django.template.loaders import filesystem
|
||||||
|
from django.template.utils import get_app_template_dirs
|
||||||
|
|
||||||
|
|
||||||
|
class Loader(filesystem.Loader):
|
||||||
|
def __init__(self, engine, app_dirname, *args):
|
||||||
|
self.app_dirname = app_dirname
|
||||||
|
super().__init__(engine, *args)
|
||||||
|
|
||||||
|
def get_dirs(self):
|
||||||
|
return get_app_template_dirs(self.app_dirname)
|
@ -0,0 +1,41 @@
|
|||||||
|
from django.template.loaders import base
|
||||||
|
from django.template import TemplateDoesNotExist, Template
|
||||||
|
|
||||||
|
# Despite deriving from base.Loader this class is actually more generic and can
|
||||||
|
# be used to leverage the Django template loader ecosystem for any kind of
|
||||||
|
# template.
|
||||||
|
class GenericLoader(base.Loader):
|
||||||
|
template_cls = Template
|
||||||
|
|
||||||
|
def __init__(self, engine):
|
||||||
|
self.engine = engine
|
||||||
|
|
||||||
|
def get_contents(self, _):
|
||||||
|
return NotImplemented("Subclasses must implement get_contents")
|
||||||
|
|
||||||
|
def get_template(self, template_name, skip=None):
|
||||||
|
tried = []
|
||||||
|
|
||||||
|
for origin in self.get_template_sources(template_name):
|
||||||
|
if skip is not None and origin in skip:
|
||||||
|
tried.append((origin, "Skipped to avoid recursion"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
contents = self.get_contents(origin)
|
||||||
|
except TemplateDoesNotExist:
|
||||||
|
tried.append((origin, "Source does not exist"))
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
return self.template_cls(
|
||||||
|
contents,
|
||||||
|
origin,
|
||||||
|
origin.template_name,
|
||||||
|
self.engine,
|
||||||
|
)
|
||||||
|
|
||||||
|
raise TemplateDoesNotExist(template_name, tried=tried)
|
||||||
|
|
||||||
|
def from_string(self, template_code):
|
||||||
|
return self.template_cls(template_code, self.engine)
|
||||||
|
|
Loading…
Reference in New Issue