From 28795063c74de6c2f44519918efc5afde4e285df Mon Sep 17 00:00:00 2001 From: Ben Beecher Date: Mon, 13 Jun 2022 19:11:53 -0400 Subject: [PATCH 01/18] First pass at renderers --- pattern_library/utils.py | 232 ++++++++++++++++++++++++--------------- pattern_library/views.py | 11 +- 2 files changed, 150 insertions(+), 93 deletions(-) diff --git a/pattern_library/utils.py b/pattern_library/utils.py index b1a81b8d..901785de 100644 --- a/pattern_library/utils.py +++ b/pattern_library/utils.py @@ -22,6 +22,9 @@ from pattern_library.exceptions import TemplateIsNotPattern + +from django.utils.html import escape + def path_to_section(): section_config = get_sections() sections = {} @@ -79,75 +82,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): @@ -227,27 +161,149 @@ 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 JinjaTemplateRenderer + + +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) + ) + + return templates + +class DTLTemplateRenderer(TemplateRenderer): + @classmethod + def get_pattern_source(cls, 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() + if context is None: + context = Context() - pattern_template = get_template(template_name) + 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) + 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(TemplateRenderer): + + @classmethod + def get_pattern_source(cls, 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.name) + parent_template_name = nodelist.find(Extends) + if parent_template_name: ancestors.append(parent_template_name) - get_template_ancestors( - parent_template_name, context=context, ancestors=ancestors - ) - break + cls.get_template_ancestors(parent_template_name, context=context, ancestors=ancestors) - return ancestors + return ancestors +>>>>>>> 68fea3c (First pass at renderers) diff --git a/pattern_library/views.py b/pattern_library/views.py index 4e3ab01d..030c1d8a 100644 --- a/pattern_library/views.py +++ b/pattern_library/views.py @@ -15,11 +15,10 @@ get_pattern_config_str, get_pattern_context, get_pattern_markdown, - get_pattern_templates, get_sections, - get_template_ancestors, is_pattern, render_pattern, + get_renderer, ) @@ -52,7 +51,8 @@ def get_first_template(self, templates): def get(self, request, pattern_template_name=None): # Get all pattern templates - templates = get_pattern_templates() + renderer = get_renderer() + templates = renderer.get_pattern_templates() if pattern_template_name is None: # Just display the first pattern if a specific one isn't requested @@ -67,7 +67,7 @@ def get(self, request, pattern_template_name=None): context = self.get_context_data() context["pattern_templates"] = templates context["pattern_template_name"] = pattern_template_name - context["pattern_source"] = escape(template.template.source) + context["pattern_source"] = renderer.get_pattern_source(template) context["pattern_config"] = escape( get_pattern_config_str(pattern_template_name) ) @@ -83,7 +83,8 @@ class RenderPatternView(TemplateView): @method_decorator(xframe_options_sameorigin) def get(self, request, pattern_template_name=None): - pattern_template_ancestors = get_template_ancestors( + renderer = get_renderer() + pattern_template_ancestors = renderer.get_template_ancestors( pattern_template_name, context=get_pattern_context(self.kwargs["pattern_template_name"]), ) From 09bb3cba7a193de4a51a1937f6c630ecadf443a1 Mon Sep 17 00:00:00 2001 From: Luis Orduz Date: Thu, 30 Jun 2022 15:24:58 -0500 Subject: [PATCH 02/18] Moving from loading at the global level to loading at the template level --- pattern_library/utils.py | 42 ++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/pattern_library/utils.py b/pattern_library/utils.py index 901785de..93aae474 100644 --- a/pattern_library/utils.py +++ b/pattern_library/utils.py @@ -82,8 +82,6 @@ def get_template_dirs(): return template_dirs - - def get_pattern_config_str(template_name): replace_pattern = "{}$".format(get_pattern_template_suffix()) context_path = re.sub(replace_pattern, "", template_name) @@ -162,13 +160,12 @@ def render_pattern(request, template_name, allow_non_patterns=False, config=None def get_renderer(): - return JinjaTemplateRenderer + return TemplateRenderer class TemplateRenderer: - @classmethod - def get_pattern_templates(cls,): + def get_pattern_templates(cls): templates = base_dict() template_dirs = get_template_dirs() @@ -239,13 +236,28 @@ def get_pattern_templates(cls,): return templates -class DTLTemplateRenderer(TemplateRenderer): @classmethod def get_pattern_source(cls, template): - return escape(template.template.source) + return cls._get_engine(template).get_pattern_source(template) @classmethod - def get_template_ancestors(cls, template_name, context=None, ancestors=None): + 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) + + @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) + + @staticmethod + 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 @@ -271,18 +283,15 @@ def get_template_ancestors(cls, template_name, context=None, ancestors=None): return ancestors - -class JinjaTemplateRenderer(TemplateRenderer): - - @classmethod - def get_pattern_source(cls, template): +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): + @staticmethod + 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 @@ -306,4 +315,3 @@ def get_template_ancestors(cls, template_name, context=None, ancestors=None): cls.get_template_ancestors(parent_template_name, context=context, ancestors=ancestors) return ancestors ->>>>>>> 68fea3c (First pass at renderers) From 3c81c97596f2b970ed6272be187d9f7a271f0963 Mon Sep 17 00:00:00 2001 From: Luis Orduz Date: Fri, 1 Jul 2022 10:48:09 -0500 Subject: [PATCH 03/18] Got pattern library working --- pattern_library/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pattern_library/utils.py b/pattern_library/utils.py index 93aae474..7230b766 100644 --- a/pattern_library/utils.py +++ b/pattern_library/utils.py @@ -256,8 +256,8 @@ class DTLTemplateRenderer: def get_pattern_source(template): return escape(template.template.source) - @staticmethod - def get_template_ancestors(template_name, context=None, ancestors=None): + @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 @@ -290,8 +290,8 @@ def get_pattern_source(template): source = escape(f.read()) return source - @staticmethod - def get_template_ancestors(template_name, context=None, ancestors=None): + @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 From 5b84ee215820443e9701e240e6f089bbe8e86d15 Mon Sep 17 00:00:00 2001 From: Luis Orduz Date: Fri, 1 Jul 2022 16:02:25 -0500 Subject: [PATCH 04/18] Fixed test_utils --- tests/tests/test_utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/tests/test_utils.py b/tests/tests/test_utils.py index 9730be5f..7c051631 100644 --- a/tests/tests/test_utils.py +++ b/tests/tests/test_utils.py @@ -5,15 +5,17 @@ from pattern_library.utils import ( get_pattern_config_str, - get_template_ancestors, get_template_dirs, + get_renderer, ) class TestGetTemplateAncestors(SimpleTestCase): + def setUp(self): + self.renderer = get_renderer() def test_page(self): self.assertEqual( - get_template_ancestors("patterns/pages/test_page/test_page.html"), + self.renderer.get_template_ancestors("patterns/pages/test_page/test_page.html"), [ "patterns/pages/test_page/test_page.html", "patterns/base_page.html", @@ -23,7 +25,7 @@ def test_page(self): def test_fragment(self): self.assertEqual( - get_template_ancestors("patterns/atoms/test_atom/test_atom.html"), + self.renderer.get_template_ancestors("patterns/atoms/test_atom/test_atom.html"), [ "patterns/atoms/test_atom/test_atom.html", ], @@ -31,7 +33,7 @@ def test_fragment(self): def test_parent_template_from_variable(self): self.assertEqual( - get_template_ancestors( + self.renderer.get_template_ancestors( "patterns/atoms/test_extends/extended.html", context={"parent_template_name": "patterns/base.html"}, ), From bc01b0c5ca734e9def6504b779e82ec131cdab88 Mon Sep 17 00:00:00 2001 From: Luis Orduz Date: Tue, 5 Jul 2022 12:48:00 -0500 Subject: [PATCH 05/18] Fixed commands --- pattern_library/management/commands/render_patterns.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pattern_library/management/commands/render_patterns.py b/pattern_library/management/commands/render_patterns.py index 482ae902..7f3caeed 100644 --- a/pattern_library/management/commands/render_patterns.py +++ b/pattern_library/management/commands/render_patterns.py @@ -7,9 +7,8 @@ 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, + get_renderer, ) @@ -44,7 +43,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("/") @@ -106,7 +106,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), ) From c514e5eb1de3c6e50de93992f48f056d5c758f3f Mon Sep 17 00:00:00 2001 From: Eddie Cohen Date: Thu, 13 Jul 2023 15:31:37 -0400 Subject: [PATCH 06/18] move everything into lib --- pattern_library/__init__.py | 8 ++++ pattern_library/loader_tags.py | 82 +++++++++++++++++++++++++++++++++ pattern_library/monkey_utils.py | 2 +- 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/pattern_library/__init__.py b/pattern_library/__init__.py index 88b173e7..0ee39613 100644 --- a/pattern_library/__init__.py +++ b/pattern_library/__init__.py @@ -64,3 +64,11 @@ def get_sections(): def get_pattern_context_var_name(): return "is_pattern_library" + +if get_pattern_template_suffix() == ".jinja": + from jinja2.compiler import CodeGenerator as JinjaCodeGenerator + from jinja2.environment import Template as JinjaTemplate + from .loader_tags import template_new_context, visit_extends + + JinjaTemplate.new_context = template_new_context + JinjaCodeGenerator.visit_Extends = visit_extends \ No newline at end of file diff --git a/pattern_library/loader_tags.py b/pattern_library/loader_tags.py index 2efb6cfb..38c318f7 100644 --- a/pattern_library/loader_tags.py +++ b/pattern_library/loader_tags.py @@ -150,3 +150,85 @@ def do_include(parser, token): extra_context=namemap, isolated_context=isolated_context, ) + +def visit_extends(self, node, frame): + """Dupe of the jinja2.compiler.CodeGenerator visit_Extends + except for + self.writeline( + "parent_template.new_context(context.get_all(), True," + f" {self.dump_local_context(frame)})" + ) + which is how we pull in context from yaml files to extended templates + """ + from jinja2.compiler import CompilerExit + + if not frame.toplevel: + self.fail("cannot use extend from a non top-level scope", node.lineno) + # if the number of extends statements in general is zero so + # far, we don't have to add a check if something extended + # the template before this one. + if self.extends_so_far > 0: + # if we have a known extends we just add a template runtime + # error into the generated code. We could catch that at compile + # time too, but i welcome it not to confuse users by throwing the + # same error at different times just "because we can". + if not self.has_known_extends: + self.writeline("if parent_template is not None:") + self.indent() + self.writeline('raise TemplateRuntimeError("extended multiple times")') + + # if we have a known extends already we don't need that code here + # as we know that the template execution will end here. + if self.has_known_extends: + raise CompilerExit() + else: + self.outdent() + self.writeline("parent_template = environment.get_template(", node) + self.visit(node.template, frame) + self.write(f", {self.name!r})") + # 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)})" + ) + self.writeline("for name, parent_block in parent_template.blocks.items():") + self.indent() + self.writeline("context.blocks.setdefault(name, []).append(parent_block)") + self.outdent() + + # if this extends statement was in the root level we can take + # advantage of that information and simplify the generated code + # in the top level from this point onwards + if frame.rootlevel: + self.has_known_extends = True + + # and now we have one more + self.extends_so_far += 1 + + +def template_new_context( + self, + vars=None, # noqa A002 + shared=False, + locals=None, # noqa A002 +): + """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 + ) \ No newline at end of file diff --git a/pattern_library/monkey_utils.py b/pattern_library/monkey_utils.py index 12eeb3e8..4feb2235 100644 --- a/pattern_library/monkey_utils.py +++ b/pattern_library/monkey_utils.py @@ -116,4 +116,4 @@ def node_render(context): return original_node - return tag_func + return tag_func \ No newline at end of file From 9cdd763cff9d5b15ac2ffca1d1f4d4779c519607 Mon Sep 17 00:00:00 2001 From: Eddie Cohen Date: Thu, 13 Jul 2023 15:35:24 -0400 Subject: [PATCH 07/18] more documentation --- pattern_library/loader_tags.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pattern_library/loader_tags.py b/pattern_library/loader_tags.py index 38c318f7..c2612945 100644 --- a/pattern_library/loader_tags.py +++ b/pattern_library/loader_tags.py @@ -152,13 +152,15 @@ def do_include(parser, token): ) def visit_extends(self, node, frame): - """Dupe of the jinja2.compiler.CodeGenerator visit_Extends + """This method serves as overriding the jinja extends tag + Dupe of the jinja2.compiler.CodeGenerator visit_Extends except for self.writeline( "parent_template.new_context(context.get_all(), True," f" {self.dump_local_context(frame)})" ) - which is how we pull in context from yaml files to extended templates + which executes at runtime to pull in the dpl context + Handles visiting extends """ from jinja2.compiler import CompilerExit @@ -214,7 +216,10 @@ def template_new_context( shared=False, locals=None, # noqa A002 ): - """Create a new :class:`Context` for this template. The vars + """This method serves as overriding 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. From 1a401fadc277b493b743cf1566f126b78657bbca Mon Sep 17 00:00:00 2001 From: Eddie Cohen Date: Thu, 13 Jul 2023 15:45:45 -0400 Subject: [PATCH 08/18] no noqa --- pattern_library/loader_tags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pattern_library/loader_tags.py b/pattern_library/loader_tags.py index c2612945..7a1f18ea 100644 --- a/pattern_library/loader_tags.py +++ b/pattern_library/loader_tags.py @@ -212,9 +212,9 @@ def visit_extends(self, node, frame): def template_new_context( self, - vars=None, # noqa A002 + vars=None, shared=False, - locals=None, # noqa A002 + locals=None, ): """This method serves as overriding the jinja include tag Is called as part of Template.render by jinja2 and is updated From 7e6a0688905ab89e8b4decb065cc702b928ff9d8 Mon Sep 17 00:00:00 2001 From: Eddie Cohen Date: Mon, 24 Jul 2023 12:07:55 -0400 Subject: [PATCH 09/18] reorg calling override, dont dupe --- pattern_library/__init__.py | 8 ----- pattern_library/loader_tags.py | 56 +++++---------------------------- pattern_library/monkey_utils.py | 17 +++++++++- 3 files changed, 24 insertions(+), 57 deletions(-) diff --git a/pattern_library/__init__.py b/pattern_library/__init__.py index 0ee39613..88b173e7 100644 --- a/pattern_library/__init__.py +++ b/pattern_library/__init__.py @@ -64,11 +64,3 @@ def get_sections(): def get_pattern_context_var_name(): return "is_pattern_library" - -if get_pattern_template_suffix() == ".jinja": - from jinja2.compiler import CodeGenerator as JinjaCodeGenerator - from jinja2.environment import Template as JinjaTemplate - from .loader_tags import template_new_context, visit_extends - - JinjaTemplate.new_context = template_new_context - JinjaCodeGenerator.visit_Extends = visit_extends \ No newline at end of file diff --git a/pattern_library/loader_tags.py b/pattern_library/loader_tags.py index 7a1f18ea..4cc6a9b8 100644 --- a/pattern_library/loader_tags.py +++ b/pattern_library/loader_tags.py @@ -152,42 +152,15 @@ def do_include(parser, token): ) def visit_extends(self, node, frame): - """This method serves as overriding the jinja extends tag - Dupe of the jinja2.compiler.CodeGenerator visit_Extends - except for - self.writeline( - "parent_template.new_context(context.get_all(), True," - f" {self.dump_local_context(frame)})" - ) - which executes at runtime to pull in the dpl context + """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 jinja2.compiler import CompilerExit - - if not frame.toplevel: - self.fail("cannot use extend from a non top-level scope", node.lineno) - # if the number of extends statements in general is zero so - # far, we don't have to add a check if something extended - # the template before this one. - if self.extends_so_far > 0: - # if we have a known extends we just add a template runtime - # error into the generated code. We could catch that at compile - # time too, but i welcome it not to confuse users by throwing the - # same error at different times just "because we can". - if not self.has_known_extends: - self.writeline("if parent_template is not None:") - self.indent() - self.writeline('raise TemplateRuntimeError("extended multiple times")') - - # if we have a known extends already we don't need that code here - # as we know that the template execution will end here. - if self.has_known_extends: - raise CompilerExit() - else: - self.outdent() - self.writeline("parent_template = environment.get_template(", node) - self.visit(node.template, frame) - self.write(f", {self.name!r})") + 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 @@ -195,19 +168,6 @@ def visit_extends(self, node, frame): "parent_template.new_context(context.get_all(), True," f" {self.dump_local_context(frame)})" ) - self.writeline("for name, parent_block in parent_template.blocks.items():") - self.indent() - self.writeline("context.blocks.setdefault(name, []).append(parent_block)") - self.outdent() - - # if this extends statement was in the root level we can take - # advantage of that information and simplify the generated code - # in the top level from this point onwards - if frame.rootlevel: - self.has_known_extends = True - - # and now we have one more - self.extends_so_far += 1 def template_new_context( @@ -216,7 +176,7 @@ def template_new_context( shared=False, locals=None, ): - """This method serves as overriding the jinja include tag + """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 diff --git a/pattern_library/monkey_utils.py b/pattern_library/monkey_utils.py index 4feb2235..1fd4236a 100644 --- a/pattern_library/monkey_utils.py +++ b/pattern_library/monkey_utils.py @@ -116,4 +116,19 @@ def node_render(context): return original_node - return tag_func \ No newline at end of file + return tag_func + +jinja_visit_Extends = None + +def override_jinja_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 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 \ No newline at end of file From ba2de4fb21ec22620168f268855fc0e915399d23 Mon Sep 17 00:00:00 2001 From: Eddie Cohen Date: Mon, 24 Jul 2023 12:20:33 -0400 Subject: [PATCH 10/18] document --- pattern_library/monkey_utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pattern_library/monkey_utils.py b/pattern_library/monkey_utils.py index 1fd4236a..c3f3bcbb 100644 --- a/pattern_library/monkey_utils.py +++ b/pattern_library/monkey_utils.py @@ -118,15 +118,21 @@ def node_render(context): 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 tags") + ModuleNotFoundError("install jinja2 to override jinja tags") from .loader_tags import template_new_context, visit_extends jinja_visit_Extends = JinjaCodeGenerator.visit_Extends From 59bbe0aa982a07c970d7ec84eb4564494189d70e Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Fri, 15 Dec 2023 16:25:58 -0300 Subject: [PATCH 11/18] Fix error accessing pattern template name --- pattern_library/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pattern_library/utils.py b/pattern_library/utils.py index 7230b766..3edc5dd7 100644 --- a/pattern_library/utils.py +++ b/pattern_library/utils.py @@ -308,7 +308,7 @@ def get_template_ancestors(cls, template_name, context=None, ancestors=None): pattern_template = get_template(template_name) #todo - make sure envrionment has context passed in environment = pattern_template.template.environment - nodelist = environment.parse(pattern_template.name) + nodelist = environment.parse(pattern_template.template.name) parent_template_name = nodelist.find(Extends) if parent_template_name: ancestors.append(parent_template_name) From 9e43ad09212bb2a1609657e87fdbfee00e5554d6 Mon Sep 17 00:00:00 2001 From: Mariana Bedran Lesche Date: Fri, 15 Dec 2023 16:26:55 -0300 Subject: [PATCH 12/18] Wrap rendered pattern HTML with mark_safe to render correctly on template --- pattern_library/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pattern_library/views.py b/pattern_library/views.py index 030c1d8a..fd9b1955 100644 --- a/pattern_library/views.py +++ b/pattern_library/views.py @@ -4,6 +4,7 @@ from django.template.loader import get_template from django.utils.decorators import method_decorator from django.utils.html import escape +from django.utils.safestring import mark_safe from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.csrf import csrf_exempt from django.views.generic.base import TemplateView @@ -99,7 +100,7 @@ def get(self, request, pattern_template_name=None): if pattern_is_fragment: context = self.get_context_data() - context["pattern_library_rendered_pattern"] = rendered_pattern + context["pattern_library_rendered_pattern"] = mark_safe(rendered_pattern) return self.render_to_response(context) return HttpResponse(rendered_pattern) From df1eb3b42747265bfd6273af6bffc19d230fb6d4 Mon Sep 17 00:00:00 2001 From: Sanyam Khurana Date: Tue, 27 Feb 2024 00:42:57 +0530 Subject: [PATCH 13/18] fix: linting --- pattern_library/loader_tags.py | 5 +++-- pattern_library/monkey_utils.py | 9 ++++++--- pattern_library/utils.py | 15 ++++++++++----- tests/tests/test_context_modifiers.py | 1 - tests/tests/test_utils.py | 9 +++++++-- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/pattern_library/loader_tags.py b/pattern_library/loader_tags.py index 4cc6a9b8..80dd1116 100644 --- a/pattern_library/loader_tags.py +++ b/pattern_library/loader_tags.py @@ -151,9 +151,10 @@ def do_include(parser, token): 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 + 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 @@ -196,4 +197,4 @@ def template_new_context( return new_context( self.environment, self.name, self.blocks, vars, shared, self.globals, locals - ) \ No newline at end of file + ) diff --git a/pattern_library/monkey_utils.py b/pattern_library/monkey_utils.py index c3f3bcbb..2da37f44 100644 --- a/pattern_library/monkey_utils.py +++ b/pattern_library/monkey_utils.py @@ -118,14 +118,16 @@ def node_render(context): 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 + Call it in your settings to override tags """ global jinja_visit_Extends try: @@ -133,8 +135,9 @@ def override_jinja_tags(): 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 \ No newline at end of file + JinjaCodeGenerator.visit_Extends = visit_extends diff --git a/pattern_library/utils.py b/pattern_library/utils.py index 3edc5dd7..1edf1f3f 100644 --- a/pattern_library/utils.py +++ b/pattern_library/utils.py @@ -22,9 +22,9 @@ from pattern_library.exceptions import TemplateIsNotPattern - from django.utils.html import escape + def path_to_section(): section_config = get_sections() sections = {} @@ -243,7 +243,9 @@ def get_pattern_source(cls, 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) + return cls._get_engine(template).get_template_ancestors( + template_name, context=context + ) @classmethod def _get_engine(cls, template): @@ -251,6 +253,7 @@ def _get_engine(cls, template): return JinjaTemplateRenderer return DTLTemplateRenderer + class DTLTemplateRenderer: @staticmethod def get_pattern_source(template): @@ -287,7 +290,7 @@ class JinjaTemplateRenderer: @staticmethod def get_pattern_source(template): with open(template.template.filename) as f: - source = escape(f.read()) + source = escape(f.read()) return source @classmethod @@ -306,12 +309,14 @@ def get_template_ancestors(cls, template_name, context=None, ancestors=None): context = Context() pattern_template = get_template(template_name) - #todo - make sure envrionment has context passed in + # 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) - cls.get_template_ancestors(parent_template_name, context=context, ancestors=ancestors) + cls.get_template_ancestors( + parent_template_name, context=context, ancestors=ancestors + ) return ancestors diff --git a/tests/tests/test_context_modifiers.py b/tests/tests/test_context_modifiers.py index 12ddd9f5..1fcb7450 100644 --- a/tests/tests/test_context_modifiers.py +++ b/tests/tests/test_context_modifiers.py @@ -33,7 +33,6 @@ def modifier_3(context, request): class ContextModifierTestCase(SimpleTestCase): - maxDiff = None def setUp(self): diff --git a/tests/tests/test_utils.py b/tests/tests/test_utils.py index 7c051631..682f9aef 100644 --- a/tests/tests/test_utils.py +++ b/tests/tests/test_utils.py @@ -13,9 +13,12 @@ class TestGetTemplateAncestors(SimpleTestCase): def setUp(self): self.renderer = get_renderer() + def test_page(self): self.assertEqual( - self.renderer.get_template_ancestors("patterns/pages/test_page/test_page.html"), + self.renderer.get_template_ancestors( + "patterns/pages/test_page/test_page.html" + ), [ "patterns/pages/test_page/test_page.html", "patterns/base_page.html", @@ -25,7 +28,9 @@ def test_page(self): def test_fragment(self): self.assertEqual( - self.renderer.get_template_ancestors("patterns/atoms/test_atom/test_atom.html"), + self.renderer.get_template_ancestors( + "patterns/atoms/test_atom/test_atom.html" + ), [ "patterns/atoms/test_atom/test_atom.html", ], From 7beca080d8388e6fb10cba9e94f370954049f4be Mon Sep 17 00:00:00 2001 From: Sanyam Khurana Date: Tue, 27 Feb 2024 00:44:25 +0530 Subject: [PATCH 14/18] Run isort --- pattern_library/management/commands/render_patterns.py | 6 +----- pattern_library/utils.py | 4 +--- pattern_library/views.py | 2 +- tests/tests/test_utils.py | 2 +- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/pattern_library/management/commands/render_patterns.py b/pattern_library/management/commands/render_patterns.py index 7f3caeed..32268b7a 100644 --- a/pattern_library/management/commands/render_patterns.py +++ b/pattern_library/management/commands/render_patterns.py @@ -5,11 +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, - render_pattern, - get_renderer, -) +from pattern_library.utils import get_pattern_context, get_renderer, render_pattern class Command(BaseCommand): diff --git a/pattern_library/utils.py b/pattern_library/utils.py index 1edf1f3f..9697c39a 100644 --- a/pattern_library/utils.py +++ b/pattern_library/utils.py @@ -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 @@ -22,9 +23,6 @@ from pattern_library.exceptions import TemplateIsNotPattern -from django.utils.html import escape - - def path_to_section(): section_config = get_sections() sections = {} diff --git a/pattern_library/views.py b/pattern_library/views.py index fd9b1955..99d1dccb 100644 --- a/pattern_library/views.py +++ b/pattern_library/views.py @@ -16,10 +16,10 @@ get_pattern_config_str, get_pattern_context, get_pattern_markdown, + get_renderer, get_sections, is_pattern, render_pattern, - get_renderer, ) diff --git a/tests/tests/test_utils.py b/tests/tests/test_utils.py index 682f9aef..fe3191c6 100644 --- a/tests/tests/test_utils.py +++ b/tests/tests/test_utils.py @@ -5,8 +5,8 @@ from pattern_library.utils import ( get_pattern_config_str, - get_template_dirs, get_renderer, + get_template_dirs, ) From f7d2963340edc09b3be0997d21b95f6e0f53fcff Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Tue, 8 Apr 2025 11:58:36 +0100 Subject: [PATCH 15/18] Add missing Jinja documentation --- docs/community/related-projects.md | 3 +-- docs/getting-started.md | 13 +++++++++++-- docs/guides/overriding-template-tags.md | 14 +++++++------- docs/index.md | 4 ++-- docs/reference/api.md | 13 ++++++++++++- docs/reference/known-issues.md | 10 +++++++--- 6 files changed, 40 insertions(+), 17 deletions(-) diff --git a/docs/community/related-projects.md b/docs/community/related-projects.md index 00b1521b..5199e8c2 100644 --- a/docs/community/related-projects.md +++ b/docs/community/related-projects.md @@ -16,10 +16,9 @@ Here are other projects that are related to django-pattern-library, and may be r - [Storybook](https://storybook.js.org/), and in particular [Storybook for Server](https://github.com/storybookjs/storybook/tree/master/app/server) – Storybook integration with server-rendered UI components. - [Pattern Lab](http://patternlab.io/) – PHP or Node pattern library, from which this project is heavily inspired. - [Astrum](http://astrum.nodividestudio.com/) – Similar to Pattern Lab, Node based. -- [rikki-patterns](https://github.com/springload/rikki-patterns) – Experimental Django-friendly pattern library generator, for Jinja2 and Nunjucks templates +- [rikki-patterns](https://github.com/springload/rikki-patterns) – Experimental Django-friendly pattern library generator, for Jinja and Nunjucks templates - [django-lookbook](https://github.com/rails-inspire-django/django-lookbook) - Empower your Django development with this pluggable app for creating a robust component library. Includes preview system, documentation engine, and parameter editor for building modular UI effortlessly. - ## Pattern libraries based on Django Here are open-source projects that maintain pattern libraries for Django. diff --git a/docs/getting-started.md b/docs/getting-started.md index f5ac2d6f..c47e882d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -19,7 +19,7 @@ We support: - Django 4.2, 5.0, 5.1 - Python 3.9, 3.10, 3.11, 3.12 -- Django Templates only, no Jinja support +- Django Templates and Jinja (experimental) - Modern “evergreen” desktop and mobile browsers ## Configuration @@ -36,7 +36,7 @@ INSTALLED_APPS = [ ] ``` -Also add `pattern_library.loader_tags` to `OPTIONS["builtins"]` into the `TEMPLATES` setting: +For Django Templates, add `pattern_library.loader_tags` to `OPTIONS["builtins"]` into the `TEMPLATES` setting: ```python hl_lines="13 14 15" TEMPLATES = [ @@ -59,6 +59,15 @@ TEMPLATES = [ ] ``` +Experimental: for Jinja support, call `override_jinja_tags` in the file that contains your Jinja environment: + +```python +from pattern_library.monkey_utils import override_jinja_tags + +if apps.is_installed("pattern_library"): + override_jinja_tags() +``` + To see the detailed error pages generated by Django when you have `DEBUG = True` in the pattern library, you'll need to make sure you have `X_FRAME_OPTIONS` set, or your browser will block the response: ```python diff --git a/docs/guides/overriding-template-tags.md b/docs/guides/overriding-template-tags.md index ea412efd..f90013ac 100644 --- a/docs/guides/overriding-template-tags.md +++ b/docs/guides/overriding-template-tags.md @@ -1,18 +1,18 @@ # Overriding template tags -The package overrides the following Django tags: +The package overrides Django’s `extends` and `include` tags, implementing custom behaviour for these tags only when rendering in the pattern library. It falls back to Django's standard behaviour on all other cases. This makes it possible to define fake template contexts once and then have it reused everywhere a template partial is included. -- `{% extends %}` -- `{% include %}` +--- -It's required to allow us to define fake template context and override other template tags in YAML files. -This package uses custom behaviour for these tags only when rendering pattern library and falls back to Django's standard behaviour on all other cases. - -The override process has two parts: +We can also override other template tags in YAML files. The override process has two parts: 1. Override your template tag with a mock implementation 2. Define fake result for your tag in a YAML file +!!! warning "No Jinja support" + + Overriding arbitrary template tags or functions is currently unsupported for Jinja templates. + ## Providing a default value for template tags To provide a default for a template tag, you need to provide a keyword argument default_html when overriding your tag. diff --git a/docs/index.md b/docs/index.md index 4cd9ef93..013cd1b6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,8 +9,9 @@ The [django-pattern-library](https://pypi.org/project/django-pattern-library/) p - Create reusable patterns by creating Django templates files as usual. - All patterns automatically show up in the pattern library’s interface. - Define data as YAML files for the templates to render with the relevant Django context. -- Override Django templates tags as needed to mock the template’s dependencies. +- Override Django Templates tags as needed to mock the template’s dependencies. - Document your patterns with Markdown. +- Experimental: support for Jinja templates. Here is a screenshot of the pattern library in action: @@ -45,4 +46,3 @@ To learn more about how this package can be used, have a look at our talk: [Reusable UI components: A journey from React to Wagtail](https://www.youtube.com/watch?v=isrOufI7TKc) [![Reusable UI components: A journey from React to Wagtail](images/pattern-library-talk-youtube.webp)](https://www.youtube.com/watch?v=isrOufI7TKc) - diff --git a/docs/reference/api.md b/docs/reference/api.md index e20f82e7..167ab91f 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -24,6 +24,7 @@ context: - 2 - 3 # Mapping from tag names to tag overrides. +# Currently unsupported for Jinja templates. tags: error_tag: include: @@ -135,7 +136,7 @@ PATTERN_LIBRARY = { ### `override_tag` -This function tells the pattern library which Django tags to override, and optionally supports providing a default value. See [Overriding template tags](../guides/overriding-template-tags.md) for more information. +This function tells the pattern library which Django Templates tags to override, and optionally supports providing a default value. See [Overriding template tags](../guides/overriding-template-tags.md) for more information. ```python from pattern_library.monkey_utils import override_tag @@ -143,6 +144,16 @@ from pattern_library.monkey_utils import override_tag override_tag(register, 'a_tag_name', default_html="https://example.com/") ``` +### `override_jinja_tags` + +🚧 Experimental. Optionally override `extends` and `include` in Jinja templates, so context for partials can be defined once and reused everywhere. See [Overriding template tags](../guides/overriding-template-tags.md). Call this in your Django settings file or at the top level of the file defining your Jinja environment. + +```python +from pattern_library.monkey_utils import override_jinja_tags + +override_jinja_tags() +``` + ## `register_context_modifier` This decorator makes it possible to override or create additional context data with Django / Python code, rather than being limited to YAML. It has to be called from within a `pattern_contexts` module, which can be at the root of any Django app. See [Modifying template contexts with Python](../guides/defining-template-context.md#modifying-template-contexts-with-python) for more information. diff --git a/docs/reference/known-issues.md b/docs/reference/known-issues.md index 97098125..a54ac9c2 100644 --- a/docs/reference/known-issues.md +++ b/docs/reference/known-issues.md @@ -4,7 +4,7 @@ django-pattern-library has a few known limitations due to its design, which are ## Overriding filters is not supported -See [#114](https://github.com/torchbox/django-pattern-library/issues/114). PRs welcome! +See [#114](https://github.com/torchbox/django-pattern-library/issues/114) for Django Templates. PRs welcome! ## Can’t override context in a child template @@ -62,12 +62,16 @@ See [#138](https://github.com/torchbox/django-pattern-library/issues/138). For e This can’t be mocked for all usage of `include_block`. -## Jinja2 support +## Jinja2 overrides -Or lack thereof! If you’re interested in this, please share your thoughts with us on [#180](https://github.com/torchbox/django-pattern-library/discussions/180). +There is experimental support, excluding overrides of arbitrary tags, functions, and filters. If you’re interested in this, please share your thoughts with us on [#180](https://github.com/torchbox/django-pattern-library/discussions/180). ## Past limitations +### Jinja2 support + +🎉 This is now addressed as of v1.4.0, though with only experimental support, and no capability to override tags, functions, filters (see above). + ### No way to specify objects that have attributes and support iteration 🎉 This is now addressed as of v0.5.0, with the [context modifiers in Python](../guides/defining-template-context.md#modifying-template-contexts-with-python) API. View our [pagination](../recipes/pagination.md) recipe. From fda5741631d4d22da51fd27abad4ff69b9baec0e Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Tue, 8 Apr 2025 11:59:14 +0100 Subject: [PATCH 16/18] Add basic Jinja tests --- README.md | 3 +- pattern_library/monkey_utils.py | 4 +- pattern_library/utils.py | 1 - tests/jinja/non-patterns/include.html | 4 ++ .../jinja/non-patterns/variable_include.html | 1 + tests/jinja/patterns_jinja/base_jinja.html | 16 ++++++ .../jinja/patterns_jinja/base_page_jinja.html | 3 + .../components/accordion/accordion.html | 17 ++++++ .../components/accordion/accordion.md | 3 + .../components/accordion/accordion.yaml | 11 ++++ .../components/button/button.html | 3 + .../components/button/button.yaml | 7 +++ .../patterns_jinja/components/icons/icon.html | 3 + .../patterns_jinja/components/icons/icon.yaml | 2 + .../components/sprites/sprites.html | 33 +++++++++++ .../components/test_atom/test_atom.html | 1 + .../components/test_atom/test_atom.md | 12 ++++ .../components/test_atom/test_atom.yaml | 2 + .../components/test_extends/base.html | 1 + .../components/test_extends/extended.html | 3 + .../components/test_extends/extended.yaml | 2 + .../test_includes/test_includes.html | 4 ++ .../test_includes/test_includes.yaml | 7 +++ .../test_molecule/test_molecule.html | 3 + .../test_molecule/test_molecule.yaml | 5 ++ .../test_molecule_no_context.html | 1 + .../patterns_jinja/pages/search/search.html | 34 +++++++++++ .../patterns_jinja/pages/search/search.yaml | 5 ++ .../pages/test_page/test_page.html | 12 ++++ .../pages/test_page/test_page.yaml | 12 ++++ tests/jinja2.py | 56 +++++++++++++++++++ tests/pattern_contexts.py | 29 +++++++++- tests/settings/base.py | 21 +++++++ .../patterns/pages/people/person_page.yaml | 8 +-- .../patterns/pages/test_page/test_page.html | 11 +++- 35 files changed, 330 insertions(+), 10 deletions(-) create mode 100644 tests/jinja/non-patterns/include.html create mode 100644 tests/jinja/non-patterns/variable_include.html create mode 100644 tests/jinja/patterns_jinja/base_jinja.html create mode 100644 tests/jinja/patterns_jinja/base_page_jinja.html create mode 100644 tests/jinja/patterns_jinja/components/accordion/accordion.html create mode 100644 tests/jinja/patterns_jinja/components/accordion/accordion.md create mode 100644 tests/jinja/patterns_jinja/components/accordion/accordion.yaml create mode 100644 tests/jinja/patterns_jinja/components/button/button.html create mode 100644 tests/jinja/patterns_jinja/components/button/button.yaml create mode 100644 tests/jinja/patterns_jinja/components/icons/icon.html create mode 100644 tests/jinja/patterns_jinja/components/icons/icon.yaml create mode 100644 tests/jinja/patterns_jinja/components/sprites/sprites.html create mode 100644 tests/jinja/patterns_jinja/components/test_atom/test_atom.html create mode 100644 tests/jinja/patterns_jinja/components/test_atom/test_atom.md create mode 100644 tests/jinja/patterns_jinja/components/test_atom/test_atom.yaml create mode 100644 tests/jinja/patterns_jinja/components/test_extends/base.html create mode 100644 tests/jinja/patterns_jinja/components/test_extends/extended.html create mode 100644 tests/jinja/patterns_jinja/components/test_extends/extended.yaml create mode 100644 tests/jinja/patterns_jinja/components/test_includes/test_includes.html create mode 100644 tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml create mode 100644 tests/jinja/patterns_jinja/components/test_molecule/test_molecule.html create mode 100644 tests/jinja/patterns_jinja/components/test_molecule/test_molecule.yaml create mode 100644 tests/jinja/patterns_jinja/components/test_molecule/test_molecule_no_context.html create mode 100644 tests/jinja/patterns_jinja/pages/search/search.html create mode 100644 tests/jinja/patterns_jinja/pages/search/search.yaml create mode 100644 tests/jinja/patterns_jinja/pages/test_page/test_page.html create mode 100644 tests/jinja/patterns_jinja/pages/test_page/test_page.yaml create mode 100644 tests/jinja2.py diff --git a/README.md b/README.md index f0f6efed..22dd4ca6 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ This package automates the maintenance of UI pattern libraries or styleguides fo - Create reusable patterns by creating Django templates files as usual. - All patterns automatically show up in the pattern library’s interface. - Define data as YAML files for the templates to render with the relevant Django context. -- Override Django templates tags as needed to mock the template’s dependencies. +- Override Django Templates tags as needed to mock the template’s dependencies. - Document your patterns with Markdown. +- Experimental: support for Jinja templates. ## Why you need this diff --git a/pattern_library/monkey_utils.py b/pattern_library/monkey_utils.py index 2da37f44..6bf962b5 100644 --- a/pattern_library/monkey_utils.py +++ b/pattern_library/monkey_utils.py @@ -126,7 +126,7 @@ def node_render(context): def override_jinja_tags(): """ - Overrides jinja extends and include tags for use in your pattern library. + Experimental. Overrides jinja extends and include tags for use in your pattern library. Call it in your settings to override tags """ global jinja_visit_Extends @@ -136,7 +136,7 @@ def override_jinja_tags(): except ModuleNotFoundError: ModuleNotFoundError("install jinja2 to override jinja tags") - from .loader_tags import template_new_context, visit_extends + from pattern_library.loader_tags import template_new_context, visit_extends jinja_visit_Extends = JinjaCodeGenerator.visit_Extends JinjaTemplate.new_context = template_new_context diff --git a/pattern_library/utils.py b/pattern_library/utils.py index 9697c39a..6389f7ad 100644 --- a/pattern_library/utils.py +++ b/pattern_library/utils.py @@ -307,7 +307,6 @@ def get_template_ancestors(cls, template_name, context=None, ancestors=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) diff --git a/tests/jinja/non-patterns/include.html b/tests/jinja/non-patterns/include.html new file mode 100644 index 00000000..db9fcf7f --- /dev/null +++ b/tests/jinja/non-patterns/include.html @@ -0,0 +1,4 @@ +SHOWME +{% if False %} +HIDEME +{% endif %} diff --git a/tests/jinja/non-patterns/variable_include.html b/tests/jinja/non-patterns/variable_include.html new file mode 100644 index 00000000..be595c6b --- /dev/null +++ b/tests/jinja/non-patterns/variable_include.html @@ -0,0 +1 @@ +included content from variable diff --git a/tests/jinja/patterns_jinja/base_jinja.html b/tests/jinja/patterns_jinja/base_jinja.html new file mode 100644 index 00000000..3b9bd618 --- /dev/null +++ b/tests/jinja/patterns_jinja/base_jinja.html @@ -0,0 +1,16 @@ + + + + + {% block title %}Fragment{% endblock %} + + + + + +
+ {% block content %}{% endblock %} +
+ + + diff --git a/tests/jinja/patterns_jinja/base_page_jinja.html b/tests/jinja/patterns_jinja/base_page_jinja.html new file mode 100644 index 00000000..894070a7 --- /dev/null +++ b/tests/jinja/patterns_jinja/base_page_jinja.html @@ -0,0 +1,3 @@ +{% extends 'patterns_jinja/base_jinja.html' %} + +{% block title %}{{ page.title }}{% endblock %} diff --git a/tests/jinja/patterns_jinja/components/accordion/accordion.html b/tests/jinja/patterns_jinja/components/accordion/accordion.html new file mode 100644 index 00000000..e63f1fbd --- /dev/null +++ b/tests/jinja/patterns_jinja/components/accordion/accordion.html @@ -0,0 +1,17 @@ +
+ {% for accordion in accordions %} +
+

+ +

+
+

{{ accordion.description }}

+
+
+ {% endfor %} +
diff --git a/tests/jinja/patterns_jinja/components/accordion/accordion.md b/tests/jinja/patterns_jinja/components/accordion/accordion.md new file mode 100644 index 00000000..f4d5a57f --- /dev/null +++ b/tests/jinja/patterns_jinja/components/accordion/accordion.md @@ -0,0 +1,3 @@ +# Accordion (Jinja) + +The accordion is a good example of a pattern library component. This one uses [Jinja](https://jinja.palletsprojects.com/). diff --git a/tests/jinja/patterns_jinja/components/accordion/accordion.yaml b/tests/jinja/patterns_jinja/components/accordion/accordion.yaml new file mode 100644 index 00000000..10cb2aca --- /dev/null +++ b/tests/jinja/patterns_jinja/components/accordion/accordion.yaml @@ -0,0 +1,11 @@ +context: + id_prefix: test + accordions: + - title: Title A (Jinja) + description: Description A. Ask for help. + - title: Title B + description: Description B. Ask for help. + - title: Title C + description: Description C. Ask for help. + - title: Title D + description: Description D. Ask for help. diff --git a/tests/jinja/patterns_jinja/components/button/button.html b/tests/jinja/patterns_jinja/components/button/button.html new file mode 100644 index 00000000..a027a3f6 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/button/button.html @@ -0,0 +1,3 @@ + + {% if label %}{{ label }}{% else %}{{ target_page.title }}{% endif %} + diff --git a/tests/jinja/patterns_jinja/components/button/button.yaml b/tests/jinja/patterns_jinja/components/button/button.yaml new file mode 100644 index 00000000..4546360f --- /dev/null +++ b/tests/jinja/patterns_jinja/components/button/button.yaml @@ -0,0 +1,7 @@ +context: + target_page: + title: Get started +tags: + pageurl: + target_page: + raw: /get-started diff --git a/tests/jinja/patterns_jinja/components/icons/icon.html b/tests/jinja/patterns_jinja/components/icons/icon.html new file mode 100644 index 00000000..2875a347 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/icons/icon.html @@ -0,0 +1,3 @@ + diff --git a/tests/jinja/patterns_jinja/components/icons/icon.yaml b/tests/jinja/patterns_jinja/components/icons/icon.yaml new file mode 100644 index 00000000..2ebc4180 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/icons/icon.yaml @@ -0,0 +1,2 @@ +context: + name: close diff --git a/tests/jinja/patterns_jinja/components/sprites/sprites.html b/tests/jinja/patterns_jinja/components/sprites/sprites.html new file mode 100644 index 00000000..06ccb956 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/sprites/sprites.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + +{# Show available icons, in pattern library view only #} +{% if is_pattern_library %} + + +{% endif %} diff --git a/tests/jinja/patterns_jinja/components/test_atom/test_atom.html b/tests/jinja/patterns_jinja/components/test_atom/test_atom.html new file mode 100644 index 00000000..9635774f --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_atom/test_atom.html @@ -0,0 +1 @@ +{{ atom_var }} diff --git a/tests/jinja/patterns_jinja/components/test_atom/test_atom.md b/tests/jinja/patterns_jinja/components/test_atom/test_atom.md new file mode 100644 index 00000000..3f4fce46 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_atom/test_atom.md @@ -0,0 +1,12 @@ +# Documentation for test atom - heading 1 + +**bold text** + +*italic text* + +## Here's a heading 2 + +### Here's a heading 3 + +- here's a list item +- and another diff --git a/tests/jinja/patterns_jinja/components/test_atom/test_atom.yaml b/tests/jinja/patterns_jinja/components/test_atom/test_atom.yaml new file mode 100644 index 00000000..f7baf100 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_atom/test_atom.yaml @@ -0,0 +1,2 @@ +context: + atom_var: 'atom_var value from test_atom.yaml' diff --git a/tests/jinja/patterns_jinja/components/test_extends/base.html b/tests/jinja/patterns_jinja/components/test_extends/base.html new file mode 100644 index 00000000..43c5ba6b --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_extends/base.html @@ -0,0 +1 @@ +{% block content %}base content{% endblock %} diff --git a/tests/jinja/patterns_jinja/components/test_extends/extended.html b/tests/jinja/patterns_jinja/components/test_extends/extended.html new file mode 100644 index 00000000..cfae2ef3 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_extends/extended.html @@ -0,0 +1,3 @@ +{% extends parent_template_name %} + +{% block content %}{{ super() }} - extended content{% endblock %} diff --git a/tests/jinja/patterns_jinja/components/test_extends/extended.yaml b/tests/jinja/patterns_jinja/components/test_extends/extended.yaml new file mode 100644 index 00000000..10ed3936 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_extends/extended.yaml @@ -0,0 +1,2 @@ +context: + parent_template_name: patterns_jinja/components/test_extends/base.html diff --git a/tests/jinja/patterns_jinja/components/test_includes/test_includes.html b/tests/jinja/patterns_jinja/components/test_includes/test_includes.html new file mode 100644 index 00000000..2a40179b --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_includes/test_includes.html @@ -0,0 +1,4 @@ +{% include 'non-patterns/include.html' %} +{% include variable_include %} + +{{ error_tag() }} diff --git a/tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml b/tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml new file mode 100644 index 00000000..757b9209 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml @@ -0,0 +1,7 @@ +context: + variable_include: non-patterns/variable_include.html + +tags: + error_tag: + include: + template_name: 'non-patterns/include.html' diff --git a/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.html b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.html new file mode 100644 index 00000000..9ca10558 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.html @@ -0,0 +1,3 @@ +{% include 'patterns_jinja/components/test_atom/test_atom.html' %} +{% set atom_var='atom_var value from test_molecule.html include tag' %} +{% include 'patterns_jinja/components/test_atom/test_atom.html' %} diff --git a/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.yaml b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.yaml new file mode 100644 index 00000000..af5614be --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule.yaml @@ -0,0 +1,5 @@ +name: Pretty name for test molecule + +context: + atom_var: 'atom_var value from test_molecule.yaml' + utf8_test_var: '你好,世界' diff --git a/tests/jinja/patterns_jinja/components/test_molecule/test_molecule_no_context.html b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule_no_context.html new file mode 100644 index 00000000..4981b0f6 --- /dev/null +++ b/tests/jinja/patterns_jinja/components/test_molecule/test_molecule_no_context.html @@ -0,0 +1 @@ +No context diff --git a/tests/jinja/patterns_jinja/pages/search/search.html b/tests/jinja/patterns_jinja/pages/search/search.html new file mode 100644 index 00000000..44423c58 --- /dev/null +++ b/tests/jinja/patterns_jinja/pages/search/search.html @@ -0,0 +1,34 @@ +{% extends "patterns_jinja/base_jinja.html" %} + +{% block content %} +

