Skip to content

Commit

Permalink
Introduce support for jinja2-based templates.
Browse files Browse the repository at this point in the history
  • Loading branch information
albu-diku committed Mar 1, 2025
1 parent 187b17c commit e5e59cd
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Byte-compiled / optimized / DLL files
__jinja__/
__pycache__/
*.py[cod]
*$py.class
Expand Down
23 changes: 21 additions & 2 deletions mig/shared/objecttypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@
# -- END_HEADER ---
#

from mig.shared.templates import init_global_templates

""" Defines valid objecttypes and provides a method to verify if an object is correct """

Check warning on line 31 in mig/shared/objecttypes.py

View workflow job for this annotation

GitHub Actions / Style Check Python with Lint

line too long (89 > 80 characters)

start = {'object_type': 'start', 'required': [], 'optional': ['headers'
]}
end = {'object_type': 'end', 'required': [], 'optional': []}
template = {'object_type': 'template'}
timing_info = {'object_type': 'timing_info', 'required': [],
'optional': []}
title = {'object_type': 'title', 'required': ['text'],
Expand Down Expand Up @@ -396,6 +399,7 @@
valid_types_list = [
start,
end,
template,
timing_info,
title,
text,
Expand Down Expand Up @@ -499,6 +503,8 @@
image_settings_list,
]

base_template_required = set(('template_name', 'template_group', 'template_args,'))

Check warning on line 506 in mig/shared/objecttypes.py

View workflow job for this annotation

GitHub Actions / Style Check Python with Lint

line too long (83 > 80 characters)

# valid_types_dict = {"title":title, "link":link, "header":header}

# autogenerate dict based on list. Dictionary access is prefered to allow
Expand Down Expand Up @@ -539,8 +545,8 @@ def get_object_type_info(object_type_list):
return out


def validate(input_object):
""" validate input_object """
def validate(input_object, configuration=None):
""" validate presented objects against their definitions """

if not type(input_object) == type([]):

Check warning on line 551 in mig/shared/objecttypes.py

View workflow job for this annotation

GitHub Actions / Style Check Python with Lint

do not compare types, for exact checks use `is` / `is not`, for instance checks use `isinstance()`
return (False, 'validate object must be a list' % ())
Expand All @@ -560,6 +566,19 @@ def validate(input_object):

this_object_type = obj['object_type']
valid_object_type = valid_types_dict[this_object_type]

if this_object_type == 'template':
# the required keys stuff below is not applicable to templates
# because templates know what they need in terms of data thus
# are self-documenting - use this fact to perform validation
#template_ref = "%s_%s.html" % (obj['template_group'], )

Check warning on line 574 in mig/shared/objecttypes.py

View workflow job for this annotation

GitHub Actions / Style Check Python with Lint

block comment should start with '# '
store = init_global_templates(configuration)
template = store.grab_template(obj['template_name'], obj['template_group'], 'html')
valid_object_type = {
'required': store.extract_variables(template)
}
obj = obj.get('template_args', None)

if 'required' in valid_object_type:
for req in valid_object_type['required']:
if req not in obj:
Expand Down
10 changes: 10 additions & 0 deletions mig/shared/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from mig.shared.prettyprinttable import pprint_table
from mig.shared.pwcrypto import sorted_hash_algos
from mig.shared.safeinput import html_escape
from mig.shared.templates import init_global_templates


row_name = ('even', 'odd')
Expand Down Expand Up @@ -746,6 +747,15 @@ def html_format(configuration, ret_val, ret_msg, out_obj):
for i in out_obj:
if i['object_type'] == 'start':
pass
elif i['object_type'] == 'template':
store = init_global_templates(configuration)
template = store.grab_template(
i['template_name'],
i['template_group'],
'html',
store.context.extend(**i['template_args'])
)
lines.append(template.render())
elif i['object_type'] == 'error_text':
msg = "%(text)s" % i
if i.get('exc', False):
Expand Down
139 changes: 139 additions & 0 deletions mig/shared/templates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/python

Check failure on line 1 in mig/shared/templates/__init__.py

View workflow job for this annotation

GitHub Actions / Style Check Python with Lint

Imports are incorrectly sorted and/or formatted.
# -*- coding: utf-8 -*-
#
# --- BEGIN_HEADER ---
#
# base - shared base helper functions
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
#
# This file is part of MiG.
#
# MiG is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# MiG is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# -- END_HEADER ---
#

from collections import ChainMap
from jinja2 import meta as jinja2_meta, select_autoescape, Environment, \
FileSystemLoader, FileSystemBytecodeCache
import os
import weakref

from mig.shared.defaults import MIG_BASE

TEMPLATES_DIR = os.path.abspath(os.path.dirname(__file__))
TEMPLATES_CACHE_DIR = os.path.join(TEMPLATES_DIR, '__jinja__')

_all_template_dirs = [
os.path.join(TEMPLATES_DIR, 'pages'),
os.path.join(TEMPLATES_DIR, 'partials')
]
_global_store = None

def cache_dir():
return TEMPLATES_CACHE_DIR


def template_dirs():
return _all_template_dirs


class _FormatContext:
def __init__(self, configuration):
self.output_format = None
self.configuration = configuration
self.conf_map = ChainMap(configuration)
self.script_map = {}
self.style_map = {}

def __getitem__(self, key):
return self.__dict__[key]

def __iter__(self):
return iter(self.__dict__)

def extend(self, **kwargs):
return ChainMap(kwargs, self)


class TemplateStore:
def __init__(self, template_dirs, cache_dir=None, extra_globals=None):
assert cache_dir is not None

self._template_globals = extra_globals
self._template_environment = Environment(
loader=FileSystemLoader(template_dirs),
bytecode_cache=FileSystemBytecodeCache(cache_dir, '%s'),
autoescape=select_autoescape()
)

@property
def context(self):
return self._template_globals

def _get_template(self, template_fqname):
return self._template_environment.get_template(template_fqname)

def _grab_template(template_name, template_group, output_format):
template_fqname = "%s_%s.%s.jinja" % (template_group, template_name, output_format)
return _template_environment.get_template(template_fqname)


def grab_template(self, template_name, template_group, output_format, template_globals=None, **kwargs):
template_fqname = "%s_%s.%s.jinja" % (template_group, template_name, output_format)
return self._template_environment.get_template(template_fqname, globals=template_globals)


def list_templates(self):
return self._template_environment.list_templates()


def extract_variables(self, template_fqname):
template = self._template_environment.get_template(template_fqname)
with open(template.filename) as f:
template_source = f.read()
ast = self._template_environment.parse(template_source)
return jinja2_meta.find_undeclared_variables(ast)

@staticmethod
def populated(template_dirs, cache_dir=None, context=None):
assert cache_dir is not None

try:
os.mkdir(cache_dir)
except FileExistsError:
pass

store = TemplateStore(template_dirs, cache_dir=cache_dir, extra_globals=context)

for template_fqname in store.list_templates():
store._get_template(template_fqname)

return store


def init_global_templates(configuration):
global _global_store

if _global_store is not None:
return _global_store

_global_store = TemplateStore.populated(
template_dirs(),
cache_dir=cache_dir(),
context=_FormatContext(configuration)
)

return _global_store
74 changes: 74 additions & 0 deletions mig/shared/templates/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/python

Check failure on line 1 in mig/shared/templates/__main__.py

View workflow job for this annotation

GitHub Actions / Style Check Python with Lint

Imports are incorrectly sorted and/or formatted.
# -*- coding: utf-8 -*-
#
# --- BEGIN_HEADER ---
#
# base - shared base helper functions
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
#
# This file is part of MiG.
#
# MiG is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# MiG is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# -- END_HEADER ---
#

from types import SimpleNamespace
import os
import sys

from mig.shared.templates import cache_dir, \
_get_template, _grab_template_vars, _template_environment


def warn(message):
print(message, file=sys.stderr, flush=True)


def main(args, _print=print):
command = args.command

if command == 'show':
print(_template_environment.list_templates())
elif command == 'prime':
try:
os.mkdir(cache_dir())
except FileExistsError:
pass

for template_name in _list_templates():
_get_template(template_name)
elif command == 'vars':
for template_ref in _template_environment.list_templates():
_print("<%s>" % (template_ref,))
for var in _grab_template_vars(template_ref):
_print(" %s" % (var,))
else:
raise RuntimeError("unknown command: %s" % (command,))


if __name__ == '__main__':
if len(sys.argv) == 2:
command = sys.argv[1]
else:
command = 'show'
args = SimpleNamespace(command=command)

try:
main(args)
sys.exit(0)
except Exception as exc:
warn(str(exc))
sys.exit(1)
1 change: 1 addition & 0 deletions mig/shared/templates/partials/partial_other.html.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<q>{{ other }}</q>
1 change: 1 addition & 0 deletions mig/shared/templates/partials/partial_something.html.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<aside>{{ content }}</aside>
2 changes: 1 addition & 1 deletion mig/wsgi-bin/migwsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def stub(configuration, client_id, import_path, backend, user_arguments_dict,
crash_helper(configuration, backend, output_objects)
return (output_objects, returnvalues.ERROR)

(val_ret, val_msg) = validate(output_objects)
(val_ret, val_msg) = validate(output_objects, configuration=configuration)
if not val_ret:
(ret_code, ret_msg) = returnvalues.OUTPUT_VALIDATION_ERROR
bailout_helper(configuration, backend, output_objects,
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ email-validator;python_version >= "3.7"
email-validator<2.0;python_version >= "3" and python_version < "3.7"
email-validator<1.3;python_version < "3"

jinja2

# NOTE: additional optional dependencies depending on site conf are listed
# in recommended.txt and can be installed in the same manner by pointing
# pip there.
1 change: 1 addition & 0 deletions tests/snapshots/test_objects_with_type_template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<aside>here!!</aside>
32 changes: 32 additions & 0 deletions tests/test_mig_shared_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import print_function

Check failure on line 1 in tests/test_mig_shared_templates.py

View workflow job for this annotation

GitHub Actions / Style Check Python with Lint

Imports are incorrectly sorted and/or formatted.

from tests.support import MigTestCase, testmain

from mig.shared.templates import TemplateStore, cache_dir, template_dirs


class TestMigSharedTemplates(MigTestCase):
def _provide_configuration(self):
return 'testconfig'

def test_the_creation_of_a_template_store(self):
store = TemplateStore.populated(template_dirs(), cache_dir=cache_dir())
self.assertIsInstance(store, TemplateStore)

def test_a_listing_all_templates(self):
store = TemplateStore.populated(template_dirs(), cache_dir=cache_dir())
self.assertEqual(len(store.list_templates()), 2)

def test_grab_template(self):
store = TemplateStore.populated(template_dirs(), cache_dir=cache_dir())
template = store.grab_template('other', 'partial', 'html')
pass

def test_variables_for_remplate_ref(self):
store = TemplateStore.populated(template_dirs(), cache_dir=cache_dir())
template_vars = store.extract_variables('partial_something.html.jinja')
self.assertEqual(template_vars, set(['content']))


if __name__ == '__main__':
testmain()
26 changes: 26 additions & 0 deletions tests/test_mig_wsgibin.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,32 @@ def test_objects_with_type_text(self):
output, _ = self.assertWsgiResponse(wsgi_result, self.fake_wsgi, 200)
self.assertSnapshotOfHtmlContent(output)

def test_objects_with_type_template(self):
output_objects = [
# workaround invalid HTML being generated with no title object
{
'object_type': 'title',
'text': 'TEST'
},
{
'object_type': 'template',
'template_name': 'something',
'template_group': 'partial',
'template_args': {
'content': 'here!!'
}
}
]
self.fake_backend.set_response(output_objects, returnvalues.OK)

wsgi_result = migwsgi.application(
*self.application_args,
**self.application_kwargs
)

output, _ = self.assertWsgiResponse(wsgi_result, self.fake_wsgi, 200)
self.assertSnapshotOfHtmlContent(output)


if __name__ == '__main__':
testmain()

0 comments on commit e5e59cd

Please sign in to comment.