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

Jinja support #247

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
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
48 changes: 48 additions & 0 deletions pattern_library/loader_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,51 @@ def do_include(parser, token):
extra_context=namemap,
isolated_context=isolated_context,
)


def visit_extends(self, node, frame):
"""This method overrides the jinja extends tag
Is called as part of the compiler CodeGenerator
and adds a line to use the template_new_context as
part of the runtime render to pull in the dpl context
Handles visiting extends
"""
from .monkey_utils import jinja_visit_Extends

jinja_visit_Extends(self, node, frame)
# addition to update the context with dpl context
# calls the template_new_context method below when
# invoked at runtime
self.writeline(
"parent_template.new_context(context.get_all(), True,"
f" {self.dump_local_context(frame)})"
)


def template_new_context(
self,
vars=None,
shared=False,
locals=None,
):
"""This method overrides the jinja include tag
Is called as part of Template.render by jinja2 and is updated
to pull in the dpl context
Create a new :class:`Context` for this template. The vars
provided will be passed to the template. Per default the globals
are added to the context. If shared is set to `True` the data
is passed as is to the context without adding the globals.

`locals` can be a dict of local variables for internal usage.
"""
from jinja2.runtime import new_context

if is_pattern_library_context(vars or {}) and (
pattern_context := get_pattern_context(self.name)
):
for k, v in pattern_context.items():
vars.setdefault(k, v)

return new_context(
self.environment, self.name, self.blocks, vars, shared, self.globals, locals
)
13 changes: 5 additions & 8 deletions pattern_library/management/commands/render_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@
from django.test.client import RequestFactory

from pattern_library import get_base_template_names, get_pattern_base_template_name
from pattern_library.utils import (
get_pattern_context,
get_pattern_templates,
get_template_ancestors,
render_pattern,
)
from pattern_library.utils import get_pattern_context, get_renderer, render_pattern


class Command(BaseCommand):
Expand Down Expand Up @@ -44,7 +39,8 @@ def handle(self, **options):
self.wrap_fragments = options["wrap_fragments"]
self.output_dir = options["output_dir"]

templates = get_pattern_templates()
renderer = get_renderer()
templates = renderer.get_pattern_templates()

factory = RequestFactory()
request = factory.get("/")
Expand Down Expand Up @@ -106,7 +102,8 @@ def render_pattern(self, request, pattern_template_name):
if not self.wrap_fragments:
return rendered_pattern

