Skip to content

Commit 0e3d381

Browse files
committed
Introduce support for jinja2-based templates.
Make sure there is a separation of the test templates from the mainline stuff and tweak paths.
1 parent d00442f commit 0e3d381

File tree

14 files changed

+370
-5
lines changed

14 files changed

+370
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Byte-compiled / optimized / DLL files
2+
__jinja__/
23
__pycache__/
34
*.py[cod]
45
*$py.class

mig/assets/templates/.gitkeep

Whitespace-only changes.

mig/lib/__init__.py

Whitespace-only changes.

mig/lib/templates/__init__.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# --- BEGIN_HEADER ---
5+
#
6+
# base - shared base helper functions
7+
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
8+
#
9+
# This file is part of MiG.
10+
#
11+
# MiG is free software: you can redistribute it and/or modify
12+
# it under the terms of the GNU General Public License as published by
13+
# the Free Software Foundation; either version 2 of the License, or
14+
# (at your option) any later version.
15+
#
16+
# MiG is distributed in the hope that it will be useful,
17+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
# GNU General Public License for more details.
20+
#
21+
# You should have received a copy of the GNU General Public License
22+
# along with this program; if not, write to the Free Software
23+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24+
#
25+
# -- END_HEADER ---
26+
#
27+
28+
import errno
29+
from jinja2 import meta as jinja2_meta, select_autoescape, Environment, \
30+
FileSystemLoader, FileSystemBytecodeCache
31+
import os
32+
import weakref
33+
34+
from mig.shared.compat import PY2
35+
from mig.shared.defaults import MIG_BASE
36+
37+
38+
if PY2:
39+
from chainmap import ChainMap
40+
else:
41+
from collections import ChainMap
42+
43+
TEMPLATES_DIR = os.path.join(MIG_BASE, 'mig/assets/templates')
44+
TEMPLATES_CACHE_DIR = os.path.join(TEMPLATES_DIR, '__jinja__')
45+
46+
_all_template_dirs = [
47+
TEMPLATES_DIR,
48+
]
49+
_global_store = None
50+
51+
52+
def _clear_global_store():
53+
global _global_store
54+
_global_store = None
55+
56+
57+
def cache_dir():
58+
return TEMPLATES_CACHE_DIR
59+
60+
61+
def template_dirs():
62+
return _all_template_dirs
63+
64+
65+
class _BonundTemplate:
66+
def __init__(self, template, template_args):
67+
self.tmpl = template
68+
self.args = template_args
69+
70+
71+
def render(self):
72+
return self.tmpl.render(**self.args)
73+
74+
75+
class _FormatContext:
76+
def __init__(self, configuration):
77+
self.output_format = None
78+
self.configuration = configuration
79+
self.script_map = {}
80+
self.style_map = {}
81+
82+
def __getitem__(self, key):
83+
return self.__dict__[key]
84+
85+
def __iter__(self):
86+
return iter(self.__dict__)
87+
88+
def extend(self, template, template_args):
89+
return _BonundTemplate(template, ChainMap(template_args, self))
90+
91+
92+
class TemplateStore:
93+
def __init__(self, template_dirs, cache_dir=None, extra_globals=None):
94+
assert cache_dir is not None
95+
96+
self._cache_dir = cache_dir
97+
self._template_globals = extra_globals
98+
self._template_environment = Environment(
99+
loader=FileSystemLoader(template_dirs),
100+
bytecode_cache=FileSystemBytecodeCache(cache_dir, '%s'),
101+
autoescape=select_autoescape()
102+
)
103+
104+
@property
105+
def cache_dir(self):
106+
return self._cache_dir
107+
108+
@property
109+
def context(self):
110+
return self._template_globals
111+
112+
def _get_template(self, template_fqname):
113+
return self._template_environment.get_template(template_fqname)
114+
115+
def grab_template(self, template_name, template_group, output_format, template_globals=None, **kwargs):
116+
template_fqname = "%s_%s.%s.jinja" % (
117+
template_group, template_name, output_format)
118+
return self._template_environment.get_template(template_fqname, globals=template_globals)
119+
120+
def list_templates(self):
121+
return [t for t in self._template_environment.list_templates() if t.endswith('.jinja')]
122+
123+
def extract_variables(self, template_fqname):
124+
template = self._template_environment.get_template(template_fqname)
125+
with open(template.filename) as f:
126+
template_source = f.read()
127+
ast = self._template_environment.parse(template_source)
128+
return jinja2_meta.find_undeclared_variables(ast)
129+
130+
@staticmethod
131+
def populated(template_dirs, cache_dir=None, context=None):
132+
assert cache_dir is not None
133+
134+
try:
135+
os.mkdir(cache_dir)
136+
except OSError as direxc:
137+
if direxc.errno != errno.EEXIST: # FileExistsError
138+
raise
139+
140+
store = TemplateStore(
141+
template_dirs, cache_dir=cache_dir, extra_globals=context)
142+
143+
for template_fqname in store.list_templates():
144+
store._get_template(template_fqname)
145+
146+
return store
147+
148+
149+
def init_global_templates(configuration, _templates_dirs=template_dirs):
150+
global _global_store
151+
152+
if _global_store is not None:
153+
return _global_store
154+
155+
_global_store = TemplateStore.populated(
156+
_templates_dirs(),
157+
cache_dir=cache_dir(),
158+
context=_FormatContext(configuration)
159+
)
160+
161+
return _global_store

