Skip to content
This repository has been archived by the owner on Jan 8, 2025. It is now read-only.

Add option to validate templates using jinja2schema #105

Open
wants to merge 1 commit into
base: master
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
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ Unidecode==1.0.22
watchdog==0.9.0
paramiko==2.4.2
git+https://github.com/kx-chen/netlify_deployer.git#egg=netlify_uploader
jinja2schema
9 changes: 8 additions & 1 deletion statik/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ def main():
help='Display version info for Statik',
action='store_true',
)

group_info.add_argument(
'--validate-templates',
help='Validate that variables used in jinja templates are actually available to be used.',
action='store_true',
)
args = parser.parse_args()

error_context = StatikErrorContext()
Expand Down Expand Up @@ -215,7 +221,8 @@ def main():
output_path=output_path,
in_memory=False,
safe_mode=args.safe_mode,
error_context=error_context
error_context=error_context,
validate_templates=args.validate_templates,
)

if args.upload and args.upload == 'SFTP':
Expand Down
4 changes: 2 additions & 2 deletions statik/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
]


def generate(input_path, output_path=None, in_memory=False, safe_mode=False, error_context=None):
def generate(input_path, output_path=None, in_memory=False, safe_mode=False, error_context=None, validate_templates=False):
"""Executes the Statik site generator using the given parameters.
"""
project = StatikProject(input_path, safe_mode=safe_mode, error_context=error_context)
project = StatikProject(input_path, validate_templates=validate_templates, safe_mode=safe_mode, error_context=error_context)
return project.generate(output_path=output_path, in_memory=in_memory)
4 changes: 3 additions & 1 deletion statik/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(self, path, **kwargs):
"""
self.error_context = kwargs.pop('error_context', None)
self.error_context = self.error_context or StatikErrorContext()
self.validate_templates = kwargs.pop('validate_templates', False)

if 'config' in kwargs and isinstance(kwargs['config'], dict):
logger.debug("Loading project configuration from constructor arguments")
Expand Down Expand Up @@ -251,7 +252,8 @@ def process_views(self):
view.process(
self.db,
safe_mode=self.safe_mode,
extra_context=self.project_context
extra_context=self.project_context,
validate_templates=self.validate_templates,
)
)
except StatikError as exc:
Expand Down
29 changes: 29 additions & 0 deletions statik/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import re

import six
from jinja2schema import to_json_schema, infer

if six.PY3:
import importlib.util
Expand Down Expand Up @@ -46,6 +47,7 @@
'uncapitalize',
'find_duplicates_in_array',
'camel_to_snake',
'validate_jinja_template',
]

DEFAULT_CONFIG_CONTENT = """project-name: Your project name
Expand Down Expand Up @@ -385,5 +387,32 @@ def find_duplicates_in_array(array):

return duplicates


def camel_to_snake(camel):
return '_'.join(re.findall(r'[A-Z][a-z]*', camel))


def validate_jinja_template(template, ctx):
"""Checks that variables used in templates are actually available to be used in context.
Logs a warning if a variable used in the template is not found in the context.

Args:
template: Path to the template to check
ctx: dictionary of the variables in context
"""
tag_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
with open(template) as file:
stripped_template = tag_re.sub('', file.read())
try:
result = to_json_schema(infer(stripped_template))
except Exception as e:
logger.warning(
"Template inspection using jinja2schema failed in %s: %s",
template, e)
return

for item in result['required']:
if item not in list(ctx.keys()):
logger.warning("'%s' was used in a template, "
"but it's not available to be used. Template: %s"
% (item, template))
15 changes: 11 additions & 4 deletions statik/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from statik.errors import *
from statik.utils import *
from statik.context import StatikContext
from statik.utils import validate_jinja_template

import logging

Expand Down Expand Up @@ -248,9 +249,12 @@ def __repr__(self):
def __str__(self):
return repr(self)

def render(self, context, db=None, safe_mode=False, extra_context=None):
def render(self, context, db=None, safe_mode=False, extra_context=None, validate_templates=False):
ctx = context.build(db=db, safe_mode=safe_mode, extra=extra_context)
logger.debug("Rendering view %s with context: %s", self.view_name, ctx)
if validate_templates:
validate_jinja_template(self.template.filename, ctx)

return dict_from_path(
self.path.render(),
final_value=self.template.render(ctx)
Expand Down Expand Up @@ -279,7 +283,7 @@ def __repr__(self):
def __str__(self):
return repr(self)

def render(self, context, db=None, safe_mode=False, extra_context=None):
def render(self, context, db=None, safe_mode=False, extra_context=None, validate_templates=False):
"""Renders the given context using the specified database, returning a dictionary
containing path segments and rendered view contents."""
if not db:
Expand All @@ -302,6 +306,8 @@ def render(self, context, db=None, safe_mode=False, extra_context=None):
extra=extra_ctx
)
inst_path = self.path.render(inst=inst, context=ctx)
if validate_templates:
validate_jinja_template(self.template.filename, ctx)
rendered_view = self.template.render(ctx)
rendered_views = deep_merge_dict(
rendered_views,
Expand Down Expand Up @@ -387,13 +393,14 @@ def __repr__(self):
def __str__(self):
return repr(self)

def process(self, db, safe_mode=False, extra_context=None):
def process(self, db, safe_mode=False, extra_context=None, validate_templates=False):
"""Deprecated. Rather use StatikView.render()."""
return self.renderer.render(
self.context,
db,
safe_mode=safe_mode,
extra_context=extra_context
extra_context=extra_context,
validate_templates=validate_templates,
)

def render(self, db, safe_mode=False, extra_context=None):
Expand Down
10 changes: 9 additions & 1 deletion tests/modular/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from statik.utils import *


TEST_XML = """
<div class="something">
Hello world!
Expand Down Expand Up @@ -85,6 +84,15 @@ def test_get_url_file_ext(self):
''
)

def test_validate_jinja_template(self):
from mock import patch, mock_open
with patch("statik.utils.logger.warning") as mock_logger:
with patch("statik.utils.open", mock_open(read_data="{{ baz }}")):
validate_jinja_template('/path/to/somewhere', {'posts': 'foo'})
mock_logger.assert_called_with("'baz' was used in a template, "
"but it's not available to be used. "
"Template: /path/to/somewhere")


if __name__ == "__main__":
unittest.main()