Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cb109/6ee383606577fca1772cdbb98e374ce3 to your computer and use it in GitHub Desktop.
Save cb109/6ee383606577fca1772cdbb98e374ce3 to your computer and use it in GitHub Desktop.
Visualize Django Template Include Hierarchy with Graphviz
"""Point script at Django template name and output DOT text describing the includes.
Starting template name must be something relative like
'myapp/mysubfolder/template_1.html'. The script will follow the inputs and output a text like:
digraph G {
node [shape="rectangle"];
"myapp/mysubfolder/template_1.html" -> "myapp/mysubfolder/template_2.html";
"myapp/mysubfolder/template_2.html" -> "myapp/mysubfolder/template_3.html";
...
}
This output can then be visualized with graphviz e.g. via
https://dreampuf.github.io/GraphvizOnline. It's supposed to quickly
visualize template hierarchies so we don't forget to update them as
needed.
Template {% extends ... %} statements are not handled here, as are other
things like CBV based template detection, custom template loaders etc.
"""
import os
import re
# from django.conf import settings
from django.template.utils import get_app_template_dirs
TEMPLATES_FOLDER_BASE_NAME = "templates"
template_dirs = list(get_app_template_dirs(TEMPLATES_FOLDER_BASE_NAME))
# for template_config in settings.TEMPLATES:
# dirs = template_config.get("DIRS")
# if not dirs:
# continue
# template_dirs.extend(dirs)
RE_PATTERN_INCLUDE = (
r"\{\%\s*include\s*(?:\"|\')(?P<template>[\w\/]+\.html)(?:\"|\')(?:.*)\%\}"
)
# Change this as needed
starting_template_name = "myapp/mysubfolder/template_1.html"
def main():
template_name_to_abs_filepath = {}
for template_dir in template_dirs:
for root, _, filenames in os.walk(template_dir):
for filename in filenames:
if not filename.endswith(".html"):
continue
abs_filepath = os.path.join(root, filename)
_, template_name = abs_filepath.split(
f"{TEMPLATES_FOLDER_BASE_NAME}/", 1
)
template_name_to_abs_filepath[template_name] = abs_filepath
def filepath_for_template_name(template_name):
return template_name_to_abs_filepath[template_name]
class TemplateNode:
def __init__(self, template_name, template_filepath, parent=None, level=0):
self.template_name = template_name
self.template_filepath = template_filepath
self.parent = parent
if self.parent:
self.parent.children.append(self)
self.children = []
self.level = level
def build_tree(self):
with open(self.template_filepath) as f:
content = f.read()
matches = re.findall(RE_PATTERN_INCLUDE, content)
for template_name in matches:
node = TemplateNode(
template_name=template_name,
template_filepath=filepath_for_template_name(template_name),
parent=self,
level=self.level + 1,
)
node.build_tree()
def collect_graphviz_lines(self, lines=None):
basename = os.path.basename(self.template_name)
dirname = os.path.dirname(self.template_name)
node_line = (
f' "{self.template_name}"'
f"[color=gray]"
f'[label=<{basename}<BR/><FONT COLOR="gray47" '
f'POINT-SIZE="11">{dirname}</FONT>>];'
)
lines.append(node_line)
if self.parent:
lines.append(
f' "{self.parent.template_name}" -> "{self.template_name}";'
)
for child in self.children:
child.collect_graphviz_lines(lines=lines)
def tree_to_graphviz(node):
lines = [
"digraph G {",
' node [shape="rectangle"][fontname="Arial"];',
]
node.collect_graphviz_lines(lines=lines)
lines.append("}")
text = "\n".join(lines)
return text
node = TemplateNode(
template_name=starting_template_name,
template_filepath=filepath_for_template_name(starting_template_name),
)
node.build_tree()
graphviz_text = tree_to_graphviz(node)
print(graphviz_text)
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment