From 15c7bbf5fab8d9d7478b906323fde2c6c147a6d7 Mon Sep 17 00:00:00 2001 From: Dave Andreoli Date: Thu, 28 Dec 2017 16:10:04 +0100 Subject: [PATCH] Pyolian template-based generator. This is a really powerfull tool that can be used to generate anything eolian releted just providing a template file. You can then render the template with the wanted scope (class, namespace, enum, etc) For example give a try at this (from the src/srcipts/pyolian folder): ./generator.py test_gen_class.template --cls Efl.Loop.Timer or ./generator.py -h for the full help Next step: maybe generate the new efl API doc using this tool? @andy I think this will make your life much easier :) --- src/scripts/pyolian/eolian.py | 6 +- src/scripts/pyolian/generator.py | 206 +++ src/scripts/pyolian/pyratemp.py | 1258 +++++++++++++++++ src/scripts/pyolian/test_gen_class.template | 45 + .../pyolian/test_gen_namespace.template | 43 + 5 files changed, 1556 insertions(+), 2 deletions(-) create mode 100755 src/scripts/pyolian/generator.py create mode 100644 src/scripts/pyolian/pyratemp.py create mode 100644 src/scripts/pyolian/test_gen_class.template create mode 100644 src/scripts/pyolian/test_gen_namespace.template diff --git a/src/scripts/pyolian/eolian.py b/src/scripts/pyolian/eolian.py index f74f350324..5baa2d3d7d 100644 --- a/src/scripts/pyolian/eolian.py +++ b/src/scripts/pyolian/eolian.py @@ -512,6 +512,7 @@ class Class(EolianBaseObject): def functions_get(self, ftype): return Iterator(Function, lib.eolian_class_functions_get(self._obj, ftype)) + @property def methods(self): return self.functions_get(Eolian_Function_Type.METHOD) @@ -886,9 +887,10 @@ class Type(EolianBaseObject): # OK (4 eolian issue) return Eolian_Type_Builtin_Type(lib.eolian_type_builtin_type_get(self._obj)) # TODO FIXME STRANGE API (need Eolian_Unit*) - # @property - # def c_type(self): + @property + def c_type(self): # return _str_to_py(lib.eolian_type_c_type_get(self._obj)) + return 'FIXME' # TODO FIXME STRANGE API (need Eolian_Unit*) # @property diff --git a/src/scripts/pyolian/generator.py b/src/scripts/pyolian/generator.py new file mode 100755 index 0000000000..b1e9bca786 --- /dev/null +++ b/src/scripts/pyolian/generator.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +""" + +Pyolian template based generator. + +This is a really powerfull template-based, output-agnostic eolian generator. +You just need a template file and then you can render it with the +wanted eolian scope (class, namespace, enum, struct, etc...). + +For example (from this source folder): + +./generator.py test_gen_class.template --cls Efl.Loop.Timer +./generator.py test_gen_namespace.template --ns Efl.Ui + +...of course you can pass any other class or namespace to the example above. + +You can also import this module and use the provided Template class if you +are more confortable from within python. + +The generator is based on the great pyratemp engine (THANKS!), you can find +the full template syntax at: www.simple-is-better.org/template/pyratemp.html + +Just keep in mind the syntax is a bit different in this implementation: + + sub_start = "${" Was "$!" in original pyratemp (and in docs) + sub_end = "}$" Was "!$" in original pyratemp (and in docs) + _block_start = "" + comment_start = "#!" + comment_end = "!#" + +""" +import os +import datetime + +import eolian +import pyratemp + + +# logging utils +be_verbose = True +def ERR(*args): print(*(('PYOLIANGEN ERROR:', ) + args)) +def WRN(*args): print(*(('PYOLIANGEN WARNING:', ) + args)) +def INF(*args): print(*(('PYOLIANGEN ', ) + args)) + + +# Use .eo files from the source tree (not the installed ones) +script_path = os.path.dirname(os.path.realpath(__file__)) +root_path = os.path.abspath(os.path.join(script_path, '..', '..', '..')) +SCAN_FOLDER = os.path.join(root_path, 'src', 'lib') + + +# load the whole eolian db +eolian_db = eolian.Eolian() +if not isinstance(eolian_db, eolian.Eolian): + raise(RuntimeError('Eolian, failed to create Eolian state')) + +if not eolian_db.directory_scan(SCAN_FOLDER): + raise(RuntimeError('Eolian, failed to scan source directory')) + +if not eolian_db.all_eot_files_parse(): + raise(RuntimeError('Eolian, failed to parse all EOT files')) + +if not eolian_db.all_eo_files_parse(): + raise(RuntimeError('Eolian, failed to parse all EO files')) + +# cleanup the database on exit +import atexit +def cleanup_db(): + global eolian_db + del eolian_db +atexit.register(cleanup_db) + + +class Template(pyratemp.Template): + """ Pyolian template based generator. + + You can directly use this class to generate custom outputs based + on the eolian database and your provided templates. + + Usage is as simple as: + t = Template() + t.render(, cls=..., ns=..., ...) + + Args: + filename: Template file to load. (REQUIRED) + data: User provided context for the template. + """ + def __init__(self, filename, encoding='utf-8', data=None, escape=None, + loader_class=pyratemp.LoaderFile, + parser_class=pyratemp.Parser, + renderer_class=pyratemp.Renderer, + eval_class=pyratemp.EvalPseudoSandbox): + + # Build the global context for the template + global_ctx = {} + # user provided context (low pri) + if data: + global_ctx.update(data) + # standard names (not overwritables) + global_ctx.update({ + # Template info + 'date': datetime.datetime.now(), + 'template_file': os.path.basename(filename), + # Eolian info + # 'eolian_version': eolian.__version__, + # 'eolian_version_info': eolian.__version_info__, + # Eolian Enums + 'Eolian_Function_Type': eolian.Eolian_Function_Type, + 'Eolian_Parameter_Dir': eolian.Eolian_Parameter_Dir, + 'Eolian_Class_Type': eolian.Eolian_Class_Type, + 'Eolian_Object_Scope': eolian.Eolian_Object_Scope, + 'Eolian_Typedecl_Type': eolian.Eolian_Typedecl_Type, + 'Eolian_Type_Type': eolian.Eolian_Type_Type, + 'Eolian_Type_Builtin_Type': eolian.Eolian_Type_Builtin_Type, + 'Eolian_C_Type_Type': eolian.Eolian_C_Type_Type, + 'Eolian_Expression_Type': eolian.Eolian_Expression_Type, + 'Eolian_Expression_Mask': eolian.Eolian_Expression_Mask, + 'Eolian_Variable_Type': eolian.Eolian_Variable_Type, + 'Eolian_Binary_Operator': eolian.Eolian_Binary_Operator, + 'Eolian_Unary_Operator': eolian.Eolian_Unary_Operator, + 'Eolian_Declaration_Type': eolian.Eolian_Declaration_Type, + 'Eolian_Doc_Token_Type': eolian.Eolian_Doc_Token_Type, + 'Eolian_Doc_Ref_Type': eolian.Eolian_Doc_Ref_Type, + }) + + # Call the parent __init__ func + self.template_filename = filename + pyratemp.Template.__init__(self, filename=filename, encoding=encoding, + data=global_ctx, escape=escape, + loader_class=loader_class, + parser_class=parser_class, + renderer_class=renderer_class, + eval_class=eval_class) + + def render(self, filename=None, cls=None, ns=None, + struct=None, enum=None, alias=None, **kargs): + # Build the context for the template + ctx = {} + if kargs: + ctx.update(kargs) + if cls: + ctx['cls'] = eolian_db.class_get_by_name(cls) + if struct: + ctx['struct'] = eolian_db.typedecl_struct_get_by_name(struct) + if enum: + ctx['enum'] = eolian_db.typedecl_enum_get_by_name(enum) + if alias: + ctx['alias'] = eolian_db.typedecl_alias_get_by_name(alias) + if ns: + ctx['namespace'] = ns + ctx['namespaces'] = ns.split('.') + ctx['classes'] = [ c for c in eolian_db.all_classes + if c.full_name.startswith(ns + '.') ] + ctx['aliases'] = [ a for a in eolian_db.typedecl_all_aliases + if a.full_name.startswith(ns + '.') ] + ctx['structs'] = [ s for s in eolian_db.typedecl_all_structs + if s.full_name.startswith(ns + '.') ] + ctx['enums'] = [ e for e in eolian_db.typedecl_all_enums + if e.full_name.startswith(ns + '.') ] + + if filename is not None: + INF('generating "%s" from template "%s"' % ( + filename, self.template_filename)) + + # render with the augmented context + output = self(**ctx) + + if filename is not None: + # create directory tree if needed + folder = os.path.dirname(filename) + if folder and not os.path.isdir(folder): + os.makedirs(folder) + # write to file + with open(filename, "w") as f: + f.write(output) + else: + # or print to stdout + print(output) + + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser(description='Pyolian generator.') + parser.add_argument('template', + help='The template file to use. (REQUIRED)') + parser.add_argument('--output', '-o', metavar='FILE', default=None, + help='Where to write the rendered output. ' + 'If not given will print to stdout.') + parser.add_argument('--cls', metavar='CLASS_NAME', default=None, + help='The full name of the class to render, ex: Efl.Loop.Timer') + parser.add_argument('--ns', metavar='NAMESPACE', default=None, + help='The namespace to render, ex: Efl.Loop') + parser.add_argument('--struct', metavar='STRUCT_NAME', default=None, + help='The name of the struct to render, ex: Efl.Loop.Arguments') + parser.add_argument('--enum', metavar='ENUM_NAME', default=None, + help='The name of the enum to render, ex: Efl.Loop.Handler.Flags') + parser.add_argument('--alias', metavar='ALIAS_NAME', default=None, + help='The name of the alias to render, ex: Efl.Font.Size') + args = parser.parse_args() + + t = Template(args.template) + t.render(args.output, cls=args.cls, ns=args.ns, + struct=args.struct, enum=args.enum, alias=args.alias) diff --git a/src/scripts/pyolian/pyratemp.py b/src/scripts/pyolian/pyratemp.py new file mode 100644 index 0000000000..c28e260b4b --- /dev/null +++ b/src/scripts/pyolian/pyratemp.py @@ -0,0 +1,1258 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Small, simple and powerful template-engine for Python. + +A template-engine for Python, which is very simple, easy to use, small, +fast, powerful, modular, extensible, well documented and pythonic. + +See documentation for a list of features, template-syntax etc. + +:Version: 0.3.2 +:Requires: Python >=2.6 / 3.x + +:Usage: + see class ``Template`` and examples below. + +:Example: + + Note that the examples are in Python 2; they also work in + Python 3 if you replace u"..." by "...", unicode() by str() + and partly "..." by b"...". + + quickstart:: + >>> t = Template("hello @!name!@") + >>> print(t(name="marvin")) + hello marvin + + quickstart with a template-file:: + # >>> t = Template(filename="mytemplate.tmpl") + # >>> print(t(name="marvin")) + # hello marvin + + generic usage:: + >>> t = Template(u"output is in Unicode \\xe4\\xf6\\xfc\\u20ac") + >>> t #doctest: +ELLIPSIS + <...Template instance at 0x...> + >>> t() + u'output is in Unicode \\xe4\\xf6\\xfc\\u20ac' + >>> unicode(t) + u'output is in Unicode \\xe4\\xf6\\xfc\\u20ac' + + with data:: + >>> t = Template("hello @!name!@", data={"name":"world"}) + >>> t() + u'hello world' + >>> t(name="worlds") + u'hello worlds' + + # >>> t(note="data must be Unicode or ASCII", name=u"\\xe4") + # u'hello \\xe4' + + escaping:: + >>> t = Template("hello escaped: @!name!@, unescaped: $!name!$") + >>> t(name='''<>&'"''') + u'hello escaped: <>&'", unescaped: <>&\\'"' + + result-encoding:: + # encode the unicode-object to your encoding with encode() + >>> t = Template(u"hello \\xe4\\xf6\\xfc\\u20ac") + >>> result = t() + >>> result + u'hello \\xe4\\xf6\\xfc\\u20ac' + >>> result.encode("utf-8") + 'hello \\xc3\\xa4\\xc3\\xb6\\xc3\\xbc\\xe2\\x82\\xac' + >>> result.encode("ascii") + Traceback (most recent call last): + ... + UnicodeEncodeError: 'ascii' codec can't encode characters in position 6-9: ordinal not in range(128) + >>> result.encode("ascii", 'xmlcharrefreplace') + 'hello äöü€' + + Python-expressions:: + >>> Template('formatted: @! "%8.5f" % value !@')(value=3.141592653) + u'formatted: 3.14159' + >>> Template("hello --@!name.upper().center(20)!@--")(name="world") + u'hello -- WORLD --' + >>> Template("calculate @!var*5+7!@")(var=7) + u'calculate 42' + + blocks (if/for/macros/...):: + >>> t = Template("barbazunknown(@!foo!@)") + >>> t(foo=2) + u'baz' + >>> t(foo=5) + u'unknown(5)' + + >>> t = Template("@!i!@ (empty)") + >>> t(mylist=[]) + u'(empty)' + >>> t(mylist=[1,2,3]) + u'1 2 3 ' + + >>> t = Template(" - @!i!@: @!elem!@") + >>> t(mylist=["a","b","c"]) + u' - 0: a - 1: b - 2: c' + + >>> t = Template('hello @!name!@ @!greetings(name=user)!@') + >>> t(user="monty") + u' hello monty' + + exists:: + >>> t = Template('YESNO') + >>> t() + u'NO' + >>> t(foo=1) + u'YES' + >>> t(foo=None) # note this difference to 'default()' + u'YES' + + default-values:: + # non-existing variables raise an error + >>> Template('hi @!optional!@')() + Traceback (most recent call last): + ... + TemplateRenderError: Cannot eval expression 'optional'. (NameError: name 'optional' is not defined) + + >>> t = Template('hi @!default("optional","anyone")!@') + >>> t() + u'hi anyone' + >>> t(optional=None) + u'hi anyone' + >>> t(optional="there") + u'hi there' + + # the 1st parameter can be any eval-expression + >>> t = Template('@!default("5*var1+var2","missing variable")!@') + >>> t(var1=10) + u'missing variable' + >>> t(var1=10, var2=2) + u'52' + + # also in blocks + >>> t = Template('yesno') + >>> t() + u'no' + >>> t(opt1=23, opt2=42) + u'yes' + + >>> t = Template('@!i!@') + >>> t() + u'' + >>> t(optional_list=[1,2,3]) + u'123' + + + # but make sure to put the expression in quotation marks, otherwise: + >>> Template('@!default(optional,"fallback")!@')() + Traceback (most recent call last): + ... + TemplateRenderError: Cannot eval expression 'default(optional,"fallback")'. (NameError: name 'optional' is not defined) + + setvar:: + >>> t = Template('$!setvar("i", "i+1")!$@!i!@') + >>> t(i=6) + u'7' + + >>> t = Template('''$!setvar("s", '"\\\\\\\\n".join(s)')!$@!s!@''') + >>> t(isinstance=isinstance, s="123") + u'123' + >>> t(isinstance=isinstance, s=["123", "456"]) + u'123\\n456' + +:Author: Roland Koebler (rk at simple-is-better dot org) +:Copyright: Roland Koebler +:License: MIT/X11-like, see __license__ + +:RCS: $Id: pyratemp.py,v 1.22 2013/09/17 07:44:13 rk Exp $ +""" +from __future__ import unicode_literals + +__version__ = "0.3.2" +__author__ = "Roland Koebler " +__license__ = """Copyright (c) Roland Koebler, 2007-2013 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE.""" + +#========================================= + +import os, re, sys, types +if sys.version_info[0] >= 3: + import builtins + unicode = str + long = int +else: + import __builtin__ as builtins + from codecs import open + +#========================================= +# some useful functions + +#---------------------- +# string-position: i <-> row,col + +def srow(string, i): + """Get line numer of ``string[i]`` in `string`. + + :Returns: row, starting at 1 + :Note: This works for text-strings with ``\\n`` or ``\\r\\n``. + """ + return string.count('\n', 0, max(0, i)) + 1 + +def scol(string, i): + """Get column number of ``string[i]`` in `string`. + + :Returns: column, starting at 1 (but may be <1 if i<0) + :Note: This works for text-strings with ``\\n`` or ``\\r\\n``. + """ + return i - string.rfind('\n', 0, max(0, i)) + +def sindex(string, row, col): + """Get index of the character at `row`/`col` in `string`. + + :Parameters: + - `row`: row number, starting at 1. + - `col`: column number, starting at 1. + :Returns: ``i``, starting at 0 (but may be <1 if row/col<0) + :Note: This works for text-strings with '\\n' or '\\r\\n'. + """ + n = 0 + for _ in range(row-1): + n = string.find('\n', n) + 1 + return n+col-1 + +#---------------------- + +def dictkeyclean(d): + """Convert all keys of the dict `d` to strings. + """ + new_d = {} + for k, v in d.items(): + new_d[str(k)] = v + return new_d + +#---------------------- + +def dummy(*_, **__): + """Dummy function, doing nothing. + """ + pass + +def dummy_raise(exception, value): + """Create an exception-raising dummy function. + + :Returns: dummy function, raising ``exception(value)`` + """ + def mydummy(*_, **__): + raise exception(value) + return mydummy + +#========================================= +# escaping + +(NONE, HTML, LATEX, MAIL_HEADER) = range(0, 4) +ESCAPE_SUPPORTED = {"NONE":None, "HTML":HTML, "LATEX":LATEX, "MAIL_HEADER":MAIL_HEADER} + +def escape(s, format=HTML): + """Replace special characters by their escape sequence. + + :Parameters: + - `s`: unicode-string to escape + - `format`: + + - `NONE`: nothing is replaced + - `HTML`: replace &<>'" by &...; + - `LATEX`: replace \#$%&_{}~^ + - `MAIL_HEADER`: escape non-ASCII mail-header-contents + :Returns: + the escaped string in unicode + :Exceptions: + - `ValueError`: if `format` is invalid. + + :Uses: + MAIL_HEADER uses module email + """ + #Note: If you have to make sure that every character gets replaced + # only once (and if you cannot achieve this with the following code), + # use something like "".join([replacedict.get(c,c) for c in s]) + # which is about 2-3 times slower (but maybe needs less memory). + #Note: This is one of the most time-consuming parts of the template. + if format is None or format == NONE: + pass + elif format == HTML: + s = s.replace("&", "&") # must be done first! + s = s.replace("<", "<") + s = s.replace(">", ">") + s = s.replace('"', """) + s = s.replace("'", "'") + elif format == LATEX: + s = s.replace("\\", "\\x") #must be done first! + s = s.replace("#", "\\#") + s = s.replace("$", "\\$") + s = s.replace("%", "\\%") + s = s.replace("&", "\\&") + s = s.replace("_", "\\_") + s = s.replace("{", "\\{") + s = s.replace("}", "\\}") + s = s.replace("\\x","\\textbackslash{}") + s = s.replace("~", "\\textasciitilde{}") + s = s.replace("^", "\\textasciicircum{}") + elif format == MAIL_HEADER: + import email.header + try: + s.encode("ascii") + return s + except UnicodeEncodeError: + return email.header.make_header([(s, "utf-8")]).encode() + else: + raise ValueError('Invalid format (only None, HTML, LATEX and MAIL_HEADER are supported).') + return s + +#========================================= + +#----------------------------------------- +# Exceptions + +class TemplateException(Exception): + """Base class for template-exceptions.""" + pass + +class TemplateParseError(TemplateException): + """Template parsing failed.""" + def __init__(self, err, errpos): + """ + :Parameters: + - `err`: error-message or exception to wrap + - `errpos`: ``(filename,row,col)`` where the error occured. + """ + self.err = err + self.filename, self.row, self.col = errpos + TemplateException.__init__(self) + def __str__(self): + if not self.filename: + return "line %d, col %d: %s" % (self.row, self.col, str(self.err)) + else: + return "file %s, line %d, col %d: %s" % (self.filename, self.row, self.col, str(self.err)) + +class TemplateSyntaxError(TemplateParseError, SyntaxError): + """Template syntax-error.""" + pass + +class TemplateIncludeError(TemplateParseError): + """Template 'include' failed.""" + pass + +class TemplateRenderError(TemplateException): + """Template rendering failed.""" + pass + +#----------------------------------------- +# Loader + +class LoaderString: + """Load template from a string/unicode. + + Note that 'include' is not possible in such templates. + """ + def __init__(self, encoding='utf-8'): + self.encoding = encoding + + def load(self, s): + """Return template-string as unicode. + """ + if isinstance(s, unicode): + u = s + else: + u = s.decode(self.encoding) + return u + +class LoaderFile: + """Load template from a file. + + When loading a template from a file, it's possible to including other + templates (by using 'include' in the template). But for simplicity + and security, all included templates have to be in the same directory! + (see ``allowed_path``) + """ + def __init__(self, allowed_path=None, encoding='utf-8'): + """Init the loader. + + :Parameters: + - `allowed_path`: path of the template-files + - `encoding`: encoding of the template-files + :Exceptions: + - `ValueError`: if `allowed_path` is not a directory + """ + if allowed_path and not os.path.isdir(allowed_path): + raise ValueError("'allowed_path' has to be a directory.") + self.path = allowed_path + self.encoding = encoding + + def load(self, filename): + """Load a template from a file. + + Check if filename is allowed and return its contens in unicode. + + :Parameters: + - `filename`: filename of the template without path + :Returns: + the contents of the template-file in unicode + :Exceptions: + - `ValueError`: if `filename` contains a path + """ + if filename != os.path.basename(filename): + raise ValueError("No path allowed in filename. (%s)" %(filename)) + filename = os.path.join(self.path, filename) + + f = open(filename, 'r', encoding=self.encoding) + u = f.read() + f.close() + + return u + +#----------------------------------------- +# Parser + +class Parser(object): + """Parse a template into a parse-tree. + + Includes a syntax-check, an optional expression-check and verbose + error-messages. + + See documentation for a description of the parse-tree. + """ + # template-syntax (original) + # _comment_start = "#!" + # _comment_end = "!#" + # _sub_start = "$!" + # _sub_end = "!$" + # _subesc_start = "@!" + # _subesc_end = "!@" + # _block_start = "" + + # template-syntax (eolian) + _comment_start = "#!" + _comment_end = "!#" + _sub_start = "${" # "$!" + _sub_end = "}$" # "!$" + _subesc_start = "${!" # "@!" + _subesc_end = "!}$" # "!@" + _block_start = "" + + # template-syntax (Jinja2 style) + # _comment_start = "" # "!#" + # _sub_start = "{{" # "$!" + # _sub_end = "}}" # "!$" + # _subesc_start = "{!" # "@!" + # _subesc_end = "!}" # "!@" + # _block_start = "{% " # "" + + # build regexps + # comment + # single-line, until end-tag or end-of-line. + _strComment = r"""%s(?P.*?)(?P%s|\n|$)""" \ + % (re.escape(_comment_start), re.escape(_comment_end)) + _reComment = re.compile(_strComment, re.M) + + # escaped or unescaped substitution + # single-line ("|$" is needed to be able to generate good error-messges) + _strSubstitution = r""" + ( + %s\s*(?P.*?)\s*(?P%s|$) #substitution + | + %s\s*(?P.*?)\s*(?P%s|$) #escaped substitution + ) + """ % (re.escape(_sub_start), re.escape(_sub_end), + re.escape(_subesc_start), re.escape(_subesc_end)) + _reSubstitution = re.compile(_strSubstitution, re.X|re.M) + + # block + # - single-line, no nesting. + # or + # - multi-line, nested by whitespace indentation: + # * start- and end-tag of a block must have exactly the same indentation. + # * start- and end-tags of *nested* blocks should have a greater indentation. + # NOTE: A single-line block must not start at beginning of the line with + # the same indentation as the enclosing multi-line blocks! + # Note that " " and "\t" are different, although they may + # look the same in an editor! + _s = re.escape(_block_start) + _e = re.escape(_block_end) + _strBlock = r""" + ^(?P[ \t]*)%send%s(?P.*)\r?\n? # multi-line end (^ IGNORED_TEXT\n) + | + (?P)%send%s # single-line end () + | + (?P[ \t]*) # single-line tag (no nesting) + %s(?P\w+)[ \t]*(?P.*?)%s + (?P.*?) + (?=(?:%s.*?%s.*?)??%send%s) # (match until end or i.e. ) + | + # multi-line tag, nested by whitespace indentation + ^(?P[ \t]*) # save indentation of start tag + %s(?P\w+)\s*(?P.*?)%s(?P.*)\r?\n + (?P(?:.*\n)*?) + (?=(?P=indent)%s(?:.|\s)*?%s) # match indentation + """ % (_s, _e, + _s, _e, + _s, _e, _s, _e, _s, _e, + _s, _e, _s, _e) + _reBlock = re.compile(_strBlock, re.X|re.M) + + # "for"-block parameters: "var(,var)* in ..." + _strForParam = r"""^(?P\w+(?:\s*,\s*\w+)*)\s+in\s+(?P.+)$""" + _reForParam = re.compile(_strForParam) + + # allowed macro-names + _reMacroParam = re.compile(r"""^\w+$""") + + + def __init__(self, loadfunc=None, testexpr=None, escape=HTML): + """Init the parser. + + :Parameters: + - `loadfunc`: function to load included templates + (i.e. ``LoaderFile(...).load``) + - `testexpr`: function to test if a template-expressions is valid + (i.e. ``EvalPseudoSandbox().compile``) + - `escape`: default-escaping (may be modified by the template) + :Exceptions: + - `ValueError`: if `testexpr` or `escape` is invalid. + """ + if loadfunc is None: + self._load = dummy_raise(NotImplementedError, "'include' not supported, since no 'loadfunc' was given.") + else: + self._load = loadfunc + + if testexpr is None: + self._testexprfunc = dummy + else: + try: # test if testexpr() works + testexpr("i==1") + except Exception as err: + raise ValueError("Invalid 'testexpr'. (%s)" %(err)) + self._testexprfunc = testexpr + + if escape not in ESCAPE_SUPPORTED.values(): + raise ValueError("Unsupported 'escape'. (%s)" %(escape)) + self.escape = escape + self._includestack = [] + + def parse(self, template): + """Parse a template. + + :Parameters: + - `template`: template-unicode-string + :Returns: the resulting parse-tree + :Exceptions: + - `TemplateSyntaxError`: for template-syntax-errors + - `TemplateIncludeError`: if template-inclusion failed + - `TemplateException` + """ + self._includestack = [(None, template)] # for error-messages (_errpos) + return self._parse(template) + + def _errpos(self, fpos): + """Convert `fpos` to ``(filename,row,column)`` for error-messages.""" + filename, string = self._includestack[-1] + return filename, srow(string, fpos), scol(string, fpos) + + def _testexpr(self, expr, fpos=0): + """Test a template-expression to detect errors.""" + try: + self._testexprfunc(expr) + except SyntaxError as err: + raise TemplateSyntaxError(err, self._errpos(fpos)) + + def _parse_sub(self, parsetree, text, fpos=0): + """Parse substitutions, and append them to the parse-tree. + + Additionally, remove comments. + """ + curr = 0 + for match in self._reSubstitution.finditer(text): + start = match.start() + if start > curr: + parsetree.append(("str", self._reComment.sub('', text[curr:start]))) + + if match.group("sub") is not None: + if not match.group("end"): + raise TemplateSyntaxError("Missing closing tag '%s' for '%s'." + % (self._sub_end, match.group()), self._errpos(fpos+start)) + if len(match.group("sub")) > 0: + self._testexpr(match.group("sub"), fpos+start) + parsetree.append(("sub", match.group("sub"))) + else: + assert(match.group("escsub") is not None) + if not match.group("escend"): + raise TemplateSyntaxError("Missing closing tag '%s' for '%s'." + % (self._subesc_end, match.group()), self._errpos(fpos+start)) + if len(match.group("escsub")) > 0: + self._testexpr(match.group("escsub"), fpos+start) + parsetree.append(("esc", self.escape, match.group("escsub"))) + + curr = match.end() + + if len(text) > curr: + parsetree.append(("str", self._reComment.sub('', text[curr:]))) + + def _parse(self, template, fpos=0): + """Recursive part of `parse()`. + + :Parameters: + - template + - fpos: position of ``template`` in the complete template (for error-messages) + """ + # blank out comments + # (So that its content does not collide with other syntax, and + # because removing them completely would falsify the character- + # position ("match.start()") of error-messages) + template = self._reComment.sub(lambda match: self._comment_start+" "*len(match.group(1))+match.group(2), template) + + # init parser + parsetree = [] + curr = 0 # current position (= end of previous block) + block_type = None # block type: if,for,macro,raw,... + block_indent = None # None: single-line, >=0: multi-line + + # find blocks + for match in self._reBlock.finditer(template): + start = match.start() + # process template-part before this block + if start > curr: + self._parse_sub(parsetree, template[curr:start], fpos) + + # analyze block syntax (incl. error-checking and -messages) + keyword = None + block = match.groupdict() + pos__ = fpos + start # shortcut + if block["sKeyw"] is not None: # single-line block tag + block_indent = None + keyword = block["sKeyw"] + param = block["sParam"] + content = block["sContent"] + if block["sSpace"]: # restore spaces before start-tag + if len(parsetree) > 0 and parsetree[-1][0] == "str": + parsetree[-1] = ("str", parsetree[-1][1] + block["sSpace"]) + else: + parsetree.append(("str", block["sSpace"])) + pos_p = fpos + match.start("sParam") # shortcuts + pos_c = fpos + match.start("sContent") + elif block["mKeyw"] is not None: # multi-line block tag + block_indent = len(block["indent"]) + keyword = block["mKeyw"] + param = block["mParam"] + content = block["mContent"] + pos_p = fpos + match.start("mParam") + pos_c = fpos + match.start("mContent") + ignored = block["mIgnored"].strip() + if ignored and ignored != self._comment_start: + raise TemplateSyntaxError("No code allowed after block-tag.", self._errpos(fpos+match.start("mIgnored"))) + elif block["mEnd"] is not None: # multi-line block end + if block_type is None: + raise TemplateSyntaxError("No block to end here/invalid indent.", self._errpos(pos__) ) + if block_indent != len(block["mEnd"]): + raise TemplateSyntaxError("Invalid indent for end-tag.", self._errpos(pos__) ) + ignored = block["meIgnored"].strip() + if ignored and ignored != self._comment_start: + raise TemplateSyntaxError("No code allowed after end-tag.", self._errpos(fpos+match.start("meIgnored"))) + block_type = None + elif block["sEnd"] is not None: # single-line block end + if block_type is None: + raise TemplateSyntaxError("No block to end here/invalid indent.", self._errpos(pos__)) + if block_indent is not None: + raise TemplateSyntaxError("Invalid indent for end-tag.", self._errpos(pos__)) + block_type = None + else: + raise TemplateException("FATAL: Block regexp error. Please contact the author. (%s)" % match.group()) + + # analyze block content (mainly error-checking and -messages) + if keyword: + keyword = keyword.lower() + if 'for' == keyword: + if block_type is not None: + raise TemplateSyntaxError("Missing block-end-tag before new block at '%s'." %(match.group()), self._errpos(pos__)) + block_type = 'for' + cond = self._reForParam.match(param) + if cond is None: + raise TemplateSyntaxError("Invalid 'for ...' at '%s'." %(param), self._errpos(pos_p)) + names = tuple(n.strip() for n in cond.group("names").split(",")) + self._testexpr(cond.group("iter"), pos_p+cond.start("iter")) + parsetree.append(("for", names, cond.group("iter"), self._parse(content, pos_c))) + elif 'if' == keyword: + if block_type is not None: + raise TemplateSyntaxError("Missing block-end-tag before new block at '%s'." %(match.group()), self._errpos(pos__)) + if not param: + raise TemplateSyntaxError("Missing condition for 'if' at '%s'." %(match.group()), self._errpos(pos__)) + block_type = 'if' + self._testexpr(param, pos_p) + parsetree.append(("if", param, self._parse(content, pos_c))) + elif 'elif' == keyword: + if block_type != 'if': + raise TemplateSyntaxError("'elif' may only appear after 'if' at '%s'." %(match.group()), self._errpos(pos__)) + if not param: + raise TemplateSyntaxError("Missing condition for 'elif' at '%s'." %(match.group()), self._errpos(pos__)) + self._testexpr(param, pos_p) + parsetree.append(("elif", param, self._parse(content, pos_c))) + elif 'else' == keyword: + if block_type not in ('if', 'for'): + raise TemplateSyntaxError("'else' may only appear after 'if' or 'for' at '%s'." %(match.group()), self._errpos(pos__)) + if param: + raise TemplateSyntaxError("'else' may not have parameters at '%s'." %(match.group()), self._errpos(pos__)) + parsetree.append(("else", self._parse(content, pos_c))) + elif 'macro' == keyword: + if block_type is not None: + raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos(pos__)) + block_type = 'macro' + # make sure param is "\w+" (instead of ".+") + if not param: + raise TemplateSyntaxError("Missing name for 'macro' at '%s'." %(match.group()), self._errpos(pos__)) + if not self._reMacroParam.match(param): + raise TemplateSyntaxError("Invalid name for 'macro' at '%s'." %(match.group()), self._errpos(pos__)) + #remove last newline + if len(content) > 0 and content[-1] == '\n': + content = content[:-1] + if len(content) > 0 and content[-1] == '\r': + content = content[:-1] + parsetree.append(("macro", param, self._parse(content, pos_c))) + + # parser-commands + elif 'raw' == keyword: + if block_type is not None: + raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos(pos__)) + if param: + raise TemplateSyntaxError("'raw' may not have parameters at '%s'." %(match.group()), self._errpos(pos__)) + block_type = 'raw' + parsetree.append(("str", content)) + elif 'include' == keyword: + if block_type is not None: + raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos(pos__)) + if param: + raise TemplateSyntaxError("'include' may not have parameters at '%s'." %(match.group()), self._errpos(pos__)) + block_type = 'include' + try: + u = self._load(content.strip()) + except Exception as err: + raise TemplateIncludeError(err, self._errpos(pos__)) + self._includestack.append((content.strip(), u)) # current filename/template for error-msg. + p = self._parse(u) + self._includestack.pop() + parsetree.extend(p) + elif 'set_escape' == keyword: + if block_type is not None: + raise TemplateSyntaxError("Missing block-end-tag before new block '%s'." %(match.group()), self._errpos(pos__)) + if param: + raise TemplateSyntaxError("'set_escape' may not have parameters at '%s'." %(match.group()), self._errpos(pos__)) + block_type = 'set_escape' + esc = content.strip().upper() + if esc not in ESCAPE_SUPPORTED: + raise TemplateSyntaxError("Unsupported escape '%s'." %(esc), self._errpos(pos__)) + self.escape = ESCAPE_SUPPORTED[esc] + else: + raise TemplateSyntaxError("Invalid keyword '%s'." %(keyword), self._errpos(pos__)) + curr = match.end() + + if block_type is not None: + raise TemplateSyntaxError("Missing end-tag.", self._errpos(pos__)) + + if len(template) > curr: # process template-part after last block + self._parse_sub(parsetree, template[curr:], fpos+curr) + + return parsetree + +#----------------------------------------- +# Evaluation + +# some checks +assert len(eval("dir()", {'__builtins__':{'dir':dir}})) == 1, \ + "FATAL: 'eval' does not work as expected (%s)." +assert compile("0 .__class__", "", "eval").co_names == ('__class__',), \ + "FATAL: 'compile' does not work as expected." + +class EvalPseudoSandbox: + """An eval-pseudo-sandbox. + + The pseudo-sandbox restricts the available functions/objects, so the + code can only access: + + - some of the builtin Python-functions, which are considered "safe" + (see safe_builtins) + - some additional functions (exists(), default(), setvar(), escape()) + - the passed objects incl. their methods. + + Additionally, names beginning with "_" are forbidden. + This is to prevent things like '0 .__class__', with which you could + easily break out of a "sandbox". + + Be careful to only pass "safe" objects/functions to the template, + because any unsafe function/method could break the sandbox! + For maximum security, restrict the access to as few objects/functions + as possible! + + :Warning: + Note that this is no real sandbox! (And although I don't know any + way to break out of the sandbox without passing-in an unsafe object, + I cannot guarantee that there is no such way. So use with care.) + + Take care if you want to use it for untrusted code!! + """ + + safe_builtins = { + "True" : True, + "False" : False, + "None" : None, + + "abs" : builtins.abs, + "chr" : builtins.chr, + "divmod" : builtins.divmod, + "hash" : builtins.hash, + "hex" : builtins.hex, + "isinstance": builtins.isinstance, + "len" : builtins.len, + "max" : builtins.max, + "min" : builtins.min, + "oct" : builtins.oct, + "ord" : builtins.ord, + "pow" : builtins.pow, + "range" : builtins.range, + "round" : builtins.round, + "sorted" : builtins.sorted, + "sum" : builtins.sum, + "unichr" : builtins.chr, + "zip" : builtins.zip, + + "bool" : builtins.bool, + "bytes" : builtins.bytes, + "complex" : builtins.complex, + "dict" : builtins.dict, + "enumerate" : builtins.enumerate, + "float" : builtins.float, + "int" : builtins.int, + "list" : builtins.list, + "long" : long, + "reversed" : builtins.reversed, + "set" : builtins.set, + "str" : builtins.str, + "tuple" : builtins.tuple, + "unicode" : unicode, + + "dir" : builtins.dir, + } + if sys.version_info[0] < 3: + safe_builtins["unichr"] = builtins.unichr + + def __init__(self): + self._compile_cache = {} + self.vars_ptr = None + self.eval_allowed_builtins = self.safe_builtins.copy() + self.register("__import__", self.f_import) + self.register("exists", self.f_exists) + self.register("default", self.f_default) + self.register("setvar", self.f_setvar) + self.register("escape", self.f_escape) + + def register(self, name, obj): + """Add an object to the "allowed eval-builtins". + + Mainly useful to add user-defined functions to the pseudo-sandbox. + """ + self.eval_allowed_builtins[name] = obj + + def _check_code_names(self, code, expr): + """Check if the code tries to access names beginning with "_". + + Used to prevent sandbox-breakouts via new-style-classes, like + ``"".__class__.__base__.__subclasses__()``. + + :Raises: + NameError if expression contains forbidden names. + """ + for name in code.co_names: + if name[0] == '_' and name != '_[1]': # _[1] is necessary for [x for x in y] + raise NameError("Name '%s' is not allowed in '%s'." % (name, expr)) + # recursively check sub-codes (e.g. lambdas) + for const in code.co_consts: + if isinstance(const, types.CodeType): + self._check_code_names(const, expr) + + def compile(self, expr): + """Compile a Python-eval-expression. + + - Use a compile-cache. + - Raise a `NameError` if `expr` contains a name beginning with ``_``. + + :Returns: the compiled `expr` + :Exceptions: + - `SyntaxError`: for compile-errors + - `NameError`: if expr contains a name beginning with ``_`` + """ + if expr not in self._compile_cache: + c = compile(expr, "", "eval") + self._check_code_names(c, expr) + self._compile_cache[expr] = c + return self._compile_cache[expr] + + def eval(self, expr, variables): + """Eval a Python-eval-expression. + + Sets ``self.vars_ptr`` to ``variables`` and compiles the code + before evaluating. + """ + sav = self.vars_ptr + self.vars_ptr = variables + + try: + x = eval(self.compile(expr), {"__builtins__": self.eval_allowed_builtins}, variables) + except NameError: + # workaround for lambdas like ``sorted(..., key=lambda x: my_f(x))`` + vars2 = {"__builtins__": self.eval_allowed_builtins} + vars2.update(variables) + x = eval(self.compile(expr), vars2) + + self.vars_ptr = sav + return x + + def f_import(self, name, *_, **__): + """``import``/``__import__()`` for the sandboxed code. + + Since "import" is insecure, the PseudoSandbox does not allow to + import other modules. But since some functions need to import + other modules (e.g. "datetime.datetime.strftime" imports "time"), + this function replaces the builtin "import" and allows to use + modules which are already accessible by the sandboxed code. + + :Note: + - This probably only works for rather simple imports. + - For security, it may be better to avoid such (complex) modules + which import other modules. (e.g. use time.localtime and + time.strftime instead of datetime.datetime.strftime, + or write a small wrapper.) + + :Example: + + >>> from datetime import datetime + >>> import pyratemp + >>> t = pyratemp.Template('@!mytime.strftime("%H:%M:%S")!@') + + # >>> print(t(mytime=datetime.now())) + # Traceback (most recent call last): + # ... + # ImportError: import not allowed in pseudo-sandbox; try to import 'time' yourself and pass it to the sandbox/template + + >>> import time + >>> print(t(mytime=datetime.strptime("13:40:54", "%H:%M:%S"), time=time)) + 13:40:54 + + # >>> print(t(mytime=datetime.now(), time=time)) + # 13:40:54 + """ + if self.vars_ptr is not None and name in self.vars_ptr and isinstance(self.vars_ptr[name], types.ModuleType): + return self.vars_ptr[name] + else: + raise ImportError("import not allowed in pseudo-sandbox; try to import '%s' yourself (and maybe pass it to the sandbox/template)" % name) + + def f_exists(self, varname): + """``exists()`` for the sandboxed code. + + Test if the variable `varname` exists in the current namespace. + + This only works for single variable names. If you want to test + complicated expressions, use i.e. `default`. + (i.e. `default("expr",False)`) + + :Note: the variable-name has to be quoted! (like in eval) + :Example: see module-docstring + """ + return (varname in self.vars_ptr) + + def f_default(self, expr, default=None): + """``default()`` for the sandboxed code. + + Try to evaluate an expression and return the result or a + fallback-/default-value; the `default`-value is used + if `expr` does not exist/is invalid/results in None. + + This is very useful for optional data. + + :Parameter: + - expr: "eval-expression" + - default: fallback-value if eval(expr) fails or is None. + :Returns: + the eval-result or the "fallback"-value. + + :Note: the eval-expression has to be quoted! (like in eval) + :Example: see module-docstring + """ + try: + r = self.eval(expr, self.vars_ptr) + if r is None: + return default + return r + #TODO: which exceptions should be catched here? + except (NameError, LookupError, TypeError, AttributeError): + return default + + def f_setvar(self, name, expr): + """``setvar()`` for the sandboxed code. + + Set a variable. + + :Example: see module-docstring + """ + self.vars_ptr[name] = self.eval(expr, self.vars_ptr) + return "" + + def f_escape(self, s, format="HTML"): + """``escape()`` for the sandboxed code. + """ + if isinstance(format, (str, unicode)): + format = ESCAPE_SUPPORTED[format.upper()] + return escape(unicode(s), format) + +#----------------------------------------- +# basic template / subtemplate + +class TemplateBase: + """Basic template-class. + + Used both for the template itself and for 'macro's ("subtemplates") in + the template. + """ + + def __init__(self, parsetree, renderfunc, data=None): + """Create the Template/Subtemplate/Macro. + + :Parameters: + - `parsetree`: parse-tree of the template/subtemplate/macro + - `renderfunc`: render-function + - `data`: data to fill into the template by default (dictionary). + This data may later be overridden when rendering the template. + :Exceptions: + - `TypeError`: if `data` is not a dictionary + """ + #TODO: parameter-checking? + self.parsetree = parsetree + if isinstance(data, dict): + self.data = data + elif data is None: + self.data = {} + else: + raise TypeError('"data" must be a dict (or None).') + self.current_data = data + self._render = renderfunc + + def __call__(self, **override): + """Fill out/render the template. + + :Parameters: + - `override`: objects to add to the data-namespace, overriding + the "default"-data. + :Returns: the filled template (in unicode) + :Note: This is also called when invoking macros + (i.e. ``$!mymacro()!$``). + """ + self.current_data = self.data.copy() + self.current_data.update(override) + u = "".join(self._render(self.parsetree, self.current_data)) + self.current_data = self.data # restore current_data + return _dontescape(u) # (see class _dontescape) + + def __unicode__(self): + """Alias for __call__().""" + return self.__call__() + def __str__(self): + """Alias for __call__().""" + return self.__call__() + +#----------------------------------------- +# Renderer + +class _dontescape(unicode): + """Unicode-string which should not be escaped. + + If ``isinstance(object,_dontescape)``, then don't escape the object in + ``@!...!@``. It's useful for not double-escaping macros, and it's + automatically used for macros/subtemplates. + + :Note: This only works if the object is used on its own in ``@!...!@``. + It i.e. does not work in ``@!object*2!@`` or ``@!object + "hi"!@``. + """ + __slots__ = [] + + +class Renderer(object): + """Render a template-parse-tree. + + :Uses: `TemplateBase` for macros + """ + + def __init__(self, evalfunc, escapefunc): + """Init the renderer. + + :Parameters: + - `evalfunc`: function for template-expression-evaluation + (i.e. ``EvalPseudoSandbox().eval``) + - `escapefunc`: function for escaping special characters + (i.e. `escape`) + """ + #TODO: test evalfunc + self.evalfunc = evalfunc + self.escapefunc = escapefunc + + def _eval(self, expr, data): + """evalfunc with error-messages""" + try: + return self.evalfunc(expr, data) + #TODO: any other errors to catch here? + except (TypeError, NameError, LookupError, AttributeError, SyntaxError) as err: + raise TemplateRenderError("Cannot eval expression '%s'. (%s: %s)" %(expr, err.__class__.__name__, err)) + + def render(self, parsetree, data): + """Render a parse-tree of a template. + + :Parameters: + - `parsetree`: the parse-tree + - `data`: the data to fill into the template (dictionary) + :Returns: the rendered output-unicode-string + :Exceptions: + - `TemplateRenderError` + """ + _eval = self._eval # shortcut + output = [] + do_else = False # use else/elif-branch? + + if parsetree is None: + return "" + for elem in parsetree: + if "str" == elem[0]: + output.append(elem[1]) + elif "sub" == elem[0]: + output.append(unicode(_eval(elem[1], data))) + elif "esc" == elem[0]: + obj = _eval(elem[2], data) + #prevent double-escape + if isinstance(obj, _dontescape) or isinstance(obj, TemplateBase): + output.append(unicode(obj)) + else: + output.append(self.escapefunc(unicode(obj), elem[1])) + elif "for" == elem[0]: + do_else = True + (names, iterable) = elem[1:3] + try: + loop_iter = iter(_eval(iterable, data)) + except TypeError: + raise TemplateRenderError("Cannot loop over '%s'." % iterable) + for i in loop_iter: + do_else = False + if len(names) == 1: + data[names[0]] = i + else: + data.update(zip(names, i)) #"for a,b,.. in list" + output.extend(self.render(elem[3], data)) + elif "if" == elem[0]: + do_else = True + if _eval(elem[1], data): + do_else = False + output.extend(self.render(elem[2], data)) + elif "elif" == elem[0]: + if do_else and _eval(elem[1], data): + do_else = False + output.extend(self.render(elem[2], data)) + elif "else" == elem[0]: + if do_else: + do_else = False + output.extend(self.render(elem[1], data)) + elif "macro" == elem[0]: + data[elem[1]] = TemplateBase(elem[2], self.render, data) + else: + raise TemplateRenderError("Invalid parse-tree (%s)." %(elem)) + + return output + +#----------------------------------------- +# template user-interface (putting it all together) + +class Template(TemplateBase): + """Template-User-Interface. + + :Usage: + :: + t = Template(...) (<- see __init__) + output = t(...) (<- see TemplateBase.__call__) + + :Example: + see module-docstring + """ + + def __init__(self, string=None,filename=None,parsetree=None, encoding='utf-8', data=None, escape=HTML, + loader_class=LoaderFile, + parser_class=Parser, + renderer_class=Renderer, + eval_class=EvalPseudoSandbox, + escape_func=escape): + """Load (+parse) a template. + + :Parameters: + - `string,filename,parsetree`: a template-string, + filename of a template to load, + or a template-parsetree. + (only one of these 3 is allowed) + - `encoding`: encoding of the template-files (only used for "filename") + - `data`: data to fill into the template by default (dictionary). + This data may later be overridden when rendering the template. + - `escape`: default-escaping for the template, may be overwritten by the template! + - `loader_class` + - `parser_class` + - `renderer_class` + - `eval_class` + - `escapefunc` + """ + if [string, filename, parsetree].count(None) != 2: + raise ValueError('Exactly 1 of string,filename,parsetree is necessary.') + + tmpl = None + # load template + if filename is not None: + incl_load = loader_class(os.path.dirname(filename), encoding).load + tmpl = incl_load(os.path.basename(filename)) + if string is not None: + incl_load = dummy_raise(NotImplementedError, "'include' not supported for template-strings.") + tmpl = LoaderString(encoding).load(string) + + # eval (incl. compile-cache) + templateeval = eval_class() + + # parse + if tmpl is not None: + p = parser_class(loadfunc=incl_load, testexpr=templateeval.compile, escape=escape) + parsetree = p.parse(tmpl) + del p + + # renderer + renderfunc = renderer_class(templateeval.eval, escape_func).render + + #create template + TemplateBase.__init__(self, parsetree, renderfunc, data) + + +#========================================= diff --git a/src/scripts/pyolian/test_gen_class.template b/src/scripts/pyolian/test_gen_class.template new file mode 100644 index 0000000000..7f03b5edd2 --- /dev/null +++ b/src/scripts/pyolian/test_gen_class.template @@ -0,0 +1,45 @@ + +================================================================================ +Class: ${cls.full_name}$ +================================================================================ +Class type: ${cls.type}$ +Base Class: ${cls.base_class.full_name if cls.base_class else None}$ +Inherits: ${list(cls.inherits)}$ +InheritsFull: ${cls.inherits_full}$ +Namespaces: ${list(cls.namespaces)}$ +File: ${cls.file}$ +Ctor enable: ${cls.ctor_enable}$ +Dtor enable: ${cls.dtor_enable}$ + +Constructors: +============= + + * ${ctor}$ + + no constructors available + + +Methods: +======== + + * ${func.name}$(...) ${func.method_scope}$ + + no methods available + + +Properties: +=========== + + * ${func.name}$ (${v.type.c_type}$ ${v.name}$, ) + + no properties available + + +Events: +======= + + * ${event.name}$ -> ${event.c_name}$ + + no events available + + diff --git a/src/scripts/pyolian/test_gen_namespace.template b/src/scripts/pyolian/test_gen_namespace.template new file mode 100644 index 0000000000..09b7763bb0 --- /dev/null +++ b/src/scripts/pyolian/test_gen_namespace.template @@ -0,0 +1,43 @@ + +================================================================================ +Namespace: ${namespace}$ ${namespaces}$ +================================================================================ + +Classes: +======== + + * ${cls.full_name}$ (${cls.type}$) + + no classes available + + +Aliases: +======== + + * ${typedecl.full_name}$ + + no alias available + + +Structs: +======== + + * ${typedecl.full_name}$ + + ${field}$ + + + no structs available + + +Enums: +====== + + * ${typedecl.full_name}$ + + ${field}$ + + + no enums available + +