pattern_template_ancestors = get_template_ancestors(
renderer = get_renderer()
pattern_template_ancestors = renderer.get_template_ancestors(
pattern_template_name,
context=get_pattern_context(pattern_template_name),
)
Expand Down
24 changes: 24 additions & 0 deletions pattern_library/monkey_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,27 @@ def node_render(context):
return original_node

return tag_func


# have to export the original jinja visit Extends
# in the case jinja tags are being overriden
jinja_visit_Extends = None


def override_jinja_tags():
"""
Overrides jinja extends and include tags for use in your pattern library.
Call it in your settings to override tags
"""
global jinja_visit_Extends
try:
from jinja2.compiler import CodeGenerator as JinjaCodeGenerator
from jinja2.environment import Template as JinjaTemplate
except ModuleNotFoundError:
ModuleNotFoundError("install jinja2 to override jinja tags")

from .loader_tags import template_new_context, visit_extends

jinja_visit_Extends = JinjaCodeGenerator.visit_Extends
JinjaTemplate.new_context = template_new_context
JinjaCodeGenerator.visit_Extends = visit_extends
243 changes: 155 additions & 88 deletions pattern_library/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.template.loader import get_template, render_to_string
from django.template.loader_tags import ExtendsNode
from django.template.loaders.app_directories import get_app_template_dirs
from django.utils.html import escape
from django.utils.safestring import mark_safe

import markdown
Expand Down Expand Up @@ -79,77 +80,6 @@ def get_template_dirs():
return template_dirs


def get_pattern_templates():
templates = base_dict()
template_dirs = get_template_dirs()

for lookup_dir in template_dirs:
for root, dirs, files in os.walk(lookup_dir, topdown=True):
# Ignore folders without files
if not files:
continue

base_path = os.path.relpath(root, lookup_dir)
section, path = section_for(base_path)

# It has no section, ignore it
if not section:
continue

found_templates = []
for current_file in files:
pattern_path = os.path.join(root, current_file)
pattern_path = os.path.relpath(pattern_path, lookup_dir)

if is_pattern(pattern_path):
template = get_template(pattern_path)
pattern_config = get_pattern_config(pattern_path)
pattern_name = pattern_config.get("name")
pattern_filename = os.path.relpath(
template.origin.template_name,
base_path,
)
if pattern_name:
template.pattern_name = pattern_name
else:
template.pattern_name = pattern_filename

template.pattern_filename = pattern_filename

found_templates.append(template)

if found_templates:
lookup_dir_relpath = os.path.relpath(root, lookup_dir)
sub_folders = os.path.relpath(lookup_dir_relpath, path)
templates_to_store = templates
for folder in [section, *sub_folders.split(os.sep)]:
try:
templates_to_store = templates_to_store["template_groups"][
folder
]
except KeyError:
templates_to_store["template_groups"][folder] = base_dict()
templates_to_store = templates_to_store["template_groups"][
folder
]

templates_to_store["templates_stored"].extend(found_templates)

# Order the templates alphabetically
for templates_objs in templates["template_groups"].values():
templates_objs["template_groups"] = order_dict(
templates_objs["template_groups"]
)

# Order the top level by the sections
section_order = [section for section, _ in get_sections()]
templates["template_groups"] = order_dict(
templates["template_groups"], key_sort=lambda key: section_order.index(key)
)

return templates


def get_pattern_config_str(template_name):
replace_pattern = "{}$".format(get_pattern_template_suffix())
context_path = re.sub(replace_pattern, "", template_name)
Expand Down Expand Up @@ -227,27 +157,164 @@ def render_pattern(request, template_name, allow_non_patterns=False, config=None
return render_to_string(template_name, request=request, context=context)


def get_template_ancestors(template_name, context=None, ancestors=None):
"""
Returns a list of template names, starting with provided name
and followed by the names of any templates that extends until
the most extended template is reached.
"""
if ancestors is None:
ancestors = [template_name]
def get_renderer():
return TemplateRenderer


class TemplateRenderer:
@classmethod
def get_pattern_templates(cls):
templates = base_dict()
template_dirs = get_template_dirs()

for lookup_dir in template_dirs:
for root, dirs, files in os.walk(lookup_dir, topdown=True):
# Ignore folders without files
if not files:
continue

base_path = os.path.relpath(root, lookup_dir)
section, path = section_for(base_path)

# It has no section, ignore it
if not section:
continue

found_templates = []
for current_file in files:
pattern_path = os.path.join(root, current_file)
pattern_path = os.path.relpath(pattern_path, lookup_dir)

if is_pattern(pattern_path):
template = get_template(pattern_path)
pattern_config = get_pattern_config(pattern_path)
pattern_name = pattern_config.get("name")
pattern_filename = os.path.relpath(
template.origin.template_name,
base_path,
)
if pattern_name:
template.pattern_name = pattern_name
else:
template.pattern_name = pattern_filename

template.pattern_filename = pattern_filename

found_templates.append(template)

if found_templates:
lookup_dir_relpath = os.path.relpath(root, lookup_dir)
sub_folders = os.path.relpath(lookup_dir_relpath, path)
templates_to_store = templates
for folder in [section, *sub_folders.split(os.sep)]:
try:
templates_to_store = templates_to_store["template_groups"][
folder
]
except KeyError:
templates_to_store["template_groups"][folder] = base_dict()

templates_to_store = templates_to_store["template_groups"][
folder
]

templates_to_store["templates_stored"].extend(found_templates)

# Order the templates alphabetically
for templates_objs in templates["template_groups"].values():
templates_objs["template_groups"] = order_dict(
templates_objs["template_groups"]
)

# Order the top level by the sections
section_order = [section for section, _ in get_sections()]
templates["template_groups"] = order_dict(
templates["template_groups"], key_sort=lambda key: section_order.index(key)
)

if context is None:
context = Context()
return templates

pattern_template = get_template(template_name)
@classmethod
def get_pattern_source(cls, template):
return cls._get_engine(template).get_pattern_source(template)

@classmethod
def get_template_ancestors(cls, template_name, context=None):
template = get_template(template_name)
return cls._get_engine(template).get_template_ancestors(
template_name, context=context
)

for node in pattern_template.template.nodelist:
if isinstance(node, ExtendsNode):
parent_template_name = node.parent_name.resolve(context)
@classmethod
def _get_engine(cls, template):
if "jinja" in str(type(template)).lower():
return JinjaTemplateRenderer
return DTLTemplateRenderer


class DTLTemplateRenderer:
@staticmethod
def get_pattern_source(template):
return escape(template.template.source)

@classmethod
def get_template_ancestors(cls, template_name, context=None, ancestors=None):
"""
Returns a list of template names, starting with provided name
and followed by the names of any templates that extends until
the most extended template is reached.
"""
if ancestors is None:
ancestors = [template_name]

if context is None:
context = Context()

pattern_template = get_template(template_name)

for node in pattern_template.template.nodelist:
if isinstance(node, ExtendsNode):
parent_template_name = node.parent_name.resolve(context)
ancestors.append(parent_template_name)
cls.get_template_ancestors(
parent_template_name, context=context, ancestors=ancestors
)
break

return ancestors


class JinjaTemplateRenderer:
@staticmethod
def get_pattern_source(template):
with open(template.template.filename) as f:
source = escape(f.read())
return source

@classmethod
def get_template_ancestors(cls, template_name, context=None, ancestors=None):
"""
Returns a list of template names, starting with provided name
and followed by the names of any templates that extends until
the most extended template is reached.
"""
from jinja2.nodes import Extends

if ancestors is None:
ancestors = [template_name]

if context is None:
context = Context()

pattern_template = get_template(template_name)
# todo - make sure envrionment has context passed in
environment = pattern_template.template.environment
nodelist = environment.parse(pattern_template.template.name)
parent_template_name = nodelist.find(Extends)
if parent_template_name:
ancestors.append(parent_template_name)
get_template_ancestors(
cls.get_template_ancestors(
parent_template_name, context=context, ancestors=ancestors
)
break

return ancestors
return ancestors
Loading
Loading