{% if search_query %}Search results for “{{ search_query }}”{% else %}Search{% endif %}

+ + {% if search_results %} + {% with count=paginator.count %} + {{ count }} result{{ count|pluralize }} found. + {% endwith %} + + {% for result in search_results %} +

{{ result.title }}

+ {% endfor %} + + {% if paginator.num_pages > 1 %} + + {% endif %} + + {% elif search_query %} + No results found. + {% endif %} +{% endblock %} diff --git a/tests/jinja/patterns_jinja/pages/search/search.yaml b/tests/jinja/patterns_jinja/pages/search/search.yaml new file mode 100644 index 00000000..69e53929 --- /dev/null +++ b/tests/jinja/patterns_jinja/pages/search/search.yaml @@ -0,0 +1,5 @@ +context: + search_query: test query + search_results: + - title: First result + - title: Second result diff --git a/tests/jinja/patterns_jinja/pages/test_page/test_page.html b/tests/jinja/patterns_jinja/pages/test_page/test_page.html new file mode 100644 index 00000000..4ffb7218 --- /dev/null +++ b/tests/jinja/patterns_jinja/pages/test_page/test_page.html @@ -0,0 +1,12 @@ +{% extends 'patterns_jinja/base_page_jinja.html' %} + +{% block content %} +

