1"""
2Template render systems
3"""
4import codecs
5import logging
6import os
7import sys
8import tempfile
9import traceback
10from pathlib import Path
11
12import jinja2
13import jinja2.ext
14import jinja2.sandbox
15import salt.utils.data
16import salt.utils.dateutils
17import salt.utils.files
18import salt.utils.hashutils
19import salt.utils.http
20import salt.utils.jinja
21import salt.utils.network
22import salt.utils.platform
23import salt.utils.stringutils
24import salt.utils.yamlencoding
25from salt import __path__ as saltpath
26from salt.exceptions import CommandExecutionError, SaltInvocationError, SaltRenderError
27from salt.features import features
28from salt.loader.context import NamedLoaderContext
29from salt.utils.decorators.jinja import JinjaFilter, JinjaGlobal, JinjaTest
30from salt.utils.odict import OrderedDict
31from salt.utils.versions import LooseVersion
32
33if sys.version_info[:2] >= (3, 5):
34    import importlib.machinery  # pylint: disable=no-name-in-module,import-error
35    import importlib.util  # pylint: disable=no-name-in-module,import-error
36
37    USE_IMPORTLIB = True
38else:
39    import imp
40
41    USE_IMPORTLIB = False
42
43
44log = logging.getLogger(__name__)
45
46
47TEMPLATE_DIRNAME = os.path.join(saltpath[0], "templates")
48
49# FIXME: also in salt/template.py
50SLS_ENCODING = "utf-8"  # this one has no BOM.
51SLS_ENCODER = codecs.getencoder(SLS_ENCODING)
52
53
54class AliasedLoader:
55    """
56    Light wrapper around the LazyLoader to redirect 'cmd.run' calls to
57    'cmd.shell', for easy use of shellisms during templating calls
58
59    Dotted aliases ('cmd.run') must resolve to another dotted alias
60    (e.g. 'cmd.shell')
61
62    Non-dotted aliases ('cmd') must resolve to a dictionary of function
63    aliases for that module (e.g. {'run': 'shell'})
64    """
65
66    def __init__(self, wrapped):
67        self.wrapped = wrapped
68
69    def __getitem__(self, name):
70        return self.wrapped[name]
71
72    def __getattr__(self, name):
73        return getattr(self.wrapped, name)
74
75    def __contains__(self, name):
76        return name in self.wrapped
77
78
79class AliasedModule:
80    """
81    Light wrapper around module objects returned by the LazyLoader's getattr
82    for the purposes of `salt.cmd.run()` syntax in templates
83
84    Allows for aliasing specific functions, such as `run` to `shell` for easy
85    use of shellisms during templating calls
86    """
87
88    def __init__(self, wrapped, aliases):
89        self.aliases = aliases
90        self.wrapped = wrapped
91
92    def __getattr__(self, name):
93        return getattr(self.wrapped, name)
94
95
96def _generate_sls_context_legacy(tmplpath, sls):
97    """
98    Legacy version of generate_sls_context, this method should be remove in the
99    Phosphorus release.
100    """
101    salt.utils.versions.warn_until(
102        "Phosphorus",
103        "There have been significant improvement to template variables. "
104        "To enable these improvements set features.enable_slsvars_fixes "
105        "to True in your config file. This feature will become the default "
106        "in the Phoshorus release.",
107    )
108    context = {}
109    slspath = sls.replace(".", "/")
110    if tmplpath is not None:
111        context["tplpath"] = tmplpath
112        if not tmplpath.lower().replace("\\", "/").endswith("/init.sls"):
113            slspath = os.path.dirname(slspath)
114        template = tmplpath.replace("\\", "/")
115        i = template.rfind(slspath.replace(".", "/"))
116        if i != -1:
117            template = template[i:]
118        tpldir = os.path.dirname(template).replace("\\", "/")
119        tpldata = {
120            "tplfile": template,
121            "tpldir": "." if tpldir == "" else tpldir,
122            "tpldot": tpldir.replace("/", "."),
123        }
124        context.update(tpldata)
125    context["slsdotpath"] = slspath.replace("/", ".")
126    context["slscolonpath"] = slspath.replace("/", ":")
127    context["sls_path"] = slspath.replace("/", "_")
128    context["slspath"] = slspath
129    return context
130
131
132def _generate_sls_context(tmplpath, sls):
133    """
134    Generate SLS/Template Context Items
135
136    Return values:
137
138    tplpath - full path to template on filesystem including filename
139    tplfile - relative path to template -- relative to file roots
140    tpldir - directory of the template relative to file roots. If none, "."
141    tpldot - tpldir using dots instead of slashes, if none, ""
142    slspath - directory containing current sls - (same as tpldir), if none, ""
143    sls_path - slspath with underscores separating parts, if none, ""
144    slsdotpath - slspath with dots separating parts, if none, ""
145    slscolonpath- slspath with colons separating parts, if none, ""
146
147    """
148
149    sls_context = {}
150
151    # Normalize SLS as path.
152    slspath = sls.replace(".", "/")
153
154    if tmplpath:
155        # Normalize template path
156        template = str(Path(tmplpath).as_posix())
157
158        # Determine proper template name without root
159        if not sls:
160            template = template.rsplit("/", 1)[-1]
161        elif template.endswith("{}.sls".format(slspath)):
162            template = template[-(4 + len(slspath)) :]
163        elif template.endswith("{}/init.sls".format(slspath)):
164            template = template[-(9 + len(slspath)) :]
165        else:
166            # Something went wrong
167            log.warning("Failed to determine proper template path")
168
169        slspath = template.rsplit("/", 1)[0] if "/" in template else ""
170
171        sls_context.update(
172            dict(
173                tplpath=tmplpath,
174                tplfile=template,
175                tpldir=slspath if slspath else ".",
176                tpldot=slspath.replace("/", "."),
177            )
178        )
179
180    # Should this be normalized?
181    sls_context.update(
182        dict(
183            slspath=slspath,
184            slsdotpath=slspath.replace("/", "."),
185            slscolonpath=slspath.replace("/", ":"),
186            sls_path=slspath.replace("/", "_"),
187        )
188    )
189
190    return sls_context
191
192
193def generate_sls_context(tmplpath, sls):
194    """
195    Generate SLS/Template Context Items
196
197    Return values:
198
199    tplpath - full path to template on filesystem including filename
200    tplfile - relative path to template -- relative to file roots
201    tpldir - directory of the template relative to file roots. If none, "."
202    tpldot - tpldir using dots instead of slashes, if none, ""
203    slspath - directory containing current sls - (same as tpldir), if none, ""
204    sls_path - slspath with underscores separating parts, if none, ""
205    slsdotpath - slspath with dots separating parts, if none, ""
206    slscolonpath- slspath with colons separating parts, if none, ""
207
208    """
209    if not features.get("enable_slsvars_fixes", False):
210        return _generate_sls_context_legacy(tmplpath, sls)
211    return _generate_sls_context(tmplpath, sls)
212
213
214def wrap_tmpl_func(render_str):
215    def render_tmpl(
216        tmplsrc, from_str=False, to_str=False, context=None, tmplpath=None, **kws
217    ):
218
219        if context is None:
220            context = {}
221
222        # Alias cmd.run to cmd.shell to make python_shell=True the default for
223        # templated calls
224        if "salt" in kws:
225            kws["salt"] = AliasedLoader(kws["salt"])
226
227        # We want explicit context to overwrite the **kws
228        kws.update(context)
229        context = kws
230        assert "opts" in context
231        assert "saltenv" in context
232
233        if "sls" in context:
234            sls_context = generate_sls_context(tmplpath, context["sls"])
235            context.update(sls_context)
236
237        if isinstance(tmplsrc, str):
238            if from_str:
239                tmplstr = tmplsrc
240            else:
241                try:
242                    if tmplpath is not None:
243                        tmplsrc = os.path.join(tmplpath, tmplsrc)
244                    with codecs.open(tmplsrc, "r", SLS_ENCODING) as _tmplsrc:
245                        tmplstr = _tmplsrc.read()
246                except (UnicodeDecodeError, ValueError, OSError) as exc:
247                    if salt.utils.files.is_binary(tmplsrc):
248                        # Template is a bin file, return the raw file
249                        return dict(result=True, data=tmplsrc)
250                    log.error(
251                        "Exception occurred while reading file %s: %s",
252                        tmplsrc,
253                        exc,
254                        exc_info_on_loglevel=logging.DEBUG,
255                    )
256                    raise
257        else:  # assume tmplsrc is file-like.
258            tmplstr = tmplsrc.read()
259            tmplsrc.close()
260        try:
261            output = render_str(tmplstr, context, tmplpath)
262            if salt.utils.platform.is_windows():
263                newline = False
264                if salt.utils.stringutils.to_unicode(
265                    output, encoding=SLS_ENCODING
266                ).endswith(("\n", os.linesep)):
267                    newline = True
268                # Write out with Windows newlines
269                output = os.linesep.join(output.splitlines())
270                if newline:
271                    output += os.linesep
272
273        except SaltRenderError as exc:
274            log.exception("Rendering exception occurred")
275            # return dict(result=False, data=str(exc))
276            raise
277        except Exception:  # pylint: disable=broad-except
278            return dict(result=False, data=traceback.format_exc())
279        else:
280            if to_str:  # then render as string
281                return dict(result=True, data=output)
282            with tempfile.NamedTemporaryFile(
283                "wb", delete=False, prefix=salt.utils.files.TEMPFILE_PREFIX
284            ) as outf:
285                outf.write(
286                    salt.utils.stringutils.to_bytes(output, encoding=SLS_ENCODING)
287                )
288                # Note: If nothing is replaced or added by the rendering
289                #       function, then the contents of the output file will
290                #       be exactly the same as the input.
291            return dict(result=True, data=outf.name)
292
293    render_tmpl.render_str = render_str
294    return render_tmpl
295
296
297def _get_jinja_error_slug(tb_data):
298    """
299    Return the line number where the template error was found
300    """
301    try:
302        return [
303            x
304            for x in tb_data
305            if x[2] in ("top-level template code", "template", "<module>")
306        ][-1]
307    except IndexError:
308        pass
309
310
311def _get_jinja_error_message(tb_data):
312    """
313    Return an understandable message from jinja error output
314    """
315    try:
316        line = _get_jinja_error_slug(tb_data)
317        return "{0}({1}):\n{3}".format(*line)
318    except IndexError:
319        pass
320    return None
321
322
323def _get_jinja_error_line(tb_data):
324    """
325    Return the line number where the template error was found
326    """
327    try:
328        return _get_jinja_error_slug(tb_data)[1]
329    except IndexError:
330        pass
331    return None
332
333
334def _get_jinja_error(trace, context=None):
335    """
336    Return the error line and error message output from
337    a stacktrace.
338    If we are in a macro, also output inside the message the
339    exact location of the error in the macro
340    """
341    if not context:
342        context = {}
343    out = ""
344    error = _get_jinja_error_slug(trace)
345    line = _get_jinja_error_line(trace)
346    msg = _get_jinja_error_message(trace)
347    # if we failed on a nested macro, output a little more info
348    # to help debugging
349    # if sls is not found in context, add output only if we can
350    # resolve the filename
351    add_log = False
352    template_path = None
353    if "sls" not in context:
354        if (error[0] != "<unknown>") and os.path.exists(error[0]):
355            template_path = error[0]
356            add_log = True
357    else:
358        # the offender error is not from the called sls
359        filen = context["sls"].replace(".", "/")
360        if not error[0].endswith(filen) and os.path.exists(error[0]):
361            add_log = True
362            template_path = error[0]
363    # if we add a log, format explicitly the exception here
364    # by telling to output the macro context after the macro
365    # error log place at the beginning
366    if add_log:
367        if template_path:
368            out = "\n{}\n".format(msg.splitlines()[0])
369            with salt.utils.files.fopen(template_path) as fp_:
370                template_contents = salt.utils.stringutils.to_unicode(fp_.read())
371            out += salt.utils.stringutils.get_context(
372                template_contents, line, marker="    <======================"
373            )
374        else:
375            out = "\n{}\n".format(msg)
376        line = 0
377    return line, out
378
379
380def render_jinja_tmpl(tmplstr, context, tmplpath=None):
381    opts = context["opts"]
382    saltenv = context["saltenv"]
383    loader = None
384    newline = False
385    file_client = context.get("fileclient", None)
386
387    if tmplstr and not isinstance(tmplstr, str):
388        # https://jinja.palletsprojects.com/en/2.11.x/api/#unicode
389        tmplstr = tmplstr.decode(SLS_ENCODING)
390
391    if tmplstr.endswith(os.linesep):
392        newline = os.linesep
393    elif tmplstr.endswith("\n"):
394        newline = "\n"
395
396    if not saltenv:
397        if tmplpath:
398            loader = jinja2.FileSystemLoader(os.path.dirname(tmplpath))
399    else:
400        loader = salt.utils.jinja.SaltCacheLoader(
401            opts,
402            saltenv,
403            pillar_rend=context.get("_pillar_rend", False),
404            _file_client=file_client,
405        )
406
407    env_args = {"extensions": [], "loader": loader}
408
409    if hasattr(jinja2.ext, "with_"):
410        env_args["extensions"].append("jinja2.ext.with_")
411    if hasattr(jinja2.ext, "do"):
412        env_args["extensions"].append("jinja2.ext.do")
413    if hasattr(jinja2.ext, "loopcontrols"):
414        env_args["extensions"].append("jinja2.ext.loopcontrols")
415    env_args["extensions"].append(salt.utils.jinja.SerializerExtension)
416
417    opt_jinja_env = opts.get("jinja_env", {})
418    opt_jinja_sls_env = opts.get("jinja_sls_env", {})
419
420    opt_jinja_env = opt_jinja_env if isinstance(opt_jinja_env, dict) else {}
421    opt_jinja_sls_env = opt_jinja_sls_env if isinstance(opt_jinja_sls_env, dict) else {}
422
423    # Pass through trim_blocks and lstrip_blocks Jinja parameters
424    # trim_blocks removes newlines around Jinja blocks
425    # lstrip_blocks strips tabs and spaces from the beginning of
426    # line to the start of a block.
427    if opts.get("jinja_trim_blocks", False):
428        log.debug("Jinja2 trim_blocks is enabled")
429        log.warning(
430            "jinja_trim_blocks is deprecated and will be removed in a future release,"
431            " please use jinja_env and/or jinja_sls_env instead"
432        )
433        opt_jinja_env["trim_blocks"] = True
434        opt_jinja_sls_env["trim_blocks"] = True
435    if opts.get("jinja_lstrip_blocks", False):
436        log.debug("Jinja2 lstrip_blocks is enabled")
437        log.warning(
438            "jinja_lstrip_blocks is deprecated and will be removed in a future release,"
439            " please use jinja_env and/or jinja_sls_env instead"
440        )
441        opt_jinja_env["lstrip_blocks"] = True
442        opt_jinja_sls_env["lstrip_blocks"] = True
443
444    def opt_jinja_env_helper(opts, optname):
445        for k, v in opts.items():
446            k = k.lower()
447            if hasattr(jinja2.defaults, k.upper()):
448                log.debug("Jinja2 environment %s was set to %s by %s", k, v, optname)
449                env_args[k] = v
450            else:
451                log.warning("Jinja2 environment %s is not recognized", k)
452
453    if "sls" in context and context["sls"] != "":
454        opt_jinja_env_helper(opt_jinja_sls_env, "jinja_sls_env")
455    else:
456        opt_jinja_env_helper(opt_jinja_env, "jinja_env")
457
458    if opts.get("allow_undefined", False):
459        jinja_env = jinja2.sandbox.SandboxedEnvironment(**env_args)
460    else:
461        jinja_env = jinja2.sandbox.SandboxedEnvironment(
462            undefined=jinja2.StrictUndefined, **env_args
463        )
464
465    indent_filter = jinja_env.filters.get("indent")
466    jinja_env.tests.update(JinjaTest.salt_jinja_tests)
467    jinja_env.filters.update(JinjaFilter.salt_jinja_filters)
468    if salt.utils.jinja.JINJA_VERSION >= LooseVersion("2.11"):
469        # Use the existing indent filter on Jinja versions where it's not broken
470        jinja_env.filters["indent"] = indent_filter
471    jinja_env.globals.update(JinjaGlobal.salt_jinja_globals)
472
473    # globals
474    jinja_env.globals["odict"] = OrderedDict
475    jinja_env.globals["show_full_context"] = salt.utils.jinja.show_full_context
476
477    jinja_env.tests["list"] = salt.utils.data.is_list
478
479    decoded_context = {}
480    for key, value in context.items():
481        if not isinstance(value, str):
482            if isinstance(value, NamedLoaderContext):
483                decoded_context[key] = value.value()
484            else:
485                decoded_context[key] = value
486            continue
487
488        try:
489            decoded_context[key] = salt.utils.stringutils.to_unicode(
490                value, encoding=SLS_ENCODING
491            )
492        except UnicodeDecodeError as ex:
493            log.debug(
494                "Failed to decode using default encoding (%s), trying system encoding",
495                SLS_ENCODING,
496            )
497            decoded_context[key] = salt.utils.data.decode(value)
498
499    jinja_env.globals.update(decoded_context)
500    try:
501        template = jinja_env.from_string(tmplstr)
502        output = template.render(**decoded_context)
503    except jinja2.exceptions.UndefinedError as exc:
504        trace = traceback.extract_tb(sys.exc_info()[2])
505        out = _get_jinja_error(trace, context=decoded_context)[1]
506        tmplstr = ""
507        # Don't include the line number, since it is misreported
508        # https://github.com/mitsuhiko/jinja2/issues/276
509        raise SaltRenderError("Jinja variable {}{}".format(exc, out), buf=tmplstr)
510    except (
511        jinja2.exceptions.TemplateRuntimeError,
512        jinja2.exceptions.TemplateSyntaxError,
513        jinja2.exceptions.SecurityError,
514    ) as exc:
515        trace = traceback.extract_tb(sys.exc_info()[2])
516        line, out = _get_jinja_error(trace, context=decoded_context)
517        if not line:
518            tmplstr = ""
519        raise SaltRenderError(
520            "Jinja syntax error: {}{}".format(exc, out), line, tmplstr
521        )
522    except (SaltInvocationError, CommandExecutionError) as exc:
523        trace = traceback.extract_tb(sys.exc_info()[2])
524        line, out = _get_jinja_error(trace, context=decoded_context)
525        if not line:
526            tmplstr = ""
527        raise SaltRenderError(
528            "Problem running salt function in Jinja template: {}{}".format(exc, out),
529            line,
530            tmplstr,
531        )
532    except Exception as exc:  # pylint: disable=broad-except
533        tracestr = traceback.format_exc()
534        trace = traceback.extract_tb(sys.exc_info()[2])
535        line, out = _get_jinja_error(trace, context=decoded_context)
536        if not line:
537            tmplstr = ""
538        else:
539            tmplstr += "\n{}".format(tracestr)
540        log.debug("Jinja Error")
541        log.debug("Exception:", exc_info=True)
542        log.debug("Out: %s", out)
543        log.debug("Line: %s", line)
544        log.debug("TmplStr: %s", tmplstr)
545        log.debug("TraceStr: %s", tracestr)
546
547        raise SaltRenderError(
548            "Jinja error: {}{}".format(exc, out), line, tmplstr, trace=tracestr
549        )
550
551    # Workaround a bug in Jinja that removes the final newline
552    # (https://github.com/mitsuhiko/jinja2/issues/75)
553    if newline:
554        output += newline
555
556    return output
557
558
559# pylint: disable=3rd-party-module-not-gated
560def render_mako_tmpl(tmplstr, context, tmplpath=None):
561    import mako.exceptions  # pylint: disable=no-name-in-module
562    from mako.template import Template  # pylint: disable=no-name-in-module
563    from salt.utils.mako import SaltMakoTemplateLookup
564
565    saltenv = context["saltenv"]
566    lookup = None
567    if not saltenv:
568        if tmplpath:
569            # i.e., the template is from a file outside the state tree
570            from mako.lookup import TemplateLookup  # pylint: disable=no-name-in-module
571
572            lookup = TemplateLookup(directories=[os.path.dirname(tmplpath)])
573    else:
574        lookup = SaltMakoTemplateLookup(
575            context["opts"], saltenv, pillar_rend=context.get("_pillar_rend", False)
576        )
577    try:
578        return Template(
579            tmplstr,
580            strict_undefined=True,
581            uri=context["sls"].replace(".", "/") if "sls" in context else None,
582            lookup=lookup,
583        ).render(**context)
584    except Exception:  # pylint: disable=broad-except
585        raise SaltRenderError(mako.exceptions.text_error_template().render())
586
587
588def render_wempy_tmpl(tmplstr, context, tmplpath=None):
589    from wemplate.wemplate import TemplateParser as Template
590
591    return Template(tmplstr).render(**context)
592
593
594def render_genshi_tmpl(tmplstr, context, tmplpath=None):
595    """
596    Render a Genshi template. A method should be passed in as part of the
597    context. If no method is passed in, xml is assumed. Valid methods are:
598
599    .. code-block:
600
601        - xml
602        - xhtml
603        - html
604        - text
605        - newtext
606        - oldtext
607
608    Note that the ``text`` method will call ``NewTextTemplate``. If ``oldtext``
609    is desired, it must be called explicitly
610    """
611    method = context.get("method", "xml")
612    if method == "text" or method == "newtext":
613        from genshi.template import NewTextTemplate  # pylint: disable=no-name-in-module
614
615        tmpl = NewTextTemplate(tmplstr)
616    elif method == "oldtext":
617        from genshi.template import OldTextTemplate  # pylint: disable=no-name-in-module
618
619        tmpl = OldTextTemplate(tmplstr)
620    else:
621        from genshi.template import MarkupTemplate  # pylint: disable=no-name-in-module
622
623        tmpl = MarkupTemplate(tmplstr)
624
625    return tmpl.generate(**context).render(method)
626
627
628def render_cheetah_tmpl(tmplstr, context, tmplpath=None):
629    """
630    Render a Cheetah template.
631    """
632    from Cheetah.Template import Template
633
634    # Compile the template and render it into the class
635    tclass = Template.compile(tmplstr)
636    data = tclass(namespaces=[context])
637
638    # Figure out which method to call based on the type of tmplstr
639    if isinstance(tmplstr, str):
640        # This should call .__unicode__()
641        res = str(data)
642    elif isinstance(tmplstr, bytes):
643        # This should call .__str()
644        res = str(data)
645    else:
646        raise SaltRenderError(
647            "Unknown type {!s} for Cheetah template while trying to render.".format(
648                type(tmplstr)
649            )
650        )
651
652    # Now we can decode it to the correct encoding
653    return salt.utils.data.decode(res)
654
655
656# pylint: enable=3rd-party-module-not-gated
657
658
659def py(sfn, string=False, **kwargs):  # pylint: disable=C0103
660    """
661    Render a template from a python source file
662
663    Returns::
664
665        {'result': bool,
666         'data': <Error data or rendered file path>}
667    """
668    if not os.path.isfile(sfn):
669        return {}
670
671    base_fname = os.path.basename(sfn)
672    name = base_fname.split(".")[0]
673
674    if USE_IMPORTLIB:
675        # pylint: disable=no-member
676        loader = importlib.machinery.SourceFileLoader(name, sfn)
677        spec = importlib.util.spec_from_file_location(name, sfn, loader=loader)
678        if spec is None:
679            raise ImportError()
680        mod = importlib.util.module_from_spec(spec)
681        spec.loader.exec_module(mod)
682        # pylint: enable=no-member
683        sys.modules[name] = mod
684    else:
685        mod = imp.load_source(name, sfn)
686
687    # File templates need these set as __var__
688    if "__env__" not in kwargs and "saltenv" in kwargs:
689        setattr(mod, "__env__", kwargs["saltenv"])
690        builtins = ["salt", "grains", "pillar", "opts"]
691        for builtin in builtins:
692            arg = "__{}__".format(builtin)
693            setattr(mod, arg, kwargs[builtin])
694
695    for kwarg in kwargs:
696        setattr(mod, kwarg, kwargs[kwarg])
697
698    try:
699        data = mod.run()
700        if string:
701            return {"result": True, "data": data}
702        tgt = salt.utils.files.mkstemp()
703        with salt.utils.files.fopen(tgt, "w+") as target:
704            target.write(salt.utils.stringutils.to_str(data))
705        return {"result": True, "data": tgt}
706    except Exception:  # pylint: disable=broad-except
707        trb = traceback.format_exc()
708        return {"result": False, "data": trb}
709
710
711JINJA = wrap_tmpl_func(render_jinja_tmpl)
712MAKO = wrap_tmpl_func(render_mako_tmpl)
713WEMPY = wrap_tmpl_func(render_wempy_tmpl)
714GENSHI = wrap_tmpl_func(render_genshi_tmpl)
715CHEETAH = wrap_tmpl_func(render_cheetah_tmpl)
716
717TEMPLATE_REGISTRY = {
718    "jinja": JINJA,
719    "mako": MAKO,
720    "py": py,
721    "wempy": WEMPY,
722    "genshi": GENSHI,
723    "cheetah": CHEETAH,
724}
725