[Python-modules-commits] [python-kajiki] 01/05: New upstream version 0.6.1

Takaki Taniguchi takaki at moszumanska.debian.org
Thu Feb 2 09:31:29 UTC 2017


This is an automated email from the git hooks/post-receive script.

takaki pushed a commit to branch master
in repository python-kajiki.

commit 886a9316f03e1889efd1a703723a73b31d2917b2
Author: TANIGUCHI Takaki <takaki at asis.media-as.org>
Date:   Thu Feb 2 18:16:04 2017 +0900

    New upstream version 0.6.1
---
 CHANGES.rst                  |  18 +++
 Kajiki.egg-info/PKG-INFO     |  20 ++-
 Kajiki.egg-info/SOURCES.txt  |   1 +
 PKG-INFO                     |  20 ++-
 docs/conf.py                 |   2 +-
 docs/runtime.rst             |  28 +++-
 kajiki/doctype.py            |  50 ++++++-
 kajiki/html_utils.py         |  17 +--
 kajiki/i18n.py               |  10 +-
 kajiki/ir.py                 |  11 +-
 kajiki/loader.py             |  13 +-
 kajiki/template.py           |  66 +++++++--
 kajiki/tests/data/error.html |   6 +
 kajiki/tests/test_ir.py      |   0
 kajiki/tests/test_runtime.py |   0
 kajiki/tests/test_text.py    |   6 +
 kajiki/tests/test_xml.py     | 144 +++++++++++++++----
 kajiki/text.py               |   4 +-
 kajiki/util.py               |   2 +-
 kajiki/version.py            |   4 +-
 kajiki/xml_template.py       | 325 +++++++++++++++++++++++++++++++++----------
 21 files changed, 593 insertions(+), 154 deletions(-)

diff --git a/CHANGES.rst b/CHANGES.rst
index d356a3f..8910a74 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,6 +1,24 @@
 CHANGES
 =======
 
+0.6.1 (2016-11-28)
+-----------------
+
+* Actually report 0.6 in kajiki/version.py
+* Expose ``strip_text`` option in loader
+
+0.6.0 (2016-11-27)
+------------------
+
+* Fixed ``py:switch`` error message wrongly mentioning ``py:with``
+* Support for multiline ``${}`` expressions
+* Subsequent text nodes are now squashed into a single text node. This allows translating whole paragraphs instead of single sentences.
+* Allow code and function calls inside tag attributes
+* Added ``strip_text`` option to XMLTemplate and i18n collector to ensure leading and trailing spaces are stipped by text nodes (also leads to minified HTML)
+* Some HTML nodes that do not require being closed but is commonly considered best practice to close them are now emitted with ending tag (IE: <p>)
+* Generally improved code documentation to lower entry barrier for contributors
+
+
 0.5.5 (2016-06-08)
 ------------------
 
diff --git a/Kajiki.egg-info/PKG-INFO b/Kajiki.egg-info/PKG-INFO
index d0889df..5415aef 100644
--- a/Kajiki.egg-info/PKG-INFO
+++ b/Kajiki.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: Kajiki
-Version: 0.5.5
+Version: 0.6.1
 Summary: Fast XML-based template engine with Genshi syntax and Jinja blocks
 Home-page: https://github.com/nandoflorestan/kajiki
 Author: Nando Florestan
@@ -78,6 +78,24 @@ Description: Kajiki provides fast well-formed XML templates
         CHANGES
         =======
         
+        0.6.1 (2016-11-28)
+        -----------------
+        
+        * Actually report 0.6 in kajiki/version.py
+        * Expose ``strip_text`` option in loader
+        
+        0.6.0 (2016-11-27)
+        ------------------
+        
+        * Fixed ``py:switch`` error message wrongly mentioning ``py:with``
+        * Support for multiline ``${}`` expressions
+        * Subsequent text nodes are now squashed into a single text node. This allows translating whole paragraphs instead of single sentences.
+        * Allow code and function calls inside tag attributes
+        * Added ``strip_text`` option to XMLTemplate and i18n collector to ensure leading and trailing spaces are stipped by text nodes (also leads to minified HTML)
+        * Some HTML nodes that do not require being closed but is commonly considered best practice to close them are now emitted with ending tag (IE: <p>)
+        * Generally improved code documentation to lower entry barrier for contributors
+        
+        
         0.5.5 (2016-06-08)
         ------------------
         
diff --git a/Kajiki.egg-info/SOURCES.txt b/Kajiki.egg-info/SOURCES.txt
index 06cd081..d484f54 100644
--- a/Kajiki.egg-info/SOURCES.txt
+++ b/Kajiki.egg-info/SOURCES.txt
@@ -59,4 +59,5 @@ kajiki/tests/test_xml.py
 kajiki/tests/data/__init__.py
 kajiki/tests/data/debug.html
 kajiki/tests/data/debug.txt
+kajiki/tests/data/error.html
 kajiki/tests/data/simple.html
\ No newline at end of file
diff --git a/PKG-INFO b/PKG-INFO
index d0889df..5415aef 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: Kajiki
-Version: 0.5.5
+Version: 0.6.1
 Summary: Fast XML-based template engine with Genshi syntax and Jinja blocks
 Home-page: https://github.com/nandoflorestan/kajiki
 Author: Nando Florestan
@@ -78,6 +78,24 @@ Description: Kajiki provides fast well-formed XML templates
         CHANGES
         =======
         
+        0.6.1 (2016-11-28)
+        -----------------
+        
+        * Actually report 0.6 in kajiki/version.py
+        * Expose ``strip_text`` option in loader
+        
+        0.6.0 (2016-11-27)
+        ------------------
+        
+        * Fixed ``py:switch`` error message wrongly mentioning ``py:with``
+        * Support for multiline ``${}`` expressions
+        * Subsequent text nodes are now squashed into a single text node. This allows translating whole paragraphs instead of single sentences.
+        * Allow code and function calls inside tag attributes
+        * Added ``strip_text`` option to XMLTemplate and i18n collector to ensure leading and trailing spaces are stipped by text nodes (also leads to minified HTML)
+        * Some HTML nodes that do not require being closed but is commonly considered best practice to close them are now emitted with ending tag (IE: <p>)
+        * Generally improved code documentation to lower entry barrier for contributors
+        
+        
         0.5.5 (2016-06-08)
         ------------------
         
diff --git a/docs/conf.py b/docs/conf.py
index fddd39f..ac13136 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -42,7 +42,7 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'Kajiki'
-copyright = u'2010-2013, Rick Copeland'
+copyright = u'2010-2016, Rick Copeland, Nando Florestan and Alessandro Molina'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
diff --git a/docs/runtime.rst b/docs/runtime.rst
index dcc2b5d..97d0509 100644
--- a/docs/runtime.rst
+++ b/docs/runtime.rst
@@ -1,6 +1,9 @@
-==================================
-Kajiki Runtime Transformations
-==================================
+==============
+Kajiki Runtime
+==============
+
+.. module:: kajiki
+    :synopsis: Kajiki Runtime APIs
 
 It's sometimes good to have a mental model of the Python code that Kajiki
 creates in order to generate your templates.  This document uses several
@@ -8,6 +11,25 @@ examples taken from the text templating language to illustrate the semantics of
 Kajiki templates.  If in doubt, you can always view the Python text generated for
 a template by examining the py_text attribute of the generated Template class.
 
+.. automethod:: kajiki.xml_template.XMLTemplate
+
+.. autoclass:: kajiki.ir.TemplateNode
+    :members:
+
+.. automethod:: kajiki.template.Template
+
+.. autoclass:: kajiki.template._Template
+    :members:
+
+.. autoclass:: kajiki.xml_template._Compiler
+    :members:
+
+.. autoclass:: kajiki.xml_template._Parser
+    :members:
+
+.. autoclass:: kajiki.xml_template._DomTransformer
+    :members:
+
 Basic Expressions
 =========================
 
diff --git a/kajiki/doctype.py b/kajiki/doctype.py
index 0fc4f31..66833d6 100644
--- a/kajiki/doctype.py
+++ b/kajiki/doctype.py
@@ -9,7 +9,31 @@ from nine.decorator import reify
 
 @nine
 class DocumentTypeDeclaration(object):
-    '''Represents a http://en.wikipedia.org/wiki/Document_Type_Declaration'''
+    """Represents a http://en.wikipedia.org/wiki/Document_Type_Declaration
+
+    This is used to lookup DTDs details by its string, DTDs can
+    be registered in :attr:`.by_uri` and can then be looked up
+    using :meth:`.matching` method::
+
+        >>> from kajiki.doctype import DocumentTypeDeclaration
+        >>> dtd = DocumentTypeDeclaration("html4transitional",
+        ...                               "-//W3C//DTD HTML 4.01 Transitional//EN",
+        ...                               "http://www.w3.org/TR/html4/loose.dtd",
+        ...                               rendering_mode='html')
+        >>> print dtd.uri
+        http://www.w3.org/TR/html4/loose.dtd
+        >>> DocumentTypeDeclaration.by_uri["http://www.w3.org/TR/html4/loose.dtd"] = dtd
+        >>> match = DocumentTypeDeclaration.matching(
+        ...     '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" '
+        ...     '"http://www.w3.org/TR/html4/loose.dtd">'
+        ... )
+        >>> print match.name
+        'html4transitional'
+
+    DocumentTypeDeclaration is used by :class:`kajiki.xml_template._Compiler`
+    to detect the document doctype and tune generated template (for example
+    by deciding if tags closed inline are allowed or not).
+    """
     def __init__(self, name, fpi='', uri='', rendering_mode='xml',
                  root_element='html', kind='PUBLIC'):
         '''*fpi* is the Formal Public Identifier.'''
@@ -99,10 +123,28 @@ for dtd in (
 
 
 def extract_dtd(markup):
-    '''Tries to find any DTD in the string *markup* and returns a tuple
+    """Lookup the DTD in the provided markup code.
+
+    Tries to find any DTD in the string *markup* and returns a tuple
     (dtd_string, position, markup_without_the_DTD). Note the first of
-    these values might be an empty string.
-    '''
+    these values might be an empty string::
+
+        >>> markup = '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+        ...             "http://www.w3.org/TR/html4/loose.dtd">
+        ...     <html>
+        ...     <head>
+        ...     ...
+        ...     </head>
+        ...     <body>
+        ...     ...
+        ...     </body>
+        ...     </html>'''
+        >>> import kajiki.doctype
+        >>> dtd, dtd_pos, html = kajiki.doctype.extract_dtd(markup)
+        >>> print dtd
+        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+            "http://www.w3.org/TR/html4/loose.dtd">
+    """
     match = DocumentTypeDeclaration.REGEX.search(markup)
     if not match:
         return '', 0, markup
diff --git a/kajiki/html_utils.py b/kajiki/html_utils.py
index ae6f276..0b4fcd7 100644
--- a/kajiki/html_utils.py
+++ b/kajiki/html_utils.py
@@ -17,29 +17,14 @@ HTML_EMPTY_ATTRS = set([
 HTML_OPTIONAL_END_TAGS = set([
     'area',
     'base',
-    'body',
     'br',
     'col',
-    'colgroup',
-    'dd',
-    'dt',
-    'head',
     'hr',
-    'html',
     'img',
     'input',
-    'li',
     'link',
     'meta',
-    'option',
-    'p',
-    'param',
-    'tbody',
-    'td',
-    'tfoot',
-    'th',
-    'thead',
-    'tr',
+    'param'
 ])
 HTML_REQUIRED_END_TAGS = set(['script'])
 HTML_CDATA_TAGS = set(('script', 'style'))
diff --git a/kajiki/i18n.py b/kajiki/i18n.py
index e7d54cc..28f9040 100644
--- a/kajiki/i18n.py
+++ b/kajiki/i18n.py
@@ -10,14 +10,13 @@ def gettext(s):
 
 
 def extract(fileobj, keywords, comment_tags, options):
-    '''Babel entry point that extracts translation strings from XML templates.
-    '''
-    from .xml_template import _Parser, _Compiler, expand
+    """Babel entry point that extracts translation strings from XML templates."""
+    from .xml_template import _Parser, _Compiler, _DomTransformer
     source = fileobj.read()
     if isinstance(source, bytes):
         source = source.decode('utf-8')
     doc = _Parser(filename='<string>', source=source).parse()
-    expand(doc)
+    doc = _DomTransformer(doc, strip_text=options.get('strip_text', False)).transform()
     compiler = _Compiler(filename='<string>', doc=doc,
                          mode=options.get('mode', 'xml'),
                          is_fragment=options.get('is_fragment', False))
@@ -25,5 +24,4 @@ def extract(fileobj, keywords, comment_tags, options):
     for node in ir:
         if isinstance(node, TranslatableTextNode):
             if node.text.strip():
-                for line in node.text.split('\n'):
-                    yield (node.lineno, '_', line, [])
+                yield (node.lineno, '_', node.text, [])
\ No newline at end of file
diff --git a/kajiki/ir.py b/kajiki/ir.py
index 425cc55..9163723 100644
--- a/kajiki/ir.py
+++ b/kajiki/ir.py
@@ -69,7 +69,16 @@ class DedentNode(Node):
 
 
 class TemplateNode(HierNode):
-    """Represents the root Intermediate Representation node of a template."""
+    """Represents the root Intermediate Representation node of a template.
+
+    Iterating over this will generate the Python code for the class
+    that provides all the functions that are part of the template including
+    the ``__main__`` function that represents the template code itself.
+
+    The generated class will then be passed to :meth:`kajiki.template.Template` to
+    create a :class:`kajiki.template._Template` subclass that has the
+    ``render`` method to render the template.
+    """
     class TemplateTail(Node):
         def py(self):
             yield self.line('template = kajiki.Template(template)')
diff --git a/kajiki/loader.py b/kajiki/loader.py
index 935f73c..25fd978 100644
--- a/kajiki/loader.py
+++ b/kajiki/loader.py
@@ -41,7 +41,8 @@ class MockLoader(Loader):
 
 class FileLoader(Loader):
     def __init__(self, path, reload=True, force_mode=None,
-                 autoescape_text=False, xml_autoblocks=None):
+                 autoescape_text=False, xml_autoblocks=None,
+                 **template_options):
         super(FileLoader, self).__init__()
         from kajiki import XMLTemplate, TextTemplate
         if isinstance(path, basestring):
@@ -53,6 +54,7 @@ class FileLoader(Loader):
         self._force_mode = force_mode
         self._autoescape_text = autoescape_text
         self._xml_autoblocks = xml_autoblocks
+        self._template_options = template_options
         self.extension_map = dict(
             txt=lambda *a, **kw: TextTemplate(
                 autoescape=self._autoescape_text, *a, **kw),
@@ -81,20 +83,23 @@ class FileLoader(Loader):
         text template files.
         '''
         from kajiki import XMLTemplate, TextTemplate
+        options = self._template_options.copy()
+        options.update(kwargs)
+
         filename = self._filename(name)
         self._timestamps[name] = os.stat(filename).st_mtime
         if self._force_mode == 'text':
             return TextTemplate(filename=filename,
-                autoescape=self._autoescape_text, *args, **kwargs)
+                autoescape=self._autoescape_text, *args, **options)
         elif self._force_mode:
             return XMLTemplate(filename=filename,
                                mode=self._force_mode,
                                autoblocks=self._xml_autoblocks,
-                               *args, **kwargs)
+                               *args, **options)
         else:
             ext = os.path.splitext(filename)[1][1:]
             return self.extension_map[ext](
-                source=None, filename=filename, *args, **kwargs)
+                source=None, filename=filename, *args, **options)
 
 
 class PackageLoader(FileLoader):
diff --git a/kajiki/template.py b/kajiki/template.py
index 285ae1b..2a46ce3 100644
--- a/kajiki/template.py
+++ b/kajiki/template.py
@@ -35,7 +35,7 @@ class _obj(object):
 class _Template(object):
     """Base Class for all compiled Kajiki Templates.
 
-    All kajiki templates created from an ``ir.TemplateNode`` will
+    All kajiki templates created from a :class:`kajiki.ir.TemplateNode` will
     be subclasses of this class.
 
     As the template body code runs inside ``__main__`` method of this
@@ -91,20 +91,34 @@ class _Template(object):
         self.__globals__.update(context)
 
     def __iter__(self):
-        '''We convert the chunk to string because it can be of any type
+        """We convert the chunk to string because it can be of any type
         -- after all, the template supports expressions such as ${x+y}.
         Here, ``chunk`` can be the computed expression result.
-        '''
+        """
         for chunk in self.__main__():
             yield str(chunk)
 
     def render(self):
+        """Render the template to a string."""
         return ''.join(self)
 
     def _push_with(self, locals_, vars):
+        """Enter a ``py:with`` block.
+
+        When a ``py:with`` block is encountered, previous values
+        of the variables assigned inside the ``py:with`` statement are
+        pushed on top of a stack by :class:`kajiki.ir.WithNode` so that
+        when the node is exited the previous values can be recovered.
+        """
         self._with_stack.append([locals_.get(k, ()) for k in vars])
 
     def _pop_with(self):
+        """Exists a ``py:with`` block.
+
+        When a ``py:with`` block is exited the values stack is popped
+        and the head returned to :class:`kajiki.ir.WithNode` so that
+        it can set any previously existing variable to its old value.
+        """
         return self._with_stack.pop()
 
     def _extend(self, parent):
@@ -143,12 +157,25 @@ class _Template(object):
         return p_inst
 
     def _push_switch(self, expr):
+        """Enter a ``py:switch`` block.
+
+        Pushes provided value on the stack used
+        to check ``py:case`` statements against.
+
+        Calling :meth:`._pop_switch` will exit the switch block.
+        """
         self._switch_stack.append(expr)
 
     def _pop_switch(self):
+        """Exit current ``py:switch`` block.
+
+        Pops current value from the stack used
+        to check ``py:case`` statements against.
+        """
         self._switch_stack.pop()
 
     def _case(self, obj):
+        """Check against current ``py:switch`` value."""
         return obj == self._switch_stack[-1]
 
     def _import(self, name, alias, gbls):
@@ -159,7 +186,7 @@ class _Template(object):
         return r
 
     def _escape(self, value):
-        "Returns the given HTML with ampersands, carets and quotes encoded."
+        """Returns the given HTML with ampersands, carets and quotes encoded."""
         if value is None or isinstance(value, flattener):
             return value
         if hasattr(value, '__html__'):
@@ -180,6 +207,12 @@ class _Template(object):
     _re_escape = re.compile(r'&|<|>|"')
 
     def _render_attrs(self, attrs, mode):
+        """Render tag attributes in key="value" format.
+
+        A :class:`kajiki.ir.AttrsNode` will generate
+        code that in fact leads to this function to generate
+        the html for tag attributes.
+        """
         if hasattr(attrs, 'items'):
             attrs = attrs.items()
         if attrs is not None:
@@ -198,7 +231,10 @@ class _Template(object):
         for part in it:
             if part is None:
                 continue
-            result.append(str(part))
+            if isinstance(part, flattener):
+                result.append(str(part.accumulate_str()))
+            else:
+                result.append(str(part))
         if result:
             return ''.join(result)
         else:
@@ -210,13 +246,14 @@ class _Template(object):
             meth.annotate_lnotab(cls.filename, py_to_tpl, dict(py_to_tpl))
 
     def defined(self, name):
+        """Check if a variable was provided to the template or not"""
         return name in self._context
 
 
 def Template(ns):
-    """Creates a new ``_Template`` subclass from an entity with ``exposed`` functions.
+    """Creates a new :class:`._Template` subclass from an entity with ``exposed`` functions.
 
-    Kajiki used classes as containers of the exposed functions for convenience,
+    Kajiki uses classes as containers of the exposed functions for convenience,
     but any object that can have the functions as attributes works.
 
     To be a valid template the original entity must provide at least a ``__main__``
@@ -245,9 +282,9 @@ def Template(ns):
 def from_ir(ir_node):
     """Creates a template class from Intermediate Representation TemplateNode.
 
-    This actually creates the class defined by the TemplateNode and returns
-    a subclass of it.
-    The returned class is a subclass of a `kajiki.template._Template`.
+    This actually creates the class defined by the TemplateNode by executing
+    its code and returns a subclass of it.
+    The returned class is a subclass of :class:`kajiki.template._Template`.
     """
     py_lines = list(generate_python(ir_node))
     py_text = '\n'.join(map(str, py_lines))
@@ -271,6 +308,15 @@ def from_ir(ir_node):
 
 
 class TplFunc(object):
+    """A template function attached to a _Template.
+
+    By default template functions (ie: __main__) depends
+    on variables like ``self``, ``local`` and so on which
+    are provided by :class:`._Template`.
+
+    This is used by :meth:`.Template` to create a new
+    ``_Template`` with the attached functions.
+    """
     def __init__(self, func, inst=None):
         self._func = func
         self._inst = inst
diff --git a/kajiki/tests/data/error.html b/kajiki/tests/data/error.html
new file mode 100644
index 0000000..b2088e9
--- /dev/null
+++ b/kajiki/tests/data/error.html
@@ -0,0 +1,6 @@
+<html>
+  <div>
+
+    ${3/0}
+  </div>
+</html>
\ No newline at end of file
diff --git a/kajiki/tests/test_ir.py b/kajiki/tests/test_ir.py
old mode 100755
new mode 100644
diff --git a/kajiki/tests/test_runtime.py b/kajiki/tests/test_runtime.py
old mode 100755
new mode 100644
diff --git a/kajiki/tests/test_text.py b/kajiki/tests/test_text.py
old mode 100755
new mode 100644
index 6da306c..1363ffe
--- a/kajiki/tests/test_text.py
+++ b/kajiki/tests/test_text.py
@@ -52,6 +52,12 @@ class TestBasic(TestCase):
         rsp = tpl(dict(obj=Empty)).render()
         assert rsp == 'Hello, Rick\n', rsp
 
+    def test_expr_multiline(self):
+        tpl = TextTemplate(source="""Hello, ${{'name': 'Rick',
+                                               'age': 26}['name']}""")
+        rsp = tpl().render()
+        assert rsp == 'Hello, Rick', (rsp, 'Hello, Rick')
+
 
 class TestSwitch(TestCase):
     def test_switch(self):
diff --git a/kajiki/tests/test_xml.py b/kajiki/tests/test_xml.py
old mode 100755
new mode 100644
index 015ed2b..9463f59
--- a/kajiki/tests/test_xml.py
+++ b/kajiki/tests/test_xml.py
@@ -7,7 +7,11 @@ import os
 import sys
 import traceback
 import xml.dom.minidom
+from io import BytesIO
 from unittest import TestCase, main
+
+from kajiki import i18n
+from kajiki.template import KajikiSyntaxError
 from nine import chr, str
 import kajiki
 from kajiki import MockLoader, XMLTemplate, FileLoader, PackageLoader
@@ -48,7 +52,7 @@ class TestExpand(TestCase):
         py:replace="replace"
         py:block="block"
         py:extends="extends">Foo</div>''').parse()