{{ page.title }}

+{{ page.body }} + +{% include "patterns_jinja/components/accordion/accordion.html" %} + +{% if is_pattern_library %} +

is_pattern_library = {{ is_pattern_library }}

+{% endif %} +{% endblock %} diff --git a/tests/jinja/patterns_jinja/pages/test_page/test_page.yaml b/tests/jinja/patterns_jinja/pages/test_page/test_page.yaml new file mode 100644 index 00000000..4812e07e --- /dev/null +++ b/tests/jinja/patterns_jinja/pages/test_page/test_page.yaml @@ -0,0 +1,12 @@ +context: + page: + title: Jinja test page + body: > +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, + quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat + non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +

diff --git a/tests/jinja2.py b/tests/jinja2.py new file mode 100644 index 00000000..898d2486 --- /dev/null +++ b/tests/jinja2.py @@ -0,0 +1,56 @@ +from django.apps import apps +from django.template.context_processors import csrf +from django.template.defaultfilters import ( + cut, + date, + linebreaks, + pluralize, + slugify, + truncatewords, + urlencode, +) +from django.contrib.staticfiles.storage import staticfiles_storage +from django.urls import reverse + +from jinja2 import Environment + +from pattern_library.monkey_utils import override_jinja_tags + + +if apps.is_installed("pattern_library"): + override_jinja_tags() + + +def error_tag(): + "Just raise an exception, never do anything" + raise Exception("error_tag raised an exception") + + +def pageurl(page): + """Approximation of wagtail built-in tag for realistic example.""" + return "/page/url" + + +def environment(**options): + env = Environment(**options) + env.globals.update( + { + "static": staticfiles_storage.url, + "url": reverse, + "csrf": csrf, + "error_tag": error_tag, + "pageurl": pageurl, + } + ) + env.filters.update( + { + "cut": cut, + "date": date, + "linebreaks": linebreaks, + "pluralize": pluralize, + "slugify": slugify, + "truncatewords": truncatewords, + "urlencode": urlencode, + } + ) + return env diff --git a/tests/pattern_contexts.py b/tests/pattern_contexts.py index 9857698e..571841e9 100644 --- a/tests/pattern_contexts.py +++ b/tests/pattern_contexts.py @@ -31,7 +31,34 @@ def replicate_pagination(context, request): # add dummy items to force pagination for i in range(50): - object_list.append(None) + object_list.append({"title": i}) + + # paginate and add ListView-like values + paginator = Paginator(object_list, original_length) + context.update( + paginator=paginator, + search_results=paginator.page(10), + is_paginated=True, + object_list=object_list, + ) + + +@register_context_modifier(template="patterns_jinja/pages/search/search.html") +def replicate_pagination_jinja(context, request): + """ + Replace lists of items using the 'page_obj.object_list' key + with a real Paginator page, and add a few other pagination-related + things to the context (like Django's `ListView` does). + """ + object_list = context.pop("search_results", None) + if object_list is None: + return + + original_length = len(object_list) + + # add dummy items to force pagination + for i in range(50): + object_list.append({"title": i}) # paginate and add ListView-like values paginator = Paginator(object_list, original_length) diff --git a/tests/settings/base.py b/tests/settings/base.py index a226c5fd..8f5f7cb4 100644 --- a/tests/settings/base.py +++ b/tests/settings/base.py @@ -1,4 +1,5 @@ import os +from pattern_library.monkey_utils import override_jinja_tags # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -42,12 +43,17 @@ ("atoms", ["patterns/atoms"]), ("molecules", ["patterns/molecules"]), ("pages", ["patterns/pages"]), + ("jinja", ["patterns_jinja/components"]), + ("jinja pages", ["patterns_jinja/pages"]), ], } TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + "tests/templates", + ], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -62,6 +68,21 @@ "builtins": ["pattern_library.loader_tags"], }, }, + { + "BACKEND": "django.template.backends.jinja2.Jinja2", + "DIRS": [ + "tests/jinja", + ], + "APP_DIRS": True, + "OPTIONS": { + "environment": "tests.jinja2.environment", + "extensions": [ + "jinja2.ext.do", + "jinja2.ext.i18n", + "jinja2.ext.loopcontrols", + ], + }, + }, ] MIDDLEWARE = [ diff --git a/tests/templates/patterns/pages/people/person_page.yaml b/tests/templates/patterns/pages/people/person_page.yaml index c82ed9bf..e8c3ea12 100644 --- a/tests/templates/patterns/pages/people/person_page.yaml +++ b/tests/templates/patterns/pages/people/person_page.yaml @@ -5,12 +5,12 @@ context: last_name: Doe photo: fake job_title: Stub person - website: 'https://example.com' - email: 'test@example.com' + website: "https://example.com" + email: "test@example.com" social_media_profile: all: - - profile_url: 'https://example.com/' - - profile_url: 'https://example.com/2' + - profile_url: "https://example.com/" + - profile_url: "https://example.com/2" phone_numbers: all: - phone_number: 12345678 diff --git a/tests/templates/patterns/pages/test_page/test_page.html b/tests/templates/patterns/pages/test_page/test_page.html index 5264f100..e5a241fb 100644 --- a/tests/templates/patterns/pages/test_page/test_page.html +++ b/tests/templates/patterns/pages/test_page/test_page.html @@ -1,3 +1,12 @@ {% extends 'patterns/base_page.html' %} -{% block content %}{{ page.body }}{% endblock %} +{% block content %} +