mig/lib/templates/__main__.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# --- BEGIN_HEADER ---
5+
#
6+
# base - shared base helper functions
7+
# Copyright (C) 2003-2024 The MiG Project by the Science HPC Center at UCPH
8+
#
9+
# This file is part of MiG.
10+
#
11+
# MiG is free software: you can redistribute it and/or modify
12+
# it under the terms of the GNU General Public License as published by
13+
# the Free Software Foundation; either version 2 of the License, or
14+
# (at your option) any later version.
15+
#
16+
# MiG is distributed in the hope that it will be useful,
17+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
# GNU General Public License for more details.
20+
#
21+
# You should have received a copy of the GNU General Public License
22+
# along with this program; if not, write to the Free Software
23+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24+
#
25+
# -- END_HEADER ---
26+
#
27+
28+
from types import SimpleNamespace
29+
import os
30+
import sys
31+
32+
from mig.lib.templates import init_global_templates
33+
from mig.shared.conf import get_configuration_object
34+
35+
36+
def warn(message):
37+
print(message, file=sys.stderr, flush=True)
38+
39+
40+
def main(args, _print=print):
41+
configuration = get_configuration_object(config_file=args.config_file)
42+
template_store = init_global_templates(configuration)
43+
44+
command = args.command
45+
if command == 'show':
46+
print(template_store.list_templates())
47+
elif command == 'prime':
48+
try:
49+
os.mkdir(template_store.cache_dir)
50+
except FileExistsError:
51+
pass
52+
53+
for template_fqname in template_store.list_templates():
54+
template_store._get_template(template_fqname)
55+
elif command == 'vars':
56+
for template_ref in template_store.list_templates():
57+
_print("<%s>" % (template_ref,))
58+
for var in template_store.extract_variables(template_ref):
59+
_print(" {{%s}}" % (var,))
60+
_print("</%s>" % (template_ref,))
61+
else:
62+
raise RuntimeError("unknown command: %s" % (command,))
63+
64+
65+
if __name__ == '__main__':
66+
import argparse
67+
68+
parser = argparse.ArgumentParser()
69+
parser.add_argument('-c', dest='config_file', default=None)
70+
parser.add_argument('command')
71+
args = parser.parse_args()
72+
73+
try:
74+
main(args)
75+
sys.exit(0)
76+
except Exception as exc:
77+
warn(str(exc))
78+
sys.exit(1)

mig/shared/objecttypes.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@
2828

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

