aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2012-03-29 20:58:05 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2012-03-29 20:58:05 -0400
commit0c191842df938a295a6850beaa91bd6fcb5d50c5 (patch)
tree166f6298ebe7b843b714c8f00f6ad7fcdeea251e
parente8b7dd036c4c485b8fef030a8d49431dc7caa40c (diff)
downloadexternal_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--CHANGES5
-rw-r--r--doc/build/runtime.rst115
-rw-r--r--doc/build/syntax.rst17
-rw-r--r--mako/codegen.py20
-rw-r--r--mako/lookup.py2
-rw-r--r--mako/parsetree.py2
-rw-r--r--mako/runtime.py5
-rw-r--r--mako/template.py51
-rw-r--r--test/test_loop.py94
9 files changed, 243 insertions, 68 deletions
diff --git a/CHANGES b/CHANGES
index e53fb97..d73f2ad 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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,
+ )
+