diff options
39 files changed, 1159 insertions, 774 deletions
@@ -12,3 +12,6 @@ /include /lib /man +
+.tox/
+.cache/
diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..69a61b8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python
+python: 2.7
+env:
+ - TOX_ENV=py34
+ - TOX_ENV=py33
+ - TOX_ENV=py27
+ - TOX_ENV=py26
+install:
+ - python -m pip install -U tox
+script:
+ - python -m tox -e $TOX_ENV
diff --git a/doc/build/changelog.rst b/doc/build/changelog.rst index 03aaec7..25d63b8 100644 --- a/doc/build/changelog.rst +++ b/doc/build/changelog.rst @@ -6,6 +6,68 @@ Changelog === .. changelog:: + :version: 1.0.4 + +.. changelog:: + :version: 1.0.3 + :released: Tue Oct 27 2015 + + .. change:: + :tags: bug, babel + :pullreq: bitbucket:21 + + Fixed an issue where the Babel plugin would not handle a translation + symbol that contained non-ascii characters. Pull request courtesy + Roman Imankulov. + +.. changelog:: + :version: 1.0.2 + :released: Wed Aug 26 2015 + + .. change:: + :tags: bug, installation + :tickets: 249 + + The "universal wheel" marker is removed from setup.cfg, because + our setup.py currently makes use of conditional dependencies. + In :ticket:`249`, the discussion is ongoing on how to correct our + setup.cfg / setup.py fully so that we can handle the per-version + dependency changes while still maintaining optimal wheel settings, + so this issue is not yet fully resolved. + + .. change:: + :tags: bug, py3k + :tickets: 250 + + Repair some calls within the ast module that no longer work on Python3.5; + additionally replace the use of ``inspect.getargspec()`` under + Python 3 (seems to be called from the TG plugin) to avoid deprecation + warnings. + + .. change:: + :tags: bug + :pullreq: bitbucket:18 + + Update the Lingua translation extraction plugin to correctly + handle templates mixing Python control statements (such as if, + for and while) with template fragments. Pull request courtesy + Laurent Daverio. + + .. change:: + :tags: feature + :tickets: 236 + + Added ``STOP_RENDERING`` keyword for returning/exiting from a + template early, which is a synonym for an empty string ``""``. + Previously, the docs suggested a bare + ``return``, but this could cause ``None`` to appear in the + rendered template result. + + .. seealso:: + + :ref:`syntax_exiting_early` + +.. changelog:: :version: 1.0.1 :released: Thu Jan 22 2015 diff --git a/doc/build/defs.rst b/doc/build/defs.rst index 3c06840..314e9b9 100644 --- a/doc/build/defs.rst +++ b/doc/build/defs.rst @@ -216,7 +216,7 @@ modules, you can define arbitrarily nestable tags right in your templates. To achieve this, the target def is invoked using the form -``<%namepacename:defname>`` instead of the normal ``${}`` +``<%namespacename:defname>`` instead of the normal ``${}`` syntax. This syntax, introduced in Mako 0.2.3, is functionally equivalent to another tag known as ``%call``, which takes the form ``<%call expr='namespacename.defname(args)'>``. While ``%call`` diff --git a/doc/build/syntax.rst b/doc/build/syntax.rst index fe4a860..e3dd7db 100644 --- a/doc/build/syntax.rst +++ b/doc/build/syntax.rst @@ -212,7 +212,7 @@ pure-Python functions you might want to declare: %> Any number of ``<%! %>`` blocks can be declared anywhere in a -template; they will be rendered in the resulting module +template; they will be rendered in the resulting module in a single contiguous block above all render callables, in the order in which they appear in the source template. @@ -443,19 +443,25 @@ Mako: <%def name="x()">${x}</%def> </%text> -Returning Early from a Template -=============================== +.. _syntax_exiting_early: + +Exiting Early from a Template +============================= Sometimes you want to stop processing a template or ``<%def>`` method in the middle and just use the text you've accumulated so -far. You can use a ``return`` statement inside a Python -block to do that. +far. This is accomplished by using ``return`` statement inside +a Python block. It's a good idea for the ``return`` statement +to return an empty string, which prevents the Python default return +value of ``None`` from being rendered by the template. This +return value is for semantic purposes provided in templates via +the ``STOP_RENDERING`` symbol: .. sourcecode:: mako % if not len(records): No records found. - <% return %> + <% return STOP_RENDERING %> % endif Or perhaps: @@ -464,6 +470,17 @@ Or perhaps: <% if not len(records): - return + return STOP_RENDERING %> +In older versions of Mako, an empty string can be substituted for +the ``STOP_RENDERING`` symbol: + +.. sourcecode:: mako + + <% return '' %> + +.. versionadded:: 1.0.2 - added the ``STOP_RENDERING`` symbol which serves + as a semantic identifier for the empty string ``""`` used by a + Python ``return`` statement. + diff --git a/doc/build/templates/base.mako b/doc/build/templates/base.mako index 32c49d8..b23be0f 100644 --- a/doc/build/templates/base.mako +++ b/doc/build/templates/base.mako @@ -19,14 +19,6 @@ <div id="wrap"> <div class="rightbar"> - % if toolbar: - <div id="gittip_nav"> - <iframe style="border: 0; margin: 0; padding: 0;" - src="https://www.gittip.com/zzzeek/widget.html" - width="48pt" height="20pt"></iframe> - </div> - % endif - <div class="slogan"> Hyperfast and lightweight templating for the Python platform. </div> diff --git a/mako/__init__.py b/mako/__init__.py index d963848..850f2a6 100644 --- a/mako/__init__.py +++ b/mako/__init__.py @@ -5,4 +5,4 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php -__version__ = '1.0.1' +__version__ = '1.0.4' diff --git a/mako/_ast_util.py b/mako/_ast_util.py index efbc4fc..cc298d5 100644 --- a/mako/_ast_util.py +++ b/mako/_ast_util.py @@ -30,7 +30,7 @@ :copyright: Copyright 2008 by Armin Ronacher. :license: Python License. """ -from _ast import * +from _ast import * # noqa from mako.compat import arg_stringname BOOLOP_SYMBOLS = { @@ -246,6 +246,7 @@ def walk(node): class NodeVisitor(object): + """ Walks the abstract syntax tree and call visitor functions for every node found. The visitor functions may return values which will be forwarded @@ -290,6 +291,7 @@ class NodeVisitor(object): class NodeTransformer(NodeVisitor): + """ Walks the abstract syntax tree and allows modifications of nodes. @@ -349,6 +351,7 @@ class NodeTransformer(NodeVisitor): class SourceGenerator(NodeVisitor): + """ This visitor is able to transform a well formed syntax tree into python sourcecode. For more details have a look at the docstring of the @@ -388,6 +391,7 @@ class SourceGenerator(NodeVisitor): def signature(self, node): want_comma = [] + def write_comma(): if want_comma: self.write(', ') @@ -460,6 +464,7 @@ class SourceGenerator(NodeVisitor): def visit_ClassDef(self, node): have_args = [] + def paren_or_comma(): if have_args: self.write(', ') @@ -481,11 +486,11 @@ class SourceGenerator(NodeVisitor): paren_or_comma() self.write(keyword.arg + '=') self.visit(keyword.value) - if node.starargs is not None: + if getattr(node, "starargs", None): paren_or_comma() self.write('*') self.visit(node.starargs) - if node.kwargs is not None: + if getattr(node, "kwargs", None): paren_or_comma() self.write('**') self.visit(node.kwargs) @@ -631,6 +636,7 @@ class SourceGenerator(NodeVisitor): def visit_Call(self, node): want_comma = [] + def write_comma(): if want_comma: self.write(', ') @@ -646,11 +652,11 @@ class SourceGenerator(NodeVisitor): write_comma() self.write(keyword.arg + '=') self.visit(keyword.value) - if node.starargs is not None: + if getattr(node, "starargs", None): write_comma() self.write('*') self.visit(node.starargs) - if node.kwargs is not None: + if getattr(node, "kwargs", None): write_comma() self.write('**') self.visit(node.kwargs) diff --git a/mako/ast.py b/mako/ast.py index 65fd84d..c55b29c 100644 --- a/mako/ast.py +++ b/mako/ast.py @@ -10,8 +10,11 @@ code, as well as generating Python from AST nodes""" from mako import exceptions, pyparser, compat import re + class PythonCode(object): + """represents information about a string containing Python code""" + def __init__(self, code, **exception_kwargs): self.code = code @@ -41,8 +44,11 @@ class PythonCode(object): f = pyparser.FindIdentifiers(self, **exception_kwargs) f.visit(expr) + class ArgumentList(object): + """parses a fragment of code as a comma-separated list of expressions""" + def __init__(self, code, **exception_kwargs): self.codeargs = [] self.args = [] @@ -52,7 +58,7 @@ class ArgumentList(object): if re.match(r"\S", code) and not re.match(r",\s*$", code): # if theres text and no trailing comma, insure its parsed # as a tuple by adding a trailing comma - code += "," + code += "," expr = pyparser.parse(code, "exec", **exception_kwargs) else: expr = code @@ -60,7 +66,9 @@ class ArgumentList(object): f = pyparser.FindTuple(self, PythonCode, **exception_kwargs) f.visit(expr) + class PythonFragment(PythonCode): + """extends PythonCode to provide identifier lookups in partial control statements @@ -70,16 +78,17 @@ class PythonFragment(PythonCode): except (MyException, e): etc. """ + def __init__(self, code, **exception_kwargs): m = re.match(r'^(\w+)(?:\s+(.*?))?:\s*(#|$)', code.strip(), re.S) if not m: raise exceptions.CompileException( - "Fragment '%s' is not a partial control statement" % - code, **exception_kwargs) + "Fragment '%s' is not a partial control statement" % + code, **exception_kwargs) if m.group(3): code = code[:m.start(3)] - (keyword, expr) = m.group(1,2) - if keyword in ['for','if', 'while']: + (keyword, expr) = m.group(1, 2) + if keyword in ['for', 'if', 'while']: code = code + "pass" elif keyword == 'try': code = code + "pass\nexcept:pass" @@ -91,13 +100,15 @@ class PythonFragment(PythonCode): code = code + "pass" else: raise exceptions.CompileException( - "Unsupported control keyword: '%s'" % - keyword, **exception_kwargs) + "Unsupported control keyword: '%s'" % + keyword, **exception_kwargs) super(PythonFragment, self).__init__(code, **exception_kwargs) class FunctionDecl(object): + """function declaration""" + def __init__(self, code, allow_kwargs=True, **exception_kwargs): self.code = code expr = pyparser.parse(code, "exec", **exception_kwargs) @@ -106,12 +117,12 @@ class FunctionDecl(object): f.visit(expr) if not hasattr(self, 'funcname'): raise exceptions.CompileException( - "Code '%s' is not a function declaration" % code, - **exception_kwargs) + "Code '%s' is not a function declaration" % code, + **exception_kwargs) if not allow_kwargs and self.kwargs: raise exceptions.CompileException( - "'**%s' keyword argument not allowed here" % - self.kwargnames[-1], **exception_kwargs) + "'**%s' keyword argument not allowed here" % + self.kwargnames[-1], **exception_kwargs) def get_argument_expressions(self, as_call=False): """Return the argument declarations of this FunctionDecl as a printable @@ -170,9 +181,11 @@ class FunctionDecl(object): def allargnames(self): return tuple(self.argnames) + tuple(self.kwargnames) + class FunctionArgs(FunctionDecl): + """the argument portion of a function declaration""" def __init__(self, code, **kwargs): super(FunctionArgs, self).__init__("def ANON(%s):pass" % code, - **kwargs) + **kwargs) diff --git a/mako/cache.py b/mako/cache.py index c405c51..c7aabd2 100644 --- a/mako/cache.py +++ b/mako/cache.py @@ -13,6 +13,7 @@ register_plugin("beaker", "mako.ext.beaker_cache", "BeakerCacheImpl") class Cache(object): + """Represents a data content cache made available to the module space of a specific :class:`.Template` object. @@ -181,6 +182,7 @@ class Cache(object): class CacheImpl(object): + """Provide a cache implementation for use by :class:`.Cache`.""" def __init__(self, cache): diff --git a/mako/cmd.py b/mako/cmd.py index 1a9ca56..50d47fc 100755 --- a/mako/cmd.py +++ b/mako/cmd.py @@ -10,26 +10,31 @@ from mako.template import Template from mako.lookup import TemplateLookup from mako import exceptions + def varsplit(var): if "=" not in var: return (var, "") return var.split("=", 1) + def _exit(): sys.stderr.write(exceptions.text_error_template().render()) sys.exit(1) + def cmdline(argv=None): parser = ArgumentParser("usage: %prog [FILENAME]") - parser.add_argument("--var", default=[], action="append", - help="variable (can be used multiple times, use name=value)") - parser.add_argument("--template-dir", default=[], action="append", - help="Directory to use for template lookup (multiple " - "directories may be provided). If not given then if the " - "template is read from stdin, the value defaults to be " - "the current directory, otherwise it defaults to be the " - "parent directory of the file provided.") + parser.add_argument( + "--var", default=[], action="append", + help="variable (can be used multiple times, use name=value)") + parser.add_argument( + "--template-dir", default=[], action="append", + help="Directory to use for template lookup (multiple " + "directories may be provided). If not given then if the " + "template is read from stdin, the value defaults to be " + "the current directory, otherwise it defaults to be the " + "parent directory of the file provided.") parser.add_argument('input', nargs='?', default='-') options = parser.parse_args(argv) diff --git a/mako/codegen.py b/mako/codegen.py index 4b0bda8..bf86d79 100644 --- a/mako/codegen.py +++ b/mako/codegen.py @@ -19,22 +19,23 @@ MAGIC_NUMBER = 10 # names which are hardwired into the # template and are not accessed via the # context itself -RESERVED_NAMES = set(['context', 'loop', 'UNDEFINED']) +TOPLEVEL_DECLARED = set(["UNDEFINED", "STOP_RENDERING"]) +RESERVED_NAMES = set(['context', 'loop']).union(TOPLEVEL_DECLARED) -def compile(node, - uri, - filename=None, - default_filters=None, - buffer_filters=None, - imports=None, - future_imports=None, - source_encoding=None, - generate_magic_comment=True, - disable_unicode=False, - strict_undefined=False, - enable_loop=True, - reserved_names=frozenset()): +def compile(node, + uri, + filename=None, + default_filters=None, + buffer_filters=None, + imports=None, + future_imports=None, + source_encoding=None, + generate_magic_comment=True, + disable_unicode=False, + strict_undefined=False, + enable_loop=True, + reserved_names=frozenset()): """Generate module source code given a parsetree node, uri, and optional source filename""" @@ -45,40 +46,41 @@ def compile(node, if not compat.py3k and isinstance(source_encoding, compat.text_type): source_encoding = source_encoding.encode(source_encoding) - buf = util.FastEncodingBuffer() printer = PythonPrinter(buf) _GenerateRenderMethod(printer, - _CompileContext(uri, - filename, - default_filters, - buffer_filters, - imports, - future_imports, - source_encoding, - generate_magic_comment, - disable_unicode, - strict_undefined, - enable_loop, - reserved_names), - node) + _CompileContext(uri, + filename, + default_filters, + buffer_filters, + imports, + future_imports, + source_encoding, + generate_magic_comment, + disable_unicode, + strict_undefined, + enable_loop, + reserved_names), + node) return buf.getvalue() + class _CompileContext(object): + def __init__(self, - uri, - filename, - default_filters, - buffer_filters, - imports, - future_imports, - source_encoding, - generate_magic_comment, - disable_unicode, - strict_undefined, - enable_loop, - reserved_names): + uri, + filename, + default_filters, + buffer_filters, + imports, + future_imports, + source_encoding, + generate_magic_comment, + disable_unicode, + strict_undefined, + enable_loop, + reserved_names): self.uri = uri self.filename = filename self.default_filters = default_filters @@ -92,11 +94,14 @@ class _CompileContext(object): self.enable_loop = enable_loop self.reserved_names = reserved_names + class _GenerateRenderMethod(object): + """A template visitor object which generates the full module source for a template. """ + def __init__(self, printer, compiler, node): self.printer = printer self.compiler = compiler @@ -124,9 +129,9 @@ class _GenerateRenderMethod(object): args += ['**pageargs'] cached = eval(pagetag.attributes.get('cached', 'False')) self.compiler.enable_loop = self.compiler.enable_loop or eval( - pagetag.attributes.get( - 'enable_loop', 'False') - ) + pagetag.attributes.get( + 'enable_loop', 'False') + ) else: args = ['**pageargs'] cached = False @@ -137,9 +142,9 @@ class _GenerateRenderMethod(object): args = [a for a in ['context'] + args] self.write_render_callable( - pagetag or node, - name, args, - buffered, filtered, cached) + pagetag or node, + name, args, + buffered, filtered, cached) if defs is not None: for node in defs: @@ -150,7 +155,7 @@ class _GenerateRenderMethod(object): def write_metadata_struct(self): self.printer.source_map[self.printer.lineno] = \ - max(self.printer.source_map) + max(self.printer.source_map) struct = { "filename": self.compiler.filename, "uri": self.compiler.uri, @@ -181,12 +186,16 @@ class _GenerateRenderMethod(object): self.compiler.pagetag = None class FindTopLevel(object): + def visitInheritTag(s, node): inherit.append(node) + def visitNamespaceTag(s, node): namespaces[node.name] = node + def visitPageTag(s, node): self.compiler.pagetag = node + def visitCode(s, node): if node.ismodule: module_code.append(node) @@ -208,49 +217,50 @@ class _GenerateRenderMethod(object): if self.compiler.generate_magic_comment and \ self.compiler.source_encoding: self.printer.writeline("# -*- coding:%s -*-" % - self.compiler.source_encoding) + self.compiler.source_encoding) if self.compiler.future_imports: self.printer.writeline("from __future__ import %s" % (", ".join(self.compiler.future_imports),)) self.printer.writeline("from mako import runtime, filters, cache") self.printer.writeline("UNDEFINED = runtime.UNDEFINED") + self.printer.writeline("STOP_RENDERING = runtime.STOP_RENDERING") self.printer.writeline("__M_dict_builtin = dict") self.printer.writeline("__M_locals_builtin = locals") self.printer.writeline("_magic_number = %r" % MAGIC_NUMBER) self.printer.writeline("_modified_time = %r" % time.time()) self.printer.writeline("_enable_loop = %r" % self.compiler.enable_loop) self.printer.writeline( - "_template_filename = %r" % self.compiler.filename) + "_template_filename = %r" % self.compiler.filename) self.printer.writeline("_template_uri = %r" % self.compiler.uri) self.printer.writeline( - "_source_encoding = %r" % self.compiler.source_encoding) + "_source_encoding = %r" % self.compiler.source_encoding) if self.compiler.imports: buf = '' for imp in self.compiler.imports: buf += imp + "\n" self.printer.writeline(imp) impcode = ast.PythonCode( - buf, - source='', lineno=0, - pos=0, - filename='template defined imports') + buf, + source='', lineno=0, + pos=0, + filename='template defined imports') else: impcode = None main_identifiers = module_identifiers.branch(self.node) module_identifiers.topleveldefs = \ module_identifiers.topleveldefs.\ - union(main_identifiers.topleveldefs) - module_identifiers.declared.add("UNDEFINED") + union(main_identifiers.topleveldefs) + module_identifiers.declared.update(TOPLEVEL_DECLARED) if impcode: module_identifiers.declared.update(impcode.declared_identifiers) self.compiler.identifiers = module_identifiers self.printer.writeline("_exports = %r" % - [n.name for n in - main_identifiers.topleveldefs.values()] - ) + [n.name for n in + main_identifiers.topleveldefs.values()] + ) self.printer.write_blanks(2) if len(module_code): @@ -265,7 +275,7 @@ class _GenerateRenderMethod(object): return list(main_identifiers.topleveldefs.values()) def write_render_callable(self, node, name, args, buffered, filtered, - cached): + cached): """write a top-level render callable. this could be the main render() method or that of a top-level def.""" @@ -274,30 +284,30 @@ class _GenerateRenderMethod(object): decorator = node.decorator if decorator: self.printer.writeline( - "@runtime._decorate_toplevel(%s)" % decorator) + "@runtime._decorate_toplevel(%s)" % decorator) self.printer.start_source(node.lineno) self.printer.writelines( "def %s(%s):" % (name, ','.join(args)), - # push new frame, assign current frame to __M_caller - "__M_caller = context.caller_stack._push_frame()", - "try:" + # push new frame, assign current frame to __M_caller + "__M_caller = context.caller_stack._push_frame()", + "try:" ) if buffered or filtered or cached: self.printer.writeline("context._push_buffer()") self.identifier_stack.append( - self.compiler.identifiers.branch(self.node)) + self.compiler.identifiers.branch(self.node)) if (not self.in_def or self.node.is_block) and '**pageargs' in args: self.identifier_stack[-1].argument_declared.add('pageargs') if not self.in_def and ( - len(self.identifiers.locally_assigned) > 0 or - len(self.identifiers.argument_declared) > 0 - ): + len(self.identifiers.locally_assigned) > 0 or + len(self.identifiers.argument_declared) > 0 + ): self.printer.writeline("__M_locals = __M_dict_builtin(%s)" % - ','.join([ - "%s=%s" % (x, x) for x in + ','.join([ + "%s=%s" % (x, x) for x in self.identifiers.argument_declared ])) @@ -311,9 +321,9 @@ class _GenerateRenderMethod(object): self.printer.write_blanks(2) if cached: self.write_cache_decorator( - node, name, - args, buffered, - self.identifiers, toplevel=True) + node, name, + args, buffered, + self.identifiers, toplevel=True) def write_module_code(self, module_code): """write module-level template code, i.e. that which @@ -327,26 +337,25 @@ class _GenerateRenderMethod(object): self.printer.writelines( "def _mako_inherit(template, context):", - "_mako_generate_namespaces(context)", - "return runtime._inherit_from(context, %s, _template_uri)" % - (node.parsed_attributes['file']), - None + "_mako_generate_namespaces(context)", + "return runtime._inherit_from(context, %s, _template_uri)" % + (node.parsed_attributes['file']), + None ) def write_namespaces(self, namespaces): """write the module-level namespace-generating callable.""" self.printer.writelines( "def _mako_get_namespace(context, name):", - "try:", - "return context.namespaces[(__name__, name)]", - "except KeyError:", - "_mako_generate_namespaces(context)", - "return context.namespaces[(__name__, name)]", + "try:", + "return context.namespaces[(__name__, name)]", + "except KeyError:", + "_mako_generate_namespaces(context)", + "return context.namespaces[(__name__, name)]", None, None ) self.printer.writeline("def _mako_generate_namespaces(context):") - for node in namespaces.values(): if 'import' in node.attributes: self.compiler.has_ns_imports = True @@ -356,7 +365,9 @@ class _GenerateRenderMethod(object): export = [] identifiers = self.compiler.identifiers.branch(node) self.in_def = True + class NSDefVisitor(object): + def visitDefTag(s, node): s.visitDefOrBase(node) @@ -384,39 +395,39 @@ class _GenerateRenderMethod(object): if 'file' in node.parsed_attributes: self.printer.writeline( - "ns = runtime.TemplateNamespace(%r," - " context._clean_inheritance_tokens()," - " templateuri=%s, callables=%s, " - " calling_uri=_template_uri)" % - ( - node.name, - node.parsed_attributes.get('file', 'None'), - callable_name, - ) - ) + "ns = runtime.TemplateNamespace(%r," + " context._clean_inheritance_tokens()," + " templateuri=%s, callables=%s, " + " calling_uri=_template_uri)" % + ( + node.name, + node.parsed_attributes.get('file', 'None'), + callable_name, + ) + ) elif 'module' in node.parsed_attributes: self.printer.writeline( - "ns = runtime.ModuleNamespace(%r," - " context._clean_inheritance_tokens()," - " callables=%s, calling_uri=_template_uri," - " module=%s)" % - ( - node.name, - callable_name, - node.parsed_attributes.get( - 'module', 'None') - ) - ) + "ns = runtime.ModuleNamespace(%r," + " context._clean_inheritance_tokens()," + " callables=%s, calling_uri=_template_uri," + " module=%s)" % + ( + node.name, + callable_name, + node.parsed_attributes.get( + 'module', 'None') + ) + ) else: self.printer.writeline( - "ns = runtime.Namespace(%r," - " context._clean_inheritance_tokens()," - " callables=%s, calling_uri=_template_uri)" % - ( - node.name, - callable_name, - ) - ) + "ns = runtime.Namespace(%r," + " context._clean_inheritance_tokens()," + " callables=%s, calling_uri=_template_uri)" % + ( + node.name, + callable_name, + ) + ) if eval(node.attributes.get('inheritable', "False")): self.printer.writeline("context['self'].%s = ns" % (node.name)) @@ -457,7 +468,7 @@ class _GenerateRenderMethod(object): # write closure functions for closures that we define # right here to_write = to_write.union( - [c.funcname for c in identifiers.closuredefs.values()]) + [c.funcname for c in identifiers.closuredefs.values()]) # remove identifiers that are declared in the argument # signature of the callable @@ -487,12 +498,12 @@ class _GenerateRenderMethod(object): for ident, ns in self.compiler.namespaces.items(): if 'import' in ns.attributes: self.printer.writeline( - "_mako_get_namespace(context, %r)." - "_populate(_import_ns, %r)" % - ( - ident, - re.split(r'\s*,\s*', ns.attributes['import']) - )) + "_mako_get_namespace(context, %r)." + "_populate(_import_ns, %r)" % + ( + ident, + re.split(r'\s*,\s*', ns.attributes['import']) + )) if has_loop: self.printer.writeline( @@ -515,35 +526,36 @@ class _GenerateRenderMethod(object): elif ident in self.compiler.namespaces: self.printer.writeline( - "%s = _mako_get_namespace(context, %r)" % - (ident, ident) - ) + "%s = _mako_get_namespace(context, %r)" % + (ident, ident) + ) else: if getattr(self.compiler, 'has_ns_imports', False): if self.compiler.strict_undefined: self.printer.writelines( - "%s = _import_ns.get(%r, UNDEFINED)" % - (ident, ident), - "if %s is UNDEFINED:" % ident, + "%s = _import_ns.get(%r, UNDEFINED)" % + (ident, ident), + "if %s is UNDEFINED:" % ident, "try:", - "%s = context[%r]" % (ident, ident), + "%s = context[%r]" % (ident, ident), "except KeyError:", - "raise NameError(\"'%s' is not defined\")" % - ident, + "raise NameError(\"'%s' is not defined\")" % + ident, None, None ) else: self.printer.writeline( - "%s = _import_ns.get(%r, context.get(%r, UNDEFINED))" % - (ident, ident, ident)) + "%s = _import_ns.get" + "(%r, context.get(%r, UNDEFINED))" % + (ident, ident, ident)) else: if self.compiler.strict_undefined: self.printer.writelines( "try:", - "%s = context[%r]" % (ident, ident), + "%s = context[%r]" % (ident, ident), "except KeyError:", - "raise NameError(\"'%s' is not defined\")" % - ident, + "raise NameError(\"'%s' is not defined\")" % + ident, None ) else: @@ -560,14 +572,14 @@ class _GenerateRenderMethod(object): nameargs = node.get_argument_expressions(as_call=True) if not self.in_def and ( - len(self.identifiers.locally_assigned) > 0 or - len(self.identifiers.argument_declared) > 0): + len(self.identifiers.locally_assigned) > 0 or + len(self.identifiers.argument_declared) > 0): nameargs.insert(0, 'context._locals(__M_locals)') else: nameargs.insert(0, 'context') self.printer.writeline("def %s(%s):" % (funcname, ",".join(namedecls))) self.printer.writeline( - "return render_%s(%s)" % (funcname, ",".join(nameargs))) + "return render_%s(%s)" % (funcname, ",".join(nameargs))) self.printer.writeline(None) def write_inline_def(self, node, identifiers, nested): @@ -578,9 +590,9 @@ class _GenerateRenderMethod(object): decorator = node.decorator if decorator: self.printer.writeline( - "@runtime._decorate_inline(context, %s)" % decorator) + "@runtime._decorate_inline(context, %s)" % decorator) self.printer.writeline( - "def %s(%s):" % (node.funcname, ",".join(namedecls))) + "def %s(%s):" % (node.funcname, ",".join(namedecls))) filtered = len(node.filter_args.args) > 0 buffered = eval(node.attributes.get('buffered', 'False')) cached = eval(node.attributes.get('cached', 'False')) @@ -607,11 +619,11 @@ class _GenerateRenderMethod(object): self.printer.writeline(None) if cached: self.write_cache_decorator(node, node.funcname, - namedecls, False, identifiers, - inline=True, toplevel=False) + namedecls, False, identifiers, + inline=True, toplevel=False) def write_def_finish(self, node, buffered, filtered, cached, - callstack=True): + callstack=True): """write the end section of a rendering function, either outermost or inline. @@ -625,7 +637,7 @@ class _GenerateRenderMethod(object): if callstack: self.printer.writelines( "finally:", - "context.caller_stack._pop_frame()", + "context.caller_stack._pop_frame()", None ) @@ -637,7 +649,7 @@ class _GenerateRenderMethod(object): # extra buffers self.printer.writelines( "finally:", - "__M_buf = context._pop_buffer()" + "__M_buf = context._pop_buffer()" ) else: self.printer.writelines( @@ -665,8 +677,8 @@ class _GenerateRenderMethod(object): ) def write_cache_decorator(self, node_or_pagetag, name, - args, buffered, identifiers, - inline=False, toplevel=False): + args, buffered, identifiers, + inline=False, toplevel=False): """write a post-function decorator to replace a rendering callable with a cached version of itself.""" @@ -698,40 +710,40 @@ class _GenerateRenderMethod(object): # form "arg1, arg2, arg3=arg3, arg4=arg4", etc. pass_args = [ - "%s=%s" % ((a.split('=')[0],) * 2) if '=' in a else a - for a in args - ] + "%s=%s" % ((a.split('=')[0],) * 2) if '=' in a else a + for a in args + ] self.write_variable_declares( - identifiers, - toplevel=toplevel, - limit=node_or_pagetag.undeclared_identifiers() - ) + identifiers, + toplevel=toplevel, + limit=node_or_pagetag.undeclared_identifiers() + ) if buffered: s = "context.get('local')."\ "cache._ctx_get_or_create("\ "%s, lambda:__M_%s(%s), context, %s__M_defname=%r)" % ( - cachekey, name, ','.join(pass_args), - ''.join(["%s=%s, " % (k, v) - for k, v in cache_args.items()]), - name - ) + cachekey, name, ','.join(pass_args), + ''.join(["%s=%s, " % (k, v) + for k, v in cache_args.items()]), + name + ) # apply buffer_filters s = self.create_filter_callable(self.compiler.buffer_filters, s, False) self.printer.writelines("return " + s, None) else: self.printer.writelines( - "__M_writer(context.get('local')." - "cache._ctx_get_or_create(" - "%s, lambda:__M_%s(%s), context, %s__M_defname=%r))" % - ( - cachekey, name, ','.join(pass_args), - ''.join(["%s=%s, " % (k, v) - for k, v in cache_args.items()]), - name, - ), - "return ''", + "__M_writer(context.get('local')." + "cache._ctx_get_or_create(" + "%s, lambda:__M_%s(%s), context, %s__M_defname=%r))" % + ( + cachekey, name, ','.join(pass_args), + ''.join(["%s=%s, " % (k, v) + for k, v in cache_args.items()]), + name, + ), + "return ''", None ) @@ -775,7 +787,7 @@ class _GenerateRenderMethod(object): ( self.compiler.pagetag is not None and len(self.compiler.pagetag.filter_args.args) - ) or \ + ) or \ len(self.compiler.default_filters): s = self.create_filter_callable(node.escapes_code.args, @@ -806,11 +818,11 @@ class _GenerateRenderMethod(object): # 3) any control line with no content other than comments if not children or ( compat.all(isinstance(c, (parsetree.Comment, - parsetree.ControlLine)) - for c in children) and + parsetree.ControlLine)) + for c in children) and compat.all((node.is_ternary(c.keyword) or c.isend) - for c in children - if isinstance(c, parsetree.ControlLine))): + for c in children + if isinstance(c, parsetree.ControlLine))): self.printer.writeline("pass") def visitText(self, node): @@ -832,9 +844,9 @@ class _GenerateRenderMethod(object): "__M_buf, __M_writer = context._pop_buffer_and_writer()", "__M_writer(%s)" % self.create_filter_callable( - node.filter_args.args, - "__M_buf.getvalue()", - False), + node.filter_args.args, + "__M_buf.getvalue()", + False), None ) @@ -861,12 +873,12 @@ class _GenerateRenderMethod(object): args = node.attributes.get('args') if args: self.printer.writeline( - "runtime._include_file(context, %s, _template_uri, %s)" % - (node.parsed_attributes['file'], args)) + "runtime._include_file(context, %s, _template_uri, %s)" % + (node.parsed_attributes['file'], args)) else: self.printer.writeline( - "runtime._include_file(context, %s, _template_uri)" % - (node.parsed_attributes['file'])) + "runtime._include_file(context, %s, _template_uri)" % + (node.parsed_attributes['file'])) def visitNamespaceTag(self, node): pass @@ -880,9 +892,10 @@ class _GenerateRenderMethod(object): else: nameargs = node.get_argument_expressions(as_call=True) nameargs += ['**pageargs'] - self.printer.writeline("if 'parent' not in context._data or " - "not hasattr(context._data['parent'], '%s'):" - % node.funcname) + self.printer.writeline( + "if 'parent' not in context._data or " + "not hasattr(context._data['parent'], '%s'):" + % node.funcname) self.printer.writeline( "context['self'].%s(%s)" % (node.funcname, ",".join(nameargs))) self.printer.writeline("\n") @@ -905,7 +918,9 @@ class _GenerateRenderMethod(object): body_identifiers.add_declared('caller') self.identifier_stack.append(body_identifiers) + class DefVisitor(object): + def visitDefTag(s, node): s.visitDefOrBase(node) @@ -954,19 +969,21 @@ class _GenerateRenderMethod(object): self.printer.writelines( # push on caller for nested call "context.caller_stack.nextcaller = " - "runtime.Namespace('caller', context, " - "callables=ccall(__M_caller))", + "runtime.Namespace('caller', context, " + "callables=ccall(__M_caller))", "try:") self.printer.start_source(node.lineno) self.printer.writelines( - "__M_writer(%s)" % self.create_filter_callable( - [], node.expression, True), + "__M_writer(%s)" % self.create_filter_callable( + [], node.expression, True), "finally:", - "context.caller_stack.nextcaller = None", + "context.caller_stack.nextcaller = None", None ) + class _Identifiers(object): + """tracks the status of identifier names as template code is rendered.""" def __init__(self, compiler, node=None, parent=None, nested=False): @@ -980,9 +997,9 @@ class _Identifiers(object): # things that have already been declared # in an enclosing namespace (i.e. names we can just use) self.declared = set(parent.declared).\ - union([c.name for c in parent.closuredefs.values()]).\ - union(parent.locally_declared).\ - union(parent.argument_declared) + union([c.name for c in parent.closuredefs.values()]).\ + union(parent.locally_declared).\ + union(parent.argument_declared) # if these identifiers correspond to a "nested" # scope, it means whatever the parent identifiers @@ -1026,13 +1043,12 @@ class _Identifiers(object): node.accept_visitor(self) illegal_names = self.compiler.reserved_names.intersection( - self.locally_declared) + self.locally_declared) if illegal_names: raise exceptions.NameConflictError( "Reserved words declared in template: %s" % ", ".join(illegal_names)) - def branch(self, node, **kwargs): """create a new Identifiers for a new Node, with this Identifiers as the parent.""" @@ -1045,15 +1061,15 @@ class _Identifiers(object): def __repr__(self): return "Identifiers(declared=%r, locally_declared=%r, "\ - "undeclared=%r, topleveldefs=%r, closuredefs=%r, "\ - "argumentdeclared=%r)" %\ - ( - list(self.declared), - list(self.locally_declared), - list(self.undeclared), - [c.name for c in self.topleveldefs.values()], - [c.name for c in self.closuredefs.values()], - self.argument_declared) + "undeclared=%r, topleveldefs=%r, closuredefs=%r, "\ + "argumentdeclared=%r)" %\ + ( + list(self.declared), + list(self.locally_declared), + list(self.undeclared), + [c.name for c in self.topleveldefs.values()], + [c.name for c in self.closuredefs.values()], + self.argument_declared) def check_declared(self, node): """update the state of this Identifiers with the undeclared @@ -1081,7 +1097,7 @@ class _Identifiers(object): if not node.ismodule: self.check_declared(node) self.locally_assigned = self.locally_assigned.union( - node.declared_identifiers()) + node.declared_identifiers()) def visitNamespaceTag(self, node): # only traverse into the sub-elements of a @@ -1095,12 +1111,12 @@ class _Identifiers(object): existing = collection.get(node.funcname) collection[node.funcname] = node if existing is not None and \ - existing is not node and \ - (node.is_block or existing.is_block): + existing is not node and \ + (node.is_block or existing.is_block): raise exceptions.CompileException( - "%%def or %%block named '%s' already " - "exists in this template." % - node.funcname, **node.exception_kwargs) + "%%def or %%block named '%s' already " + "exists in this template." % + node.funcname, **node.exception_kwargs) def visitDefTag(self, node): if node.is_root() and not node.is_anonymous: @@ -1126,13 +1142,13 @@ class _Identifiers(object): if isinstance(self.node, parsetree.DefTag): raise exceptions.CompileException( - "Named block '%s' not allowed inside of def '%s'" - % (node.name, self.node.name), **node.exception_kwargs) + "Named block '%s' not allowed inside of def '%s'" + % (node.name, self.node.name), **node.exception_kwargs) elif isinstance(self.node, (parsetree.CallTag, parsetree.CallNamespaceTag)): raise exceptions.CompileException( - "Named block '%s' not allowed inside of <%%call> tag" - % (node.name, ), **node.exception_kwargs) + "Named block '%s' not allowed inside of <%%call> tag" + % (node.name, ), **node.exception_kwargs) for ident in node.undeclared_identifiers(): if ident != 'context' and \ @@ -1171,7 +1187,7 @@ class _Identifiers(object): for ident in node.undeclared_identifiers(): if ident != 'context' and \ ident not in self.declared.union( - self.locally_declared): + self.locally_declared): self.undeclared.add(ident) for ident in node.declared_identifiers(): self.argument_declared.add(ident) @@ -1181,15 +1197,16 @@ class _Identifiers(object): for ident in node.undeclared_identifiers(): if ident != 'context' and \ ident not in self.declared.union( - self.locally_declared): + self.locally_declared): self.undeclared.add(ident) _FOR_LOOP = re.compile( - r'^for\s+((?:\(?)\s*[A-Za-z_][A-Za-z_0-9]*' - r'(?:\s*,\s*(?:[A-Za-z_][A-Za-z0-9_]*),??)*\s*(?:\)?))\s+in\s+(.*):' + r'^for\s+((?:\(?)\s*[A-Za-z_][A-Za-z_0-9]*' + r'(?:\s*,\s*(?:[A-Za-z_][A-Za-z0-9_]*),??)*\s*(?:\)?))\s+in\s+(.*):' ) + def mangle_mako_loop(node, printer): """converts a for loop into a context manager wrapped around a for loop when access to the `loop` variable has been detected in the for loop body @@ -1201,9 +1218,9 @@ def mangle_mako_loop(node, printer): match = _FOR_LOOP.match(node.text) if match: printer.writelines( - 'loop = __M_loop._enter(%s)' % match.group(2), - 'try:' - #'with __M_loop(%s) as loop:' % match.group(2) + 'loop = __M_loop._enter(%s)' % match.group(2), + 'try:' + # 'with __M_loop(%s) as loop:' % match.group(2) ) text = 'for %s in loop:' % match.group(1) else: @@ -1214,6 +1231,7 @@ def mangle_mako_loop(node, printer): class LoopVariable(object): + """A node visitor which looks for the name 'loop' within undeclared identifiers.""" diff --git a/mako/compat.py b/mako/compat.py index fe277bb..db22b99 100644 --- a/mako/compat.py +++ b/mako/compat.py @@ -10,6 +10,27 @@ win32 = sys.platform.startswith('win') pypy = hasattr(sys, 'pypy_version_info') if py3k: + # create a "getargspec" from getfullargspec(), which is not deprecated + # in Py3K; getargspec() has started to emit warnings as of Py3.5. + # As of Py3.4, now they are trying to move from getfullargspec() + # to "signature()", but getfullargspec() is not deprecated, so stick + # with that for now. + + import collections + ArgSpec = collections.namedtuple( + "ArgSpec", + ["args", "varargs", "keywords", "defaults"]) + from inspect import getfullargspec as inspect_getfullargspec + + def inspect_getargspec(func): + return ArgSpec( + *inspect_getfullargspec(func)[0:4] + ) +else: + from inspect import getargspec as inspect_getargspec # noqa + + +if py3k: from io import StringIO import builtins as compat_builtins from urllib.parse import quote_plus, unquote_plus @@ -30,7 +51,7 @@ if py3k: return eval("0o" + lit) else: - import __builtin__ as compat_builtins + import __builtin__ as compat_builtins # noqa try: from cStringIO import StringIO except: @@ -38,14 +59,14 @@ else: byte_buffer = StringIO - from urllib import quote_plus, unquote_plus - from htmlentitydefs import codepoint2name, name2codepoint - string_types = basestring, + from urllib import quote_plus, unquote_plus # noqa + from htmlentitydefs import codepoint2name, name2codepoint # noqa + string_types = basestring, # noqa binary_type = str - text_type = unicode + text_type = unicode # noqa def u(s): - return unicode(s, "utf-8") + return unicode(s, "utf-8") # noqa def b(s): return s @@ -56,10 +77,12 @@ else: if py33: from importlib import machinery + def load_module(module_id, path): return machinery.SourceFileLoader(module_id, path).load_module() else: import imp + def load_module(module_id, path): fp = open(path, 'rb') try: @@ -77,7 +100,7 @@ if py3k: raise value else: exec("def reraise(tp, value, tb=None, cause=None):\n" - " raise tp, value, tb\n") + " raise tp, value, tb\n") def exception_as(): @@ -90,11 +113,11 @@ try: else: import thread except ImportError: - import dummy_threading as threading + import dummy_threading as threading # noqa if py3k: import _dummy_thread as thread else: - import dummy_thread as thread + import dummy_thread as thread # noqa if win32 or jython: time_func = time.clock @@ -113,13 +136,15 @@ except: all = all -import json +import json # noqa + def exception_name(exc): return exc.__class__.__name__ try: from inspect import CO_VARKEYWORDS, CO_VARARGS + def inspect_func_args(fn): if py3k: co = fn.__code__ @@ -144,6 +169,7 @@ try: return args, varargs, varkw, fn.func_defaults except ImportError: import inspect + def inspect_func_args(fn): return inspect.getargspec(fn) diff --git a/mako/exceptions.py b/mako/exceptions.py index c531f21..84d2297 100644 --- a/mako/exceptions.py +++ b/mako/exceptions.py @@ -10,12 +10,15 @@ import traceback import sys from mako import util, compat + class MakoException(Exception): pass + class RuntimeException(MakoException): pass + def _format_filepos(lineno, pos, filename): if filename is None: return " at line: %d char: %d" % (lineno, pos) @@ -24,42 +27,56 @@ def _format_filepos(lineno, pos, filename): class CompileException(MakoException): + def __init__(self, message, source, lineno, pos, filename): - MakoException.__init__(self, - message + _format_filepos(lineno, pos, filename)) + MakoException.__init__( + self, + message + _format_filepos(lineno, pos, filename)) self.lineno = lineno self.pos = pos self.filename = filename self.source = source + class SyntaxException(MakoException): + def __init__(self, message, source, lineno, pos, filename): - MakoException.__init__(self, - message + _format_filepos(lineno, pos, filename)) + MakoException.__init__( + self, + message + _format_filepos(lineno, pos, filename)) self.lineno = lineno self.pos = pos self.filename = filename self.source = source + class UnsupportedError(MakoException): + """raised when a retired feature is used.""" + class NameConflictError(MakoException): + """raised when a reserved word is used inappropriately""" + class TemplateLookupException(MakoException): pass + class TopLevelLookupException(TemplateLookupException): pass + class RichTraceback(object): + """Pull the current exception from the ``sys`` traceback and extracts Mako-specific template information. See the usage examples in :ref:`handling_exceptions`. """ + def __init__(self, error=None, traceback=None): self.source, self.lineno = "", 0 @@ -162,18 +179,18 @@ class RichTraceback(object): else: line = line.decode('ascii', 'replace') new_trcback.append((filename, lineno, function, line, - None, None, None, None)) + None, None, None, None)) continue template_ln = 1 source_map = mako.template.ModuleInfo.\ - get_module_source_metadata( - module_source, full_line_map=True) + get_module_source_metadata( + module_source, full_line_map=True) line_map = source_map['full_line_map'] - template_lines = [line for line in - template_source.split("\n")] + template_lines = [line_ for line_ in + template_source.split("\n")] mods[filename] = (line_map, template_lines) template_ln = line_map[lineno - 1] @@ -235,16 +252,19 @@ ${tback.errorname}: ${tback.message} def _install_pygments(): global syntax_highlight, pygments_html_formatter - from mako.ext.pygmentplugin import syntax_highlight,\ - pygments_html_formatter + from mako.ext.pygmentplugin import syntax_highlight # noqa + from mako.ext.pygmentplugin import pygments_html_formatter # noqa + def _install_fallback(): global syntax_highlight, pygments_html_formatter from mako.filters import html_escape pygments_html_formatter = None + def syntax_highlight(filename='', language=None): return html_escape + def _install_highlighting(): try: _install_pygments() @@ -252,6 +272,7 @@ def _install_highlighting(): _install_fallback() _install_highlighting() + def html_error_template(): """Provides a template that renders a stack trace in an HTML format, providing an excerpt of code as well as substituting source template @@ -370,4 +391,4 @@ def html_error_template(): </html> % endif """, output_encoding=sys.getdefaultencoding(), - encoding_errors='htmlentityreplace') + encoding_errors='htmlentityreplace') diff --git a/mako/ext/autohandler.py b/mako/ext/autohandler.py index 8deaae1..9ee780a 100644 --- a/mako/ext/autohandler.py +++ b/mako/ext/autohandler.py @@ -25,7 +25,10 @@ or with custom autohandler filename: """ -import posixpath, os, re +import posixpath +import os +import re + def autohandler(template, context, name='autohandler'): lookup = context.lookup @@ -42,24 +45,24 @@ def autohandler(template, context, name='autohandler'): if path != _template_uri and _file_exists(lookup, path): if not lookup.filesystem_checks: return lookup._uri_cache.setdefault( - (autohandler, _template_uri, name), path) + (autohandler, _template_uri, name), path) else: return path if len(tokens) == 1: break tokens[-2:] = [name] - + if not lookup.filesystem_checks: return lookup._uri_cache.setdefault( - (autohandler, _template_uri, name), None) + (autohandler, _template_uri, name), None) else: return None + def _file_exists(lookup, path): - psub = re.sub(r'^/', '',path) + psub = re.sub(r'^/', '', path) for d in lookup.directories: if os.path.exists(d + '/' + psub): return True else: return False - diff --git a/mako/ext/babelplugin.py b/mako/ext/babelplugin.py index ead7081..53d62ba 100644 --- a/mako/ext/babelplugin.py +++ b/mako/ext/babelplugin.py @@ -10,14 +10,15 @@ from mako.ext.extract import MessageExtractor class BabelMakoExtractor(MessageExtractor): + def __init__(self, keywords, comment_tags, options): self.keywords = keywords self.options = options self.config = { - 'comment-tags': u' '.join(comment_tags), - 'encoding': options.get('input_encoding', - options.get('encoding', None)), - } + 'comment-tags': u' '.join(comment_tags), + 'encoding': options.get('input_encoding', + options.get('encoding', None)), + } super(BabelMakoExtractor, self).__init__() def __call__(self, fileobj): @@ -27,7 +28,7 @@ class BabelMakoExtractor(MessageExtractor): comment_tags = self.config['comment-tags'] for lineno, funcname, messages, python_translator_comments \ in extract_python(code, - self.keywords, comment_tags, self.options): + self.keywords, comment_tags, self.options): yield (code_lineno + (lineno - 1), funcname, messages, translator_strings + python_translator_comments) diff --git a/mako/ext/beaker_cache.py b/mako/ext/beaker_cache.py index 40ef774..c7c260d 100644 --- a/mako/ext/beaker_cache.py +++ b/mako/ext/beaker_cache.py @@ -15,6 +15,7 @@ _beaker_cache = None class BeakerCacheImpl(CacheImpl): + """A :class:`.CacheImpl` provided for the Beaker caching system. This plugin is used by default, based on the default diff --git a/mako/ext/extract.py b/mako/ext/extract.py index 5ce5175..8dd2e96 100644 --- a/mako/ext/extract.py +++ b/mako/ext/extract.py @@ -5,6 +5,7 @@ from mako import parsetree class MessageExtractor(object): + def process_file(self, fileobj): template_node = lexer.Lexer( fileobj.read(), @@ -15,6 +16,7 @@ class MessageExtractor(object): def extract_nodes(self, nodes): translator_comments = [] in_translator_comments = False + input_encoding = self.config['encoding'] or 'ascii' comment_tags = list( filter(None, re.split(r'\s+', self.config['comment-tags']))) @@ -75,13 +77,18 @@ class MessageExtractor(object): comment[1] for comment in translator_comments] if isinstance(code, compat.text_type): - code = code.encode('ascii', 'backslashreplace') + code = code.encode(input_encoding, 'backslashreplace') used_translator_comments = False - code = compat.byte_buffer(code) + # We add extra newline to work around a pybabel bug + # (see python-babel/babel#274, parse_encoding dies if the first + # input string of the input is non-ascii) + # Also, because we added it, we have to subtract one from + # node.lineno + code = compat.byte_buffer(compat.b('\n') + code) for message in self.process_python( - code, node.lineno, translator_strings): + code, node.lineno - 1, translator_strings): yield message used_translator_comments = True diff --git a/mako/ext/linguaplugin.py b/mako/ext/linguaplugin.py index a809072..46b0d6a 100644 --- a/mako/ext/linguaplugin.py +++ b/mako/ext/linguaplugin.py @@ -7,6 +7,7 @@ from mako import compat class LinguaMakoExtractor(Extractor, MessageExtractor): + '''Mako templates''' extensions = ['.mako'] default_config = { @@ -25,10 +26,14 @@ class LinguaMakoExtractor(Extractor, MessageExtractor): def process_python(self, code, code_lineno, translator_strings): source = code.getvalue().strip() if source.endswith(compat.b(':')): - source += compat.b(' pass') - code = io.BytesIO(source) + if source in (compat.b('try:'), compat.b('else:')) or source.startswith(compat.b('except')): + source = compat.b('') # Ignore try/except and else + elif source.startswith(compat.b('elif')): + source = source[2:] # Replace "elif" with "if" + source += compat.b('pass') + code = io.BytesIO(source) for msg in self.python_extractor( - self.filename, self.options, code, code_lineno): + self.filename, self.options, code, code_lineno -1): if translator_strings: msg = Message(msg.msgctxt, msg.msgid, msg.msgid_plural, msg.flags, diff --git a/mako/ext/preprocessors.py b/mako/ext/preprocessors.py index c24893b..5624f70 100644 --- a/mako/ext/preprocessors.py +++ b/mako/ext/preprocessors.py @@ -4,17 +4,17 @@ # This module is part of Mako and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""preprocessing functions, used with the 'preprocessor' +"""preprocessing functions, used with the 'preprocessor' argument on Template, TemplateLookup""" import re + def convert_comments(text): """preprocess old style comments. - + example: - + from mako.ext.preprocessors import convert_comments t = Template(..., preprocessor=convert_comments)""" return re.sub(r'(?<=\n)\s*#[^#]', "##", text) - diff --git a/mako/ext/pygmentplugin.py b/mako/ext/pygmentplugin.py index 3adcfb8..1121c5d 100644 --- a/mako/ext/pygmentplugin.py +++ b/mako/ext/pygmentplugin.py @@ -5,16 +5,17 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php from pygments.lexers.web import \ - HtmlLexer, XmlLexer, JavascriptLexer, CssLexer + HtmlLexer, XmlLexer, JavascriptLexer, CssLexer from pygments.lexers.agile import PythonLexer, Python3Lexer from pygments.lexer import DelegatingLexer, RegexLexer, bygroups, \ - include, using + include, using from pygments.token import \ - Text, Comment, Operator, Keyword, Name, String, Other + Text, Comment, Operator, Keyword, Name, String, Other from pygments.formatters.html import HtmlFormatter from pygments import highlight from mako import compat + class MakoLexer(RegexLexer): name = 'Mako' aliases = ['mako'] @@ -27,15 +28,15 @@ class MakoLexer(RegexLexer): (r'(\s*)(\%(?!%))([^\n]*)(\n|\Z)', bygroups(Text, Comment.Preproc, using(PythonLexer), Other)), (r'(\s*)(##[^\n]*)(\n|\Z)', - bygroups(Text, Comment.Preproc, Other)), + bygroups(Text, Comment.Preproc, Other)), (r'''(?s)<%doc>.*?</%doc>''', Comment.Preproc), (r'(<%)([\w\.\:]+)', - bygroups(Comment.Preproc, Name.Builtin), 'tag'), + bygroups(Comment.Preproc, Name.Builtin), 'tag'), (r'(</%)([\w\.\:]+)(>)', - bygroups(Comment.Preproc, Name.Builtin, Comment.Preproc)), + bygroups(Comment.Preproc, Name.Builtin, Comment.Preproc)), (r'<%(?=([\w\.\:]+))', Comment.Preproc, 'ondeftags'), (r'(<%(?:!?))(.*?)(%>)(?s)', - bygroups(Comment.Preproc, using(PythonLexer), Comment.Preproc)), + bygroups(Comment.Preproc, using(PythonLexer), Comment.Preproc)), (r'(\$\{)(.*?)(\})', bygroups(Comment.Preproc, using(PythonLexer), Comment.Preproc)), (r'''(?sx) @@ -79,7 +80,8 @@ class MakoHtmlLexer(DelegatingLexer): def __init__(self, **options): super(MakoHtmlLexer, self).__init__(HtmlLexer, MakoLexer, - **options) + **options) + class MakoXmlLexer(DelegatingLexer): name = 'XML+Mako' @@ -87,7 +89,8 @@ class MakoXmlLexer(DelegatingLexer): def __init__(self, **options): super(MakoXmlLexer, self).__init__(XmlLexer, MakoLexer, - **options) + **options) + class MakoJavascriptLexer(DelegatingLexer): name = 'JavaScript+Mako' @@ -95,7 +98,8 @@ class MakoJavascriptLexer(DelegatingLexer): def __init__(self, **options): super(MakoJavascriptLexer, self).__init__(JavascriptLexer, - MakoLexer, **options) + MakoLexer, **options) + class MakoCssLexer(DelegatingLexer): name = 'CSS+Mako' @@ -103,11 +107,13 @@ class MakoCssLexer(DelegatingLexer): def __init__(self, **options): super(MakoCssLexer, self).__init__(CssLexer, MakoLexer, - **options) + **options) pygments_html_formatter = HtmlFormatter(cssclass='syntax-highlighted', linenos=True) + + def syntax_highlight(filename='', language=None): mako_lexer = MakoLexer() if compat.py3k: @@ -119,4 +125,3 @@ def syntax_highlight(filename='', language=None): pygments_html_formatter) return lambda string: highlight(string, python_lexer, pygments_html_formatter) - diff --git a/mako/ext/turbogears.py b/mako/ext/turbogears.py index d3976d9..2e7d039 100644 --- a/mako/ext/turbogears.py +++ b/mako/ext/turbogears.py @@ -4,12 +4,13 @@ # This module is part of Mako and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -import inspect from mako import compat from mako.lookup import TemplateLookup from mako.template import Template + class TGPlugin(object): + """TurboGears compatible Template Plugin.""" def __init__(self, extra_vars_func=None, options=None, extension='mak'): @@ -30,7 +31,7 @@ class TGPlugin(object): self.tmpl_options = {} # transfer lookup args to template args, based on those available # in getargspec - for kw in inspect.getargspec(Template.__init__)[0]: + for kw in compat.inspect_getargspec(Template.__init__)[0]: if kw in lookup_options: self.tmpl_options[kw] = lookup_options[kw] @@ -41,7 +42,7 @@ class TGPlugin(object): # Translate TG dot notation to normal / template path if '/' not in templatename: templatename = '/' + templatename.replace('.', '/') + '.' +\ - self.extension + self.extension # Lookup template return self.lookup.get_template(templatename) @@ -55,4 +56,3 @@ class TGPlugin(object): info.update(self.extra_vars_func()) return template.render(**info) - diff --git a/mako/filters.py b/mako/filters.py index d79ce23..525aeb8 100644 --- a/mako/filters.py +++ b/mako/filters.py @@ -9,7 +9,7 @@ import re import codecs from mako.compat import quote_plus, unquote_plus, codepoint2name, \ - name2codepoint + name2codepoint from mako import compat @@ -24,6 +24,7 @@ xml_escapes = { # XXX: " is valid in HTML and XML # ' is not valid HTML, but is valid XML + def legacy_html_escape(s): """legacy HTML escape for non-unicode mode.""" s = s.replace("&", "&") @@ -40,29 +41,35 @@ try: except ImportError: html_escape = legacy_html_escape + def xml_escape(string): return re.sub(r'([&<"\'>])', lambda m: xml_escapes[m.group()], string) + def url_escape(string): # convert into a list of octets string = string.encode("utf8") return quote_plus(string) + def legacy_url_escape(string): # convert into a list of octets return quote_plus(string) + def url_unescape(string): text = unquote_plus(string) if not is_ascii_str(text): text = text.decode("utf8") return text + def trim(string): return string.strip() class Decode(object): + def __getattr__(self, key): def decode(x): if isinstance(x, compat.text_type): @@ -77,12 +84,15 @@ decode = Decode() _ASCII_re = re.compile(r'\A[\x00-\x7f]*\Z') + def is_ascii_str(text): return isinstance(text, str) and _ASCII_re.match(text) ################################################################ + class XMLEntityEscaper(object): + def __init__(self, codepoint2name, name2codepoint): self.codepoint2entity = dict([(c, compat.text_type('&%s;' % n)) for c, n in codepoint2name.items()]) @@ -102,7 +112,6 @@ class XMLEntityEscaper(object): except (KeyError, IndexError): return '&#x%X;' % codepoint - __escapable = re.compile(r'["&<>]|[^\x00-\x7f]') def escape(self, text): @@ -198,4 +207,3 @@ if compat.py3k: NON_UNICODE_ESCAPES = DEFAULT_ESCAPES.copy() NON_UNICODE_ESCAPES['h'] = 'filters.legacy_html_escape' NON_UNICODE_ESCAPES['u'] = 'filters.legacy_url_escape' - diff --git a/mako/lexer.py b/mako/lexer.py index 1dda398..2fa08e4 100644 --- a/mako/lexer.py +++ b/mako/lexer.py @@ -13,10 +13,12 @@ from mako.pygen import adjust_whitespace _regexp_cache = {} + class Lexer(object): + def __init__(self, text, filename=None, - disable_unicode=False, - input_encoding=None, preprocessor=None): + disable_unicode=False, + input_encoding=None, preprocessor=None): self.text = text self.filename = filename self.template = parsetree.TemplateNode(self.filename) @@ -32,8 +34,8 @@ class Lexer(object): if compat.py3k and disable_unicode: raise exceptions.UnsupportedError( - "Mako for Python 3 does not " - "support disabling Unicode") + "Mako for Python 3 does not " + "support disabling Unicode") if preprocessor is None: self.preprocessor = [] @@ -87,9 +89,9 @@ class Lexer(object): cp -= 1 self.matched_charpos = mp - cp self.lineno += len(lines) - #print "MATCHED:", match.group(0), "LINE START:", + # print "MATCHED:", match.group(0), "LINE START:", # self.matched_lineno, "LINE END:", self.lineno - #print "MATCH:", regexp, "\n", self.text[mp : mp + 15], \ + # print "MATCH:", regexp, "\n", self.text[mp : mp + 15], \ # (match and "TRUE" or "FALSE") return match @@ -120,9 +122,9 @@ class Lexer(object): brace_level -= match.group(1).count('}') continue raise exceptions.SyntaxException( - "Expected: %s" % - ','.join(text), - **self.exception_kwargs) + "Expected: %s" % + ','.join(text), + **self.exception_kwargs) def append_node(self, nodecls, *args, **kwargs): kwargs.setdefault('source', self.text) @@ -162,9 +164,9 @@ class Lexer(object): elif self.control_line and \ not self.control_line[-1].is_ternary(node.keyword): raise exceptions.SyntaxException( - "Keyword '%s' not a legal ternary for keyword '%s'" % - (node.keyword, self.control_line[-1].keyword), - **self.exception_kwargs) + "Keyword '%s' not a legal ternary for keyword '%s'" % + (node.keyword, self.control_line[-1].keyword), + **self.exception_kwargs) _coding_re = re.compile(r'#.*coding[:=]\s*([-\w.]+).*\r?\n') @@ -185,10 +187,10 @@ class Lexer(object): m = self._coding_re.match(text.decode('utf-8', 'ignore')) if m is not None and m.group(1) != 'utf-8': raise exceptions.CompileException( - "Found utf-8 BOM in file, with conflicting " - "magic encoding comment of '%s'" % m.group(1), - text.decode('utf-8', 'ignore'), - 0, 0, filename) + "Found utf-8 BOM in file, with conflicting " + "magic encoding comment of '%s'" % m.group(1), + text.decode('utf-8', 'ignore'), + 0, 0, filename) else: m = self._coding_re.match(text.decode('utf-8', 'ignore')) if m: @@ -201,18 +203,19 @@ class Lexer(object): text = text.decode(parsed_encoding) except UnicodeDecodeError: raise exceptions.CompileException( - "Unicode decode operation of encoding '%s' failed" % - parsed_encoding, - text.decode('utf-8', 'ignore'), - 0, 0, filename) + "Unicode decode operation of encoding '%s' failed" % + parsed_encoding, + text.decode('utf-8', 'ignore'), + 0, 0, filename) return parsed_encoding, text def parse(self): - self.encoding, self.text = self.decode_raw_stream(self.text, - not self.disable_unicode, - self.encoding, - self.filename,) + self.encoding, self.text = self.decode_raw_stream( + self.text, + not self.disable_unicode, + self.encoding, + self.filename) for preproc in self.preprocessor: self.text = preproc(self.text) @@ -250,15 +253,15 @@ class Lexer(object): if len(self.tag): raise exceptions.SyntaxException("Unclosed tag: <%%%s>" % - self.tag[-1].keyword, - **self.exception_kwargs) + self.tag[-1].keyword, + **self.exception_kwargs) if len(self.control_line): raise exceptions.SyntaxException( - "Unterminated control keyword: '%s'" % - self.control_line[-1].keyword, - self.text, - self.control_line[-1].lineno, - self.control_line[-1].pos, self.filename) + "Unterminated control keyword: '%s'" % + self.control_line[-1].keyword, + self.text, + self.control_line[-1].lineno, + self.control_line[-1].pos, self.filename) return self.template def match_tag_start(self): @@ -276,7 +279,7 @@ class Lexer(object): ''', - re.I | re.S | re.X) + re.I | re.S | re.X) if match: keyword, attr, isend = match.groups() @@ -284,7 +287,7 @@ class Lexer(object): attributes = {} if attr: for att in re.findall( - r"\s*(\w+)\s*=\s*(?:'([^']*)'|\"([^\"]*)\")", attr): + r"\s*(\w+)\s*=\s*(?:'([^']*)'|\"([^\"]*)\")", attr): key, val1, val2 = att text = val1 or val2 text = text.replace('\r\n', '\n') @@ -294,12 +297,12 @@ class Lexer(object): self.tag.pop() else: if keyword == 'text': - match = self.match(r'(.*?)(?=\</%text>)', re.S) + match = self.match(r'(.*?)(?=\</%text>)', re.S) if not match: raise exceptions.SyntaxException( - "Unclosed tag: <%%%s>" % - self.tag[-1].keyword, - **self.exception_kwargs) + "Unclosed tag: <%%%s>" % + self.tag[-1].keyword, + **self.exception_kwargs) self.append_node(parsetree.Text, match.group(1)) return self.match_tag_end() return True @@ -311,14 +314,14 @@ class Lexer(object): if match: if not len(self.tag): raise exceptions.SyntaxException( - "Closing tag without opening tag: </%%%s>" % - match.group(1), - **self.exception_kwargs) + "Closing tag without opening tag: </%%%s>" % + match.group(1), + **self.exception_kwargs) elif self.tag[-1].keyword != match.group(1): raise exceptions.SyntaxException( - "Closing tag </%%%s> does not match tag: <%%%s>" % - (match.group(1), self.tag[-1].keyword), - **self.exception_kwargs) + "Closing tag </%%%s> does not match tag: <%%%s>" % + (match.group(1), self.tag[-1].keyword), + **self.exception_kwargs) self.tag.pop() return True else: @@ -370,9 +373,9 @@ class Lexer(object): # compiler.parse() not complain about indentation text = adjust_whitespace(text) + "\n" self.append_node( - parsetree.Code, - text, - match.group(1) == '!', lineno=line, pos=pos) + parsetree.Code, + text, + match.group(1) == '!', lineno=line, pos=pos) return True else: return False @@ -388,17 +391,17 @@ class Lexer(object): escapes = "" text = text.replace('\r\n', '\n') self.append_node( - parsetree.Expression, - text, escapes.strip(), - lineno=line, pos=pos) + parsetree.Expression, + text, escapes.strip(), + lineno=line, pos=pos) return True else: return False def match_control_line(self): match = self.match( - r"(?<=^)[\t ]*(%(?!%)|##)[\t ]*((?:(?:\\r?\n)|[^\r\n])*)" - r"(?:\r?\n|\Z)", re.M) + r"(?<=^)[\t ]*(%(?!%)|##)[\t ]*((?:(?:\\r?\n)|[^\r\n])*)" + r"(?:\r?\n|\Z)", re.M) if match: operator = match.group(1) text = match.group(2) @@ -406,23 +409,23 @@ class Lexer(object): m2 = re.match(r'(end)?(\w+)\s*(.*)', text) if not m2: raise exceptions.SyntaxException( - "Invalid control line: '%s'" % - text, - **self.exception_kwargs) + "Invalid control line: '%s'" % + text, + **self.exception_kwargs) isend, keyword = m2.group(1, 2) isend = (isend is not None) if isend: if not len(self.control_line): raise exceptions.SyntaxException( - "No starting keyword '%s' for '%s'" % - (keyword, text), - **self.exception_kwargs) + "No starting keyword '%s' for '%s'" % + (keyword, text), + **self.exception_kwargs) elif self.control_line[-1].keyword != keyword: raise exceptions.SyntaxException( - "Keyword '%s' doesn't match keyword '%s'" % - (text, self.control_line[-1].keyword), - **self.exception_kwargs) + "Keyword '%s' doesn't match keyword '%s'" % + (text, self.control_line[-1].keyword), + **self.exception_kwargs) self.append_node(parsetree.ControlLine, keyword, isend, text) else: self.append_node(parsetree.Comment, text) @@ -438,4 +441,3 @@ class Lexer(object): return True else: return False - diff --git a/mako/lookup.py b/mako/lookup.py index 2af5411..e6dff9d 100644 --- a/mako/lookup.py +++ b/mako/lookup.py @@ -4,7 +4,10 @@ # This module is part of Mako and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -import os, stat, posixpath, re +import os +import stat +import posixpath +import re from mako import exceptions, util from mako.template import Template @@ -13,7 +16,9 @@ try: except: import dummy_threading as threading + class TemplateCollection(object): + """Represent a collection of :class:`.Template` objects, identifiable via URI. @@ -79,7 +84,9 @@ class TemplateCollection(object): """ return uri + class TemplateLookup(TemplateCollection): + """Represent a collection of templates that locates template source files from the local filesystem. @@ -145,35 +152,35 @@ class TemplateLookup(TemplateCollection): """ def __init__(self, - directories=None, - module_directory=None, - filesystem_checks=True, - collection_size=-1, - format_exceptions=False, - error_handler=None, - disable_unicode=False, - bytestring_passthrough=False, - output_encoding=None, - encoding_errors='strict', - - cache_args=None, - cache_impl='beaker', - cache_enabled=True, - cache_type=None, - cache_dir=None, - cache_url=None, - - modulename_callable=None, - module_writer=None, - default_filters=None, - buffer_filters=(), - strict_undefined=False, - imports=None, - future_imports=None, - enable_loop=True, - input_encoding=None, - preprocessor=None, - lexer_cls=None): + directories=None, + module_directory=None, + filesystem_checks=True, + collection_size=-1, + format_exceptions=False, + error_handler=None, + disable_unicode=False, + bytestring_passthrough=False, + output_encoding=None, + encoding_errors='strict', + + cache_args=None, + cache_impl='beaker', + cache_enabled=True, + cache_type=None, + cache_dir=None, + cache_url=None, + + modulename_callable=None, + module_writer=None, + default_filters=None, + buffer_filters=(), + strict_undefined=False, + imports=None, + future_imports=None, + enable_loop=True, + input_encoding=None, + preprocessor=None, + lexer_cls=None): self.directories = [posixpath.normpath(d) for d in util.to_list(directories, ()) @@ -194,26 +201,26 @@ class TemplateLookup(TemplateCollection): cache_args.setdefault('type', cache_type) self.template_args = { - 'format_exceptions':format_exceptions, - 'error_handler':error_handler, - 'disable_unicode':disable_unicode, - 'bytestring_passthrough':bytestring_passthrough, - 'output_encoding':output_encoding, - 'cache_impl':cache_impl, - 'encoding_errors':encoding_errors, - 'input_encoding':input_encoding, - 'module_directory':module_directory, - 'module_writer':module_writer, - 'cache_args':cache_args, - 'cache_enabled':cache_enabled, - 'default_filters':default_filters, - 'buffer_filters':buffer_filters, - 'strict_undefined':strict_undefined, - 'imports':imports, - 'future_imports':future_imports, - 'enable_loop':enable_loop, - 'preprocessor':preprocessor, - 'lexer_cls':lexer_cls + 'format_exceptions': format_exceptions, + 'error_handler': error_handler, + 'disable_unicode': disable_unicode, + 'bytestring_passthrough': bytestring_passthrough, + 'output_encoding': output_encoding, + 'cache_impl': cache_impl, + 'encoding_errors': encoding_errors, + 'input_encoding': input_encoding, + 'module_directory': module_directory, + 'module_writer': module_writer, + 'cache_args': cache_args, + 'cache_enabled': cache_enabled, + 'default_filters': default_filters, + 'buffer_filters': buffer_filters, + 'strict_undefined': strict_undefined, + 'imports': imports, + 'future_imports': future_imports, + 'enable_loop': enable_loop, + 'preprocessor': preprocessor, + 'lexer_cls': lexer_cls } if collection_size == -1: @@ -228,7 +235,8 @@ class TemplateLookup(TemplateCollection): """Return a :class:`.Template` object corresponding to the given ``uri``. - .. note:: The ``relativeto`` argument is not supported here at the moment. + .. note:: The ``relativeto`` argument is not supported here at + the moment. """ @@ -240,12 +248,15 @@ class TemplateLookup(TemplateCollection): except KeyError: u = re.sub(r'^\/+', '', uri) for dir in self.directories: + # make sure the path seperators are posix - os.altsep is empty + # on POSIX and cannot be used. + dir = dir.replace(os.path.sep, posixpath.sep) srcfile = posixpath.normpath(posixpath.join(dir, u)) if os.path.isfile(srcfile): return self._load(srcfile, uri) else: raise exceptions.TopLevelLookupException( - "Cant locate template for uri %r" % uri) + "Cant locate template for uri %r" % uri) def adjust_uri(self, uri, relativeto): """Adjust the given ``uri`` based on the given relative URI.""" @@ -257,14 +268,13 @@ class TemplateLookup(TemplateCollection): if uri[0] != '/': if relativeto is not None: v = self._uri_cache[key] = posixpath.join( - posixpath.dirname(relativeto), uri) + posixpath.dirname(relativeto), uri) else: v = self._uri_cache[key] = '/' + uri else: v = self._uri_cache[key] = uri return v - def filename_to_uri(self, filename): """Convert the given ``filename`` to a URI relative to this :class:`.TemplateCollection`.""" @@ -304,11 +314,11 @@ class TemplateLookup(TemplateCollection): else: module_filename = None self._collection[uri] = template = Template( - uri=uri, - filename=posixpath.normpath(filename), - lookup=self, - module_filename=module_filename, - **self.template_args) + uri=uri, + filename=posixpath.normpath(filename), + lookup=self, + module_filename=module_filename, + **self.template_args) return template except: # if compilation fails etc, ensure @@ -326,7 +336,7 @@ class TemplateLookup(TemplateCollection): try: template_stat = os.stat(template.filename) if template.module._modified_time < \ - template_stat[stat.ST_MTIME]: + template_stat[stat.ST_MTIME]: self._collection.pop(uri, None) return self._load(template.filename, uri) else: @@ -334,8 +344,7 @@ class TemplateLookup(TemplateCollection): except OSError: self._collection.pop(uri, None) raise exceptions.TemplateLookupException( - "Cant locate template for uri %r" % uri) - + "Cant locate template for uri %r" % uri) def put_string(self, uri, text): """Place a new :class:`.Template` object into this @@ -344,10 +353,10 @@ class TemplateLookup(TemplateCollection): """ self._collection[uri] = Template( - text, - lookup=self, - uri=uri, - **self.template_args) + text, + lookup=self, + uri=uri, + **self.template_args) def put_template(self, uri, template): """Place a new :class:`.Template` object into this @@ -356,4 +365,3 @@ class TemplateLookup(TemplateCollection): """ self._collection[uri] = template - diff --git a/mako/parsetree.py b/mako/parsetree.py index 49ec4e0..e7af4bc 100644 --- a/mako/parsetree.py +++ b/mako/parsetree.py @@ -9,7 +9,9 @@ from mako import exceptions, ast, util, filters, compat import re + class Node(object): + """base class for a Node in the parse tree.""" def __init__(self, source, lineno, pos, filename): @@ -34,7 +36,9 @@ class Node(object): method = getattr(visitor, "visit" + self.__class__.__name__, traverse) method(self) + class TemplateNode(Node): + """a 'container' node that stores the overall collection of nodes.""" def __init__(self, filename): @@ -47,10 +51,12 @@ class TemplateNode(Node): def __repr__(self): return "TemplateNode(%s, %r)" % ( - util.sorted_dict_repr(self.page_attributes), - self.nodes) + util.sorted_dict_repr(self.page_attributes), + self.nodes) + class ControlLine(Node): + """defines a control line, a line-oriented python line or end tag. e.g.:: @@ -92,9 +98,9 @@ class ControlLine(Node): for this ControlLine""" return keyword in { - 'if':set(['else', 'elif']), - 'try':set(['except', 'finally']), - 'for':set(['else']) + 'if': set(['else', 'elif']), + 'try': set(['except', 'finally']), + 'for': set(['else']) }.get(self.keyword, []) def __repr__(self): @@ -105,7 +111,9 @@ class ControlLine(Node): (self.lineno, self.pos) ) + class Text(Node): + """defines plain text in the template.""" def __init__(self, content, **kwargs): @@ -115,7 +123,9 @@ class Text(Node): def __repr__(self): return "Text(%r, %r)" % (self.content, (self.lineno, self.pos)) + class Code(Node): + """defines a Python code block, either inline or module level. e.g.:: @@ -151,7 +161,9 @@ class Code(Node): (self.lineno, self.pos) ) + class Comment(Node): + """defines a comment line. # this is a comment @@ -165,7 +177,9 @@ class Comment(Node): def __repr__(self): return "Comment(%r, %r)" % (self.text, (self.lineno, self.pos)) + class Expression(Node): + """defines an inline expression. ${x+y} @@ -185,10 +199,10 @@ class Expression(Node): def undeclared_identifiers(self): # TODO: make the "filter" shortcut list configurable at parse/gen time return self.code.undeclared_identifiers.union( - self.escapes_code.undeclared_identifiers.difference( - set(filters.DEFAULT_ESCAPES.keys()) - ) - ).difference(self.code.declared_identifiers) + self.escapes_code.undeclared_identifiers.difference( + set(filters.DEFAULT_ESCAPES.keys()) + ) + ).difference(self.code.declared_identifiers) def __repr__(self): return "Expression(%r, %r, %r)" % ( @@ -197,7 +211,9 @@ class Expression(Node): (self.lineno, self.pos) ) + class _TagMeta(type): + """metaclass to allow Tag to produce a subclass according to its keyword""" @@ -212,7 +228,7 @@ class _TagMeta(type): if ":" in keyword: ns, defname = keyword.split(':') return type.__call__(CallNamespaceTag, ns, defname, - attributes, **kwargs) + attributes, **kwargs) try: cls = _TagMeta._classmap[keyword] @@ -226,7 +242,9 @@ class _TagMeta(type): ) return type.__call__(cls, keyword, attributes, **kwargs) + class Tag(compat.with_metaclass(_TagMeta, Node)): + """abstract base class for tags. <%sometag/> @@ -239,7 +257,7 @@ class Tag(compat.with_metaclass(_TagMeta, Node)): __keyword__ = None def __init__(self, keyword, attributes, expressions, - nonexpressions, required, **kwargs): + nonexpressions, required, **kwargs): """construct a new Tag instance. this constructor not called directly, and is only called @@ -267,7 +285,7 @@ class Tag(compat.with_metaclass(_TagMeta, Node)): if len(missing): raise exceptions.CompileException( "Missing attribute(s): %s" % - ",".join([repr(m) for m in missing]), + ",".join([repr(m) for m in missing]), **self.exception_kwargs) self.parent = None self.nodes = [] @@ -289,14 +307,14 @@ class Tag(compat.with_metaclass(_TagMeta, Node)): m = re.compile(r'^\${(.+?)}$', re.S).match(x) if m: code = ast.PythonCode(m.group(1).rstrip(), - **self.exception_kwargs) + **self.exception_kwargs) # we aren't discarding "declared_identifiers" here, # which we do so that list comprehension-declared # variables aren't counted. As yet can't find a # condition that requires it here. undeclared_identifiers = \ undeclared_identifiers.union( - code.undeclared_identifiers) + code.undeclared_identifiers) expr.append('(%s)' % m.group(1)) else: if x: @@ -305,15 +323,15 @@ class Tag(compat.with_metaclass(_TagMeta, Node)): elif key in nonexpressions: if re.search(r'\${.+?}', self.attributes[key]): raise exceptions.CompileException( - "Attibute '%s' in tag '%s' does not allow embedded " - "expressions" % (key, self.keyword), - **self.exception_kwargs) + "Attibute '%s' in tag '%s' does not allow embedded " + "expressions" % (key, self.keyword), + **self.exception_kwargs) self.parsed_attributes[key] = repr(self.attributes[key]) else: raise exceptions.CompileException( - "Invalid attribute for tag '%s': '%s'" % - (self.keyword, key), - **self.exception_kwargs) + "Invalid attribute for tag '%s': '%s'" % + (self.keyword, key), + **self.exception_kwargs) self.expression_undeclared_identifiers = undeclared_identifiers def declared_identifiers(self): @@ -324,48 +342,50 @@ class Tag(compat.with_metaclass(_TagMeta, Node)): def __repr__(self): return "%s(%r, %s, %r, %r)" % (self.__class__.__name__, - self.keyword, - util.sorted_dict_repr(self.attributes), - (self.lineno, self.pos), - self.nodes - ) + self.keyword, + util.sorted_dict_repr(self.attributes), + (self.lineno, self.pos), + self.nodes + ) + class IncludeTag(Tag): __keyword__ = 'include' def __init__(self, keyword, attributes, **kwargs): super(IncludeTag, self).__init__( - keyword, - attributes, - ('file', 'import', 'args'), - (), ('file',), **kwargs) + keyword, + attributes, + ('file', 'import', 'args'), + (), ('file',), **kwargs) self.page_args = ast.PythonCode( - "__DUMMY(%s)" % attributes.get('args', ''), - **self.exception_kwargs) + "__DUMMY(%s)" % attributes.get('args', ''), + **self.exception_kwargs) def declared_identifiers(self): return [] def undeclared_identifiers(self): identifiers = self.page_args.undeclared_identifiers.\ - difference(set(["__DUMMY"])).\ - difference(self.page_args.declared_identifiers) + difference(set(["__DUMMY"])).\ + difference(self.page_args.declared_identifiers) return identifiers.union(super(IncludeTag, self). - undeclared_identifiers()) + undeclared_identifiers()) + class NamespaceTag(Tag): __keyword__ = 'namespace' def __init__(self, keyword, attributes, **kwargs): super(NamespaceTag, self).__init__( - keyword, attributes, - ('file',), - ('name','inheritable', - 'import','module'), - (), **kwargs) + keyword, attributes, + ('file',), + ('name', 'inheritable', + 'import', 'module'), + (), **kwargs) self.name = attributes.get('name', '__anon_%s' % hex(abs(id(self)))) - if not 'name' in attributes and not 'import' in attributes: + if 'name' not in attributes and 'import' not in attributes: raise exceptions.CompileException( "'name' and/or 'import' attributes are required " "for <%namespace>", @@ -379,52 +399,53 @@ class NamespaceTag(Tag): def declared_identifiers(self): return [] + class TextTag(Tag): __keyword__ = 'text' def __init__(self, keyword, attributes, **kwargs): super(TextTag, self).__init__( - keyword, - attributes, (), - ('filter'), (), **kwargs) + keyword, + attributes, (), + ('filter'), (), **kwargs) self.filter_args = ast.ArgumentList( - attributes.get('filter', ''), - **self.exception_kwargs) + attributes.get('filter', ''), + **self.exception_kwargs) def undeclared_identifiers(self): return self.filter_args.\ - undeclared_identifiers.\ - difference(filters.DEFAULT_ESCAPES.keys()).union( - self.expression_undeclared_identifiers - ) + undeclared_identifiers.\ + difference(filters.DEFAULT_ESCAPES.keys()).union( + self.expression_undeclared_identifiers + ) + class DefTag(Tag): __keyword__ = 'def' def __init__(self, keyword, attributes, **kwargs): expressions = ['buffered', 'cached'] + [ - c for c in attributes if c.startswith('cache_')] - + c for c in attributes if c.startswith('cache_')] super(DefTag, self).__init__( - keyword, - attributes, - expressions, - ('name', 'filter', 'decorator'), - ('name',), - **kwargs) + keyword, + attributes, + expressions, + ('name', 'filter', 'decorator'), + ('name',), + **kwargs) name = attributes['name'] if re.match(r'^[\w_]+$', name): raise exceptions.CompileException( - "Missing parenthesis in %def", - **self.exception_kwargs) + "Missing parenthesis in %def", + **self.exception_kwargs) self.function_decl = ast.FunctionDecl("def " + name + ":pass", - **self.exception_kwargs) + **self.exception_kwargs) self.name = self.function_decl.funcname self.decorator = attributes.get('decorator', '') self.filter_args = ast.ArgumentList( - attributes.get('filter', ''), - **self.exception_kwargs) + attributes.get('filter', ''), + **self.exception_kwargs) is_anonymous = False is_block = False @@ -443,50 +464,50 @@ class DefTag(Tag): res = [] for c in self.function_decl.defaults: res += list(ast.PythonCode(c, **self.exception_kwargs). - undeclared_identifiers) + undeclared_identifiers) return set(res).union( - self.filter_args.\ - undeclared_identifiers.\ - difference(filters.DEFAULT_ESCAPES.keys()) + self.filter_args. + undeclared_identifiers. + difference(filters.DEFAULT_ESCAPES.keys()) ).union( self.expression_undeclared_identifiers ).difference( self.function_decl.allargnames ) + class BlockTag(Tag): __keyword__ = 'block' def __init__(self, keyword, attributes, **kwargs): expressions = ['buffered', 'cached', 'args'] + [ - c for c in attributes if c.startswith('cache_')] + c for c in attributes if c.startswith('cache_')] super(BlockTag, self).__init__( - keyword, - attributes, - expressions, - ('name','filter', 'decorator'), - (), - **kwargs) + keyword, + attributes, + expressions, + ('name', 'filter', 'decorator'), + (), + **kwargs) name = attributes.get('name') - if name and not re.match(r'^[\w_]+$',name): + if name and not re.match(r'^[\w_]+$', name): raise exceptions.CompileException( - "%block may not specify an argument signature", - **self.exception_kwargs) + "%block may not specify an argument signature", + **self.exception_kwargs) if not name and attributes.get('args', None): raise exceptions.CompileException( - "Only named %blocks may specify args", - **self.exception_kwargs - ) + "Only named %blocks may specify args", + **self.exception_kwargs + ) self.body_decl = ast.FunctionArgs(attributes.get('args', ''), - **self.exception_kwargs) + **self.exception_kwargs) self.name = name self.decorator = attributes.get('decorator', '') self.filter_args = ast.ArgumentList( - attributes.get('filter', ''), - **self.exception_kwargs) - + attributes.get('filter', ''), + **self.exception_kwargs) is_block = True @@ -505,90 +526,91 @@ class BlockTag(Tag): return self.body_decl.allargnames def undeclared_identifiers(self): - return (self.filter_args.\ - undeclared_identifiers.\ - difference(filters.DEFAULT_ESCAPES.keys()) + return (self.filter_args. + undeclared_identifiers. + difference(filters.DEFAULT_ESCAPES.keys()) ).union(self.expression_undeclared_identifiers) - class CallTag(Tag): __keyword__ = 'call' def __init__(self, keyword, attributes, **kwargs): super(CallTag, self).__init__(keyword, attributes, - ('args'), ('expr',), ('expr',), **kwargs) + ('args'), ('expr',), ('expr',), **kwargs) self.expression = attributes['expr'] self.code = ast.PythonCode(self.expression, **self.exception_kwargs) self.body_decl = ast.FunctionArgs(attributes.get('args', ''), - **self.exception_kwargs) + **self.exception_kwargs) def declared_identifiers(self): return self.code.declared_identifiers.union(self.body_decl.allargnames) def undeclared_identifiers(self): return self.code.undeclared_identifiers.\ - difference(self.code.declared_identifiers) + difference(self.code.declared_identifiers) + class CallNamespaceTag(Tag): def __init__(self, namespace, defname, attributes, **kwargs): super(CallNamespaceTag, self).__init__( - namespace + ":" + defname, - attributes, - tuple(attributes.keys()) + ('args', ), - (), - (), - **kwargs) + namespace + ":" + defname, + attributes, + tuple(attributes.keys()) + ('args', ), + (), + (), + **kwargs) self.expression = "%s.%s(%s)" % ( - namespace, - defname, - ",".join(["%s=%s" % (k, v) for k, v in - self.parsed_attributes.items() - if k != 'args']) - ) + namespace, + defname, + ",".join(["%s=%s" % (k, v) for k, v in + self.parsed_attributes.items() + if k != 'args']) + ) self.code = ast.PythonCode(self.expression, **self.exception_kwargs) self.body_decl = ast.FunctionArgs( - attributes.get('args', ''), - **self.exception_kwargs) + attributes.get('args', ''), + **self.exception_kwargs) def declared_identifiers(self): return self.code.declared_identifiers.union(self.body_decl.allargnames) def undeclared_identifiers(self): return self.code.undeclared_identifiers.\ - difference(self.code.declared_identifiers) + difference(self.code.declared_identifiers) + class InheritTag(Tag): __keyword__ = 'inherit' def __init__(self, keyword, attributes, **kwargs): super(InheritTag, self).__init__( - keyword, attributes, - ('file',), (), ('file',), **kwargs) + keyword, attributes, + ('file',), (), ('file',), **kwargs) + class PageTag(Tag): __keyword__ = 'page' def __init__(self, keyword, attributes, **kwargs): - expressions = ['cached', 'args', 'expression_filter', 'enable_loop'] + [ - c for c in attributes if c.startswith('cache_')] + expressions = \ + ['cached', 'args', 'expression_filter', 'enable_loop'] + \ + [c for c in attributes if c.startswith('cache_')] super(PageTag, self).__init__( - keyword, - attributes, - expressions, - (), - (), - **kwargs) + keyword, + attributes, + expressions, + (), + (), + **kwargs) self.body_decl = ast.FunctionArgs(attributes.get('args', ''), - **self.exception_kwargs) + **self.exception_kwargs) self.filter_args = ast.ArgumentList( - attributes.get('expression_filter', ''), - **self.exception_kwargs) + attributes.get('expression_filter', ''), + **self.exception_kwargs) def declared_identifiers(self): return self.body_decl.allargnames - - diff --git a/mako/pygen.py b/mako/pygen.py index 5ba5125..5d87bbd 100644 --- a/mako/pygen.py +++ b/mako/pygen.py @@ -9,7 +9,9 @@ import re from mako import exceptions + class PythonPrinter(object): + def __init__(self, stream): # indentation counter self.indent = 0 @@ -80,10 +82,11 @@ class PythonPrinter(object): self._flush_adjusted_lines() self.in_indent_lines = True - if (line is None or - re.match(r"^\s*#",line) or + if ( + line is None or + re.match(r"^\s*#", line) or re.match(r"^\s*$", line) - ): + ): hastext = False else: hastext = True @@ -91,9 +94,10 @@ class PythonPrinter(object): is_comment = line and len(line) and line[0] == '#' # see if this line should decrease the indentation level - if (not is_comment and + if ( + not is_comment and (not hastext or self._is_unindentor(line)) - ): + ): if self.indent > 0: self.indent -= 1 @@ -102,7 +106,7 @@ class PythonPrinter(object): # module wont compile. if len(self.indent_detail) == 0: raise exceptions.SyntaxException( - "Too many whitespace closures") + "Too many whitespace closures") self.indent_detail.pop() if line is None: @@ -172,18 +176,18 @@ class PythonPrinter(object): # should we decide that its not good enough, heres # more stuff to check. - #keyword = match.group(1) + # keyword = match.group(1) # match the original indent keyword - #for crit in [ + # for crit in [ # (r'if|elif', r'else|elif'), # (r'try', r'except|finally|else'), # (r'while|for', r'else'), - #]: + # ]: # if re.match(crit[0], indentor) and re.match(crit[1], keyword): # return True - #return False + # return False def _indent_line(self, line, stripspace=''): """indent the given line according to the current indent level. diff --git a/mako/pyparser.py b/mako/pyparser.py index bfa46a9..96e5335 100644 --- a/mako/pyparser.py +++ b/mako/pyparser.py @@ -41,11 +41,11 @@ def parse(code, mode='exec', **exception_kwargs): return _ast_util.parse(code, '<unknown>', mode) except Exception: raise exceptions.SyntaxException( - "(%s) %s (%r)" % ( - compat.exception_as().__class__.__name__, - compat.exception_as(), - code[0:50] - ), **exception_kwargs) + "(%s) %s (%r)" % ( + compat.exception_as().__class__.__name__, + compat.exception_as(), + code[0:50] + ), **exception_kwargs) class FindIdentifiers(_ast_util.NodeVisitor): @@ -186,10 +186,10 @@ class FindTuple(_ast_util.NodeVisitor): self.listener.args.append(ExpressionGenerator(n).value()) self.listener.declared_identifiers = \ self.listener.declared_identifiers.union( - p.declared_identifiers) + p.declared_identifiers) self.listener.undeclared_identifiers = \ self.listener.undeclared_identifiers.union( - p.undeclared_identifiers) + p.undeclared_identifiers) class ParseFunc(_ast_util.NodeVisitor): @@ -222,6 +222,7 @@ class ParseFunc(_ast_util.NodeVisitor): self.listener.varargs = node.args.vararg self.listener.kwargs = node.args.kwarg + class ExpressionGenerator(object): def __init__(self, astnode): diff --git a/mako/runtime.py b/mako/runtime.py index 6b6a35a..8d2f4a9 100644 --- a/mako/runtime.py +++ b/mako/runtime.py @@ -13,6 +13,7 @@ import sys class Context(object): + """Provides runtime namespace, output buffer, and various callstacks for templates. @@ -80,7 +81,6 @@ class Context(object): """Push a ``caller`` callable onto the callstack for this :class:`.Context`.""" - self.caller_stack.append(caller) def pop_caller(self): @@ -182,7 +182,9 @@ class Context(object): x.pop('next', None) return c + class CallerStack(list): + def __init__(self): self.nextcaller = None @@ -211,6 +213,7 @@ class CallerStack(list): class Undefined(object): + """Represents an undefined value in a template. All template modules have a constant value @@ -218,6 +221,7 @@ class Undefined(object): object. """ + def __str__(self): raise NameError("Undefined") @@ -228,8 +232,11 @@ class Undefined(object): return False UNDEFINED = Undefined() +STOP_RENDERING = "" + class LoopStack(object): + """a stack for LoopContexts that implements the context manager protocol to automatically pop off the top of the stack on context exit """ @@ -269,6 +276,7 @@ class LoopStack(object): class LoopContext(object): + """A magic loop variable. Automatically accessible in any ``% for`` block. @@ -334,8 +342,10 @@ class LoopContext(object): class _NSAttr(object): + def __init__(self, parent): self.__parent = parent + def __getattr__(self, key): ns = self.__parent while ns: @@ -345,7 +355,9 @@ class _NSAttr(object): ns = ns.inherits raise AttributeError(key) + class Namespace(object): + """Provides access to collections of rendering methods, which can be local, from other templates, or from imported modules. @@ -362,8 +374,8 @@ class Namespace(object): """ def __init__(self, name, context, - callables=None, inherits=None, - populate_self=True, calling_uri=None): + callables=None, inherits=None, + populate_self=True, calling_uri=None): self.name = name self.context = context self.inherits = inherits @@ -462,8 +474,8 @@ class Namespace(object): return self.context.namespaces[key] else: ns = TemplateNamespace(uri, self.context._copy(), - templateuri=uri, - calling_uri=self._templateuri) + templateuri=uri, + calling_uri=self._templateuri) self.context.namespaces[key] = ns return ns @@ -524,17 +536,19 @@ class Namespace(object): val = getattr(self.inherits, key) else: raise AttributeError( - "Namespace '%s' has no member '%s'" % - (self.name, key)) + "Namespace '%s' has no member '%s'" % + (self.name, key)) setattr(self, key, val) return val + class TemplateNamespace(Namespace): + """A :class:`.Namespace` specific to a :class:`.Template` instance.""" def __init__(self, name, context, template=None, templateuri=None, - callables=None, inherits=None, - populate_self=True, calling_uri=None): + callables=None, inherits=None, + populate_self=True, calling_uri=None): self.name = name self.context = context self.inherits = inherits @@ -543,7 +557,7 @@ class TemplateNamespace(Namespace): if templateuri is not None: self.template = _lookup_template(context, templateuri, - calling_uri) + calling_uri) self._templateuri = self.template.module._template_uri elif template is not None: self.template = template @@ -553,8 +567,8 @@ class TemplateNamespace(Namespace): if populate_self: lclcallable, lclcontext = \ - _populate_self_namespace(context, self.template, - self_ns=self) + _populate_self_namespace(context, self.template, + self_ns=self) @property def module(self): @@ -589,6 +603,7 @@ class TemplateNamespace(Namespace): if self.callables: for key in self.callables: yield (key, self.callables[key]) + def get(key): callable_ = self.template._get_def_callable(key) return compat.partial(callable_, self.context) @@ -606,17 +621,19 @@ class TemplateNamespace(Namespace): else: raise AttributeError( - "Namespace '%s' has no member '%s'" % - (self.name, key)) + "Namespace '%s' has no member '%s'" % + (self.name, key)) setattr(self, key, val) return val + class ModuleNamespace(Namespace): + """A :class:`.Namespace` specific to a Python module instance.""" def __init__(self, name, context, module, - callables=None, inherits=None, - populate_self=True, calling_uri=None): + callables=None, inherits=None, + populate_self=True, calling_uri=None): self.name = name self.context = context self.inherits = inherits @@ -645,7 +662,6 @@ class ModuleNamespace(Namespace): if compat.callable(callable_): yield key, compat.partial(callable_, self.context) - def __getattr__(self, key): if key in self.callables: val = self.callables[key] @@ -656,11 +672,12 @@ class ModuleNamespace(Namespace): val = getattr(self.inherits, key) else: raise AttributeError( - "Namespace '%s' has no member '%s'" % - (self.name, key)) + "Namespace '%s' has no member '%s'" % + (self.name, key)) setattr(self, key, val) return val + def supports_caller(func): """Apply a caller_stack compatibility decorator to a plain Python function. @@ -677,6 +694,7 @@ def supports_caller(func): context.caller_stack._pop_frame() return wrap_stackframe + def capture(context, callable_, *args, **kwargs): """Execute the given template def, capturing the output into a buffer. @@ -687,9 +705,9 @@ def capture(context, callable_, *args, **kwargs): if not compat.callable(callable_): raise exceptions.RuntimeException( - "capture() function expects a callable as " - "its argument (i.e. capture(func, *args, **kwargs))" - ) + "capture() function expects a callable as " + "its argument (i.e. capture(func, *args, **kwargs))" + ) context._push_buffer() try: callable_(*args, **kwargs) @@ -697,6 +715,7 @@ def capture(context, callable_, *args, **kwargs): buf = context._pop_buffer() return buf.getvalue() + def _decorate_toplevel(fn): def decorate_render(render_fn): def go(context, *args, **kw): @@ -711,24 +730,28 @@ def _decorate_toplevel(fn): return go return decorate_render + def _decorate_inline(context, fn): def decorate_render(render_fn): dec = fn(render_fn) + def go(*args, **kw): return dec(context, *args, **kw) return go return decorate_render + def _include_file(context, uri, calling_uri, **kwargs): """locate the template from the given uri and include it in the current output.""" template = _lookup_template(context, uri, calling_uri) (callable_, ctx) = _populate_self_namespace( - context._clean_inheritance_tokens(), - template) + context._clean_inheritance_tokens(), + template) callable_(ctx, **_kwargs_for_include(callable_, context._data, **kwargs)) + def _inherit_from(context, uri, calling_uri): """called by the _inherit method in template modules to set up the inheritance chain at the start of a template's @@ -743,9 +766,9 @@ def _inherit_from(context, uri, calling_uri): ih = ih.inherits lclcontext = context._locals({'next': ih}) ih.inherits = TemplateNamespace("self:%s" % template.uri, - lclcontext, - template=template, - populate_self=False) + lclcontext, + template=template, + populate_self=False) context._data['parent'] = lclcontext._data['local'] = ih.inherits callable_ = getattr(template.module, '_mako_inherit', None) if callable_ is not None: @@ -758,23 +781,25 @@ def _inherit_from(context, uri, calling_uri): gen_ns(context) return (template.callable_, lclcontext) + def _lookup_template(context, uri, relativeto): lookup = context._with_template.lookup if lookup is None: raise exceptions.TemplateLookupException( - "Template '%s' has no TemplateLookup associated" % - context._with_template.uri) + "Template '%s' has no TemplateLookup associated" % + context._with_template.uri) uri = lookup.adjust_uri(uri, relativeto) try: return lookup.get_template(uri) except exceptions.TopLevelLookupException: raise exceptions.TemplateLookupException(str(compat.exception_as())) + def _populate_self_namespace(context, template, self_ns=None): if self_ns is None: self_ns = TemplateNamespace('self:%s' % template.uri, - context, template=template, - populate_self=False) + context, template=template, + populate_self=False) context._data['self'] = context._data['local'] = self_ns if hasattr(template.module, '_mako_inherit'): ret = template.module._mako_inherit(template, context) @@ -782,6 +807,7 @@ def _populate_self_namespace(context, template, self_ns=None): return ret return (template.callable_, context) + def _render(template, callable_, args, data, as_unicode=False): """create a Context and return the string output of the given template and template callable.""" @@ -792,17 +818,18 @@ def _render(template, callable_, args, data, as_unicode=False): buf = compat.StringIO() else: buf = util.FastEncodingBuffer( - as_unicode=as_unicode, - encoding=template.output_encoding, - errors=template.encoding_errors) + as_unicode=as_unicode, + encoding=template.output_encoding, + errors=template.encoding_errors) context = Context(buf, **data) context._outputting_as_unicode = as_unicode context._set_with_template(template) _render_context(template, callable_, context, *args, - **_kwargs_for_callable(callable_, data)) + **_kwargs_for_callable(callable_, data)) return context._pop_buffer().getvalue() + def _kwargs_for_callable(callable_, data): argspec = compat.inspect_func_args(callable_) # for normal pages, **pageargs is usually present @@ -817,6 +844,7 @@ def _kwargs_for_callable(callable_, data): kwargs[arg] = data[arg] return kwargs + def _kwargs_for_include(callable_, data, **kwargs): argspec = compat.inspect_func_args(callable_) namedargs = argspec[0] + [v for v in argspec[1:3] if v is not None] @@ -825,6 +853,7 @@ def _kwargs_for_include(callable_, data, **kwargs): kwargs[arg] = data[arg] return kwargs + def _render_context(tmpl, callable_, context, *args, **kwargs): import mako.template as template # create polymorphic 'self' namespace for this @@ -838,6 +867,7 @@ def _render_context(tmpl, callable_, context, *args, **kwargs): (inherit, lclcontext) = _populate_self_namespace(context, tmpl.parent) _exec_template(callable_, context, args=args, kwargs=kwargs) + def _exec_template(callable_, context, args=None, kwargs=None): """execute a rendering callable given the callable, a Context, and optional explicit arguments @@ -859,6 +889,7 @@ def _exec_template(callable_, context, args=None, kwargs=None): else: callable_(context, *args, **kwargs) + def _render_error(template, context, error): if template.error_handler: result = template.error_handler(context, error) @@ -868,11 +899,11 @@ def _render_error(template, context, error): error_template = exceptions.html_error_template() if context._outputting_as_unicode: context._buffer_stack[:] = [ - util.FastEncodingBuffer(as_unicode=True)] + util.FastEncodingBuffer(as_unicode=True)] else: context._buffer_stack[:] = [util.FastEncodingBuffer( - error_template.output_encoding, - error_template.encoding_errors)] + error_template.output_encoding, + error_template.encoding_errors)] context._set_with_template(error_template) error_template.render_context(context, error=error) diff --git a/mako/template.py b/mako/template.py index 2adf0f6..8fd6062 100644 --- a/mako/template.py +++ b/mako/template.py @@ -20,6 +20,7 @@ import weakref class Template(object): + """Represents a compiled template. :class:`.Template` includes a reference to the original @@ -215,34 +216,34 @@ class Template(object): lexer_cls = Lexer def __init__(self, - text=None, - filename=None, - uri=None, - format_exceptions=False, - error_handler=None, - lookup=None, - output_encoding=None, - encoding_errors='strict', - module_directory=None, - cache_args=None, - cache_impl='beaker', - cache_enabled=True, - cache_type=None, - cache_dir=None, - cache_url=None, - module_filename=None, - input_encoding=None, - disable_unicode=False, - module_writer=None, - bytestring_passthrough=False, - default_filters=None, - buffer_filters=(), - strict_undefined=False, - imports=None, - future_imports=None, - enable_loop=True, - preprocessor=None, - lexer_cls=None): + text=None, + filename=None, + uri=None, + format_exceptions=False, + error_handler=None, + lookup=None, + output_encoding=None, + encoding_errors='strict', + module_directory=None, + cache_args=None, + cache_impl='beaker', + cache_enabled=True, + cache_type=None, + cache_dir=None, + cache_url=None, + module_filename=None, + input_encoding=None, + disable_unicode=False, + module_writer=None, + bytestring_passthrough=False, + default_filters=None, + buffer_filters=(), + strict_undefined=False, + imports=None, + future_imports=None, + enable_loop=True, + preprocessor=None, + lexer_cls=None): if uri: self.module_id = re.sub(r'\W', "_", uri) self.uri = uri @@ -261,9 +262,9 @@ class Template(object): u_norm = os.path.normpath(u_norm) if u_norm.startswith(".."): raise exceptions.TemplateLookupException( - "Template uri \"%s\" is invalid - " - "it cannot be relative outside " - "of the root path." % self.uri) + "Template uri \"%s\" is invalid - " + "it cannot be relative outside " + "of the root path." % self.uri) self.input_encoding = input_encoding self.output_encoding = output_encoding @@ -276,12 +277,12 @@ class Template(object): if compat.py3k and disable_unicode: raise exceptions.UnsupportedError( - "Mako for Python 3 does not " - "support disabling Unicode") + "Mako for Python 3 does not " + "support disabling Unicode") elif output_encoding and disable_unicode: raise exceptions.UnsupportedError( - "output_encoding must be set to " - "None when disable_unicode is used.") + "output_encoding must be set to " + "None when disable_unicode is used.") if default_filters is None: if compat.py3k or self.disable_unicode: self.default_filters = ['str'] @@ -311,17 +312,17 @@ class Template(object): path = module_filename elif module_directory is not None: path = os.path.abspath( - os.path.join( - os.path.normpath(module_directory), - u_norm + ".py" - ) - ) + os.path.join( + os.path.normpath(module_directory), + u_norm + ".py" + ) + ) else: path = None module = self._compile_from_file(path, filename) else: raise exceptions.RuntimeException( - "Template requires text or filename") + "Template requires text or filename") self.module = module self.filename = filename @@ -337,7 +338,6 @@ class Template(object): cache_type, cache_dir, cache_url ) - @util.memoized_property def reserved_names(self): if self.enable_loop: @@ -346,8 +346,8 @@ class Template(object): return codegen.RESERVED_NAMES.difference(['loop']) def _setup_cache_args(self, - cache_impl, cache_enabled, cache_args, - cache_type, cache_dir, cache_url): + cache_impl, cache_enabled, cache_args, + cache_type, cache_dir, cache_url): self.cache_impl = cache_impl self.cache_enabled = cache_enabled if cache_args: @@ -368,24 +368,24 @@ class Template(object): util.verify_directory(os.path.dirname(path)) filemtime = os.stat(filename)[stat.ST_MTIME] if not os.path.exists(path) or \ - os.stat(path)[stat.ST_MTIME] < filemtime: + os.stat(path)[stat.ST_MTIME] < filemtime: data = util.read_file(filename) _compile_module_file( - self, - data, - filename, - path, - self.module_writer) + self, + data, + filename, + path, + self.module_writer) module = compat.load_module(self.module_id, path) del sys.modules[self.module_id] if module._magic_number != codegen.MAGIC_NUMBER: data = util.read_file(filename) _compile_module_file( - self, - data, - filename, - path, - self.module_writer) + self, + data, + filename, + path, + self.module_writer) module = compat.load_module(self.module_id, path) del sys.modules[self.module_id] ModuleInfo(module, path, self, filename, None, None) @@ -394,9 +394,9 @@ class Template(object): # in memory data = util.read_file(filename) code, module = _compile_text( - self, - data, - filename) + self, + data, + filename) self._source = None self._code = code ModuleInfo(module, None, self, filename, code, None) @@ -421,9 +421,11 @@ class Template(object): @property def cache_dir(self): return self.cache_args['dir'] + @property def cache_url(self): return self.cache_args['url'] + @property def cache_type(self): return self.cache_args['type'] @@ -446,10 +448,10 @@ class Template(object): """Render the output of this template as a unicode object.""" return runtime._render(self, - self.callable_, - args, - data, - as_unicode=True) + self.callable_, + args, + data, + as_unicode=True) def render_context(self, context, *args, **kwargs): """Render this :class:`.Template` with the given context. @@ -484,7 +486,9 @@ class Template(object): def last_modified(self): return self.module._modified_time + class ModuleTemplate(Template): + """A Template which is constructed given an existing Python module. e.g.:: @@ -502,25 +506,25 @@ class ModuleTemplate(Template): """ def __init__(self, module, - module_filename=None, - template=None, - template_filename=None, - module_source=None, - template_source=None, - output_encoding=None, - encoding_errors='strict', - disable_unicode=False, - bytestring_passthrough=False, - format_exceptions=False, - error_handler=None, - lookup=None, - cache_args=None, - cache_impl='beaker', - cache_enabled=True, - cache_type=None, - cache_dir=None, - cache_url=None, - ): + module_filename=None, + template=None, + template_filename=None, + module_source=None, + template_source=None, + output_encoding=None, + encoding_errors='strict', + disable_unicode=False, + bytestring_passthrough=False, + format_exceptions=False, + error_handler=None, + lookup=None, + cache_args=None, + cache_impl='beaker', + cache_enabled=True, + cache_type=None, + cache_dir=None, + cache_url=None, + ): self.module_id = re.sub(r'\W', "_", module._template_uri) self.uri = module._template_uri self.input_encoding = module._source_encoding @@ -532,21 +536,21 @@ class ModuleTemplate(Template): if compat.py3k and disable_unicode: raise exceptions.UnsupportedError( - "Mako for Python 3 does not " - "support disabling Unicode") + "Mako for Python 3 does not " + "support disabling Unicode") elif output_encoding and disable_unicode: raise exceptions.UnsupportedError( - "output_encoding must be set to " - "None when disable_unicode is used.") + "output_encoding must be set to " + "None when disable_unicode is used.") self.module = module self.filename = template_filename ModuleInfo(module, - module_filename, - self, - template_filename, - module_source, - template_source) + module_filename, + self, + template_filename, + module_source, + template_source) self.callable_ = self.module.render_body self.format_exceptions = format_exceptions @@ -557,7 +561,9 @@ class ModuleTemplate(Template): cache_type, cache_dir, cache_url ) + class DefTemplate(Template): + """A :class:`.Template` which represents a callable def in a parent template.""" @@ -576,7 +582,9 @@ class DefTemplate(Template): def get_def(self, name): return self.parent.get_def(name) + class ModuleInfo(object): + """Stores information about a module currently loaded into memory, provides reverse lookups of template source, module source code based on a module's identifier. @@ -585,12 +593,12 @@ class ModuleInfo(object): _modules = weakref.WeakValueDictionary() def __init__(self, - module, - module_filename, - template, - template_filename, - module_source, - template_source): + module, + module_filename, + template, + template_filename, + module_source, + template_source): self.module = module self.module_filename = module_filename self.template_filename = template_filename @@ -603,11 +611,12 @@ class ModuleInfo(object): @classmethod def get_module_source_metadata(cls, module_source, full_line_map=False): source_map = re.search( - r"__M_BEGIN_METADATA(.+?)__M_END_METADATA", - module_source, re.S).group(1) + r"__M_BEGIN_METADATA(.+?)__M_END_METADATA", + module_source, re.S).group(1) source_map = compat.json.loads(source_map) - source_map['line_map'] = dict((int(k), int(v)) - for k, v in source_map['line_map'].items()) + source_map['line_map'] = dict( + (int(k), int(v)) + for k, v in source_map['line_map'].items()) if full_line_map: f_line_map = source_map['full_line_map'] = [] line_map = source_map['line_map'] @@ -632,7 +641,7 @@ class ModuleInfo(object): if self.module._source_encoding and \ not isinstance(self.template_source, compat.text_type): return self.template_source.decode( - self.module._source_encoding) + self.module._source_encoding) else: return self.template_source else: @@ -642,32 +651,34 @@ class ModuleInfo(object): else: return data + def _compile(template, text, filename, generate_magic_comment): lexer = template.lexer_cls(text, - filename, - disable_unicode=template.disable_unicode, - input_encoding=template.input_encoding, - preprocessor=template.preprocessor) + filename, + disable_unicode=template.disable_unicode, + input_encoding=template.input_encoding, + preprocessor=template.preprocessor) node = lexer.parse() source = codegen.compile(node, - template.uri, - filename, - default_filters=template.default_filters, - buffer_filters=template.buffer_filters, - imports=template.imports, - future_imports=template.future_imports, - source_encoding=lexer.encoding, - generate_magic_comment=generate_magic_comment, - disable_unicode=template.disable_unicode, - strict_undefined=template.strict_undefined, - enable_loop=template.enable_loop, - reserved_names=template.reserved_names) + template.uri, + filename, + default_filters=template.default_filters, + buffer_filters=template.buffer_filters, + imports=template.imports, + future_imports=template.future_imports, + source_encoding=lexer.encoding, + generate_magic_comment=generate_magic_comment, + disable_unicode=template.disable_unicode, + strict_undefined=template.strict_undefined, + enable_loop=template.enable_loop, + reserved_names=template.reserved_names) return source, lexer + def _compile_text(template, text, filename): identifier = template.module_id source, lexer = _compile(template, text, filename, - generate_magic_comment=template.disable_unicode) + generate_magic_comment=template.disable_unicode) cid = identifier if not compat.py3k and isinstance(cid, compat.text_type): @@ -679,9 +690,10 @@ def _compile_text(template, text, filename): exec(code, module.__dict__, module.__dict__) return (source, module) + def _compile_module_file(template, text, filename, outputpath, module_writer): source, lexer = _compile(template, text, filename, - generate_magic_comment=True) + generate_magic_comment=True) if isinstance(source, compat.text_type): source = source.encode(lexer.encoding or 'ascii') @@ -698,12 +710,13 @@ def _compile_module_file(template, text, filename, outputpath, module_writer): os.close(dest) shutil.move(name, outputpath) + def _get_module_info_from_callable(callable_): if compat.py3k: return _get_module_info(callable_.__globals__['__name__']) else: return _get_module_info(callable_.func_globals['__name__']) + def _get_module_info(filename): return ModuleInfo._modules[filename] - diff --git a/mako/util.py b/mako/util.py index cba2ab7..c7dad65 100644 --- a/mako/util.py +++ b/mako/util.py @@ -11,6 +11,7 @@ import os from mako import compat import operator + def update_wrapper(decorated, fn): decorated.__wrapped__ = fn decorated.__name__ = fn.__name__ @@ -18,6 +19,7 @@ def update_wrapper(decorated, fn): class PluginLoader(object): + def __init__(self, group): self.group = group self.impls = {} @@ -28,15 +30,15 @@ class PluginLoader(object): else: import pkg_resources for impl in pkg_resources.iter_entry_points( - self.group, - name): + self.group, + name): self.impls[name] = impl.load return impl.load() else: from mako import exceptions raise exceptions.RuntimeException( - "Can't load plugin %s %s" % - (self.group, name)) + "Can't load plugin %s %s" % + (self.group, name)) def register(self, name, modulepath, objname): def load(): @@ -46,6 +48,7 @@ class PluginLoader(object): return getattr(mod, objname) self.impls[name] = load + def verify_directory(dir): """create and/or verify a filesystem directory.""" @@ -59,6 +62,7 @@ def verify_directory(dir): if tries > 5: raise + def to_list(x, default=None): if x is None: return default @@ -69,7 +73,9 @@ def to_list(x, default=None): class memoized_property(object): + """A read-only @property that is only evaluated once.""" + def __init__(self, fget, doc=None): self.fget = fget self.__doc__ = doc or fget.__doc__ @@ -81,7 +87,9 @@ class memoized_property(object): obj.__dict__[self.__name__] = result = self.fget(obj) return result + class memoized_instancemethod(object): + """Decorate a method memoize its return value. Best applied to no-arg methods: memoization is not sensitive to @@ -89,6 +97,7 @@ class memoized_instancemethod(object): called with different arguments. """ + def __init__(self, fget, doc=None): self.fget = fget self.__doc__ = doc or fget.__doc__ @@ -97,6 +106,7 @@ class memoized_instancemethod(object): def __get__(self, obj, cls): if obj is None: return self + def oneshot(*args, **kw): result = self.fget(obj, *args, **kw) memo = lambda *a, **kw: result @@ -108,8 +118,11 @@ class memoized_instancemethod(object): oneshot.__doc__ = self.__doc__ return oneshot + class SetLikeDict(dict): + """a dictionary that has some setlike methods on it""" + def union(self, other): """produce a 'union' of this dict and another (at the key level). @@ -118,7 +131,9 @@ class SetLikeDict(dict): x.update(other) return x + class FastEncodingBuffer(object): + """a very rudimentary buffer that is faster than StringIO, but doesn't crash on unicode data like cStringIO.""" @@ -144,7 +159,9 @@ class FastEncodingBuffer(object): else: return self.delim.join(self.data) + class LRUCache(dict): + """A dictionary-like object that stores a limited number of items, discarding lesser used items periodically. @@ -154,10 +171,12 @@ class LRUCache(dict): """ class _Item(object): + def __init__(self, key, value): self.key = key self.value = value self.timestamp = compat.time_func() + def __repr__(self): return repr(self.value) @@ -206,6 +225,7 @@ _PYTHON_MAGIC_COMMENT_re = re.compile( r'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE) + def parse_encoding(fp): """Deduce the encoding of a Python source file (binary mode) from magic comment. @@ -238,12 +258,13 @@ def parse_encoding(fp): else: line2 = fp.readline() m = _PYTHON_MAGIC_COMMENT_re.match( - line2.decode('ascii', 'ignore')) + line2.decode('ascii', 'ignore')) if has_bom: if m: - raise SyntaxError("python refuses to compile code with both a UTF8" \ - " byte-order-mark and a magic encoding comment") + raise SyntaxError( + "python refuses to compile code with both a UTF8" + " byte-order-mark and a magic encoding comment") return 'utf_8' elif m: return m.group(1) @@ -252,6 +273,7 @@ def parse_encoding(fp): finally: fp.seek(pos) + def sorted_dict_repr(d): """repr() a dictionary with the keys in order. @@ -262,6 +284,7 @@ def sorted_dict_repr(d): keys.sort() return "{" + ", ".join(["%r: %r" % (k, d[k]) for k in keys]) + "}" + def restore__ast(_ast): """Attempt to restore the required classes to the _ast module if it appears to be missing them @@ -338,7 +361,6 @@ mako in baz not in mako""", '<unknown>', 'exec', _ast.PyCF_ONLY_AST) _ast.NotIn = type(m.body[12].value.ops[1]) - def read_file(path, mode='rb'): fp = open(path, mode) try: @@ -347,6 +369,7 @@ def read_file(path, mode='rb'): finally: fp.close() + def read_python_file(path): fp = open(path, "rb") try: @@ -357,4 +380,3 @@ def read_python_file(path): return data finally: fp.close() - @@ -2,8 +2,6 @@ tag_build = dev -[wheel] -universal = 1 [pytest] addopts= --tb native -v -r fxX diff --git a/test/__init__.py b/test/__init__.py index 666318f..22d1c83 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -8,9 +8,11 @@ import re from mako.cache import CacheImpl, register_plugin try: + # unitttest has a SkipTest also but pytest doesn't + # honor it unless nose is imported too... from nose import SkipTest except ImportError: - from unittest import SkipTest + from _pytest.runner import Skipped as SkipTest import contextlib diff --git a/test/ext/test_babelplugin.py b/test/ext/test_babelplugin.py index c66260e..3658c50 100644 --- a/test/ext/test_babelplugin.py +++ b/test/ext/test_babelplugin.py @@ -6,11 +6,10 @@ from mako import compat try: import babel.messages.extract as babel -except: - babel = None - -if babel is not None: from mako.ext.babelplugin import extract + +except ImportError: + babel = None def skip(): @@ -79,3 +78,16 @@ class ExtractMakoTestCase(TemplateTest): (99, '_', 'No action at a distance.', []), ] self.assertEqual(expected, messages) + + @skip() + def test_extract_utf8(self): + mako_tmpl = open(os.path.join(template_base, 'gettext_utf8.mako'), 'rb') + message = next(extract(mako_tmpl, set(['_', None]), [], {'encoding': 'utf-8'})) + assert message == (1, '_', u'K\xf6ln', []) + + @skip() + def test_extract_cp1251(self): + mako_tmpl = open(os.path.join(template_base, 'gettext_cp1251.mako'), 'rb') + message = next(extract(mako_tmpl, set(['_', None]), [], {'encoding': 'cp1251'})) + # "test" in Rusian. File encoding is cp1251 (aka "windows-1251") + assert message == (1, '_', u'\u0442\u0435\u0441\u0442', []) diff --git a/test/templates/gettext.mako b/test/templates/gettext.mako index ad07c5d..45b8262 100644 --- a/test/templates/gettext.mako +++ b/test/templates/gettext.mako @@ -97,3 +97,34 @@ ${_(u'bar')} ## TRANSLATOR: we still ignore comments too far from the string <p>${_("No action at a distance.")}</p> + +## TRANSLATOR: nothing to extract from these blocks + +% if 1==1: +<p>One is one!</p> +% elif 1==2: +<p>One is two!</p> +% else: +<p>How much is one?</p> +% endif + +% for i in range(10): +<p>${i} squared is ${i*i}</p> +% else: +<p>Done with squares!</p> +% endfor + +% while random.randint(1,6) != 6: +<p>Not 6!</p> +% endwhile + +## TRANSLATOR: for now, try/except blocks are ignored + +% try: +<% 1/0 %> +% except: +<p>Failed!</p> +% endtry + +## TRANSLATOR: this should not cause a parse error +${ 1 } diff --git a/test/templates/gettext_cp1251.mako b/test/templates/gettext_cp1251.mako new file mode 100644 index 0000000..9341d93 --- /dev/null +++ b/test/templates/gettext_cp1251.mako @@ -0,0 +1 @@ +${_("òåñò")} diff --git a/test/templates/gettext_utf8.mako b/test/templates/gettext_utf8.mako new file mode 100644 index 0000000..761f946 --- /dev/null +++ b/test/templates/gettext_utf8.mako @@ -0,0 +1 @@ +${_("Köln")} diff --git a/test/test_template.py b/test/test_template.py index c5873dc..f551230 100644 --- a/test/test_template.py +++ b/test/test_template.py @@ -757,9 +757,22 @@ class UndefinedVarsTest(TemplateTest): ['t is: T', 'a,b,c'] ) +class StopRenderingTest(TemplateTest): + def test_return_in_template(self): + t = Template(""" + Line one + <% return STOP_RENDERING %> + Line Three + """, strict_undefined=True) + + eq_( + result_lines(t.render()), + ['Line one'] + ) + class ReservedNameTest(TemplateTest): def test_names_on_context(self): - for name in ('context', 'loop', 'UNDEFINED'): + for name in ('context', 'loop', 'UNDEFINED', 'STOP_RENDERING'): assert_raises_message( exceptions.NameConflictError, r"Reserved words passed to render\(\): %s" % name, @@ -767,7 +780,7 @@ class ReservedNameTest(TemplateTest): ) def test_names_in_template(self): - for name in ('context', 'loop', 'UNDEFINED'): + for name in ('context', 'loop', 'UNDEFINED', 'STOP_RENDERING'): assert_raises_message( exceptions.NameConflictError, r"Reserved words declared in template: %s" % name, @@ -1251,13 +1264,13 @@ Text eq_( ModuleInfo.get_module_source_metadata(t.code, full_line_map=True), { - 'full_line_map': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, - 0, 0, 0, 0, 0, 0, 1, 4, 5, 5, 5, 7, 8, - 8, 8, 8, 8, 8, 8], + 'full_line_map': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 4, 5, 5, 5, 7, + 8, 8, 8, 8, 8, 8, 8], 'source_encoding': 'ascii', 'filename': None, - 'line_map': {34: 28, 14: 0, 21: 1, 22: 4, 23: 5, 24: 5, - 25: 5, 26: 7, 27: 8, 28: 8}, + 'line_map': {35: 29, 15: 0, 22: 1, 23: 4, 24: 5, 25: 5, + 26: 5, 27: 7, 28: 8, 29: 8}, 'uri': '/some/template' } @@ -1280,17 +1293,19 @@ Text eq_( ModuleInfo.get_module_source_metadata(t.code, full_line_map=True), { - 'full_line_map': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 5, 5, 5, 7, 7, 7, - 7, 7, 10, 10, 10, 10, 10, 10, 8, 8, 8, 8, 8, - 8, 8, 8, 8, 8, 8, 8], + 'full_line_map': [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 5, 5, 5, 7, 7, + 7, 7, 7, 10, 10, 10, 10, 10, 10, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8], 'source_encoding': 'ascii', 'filename': None, - 'line_map': {33: 10, 39: 8, 45: 8, 14: 0, 51: 45, 23: 1, - 24: 4, 25: 5, 26: 5, 27: 5, 28: 7}, + 'line_map': {34: 10, 40: 8, 46: 8, 15: 0, 52: 46, + 24: 1, 25: 4, 26: 5, 27: 5, 28: 5, 29: 7}, 'uri': '/some/template'} ) + class PreprocessTest(TemplateTest): def test_old_comments(self): t = Template(""" @@ -0,0 +1,16 @@ +# content of: tox.ini , put in same dir as setup.py +[tox] +envlist = py{26,27,33,34} + +[testenv] +deps=pytest + py{26,27}: mock + beaker + markupsafe + pygments + babel + dogpile.cache + lingua + +commands=py.test + |