-        kajiki.xml_template.expand(doc)
+        doc = kajiki.xml_template._DomTransformer(doc).transform()
         node = doc.childNodes[0]
         for tagname, attr in kajiki.markup_template.QDIRECTIVES:
             if node.tagName == 'div':
@@ -66,10 +70,8 @@ class TestExpand(TestCase):
             node = node.childNodes[0]
 
 
-def perform(source, expected_output, context=dict(name='Rick'),
-            mode='xml', is_fragment=True, cdata_scripts=True):
-    tpl = XMLTemplate(source, mode=mode, is_fragment=is_fragment,
-                      cdata_scripts=cdata_scripts)
+def perform(source, expected_output, context=dict(name='Rick'), **options):
+    tpl = XMLTemplate(source, **options)
     try:
         rsp = tpl(context).render()
         assert isinstance(rsp, str), 'render() must return a unicode string.'
@@ -101,9 +103,9 @@ class TestSimple(TestCase):
     def test_script(self):
         'Always close script tags, even in xml mode.'
         source = '<html><script src="public"/></html>'
-        output = '<html><script src="public"></script>'
+        output = '<html><script src="public"></script></html>'
         perform(source, output, mode='html')
-        perform(source, output + '</html>', mode='xml')
+        perform(source, output, mode='xml')
 
     def test_script_escaping(self):
         '''In HTML script and style tags are automatically CDATA; in XML they
@@ -152,20 +154,6 @@ class TestSimple(TestCase):
         perform(src, '<script>/*<![CDATA[*//**/\n{0}/**//*]]>*/</script>'.format(
                 script), mode='xml')
 
-    def test_scripts_non_translatable(self):
-        src = '<xml><div>Hi</div><script>hello world</script><style>hello style</style></xml>'
-        doc = _Parser('<string>', src).parse()
-
-        for n in _Compiler('<string>', doc).compile():
-            text = getattr(n, 'text', '')
-            if text in ('hello world', 'hello style'):
-                self.assertFalse(isinstance(n, TranslatableTextNode))
-
-        for n in _Compiler('<string>', doc, cdata_scripts=False).compile():
-            text = getattr(n, 'text', '')
-            if text in ('hello world', 'hello style'):
-                self.assertFalse(isinstance(n, TranslatableTextNode))
-
     def test_escape_dollar(self):
         perform('<div>$$</div>', '<div>$</div>')
 
@@ -189,6 +177,16 @@ class TestSimple(TestCase):
         perform("<div>Hello, ${{'name':name}['name']}</div>",
                 '<div>Hello, Rick</div>')
 
+    def test_expr_multiline(self):
+        perform("""<div>Hello, ${{'name': 'Rick',
+                                 'age': 26}['name']}</div>""",
+                '<div>Hello, Rick</div>')
+
+    def test_expr_multiline_cdata(self):
+        perform("""<script><![CDATA[Hello, ${{'name': 'Rick',
+                                 'age': 26}['name']}]]></script>""",
+                '<script>/*<![CDATA[*/Hello, Rick/*]]>*/</script>')
+
     def test_jquery_call_is_not_expr(self):
         '''Ensure we handle '$(' as a text literal, since it cannot be a
         valid variable sequence.  This simplifies, for example,
