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