Skip to content

Commit

Permalink
Add option to validate templates using jinja2schema
Browse files Browse the repository at this point in the history
Use jinja2schema to validate templates when --validate-templates
is passed in.

Closes thanethomson#99
  • Loading branch information
kx-chen committed Dec 12, 2018
1 parent 0a90187 commit f356972
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 9 deletions.
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 @@ -248,7 +249,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
27 changes: 27 additions & 0 deletions statik/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import stat
from copy import deepcopy, copy
import shutil
import re

import six
from jinja2schema import to_json_schema, infer

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

DEFAULT_CONFIG_CONTENT = """project-name: Your project name
Expand Down Expand Up @@ -382,3 +385,27 @@ def find_duplicates_in_array(array):
duplicates.append(item)

return duplicates


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))
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))
except Exception as e:
logger.warning(
"Template validation failed (likely an issue with jinja2schema): %s: %s",
template, e)
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()

0 comments on commit f356972

Please sign in to comment.