diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-03-29 20:58:05 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-03-29 20:58:05 -0400 |
commit | 0c191842df938a295a6850beaa91bd6fcb5d50c5 (patch) | |
tree | 166f6298ebe7b843b714c8f00f6ad7fcdeea251e | |
parent | e8b7dd036c4c485b8fef030a8d49431dc7caa40c (diff) | |
download | external_python_mako-0c191842df938a295a6850beaa91bd6fcb5d50c5.tar.gz external_python_mako-0c191842df938a295a6850beaa91bd6fcb5d50c5.tar.bz2 external_python_mako-0c191842df938a295a6850beaa91bd6fcb5d50c5.zip |
- add a path to disable the loop feature - enable_loop=False
- fix up links, formatting in docs
- remove some repetition in the _compile logic
-rw-r--r-- | CHANGES | 5 | ||||
-rw-r--r-- | doc/build/runtime.rst | 115 | ||||
-rw-r--r-- | doc/build/syntax.rst | 17 | ||||
-rw-r--r-- | mako/codegen.py | 20 | ||||
-rw-r--r-- | mako/lookup.py | 2 | ||||
-rw-r--r-- | mako/parsetree.py | 2 | ||||
-rw-r--r-- | mako/runtime.py | 5 | ||||
-rw-r--r-- | mako/template.py | 51 | ||||
-rw-r--r-- | test/test_loop.py | 94 |
9 files changed, 243 insertions, 68 deletions
@@ -2,7 +2,10 @@ - [feature] Added new "loop" variable to templates, is provided within a % for block to provide info about the loop such as index, first/last, - odd/even, etc. Thanks to Ben Trofatter for all + odd/even, etc. A migration path is also provided + for legacy templates via the "enable_loop" argument + available on Template, TemplateLookup, and <%page>. + Thanks to Ben Trofatter for all the work on this [ticket:125] - [feature] Added a real check for "reserved" diff --git a/doc/build/runtime.rst b/doc/build/runtime.rst index fd5a7ce..3439955 100644 --- a/doc/build/runtime.rst +++ b/doc/build/runtime.rst @@ -15,7 +15,7 @@ Context The :class:`.Context` is the central object that is created when a template is first executed, and is responsible for handling all communication with the outside world. Within the template -environment, it is available via the :ref:`reserved name <reserved-names>` +environment, it is available via the :ref:`reserved name <reserved_names>` ``context``. The :class:`.Context` includes two major components, one of which is the output buffer, which is a file-like object such as Python's ``StringIO`` or similar, and @@ -188,15 +188,24 @@ Significant members of :class:`.Context` include: :class:`.TemplateLookup` of the originally-called :class:`.Template` gets used in a particular execution). -.. loop-context +.. _loop_context: -Loop Context -============ +The Loop Context +================ -Within ``% for`` blocks, the :ref:`reserved name<reserved-names>` ``loop`` -is available. A new feature of Mako 0.7.0, ``loop`` tracks the progress of +Within ``% for`` blocks, the :ref:`reserved name<reserved_names>` ``loop`` +is available. A new feature of Mako 0.7, ``loop`` tracks the progress of the ``for`` loop and makes it easy to use the iteration state to control -template behavior. +template behavior: + +.. sourcecode:: mako + + <ul> + % for a in ("one", "two", "three"): + <li>Item ${loop.index}: ${a}</li> + % endfor + </ul> + Iterations ---------- @@ -214,30 +223,28 @@ Cycling ------- Cycling is available regardless of whether the iterable you're using provides -a ``__len__`` method. Prior to Mako 0.7.0, you might have generated a simple -zebra striped list with either:: +a ``__len__`` method. Prior to Mako 0.7, you might have generated a simple +zebra striped list using ``enumerate``: .. sourcecode:: mako <ul> % for i, item in enumerate(('spam', 'ham', 'eggs')): - <li class="${'odd' if i%2 else 'even'}">${item}</li> + <li class="${'odd' if i % 2 else 'even'}">${item}</li> % endfor </ul> -or:: +With ``loop``, you get the same results with cleaner code and less prep work: .. sourcecode:: mako - <%! from itertools import cycle %> - <% parity = cycle(('even', 'odd')) %> <ul> % for item in ('spam', 'ham', 'eggs'): - <li class="${next(parity)}">${item}</li> + <li class="${loop.cycle('even', 'odd')}">${item}</li> % endfor </ul> -both of which give you:: +Both approaches produce output like the following: .. sourcecode:: html @@ -247,15 +254,6 @@ both of which give you:: <li class="even">eggs</li> </ul> -With ``loop``, you get the same results with cleaner code and less prep work.:: - -.. sourcecode:: mako - - <ul> - % for item in ('spam', 'ham', 'eggs'): - <li class="${loop.cycle('even', 'odd')}">${item}</li> - % endfor - </ul> Parent loops ------------ @@ -264,11 +262,10 @@ Loop contexts can also be transparently nested, and the Mako runtime will do the right thing and manage the scope for you. You can access the parent loop context through ``loop.parent``. -This -also allows you to easily reach all the way back up through the loop stack by +This allows you to reach all the way back up through the loop stack by chaining ``parent`` attribute accesses, i.e. ``loop.parent.parent....`` as long as the stack depth isn't exceeded. For example, you can use the parent -loop to make a checkered table:: +loop to make a checkered table: .. sourcecode:: mako @@ -322,14 +319,43 @@ loop to make a checkered table:: </tr> </table> -.. reserved-names -Reserved names -============== +.. _migrating_loop: + +Migrating Legacy Templates that Use the Word "loop" +--------------------------------------------------- -As of Mako 0.7.0, there are two reserved identifiers within the Mako runtime: -* :ref:`context <context>` -* :ref:`loop <loop-context>` +The ``loop`` name is now :ref:`reserved <reserved_names>` in Mako, which means a template that refers to a +variable named ``loop`` won't function correctly when used in Mako 0.7. To ease +the transition for such systems, the feature can be disabled across the board for +all templates, then re-enabled on a per-template basis for those templates which wish +to make use of the new system. + +First, the ``enable_loop=False`` flag is passed to either the :class:`.TemplateLookup` +or :class:`.Template` object in use:: + + lookup = TemplateLookup(directories=['/docs'], enable_loop=False) + +or:: + + template = Template("some template", enable_loop=False) + +An individual template can make usage of the feature when ``enable_loop`` is set to +``False`` by switching it back on within the ``<%page>`` tag: + +.. sourcecode:: mako + + <%page enable_loop="True"/> + + % for i in collection: + ${i} ${loop.index} + % endfor + +Using the above scheme, it's safe to pass the name ``loop`` to the :meth:`.Template.render` +method as well as to freely make usage of a variable named ``loop`` within a template, provided +the ``<%page>`` tag doesn't override it. New templates that want to use the ``loop`` context +can then set up ``<%page enable_loop="True"/>`` to use the new feature without affecting +old templates. All the built-in names ====================== @@ -338,7 +364,9 @@ A one-stop shop for all the names Mako defines. Most of these names are instances of :class:`.Namespace`, which are described in the next section, :ref:`namespaces_toplevel`. Also, most of these names other than :class:`.Context` and ``UNDEFINED`` are -also present *within* the :class:`.Context` itself. +also present *within* the :class:`.Context` itself. There are only +two names, ``context`` and ``loop``, that are themselves not defined +in the context and can't be replaced - see the section :ref:`reserved_names`. * ``context`` - this is the :class:`.Context` object, introduced at :ref:`context`. @@ -356,8 +384,8 @@ also present *within* the :class:`.Context` itself. * ``caller`` - a "mini" namespace created when using the ``<%call>`` tag to define a "def call with content"; described in :ref:`defs_with_content`. -* ``loop`` - this provides access to :class:`.LoopContext`\ s when - they are requested within ``% for`` loops, introduced at :ref:`loop`. +* ``loop`` - this provides access to :class:`.LoopContext` objects when + they are requested within ``% for`` loops, introduced at :ref:`loop_context`. * ``capture`` - a function that calls a given def and captures its resulting content into a string, which is returned. Usage is described in :ref:`filtering_toplevel`. @@ -376,6 +404,21 @@ also present *within* the :class:`.Context` itself. makes no sense, it shouldn't; read the section :ref:`namespaces_body`. +.. _reserved_names: + +Reserved names +-------------- + +Mako has two words that are considered to be "reserved" and can't be used +as variable names. As of 0.7, Mako raises an error if these words are found +passed to the template as context arguments, whereas in previous versions they'd be silently +ignored or lead to other error messages. + +* ``context`` - see :ref:`context` +* ``loop`` - see :ref:`loop_context`. Note this can be disabled for legacy templates + via the ``enable_loop=False`` argument; see :ref:`migrating_loop`. + + API Reference ============== diff --git a/doc/build/syntax.rst b/doc/build/syntax.rst index f98fca1..239bd16 100644 --- a/doc/build/syntax.rst +++ b/doc/build/syntax.rst @@ -109,6 +109,23 @@ line, by escaping it as in ``%%``: %% some more text +The Loop Context +---------------- + +Mako 0.7 includes a new feature called the **loop context** which +provides additional information about a loop while inside of a ``% for`` +structure: + +.. sourcecode:: mako + + <ul> + % for a in ("one", "two", "three"): + <li>Item ${loop.index}: ${a}</li> + % endfor + </ul> + +See :ref:`loop_context` for more information on this feature. + Comments ======== diff --git a/mako/codegen.py b/mako/codegen.py index c8da0c7..f42c17d 100644 --- a/mako/codegen.py +++ b/mako/codegen.py @@ -29,6 +29,7 @@ def compile(node, generate_magic_comment=True, disable_unicode=False, strict_undefined=False, + enable_loop=True, reserved_names=()): """Generate module source code given a parsetree node, @@ -55,6 +56,7 @@ def compile(node, generate_magic_comment, disable_unicode, strict_undefined, + enable_loop, reserved_names), node) return buf.getvalue() @@ -70,6 +72,7 @@ class _CompileContext(object): generate_magic_comment, disable_unicode, strict_undefined, + enable_loop, reserved_names): self.uri = uri self.filename = filename @@ -80,6 +83,7 @@ class _CompileContext(object): self.generate_magic_comment = generate_magic_comment self.disable_unicode = disable_unicode self.strict_undefined = strict_undefined + self.enable_loop = enable_loop self.reserved_names = reserved_names class _GenerateRenderMethod(object): @@ -115,6 +119,10 @@ class _GenerateRenderMethod(object): if not pagetag.body_decl.kwargs: 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') + ) else: args = ['**pageargs'] cached = False @@ -185,6 +193,7 @@ class _GenerateRenderMethod(object): 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) self.printer.writeline("_template_uri = %r" % self.compiler.uri) @@ -433,8 +442,11 @@ class _GenerateRenderMethod(object): # which cannot be referenced beforehand. to_write = to_write.difference(identifiers.locally_declared) - has_loop = "loop" in to_write - to_write.discard("loop") + if self.compiler.enable_loop: + has_loop = "loop" in to_write + to_write.discard("loop") + else: + has_loop = False # if a limiting set was sent, constraint to those items in that list # (this is used for the caching decorator) @@ -759,7 +771,7 @@ class _GenerateRenderMethod(object): self.printer.writeline(None) else: self.write_source_comment(node) - if node.keyword == 'for': + if self.compiler.enable_loop and node.keyword == 'for': text = mangle_mako_loop(node, self.printer) else: text = node.text @@ -791,8 +803,6 @@ class _GenerateRenderMethod(object): ) def visitCode(self, node): - # mangle loop variables within the scope of a loop context, - # if applicable if not node.ismodule: self.write_source_comment(node) self.printer.write_indented_block(node.text) diff --git a/mako/lookup.py b/mako/lookup.py index b0d67d4..dd8bf3c 100644 --- a/mako/lookup.py +++ b/mako/lookup.py @@ -165,6 +165,7 @@ class TemplateLookup(TemplateCollection): buffer_filters=(), strict_undefined=False, imports=None, + enable_loop=True, input_encoding=None, preprocessor=None): @@ -203,6 +204,7 @@ class TemplateLookup(TemplateCollection): 'buffer_filters':buffer_filters, 'strict_undefined':strict_undefined, 'imports':imports, + 'enable_loop':enable_loop, 'preprocessor':preprocessor} if collection_size == -1: diff --git a/mako/parsetree.py b/mako/parsetree.py index 2425931..e72ac04 100644 --- a/mako/parsetree.py +++ b/mako/parsetree.py @@ -565,7 +565,7 @@ class PageTag(Tag): __keyword__ = 'page' def __init__(self, keyword, attributes, **kwargs): - expressions = ['cached', 'args', 'expression_filter'] + [ + expressions = ['cached', 'args', 'expression_filter', 'enable_loop'] + [ c for c in attributes if c.startswith('cache_')] super(PageTag, self).__init__( diff --git a/mako/runtime.py b/mako/runtime.py index 9101af3..eb9a5c5 100644 --- a/mako/runtime.py +++ b/mako/runtime.py @@ -238,7 +238,10 @@ class LoopStack(object): class LoopContext(object): """A magic loop variable. - Automatically accessible in any %for block. + Automatically accessible in any ``% for`` block. + + See the section :ref:`loop_context` for usage + notes. :attr:`parent` -> LoopContext or None The parent loop, if one exists diff --git a/mako/template.py b/mako/template.py index e3a6399..92345b1 100644 --- a/mako/template.py +++ b/mako/template.py @@ -72,6 +72,13 @@ class Template(object): :param disable_unicode: Disables all awareness of Python Unicode objects. See :ref:`unicode_disabled`. + :param enable_loop: When ``True``, enable the ``loop`` context variable. + This can be set to ``False`` to support templates that may + be making usage of the name "loop". Individual templates can + re-enable the "loop" context by placing the directive + ``enable_loop="True"`` inside the ``<%page>`` tag - see + :ref:`migrating_loop`. + :param encoding_errors: Error parameter passed to ``encode()`` when string encoding is performed. See :ref:`usage_unicode`. @@ -193,6 +200,7 @@ class Template(object): buffer_filters=(), strict_undefined=False, imports=None, + enable_loop=True, preprocessor=None): if uri: self.module_id = re.sub(r'\W', "_", uri) @@ -221,6 +229,7 @@ class Template(object): self.encoding_errors = encoding_errors self.disable_unicode = disable_unicode self.bytestring_passthrough = bytestring_passthrough or disable_unicode + self.enable_loop = enable_loop self.strict_undefined = strict_undefined self.module_writer = module_writer @@ -285,7 +294,10 @@ class Template(object): @util.memoized_property def reserved_names(self): - return codegen.RESERVED_NAMES + if self.enable_loop: + return codegen.RESERVED_NAMES + else: + return codegen.RESERVED_NAMES.difference(['loop']) def _setup_cache_args(self, cache_impl, cache_enabled, cache_args, @@ -466,6 +478,7 @@ class ModuleTemplate(Template): self.encoding_errors = encoding_errors self.disable_unicode = disable_unicode self.bytestring_passthrough = bytestring_passthrough or disable_unicode + self.enable_loop = module._enable_loop if util.py3k and disable_unicode: raise exceptions.UnsupportedError( @@ -506,6 +519,7 @@ class DefTemplate(Template): self.encoding_errors = parent.encoding_errors self.format_exceptions = parent.format_exceptions self.error_handler = parent.error_handler + self.enable_loop = parent.enable_loop self.lookup = parent.lookup self.bytestring_passthrough = parent.bytestring_passthrough @@ -558,16 +572,14 @@ class ModuleInfo(object): return data.decode(self.module._source_encoding) else: return data - -def _compile_text(template, text, filename): - identifier = template.module_id + +def _compile(template, text, filename, generate_magic_comment): lexer = Lexer(text, filename, disable_unicode=template.disable_unicode, input_encoding=template.input_encoding, preprocessor=template.preprocessor) node = lexer.parse() - source = codegen.compile(node, template.uri, filename, @@ -575,10 +587,17 @@ def _compile_text(template, text, filename): buffer_filters=template.buffer_filters, imports=template.imports, source_encoding=lexer.encoding, - generate_magic_comment=template.disable_unicode, + 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) cid = identifier if not util.py3k and isinstance(cid, unicode): @@ -590,24 +609,8 @@ def _compile_text(template, text, filename): def _compile_module_file(template, text, filename, outputpath, module_writer): identifier = template.module_id - lexer = Lexer(text, - 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, - source_encoding=lexer.encoding, - generate_magic_comment=True, - disable_unicode=template.disable_unicode, - strict_undefined=template.strict_undefined, - reserved_names=template.reserved_names) + source, lexer = _compile(template, text, filename, + generate_magic_comment=True) if isinstance(source, unicode): source = source.encode(lexer.encoding or 'ascii') diff --git a/test/test_loop.py b/test/test_loop.py index a86f8fd..14912ee 100644 --- a/test/test_loop.py +++ b/test/test_loop.py @@ -2,12 +2,15 @@ import re import unittest from mako.template import Template +from mako.lookup import TemplateLookup from mako.codegen import ( _FOR_LOOP, mangle_mako_loop, LoopVariable ) from mako.runtime import LoopStack, LoopContext from mako import exceptions from test import assert_raises_message +from test import TemplateTest, eq_ +from util import flatten_result, result_lines class TestLoop(unittest.TestCase): @@ -199,3 +202,94 @@ class TestLoopContext(unittest.TestCase): expected = ('a', 'b', 'a') actual = tuple(self.ctx.cycle('a', 'b') for i in self.ctx) assert expected == actual, "cycle endlessly cycles through the values" + +class TestLoopFlags(TemplateTest): + def test_loop_disabled_template(self): + self._do_memory_test( + """ + the loop: ${loop} + """, + "the loop: hi", + template_args=dict(loop='hi'), + filters=flatten_result, + enable_loop=False + ) + + def test_loop_disabled_lookup(self): + l = TemplateLookup(enable_loop=False) + l.put_string("x", + """ + the loop: ${loop} + """ + ) + + self._do_test( + l.get_template("x"), + "the loop: hi", + template_args=dict(loop='hi'), + filters=flatten_result, + ) + + def test_loop_disabled_override_template(self): + self._do_memory_test( + """ + <%page enable_loop="True" /> + % for i in (1, 2, 3): + ${i} ${loop.index} + % endfor + """, + "1 0 2 1 3 2", + template_args=dict(loop='hi'), + filters=flatten_result, + enable_loop=False + ) + + def test_loop_disabled_override_lookup(self): + l = TemplateLookup(enable_loop=False) + l.put_string("x", + """ + <%page enable_loop="True" /> + % for i in (1, 2, 3): + ${i} ${loop.index} + % endfor + """ + ) + + self._do_test( + l.get_template("x"), + "1 0 2 1 3 2", + template_args=dict(loop='hi'), + filters=flatten_result, + ) + + def test_loop_enabled_override_template(self): + self._do_memory_test( + """ + <%page enable_loop="True" /> + % for i in (1, 2, 3): + ${i} ${loop.index} + % endfor + """, + "1 0 2 1 3 2", + template_args=dict(), + filters=flatten_result, + ) + + def test_loop_enabled_override_lookup(self): + l = TemplateLookup() + l.put_string("x", + """ + <%page enable_loop="True" /> + % for i in (1, 2, 3): + ${i} ${loop.index} + % endfor + """ + ) + + self._do_test( + l.get_template("x"), + "1 0 2 1 3 2", + template_args=dict(), + filters=flatten_result, + ) + |