From ebbce747c8655b0647d82aee84310af4ec0e1a76 Mon Sep 17 00:00:00 2001 From: Mike Bayer <mike_mp@zzzcomputing.com> Date: Sat, 3 May 2008 15:27:11 +0000 Subject: [PATCH] - CHANGES cleanup - some functions on Context are now private: _push_buffer(), _pop_buffer(), caller_stack._push_frame(), caller_stack._pop_frame(). - implemented [ticket:76] inlining of context.write() --- CHANGES | 103 +++++++++++++++++++++++++++------------- examples/bench/basic.py | 2 +- lib/mako/codegen.py | 36 +++++++------- lib/mako/runtime.py | 54 +++++++++++++++++---- test/call.py | 7 ++- test/filters.py | 1 + 6 files changed, 141 insertions(+), 62 deletions(-) diff --git a/CHANGES b/CHANGES index 1e830ef..7e5c11c 100644 --- a/CHANGES +++ b/CHANGES @@ -1,36 +1,75 @@ 0.2.0 -- added "attr" accessor to namespaces. Returns attributes - configured as module level attributes, i.e. within - <%! %> sections [ticket:62] -- fixed bug in python generation when variable names are used - with identifiers like "else", "finally", etc. inside them - [ticket:68] -- fixed codegen bug which occured when using <%page> level - caching, combined with an expression-based cache_key, - combined with the usage of <%namespace import="*"/> -- fixed lexer exceptions not cleaning up temporary files, which - could lead to a maximum number of file descriptors used in the - process [ticket:69] -- fixed issue with inline format_exceptions that was producing - blank exception pages when an inheriting template is - present [ticket:71] -- format_exceptions will apply the encoding options - of html_error_template() to the buffered output -- rewrote the "whitespace adjuster" function to work with - more elaborate combinations of quotes and comments - [ticket:75] -- cache_key argument can now render arguments passed - directly to the %page or %def, i.e. - <%def name="foo(x)" cached="True" cache_key="${x}"/> - [ticket:78] -- added "bytestring passthru" mode, via `disable_unicode=True` - argument passed to Template or TemplateLookup. All - unicode-awareness and filtering is turned off, and template - modules are generated with the appropriate magic encoding - comment. In this mode, template expressions can only - receive raw bytestrings or Unicode objects which represent - straight ASCII, and render_unicode() may not be used. - [ticket:77] (courtesy anonymous guest) +- Speed improvements (as though we needed them, but people + contributed and there you go): + + - added "bytestring passthru" mode, via + `disable_unicode=True` argument passed to Template or + TemplateLookup. All unicode-awareness and filtering is + turned off, and template modules are generated with + the appropriate magic encoding comment. In this mode, + template expressions can only receive raw bytestrings + or Unicode objects which represent straight ASCII, and + render_unicode() may not be used if multibyte + characters are present. When enabled, speed + improvement around 10-20%. [ticket:77] (courtesy + anonymous guest) + + - inlined the "write" function of Context into a local + template variable. This affords a 12-30% speedup in + template render time. (idea courtesy same anonymous + guest) [ticket:76] + +- New Features, API changes: + + - added "attr" accessor to namespaces. Returns + attributes configured as module level attributes, i.e. + within <%! %> sections. [ticket:62] i.e.: + + # somefile.html + <%! + foo = 27 + %> + + # some other template + <%namespace name="myns" file="somefile.html"/> + ${myns.attr.foo} + + The slight backwards incompatibility here is, you + can't have namespace defs named "attr" since the + "attr" descriptor will occlude it. + + - cache_key argument can now render arguments passed + directly to the %page or %def, i.e. <%def + name="foo(x)" cached="True" cache_key="${x}"/> + [ticket:78] + + - some functions on Context are now private: + _push_buffer(), _pop_buffer(), + caller_stack._push_frame(), caller_stack._pop_frame(). + +- Bugfixes: + + - fixed bug in python generation when variable names are + used with identifiers like "else", "finally", etc. + inside them [ticket:68] + + - fixed codegen bug which occured when using <%page> + level caching, combined with an expression-based + cache_key, combined with the usage of <%namespace + import="*"/> - fixed lexer exceptions not cleaning up + temporary files, which could lead to a maximum number + of file descriptors used in the process [ticket:69] + + - fixed issue with inline format_exceptions that was + producing blank exception pages when an inheriting + template is present [ticket:71] + + - format_exceptions will apply the encoding options of + html_error_template() to the buffered output + + - rewrote the "whitespace adjuster" function to work + with more elaborate combinations of quotes and + comments [ticket:75] 0.1.10 - fixed propagation of 'caller' such that nested %def calls diff --git a/examples/bench/basic.py b/examples/bench/basic.py index c603a26..84c4b11 100644 --- a/examples/bench/basic.py +++ b/examples/bench/basic.py @@ -65,7 +65,7 @@ def myghty(dirname, verbose=False): def mako(dirname, verbose=False): from mako.template import Template from mako.lookup import TemplateLookup - lookup = TemplateLookup(directories=[dirname], filesystem_checks=False) + lookup = TemplateLookup(directories=[dirname], filesystem_checks=False, disable_unicode=True) template = lookup.get_template('template.html') def render(): return template.render(title="Just a test", user="joe", list_items=[u'Number %d' % num for num in range(1,15)]) diff --git a/lib/mako/codegen.py b/lib/mako/codegen.py index d59e487..e6b5476 100644 --- a/lib/mako/codegen.py +++ b/lib/mako/codegen.py @@ -163,11 +163,11 @@ class _GenerateRenderMethod(object): this could be the main render() method or that of a top-level def.""" self.printer.writelines( "def %s(%s):" % (name, ','.join(args)), - "context.caller_stack.push_frame()", + "context.caller_stack._push_frame()", "try:" ) if buffered or filtered or cached: - self.printer.writeline("context.push_buffer()") + self.printer.writeline("context._push_buffer()") self.identifier_stack.append(self.compiler.identifiers.branch(self.node)) if not self.in_def and '**pageargs' in args: @@ -302,6 +302,8 @@ class _GenerateRenderMethod(object): else: self.printer.writeline("%s = context.get(%s, UNDEFINED)" % (ident, repr(ident))) + 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: @@ -329,12 +331,12 @@ class _GenerateRenderMethod(object): buffered = eval(node.attributes.get('buffered', 'False')) cached = eval(node.attributes.get('cached', 'False')) self.printer.writelines( - "context.caller_stack.push_frame()", + "context.caller_stack._push_frame()", "try:" ) if buffered or filtered or cached: self.printer.writelines( - "context.push_buffer()", + "context._push_buffer()", ) identifiers = identifiers.branch(node, nested=nested) @@ -362,16 +364,16 @@ class _GenerateRenderMethod(object): if callstack: self.printer.writelines( "finally:", - "context.caller_stack.pop_frame()", + "context.caller_stack._pop_frame()", None ) if buffered or filtered or cached: self.printer.writelines( "finally:", - "__M_buf = context.pop_buffer()" + "__M_buf, __M_writer = context._pop_buffer_and_writer()" ) if callstack: - self.printer.writeline("context.caller_stack.pop_frame()") + self.printer.writeline("context.caller_stack._pop_frame()") s = "__M_buf.getvalue()" if filtered: s = self.create_filter_callable(node.filter_args.args, s, False) @@ -382,7 +384,7 @@ class _GenerateRenderMethod(object): self.printer.writeline("return %s" % s) else: self.printer.writelines( - "context.write(%s)" % s, + "__M_writer(%s)" % s, "return ''" ) @@ -420,7 +422,7 @@ class _GenerateRenderMethod(object): self.printer.writelines("return " + s,None) else: self.printer.writelines( - "context.write(context.get('local').get_cached(%s, %screatefunc=lambda:__M_%s(%s)))" % (cachekey, ''.join(["%s=%s, " % (k,v) for k, v in cacheargs.iteritems()]), name, ','.join(pass_args)), + "__M_writer(context.get('local').get_cached(%s, %screatefunc=lambda:__M_%s(%s)))" % (cachekey, ''.join(["%s=%s, " % (k,v) for k, v in cacheargs.iteritems()]), name, ','.join(pass_args)), "return ''", None ) @@ -460,9 +462,9 @@ class _GenerateRenderMethod(object): self.write_source_comment(node) if len(node.escapes) or (self.compiler.pagetag is not None and len(self.compiler.pagetag.filter_args.args)) or len(self.compiler.default_filters): s = self.create_filter_callable(node.escapes_code.args, "%s" % node.text, True) - self.printer.writeline("context.write(%s)" % s) + self.printer.writeline("__M_writer(%s)" % s) else: - self.printer.writeline("context.write(%s)" % node.text) + self.printer.writeline("__M_writer(%s)" % node.text) def visitControlLine(self, node): if node.isend: @@ -472,12 +474,12 @@ class _GenerateRenderMethod(object): self.printer.writeline(node.text) def visitText(self, node): self.write_source_comment(node) - self.printer.writeline("context.write(%s)" % repr(node.content)) + self.printer.writeline("__M_writer(%s)" % repr(node.content)) def visitTextTag(self, node): filtered = len(node.filter_args.args) > 0 if filtered: self.printer.writelines( - "context.push_buffer()", + "__M_writer = context._push_writer()", "try:", ) for n in node.nodes: @@ -485,8 +487,8 @@ class _GenerateRenderMethod(object): if filtered: self.printer.writelines( "finally:", - "__M_buf = context.pop_buffer()", - "context.write(%s)" % self.create_filter_callable(node.filter_args.args, "__M_buf.getvalue()", False), + "__M_buf, __M_writer = context._pop_buffer_and_writer()", + "__M_writer(%s)" % self.create_filter_callable(node.filter_args.args, "__M_buf.getvalue()", False), None ) @@ -544,7 +546,7 @@ class _GenerateRenderMethod(object): buffered = False if buffered: self.printer.writelines( - "context.push_buffer()", + "context._push_buffer()", "try:" ) self.write_variable_declares(body_identifiers) @@ -569,7 +571,7 @@ class _GenerateRenderMethod(object): "try:") self.write_source_comment(node) self.printer.writelines( - "context.write(%s)" % self.create_filter_callable([], node.attributes['expr'], True), + "__M_writer(%s)" % self.create_filter_callable([], node.attributes['expr'], True), "finally:", "context.caller_stack.nextcaller = None", None diff --git a/lib/mako/runtime.py b/lib/mako/runtime.py index f0ae468..e124b6b 100644 --- a/lib/mako/runtime.py +++ b/lib/mako/runtime.py @@ -23,27 +23,61 @@ class Context(object): # "caller" stack used by def calls with content self.caller_stack = data['caller'] = CallerStack() + lookup = property(lambda self:self._with_template.lookup) kwargs = property(lambda self:self._kwargs.copy()) + def push_caller(self, caller): self.caller_stack.append(caller) + def pop_caller(self): del self.caller_stack[-1] + def keys(self): return self._data.keys() + def __getitem__(self, key): return self._data[key] - def push_buffer(self): + + def _push_writer(self): + """push a capturing buffer onto this Context and return the new Writer function.""" + + buf = util.FastEncodingBuffer() + self._buffer_stack.append(buf) + return buf.write + + def _pop_buffer_and_writer(self): + """pop the most recent capturing buffer from this Context + and return the current writer after the pop. + + """ + + buf = self._buffer_stack.pop() + return buf, self._buffer_stack[-1].write + + def _push_buffer(self): """push a capturing buffer onto this Context.""" - self._buffer_stack.append(util.FastEncodingBuffer()) - def pop_buffer(self): + + self._push_writer() + + def _pop_buffer(self): """pop the most recent capturing buffer from this Context.""" + return self._buffer_stack.pop() + def get(self, key, default=None): return self._data.get(key, default) + def write(self, string): """write a string to this Context's underlying output buffer.""" + self._buffer_stack[-1].write(string) + + def writer(self): + """return the current writer function""" + + return self._buffer_stack[-1].write + def _copy(self): c = Context.__new__(Context) c._buffer_stack = self._buffer_stack @@ -78,10 +112,10 @@ class CallerStack(list): return self[-1] def __getattr__(self, key): return getattr(self._get_caller(), key) - def push_frame(self): + def _push_frame(self): self.append(self.nextcaller or None) self.nextcaller = None - def pop_frame(self): + def _pop_frame(self): self.nextcaller = self.pop() @@ -217,22 +251,22 @@ class Namespace(object): def supports_caller(func): """apply a caller_stack compatibility decorator to a plain Python function.""" def wrap_stackframe(context, *args, **kwargs): - context.caller_stack.push_frame() + context.caller_stack._push_frame() try: return func(context, *args, **kwargs) finally: - context.caller_stack.pop_frame() + 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.""" if not callable(callable_): raise exceptions.RuntimeException("capture() function expects a callable as its argument (i.e. capture(func, *args, **kwargs))") - context.push_buffer() + context._push_buffer() try: callable_(*args, **kwargs) finally: - buf = context.pop_buffer() + buf = context._pop_buffer() return buf.getvalue() def _include_file(context, uri, calling_uri, **kwargs): @@ -296,7 +330,7 @@ def _render(template, callable_, args, data, as_unicode=False): context = Context(buf, **data) context._with_template = template _render_context(template, callable_, context, *args, **_kwargs_for_callable(callable_, data)) - return context.pop_buffer().getvalue() + return context._pop_buffer().getvalue() def _kwargs_for_callable(callable_, data, **kwargs): argspec = inspect.getargspec(callable_) diff --git a/test/call.py b/test/call.py index 52ebeb0..0c89694 100644 --- a/test/call.py +++ b/test/call.py @@ -105,6 +105,7 @@ class CallTest(unittest.TestCase): </%call> """) + assert result_lines(t.render()) == [ "<h1>", "Some title", @@ -395,6 +396,8 @@ class CallTest(unittest.TestCase): assert result_lines(t.render()) == ['this is a', 'this is b', 'this is c:', "this is the body in b's call", 'the embedded "d" is:', 'this is d'] class SelfCacheTest(unittest.TestCase): + """this test uses a now non-public API.""" + def test_basic(self): t = Template(""" <%! @@ -405,11 +408,11 @@ class SelfCacheTest(unittest.TestCase): global cached if cached: return "cached: " + cached - context.push_buffer() + __M_writer = context._push_writer() %> this is foo <% - buf = context.pop_buffer() + buf, __M_writer = context._pop_buffer_and_writer() cached = buf.getvalue() return cached %> diff --git a/test/filters.py b/test/filters.py index da7caa9..4fdd342 100644 --- a/test/filters.py +++ b/test/filters.py @@ -34,6 +34,7 @@ class FilterTest(unittest.TestCase): </%def> ${foo()} """) + assert flatten_result(t.render(x="this is x", myfilter=lambda t: "MYFILTER->%s<-MYFILTER" % t)) == "MYFILTER-> this is foo <-MYFILTER" def test_import(self): -- GitLab