31+
from mig.lib.templates import init_global_templates
32+
33+
3134
start = {'object_type': 'start', 'required': [], 'optional': ['headers'
3235
]}
3336
end = {'object_type': 'end', 'required': [], 'optional': []}
37+
template = {'object_type': 'template'}
3438
timing_info = {'object_type': 'timing_info', 'required': [],
3539
'optional': []}
3640
title = {'object_type': 'title', 'required': ['text'],
@@ -396,6 +400,7 @@
396400
valid_types_list = [
397401
start,
398402
end,
403+
template,
399404
timing_info,
400405
title,
401406
text,
@@ -499,6 +504,8 @@
499504
image_settings_list,
500505
]
501506

507+
base_template_required = set(('template_name', 'template_group', 'template_args,'))
508+
502509
# valid_types_dict = {"title":title, "link":link, "header":header}
503510

504511
# autogenerate dict based on list. Dictionary access is prefered to allow
@@ -539,8 +546,8 @@ def get_object_type_info(object_type_list):
539546
return out
540547

541548

542-
def validate(input_object):
543-
""" validate input_object """
549+
def validate(input_object, configuration=None):
550+
""" validate presented objects against their definitions """
544551

545552
if not type(input_object) == type([]):
546553
return (False, 'validate object must be a list' % ())
@@ -560,6 +567,19 @@ def validate(input_object):
560567

561568
this_object_type = obj['object_type']
562569
valid_object_type = valid_types_dict[this_object_type]
570+
571+
if this_object_type == 'template':
572+
# the required keys stuff below is not applicable to templates
573+
# because templates know what they need in terms of data thus
574+
# are self-documenting - use this fact to perform validation
575+
#template_ref = "%s_%s.html" % (obj['template_group'], )
576+
store = init_global_templates(configuration)
577+
template = store.grab_template(obj['template_name'], obj['template_group'], 'html')
578+
valid_object_type = {
579+
'required': store.extract_variables(template)
580+
}
581+
obj = obj.get('template_args', None)
582+
563583
if 'required' in valid_object_type:
564584
for req in valid_object_type['required']:
565585
if req not in obj:

mig/shared/output.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import time
4444
import traceback
4545

46+
from mig.lib.templates import init_global_templates
4647
from mig.shared import returnvalues
4748
from mig.shared.bailout import bailout_title, crash_helper, \
4849
filter_output_objects
@@ -746,6 +747,15 @@ def html_format(configuration, ret_val, ret_msg, out_obj):
746747
for i in out_obj:
747748
if i['object_type'] == 'start':
748749
pass
750+
elif i['object_type'] == 'template':
751+
store = init_global_templates(configuration)
752+
template = store.grab_template(
753+
i['template_name'],
754+
i['template_group'],
755+
'html',
756+
)
757+
bound = store.context.extend(template, i['template_args'])
758+
lines.append(bound.render())
749759
elif i['object_type'] == 'error_text':
750760
msg = "%(text)s" % i
751761
if i.get('exc', False):
@@ -2821,7 +2831,7 @@ def format_output(
28212831
logger = configuration.logger
28222832
#logger.debug("format output to %s" % outputformat)
28232833
valid_formats = get_valid_outputformats()
2824-
(val_ret, val_msg) = validate(out_obj)
2834+
(val_ret, val_msg) = validate(out_obj, configuration)
28252835
if not val_ret:
28262836
logger.error("%s formatting failed: %s (%s)" %
28272837
(outputformat, val_msg, val_ret))

mig/wsgi-bin/migwsgi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def stub(configuration, client_id, import_path, backend, user_arguments_dict,
130130
crash_helper(configuration, backend, output_objects)
131131
return (output_objects, returnvalues.ERROR)
132132

133-
(val_ret, val_msg) = validate(output_objects)
133+
(val_ret, val_msg) = validate(output_objects, configuration=configuration)
134134
if not val_ret:
135135
(ret_code, ret_msg) = returnvalues.OUTPUT_VALIDATION_ERROR
136136
bailout_helper(configuration, backend, output_objects,

requirements.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# https://pip.pypa.io/en/stable/reference/requirement-specifiers/
33
future
44

5+
chainmap;python_version < "3.3"
6+
57
# cgi was removed from the standard library in Python 3.13
68
legacy-cgi;python_version >= "3.13"
79

@@ -26,6 +28,10 @@ email-validator;python_version >= "3.7"
2628
email-validator<2.0;python_version >= "3" and python_version < "3.7"
2729
email-validator<1.3;python_version < "3"
2830

31+
jinja2<3;python_version < "3"
32+
jinja2==3.0.*;python_version >= "3" and python_version < "3.7"
33+
jinja2;python_version >= "3.7"
34+
2935
# NOTE: additional optional dependencies depending on site conf are listed
3036
# in recommended.txt and can be installed in the same manner by pointing
3137
# pip there.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<q>{{ other }}</q>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<aside>{{ content }}</aside>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<aside>here!!</aside>

0 commit comments

Comments
 (0)