diff --git a/doc/build/changelog.rst b/doc/build/changelog.rst index f43b26df3e3fb4c84ba82f996d3705cd81675d33..8e1c9ebdcffea4e156554a67b2be7ecbacdaede0 100644 --- a/doc/build/changelog.rst +++ b/doc/build/changelog.rst @@ -16,6 +16,23 @@ Changelog is now dropping support for Python 2.4 and Python 2.5 altogether. The source base is now targeted at Python 2.6 and forwards. + .. change:: + :tags: feature + + Template modules now generate a JSON "metadata" structure at the bottom + of the source file which includes parseable information about the + templates' source file, encoding etc. as well as a mapping of module + source lines to template lines, thus replacing the "# SOURCE LINE" + markers throughout the source code. The structure also indicates those + lines that are explicitly not part of the template's source; the goal + here is to allow better integration with coverage and other tools. + + .. change:: + :tags: bug, py3k + + Fixed bug in ``decode.<encoding>`` filter where a non-string object + would not be correctly interpreted in Python 3. + .. change:: :tags: bug, py3k @@ -47,13 +64,6 @@ Changelog into the text error handler, and exit with a non-zero exit code. Pull request courtesy Derek Harland. - .. change:: - :tags: feature, py3k - :pullreq: github:7 - - Support is added for Python 3 "keyword only" arguments, as used in - defs. Pull request courtesy Eevee. - .. change:: :tags: bug :pullreq: bitbucket:2 @@ -67,6 +77,14 @@ Changelog template lookup directories. Standard input for templates also works now too. Pull request courtesy Derek Harland. + .. change:: + :tags: feature, py3k + :pullreq: github:7 + + Support is added for Python 3 "keyword only" arguments, as used in + defs. Pull request courtesy Eevee. + + 0.9 === diff --git a/mako/codegen.py b/mako/codegen.py index 2240ba28fa38d85383bc234e2a1239945f7df80c..63e76a7c8b10ad44e5b76328bebf9f0375142be6 100644 --- a/mako/codegen.py +++ b/mako/codegen.py @@ -14,7 +14,7 @@ from mako import util, ast, parsetree, filters, exceptions from mako import compat -MAGIC_NUMBER = 9 +MAGIC_NUMBER = 10 # names which are hardwired into the # template and are not accessed via the @@ -99,7 +99,6 @@ class _GenerateRenderMethod(object): """ def __init__(self, printer, compiler, node): self.printer = printer - self.last_source_line = -1 self.compiler = compiler self.node = node self.identifier_stack = [None] @@ -146,6 +145,26 @@ class _GenerateRenderMethod(object): for node in defs: _GenerateRenderMethod(printer, compiler, node) + if not self.in_def: + self.write_metadata_struct() + + def write_metadata_struct(self): + self.printer.source_map[self.printer.lineno] = self.printer.last_source_line + struct = { + "filename": self.compiler.filename, + "uri": self.compiler.uri, + "source_encoding": self.compiler.source_encoding, + "line_map": self.printer.source_map, + "boilerplate_lines": self.printer.boilerplate_map + } + self.printer.writelines( + '"""', + '__M_BEGIN_METADATA', + compat.json.dumps(struct), + '__M_END_METADATA\n' + '"""' + ) + @property def identifiers(self): return self.identifier_stack[-1] @@ -232,7 +251,7 @@ class _GenerateRenderMethod(object): [n.name for n in main_identifiers.topleveldefs.values()] ) - self.printer.write("\n\n") + self.printer.write_blanks(2) if len(module_code): self.write_module_code(module_code) @@ -288,7 +307,7 @@ class _GenerateRenderMethod(object): self.write_def_finish(self.node, buffered, filtered, cached) self.printer.writeline(None) - self.printer.write("\n\n") + self.printer.write_blanks(2) if cached: self.write_cache_decorator( node, name, @@ -299,7 +318,7 @@ class _GenerateRenderMethod(object): """write module-level template code, i.e. that which is enclosed in <%! %> tags in the template.""" for n in module_code: - self.write_source_comment(n) + self.printer.start_source(n.lineno) self.printer.write_indented_block(n.text) def write_inherit(self, node): @@ -330,7 +349,7 @@ class _GenerateRenderMethod(object): for node in namespaces.values(): if 'import' in node.attributes: self.compiler.has_ns_imports = True - self.write_source_comment(node) + self.printer.start_source(node.lineno) if len(node.nodes): self.printer.writeline("def make_namespace():") export = [] @@ -402,7 +421,7 @@ class _GenerateRenderMethod(object): self.printer.writeline( "context.namespaces[(__name__, %s)] = ns" % repr(node.name)) - self.printer.write("\n") + self.printer.write_blanks(1) if not len(namespaces): self.printer.writeline("pass") self.printer.writeline(None) @@ -533,13 +552,6 @@ class _GenerateRenderMethod(object): self.printer.writeline("__M_writer = context.writer()") - def write_source_comment(self, node): - """write a source comment containing the line number of the - corresponding template line.""" - if self.last_source_line != node.lineno: - self.printer.writeline("# SOURCE LINE %d" % node.lineno) - self.last_source_line = node.lineno - def write_def_decl(self, node, identifiers): """write a locally-available callable referencing a top-level def""" funcname = node.funcname @@ -757,7 +769,7 @@ class _GenerateRenderMethod(object): return target def visitExpression(self, node): - self.write_source_comment(node) + self.printer.start_source(node.lineno) if len(node.escapes) or \ ( self.compiler.pagetag is not None and @@ -779,7 +791,7 @@ class _GenerateRenderMethod(object): self.printer.writeline("loop = __M_loop._exit()") self.printer.writeline(None) else: - self.write_source_comment(node) + self.printer.start_source(node.lineno) if self.compiler.enable_loop and node.keyword == 'for': text = mangle_mako_loop(node, self.printer) else: @@ -801,7 +813,7 @@ class _GenerateRenderMethod(object): self.printer.writeline("pass") def visitText(self, node): - self.write_source_comment(node) + self.printer.start_source(node.lineno) self.printer.writeline("__M_writer(%s)" % repr(node.content)) def visitTextTag(self, node): @@ -827,7 +839,7 @@ class _GenerateRenderMethod(object): def visitCode(self, node): if not node.ismodule: - self.write_source_comment(node) + self.printer.start_source(node.lineno) self.printer.write_indented_block(node.text) if not self.in_def and len(self.identifiers.locally_assigned) > 0: @@ -844,7 +856,7 @@ class _GenerateRenderMethod(object): ','.join([repr(x) for x in node.declared_identifiers()])) def visitIncludeTag(self, node): - self.write_source_comment(node) + self.printer.start_source(node.lineno) args = node.attributes.get('args') if args: self.printer.writeline( @@ -944,7 +956,7 @@ class _GenerateRenderMethod(object): "runtime.Namespace('caller', context, " "callables=ccall(__M_caller))", "try:") - self.write_source_comment(node) + self.printer.start_source(node.lineno) self.printer.writelines( "__M_writer(%s)" % self.create_filter_callable( [], node.expression, True), diff --git a/mako/compat.py b/mako/compat.py index f782aa94fdc4b1f637a1e2e48923a21d943bd5d2..dea1b308d7c176974775520f42d6a65cbd0c4140 100644 --- a/mako/compat.py +++ b/mako/compat.py @@ -100,6 +100,7 @@ except: return newfunc all = all +import json def exception_name(exc): return exc.__class__.__name__ diff --git a/mako/exceptions.py b/mako/exceptions.py index a7bab8cbfb4bfbde3bc1aa281c8c68ddb909b888..20b4dce41ceedee4a1283918fe66fc3343f7011d 100644 --- a/mako/exceptions.py +++ b/mako/exceptions.py @@ -8,7 +8,6 @@ import traceback import sys -import re from mako import util, compat class MakoException(Exception): @@ -77,7 +76,6 @@ class RichTraceback(object): self.records = self._init(traceback) if isinstance(self.error, (CompileException, SyntaxException)): - import mako.template self.source = self.error.source self.lineno = self.error.lineno self._has_source = True @@ -167,14 +165,13 @@ class RichTraceback(object): None, None, None, None)) continue - template_ln = module_ln = 1 - line_map = {} - for line in module_source.split("\n"): - match = re.match(r'\s*# SOURCE LINE (\d+)', line) - if match: - template_ln = int(match.group(1)) - module_ln += 1 - line_map[module_ln] = template_ln + template_ln = 1 + + source_map = mako.template.ModuleInfo.\ + 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")] mods[filename] = (line_map, template_lines) @@ -188,7 +185,7 @@ class RichTraceback(object): line, template_filename, template_ln, template_line, template_source)) if not self.source: - for l in range(len(new_trcback)-1, 0, -1): + for l in range(len(new_trcback) - 1, 0, -1): if new_trcback[l][5]: self.source = new_trcback[l][7] self.lineno = new_trcback[l][5] diff --git a/mako/pygen.py b/mako/pygen.py index 52e32beb1bec28d4ac01fe59a1cee372241b6885..dfd83d3f4a4fd168f067aa230bb28c70d8d3cdff 100644 --- a/mako/pygen.py +++ b/mako/pygen.py @@ -26,6 +26,9 @@ class PythonPrinter(object): # the stream we are writing to self.stream = stream + # current line number + self.lineno = 0 + # a list of lines that represents a buffered "block" of code, # which can be later printed relative to an indent level self.line_buffer = [] @@ -34,8 +37,35 @@ class PythonPrinter(object): self._reset_multi_line_flags() - def write(self, text): - self.stream.write(text) + # marker for template source lines; this + # is part of source/template line mapping + self.last_source_line = -1 + + self.last_boilerplate_line = -1 + + # mapping of generated python lines to template + # source lines + self.source_map = {} + + # list of "boilerplate" lines, these are lines + # that precede/follow a set of template source-mapped lines + self.boilerplate_map = [] + + + def _update_lineno(self, num): + if self.last_boilerplate_line <= self.last_source_line: + self.boilerplate_map.append(self.lineno) + self.last_boilerplate_line = self.lineno + self.lineno += num + + def start_source(self, lineno): + if self.last_source_line != lineno: + self.source_map[self.lineno] = lineno + self.last_source_line = lineno + + def write_blanks(self, num): + self.stream.write("\n" * num) + self._update_lineno(num) def write_indented_block(self, block): """print a line or lines of python which already contain indentation. @@ -94,6 +124,7 @@ class PythonPrinter(object): # write the line self.stream.write(self._indent_line(line) + "\n") + self._update_lineno(1) # see if this line should increase the indentation level. # note that a line can both decrase (before printing) and @@ -213,11 +244,13 @@ class PythonPrinter(object): for entry in self.line_buffer: if self._in_multi_line(entry): self.stream.write(entry + "\n") + self._update_lineno(1) else: entry = entry.expandtabs() if stripspace is None and re.search(r"^[ \t]*[^# \t]", entry): stripspace = re.match(r"^([ \t]*)", entry).group(1) self.stream.write(self._indent_line(entry, stripspace) + "\n") + self._update_lineno(1) self.line_buffer = [] self._reset_multi_line_flags() diff --git a/mako/template.py b/mako/template.py index 00783b775591c712bd1bc33afff778cf96cb0c5f..c19a66a718be74d1e4c789aad9763688e4d56f34 100644 --- a/mako/template.py +++ b/mako/template.py @@ -596,6 +596,26 @@ class ModuleInfo(object): if module_filename: self._modules[module_filename] = self + @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) + source_map = compat.json.loads(source_map) + if full_line_map: + line_map = source_map['full_line_map'] = dict( + (int(k), v) for k, v in source_map['line_map'].items() + ) + + for mod_line in reversed(sorted(line_map)): + tmpl_line = line_map[mod_line] + while mod_line > 0: + mod_line -= 1 + if mod_line in line_map: + break + line_map[mod_line] = tmpl_line + return source_map + @property def code(self): if self.module_source is not None: diff --git a/setup.py b/setup.py index bbab08e07d73e810feb642e84b495c6924c404ab..0094901012f764e2b6253de47c88c9fd4fec3145 100644 --- a/setup.py +++ b/setup.py @@ -16,10 +16,10 @@ markupsafe_installs = ( sys.version_info >= (2, 6) and sys.version_info < (3, 0) ) or sys.version_info >= (3, 3) +install_requires = [] + if markupsafe_installs: - install_requires = ['MarkupSafe>=0.9.2'] -else: - install_requires = [] + install_requires.append('MarkupSafe>=0.9.2') try: import argparse