diff options
| -rw-r--r-- | doc/build/changelog.rst | 11 | ||||
| -rw-r--r-- | mako/codegen.py | 52 | ||||
| -rw-r--r-- | mako/compat.py | 5 | ||||
| -rw-r--r-- | mako/exceptions.py | 26 | ||||
| -rw-r--r-- | mako/pygen.py | 37 | ||||
| -rw-r--r-- | setup.py | 13 |
6 files changed, 111 insertions, 33 deletions
diff --git a/doc/build/changelog.rst b/doc/build/changelog.rst index 6b79b30..27c12e5 100644 --- a/doc/build/changelog.rst +++ b/doc/build/changelog.rst @@ -10,6 +10,17 @@ Changelog :released: .. 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 integration with coverage tools. + + .. change:: :tags: bug, py3k :tickets: 227 diff --git a/mako/codegen.py b/mako/codegen.py index 045d03c..6380b40 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 = [] @@ -401,7 +420,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) @@ -532,13 +551,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 @@ -756,7 +768,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 @@ -778,7 +790,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: @@ -800,7 +812,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): @@ -826,7 +838,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: @@ -843,7 +855,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( @@ -943,7 +955,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 c5ef84b..8e8ee70 100644 --- a/mako/compat.py +++ b/mako/compat.py @@ -94,6 +94,11 @@ except: return func(*(args + fargs), **newkeywords) return newfunc +if py26: + import json +else: + import simplejson as json + if not py25: def all(iterable): for i in iterable: diff --git a/mako/exceptions.py b/mako/exceptions.py index b8f97ee..523805f 100644 --- a/mako/exceptions.py +++ b/mako/exceptions.py @@ -167,14 +167,24 @@ 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 = re.search( + r"__M_BEGIN_METADATA(.+?)__M_END_METADATA", + module_source, re.S).group(1) + source_map = compat.json.loads(source_map) + 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 + template_lines = [line for line in template_source.split("\n")] mods[filename] = (line_map, template_lines) diff --git a/mako/pygen.py b/mako/pygen.py index cba9464..62967e3 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() @@ -13,10 +13,17 @@ markupsafe_installs = ( sys.version_info >= (2, 6) and sys.version_info < (3, 0) ) or sys.version_info >= (3, 3) +json_installs = ( + sys.version_info < (2, 6) + ) + +install_requires = [] + if markupsafe_installs: - install_requires = ['MarkupSafe>=0.9.2'] -else: - install_requires = [] + install_requires.append('MarkupSafe>=0.9.2') + +if json_installs: + install_requires.append('simplejson') setup(name='Mako', version=VERSION, |