@@ -250,6 +248,15 @@ $i is <py:switch test="i % 4">
 6 is nope</div><div>
 7 is nope</div>''')
 
+    def test_case_elem(self):
+        perform('''<div>
+    <py:switch test="True">
+      <span py:case="0 == 1">0</span>
+      <span py:case="1 == 1">1</span>
+      <span py:else="">2</span>
+    </py:switch>
+  </div>''', '<div>\n    <span>1</span>\n  </div>')
+
     def test_switch_div(self):
         try:
             tpl = perform('''
@@ -259,7 +266,7 @@ $i is <py:switch test="i % 4">
         </div>''', '<div><div>False</div></div>')
         except XMLTemplateCompileError as e:
             self.assertTrue(
-                'py:with directive can only contain py:case and py:else nodes' in str(e)
+                'py:switch directive can only contain py:case and py:else nodes' in str(e)
             )
         else:
             self.assertTrue(False, msg='Should have raised XMLTemplateParseError')
@@ -334,6 +341,10 @@ class TestWith(TestCase):
         perform('''<div py:with="a=';';b='-)'">$a$b</div>''',
                 '<div>;-)</div>')
 
+    def test_standalone(self):
+        perform('''<div><py:with vars="a=';';b='-)'">$a$b</py:with></div>''',
+                '<div>;-)</div>')
+
 
 class TestFunction(TestCase):
     def test_function(self):
@@ -351,6 +362,13 @@ class TestFunction(TestCase):
         perform('<div><py:def function="bruhaha()"></py:def></div>',
                 '<div></div>')
 
+    def test_function_in_attr(self):
+        '''Attribute value with a function call.'''
+        perform('''<div
+><py:def function="attrtest(n, sz=16)">text/$sz/$n</py:def><img
+src="${attrtest(name)}"/></div>''',
+                '<div><img src="text/16/Rick"/></div>')
+
 
 class TestCall(TestCase):
     def test_call(self):
@@ -710,20 +728,20 @@ class TestAttributes(TestCase):
         perform(TPL, '<input type="checkbox"/>', context0, mode='xml')
         perform(TPL, '<input checked="True" type="checkbox"/>',
                 context1, mode='xml')
-        perform(TPL, '<input type="checkbox">', context0, 'html')
+        perform(TPL, '<input type="checkbox">', context0, mode='html')
         perform(TPL, '<input checked type="checkbox">',
-                context1, 'html')
-        perform(TPL, '<!DOCTYPE html><input checked type="checkbox">',
+                context1, mode='html')
+        perform(TPL, '<!DOCTYPE html>\n<input checked type="checkbox">',
                 context1, mode='html5', is_fragment=False)
         perform('<!DOCTYPE html>\n' + TPL,
-                '<!DOCTYPE html><input checked type="checkbox">',
+                '<!DOCTYPE html>\n<input checked type="checkbox">',
                 context1, mode=None, is_fragment=False)
 
     def test_xml_namespaces(self):
         '''Namespaced attributes pass through.'''
         TPL = '<p xml:lang="en">English text</p>'
         perform(TPL, TPL, mode='xml')
-        perform(TPL, TPL[:-4], mode='html')
+        perform(TPL, TPL, mode='html')
 
     def test_escape_attr_values(self):
         '''Escape static and dynamic attribute values.'''
@@ -787,5 +805,77 @@ context=dict(parrot='Bereft of life, it rests in peace'))
         assert ''.join(list(literal(markup))) == markup
 
 
+class TestTranslation(TestCase):
+    def test_scripts_non_translatable(self):
+        src = '<xml><div>Hi</div><script>hello world</script><style>hello style</style></xml>'
+        doc = _Parser('<string>', src).parse()
+
+        for n in _Compiler('<string>', doc).compile():
+            text = getattr(n, 'text', '')
+            if text in ('hello world', 'hello style'):
+                self.assertFalse(isinstance(n, TranslatableTextNode))
+
+        for n in _Compiler('<string>', doc, cdata_scripts=False).compile():
+            text = getattr(n, 'text', '')
+            if text in ('hello world', 'hello style'):
+                self.assertFalse(isinstance(n, TranslatableTextNode))
+
+    def test_extract_translate(self):
+        src = '''<xml><div>Hi</div><p>
+
+        Hello
+        World</p></xml>'''
+        expected = {
+            False: '''<xml><div>TRANSLATED(Hi)</div><p>TRANSLATED(
+
+        Hello
+        World)</p></xml>''',
+            True: '''<xml><div>TRANSLATED(Hi)</div><p>TRANSLATED(Hello
+        World)</p></xml>'''
+        }
+
+        for strip_text in (False, True):
+            # Build translation table
+            messages = {}
+            for _, _, msgid, _ in i18n.extract(BytesIO(src.encode('utf-8')), None, None, {
+                'strip_text': strip_text
+            }):
+                messages[msgid] = 'TRANSLATED(%s)' % msgid
+
+            # Provide a fake translation function
+            default_gettext = i18n.gettext
+            i18n.gettext = lambda s: messages[s]
+            try:
+                perform(src, expected[strip_text], strip_text=strip_text)
+            finally:
+                i18n.gettext = default_gettext
+
+
+class TestErrorReporting(TestCase):
+    def test_syntax_error(self):
+        for strip_text in (False, True):
+            try:
+                perform('<div py:for="i i range(1, 2)">${i}</div>', '', strip_text=strip_text)
+            except KajikiSyntaxError as exc:
+                assert '-->         for i i range(1, 2):' in str(exc), exc
+            else:
+                assert False
+
+    def test_code_error(self):
+        for strip_text in (False, True):
+            try:
+                child = FileLoader(
+                    os.path.join(os.path.dirname(__file__), 'data')
+                ).load('error.html', strip_text=strip_text)
+                child().render()
+            except ZeroDivisionError as exc:
+                import traceback, sys
+                l = traceback.format_exception(*sys.exc_info())
+                last_line = l[-2]
+                assert '${3/0}' in last_line, last_line
+            else:
+                assert False
+
+
 if __name__ == '__main__':
     main()
diff --git a/kajiki/text.py b/kajiki/text.py
index e85d1fb..1cbe583 100644
--- a/kajiki/text.py
+++ b/kajiki/text.py
@@ -156,7 +156,9 @@ class _Scanner(object):
         try:
             compile(self.source[self.pos:], '', 'eval')
         except SyntaxError as se:
-            end = se.offset + self.pos
+            end = self.pos + sum([se.offset] + [len(line) + 1
+                                                for idx, line in enumerate(self.source[self.pos:].splitlines())
+                                                if idx < se.lineno - 1])
             text = self.source[self.pos:end - 1]
             self.pos = end
             return self.expr(text)
diff --git a/kajiki/util.py b/kajiki/util.py
index 7604856..bf91401 100644
--- a/kajiki/util.py
+++ b/kajiki/util.py
@@ -61,7 +61,7 @@ class flattener(object):
         iter_stack = [self.iterator]
         while iter_stack:
             try:
-                x = iter_stack[-1].next()
+                x = next(iter_stack[-1])
             except StopIteration:
                 iter_stack.pop()
                 continue
diff --git a/kajiki/version.py b/kajiki/version.py
index 1606760..09418bb 100644
--- a/kajiki/version.py
+++ b/kajiki/version.py
@@ -2,5 +2,5 @@
 from __future__ import (absolute_import, division, print_function,
                         unicode_literals)
 
-__version__ = '0.5'
-__release__ = '0.5.5'
+__version__ = '0.6'
+__release__ = '0.6.1'
diff --git a/kajiki/xml_template.py b/kajiki/xml_template.py
index befb26d..27ba86d 100644
--- a/kajiki/xml_template.py
+++ b/kajiki/xml_template.py
@@ -28,14 +28,21 @@ impl = dom.getDOMImplementation(' ')
 
 
 def XMLTemplate(source=None, filename=None, mode=None, is_fragment=False,
-                encoding='utf-8', autoblocks=None, cdata_scripts=True):
-    """Given XML source code of a Kajiki Templates parses returns a Template class.
-
-    The source code is parsed to its DOM representation, which is then
-    expanded to separate directives from tags and then compiled to the
-    Intermediate Representation tree. The Intermediate Representation
-    is then processed to create the Python code which defined the template
-    class of which a new instance is returned.
+                encoding='utf-8', autoblocks=None, cdata_scripts=True,
+                strip_text=False):
+    """Given XML source code of a Kajiki Templates parses and returns a template class.
+
+    The source code is parsed to its DOM representation by :class:`._Parser`,
+    which is then expanded to separate directives from tags by :class:`._DomTransformer`
+    and then compiled to the *Intermediate Representation* tree by :class:`._Compiler`.
+
+    The *Intermediate Representation* generates the Python code
+    which creates a new :class:`kajiki.template._Template` subclass through
+    :meth:`kajiki.template.Template`.
+
+    The generated code is then executed to return the newly created class.
+
+    Calling ``.render()`` on an instance of the generate class will then render the template.
     """
     if source is None:
         with open(filename, encoding=encoding) as f:
@@ -43,10 +50,9 @@ def XMLTemplate(source=None, filename=None, mode=None, is_fragment=False,
     if filename is None:
         filename = '<string>'
     doc = _Parser(filename, source).parse()
-    expand(doc)
-    compiler = _Compiler(filename, doc, mode=mode, is_fragment=is_fragment,
-                         autoblocks=autoblocks, cdata_scripts=cdata_scripts)
-    ir_ = compiler.compile()
+    doc = _DomTransformer(doc, strip_text=strip_text).transform()
+    ir_ = _Compiler(filename, doc, mode=mode, is_fragment=is_fragment,
+                    autoblocks=autoblocks, cdata_scripts=cdata_scripts).compile()
     return template.from_ir(ir_)
 
 
@@ -59,7 +65,7 @@ def annotate(gen):
 
 
 class _Compiler(object):
-    """Compiles a DOM tree into Intermediate Representation TemplateNode.
+    """Compiles a DOM tree into Intermediate Representation :class:`kajiki.ir.TemplateNode`.
 
     Intermediate Representation is a tree of nodes that represent
     Python Code that should be generated to execute the template.
@@ -88,6 +94,26 @@ class _Compiler(object):
             self.mode = 'xml'  # by default
 
     def compile(self):
+        """Compile the document provided by :class:`._Parser`.
+
+        Returns as :class:`kajiki.ir.TemplateNode` instance representing
+        the whole tree of nodes as their intermediate representation.
+
+        The returned template will include at least a ``__main__``
+        function which is the document itself including a DOCTYPE and
+        any function declared through ``py:def`` or as a ``py:block``.
+
+        The ``TemplateNode`` will also include the module level
+        code specified through ``<?py %``.
+
+        If the compiled document didn't specify a DOCTYPE provides
+        one at least for HTML5.
+
+        .. note::
+            As this alters the functions and mode wide code
+            registries of the compiler ``compile`` should
+            never be called twice or might lead to unexpected results.
+        """
         body = list(self._compile_node(self.doc.firstChild))
         # Never emit doctypes on fragments
         if not self.is_fragment and not self.is_child:
@@ -98,7 +124,7 @@ class _Compiler(object):
             else:
                 dtd = None
             if dtd:
-                dtd = ir.TextNode(dtd)
+                dtd = ir.TextNode(dtd.strip()+'\n')
                 dtd.filename = self.filename
                 dtd.lineno = 1
                 body.insert(0, dtd)
@@ -136,6 +162,14 @@ class _Compiler(object):
         return True
 
     def _compile_node(self, node):
+        """Convert a DOM node to its intermediate representation.
+
+        Calls specific compile functions for special nodes and any
+        directive that was expanded by :meth:`._DomTransformer._expand_directives`.
+        For any plain XML node forward it to :meth:`._compile_xml`.
+
+        Automatically converts any ``autoblock`` node to a ``py:block`` directive.
+        """
         if isinstance(node, dom.Comment):
             return self._compile_comment(node)
         elif isinstance(node, dom.Text):
@@ -157,6 +191,22 @@ class _Compiler(object):
 
     @annotate
     def _compile_xml(self, node):
+        """Compile plain XML nodes.
+
+        When compiling a node also take care of directives that
+        only modify the node itself (``py:strip``, ``py:attrs``
+        and ``py:content``) as all directives wrapping the node
+        and its children have already been handled by :meth:`._compile_node`.
... 435 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-kajiki.git



More information about the Python-modules-commits mailing list