[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