{{ page.title }}

+{{ page.body }} + +{% include "patterns/molecules/accordion/accordion.html" %} + +{% if is_pattern_library %} +

is_pattern_library = {{ is_pattern_library }}

+{% endif %} +{% endblock %} From dd9e336c20ce8d70ca546dec9cb9f9aad3e3011c Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Tue, 8 Apr 2025 12:40:22 +0100 Subject: [PATCH 17/18] TODO-ify the missing Jinja tags / functions override support --- docs/reference/known-issues.md | 2 +- .../components/test_includes/test_includes.html | 3 ++- .../components/test_includes/test_includes.yaml | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/reference/known-issues.md b/docs/reference/known-issues.md index a54ac9c2..583b3886 100644 --- a/docs/reference/known-issues.md +++ b/docs/reference/known-issues.md @@ -70,7 +70,7 @@ There is experimental support, excluding overrides of arbitrary tags, functions, ### Jinja2 support -🎉 This is now addressed as of v1.4.0, though with only experimental support, and no capability to override tags, functions, filters (see above). +🎉 This is now addressed as of v1.5.0, though with only experimental support, and no capability to override tags, functions, filters (see above). ### No way to specify objects that have attributes and support iteration diff --git a/tests/jinja/patterns_jinja/components/test_includes/test_includes.html b/tests/jinja/patterns_jinja/components/test_includes/test_includes.html index 2a40179b..363b9015 100644 --- a/tests/jinja/patterns_jinja/components/test_includes/test_includes.html +++ b/tests/jinja/patterns_jinja/components/test_includes/test_includes.html @@ -1,4 +1,5 @@ {% include 'non-patterns/include.html' %} {% include variable_include %} -{{ error_tag() }} +{# TODO: Unsupported override with Jinja #} +{# {{ error_tag() }} #} diff --git a/tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml b/tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml index 757b9209..e4fa1aab 100644 --- a/tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml +++ b/tests/jinja/patterns_jinja/components/test_includes/test_includes.yaml @@ -1,7 +1,8 @@ context: - variable_include: non-patterns/variable_include.html + variable_include: non-patterns/variable_include.html +# TODO: Unsupported override with Jinja tags: error_tag: include: - template_name: 'non-patterns/include.html' + template_name: "non-patterns/include.html" From 69f805c1226e4d59d06523ec8e65f4399f581561 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Tue, 8 Apr 2025 12:52:05 +0100 Subject: [PATCH 18/18] Fix linting issues --- tests/jinja2.py | 3 +-- tests/settings/base.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/jinja2.py b/tests/jinja2.py index 898d2486..fa55978c 100644 --- a/tests/jinja2.py +++ b/tests/jinja2.py @@ -1,4 +1,5 @@ from django.apps import apps +from django.contrib.staticfiles.storage import staticfiles_storage from django.template.context_processors import csrf from django.template.defaultfilters import ( cut, @@ -9,14 +10,12 @@ truncatewords, urlencode, ) -from django.contrib.staticfiles.storage import staticfiles_storage from django.urls import reverse from jinja2 import Environment from pattern_library.monkey_utils import override_jinja_tags - if apps.is_installed("pattern_library"): override_jinja_tags() diff --git a/tests/settings/base.py b/tests/settings/base.py index 8f5f7cb4..7a615f8f 100644 --- a/tests/settings/base.py +++ b/tests/settings/base.py @@ -1,5 +1,4 @@ import os -from pattern_library.monkey_utils import override_jinja_tags # Build paths inside the project like this: os.path.join(BASE_DIR, ...)