mirror of https://github.com/estheruary/subjugate
Actually kinda working
parent
b2562d59fe
commit
63b26ca186
@ -1 +1,54 @@
|
||||
__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