From 70e980c5c0c748295642b71cbe64b13c93f8a7bd Mon Sep 17 00:00:00 2001 From: s3lph Date: Mon, 25 Nov 2019 04:35:50 +0100 Subject: [PATCH] Add unit tests for the template engine --- spaceapi_server/plugins/__init__.py | 8 +-- spaceapi_server/server/__init__.py | 26 +------- spaceapi_server/template/__init__.py | 40 +++++++++--- spaceapi_server/template/test/__init__.py | 0 .../template/test/test_template.py | 61 +++++++++++++++++++ 5 files changed, 98 insertions(+), 37 deletions(-) create mode 100644 spaceapi_server/template/test/__init__.py create mode 100644 spaceapi_server/template/test/test_template.py diff --git a/spaceapi_server/plugins/__init__.py b/spaceapi_server/plugins/__init__.py index a593eab..b1a4e5a 100644 --- a/spaceapi_server/plugins/__init__.py +++ b/spaceapi_server/plugins/__init__.py @@ -1,5 +1,5 @@ -from spaceapi_server.template import env_init +from spaceapi_server.template import _env_init def template_function(fn): @@ -8,7 +8,7 @@ def template_function(fn): :param fn: The function to register. """ # Make sure the Jinja2 environment is initialized - env = env_init() + env = _env_init() # Add the function to the environment's globals env.globals[fn.__name__] = fn return fn @@ -20,7 +20,7 @@ def template_filter(fn): :param fn: The function to register. """ # Make sure the Jinja2 environment is initialized - env = env_init() + env = _env_init() # Add the function to the environment's filters env.filters[fn.__name__] = fn return fn @@ -32,7 +32,7 @@ def template_test(fn): :param fn: The function to register. """ # Make sure the Jinja2 environment is initialized - env = env_init() + env = _env_init() # Add the function to the environment's tests env.tests[fn.__name__] = fn return fn diff --git a/spaceapi_server/server/__init__.py b/spaceapi_server/server/__init__.py index 36193da..af00c60 100644 --- a/spaceapi_server/server/__init__.py +++ b/spaceapi_server/server/__init__.py @@ -13,35 +13,11 @@ from spaceapi_server import template, config __TEMPLATE = None -def render_traverse(obj): - """ - Walk through a complex, JSON-serializable data structure, and pass - string objects through the Jinja2 templating engine. - :param obj: The object to traverse. - """ - if isinstance(obj, list): - # list -> recurse into each item - for i in range(len(obj)): - obj[i] = render_traverse(obj[i]) - return obj - elif isinstance(obj, dict): - # dict -> recurse into the value of each (key, value) - for k, v in obj.items(): - obj[k] = render_traverse(obj[k]) - return obj - elif isinstance(obj, str): - # str -> template - return template.render(obj) - else: - # anything else -> return as-is - return obj - - @bottle.route('/') def serve(): global __TEMPLATE # Render the response template - rendered = render_traverse(__TEMPLATE) + rendered = template.render_traverse(__TEMPLATE) # Set the response Content-Type bottle.response.content_type = 'application/json; charset=utf-8' # CORS "whitelist" diff --git a/spaceapi_server/template/__init__.py b/spaceapi_server/template/__init__.py index 59b7663..6b1cbbc 100644 --- a/spaceapi_server/template/__init__.py +++ b/spaceapi_server/template/__init__.py @@ -4,19 +4,19 @@ import jinja2 # The Jinja2 environment -_ENV = None +__ENV = None -def env_init(force: bool = False): +def _env_init(force: bool = False): """ Initialize the Jinja2 environment. :param force: If true, force reload the environment. """ - global _ENV - if _ENV is None or force: + global __ENV + if __ENV is None or force: # Use json.dumps as finalizer in order to preserve complex data structures - _ENV = jinja2.Environment(finalize=json.dumps) - return _ENV + __ENV = jinja2.Environment(finalize=json.dumps) + return __ENV def render(template: str): @@ -24,9 +24,33 @@ def render(template: str): Render the given string as a Jinja2 template. :param template: The template string to render. """ - # Make sure the Jinaj2 environment is initialized - env = env_init() + # Make sure the Jinja2 environment is initialized + env = _env_init() # Create a Jinja2 template from the input string t = env.from_string(template) # Render the template and turn the JSON dump back into complex data structures return json.loads(t.render()) + + +def render_traverse(obj): + """ + Walk through a complex, JSON-serializable data structure, and pass + string objects through the Jinja2 templating engine. + :param obj: The object to traverse. + """ + if isinstance(obj, list): + # list -> recurse into each item + for i in range(len(obj)): + obj[i] = render_traverse(obj[i]) + return obj + elif isinstance(obj, dict): + # dict -> recurse into the value of each (key, value) + for k, v in obj.items(): + obj[k] = render_traverse(obj[k]) + return obj + elif isinstance(obj, str): + # str -> template + return render(obj) + else: + # anything else -> return as-is + return obj diff --git a/spaceapi_server/template/test/__init__.py b/spaceapi_server/template/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spaceapi_server/template/test/test_template.py b/spaceapi_server/template/test/test_template.py new file mode 100644 index 0000000..df4404d --- /dev/null +++ b/spaceapi_server/template/test/test_template.py @@ -0,0 +1,61 @@ + +import unittest + +from spaceapi_server.template import _env_init, render, render_traverse +from spaceapi_server.plugins import template_function, template_filter, template_test + + +class TemplateTest(unittest.TestCase): + + @staticmethod + @template_function + def template_test_function(value): + return f'test_{value}' + + @staticmethod + @template_filter + def template_test_filter(value, other): + return f'test_{other}_{value}' + + @staticmethod + @template_test + def template_test_test(value): + return value == 'baz' + + def test_template(self): + env = _env_init() + self.assertEqual('"not a template"', env.from_string('not a template').render()) + self.assertEqual('"a template"', env.from_string('{{ "a template" }}').render()) + self.assertEqual('42', env.from_string('{{ 42 }}').render()) + self.assertEqual('["foo"]', env.from_string('{{ [ "foo" ] }}').render()) + self.assertEqual('{"foo": ["bar"]}', env.from_string('{{ { "foo": [ "bar" ] } }}').render()) + + def test_render(self): + self.assertEqual('not a template', render('not a template')) + self.assertEqual('a template', render('{{ "a template" }}')) + self.assertEqual(42, render('{{ 42 }}')) + self.assertEqual(['foo'], render('{{ [ "foo" ] }}')) + self.assertEqual({'foo': ['bar']}, render('{{ { "foo": [ "bar" ] } }}')) + + def test_render_traverse(self): + template = { + 'foo': 42, + 'bar': [1, 2, 3], + 'notemplate': 'foo', + 'builtin': '{{ [ 1337, 42 ] | first }}', + 'test_functions': { + 'test_function': '{{ template_test_function("foo") }}', + 'test_filter': '{{ "bar" | template_test_filter("other") }}', + 'test_test_true': '{{ "baz" is template_test_test }}', + 'test_test_false': '{{ "foo" is template_test_test }}' + } + } + rendered = render_traverse(template) + self.assertEqual(42, rendered['foo']) + self.assertEqual([1, 2, 3], rendered['bar']) + self.assertEqual('foo', rendered['notemplate']) + self.assertEqual(1337, rendered['builtin']) + self.assertEqual('test_foo', rendered['test_functions']['test_function']) + self.assertEqual('test_other_bar', rendered['test_functions']['test_filter']) + self.assertTrue(rendered['test_functions']['test_test_true']) + self.assertFalse(rendered['test_functions']['test_test_false'])