# HG changeset patch # User cmlenz # Date 1236793866 0 # Node ID 1837f39efd6fe1e812e8c185dbdf4a91afeac991 # Parent 0742f421caba747967488b1f8fd443931162b959 Sync (old) experimental inline branch with trunk@1027. diff --git a/COPYING b/COPYING --- a/COPYING +++ b/COPYING @@ -1,4 +1,4 @@ -Copyright (C) 2006-2007 Edgewall Software +Copyright (C) 2006-2008 Edgewall Software All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/ChangeLog b/ChangeLog --- a/ChangeLog +++ b/ChangeLog @@ -1,18 +1,189 @@ +Version 0.6 +http://svn.edgewall.org/repos/genshi/tags/0.6.0/ +(???, from branches/stable/0.6.x) + + * Support for Python 2.3 has been dropped. + + +Version 0.5.2 +http://svn.edgewall.org/repos/genshi/tags/0.5.2/ +(???, from branches/stable/0.5.x) + + * Fix problem with I18n filter that would get confused by expressions in + attribute values when inside an `i18n:msg` block (ticket #250). + * Fix problem with the transformation filter dropping events after the + selection (ticket #290). + * `for` loops in template code blocks no longer establish their own locals + scope, meaning you can now access variables assigned in the loop outside + of the loop, just as you can in regular Python code (ticket #259). + * Import statements inside function definitions in template code blocks no + longer result in an UndefinedError when the imported name is accessed + (ticket #276). + + +Version 0.5.1 +http://svn.edgewall.org/repos/genshi/tags/0.5.1/ +(Jul 9 2008, from branches/stable/0.5.x) + + * Fix problem with nested match templates not being applied when buffering + on the outer `py:match` is disabled. Thanks to Erik Bray for reporting the + problem and providing a test case! + * Fix problem in `Translator` filter that would cause the translation of + text nodes to fail if the translation function returned an object that was + not directly a string, but rather something like an instance of the + `LazyProxy` class in Babel (ticket #145). + * Fix problem with match templates incorrectly being applied multiple times. + * Includes from templates loaded via an absolute path now include the correct + file in nested directories as long if no search path has been configured + (ticket #240). + * Unbuffered match templates could result in parts of the matched content + being included in the output if the match template didn't actually consume + it via one or more calls to the `select()` function (ticket #243). + + Version 0.5 http://svn.edgewall.org/repos/genshi/tags/0.5.0/ -(?, from branches/stable/0.5.x) +(Jun 9 2008, from branches/stable/0.5.x) * Added #include directive for text templates (ticket #115). + * Added new markup transformation filter contributed by Alec Thomas. This + provides gorgeous jQuery-inspired stream transformation capabilities based + on XPath expressions. + * When using HTML or XHTML serialization, the `xml:lang` attribute is + automatically translated to the `lang` attribute which HTML user agents + understand. + * Added support for the XPath 2 `matches()` function in XPath expressions, + which allow matching against regular expressions. + * Support for Python code blocks in templates can now be disabled + (ticket #123). + * Includes are now processed when the template is parsed if possible, but + only if the template loader is not set to do automatic reloading. Included + templates are basically inlined into the including template, which can + speed up rendering of that template a bit. + * Added new syntax for text templates, which is more powerful and flexible + with respect to white-space and line breaks. It also supports Python code + blocks. The old syntax is still available and the default for now, but in a + future release the new syntax will become the default, and some time after + that the old syntax will be removed. + * Added support for passing optimization hints to `` directives, + which can speed up match templates in many cases, for example when a match + template should only be applied once to a stream, or when it should not be + applied recursively. + * Text templates now default to rendering as plain text; it is no longer + necessary to explicitly specify the "text" method to the `render()` or + `serialize()` method of the generated markup stream. + * XInclude elements in markup templates now support the `parse` attribute; + when set to "xml" (the default), the include is processed as before, but + when set to "text", the included template is parsed as a text template using + the new syntax (ticket #101). + * Python code blocks inside match templates are now executed (ticket #155). + * The template engine plugin no longer adds the `default_doctype` when the + `fragment` parameter is `True`. + * The `striptags` function now also removes HTML/XML-style comments (ticket + #150). + * The `py:replace` directive can now also be used as an element, with an + attribute named `value` (ticket #144). + * The `TextSerializer` class no longer strips all markup in text by default, + so that it is still possible to use the Genshi `escape` function even with + text templates. The old behavior is available via the `strip_markup` option + of the serializer (ticket #146). + * Assigning to a variable named `data` in a Python code block no longer + breaks context lookup. + * The `Stream.render` now accepts an optional `out` parameter that can be + used to pass in a writable file-like object to use for assembling the + output, instead of building a big string and returning it. + * The XHTML serializer now strips `xml:space` attributes as they are only + allowed on very few tags. + * Match templates are now applied in a more controlled fashion: in the order + they are declared in the template source, all match templates up to (and + including) the matching template itself are applied to the matched content, + whereas the match templates declared after the matching template are only + applied to the generated content (ticket #186). + * The `TemplateLoader` class now provides an `_instantiate()` method that can + be overridden by subclasses to implement advanced template instantiation + logic (ticket #204). + * The search path of the `TemplateLoader` class can now contain ''load + functions'' in addition to path strings. A load function is passed the + name of the requested template file, and should return a file-like object + and some metadata. New load functions are supplied for loading from egg + package data, and loading from different loaders depending on the path + prefix of the requested filename (ticket #182). + * Match templates can now be processed without keeping the complete matched + content in memory, which could cause excessive memory use on long pages. + The buffering can be disabled using the new `buffer` optimization hint on + the `` directive. + * Improve error reporting when accessing an attribute in a Python expression + raises an `AttributeError` (ticket #191). + * The `Markup` class now supports mappings for right hand of the `%` (modulo) + operator in the same way the Python string classes do, except that the + substituted values are escape. Also, the special constructor which took + positional arguments that would be substituted was removed. Thus the + `Markup` class now supports the same arguments as that of its `unicode` + base class (ticket #211). + * The `Template` class and its subclasses, as well as the interpolation API, + now take an `filepath` parameter instead of `basedir` (ticket #207). + * The `XHTMLSerializer` now has a `drop_xml_decl` option that defaults to + `True`. Setting it to `False` will cause any XML decl in the serialized + stream to be included in the output as it would for XML serialization. + * Add support for a protocol that would allow interoperability of different + Python packages that generate and/or consume markup, based on the special + `__html__()` method (ticket #202). + + +Version 0.4.4 +http://svn.edgewall.org/repos/genshi/tags/0.4.4/ +(Aug 14, 2007, from branches/stable/0.4.x) + + * Fixed augmented assignment to local variables in Python code blocks. + * Fixed handling of nested function and class definitions in Python code + blocks. + * Includes were not raising `TemplateNotFound` exceptions even when no + fallback has been specified. That has been corrected. + * The template loader now raises a `TemplateNotFound` error when a previously + cached template is removed or renamed, where it previously was passing up + an `OSError`. + * The Genshi I18n filter can be configured to only extract messages found in + `gettext` function calls, ignoring any text nodes and attribute values + (ticket #138). + + +Version 0.4.3 +http://svn.edgewall.org/repos/genshi/tags/0.4.3/ +(Jul 17 2007, from branches/stable/0.4.x) + + * The I18n filter no longer extracts or translates literal strings in + attribute values that also contain expressions. + * Added `loader_callback` option to plugin interface, which allows specifying + a callback function that the template loader should invoke whenever a new + template is loaded (ticket #130). Note that the value for this option can + not be specified as a string, only as an actual function object, which means + it is not available for use through configuration files. + * The I18n filter now extracts messages from gettext functions even inside + ignored tags (ticket #132). + * The HTML sanitizer now strips any CSS comments in style attributes, which + could previously be used to hide malicious property values. + * The HTML sanitizer now also removes any HTML comments encountered, as those + may be used to hide malicious payloads targetting a certain "innovative" + browser that goes and interprets the content of specially prepared comments. + * Attribute access in template expressions no longer silently ignores + exceptions other than `AttributeError` raised in the attribute accessor. Version 0.4.2 http://svn.edgewall.org/repos/genshi/tags/0.4.2/ -(?, from branches/stable/0.4.x) +(Jun 20 2007, from branches/stable/0.4.x) * The `doctype` parameter of the markup serializers now also accepts the "name" of the doctype as string, in addition to the `(name, pubid, sysid)` tuple. * The I18n filter was not replacing the original attributes with the translation, but instead adding a second attribute with the same name. + * `TextTemplate` can now handle unicode source (ticket #125). + * A `` processing instruction containing trailing whitespace no + longer causes a syntax error (ticket #127). + * The I18n filter now skips the content of elements that have an `xml:lang` + attribute with a fixed string value. Basically, `xml:lang` can now be used + as a flag to mark specific sections as not needing localization. + * Added plugin for message extraction via Babel (http://babel.edgewall.org/). Version 0.4.1 diff --git a/INSTALL.txt b/INSTALL.txt deleted file mode 100644 --- a/INSTALL.txt +++ /dev/null @@ -1,37 +0,0 @@ -Installing Genshi -================= - -Prerequisites -------------- - - * Python 2.3 or later (2.4 or later is recommended) - * Optional: setuptools 0.6a2 or later - - -Installation ------------- - -Once you've downloaded and unpacked a Genshi source release, enter the -directory where the archive was unpacked, and run: - - $ python setup.py install - -Note that you may need administrator/root privileges for this step, as -this command will by default attempt to install Genshi to the Python -site-packages directory on your system. - -For advanced options, please refer to the easy_install and/or the distutils -documentation: - - http://peak.telecommunity.com/DevCenter/EasyInstall - http://docs.python.org/inst/inst.html - - -Support -------- - -If you encounter any problems with Genshi, please don't hesitate to ask -questions on the Genshi mailing list or IRC channel: - - http://genshi.edgewall.org/wiki/MailingList - http://genshi.edgewall.org/wiki/IrcChannel diff --git a/README.txt b/README.txt --- a/README.txt +++ b/README.txt @@ -1,11 +1,12 @@ About Genshi ============ -Genshi is a Python library that provides an integrated set of components -for parsing, generating, and processing HTML, XML or other textual -content for output generation on the web. The major feature is a -template language, which is heavily inspired by Kid. +Genshi is a Python library that provides an integrated set of +components for parsing, generating, and processing HTML, XML or other +textual content for output generation on the web. The major feature is +a template language, which is heavily inspired by Kid. -For more information please visit the Genshi web site: +For more information please see the documentation in the `doc` +directory, and visit the Genshi web site: diff --git a/UPGRADE.txt b/UPGRADE.txt deleted file mode 100644 --- a/UPGRADE.txt +++ /dev/null @@ -1,51 +0,0 @@ -Upgrading Genshi -================ - -Upgrading from Genshi 0.3.x to 0.4.x ------------------------------------- - -The modules ``genshi.filters`` and ``genshi.template`` have been -refactored into packages containing multiple modules. While code using -the regular APIs should continue to work without problems, you should -make sure to remove any leftover traces of the ``template.py`` file on -the installation path. This is not necessary when Genshi was installed -as a Python egg. - -Results of evaluating template expressions are no longer implicitly -called if they are callable. If you have been using that feature, you -will need to add the parenthesis to actually call the function. - -Instances of `genshi.core.Attrs` are now immutable. Filters -manipulating the attributes in a stream may need to be updated. Also, -the `Attrs` class no longer automatically wraps all attribute names -in `QName` objects, so users of the `Attrs` class need to do this -themselves. See the documentation of the `Attrs` class for more -information. - - -Upgrading from Markup ---------------------- - -Prior to version 0.3, the name of the Genshi project was "Markup". The -name change means that you will have to adjust your import statements -and the namespace URI of XML templates, among other things: - - * The package name was changed from "markup" to "genshi". Please - adjust any import statements referring to the old package name. - * The namespace URI for directives in Genshi XML templates has changed - from http://markup.edgewall.org/ to http://genshi.edgewall.org/. - Please update the xmlns:py declaration in your template files - accordingly. - -Furthermore, due to the inclusion of a text-based template language, -the class: - - `markup.template.Template` - -has been renamed to: - - `genshi.template.MarkupTemplate` - -If you've been using the Template class directly, you'll need to -update your code (a simple find/replace should do--the API itself -did not change). diff --git a/doc/2000ft.graffle b/doc/2000ft.graffle --- a/doc/2000ft.graffle +++ b/doc/2000ft.graffle @@ -201,7 +201,7 @@ {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural -\f0\fs22 \cf0 ...} +\f0\fs22 \cf0 Transformer} @@ -1249,7 +1249,7 @@ ModificationDate - 2007-04-13 15:15:25 +0200 + 2007-06-17 13:12:07 +0200 Modifier Christopher Lenz NotesVisible diff --git a/doc/2000ft.png b/doc/2000ft.png index d0209916dd84c14e852056736dffa991c1cc1506..b4ce041211763686822d58697d549d3a48137327 GIT binary patch literal 29368 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 from genshi.filters import HTMLFormFiller >>> from genshi.template import MarkupTemplate + >>> template = MarkupTemplate("""
    ...

    ...

    ...

    Innocent looking text.

    ... @@ -112,10 +119,13 @@ filter will still perform sanitization on the contents any encountered inline styles: the proprietary ``expression()`` function (supported only by Internet Explorer) is removed, and any property using an ``url()`` which a potentially -dangerous URL scheme (such as ``javascript:``) are also stripped out:: +dangerous URL scheme (such as ``javascript:``) are also stripped out: + +.. code-block:: pycon >>> from genshi.filters import HTMLSanitizer >>> from genshi.input import HTML + >>> html = HTML("""
    ...
    ...
    """) @@ -130,3 +140,104 @@ suspect to various browser bugs. If you can somehow get away with not allowing inline styles in user-submitted content, that would definitely be the safer route to follow. + + +Transformer +=========== + +The filter ``genshi.filters.transform.Transformer`` provides a convenient way to +transform or otherwise work with markup event streams. It allows you to specify +which parts of the stream you're interested in with XPath expressions, and then +attach a variety of transformations to the parts that match: + +.. code-block:: pycon + + >>> from genshi.builder import tag + >>> from genshi.core import TEXT + >>> from genshi.filters import Transformer + >>> from genshi.input import HTML + + >>> html = HTML(''' + ... Some Title + ... + ... Some body text. + ... + ... ''') + + >>> print html | Transformer('body/em').map(unicode.upper, TEXT) \ + ... .unwrap().wrap(tag.u).end() \ + ... .select('body/u') \ + ... .prepend('underlined ') + + Some Title + + Some underlined BODY text. + + + +This example sets up a transformation that: + + 1. matches any `` element anywhere in the body, + 2. uppercases any text nodes in the element, + 3. strips off the `` start and close tags, + 4. wraps the content in a `` tag, and + 5. inserts the text `underlined` inside the `` tag. + +A number of commonly useful transformations are available for this filter. +Please consult the API documentation a complete list. + +In addition, you can also perform custom transformations. For example, the +following defines a transformation that changes the name of a tag: + +.. code-block:: pycon + + >>> from genshi import QName + >>> from genshi.filters.transform import ENTER, EXIT + + >>> class RenameTransformation(object): + ... def __init__(self, name): + ... self.name = QName(name) + ... def __call__(self, stream): + ... for mark, (kind, data, pos) in stream: + ... if mark is ENTER: + ... data = self.name, data[1] + ... elif mark is EXIT: + ... data = self.name + ... yield mark, (kind, data, pos) + +A transformation can be any callable object that accepts an augmented event +stream. In this case we define a class, so that we can initialize it with the +tag name. + +Custom transformations can be applied using the `apply()` method of a +transformer instance: + +.. code-block:: pycon + + >>> xform = Transformer('body//em').map(unicode.upper, TEXT) \ + >>> xform = xform.apply(RenameTransformation('u')) + >>> print html | xform + + Some Title + + Some BODY text. + + + +.. note:: The transformation filter was added in Genshi 0.5. + + +Translator +========== + +The ``genshi.filters.i18n.Translator`` filter implements basic support for +internationalizing and localizing templates. When used as a filter, it +translates a configurable set of text nodes and attribute values using a +``gettext``-style translation function. + +The ``Translator`` class also defines the ``extract`` class method, which can +be used to extract localizable messages from a template. + +Please refer to the API documentation for more information on this filter. + +.. note:: The translation filter was added in Genshi 0.4. diff --git a/doc/i18n.txt b/doc/i18n.txt new file mode 100644 --- /dev/null +++ b/doc/i18n.txt @@ -0,0 +1,248 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +===================================== +Internationalization and Localization +===================================== + +Genshi provides basic supporting infrastructure for internationalizing +and localizing templates. That includes functionality for extracting localizable +strings from templates, as well as a template filter that can apply translations +to templates as they get rendered. + +This support is based on `gettext`_ message catalogs and the `gettext Python +module`_. The extraction process can be used from the API level, or through the +front-ends implemented by the `Babel`_ project, for which Genshi provides a +plugin. + +.. _`gettext`: http://www.gnu.org/software/gettext/ +.. _`gettext python module`: http://docs.python.org/lib/module-gettext.html +.. _`babel`: http://babel.edgewall.org/ + + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +Basics +====== + +The simplest way to internationalize and translate templates would be to wrap +all localizable strings in a ``gettext()`` function call (which is often aliased +to ``_()`` for brevity). In that case, no extra template filter is required. + +.. code-block:: genshi + +

    ${_("Hello, world!")}

    + +However, this approach results in significant “character noise” in templates, +making them harder to read and preview. + +The ``genshi.filters.Translator`` filter allows you to get rid of the +explicit `gettext`_ function calls, so you can continue to just write: + +.. code-block:: genshi + +

    Hello, world!

    + +This text will still be extracted and translated as if you had wrapped it in a +``_()`` call. + +.. note:: For parameterized or pluralizable messages, you need to continue using + the appropriate ``gettext`` functions. + +You can control which tags should be ignored by this process; for example, it +doesn't really make sense to translate the content of the HTML +```` element. Both ````. Also, it will normalize any boolean attributes values + that are minimized in HTML, so that for example ``
    `` + becomes ``
    ``. + + This serializer supports the use of namespaces for compound documents, for + example to use inline SVG inside an XHTML document. + +``html`` + The ``HTMLSerializer`` produces proper HTML markup. The main differences + compared to ``xhtml`` serialization are that boolean attributes are + minimized, empty tags are not self-closing (so it's ``
    `` instead of + ``
    ``), and that the contents of ``') + >>> print tmpl.generate() + + +On the other hand, Genshi will always replace two dollar signs in text with a +single dollar sign, so you'll need to use three dollar signs to get two in the +output: + +.. code-block:: pycon + + >>> tmpl = MarkupTemplate('') + >>> print tmpl.generate() + + + .. _`code blocks`: Code Blocks =========== -XML templates also support full Python code blocks using the ```` -processing instruction:: +Templates also support full Python code blocks, using the ```` +processing instruction in XML templates: + +.. code-block:: genshi
    + return tag.b('Hello, %s!' % name) ?> ${greeting('world')}
    -This will produce the following output:: +This will produce the following output: + +.. code-block:: xml
    Hello, world!
    +In text templates (although only those using the new syntax introduced in +Genshi 0.5), code blocks use the special ``{% python %}`` directive: + +.. code-block:: genshitext + + {% python + from genshi.builder import tag + def greeting(name): + return 'Hello, %s!' % name + %} + ${greeting('world')} + +This will produce the following output:: + + Hello, world! + + Code blocks can import modules, define classes and functions, and basically do anything you can do in normal Python code. What code blocks can *not* do is to -produce content that is included directly in the generated page. +produce content that is emitted directly tp the generated output. .. note:: Using the ``print`` statement will print to the standard output stream, just as it does for other Python code in your application. @@ -220,7 +300,13 @@ design. If you're using many code blocks, that may be a sign that you should move such code into separate Python modules. -.. note:: Code blocks are not currently supported in text templates. +If you'd rather not allow the use of Python code blocks in templates, you can +simply set the ``allow_exec`` parameter (available on the ``Template`` and the +``TemplateLoader`` initializers) to ``False``. In that case Genshi will raise +a syntax error when a ```` processing instruction is encountered. +But please note that disallowing code blocks in templates does not turn Genshi +into a sandboxable template engine; there are sufficient ways to do harm even +using plain expressions. .. _`error handling`: @@ -228,62 +314,85 @@ Error Handling ============== -By default, Genshi allows you to access variables that are not defined, without -raising a ``NameError`` exception as regular Python code would:: +By default, Genshi raises an ``UndefinedError`` if a template expression +attempts to access a variable that is not defined: + +.. code-block:: pycon >>> from genshi.template import MarkupTemplate >>> tmpl = MarkupTemplate('

    ${doh}

    ') + >>> tmpl.generate().render('xhtml') + Traceback (most recent call last): + ... + UndefinedError: "doh" not defined + +You can change this behavior by setting the variable lookup mode to "lenient". +In that case, accessing undefined variables returns an `Undefined` object, +meaning that the expression does not fail immediately. See below for details. + +If you need to check whether a variable exists in the template context, use the +defined_ or the value_of_ function described below. To check for existence of +attributes on an object, or keys in a dictionary, use the ``hasattr()``, +``getattr()`` or ``get()`` functions, or the ``in`` operator, just as you would +in regular Python code: + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('

    ${defined("doh")}

    ') + >>> print tmpl.generate().render('xhtml') +

    False

    + +.. note:: Lenient error handling was the default in Genshi prior to version 0.5. + Strict mode was introduced in version 0.4, and became the default in + 0.5. The reason for this change was that the lenient error handling + was masking actual errors in templates, thereby also making it harder + to debug some problems. + + +.. _`lenient`: + +Lenient Mode +------------ + +If you instruct Genshi to use the lenient variable lookup mode, it allows you +to access variables that are not defined, without raising an ``UndefinedError``. + +This mode can be chosen by passing the ``lookup='lenient'`` keyword argument to +the template initializer, or by passing the ``variable_lookup='lenient'`` +keyword argument to the ``TemplateLoader`` initializer: + +.. code-block:: pycon + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('

    ${doh}

    ', lookup='lenient') >>> print tmpl.generate().render('xhtml')

    You *will* however get an exception if you try to call an undefined variable, or -do anything else with it, such as accessing its attributes:: +do anything else with it, such as accessing its attributes: + +.. code-block:: pycon >>> from genshi.template import MarkupTemplate - >>> tmpl = MarkupTemplate('

    ${doh.oops}

    ') + >>> tmpl = MarkupTemplate('

    ${doh.oops}

    ', lookup='lenient') >>> print tmpl.generate().render('xhtml') Traceback (most recent call last): ... UndefinedError: "doh" not defined If you need to know whether a variable is defined, you can check its type -against the ``Undefined`` class, for example in a conditional directive:: +against the ``Undefined`` class, for example in a conditional directive: + +.. code-block:: pycon >>> from genshi.template import MarkupTemplate - >>> tmpl = MarkupTemplate('

    ${type(doh) is not Undefined}

    ') + >>> tmpl = MarkupTemplate('

    ${type(doh) is not Undefined}

    ', + ... lookup='lenient') >>> print tmpl.generate().render('xhtml')

    False

    Alternatively, the built-in functions defined_ or value_of_ can be used in this case. -Strict Mode ------------ - -In addition to the default "lenient" error handling, Genshi lets you use a less -forgiving mode if you prefer errors blowing up loudly instead of being ignored -silently. - -This mode can be chosen by passing the ``lookup='strict'`` keyword argument to -the template initializer, or by passing the ``variable_lookup='strict'`` keyword -argument to the ``TemplateLoader`` initializer:: - - >>> from genshi.template import MarkupTemplate - >>> tmpl = MarkupTemplate('

    ${doh}

    ', lookup='strict') - >>> print tmpl.generate().render('xhtml') - Traceback (most recent call last): - ... - UndefinedError: "doh" not defined - -When using strict mode, any reference to an undefined variable, as well as -trying to access an non-existing item or attribute of an object, will cause an -``UndefinedError`` to be raised immediately. - -.. note:: While this mode is currently not the default, it may be promoted to - the default in future versions of Genshi. In general, the default - lenient error handling mode can be considered dangerous as it silently - ignores typos. - Custom Modes ------------ diff --git a/doc/text-templates.txt b/doc/text-templates.txt --- a/doc/text-templates.txt +++ b/doc/text-templates.txt @@ -6,10 +6,7 @@ In addition to the XML-based template language, Genshi provides a simple text-based template language, intended for basic plain text generation needs. -The language is similar to Cheetah_ or Velocity_. - -.. _cheetah: http://cheetahtemplate.org/ -.. _velocity: http://jakarta.apache.org/velocity/ +The language is similar to the Django_ template language. This document describes the template language and will be most useful as reference to those developing Genshi text templates. Templates are text files of @@ -20,6 +17,13 @@ See `Genshi Templating Basics `_ for general information on embedding Python code in templates. +.. note:: Actually, Genshi currently has two different syntaxes for text + templates languages: One implemented by the class ``OldTextTemplate`` + and another implemented by ``NewTextTemplate``. This documentation + concentrates on the latter, which is planned to completely replace the + older syntax. The older syntax is briefly described under legacy_. + +.. _django: http://www.djangoproject.com/ .. contents:: Contents :depth: 3 @@ -32,36 +36,35 @@ Template Directives ------------------- -Directives are lines starting with a ``#`` character followed immediately by -the directive name. They can affect how the template is rendered in a number of -ways: Genshi provides directives for conditionals and looping, among others. +Directives are template commands enclosed by ``{% ... %}`` characters. They can +affect how the template is rendered in a number of ways: Genshi provides +directives for conditionals and looping, among others. -Directives must be on separate lines, and the ``#`` character must be be the -first non-whitespace character on that line. Each directive must be “closed” -using a ``#end`` marker. You can add after the ``#end`` marker, for example to -document which directive is being closed, or even the expression associated with -that directive. Any text after ``#end`` (but on the same line) is ignored, -and effectively treated as a comment. +Each directive must be terminated using an ``{% end %}`` marker. You can add +a string inside the ``{% end %}`` marker, for example to document which +directive is being closed, or even the expression associated with that +directive. Any text after ``end`` inside the delimiters is ignored, and +effectively treated as a comment. -If you want to include a literal ``#`` in the output, you need to escape it -by prepending a backslash character (``\``). Note that this is **not** required -if the ``#`` isn't immediately followed by a letter, or it isn't the first -non-whitespace character on the line. +If you want to include a literal delimiter in the output, you need to escape it +by prepending a backslash character (``\``). Conditional Sections ==================== -.. _`#if`: - -``#if`` ---------- +.. _`if`: -The content is only rendered if the expression evaluates to a truth value:: +``{% if %}`` +------------ - #if foo +The content is only rendered if the expression evaluates to a truth value: + +.. code-block:: genshitext + + {% if foo %} ${bar} - #end + {% end %} Given the data ``foo=True`` and ``bar='Hello'`` in the template context, this would produce:: @@ -69,54 +72,46 @@ Hello -.. _`#choose`: -.. _`#when`: -.. _`#otherwise`: - -``#choose`` -------------- +.. _`choose`: +.. _`when`: +.. _`otherwise`: -The ``#choose`` directive, in combination with the directives ``#when`` and -``#otherwise`` provides advanced contional processing for rendering one of -several alternatives. The first matching ``#when`` branch is rendered, or, if -no ``#when`` branch matches, the ``#otherwise`` branch is be rendered. +``{% choose %}`` +---------------- -If the ``#choose`` directive has no argument the nested ``#when`` directives -will be tested for truth:: +The ``choose`` directive, in combination with the directives ``when`` and +``otherwise``, provides advanced contional processing for rendering one of +several alternatives. The first matching ``when`` branch is rendered, or, if +no ``when`` branch matches, the ``otherwise`` branch is be rendered. + +If the ``choose`` directive has no argument the nested ``when`` directives will +be tested for truth: + +.. code-block:: genshitext The answer is: - #choose - #when 0 == 1 - 0 - #end - #when 1 == 1 - 1 - #end - #otherwise - 2 - #end - #end + {% choose %} + {% when 0 == 1 %}0{% end %} + {% when 1 == 1 %}1{% end %} + {% otherwise %}2{% end %} + {% end %} This would produce the following output:: The answer is: - 1 + 1 -If the ``#choose`` does have an argument, the nested ``#when`` directives will -be tested for equality to the parent ``#choose`` value:: +If the ``choose`` does have an argument, the nested ``when`` directives will +be tested for equality to the parent ``choose`` value: + +.. code-block:: genshitext The answer is: - #choose 1 - #when 0 - 0 - #end - #when 1 - 1 - #end - #otherwise - 2 - #end - #end + {% choose 1 %}\ + {% when 0 %}0{% end %}\ + {% when 1 %}1{% end %}\ + {% otherwise %}2{% end %}\ + {% end %} This would produce the following output:: @@ -127,17 +122,19 @@ Looping ======= -.. _`#for`: +.. _`for`: -``#for`` ----------- +``{% for %}`` +------------- -The content is repeated for every item in an iterable:: +The content is repeated for every item in an iterable: + +.. code-block:: genshitext Your items: - #for item in items + {% for item in items %}\ * ${item} - #end + {% end %} Given ``items=[1, 2, 3]`` in the context data, this would produce:: @@ -150,19 +147,21 @@ Snippet Reuse ============= -.. _`#def`: +.. _`def`: .. _`macros`: -``#def`` ----------- +``{% def %}`` +------------- -The ``#def`` directive can be used to create macros, i.e. snippets of template +The ``def`` directive can be used to create macros, i.e. snippets of template text that have a name and optionally some parameters, and that can be inserted -in other places:: +in other places: - #def greeting(name) +.. code-block:: genshitext + + {% def greeting(name) %} Hello, ${name}! - #end + {% end %} ${greeting('world')} ${greeting('everyone else')} @@ -171,13 +170,15 @@ Hello, world! Hello, everyone else! -If a macro doesn't require parameters, it can be defined as well as called -without the parenthesis. For example:: +If a macro doesn't require parameters, it can be defined without the +parenthesis. For example: - #def greeting +.. code-block:: genshitext + + {% def greeting %} Hello, world! - #end - ${greeting} + {% end %} + ${greeting()} The above would be rendered to:: @@ -185,15 +186,17 @@ .. _includes: -.. _`#include`: +.. _`include`: -``#include`` ------------- +``{% include %}`` +----------------- To reuse common parts of template text across template files, you can include -other files using the ``#include`` directive:: +other files using the ``include`` directive: - #include "base.txt" +.. code-block:: genshitext + + {% include base.txt %} Any content included this way is inserted into the generated output. The included template sees the context data as it exists at the point of the @@ -206,11 +209,13 @@ the included file at "``myapp/base.txt``". You can also use Unix-style relative paths, for example "``../base.txt``" to look in the parent directory. -Just like other directives, the argument to the ``#include`` directive accepts +Just like other directives, the argument to the ``include`` directive accepts any Python expression, so the path to the included template can be determined -dynamically:: +dynamically: - #include '%s.txt' % filename +.. code-block:: genshitext + + {% include ${'%s.txt' % filename} %} Note that a ``TemplateNotFound`` exception is raised if an included file can't be found. @@ -221,23 +226,25 @@ Variable Binding ================ -.. _`#with`: +.. _`with`: -``#with`` ------------ +``{% with %}`` +-------------- -The ``#with`` directive lets you assign expressions to variables, which can +The ``{% with %}`` directive lets you assign expressions to variables, which can be used to make expressions inside the directive less verbose and more efficient. For example, if you need use the expression ``author.posts`` more than once, and that actually results in a database query, assigning the results to a variable using this directive would probably help. -For example:: +For example: + +.. code-block:: genshitext Magic numbers! - #with y=7; z=x+10 + {% with y=7; z=x+10 %} $x $y $z - #end + {% end %} Given ``x=42`` in the context data, this would produce:: @@ -245,17 +252,109 @@ 42 7 52 Note that if a variable of the same name already existed outside of the scope -of the ``#with`` directive, it will **not** be overwritten. Instead, it will -have the same value it had prior to the ``#with`` assignment. Effectively, +of the ``with`` directive, it will **not** be overwritten. Instead, it will +have the same value it had prior to the ``with`` assignment. Effectively, this means that variables are immutable in Genshi. +.. _whitespace: + +--------------------------- +White-space and Line Breaks +--------------------------- + +Note that space or line breaks around directives is never automatically removed. +Consider the following example: + +.. code-block:: genshitext + + {% for item in items %} + {% if item.visible %} + ${item} + {% end %} + {% end %} + +This will result in two empty lines above and beneath every item, plus the +spaces used for indentation. If you want to supress a line break, simply end +the line with a backslash: + +.. code-block:: genshitext + + {% for item in items %}\ + {% if item.visible %}\ + ${item} + {% end %}\ + {% end %}\ + +Now there would be no empty lines between the items in the output. But you still +get the spaces used for indentation, and because the line breaks are removed, +they actually continue and add up between lines. There are numerous ways to +control white-space in the output while keeping the template readable, such as +moving the indentation into the delimiters, or moving the end delimiter on the +next line, and so on. + + .. _comments: -------- Comments -------- -Lines where the first non-whitespace characters are ``##`` are removed from -the output, and can thus be used for comments. This can be escaped using a +Parts in templates can be commented out using the delimiters ``{# ... #}``. +Any content in comments are removed from the output. + +.. code-block:: genshitext + + {# This won't end up in the output #} + This will. + +Just like directive delimiters, these can be escaped by prefixing with a backslash. + +.. code-block:: genshitext + + \{# This *will* end up in the output, including delimiters #} + This too. + + +.. _legacy: + +--------------------------- +Legacy Text Template Syntax +--------------------------- + +The syntax for text templates was redesigned in version 0.5 of Genshi to make +the language more flexible and powerful. The older syntax is based on line +starting with dollar signs, similar to e.g. Cheetah_ or Velocity_. + +.. _cheetah: http://cheetahtemplate.org/ +.. _velocity: http://jakarta.apache.org/velocity/ + +A simple template using the old syntax looked like this: + +.. code-block:: genshitext + + Dear $name, + + We have the following items for you: + #for item in items + * $item + #end + + All the best, + Foobar + +Beyond the requirement of putting directives on separate lines prefixed with +dollar signs, the language itself is very similar to the new one. Except that +comments are lines that start with two ``#`` characters, and a line-break at the +end of a directive is removed automatically. + +.. note:: If you're using this old syntax, it is strongly recommended to + migrate to the new syntax. Simply replace any references to + ``TextTemplate`` by ``NewTextTemplate`` (and also change the + text templates, of course). On the other hand, if you want to stick + with the old syntax for a while longer, replace references to + ``TextTemplate`` by ``OldTextTemplate``; while ``TextTemplate`` is + still an alias for the old language at this point, that will change + in a future release. But also note that the old syntax may be + dropped entirely in a future release. diff --git a/doc/upgrade.txt b/doc/upgrade.txt new file mode 100644 --- /dev/null +++ b/doc/upgrade.txt @@ -0,0 +1,144 @@ +================ +Upgrading Genshi +================ + + +.. contents:: Contents + :depth: 2 +.. sectnum:: + + +------------------------------------ +Upgrading from Genshi 0.5.x to 0.6.x +------------------------------------ + +Required Python Version +----------------------- + +Support for Python 2.3 has been dropped in this release. Python 2.4 is +now the minimum version of Python required to run Genshi. + + +------------------------------------ +Upgrading from Genshi 0.4.x to 0.5.x +------------------------------------ + +Error Handling +-------------- + +The default error handling mode has been changed to "strict". This +means that accessing variables not defined in the template data will +now generate an immediate exception, as will accessing object +attributes or dictionary keys that don't exist. If your templates rely +on the old lenient behavior, you can configure Genshi to use that +instead. See the documentation for details on how to do that. But be +warned that lenient error handling may be removed completely in a +future release. + +Match Template Processing +------------------------- + +There has also been a subtle change to how ``py:match`` templates are +processed: in previous versions, all match templates would be applied +to the content generated by the matching template, and only the +matching template itself was applied recursively to the original +content. This behavior resulted in problems with many kinds of +recursive matching, and hence was changed for 0.5: now, all match +templates declared before the matching template are applied to the +original content, and match templates declared after the matching +template are applied to the generated content. This change should not +have any effect on most applications, but you may want to check your +use of match templates to make sure. + +Text Templates +-------------- + +Genshi 0.5 introduces a new, alternative syntax for text templates, +which is more flexible and powerful compared to the old syntax. For +backwards compatibility, this new syntax is not used by default, +though it will be in a future version. It is recommended that you +migrate to using this new syntax. To do so, simply rename any +references in your code to ``TextTemplate`` to ``NewTextTemplate``. To +explicitly use the old syntax, use ``OldTextTemplate`` instead, so +that you can be sure you'll be using the same language when the +default in Genshi is changed (at least until the old implementation is +completely removed). + +``Markup`` Constructor +---------------------- + +The ``Markup`` class no longer has a specialized constructor. The old +(undocumented) constructor provided a shorthand for doing positional +substitutions. If you have code like this: + +.. code-block:: python + + Markup('%s', name) + +You must replace it by the more explicit: + +.. code-block:: python + + Markup('%s') % name + +``Template`` Constructor +------------------------ + +The constructor of the ``Template`` class and its subclasses has changed +slightly: instead of the optional ``basedir`` parameter, it now expects +an (also optional) ``filepath`` parameter, which specifies the absolute +path to the template. You probably aren't using those constructors +directly, anyway, but using the ``TemplateLoader`` API instead. + + +------------------------------------ +Upgrading from Genshi 0.3.x to 0.4.x +------------------------------------ + +The modules ``genshi.filters`` and ``genshi.template`` have been +refactored into packages containing multiple modules. While code using +the regular APIs should continue to work without problems, you should +make sure to remove any leftover traces of the files ``filters.py`` +and ``template.py`` in the ``genshi`` package on the installation +path (including the corresponding ``.pyc`` files). This is not +necessary when Genshi was installed as a Python egg. + +Results of evaluating template expressions are no longer implicitly +called if they are callable. If you have been using that feature, you +will need to add the parenthesis to actually call the function. + +Instances of ``genshi.core.Attrs`` are now immutable. Filters +manipulating the attributes in a stream may need to be updated. Also, +the ``Attrs`` class no longer automatically wraps all attribute names +in ``QName`` objects, so users of the ``Attrs`` class need to do this +themselves. See the documentation of the ``Attrs`` class for more +information. + + +--------------------- +Upgrading from Markup +--------------------- + +Prior to version 0.3, the name of the Genshi project was "Markup". The +name change means that you will have to adjust your import statements +and the namespace URI of XML templates, among other things: + +* The package name was changed from "markup" to "genshi". Please + adjust any import statements referring to the old package name. +* The namespace URI for directives in Genshi XML templates has changed + from ``http://markup.edgewall.org/`` to + ``http://genshi.edgewall.org/``. Please update the ``xmlns:py`` + declaration in your template files accordingly. + +Furthermore, due to the inclusion of a text-based template language, +the class:: + + markup.template.Template + +has been renamed to:: + + genshi.template.MarkupTemplate + +If you've been using the Template class directly, you'll need to +update your code (a simple find/replace should do—the API itself +did not change). diff --git a/doc/xml-templates.txt b/doc/xml-templates.txt --- a/doc/xml-templates.txt +++ b/doc/xml-templates.txt @@ -42,7 +42,9 @@ conditionals and looping, among others. To use directives in a template, the namespace must be declared, which is -usually done on the root element:: +usually done on the root element: + +.. code-block:: genshi -This is basically equivalent to the following:: +This is basically equivalent to the following: + +.. code-block:: genshi ${bar}
    Given the data ``foo=True`` and ``bar='Hello'`` in the template context, this -would produce:: +would produce: + +.. code-block:: xml
    Hello
    -This directive can also be used as an element:: +But setting ``foo=False`` would result in the following output: + +.. code-block:: xml + +
    +
    + +This directive can also be used as an element: + +.. code-block:: genshi
    @@ -129,7 +149,9 @@ if no ``py:when`` branch matches, the ``py:otherwise`` branch is rendered. If the ``py:choose`` directive is empty the nested ``py:when`` directives will -be tested for truth:: +be tested for truth: + +.. code-block:: genshi
    0 @@ -137,14 +159,18 @@ 2
    -This would produce the following output:: +This would produce the following output: + +.. code-block:: xml
    1
    If the ``py:choose`` directive contains an expression the nested ``py:when`` -directives will be tested for equality to the parent ``py:choose`` value:: +directives will be tested for equality to the parent ``py:choose`` value: + +.. code-block:: genshi
    0 @@ -152,12 +178,23 @@ 2
    -This would produce the following output:: +This would produce the following output: + +.. code-block:: xml
    1
    +These directives can also be used as elements: + +.. code-block:: genshi + + + 0 + 1 + 2 + Looping ======= @@ -167,19 +204,25 @@ ``py:for`` ---------- -The element is repeated for every item in an iterable:: +The element is repeated for every item in an iterable: + +.. code-block:: genshi
    • ${item}
    -Given ``items=[1, 2, 3]`` in the context data, this would produce:: +Given ``items=[1, 2, 3]`` in the context data, this would produce: + +.. code-block:: xml
    • 1
    • 2
    • 3
    -This directive can also be used as an element:: +This directive can also be used as an element: + +.. code-block:: genshi
      @@ -199,7 +242,9 @@ The ``py:def`` directive can be used to create macros, i.e. snippets of template code that have a name and optionally some parameters, and that can be -inserted in other places:: +inserted in other places: + +.. code-block:: genshi

      @@ -209,7 +254,9 @@ ${greeting('everyone else')}

      -The above would be rendered to:: +The above would be rendered to: + +.. code-block:: xml

      @@ -221,7 +268,9 @@

      If a macro doesn't require parameters, it can be defined without the -parenthesis. For example:: +parenthesis. For example: + +.. code-block:: genshi

      @@ -230,7 +279,9 @@ ${greeting()}

      -The above would be rendered to:: +The above would be rendered to: + +.. code-block:: xml

      @@ -238,7 +289,9 @@

      -This directive can also be used as an element:: +This directive can also be used as an element: + +.. code-block:: genshi
      @@ -258,7 +311,9 @@ content. For example, the match template defined in the following template matches any -element with the tag name “greeting”:: +element with the tag name “greeting”: + +.. code-block:: genshi
      @@ -267,7 +322,9 @@
      -This would result in the following output:: +This would result in the following output: + +.. code-block:: xml
      @@ -282,7 +339,15 @@ .. _`Using XPath`: streams.html#using-xpath -This directive can also be used as an element:: +Match templates are applied both to the original markup as well to the +generated markup. The order in which they are applied depends on the order +they are declared in the template source: a match template defined after +another match template is applied to the output generated by the first match +template. The match templates basically form a pipeline. + +This directive can also be used as an element: + +.. code-block:: genshi
      @@ -291,6 +356,54 @@
      +When used this way, the ``py:match`` directive can also be annotated with a +couple of optimization hints. For example, the following informs the matching +engine that the match should only be applied once: + +.. code-block:: genshi + + + + + ${select("*|text()")} + + + + +The following optimization hints are recognized: + ++---------------+-----------+-----------------------------------------------+ +| Attribute | Default | Description | ++===============+===========+===============================================+ +| ``buffer`` | ``true`` | Whether the matched content should be | +| | | buffered in memory. Buffering can improve | +| | | performance a bit at the cost of needing more | +| | | memory during rendering. Buffering is | +| | | ''required'' for match templates that contain | +| | | more than one invocation of the ``select()`` | +| | | function. If there is only one call, and the | +| | | matched content can potentially be very long, | +| | | consider disabling buffering to avoid | +| | | excessive memory use. | ++---------------+-----------+-----------------------------------------------+ +| ``once`` | ``false`` | Whether the engine should stop looking for | +| | | more matching elements after the first match. | +| | | Use this on match templates that match | +| | | elements that can only occur once in the | +| | | stream, such as the ```` or ```` | +| | | elements in an HTML template, or elements | +| | | with a specific ID. | ++---------------+-----------+-----------------------------------------------+ +| ``recursive`` | ``true`` | Whether the match template should be applied | +| | | to its own output. Note that ``once`` implies | +| | | non-recursive behavior, so this attribute | +| | | only needs to be set for match templates that | +| | | don't also have ``once`` set. | ++---------------+-----------+-----------------------------------------------+ + +.. note:: The ``py:match`` optimization hints were added in the 0.5 release. In + earlier versions, the attributes have no effect. + Variable Binding ================ @@ -306,19 +419,25 @@ than once, and that actually results in a database query, assigning the results to a variable using this directive would probably help. -For example:: +For example: + +.. code-block:: genshi
      $x $y $z
      -Given ``x=42`` in the context data, this would produce:: +Given ``x=42`` in the context data, this would produce: + +.. code-block:: xml
      42 7 52
      -This directive can also be used as an element:: +This directive can also be used as an element: + +.. code-block:: genshi
      $x $y $z @@ -338,21 +457,27 @@ ``py:attrs`` ------------ -This directive adds, modifies or removes attributes from the element:: +This directive adds, modifies or removes attributes from the element: + +.. code-block:: genshi
      • Bar
      Given ``foo={'class': 'collapse'}`` in the template context, this would -produce:: +produce: + +.. code-block:: xml
      • Bar
      Attributes with the value ``None`` are omitted, so given ``foo={'class': None}`` -in the context for the same template this would produce:: +in the context for the same template this would produce: + +.. code-block:: xml
      • Bar
      • @@ -367,13 +492,17 @@ -------------- This directive replaces any nested content with the result of evaluating the -expression:: +expression: + +.. code-block:: genshi
        • Hello
        -Given ``bar='Bye'`` in the context data, this would produce:: +Given ``bar='Bye'`` in the context data, this would produce: + +.. code-block:: xml
        • Bye
        • @@ -388,19 +517,30 @@ -------------- This directive replaces the element itself with the result of evaluating the -expression:: +expression: + +.. code-block:: genshi
          Hello
          -Given ``bar='Bye'`` in the context data, this would produce:: +Given ``bar='Bye'`` in the context data, this would produce: + +.. code-block:: xml
          Bye
          -This directive can only be used as an attribute. +This directive can also be used as an element (since version 0.5): + +.. code-block:: genshi + +
          + Placeholder +
          + .. _`py:strip`: @@ -410,13 +550,17 @@ This directive conditionally strips the top-level element from the output. When the value of the ``py:strip`` attribute evaluates to ``True``, the element is -stripped from the output:: +stripped from the output: + +.. code-block:: genshi
          foo
          -This would be rendered as:: +This would be rendered as: + +.. code-block:: xml
          foo @@ -462,7 +606,9 @@ For this, you need to declare the XInclude namespace (commonly bound to the prefix “xi”) and use the ```` element where you want the external -file to be pulled in:: +file to be pulled in: + +.. code-block:: genshi @@ -494,32 +642,61 @@ .. _`xinclude specification`: http://www.w3.org/TR/xinclude/ + +Dynamic Includes +================ + Incudes in Genshi are fully dynamic: Just like normal attributes, the `href` attribute accepts expressions, and directives_ can be used on the ```` element just as on any other element, meaning you can do -things like conditional includes:: +things like conditional includes: + +.. code-block:: genshi +Including Text Templates +======================== + +The ``parse`` attribute of the ```` element can be used to specify +whether the included template is an XML template or a text template (using the +new syntax added in Genshi 0.5): + +.. code-block:: genshi + + + +This example would load the ``myscript.js`` file as a ``NewTextTemplate``. See +`text templates`_ for details on the syntax of text templates. + +.. _`text templates`: text-templates.html + + .. _comments: -------- Comments -------- -Normal XML/HTML comment syntax can be used in templates:: +Normal XML/HTML comment syntax can be used in templates: + +.. code-block:: genshi However, such comments get passed through the processing pipeline and are by default included in the final output. If that's not desired, prefix the comment -text with an exclamation mark:: +text with an exclamation mark: + +.. code-block:: genshi Note that it does not matter whether there's whitespace before or after the -exclamation mark, so the above could also be written as follows:: +exclamation mark, so the above could also be written as follows: + +.. code-block:: genshi diff --git a/doc/xpath.txt b/doc/xpath.txt --- a/doc/xpath.txt +++ b/doc/xpath.txt @@ -52,8 +52,8 @@ * ``sum()`` The mathematical operators (``+``, ``-``, ``*``, ``div``, and ``mod``) are not -yet supported, whereas the various comparison and logical operators should work -as expected. +yet supported, whereas sub-expressions and the various comparison and logical +operators should work as expected. You can also use XPath variable references (``$var``) inside predicates. @@ -62,25 +62,34 @@ Querying Streams ---------------- -:: - - from genshi.input import XML +The ``Stream`` class provides a ``select(path)`` function that can be used to +retrieve subsets of the stream: - doc = XML(''' - - - Foo - - - Bar - - - ''') - print doc.select('items/item[@status="closed"]/summary/text()') +.. code-block:: pycon -This would result in the following output:: + >>> from genshi.input import XML - Bar + >>> doc = XML(''' + ... + ... + ... Foo + ... + ... + ... Bar + ... + ... + ... Baz + ... + ... + ... Waz + ... + ... + ... ''') + + >>> print doc.select('items/item[@status="closed" and ' + ... '(@resolution="invalid" or not(@resolution))]/summary/text()') + BarBaz + --------------------- diff --git a/examples/bench/basic.py b/examples/bench/basic.py --- a/examples/bench/basic.py +++ b/examples/bench/basic.py @@ -9,7 +9,8 @@ import sys import timeit -__all__ = ['clearsilver', 'myghty', 'django', 'kid', 'genshi', 'cheetah'] +__all__ = ['clearsilver', 'mako', 'django', 'kid', 'genshi', 'genshi_text', + 'simpletal'] def genshi(dirname, verbose=False): from genshi.template import TemplateLoader @@ -24,19 +25,28 @@ print render() return render -def myghty(dirname, verbose=False): - try: - from myghty import interp - except ImportError: - print>>sys.stderr, 'Mighty not installed, skipping' - return lambda: None - interpreter = interp.Interpreter(component_root=dirname) +def genshi_text(dirname, verbose=False): + from genshi.core import escape + from genshi.template import TemplateLoader, NewTextTemplate + loader = TemplateLoader([dirname], auto_reload=False) + template = loader.load('template.txt', cls=NewTextTemplate) + def render(): + data = dict(escape=escape, title='Just a test', user='joe', + items=['Number %d' % num for num in range(1, 15)]) + return template.generate(**data).render('text') + + if verbose: + print render() + return render + +def mako(dirname, verbose=False): + from mako.lookup import TemplateLookup + lookup = TemplateLookup(directories=[dirname], filesystem_checks=False) + template = lookup.get_template('template.html') def render(): data = dict(title='Just a test', user='joe', - items=['Number %d' % num for num in range(1, 15)]) - buffer = StringIO() - interpreter.execute("template.myt", request_args=data, out_buffer=buffer) - return buffer.getvalue() + list_items=['Number %d' % num for num in range(1, 15)]) + return template.render(**data) if verbose: print render() return render @@ -112,7 +122,7 @@ try: import kid except ImportError: - print>>sys.stderr, "SimpleTAL not installed, skipping" + print>>sys.stderr, "Kid not installed, skipping" return lambda: None kid.path = kid.TemplatePath([dirname]) template = kid.load_template('template.kid').Template @@ -180,12 +190,15 @@ verbose = '-v' in sys.argv if '-p' in sys.argv: - import hotshot, hotshot.stats - prof = hotshot.Profile("template.prof") - benchtime = prof.runcall(run, engines, number=100, verbose=verbose) - stats = hotshot.stats.load("template.prof") + import cProfile, pstats + prof = cProfile.Profile() + prof.run('run(%r, number=200, verbose=%r)' % (engines, verbose)) + stats = pstats.Stats(prof) stats.strip_dirs() - stats.sort_stats('time', 'calls') - stats.print_stats() + stats.sort_stats('calls') + stats.print_stats(25) + if verbose: + stats.print_callees() + stats.print_callers() else: run(engines, verbose=verbose) diff --git a/examples/bench/bigtable.py b/examples/bench/bigtable.py --- a/examples/bench/bigtable.py +++ b/examples/bench/bigtable.py @@ -10,7 +10,7 @@ import timeit from StringIO import StringIO from genshi.builder import tag -from genshi.template import MarkupTemplate +from genshi.template import MarkupTemplate, NewTextTemplate try: from elementtree import ElementTree as et @@ -41,9 +41,9 @@ DjangoContext = DjangoTemplate = None try: - from myghty.interp import Interpreter as MyghtyInterpreter + from mako.template import Template as MakoTemplate except ImportError: - MyghtyInterpreter = None + MakoTemplate = None table = [dict(a=1,b=2,c=3,d=4,e=5,f=6,g=7,h=8,i=9,j=10) for x in range(1000)] @@ -60,6 +60,14 @@ $table
          """) +genshi_text_tmpl = NewTextTemplate(""" + +{% for row in table %} +{% for c in row.values() %}{% end %} +{% end %} +
          $c
          +""") + if DjangoTemplate: django_tmpl = DjangoTemplate(""" @@ -74,29 +82,32 @@ context = DjangoContext({'table': table}) django_tmpl.render(context) -if MyghtyInterpreter: - interpreter = MyghtyInterpreter() - component = interpreter.make_component(""" +if MakoTemplate: + mako_tmpl = MakoTemplate("""
          -% for row in ARGS['table']: - -% for col in row.values(): - -% -% - + % for row in table: + + % for col in row.values(): + + % endfor + + % endfor
          <% col %>
          ${ col | h }
          """) - def test_myghty(): - """Myghty Template""" - buf = StringIO() - interpreter.execute(component, request_args={'table':table}, out_buffer=buf) + def test_mako(): + """Mako Template""" + mako_tmpl.render(table=table) def test_genshi(): """Genshi template""" stream = genshi_tmpl.generate(table=table) stream.render('html', strip_whitespace=False) +def test_genshi_text(): + """Genshi text template""" + stream = genshi_text_tmpl.generate(table=table) + stream.render('text') + def test_genshi_builder(): """Genshi template + tag builder""" stream = tag.TABLE([ @@ -185,9 +196,9 @@ def run(which=None, number=10): - tests = ['test_builder', 'test_genshi', 'test_genshi_builder', - 'test_myghty', 'test_kid', 'test_kid_et', 'test_et', 'test_cet', - 'test_clearsilver', 'test_django'] + tests = ['test_builder', 'test_genshi', 'test_genshi_text', + 'test_genshi_builder', 'test_mako', 'test_kid', 'test_kid_et', + 'test_et', 'test_cet', 'test_clearsilver', 'test_django'] if which: tests = filter(lambda n: n[5:] in which, tests) @@ -208,12 +219,15 @@ which = [arg for arg in sys.argv[1:] if arg[0] != '-'] if '-p' in sys.argv: - import hotshot, hotshot.stats - prof = hotshot.Profile("template.prof") - benchtime = prof.runcall(run, which, number=1) - stats = hotshot.stats.load("template.prof") + import cProfile, pstats + prof = cProfile.Profile() + prof.run('run(%r, number=1)' % which) + stats = pstats.Stats(prof) stats.strip_dirs() stats.sort_stats('time', 'calls') - stats.print_stats() + stats.print_stats(25) + if '-v' in sys.argv: + stats.print_callees() + stats.print_callers() else: run(which) diff --git a/examples/bench/genshi/base.html b/examples/bench/genshi/base.html --- a/examples/bench/genshi/base.html +++ b/examples/bench/genshi/base.html @@ -6,12 +6,12 @@ Hello, ${name}!

          - + ${select('*')}
        """ - EXEC = StreamEventKind('EXEC') - """Stream event kind representing a Python code suite to execute.""" - DIRECTIVE_NAMESPACE = Namespace('http://genshi.edgewall.org/') - XINCLUDE_NAMESPACE = Namespace('http://www.w3.org/2001/XInclude') + DIRECTIVE_NAMESPACE = 'http://genshi.edgewall.org/' + XINCLUDE_NAMESPACE = 'http://www.w3.org/2001/XInclude' directives = [('def', DefDirective), ('match', MatchDirective), @@ -65,140 +57,44 @@ ('content', ContentDirective), ('attrs', AttrsDirective), ('strip', StripDirective)] + serializer = 'xml' + _number_conv = Markup - def __init__(self, source, basedir=None, filename=None, loader=None, - encoding=None, lookup='lenient'): - Template.__init__(self, source, basedir=basedir, filename=filename, - loader=loader, encoding=encoding, lookup=lookup) + def __init__(self, source, filepath=None, filename=None, loader=None, + encoding=None, lookup='strict', allow_exec=True): + Template.__init__(self, source, filepath=filepath, filename=filename, + loader=loader, encoding=encoding, lookup=lookup, + allow_exec=allow_exec) + self.add_directives(self.DIRECTIVE_NAMESPACE, self) + + def _init_filters(self): + Template._init_filters(self) # Make sure the include filter comes after the match filter - if loader: + if self.loader: self.filters.remove(self._include) - self.filters += [self._exec, self._match] - if loader: + self.filters += [self._match] + if self.loader: self.filters.append(self._include) def _parse(self, source, encoding): - streams = [[]] # stacked lists of events of the "compiled" template - dirmap = {} # temporary mapping of directives to elements - ns_prefix = {} - depth = 0 - in_fallback = 0 - include_href = None - if not isinstance(source, Stream): source = XMLParser(source, filename=self.filename, encoding=encoding) + stream = [] for kind, data, pos in source: - stream = streams[-1] - if kind is START_NS: - # Strip out the namespace declaration for template directives - prefix, uri = data - ns_prefix[prefix] = uri - if uri not in (self.DIRECTIVE_NAMESPACE, - self.XINCLUDE_NAMESPACE): - stream.append((kind, data, pos)) - - elif kind is END_NS: - uri = ns_prefix.pop(data, None) - if uri and uri not in (self.DIRECTIVE_NAMESPACE, - self.XINCLUDE_NAMESPACE): + if kind is TEXT: + for kind, data, pos in interpolate(data, self.filepath, pos[1], + pos[2], lookup=self.lookup): stream.append((kind, data, pos)) - elif kind is START: - # Record any directive attributes in start tags - tag, attrs = data - directives = [] - strip = False - - if tag in self.DIRECTIVE_NAMESPACE: - cls = self._dir_by_name.get(tag.localname) - if cls is None: - raise BadDirectiveError(tag.localname, self.filepath, - pos[1]) - value = attrs.get(getattr(cls, 'ATTRIBUTE', None), '') - directives.append((cls, value, ns_prefix.copy(), pos)) - strip = True - - new_attrs = [] - for name, value in attrs: - if name in self.DIRECTIVE_NAMESPACE: - cls = self._dir_by_name.get(name.localname) - if cls is None: - raise BadDirectiveError(name.localname, - self.filepath, pos[1]) - directives.append((cls, value, ns_prefix.copy(), pos)) - else: - if value: - value = list(interpolate(value, self.basedir, - pos[0], pos[1], pos[2], - lookup=self.lookup)) - if len(value) == 1 and value[0][0] is TEXT: - value = value[0][1] - else: - value = [(TEXT, u'', pos)] - new_attrs.append((name, value)) - new_attrs = Attrs(new_attrs) - - if directives: - index = self._dir_order.index - directives.sort(lambda a, b: cmp(index(a[0]), index(b[0]))) - dirmap[(depth, tag)] = (directives, len(stream), strip) - - if tag in self.XINCLUDE_NAMESPACE: - if tag.localname == 'include': - include_href = new_attrs.get('href') - if not include_href: - raise TemplateSyntaxError('Include misses required ' - 'attribute "href"', - self.filepath, *pos[1:]) - streams.append([]) - elif tag.localname == 'fallback': - in_fallback += 1 - - else: - stream.append((kind, (tag, new_attrs), pos)) - - depth += 1 - - elif kind is END: - depth -= 1 - - if in_fallback and data == self.XINCLUDE_NAMESPACE['fallback']: - in_fallback -= 1 - elif data == self.XINCLUDE_NAMESPACE['include']: - fallback = streams.pop() - stream = streams[-1] - stream.append((INCLUDE, (include_href, fallback), pos)) - else: - stream.append((kind, data, pos)) - - # If there have have directive attributes with the corresponding - # start tag, move the events inbetween into a "subprogram" - if (depth, data) in dirmap: - directives, start_offset, strip = dirmap.pop((depth, data)) - substream = stream[start_offset:] - if strip: - substream = substream[1:-1] - stream[start_offset:] = [(SUB, (directives, substream), - pos)] - elif kind is PI and data[0] == 'python': + if not self.allow_exec: + raise TemplateSyntaxError('Python code blocks not allowed', + self.filepath, *pos[1:]) try: - # As Expat doesn't report whitespace between the PI target - # and the data, we have to jump through some hoops here to - # get correctly indented Python code - # Unfortunately, we'll still probably not get the line - # number quite right - lines = [line.expandtabs() for line in data[1].splitlines()] - first = lines[0] - rest = dedent('\n'.join(lines[1:])) - if first.rstrip().endswith(':') and not rest[0].isspace(): - rest = '\n'.join([' ' + line for line - in rest.splitlines()]) - source = '\n'.join([first, rest]) - suite = Suite(source, self.filepath, pos[1], + suite = Suite(data[1], self.filepath, pos[1], lookup=self.lookup) except SyntaxError, err: raise TemplateSyntaxError(err, self.filepath, @@ -206,12 +102,6 @@ pos[2] + (err.offset or 0)) stream.append((EXEC, suite, pos)) - elif kind is TEXT: - for kind, data, pos in interpolate(data, self.basedir, pos[0], - pos[1], pos[2], - lookup=self.lookup): - stream.append((kind, data, pos)) - elif kind is COMMENT: if not data.lstrip().startswith('!'): stream.append((kind, data, pos)) @@ -219,25 +109,206 @@ else: stream.append((kind, data, pos)) + return stream + + def _extract_directives(self, stream, namespace, factory): + depth = 0 + dirmap = {} # temporary mapping of directives to elements + new_stream = [] + ns_prefix = {} # namespace prefixes in use + + for kind, data, pos in stream: + + if kind is START: + tag, attrs = data + directives = [] + strip = False + + if tag.namespace == namespace: + cls = factory.get_directive(tag.localname) + if cls is None: + raise BadDirectiveError(tag.localname, + self.filepath, pos[1]) + args = dict([(name.localname, value) for name, value + in attrs if not name.namespace]) + directives.append((cls, args, ns_prefix.copy(), pos)) + strip = True + + new_attrs = [] + for name, value in attrs: + if name.namespace == namespace: + cls = factory.get_directive(name.localname) + if cls is None: + raise BadDirectiveError(name.localname, + self.filepath, pos[1]) + if type(value) is list and len(value) == 1: + value = value[0][1] + directives.append((cls, value, ns_prefix.copy(), + pos)) + else: + new_attrs.append((name, value)) + new_attrs = Attrs(new_attrs) + + if directives: + directives.sort(self.compare_directives()) + dirmap[(depth, tag)] = (directives, len(new_stream), + strip) + + new_stream.append((kind, (tag, new_attrs), pos)) + depth += 1 + + elif kind is END: + depth -= 1 + new_stream.append((kind, data, pos)) + + # If there have have directive attributes with the + # corresponding start tag, move the events inbetween into + # a "subprogram" + if (depth, data) in dirmap: + directives, offset, strip = dirmap.pop((depth, data)) + substream = new_stream[offset:] + if strip: + substream = substream[1:-1] + new_stream[offset:] = [ + (SUB, (directives, substream), pos) + ] + + elif kind is SUB: + directives, substream = data + substream = self._extract_directives(substream, namespace, + factory) + + if len(substream) == 1 and substream[0][0] is SUB: + added_directives, substream = substream[0][1] + directives += added_directives + + new_stream.append((kind, (directives, substream), pos)) + + elif kind is START_NS: + # Strip out the namespace declaration for template + # directives + prefix, uri = data + ns_prefix[prefix] = uri + if uri != namespace: + new_stream.append((kind, data, pos)) + + elif kind is END_NS: + uri = ns_prefix.pop(data, None) + if uri and uri != namespace: + new_stream.append((kind, data, pos)) + + else: + new_stream.append((kind, data, pos)) + + return new_stream + + def _extract_includes(self, stream): + streams = [[]] # stacked lists of events of the "compiled" template + prefixes = {} + fallbacks = [] + includes = [] + xinclude_ns = Namespace(self.XINCLUDE_NAMESPACE) + + for kind, data, pos in stream: + stream = streams[-1] + + if kind is START: + # Record any directive attributes in start tags + tag, attrs = data + if tag in xinclude_ns: + if tag.localname == 'include': + include_href = attrs.get('href') + if not include_href: + raise TemplateSyntaxError('Include misses required ' + 'attribute "href"', + self.filepath, *pos[1:]) + includes.append((include_href, attrs.get('parse'))) + streams.append([]) + elif tag.localname == 'fallback': + streams.append([]) + fallbacks.append(streams[-1]) + else: + stream.append((kind, (tag, attrs), pos)) + + elif kind is END: + if fallbacks and data == xinclude_ns['fallback']: + assert streams.pop() is fallbacks[-1] + elif data == xinclude_ns['include']: + fallback = None + if len(fallbacks) == len(includes): + fallback = fallbacks.pop() + streams.pop() # discard anything between the include tags + # and the fallback element + stream = streams[-1] + href, parse = includes.pop() + try: + cls = { + 'xml': MarkupTemplate, + 'text': NewTextTemplate + }[parse or 'xml'] + except KeyError: + raise TemplateSyntaxError('Invalid value for "parse" ' + 'attribute of include', + self.filepath, *pos[1:]) + stream.append((INCLUDE, (href, cls, fallback), pos)) + else: + stream.append((kind, data, pos)) + + elif kind is START_NS and data[1] == xinclude_ns: + # Strip out the XInclude namespace + prefixes[data[0]] = data[1] + + elif kind is END_NS and data in prefixes: + prefixes.pop(data) + + else: + stream.append((kind, data, pos)) + assert len(streams) == 1 return streams[0] - def _exec(self, stream, ctxt): - """Internal stream filter that executes code in ```` - processing instructions. + def _interpolate_attrs(self, stream): + for kind, data, pos in stream: + + if kind is START: + # Record any directive attributes in start tags + tag, attrs = data + new_attrs = [] + for name, value in attrs: + if value: + value = list(interpolate(value, self.filepath, pos[1], + pos[2], lookup=self.lookup)) + if len(value) == 1 and value[0][0] is TEXT: + value = value[0][1] + new_attrs.append((name, value)) + data = tag, Attrs(new_attrs) + + yield kind, data, pos + + def _prepare(self, stream): + return Template._prepare(self, + self._extract_includes(self._interpolate_attrs(stream)) + ) + + def add_directives(self, namespace, factory): + """Register a custom `DirectiveFactory` for a given namespace. + + :param namespace: the namespace URI + :type namespace: `basestring` + :param factory: the directive factory to register + :type factory: `DirectiveFactory` + :since: version 0.6 """ - for event in stream: - if event[0] is EXEC: - event[1].execute(_ctxt2dict(ctxt)) - else: - yield event + assert not self._prepared, 'Too late for adding directives, ' \ + 'template already prepared' + self._stream = self._extract_directives(self._stream, namespace, + factory) - def _match(self, stream, ctxt, match_templates=None): + def _match(self, stream, ctxt, start=0, end=None, **vars): """Internal stream filter that applies any defined match templates to the stream. """ - if match_templates is None: - match_templates = ctxt._match_templates + match_templates = ctxt._match_templates tail = [] def _strip(stream): @@ -263,10 +334,15 @@ yield event continue - for idx, (test, path, template, namespaces, directives) in \ - enumerate(match_templates): + for idx, (test, path, template, hints, namespaces, directives) \ + in enumerate(match_templates): + if idx < start or end is not None and idx >= end: + continue if test(event, namespaces, ctxt) is True: + if 'match_once' in hints: + del match_templates[idx] + idx -= 1 # Let the remaining match templates know about the event so # they get a chance to update their internal state @@ -275,35 +351,46 @@ # Consume and store all events until an end event # corresponding to this start event is encountered - content = chain([event], - self._match(_strip(stream), ctxt, - [match_templates[idx]]), - tail) - content = list(self._include(content, ctxt)) - - for test in [mt[0] for mt in match_templates]: - test(tail[0], namespaces, ctxt, updateonly=True) + pre_end = idx + 1 + if 'match_once' not in hints and 'not_recursive' in hints: + pre_end -= 1 + inner = _strip(stream) + if pre_end > 0: + inner = self._match(inner, ctxt, end=pre_end) + content = self._include(chain([event], inner, tail), ctxt) + if 'not_buffered' not in hints: + content = list(content) # Make the select() function available in the body of the # match template + selected = [False] def select(path): + selected[0] = True return Stream(content).select(path, namespaces, ctxt) - ctxt.push(dict(select=select)) + vars = dict(select=select) # Recursively process the output - template = _apply_directives(template, ctxt, directives) - for event in self._match(self._eval(self._flatten(template, - ctxt), - ctxt), ctxt, - match_templates[:idx] + - match_templates[idx + 1:]): + template = _apply_directives(template, directives, ctxt, + **vars) + for event in self._match(self._flatten(template, ctxt, + **vars), + ctxt, start=idx + 1, **vars): yield event - ctxt.pop() + # If the match template did not actually call select to + # consume the matched stream, the original events need to + # be consumed here or they'll get appended to the output + if not selected[0]: + for event in content: + pass + + # Let the remaining match templates know about the last + # event in the matched content, so they can update their + # internal state accordingly + for test in [mt[0] for mt in match_templates]: + test(tail[0], namespaces, ctxt, updateonly=True) + break else: # no matches yield event - - -EXEC = MarkupTemplate.EXEC diff --git a/genshi/template/plugin.py b/genshi/template/plugin.py --- a/genshi/template/plugin.py +++ b/genshi/template/plugin.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006-2007 Edgewall Software +# Copyright (C) 2006-2008 Edgewall Software # Copyright (C) 2006 Matthew Good # All rights reserved. # @@ -16,14 +16,12 @@ CherryPy/Buffet. """ -from pkg_resources import resource_filename - from genshi.input import ET, HTML, XML from genshi.output import DocType from genshi.template.base import Template from genshi.template.loader import TemplateLoader from genshi.template.markup import MarkupTemplate -from genshi.template.text import TextTemplate +from genshi.template.text import TextTemplate, NewTextTemplate __all__ = ['ConfigurationError', 'AbstractTemplateEnginePlugin', 'MarkupTemplateEnginePlugin', 'TextTemplateEnginePlugin'] @@ -58,16 +56,28 @@ raise ConfigurationError('Invalid value for max_cache_size: "%s"' % options.get('genshi.max_cache_size')) - lookup_errors = options.get('genshi.lookup_errors', 'lenient') + loader_callback = options.get('genshi.loader_callback', None) + if loader_callback and not hasattr(loader_callback, '__call__'): + raise ConfigurationError('loader callback must be a function') + + lookup_errors = options.get('genshi.lookup_errors', 'strict') if lookup_errors not in ('lenient', 'strict'): raise ConfigurationError('Unknown lookup errors mode "%s"' % lookup_errors) + try: + allow_exec = bool(options.get('genshi.allow_exec', True)) + except ValueError: + raise ConfigurationError('Invalid value for allow_exec "%s"' % + options.get('genshi.allow_exec')) + self.loader = TemplateLoader(filter(None, search_path), auto_reload=auto_reload, max_cache_size=max_cache_size, default_class=self.template_class, - variable_lookup=lookup_errors) + variable_lookup=lookup_errors, + allow_exec=allow_exec, + callback=loader_callback) def load_template(self, templatename, template_string=None): """Find a template specified in python 'dot' notation, or load one from @@ -79,13 +89,14 @@ if self.use_package_naming: divider = templatename.rfind('.') if divider >= 0: + from pkg_resources import resource_filename package = templatename[:divider] basename = templatename[divider + 1:] + self.extension templatename = resource_filename(package, basename) return self.loader.load(templatename) - def _get_render_options(self, format=None): + def _get_render_options(self, format=None, fragment=False): if format is None: format = self.default_format kwargs = {'method': format} @@ -95,7 +106,7 @@ def render(self, info, format=None, fragment=False, template=None): """Render the template to a string using the provided info.""" - kwargs = self._get_render_options(format=format) + kwargs = self._get_render_options(format=format, fragment=fragment) return self.transform(info, template).render(**kwargs) def transform(self, info, template): @@ -128,10 +139,10 @@ raise ConfigurationError('Unknown output format %r' % format) self.default_format = format - def _get_render_options(self, format=None): + def _get_render_options(self, format=None, fragment=False): kwargs = super(MarkupTemplateEnginePlugin, - self)._get_render_options(format) - if self.default_doctype: + self)._get_render_options(format, fragment) + if self.default_doctype and not fragment: kwargs['doctype'] = self.default_doctype return kwargs @@ -150,3 +161,15 @@ template_class = TextTemplate extension = '.txt' default_format = 'text' + + def __init__(self, extra_vars_func=None, options=None): + if options is None: + options = {} + + new_syntax = options.get('genshi.new_text_syntax') + if isinstance(new_syntax, basestring): + new_syntax = new_syntax.lower() in ('1', 'on', 'yes', 'true') + if new_syntax: + self.template_class = NewTextTemplate + + AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options) diff --git a/genshi/template/tests/directives.py b/genshi/template/tests/directives.py --- a/genshi/template/tests/directives.py +++ b/genshi/template/tests/directives.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006 Edgewall Software +# Copyright (C) 2006-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -382,6 +382,7 @@ """) self.assertEqual(""" Hi, you! + """, str(tmpl.generate())) def test_function_with_star_args(self): @@ -482,9 +483,35 @@ try: list(tmpl.generate(foo=12)) self.fail('Expected TemplateRuntimeError') - except TemplateRuntimeError, e: + except TypeError, e: + assert (str(e) == "iteration over non-sequence" or + str(e) == "'int' object is not iterable") + exc_type, exc_value, exc_traceback = sys.exc_info() + frame = exc_traceback.tb_next + frames = [] + while frame.tb_next: + frame = frame.tb_next + frames.append(frame) + self.assertEqual("", + frames[-1].tb_frame.f_code.co_name) + self.assertEqual('test.html', + frames[-1].tb_frame.f_code.co_filename) + self.assertEqual(2, frames[-1].tb_lineno) + + def test_for_with_empty_value(self): + """ + Verify an empty 'for' value is an error + """ + try: + MarkupTemplate(""" + + empty + + """, filename='test.html').generate() + self.fail('ExpectedTemplateSyntaxError') + except TemplateSyntaxError, e: self.assertEqual('test.html', e.filename) - if sys.version_info[:2] >= (2, 4): + if sys.version_info[:2] > (2,4): self.assertEqual(2, e.lineno) @@ -620,6 +647,32 @@ """, str(tmpl.generate())) + def test_recursive_match_3(self): + tmpl = MarkupTemplate(""" + + ${select('*|text()')} + + +
          ${select('*')}
        +
        + + ${select('*|text()')} + + + + + 1 + 2 + + +
        + """) + self.assertEqual(""" + +
          12
        +
        +
        """, str(tmpl.generate())) + def test_not_match_self(self): """ See http://genshi.edgewall.org/ticket/77 @@ -842,6 +895,54 @@ """, str(tmpl.generate())) + def test_match_with_once_attribute(self): + tmpl = MarkupTemplate(""" + +
        + ${select("*")} +
        +
        + +

        Foo

        + + +

        Bar

        + + """) + self.assertEqual(""" + +
        +

        Foo

        +
        + + +

        Bar

        + + """, str(tmpl.generate())) + + def test_match_with_recursive_attribute(self): + tmpl = MarkupTemplate(""" + +
        + ${select('*')} +
        +
        + + + + + +
        """) + self.assertEqual(""" + +
        + + + +
        +
        +
        """, str(tmpl.generate())) + # FIXME #def test_match_after_step(self): # tmpl = MarkupTemplate("""
        @@ -857,6 +958,20 @@ #
        """, str(tmpl.generate())) +class ContentDirectiveTestCase(unittest.TestCase): + """Tests for the `py:content` template directive.""" + + def test_as_element(self): + try: + MarkupTemplate(""" + Foo + """, filename='test.html').generate() + self.fail('Expected TemplateSyntaxError') + except TemplateSyntaxError, e: + self.assertEqual('test.html', e.filename) + self.assertEqual(2, e.lineno) + + class ReplaceDirectiveTestCase(unittest.TestCase): """Tests for the `py:replace` template directive.""" @@ -866,14 +981,21 @@ expression is supplied. """ try: - tmpl = MarkupTemplate(""" + MarkupTemplate(""" Foo - """, filename='test.html') + """, filename='test.html').generate() self.fail('Expected TemplateSyntaxError') except TemplateSyntaxError, e: self.assertEqual('test.html', e.filename) - if sys.version_info[:2] >= (2, 4): - self.assertEqual(2, e.lineno) + self.assertEqual(2, e.lineno) + + def test_as_element(self): + tmpl = MarkupTemplate("""
        + +
        """, filename='test.html') + self.assertEqual("""
        + Test +
        """, str(tmpl.generate(title='Test'))) class StripDirectiveTestCase(unittest.TestCase): @@ -968,6 +1090,22 @@ here are two semicolons: ;;
      """, str(tmpl.generate())) + def test_ast_transformation(self): + """ + Verify that the usual template expression AST transformations are + applied despite the code being compiled to a `Suite` object. + """ + tmpl = MarkupTemplate("""
      + + $bar + +
      """) + self.assertEqual("""
      + + 42 + +
      """, str(tmpl.generate(foo={'bar': 42}))) + def test_unicode_expr(self): tmpl = MarkupTemplate("""
      @@ -979,6 +1117,16 @@ 一二三四五六日
      """, str(tmpl.generate())) + + def test_with_empty_value(self): + """ + Verify that an empty py:with works (useless, but legal) + """ + tmpl = MarkupTemplate("""
      + Text
      """) + + self.assertEqual("""
      + Text
      """, str(tmpl.generate())) def suite(): @@ -990,6 +1138,7 @@ suite.addTest(unittest.makeSuite(ForDirectiveTestCase, 'test')) suite.addTest(unittest.makeSuite(IfDirectiveTestCase, 'test')) suite.addTest(unittest.makeSuite(MatchDirectiveTestCase, 'test')) + suite.addTest(unittest.makeSuite(ContentDirectiveTestCase, 'test')) suite.addTest(unittest.makeSuite(ReplaceDirectiveTestCase, 'test')) suite.addTest(unittest.makeSuite(StripDirectiveTestCase, 'test')) suite.addTest(unittest.makeSuite(WithDirectiveTestCase, 'test')) diff --git a/genshi/template/tests/eval.py b/genshi/template/tests/eval.py --- a/genshi/template/tests/eval.py +++ b/genshi/template/tests/eval.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006-2007 Edgewall Software +# Copyright (C) 2006-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -12,10 +12,13 @@ # history and logs, available at http://genshi.edgewall.org/log/. import doctest +import pickle +from StringIO import StringIO import sys import unittest from genshi.core import Markup +from genshi.template.base import Context from genshi.template.eval import Expression, Suite, Undefined, UndefinedError, \ UNDEFINED @@ -32,6 +35,14 @@ self.assertEqual(hash(expr), hash(Expression('x,y'))) self.assertNotEqual(hash(expr), hash(Expression('y, x'))) + def test_pickle(self): + expr = Expression('1 < 2') + buf = StringIO() + pickle.dump(expr, buf, 2) + buf.seek(0) + unpickled = pickle.load(buf) + assert unpickled.evaluate({}) is True + def test_name_lookup(self): self.assertEqual('bar', Expression('foo').evaluate({'foo': 'bar'})) self.assertEqual(id, Expression('id').evaluate({})) @@ -184,8 +195,9 @@ def test_compare_ne(self): self.assertEqual(False, Expression("1 != 1").evaluate({})) self.assertEqual(False, Expression("x != y").evaluate({'x': 1, 'y': 1})) - self.assertEqual(False, Expression("1 <> 1").evaluate({})) - self.assertEqual(False, Expression("x <> y").evaluate({'x': 1, 'y': 1})) + if sys.version < '3': + self.assertEqual(False, Expression("1 <> 1").evaluate({})) + self.assertEqual(False, Expression("x <> y").evaluate({'x': 1, 'y': 1})) def test_compare_lt(self): self.assertEqual(True, Expression("1 < 2").evaluate({})) @@ -230,16 +242,9 @@ self.assertEqual(42, expr.evaluate({'foo': foo, 'bar': {"x": 42}})) def test_lambda(self): - # Define a custom `sorted` function cause the builtin isn't available - # on Python 2.3 - def sorted(items, compfunc): - items.sort(compfunc) - return items - data = {'items': [{'name': 'b', 'value': 0}, {'name': 'a', 'value': 1}], - 'sorted': sorted} - expr = Expression("sorted(items, lambda a, b: cmp(a.name, b.name))") - self.assertEqual([{'name': 'a', 'value': 1}, {'name': 'b', 'value': 0}], - expr.evaluate(data)) + data = {'items': range(5)} + expr = Expression("filter(lambda x: x > 2, items)") + self.assertEqual([3, 4], expr.evaluate(data)) def test_list_comprehension(self): expr = Expression("[n for n in numbers if n < 2]") @@ -263,30 +268,27 @@ expr = Expression("[i['name'] for i in items if i['value'] > 1]") self.assertEqual(['b'], expr.evaluate({'items': items})) - if sys.version_info >= (2, 4): - # Generator expressions only supported in Python 2.4 and up - - def test_generator_expression(self): - expr = Expression("list(n for n in numbers if n < 2)") - self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)})) - - expr = Expression("list((i, n + 1) for i, n in enumerate(numbers))") - self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], - expr.evaluate({'numbers': range(5)})) + def test_generator_expression(self): + expr = Expression("list(n for n in numbers if n < 2)") + self.assertEqual([0, 1], expr.evaluate({'numbers': range(5)})) - expr = Expression("list(offset + n for n in numbers)") - self.assertEqual([2, 3, 4, 5, 6], - expr.evaluate({'numbers': range(5), 'offset': 2})) + expr = Expression("list((i, n + 1) for i, n in enumerate(numbers))") + self.assertEqual([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)], + expr.evaluate({'numbers': range(5)})) - def test_generator_expression_with_getattr(self): - items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] - expr = Expression("list(i.name for i in items if i.value > 1)") - self.assertEqual(['b'], expr.evaluate({'items': items})) + expr = Expression("list(offset + n for n in numbers)") + self.assertEqual([2, 3, 4, 5, 6], + expr.evaluate({'numbers': range(5), 'offset': 2})) - def test_generator_expression_with_getitem(self): - items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] - expr = Expression("list(i['name'] for i in items if i['value'] > 1)") - self.assertEqual(['b'], expr.evaluate({'items': items})) + def test_generator_expression_with_getattr(self): + items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] + expr = Expression("list(i.name for i in items if i.value > 1)") + self.assertEqual(['b'], expr.evaluate({'items': items})) + + def test_generator_expression_with_getitem(self): + items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] + expr = Expression("list(i['name'] for i in items if i['value'] > 1)") + self.assertEqual(['b'], expr.evaluate({'items': items})) if sys.version_info >= (2, 5): def test_conditional_expression(self): @@ -321,7 +323,8 @@ self.assertEqual([0, 1, 2, 3], expr.evaluate({'numbers': range(5)})) def test_access_undefined(self): - expr = Expression("nothing", filename='index.html', lineno=50) + expr = Expression("nothing", filename='index.html', lineno=50, + lookup='lenient') retval = expr.evaluate({}) assert isinstance(retval, Undefined) self.assertEqual('nothing', retval._name) @@ -332,23 +335,45 @@ def __repr__(self): return '' something = Something() - expr = Expression('something.nil', filename='index.html', lineno=50) + expr = Expression('something.nil', filename='index.html', lineno=50, + lookup='lenient') retval = expr.evaluate({'something': something}) assert isinstance(retval, Undefined) self.assertEqual('nil', retval._name) assert retval._owner is something + def test_getattr_exception(self): + class Something(object): + def prop_a(self): + raise NotImplementedError + prop_a = property(prop_a) + def prop_b(self): + raise AttributeError + prop_b = property(prop_b) + self.assertRaises(NotImplementedError, + Expression('s.prop_a').evaluate, {'s': Something()}) + self.assertRaises(AttributeError, + Expression('s.prop_b').evaluate, {'s': Something()}) + def test_getitem_undefined_string(self): class Something(object): def __repr__(self): return '' something = Something() - expr = Expression('something["nil"]', filename='index.html', lineno=50) + expr = Expression('something["nil"]', filename='index.html', lineno=50, + lookup='lenient') retval = expr.evaluate({'something': something}) assert isinstance(retval, Undefined) self.assertEqual('nil', retval._name) assert retval._owner is something + def test_getitem_exception(self): + class Something(object): + def __getitem__(self, key): + raise NotImplementedError + self.assertRaises(NotImplementedError, + Expression('s["foo"]').evaluate, {'s': Something()}) + def test_error_access_undefined(self): expr = Expression("nothing", filename='index.html', lineno=50, lookup='strict') @@ -420,6 +445,29 @@ class SuiteTestCase(unittest.TestCase): + def test_pickle(self): + suite = Suite('foo = 42') + buf = StringIO() + pickle.dump(suite, buf, 2) + buf.seek(0) + unpickled = pickle.load(buf) + data = {} + unpickled.execute(data) + self.assertEqual(42, data['foo']) + + def test_internal_shadowing(self): + # The context itself is stored in the global execution scope of a suite + # It used to get stored under the name 'data', which meant the + # following test would fail, as the user defined 'data' variable + # shadowed the Genshi one. We now use the name '__data__' to avoid + # conflicts + suite = Suite("""data = [] +bar = foo +""") + data = {'foo': 42} + suite.execute(data) + self.assertEqual(42, data['bar']) + def test_assign(self): suite = Suite("foo = 42") data = {} @@ -434,7 +482,8 @@ self.assertEqual(None, data['donothing']()) def test_def_with_multiple_statements(self): - suite = Suite("""def donothing(): + suite = Suite(""" +def donothing(): if True: return foo """) @@ -443,6 +492,84 @@ assert 'donothing' in data self.assertEqual('bar', data['donothing']()) + def test_def_using_nonlocal(self): + suite = Suite(""" +values = [] +def add(value): + if value not in values: + values.append(value) +add('foo') +add('bar') +""") + data = {} + suite.execute(data) + self.assertEqual(['foo', 'bar'], data['values']) + + def test_def_some_defaults(self): + suite = Suite(""" +def difference(v1, v2=10): + return v1 - v2 +x = difference(20, 19) +y = difference(20) +""") + data = {} + suite.execute(data) + self.assertEqual(1, data['x']) + self.assertEqual(10, data['y']) + + def test_def_all_defaults(self): + suite = Suite(""" +def difference(v1=100, v2=10): + return v1 - v2 +x = difference(20, 19) +y = difference(20) +z = difference() +""") + data = {} + suite.execute(data) + self.assertEqual(1, data['x']) + self.assertEqual(10, data['y']) + self.assertEqual(90, data['z']) + + def test_def_vararg(self): + suite = Suite(""" +def mysum(*others): + rv = 0 + for n in others: + rv = rv + n + return rv +x = mysum(1, 2, 3) +""") + data = {} + suite.execute(data) + self.assertEqual(6, data['x']) + + def test_def_kwargs(self): + suite = Suite(""" +def smash(**kw): + return [''.join(i) for i in kw.items()] +x = smash(foo='abc', bar='def') +""") + data = {} + suite.execute(data) + self.assertEqual(['fooabc', 'bardef'], data['x']) + + def test_def_nested(self): + suite = Suite(""" +def doit(): + values = [] + def add(value): + if value not in values: + values.append(value) + add('foo') + add('bar') + return values +x = doit() +""") + data = {} + suite.execute(data) + self.assertEqual(['foo', 'bar'], data['x']) + def test_delete(self): suite = Suite("""foo = 42 del foo @@ -457,12 +584,50 @@ suite.execute(data) assert 'plain' in data + def test_class_in_def(self): + suite = Suite(""" +def create(): + class Foobar(object): + def __str__(self): + return 'foobar' + return Foobar() +x = create() +""") + data = {} + suite.execute(data) + self.assertEqual('foobar', str(data['x'])) + + def test_class_with_methods(self): + suite = Suite("""class plain(object): + def donothing(): + pass +""") + data = {} + suite.execute(data) + assert 'plain' in data + def test_import(self): suite = Suite("from itertools import ifilter") data = {} suite.execute(data) assert 'ifilter' in data + def test_import_star(self): + suite = Suite("from itertools import *") + data = Context() + suite.execute(data) + assert 'ifilter' in data + + def test_import_in_def(self): + suite = Suite("""def fun(): + from itertools import ifilter + return ifilter(None, xrange(3)) +""") + data = Context() + suite.execute(data) + assert 'ifilter' not in data + self.assertEqual([1, 2], list(data['fun']())) + def test_for(self): suite = Suite("""x = [] for i in range(3): @@ -472,6 +637,18 @@ suite.execute(data) self.assertEqual([0, 1, 4], data['x']) + def test_for_in_def(self): + suite = Suite("""def loop(): + for i in range(10): + if i == 5: + break + return i +""") + data = {} + suite.execute(data) + assert 'loop' in data + self.assertEqual(5, data['loop']()) + def test_if(self): suite = Suite("""if foo == 42: x = True @@ -525,6 +702,25 @@ def test_local_augmented_assign(self): Suite("x = 1; x += 42; assert x == 43").execute({}) + def test_augmented_assign_in_def(self): + d = {} + Suite("""def foo(): + i = 1 + i += 1 + return i +x = foo()""").execute(d) + self.assertEqual(2, d['x']) + + def test_augmented_assign_in_loop_in_def(self): + d = {} + Suite("""def foo(): + i = 0 + for n in range(5): + i += n + return i +x = foo()""").execute(d) + self.assertEqual(10, d['x']) + def test_assign_in_list(self): suite = Suite("[d['k']] = 'foo',; assert d['k'] == 'foo'") d = {"k": "bar"} @@ -569,7 +765,7 @@ def test_delitem(self): d = {'k': 'foo'} Suite("del d['k']").execute({'d': d}) - self.failIf('k' in d, `d`) + self.failIf('k' in d, repr(d)) def suite(): diff --git a/genshi/template/tests/interpolation.py b/genshi/template/tests/interpolation.py --- a/genshi/template/tests/interpolation.py +++ b/genshi/template/tests/interpolation.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2007 Edgewall Software +# Copyright (C) 2007-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -60,6 +60,12 @@ self.assertEqual(TEXT, parts[0][0]) self.assertEqual('$bla', parts[0][1]) + def test_interpolate_short_escaped_2(self): + parts = list(interpolate('my $$bla = 2')) + self.assertEqual(1, len(parts)) + self.assertEqual(TEXT, parts[0][0]) + self.assertEqual('my $bla = 2', parts[0][1]) + def test_interpolate_short_doubleescaped(self): parts = list(interpolate('$$$bla')) self.assertEqual(2, len(parts)) @@ -180,6 +186,11 @@ self.assertEqual(TEXT, parts[2][0]) self.assertEqual(' baz', parts[2][1]) + def test_interpolate_triplequoted(self): + parts = list(interpolate('${"""foo\nbar"""}')) + self.assertEqual(1, len(parts)) + self.assertEqual('"""foo\nbar"""', parts[0][1].source) + def suite(): suite = unittest.TestSuite() diff --git a/genshi/template/tests/loader.py b/genshi/template/tests/loader.py --- a/genshi/template/tests/loader.py +++ b/genshi/template/tests/loader.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006 Edgewall Software +# Copyright (C) 2006-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -104,6 +104,34 @@
      Included
      """, tmpl.generate().render()) + def test_relative_include_samesubdir(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write("""
      Included tmpl1.html
      """) + finally: + file1.close() + + os.mkdir(os.path.join(self.dirname, 'sub')) + file2 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w') + try: + file2.write("""
      Included sub/tmpl1.html
      """) + finally: + file2.close() + + file3 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w') + try: + file3.write(""" + + """) + finally: + file3.close() + + loader = TemplateLoader([self.dirname]) + tmpl = loader.load('sub/tmpl2.html') + self.assertEqual(""" +
      Included sub/tmpl1.html
      + """, tmpl.generate().render()) + def test_relative_include_without_search_path(self): file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') try: @@ -172,6 +200,108 @@
      Included
      """, tmpl2.generate().render()) + def test_relative_absolute_template_preferred(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write("""
      Included
      """) + finally: + file1.close() + + os.mkdir(os.path.join(self.dirname, 'sub')) + file2 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w') + try: + file2.write("""
      Included from sub
      """) + finally: + file2.close() + + file3 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w') + try: + file3.write(""" + + """) + finally: + file3.close() + + loader = TemplateLoader() + tmpl = loader.load(os.path.abspath(os.path.join(self.dirname, 'sub', + 'tmpl2.html'))) + self.assertEqual(""" +
      Included from sub
      + """, tmpl.generate().render()) + + def test_abspath_caching(self): + abspath = os.path.join(self.dirname, 'abs') + os.mkdir(abspath) + file1 = open(os.path.join(abspath, 'tmpl1.html'), 'w') + try: + file1.write(""" + + """) + finally: + file1.close() + + file2 = open(os.path.join(abspath, 'tmpl2.html'), 'w') + try: + file2.write("""
      Included from abspath.
      """) + finally: + file2.close() + + searchpath = os.path.join(self.dirname, 'searchpath') + os.mkdir(searchpath) + file3 = open(os.path.join(searchpath, 'tmpl2.html'), 'w') + try: + file3.write("""
      Included from searchpath.
      """) + finally: + file3.close() + + loader = TemplateLoader(searchpath) + tmpl1 = loader.load(os.path.join(abspath, 'tmpl1.html')) + self.assertEqual(""" +
      Included from searchpath.
      + """, tmpl1.generate().render()) + assert 'tmpl2.html' in loader._cache + + def test_abspath_include_caching_without_search_path(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.html'), 'w') + try: + file1.write(""" + + """) + finally: + file1.close() + + file2 = open(os.path.join(self.dirname, 'tmpl2.html'), 'w') + try: + file2.write("""
      Included
      """) + finally: + file2.close() + + os.mkdir(os.path.join(self.dirname, 'sub')) + file3 = open(os.path.join(self.dirname, 'sub', 'tmpl1.html'), 'w') + try: + file3.write(""" + + """) + finally: + file3.close() + + file4 = open(os.path.join(self.dirname, 'sub', 'tmpl2.html'), 'w') + try: + file4.write("""
      Included from sub
      """) + finally: + file4.close() + + loader = TemplateLoader() + tmpl1 = loader.load(os.path.join(self.dirname, 'tmpl1.html')) + self.assertEqual(""" +
      Included
      + """, tmpl1.generate().render()) + tmpl2 = loader.load(os.path.join(self.dirname, 'sub', 'tmpl1.html')) + self.assertEqual(""" +
      Included from sub
      + """, tmpl2.generate().render()) + assert 'tmpl2.html' not in loader._cache + def test_load_with_default_encoding(self): f = open(os.path.join(self.dirname, 'tmpl.html'), 'w') try: @@ -219,6 +349,108 @@

      Hello, hello

      """, tmpl.generate().render()) + def test_prefix_delegation_to_directories(self): + """ + Test prefix delegation with the following layout: + + templates/foo.html + sub1/templates/tmpl1.html + sub2/templates/tmpl2.html + + Where sub1 and sub2 are prefixes, and both tmpl1.html and tmpl2.html + incldue foo.html. + """ + dir1 = os.path.join(self.dirname, 'templates') + os.mkdir(dir1) + file1 = open(os.path.join(dir1, 'foo.html'), 'w') + try: + file1.write("""
      Included foo
      """) + finally: + file1.close() + + dir2 = os.path.join(self.dirname, 'sub1', 'templates') + os.makedirs(dir2) + file2 = open(os.path.join(dir2, 'tmpl1.html'), 'w') + try: + file2.write(""" + from sub1 + """) + finally: + file2.close() + + dir3 = os.path.join(self.dirname, 'sub2', 'templates') + os.makedirs(dir3) + file3 = open(os.path.join(dir3, 'tmpl2.html'), 'w') + try: + file3.write("""
      tmpl2
      """) + finally: + file3.close() + + loader = TemplateLoader([dir1, TemplateLoader.prefixed( + sub1 = dir2, + sub2 = dir3 + )]) + tmpl = loader.load('sub1/tmpl1.html') + self.assertEqual(""" +
      Included foo
      from sub1 + """, tmpl.generate().render()) + + def test_prefix_delegation_to_directories_with_subdirs(self): + """ + Test prefix delegation with the following layout: + + templates/foo.html + sub1/templates/tmpl1.html + sub1/templates/tmpl2.html + sub1/templates/bar/tmpl3.html + + Where sub1 is a prefix, and tmpl1.html includes all the others. + """ + dir1 = os.path.join(self.dirname, 'templates') + os.mkdir(dir1) + file1 = open(os.path.join(dir1, 'foo.html'), 'w') + try: + file1.write("""
      Included foo
      """) + finally: + file1.close() + + dir2 = os.path.join(self.dirname, 'sub1', 'templates') + os.makedirs(dir2) + file2 = open(os.path.join(dir2, 'tmpl1.html'), 'w') + try: + file2.write(""" + from sub1 + from sub1 + from sub1 + """) + finally: + file2.close() + + file3 = open(os.path.join(dir2, 'tmpl2.html'), 'w') + try: + file3.write("""
      tmpl2
      """) + finally: + file3.close() + + dir3 = os.path.join(self.dirname, 'sub1', 'templates', 'bar') + os.makedirs(dir3) + file4 = open(os.path.join(dir3, 'tmpl3.html'), 'w') + try: + file4.write("""
      bar/tmpl3
      """) + finally: + file4.close() + + loader = TemplateLoader([dir1, TemplateLoader.prefixed( + sub1 = os.path.join(dir2), + sub2 = os.path.join(dir3) + )]) + tmpl = loader.load('sub1/tmpl1.html') + self.assertEqual(""" +
      Included foo
      from sub1 +
      tmpl2
      from sub1 +
      bar/tmpl3
      from sub1 + """, tmpl.generate().render()) + def suite(): suite = unittest.TestSuite() diff --git a/genshi/template/tests/markup.py b/genshi/template/tests/markup.py --- a/genshi/template/tests/markup.py +++ b/genshi/template/tests/markup.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006-2007 Edgewall Software +# Copyright (C) 2006-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -13,6 +13,7 @@ import doctest import os +import pickle import shutil from StringIO import StringIO import sys @@ -22,7 +23,7 @@ from genshi.core import Markup from genshi.input import XML from genshi.template.base import BadDirectiveError, TemplateSyntaxError -from genshi.template.loader import TemplateLoader +from genshi.template.loader import TemplateLoader, TemplateNotFound from genshi.template.markup import MarkupTemplate @@ -39,6 +40,15 @@ tmpl = MarkupTemplate(stream) self.assertEqual(' 42 42', str(tmpl.generate(var=42))) + def test_pickle(self): + stream = XML('$var') + tmpl = MarkupTemplate(stream) + buf = StringIO() + pickle.dump(tmpl, buf, 2) + buf.seek(0) + unpickled = pickle.load(buf) + self.assertEqual('42', str(unpickled.generate(var=42))) + def test_interpolate_mixed3(self): tmpl = MarkupTemplate(' ${var} $var') self.assertEqual(' 42 42', str(tmpl.generate(var=42))) @@ -71,18 +81,16 @@ tmpl = MarkupTemplate(xml, filename='test.html') except BadDirectiveError, e: self.assertEqual('test.html', e.filename) - if sys.version_info[:2] >= (2, 4): - self.assertEqual(1, e.lineno) + self.assertEqual(1, e.lineno) def test_directive_value_syntax_error(self): xml = """

      """ try: - tmpl = MarkupTemplate(xml, filename='test.html') - self.fail('Expected SyntaxError') + tmpl = MarkupTemplate(xml, filename='test.html').generate() + self.fail('Expected TemplateSyntaxError') except TemplateSyntaxError, e: self.assertEqual('test.html', e.filename) - if sys.version_info[:2] >= (2, 4): - self.assertEqual(1, e.lineno) + self.assertEqual(1, e.lineno) def test_expression_syntax_error(self): xml = """

      @@ -90,11 +98,10 @@

      """ try: tmpl = MarkupTemplate(xml, filename='test.html') - self.fail('Expected SyntaxError') + self.fail('Expected TemplateSyntaxError') except TemplateSyntaxError, e: self.assertEqual('test.html', e.filename) - if sys.version_info[:2] >= (2, 4): - self.assertEqual(2, e.lineno) + self.assertEqual(2, e.lineno) def test_expression_syntax_error_multi_line(self): xml = """

      @@ -104,11 +111,10 @@

      """ try: tmpl = MarkupTemplate(xml, filename='test.html') - self.fail('Expected SyntaxError') + self.fail('Expected TemplateSyntaxError') except TemplateSyntaxError, e: self.assertEqual('test.html', e.filename) - if sys.version_info[:2] >= (2, 4): - self.assertEqual(3, e.lineno) + self.assertEqual(3, e.lineno) def test_markup_noescape(self): """ @@ -124,7 +130,8 @@ def test_text_noescape_quotes(self): """ - Verify that outputting context data in text nodes doesn't escape quotes. + Verify that outputting context data in text nodes doesn't escape + quotes. """ tmpl = MarkupTemplate("""
      $myvar @@ -195,6 +202,17 @@ \xf6
      """, unicode(tmpl.generate())) + def test_exec_with_trailing_space(self): + """ + Verify that a code block processing instruction with trailing space + does not cause a syntax error (see ticket #127). + """ + MarkupTemplate(u""" + + """) + def test_exec_import(self): tmpl = MarkupTemplate(u"""
      @@ -259,7 +277,7 @@ finally: shutil.rmtree(dirname) - def test_dynamic_inlude_href(self): + def test_dynamic_include_href(self): dirname = tempfile.mkdtemp(suffix='genshi_test') try: file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w') @@ -285,7 +303,7 @@ finally: shutil.rmtree(dirname) - def test_select_inluded_elements(self): + def test_select_included_elements(self): dirname = tempfile.mkdtemp(suffix='genshi_test') try: file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w') @@ -340,6 +358,23 @@ finally: shutil.rmtree(dirname) + def test_error_when_include_not_found(self): + dirname = tempfile.mkdtemp(suffix='genshi_test') + try: + file2 = open(os.path.join(dirname, 'tmpl2.html'), 'w') + try: + file2.write(""" + + """) + finally: + file2.close() + + loader = TemplateLoader([dirname], auto_reload=True) + tmpl = loader.load('tmpl2.html') + self.assertRaises(TemplateNotFound, tmpl.generate().render) + finally: + shutil.rmtree(dirname) + def test_fallback_when_include_not_found(self): dirname = tempfile.mkdtemp(suffix='genshi_test') try: @@ -360,6 +395,26 @@ finally: shutil.rmtree(dirname) + def test_fallback_when_auto_reload_true(self): + dirname = tempfile.mkdtemp(suffix='genshi_test') + try: + file2 = open(os.path.join(dirname, 'tmpl2.html'), 'w') + try: + file2.write(""" + + Missing + """) + finally: + file2.close() + + loader = TemplateLoader([dirname], auto_reload=True) + tmpl = loader.load('tmpl2.html') + self.assertEqual(""" + Missing + """, tmpl.generate().render()) + finally: + shutil.rmtree(dirname) + def test_include_in_fallback(self): dirname = tempfile.mkdtemp(suffix='genshi_test') try: @@ -386,7 +441,7 @@ loader = TemplateLoader([dirname]) tmpl = loader.load('tmpl3.html') self.assertEqual(""" -
      Included
      +
      Included
      """, tmpl.generate().render()) finally: shutil.rmtree(dirname) @@ -411,7 +466,36 @@ loader = TemplateLoader([dirname]) tmpl = loader.load('tmpl3.html') self.assertEqual(""" - Missing + Missing + """, tmpl.generate().render()) + finally: + shutil.rmtree(dirname) + + def test_nested_include_in_fallback(self): + dirname = tempfile.mkdtemp(suffix='genshi_test') + try: + file1 = open(os.path.join(dirname, 'tmpl2.html'), 'w') + try: + file1.write("""
      Included
      """) + finally: + file1.close() + + file2 = open(os.path.join(dirname, 'tmpl3.html'), 'w') + try: + file2.write(""" + + + + + + """) + finally: + file2.close() + + loader = TemplateLoader([dirname]) + tmpl = loader.load('tmpl3.html') + self.assertEqual(""" +
      Included
      """, tmpl.generate().render()) finally: shutil.rmtree(dirname) @@ -438,6 +522,212 @@ finally: shutil.rmtree(dirname) + def test_include_inlined(self): + dirname = tempfile.mkdtemp(suffix='genshi_test') + try: + file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w') + try: + file1.write("""
      Included
      """) + finally: + file1.close() + + file2 = open(os.path.join(dirname, 'tmpl2.html'), 'w') + try: + file2.write(""" + + """) + finally: + file2.close() + + loader = TemplateLoader([dirname], auto_reload=False) + tmpl = loader.load('tmpl2.html') + # if not inlined the following would be 5 + self.assertEqual(7, len(tmpl.stream)) + self.assertEqual(""" +
      Included
      + """, tmpl.generate().render()) + finally: + shutil.rmtree(dirname) + + def test_include_inlined_in_loop(self): + dirname = tempfile.mkdtemp(suffix='genshi_test') + try: + file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w') + try: + file1.write("""
      Included $idx
      """) + finally: + file1.close() + + file2 = open(os.path.join(dirname, 'tmpl2.html'), 'w') + try: + file2.write(""" + + """) + finally: + file2.close() + + loader = TemplateLoader([dirname], auto_reload=False) + tmpl = loader.load('tmpl2.html') + self.assertEqual(""" +
      Included 0
      Included 1
      Included 2
      + """, tmpl.generate().render()) + finally: + shutil.rmtree(dirname) + + def test_allow_exec_false(self): + xml = (""" + + + This is replaced. + + """) + try: + tmpl = MarkupTemplate(xml, filename='test.html', + allow_exec=False) + self.fail('Expected SyntaxError') + except TemplateSyntaxError, e: + pass + + def test_allow_exec_true(self): + xml = (""" + + + This is replaced. + + """) + tmpl = MarkupTemplate(xml, filename='test.html', allow_exec=True) + + def test_exec_in_match(self): + xml = (""" + + + ${title} + +

      moot text

      + """) + tmpl = MarkupTemplate(xml, filename='test.html', allow_exec=True) + self.assertEqual(""" + + wakka wakka wakka + + """, tmpl.generate().render()) + + def test_with_in_match(self): + xml = (""" + +

      ${select('text()')}

      + ${select('.')} +
      +

      ${foo}

      + """) + tmpl = MarkupTemplate(xml, filename='test.html') + self.assertEqual(""" + +

      bar

      +

      bar

      + + """, tmpl.generate().render()) + + def test_nested_include_matches(self): + # See ticket #157 + dirname = tempfile.mkdtemp(suffix='genshi_test') + try: + file1 = open(os.path.join(dirname, 'tmpl1.html'), 'w') + try: + file1.write(""" +
      Some content.
      +""") + finally: + file1.close() + + file2 = open(os.path.join(dirname, 'tmpl2.html'), 'w') + try: + file2.write(""" + +

      Some full html document that includes file1.html

      + + +""") + finally: + file2.close() + + file3 = open(os.path.join(dirname, 'tmpl3.html'), 'w') + try: + file3.write(""" +
      + Some added stuff. + ${select('*|text()')} +
      + + +""") + finally: + file3.close() + + loader = TemplateLoader([dirname]) + tmpl = loader.load('tmpl3.html') + self.assertEqual(""" + + +

      Some full html document that includes file1.html

      +
      + Some added stuff. + Some content. +
      + + +""", tmpl.generate().render()) + finally: + shutil.rmtree(dirname) + + def test_nested_matches_without_buffering(self): + xml = (""" + + + ${select('*|text')} + And some other stuff... + + + + Foo + Bar + + """) + tmpl = MarkupTemplate(xml, filename='test.html') + self.assertEqual(""" + + Foo + And some other stuff... + + """, tmpl.generate().render()) + + def test_match_without_select(self): + # See + xml = (""" + + + This replaces the other text. + + + + This gets replaced. + + """) + tmpl = MarkupTemplate(xml, filename='test.html') + self.assertEqual(""" + + This replaces the other text. + + """, tmpl.generate().render()) + def suite(): suite = unittest.TestSuite() diff --git a/genshi/template/tests/plugin.py b/genshi/template/tests/plugin.py --- a/genshi/template/tests/plugin.py +++ b/genshi/template/tests/plugin.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006 Edgewall Software +# Copyright (C) 2006-2007 Edgewall Software # Copyright (C) 2006 Matthew Good # All rights reserved. # @@ -18,7 +18,7 @@ from genshi.core import Stream from genshi.output import DocType -from genshi.template import MarkupTemplate, TextTemplate +from genshi.template import MarkupTemplate, TextTemplate, NewTextTemplate from genshi.template.plugin import ConfigurationError, \ MarkupTemplateEnginePlugin, \ TextTemplateEnginePlugin @@ -145,6 +145,23 @@ """, output) + def test_render_fragment_with_doctype(self): + plugin = MarkupTemplateEnginePlugin(options={ + 'genshi.default_doctype': 'html-strict', + }) + tmpl = plugin.load_template(PACKAGE + '.templates.test_no_doctype') + output = plugin.render({'message': 'Hello'}, template=tmpl, + fragment=True) + self.assertEqual(""" + + Test + + +

      Test

      +

      Hello

      + +""", output) + def test_helper_functions(self): plugin = MarkupTemplateEnginePlugin() tmpl = plugin.load_template(PACKAGE + '.templates.functions') @@ -185,6 +202,15 @@ }) self.assertEqual('iso-8859-15', plugin.default_encoding) + def test_init_with_new_syntax(self): + plugin = TextTemplateEnginePlugin(options={ + 'genshi.new_text_syntax': 'yes', + }) + self.assertEqual(NewTextTemplate, plugin.template_class) + tmpl = plugin.load_template(PACKAGE + '.templates.new_syntax') + output = plugin.render({'foo': True}, template=tmpl) + self.assertEqual('bar', output) + def test_load_template_from_file(self): plugin = TextTemplateEnginePlugin() tmpl = plugin.load_template(PACKAGE + '.templates.test') diff --git a/genshi/template/tests/templates/new_syntax.txt b/genshi/template/tests/templates/new_syntax.txt new file mode 100644 --- /dev/null +++ b/genshi/template/tests/templates/new_syntax.txt @@ -0,0 +1,1 @@ +{% if foo %}bar{% end %} \ No newline at end of file diff --git a/genshi/template/tests/templates/test_no_doctype.html b/genshi/template/tests/templates/test_no_doctype.html new file mode 100644 --- /dev/null +++ b/genshi/template/tests/templates/test_no_doctype.html @@ -0,0 +1,13 @@ + + + + Test + + + +

      Test

      +

      $message

      + + + diff --git a/genshi/template/tests/text.py b/genshi/template/tests/text.py --- a/genshi/template/tests/text.py +++ b/genshi/template/tests/text.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006 Edgewall Software +# Copyright (C) 2006-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -17,11 +17,12 @@ import tempfile import unittest +from genshi.template.base import TemplateSyntaxError from genshi.template.loader import TemplateLoader -from genshi.template.text import TextTemplate +from genshi.template.text import OldTextTemplate, NewTextTemplate -class TextTemplateTestCase(unittest.TestCase): +class OldTextTemplateTestCase(unittest.TestCase): """Tests for text template processing.""" def setUp(self): @@ -31,19 +32,19 @@ shutil.rmtree(self.dirname) def test_escaping(self): - tmpl = TextTemplate('\\#escaped') + tmpl = OldTextTemplate('\\#escaped') self.assertEqual('#escaped', str(tmpl.generate())) def test_comment(self): - tmpl = TextTemplate('## a comment') + tmpl = OldTextTemplate('## a comment') self.assertEqual('', str(tmpl.generate())) def test_comment_escaping(self): - tmpl = TextTemplate('\\## escaped comment') + tmpl = OldTextTemplate('\\## escaped comment') self.assertEqual('## escaped comment', str(tmpl.generate())) def test_end_with_args(self): - tmpl = TextTemplate(""" + tmpl = OldTextTemplate(""" #if foo bar #end 'if foo'""") @@ -51,11 +52,16 @@ def test_latin1_encoded(self): text = u'$foo\xf6$bar'.encode('iso-8859-1') - tmpl = TextTemplate(text, encoding='iso-8859-1') + tmpl = OldTextTemplate(text, encoding='iso-8859-1') + self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y'))) + + def test_unicode_input(self): + text = u'$foo\xf6$bar' + tmpl = OldTextTemplate(text) self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y'))) def test_empty_lines1(self): - tmpl = TextTemplate("""Your items: + tmpl = OldTextTemplate("""Your items: #for item in items * ${item} @@ -65,10 +71,10 @@ * 0 * 1 * 2 -""", tmpl.generate(items=range(3)).render('text')) +""", tmpl.generate(items=range(3)).render()) def test_empty_lines2(self): - tmpl = TextTemplate("""Your items: + tmpl = OldTextTemplate("""Your items: #for item in items * ${item} @@ -82,7 +88,7 @@ * 2 -""", tmpl.generate(items=range(3)).render('text')) +""", tmpl.generate(items=range(3)).render()) def test_include(self): file1 = open(os.path.join(self.dirname, 'tmpl1.txt'), 'w') @@ -100,16 +106,159 @@ file2.close() loader = TemplateLoader([self.dirname]) - tmpl = loader.load('tmpl2.txt', cls=TextTemplate) + tmpl = loader.load('tmpl2.txt', cls=OldTextTemplate) self.assertEqual("""----- Included data below this line ----- Included ----- Included data above this line -----""", tmpl.generate().render()) - + + +class NewTextTemplateTestCase(unittest.TestCase): + """Tests for text template processing.""" + + def setUp(self): + self.dirname = tempfile.mkdtemp(suffix='markup_test') + + def tearDown(self): + shutil.rmtree(self.dirname) + + def test_escaping(self): + tmpl = NewTextTemplate('\\{% escaped %}') + self.assertEqual('{% escaped %}', str(tmpl.generate())) + + def test_comment(self): + tmpl = NewTextTemplate('{# a comment #}') + self.assertEqual('', str(tmpl.generate())) + + def test_comment_escaping(self): + tmpl = NewTextTemplate('\\{# escaped comment #}') + self.assertEqual('{# escaped comment #}', str(tmpl.generate())) + + def test_end_with_args(self): + tmpl = NewTextTemplate(""" +{% if foo %} + bar +{% end 'if foo' %}""") + self.assertEqual('\n', str(tmpl.generate(foo=False))) + + def test_latin1_encoded(self): + text = u'$foo\xf6$bar'.encode('iso-8859-1') + tmpl = NewTextTemplate(text, encoding='iso-8859-1') + self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y'))) + + def test_unicode_input(self): + text = u'$foo\xf6$bar' + tmpl = NewTextTemplate(text) + self.assertEqual(u'x\xf6y', unicode(tmpl.generate(foo='x', bar='y'))) + + def test_empty_lines1(self): + tmpl = NewTextTemplate("""Your items: + +{% for item in items %}\ + * ${item} +{% end %}""") + self.assertEqual("""Your items: + + * 0 + * 1 + * 2 +""", tmpl.generate(items=range(3)).render()) + + def test_empty_lines2(self): + tmpl = NewTextTemplate("""Your items: + +{% for item in items %}\ + * ${item} + +{% end %}""") + self.assertEqual("""Your items: + + * 0 + + * 1 + + * 2 + +""", tmpl.generate(items=range(3)).render()) + + def test_exec_with_trailing_space(self): + """ + Verify that a code block with trailing space does not cause a syntax + error (see ticket #127). + """ + NewTextTemplate(u""" + {% python + bar = 42 + $} + """) + + def test_exec_import(self): + tmpl = NewTextTemplate(u"""{% python from datetime import timedelta %} + ${timedelta(days=2)} + """) + self.assertEqual(""" + 2 days, 0:00:00 + """, str(tmpl.generate())) + + def test_exec_def(self): + tmpl = NewTextTemplate(u"""{% python + def foo(): + return 42 + %} + ${foo()} + """) + self.assertEqual(u""" + 42 + """, str(tmpl.generate())) + + def test_include(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.txt'), 'w') + try: + file1.write("Included") + finally: + file1.close() + + file2 = open(os.path.join(self.dirname, 'tmpl2.txt'), 'w') + try: + file2.write("""----- Included data below this line ----- +{% include tmpl1.txt %} +----- Included data above this line -----""") + finally: + file2.close() + + loader = TemplateLoader([self.dirname]) + tmpl = loader.load('tmpl2.txt', cls=NewTextTemplate) + self.assertEqual("""----- Included data below this line ----- +Included +----- Included data above this line -----""", tmpl.generate().render()) + + def test_include_expr(self): + file1 = open(os.path.join(self.dirname, 'tmpl1.txt'), 'w') + try: + file1.write("Included") + finally: + file1.close() + + file2 = open(os.path.join(self.dirname, 'tmpl2.txt'), 'w') + try: + file2.write("""----- Included data below this line ----- + {% include ${'%s.txt' % ('tmpl1',)} %} + ----- Included data above this line -----""") + finally: + file2.close() + + loader = TemplateLoader([self.dirname]) + tmpl = loader.load('tmpl2.txt', cls=NewTextTemplate) + self.assertEqual("""----- Included data below this line ----- + Included + ----- Included data above this line -----""", tmpl.generate().render()) + + def suite(): suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(TextTemplate.__module__)) - suite.addTest(unittest.makeSuite(TextTemplateTestCase, 'test')) + suite.addTest(doctest.DocTestSuite(NewTextTemplate.__module__)) + suite.addTest(unittest.makeSuite(OldTextTemplateTestCase, 'test')) + suite.addTest(unittest.makeSuite(NewTextTemplateTestCase, 'test')) return suite if __name__ == '__main__': diff --git a/genshi/template/text.py b/genshi/template/text.py --- a/genshi/template/text.py +++ b/genshi/template/text.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006-2007 Edgewall Software +# Copyright (C) 2006-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -11,23 +11,235 @@ # individuals. For the exact contribution history, see the revision # history and logs, available at http://genshi.edgewall.org/log/. -"""Plain text templating engine.""" +"""Plain text templating engine. + +This module implements two template language syntaxes, at least for a certain +transitional period. `OldTextTemplate` (aliased to just `TextTemplate`) defines +a syntax that was inspired by Cheetah/Velocity. `NewTextTemplate` on the other +hand is inspired by the syntax of the Django template language, which has more +explicit delimiting of directives, and is more flexible with regards to +white space and line breaks. + +In a future release, `OldTextTemplate` will be phased out in favor of +`NewTextTemplate`, as the names imply. Therefore the new syntax is strongly +recommended for new projects, and existing projects may want to migrate to the +new syntax to remain compatible with future Genshi releases. +""" import re -from genshi.template.base import BadDirectiveError, Template, INCLUDE, SUB +from genshi.core import TEXT +from genshi.template.base import BadDirectiveError, Template, \ + TemplateSyntaxError, EXEC, INCLUDE, SUB +from genshi.template.eval import Suite from genshi.template.directives import * -from genshi.template.directives import Directive, _apply_directives +from genshi.template.directives import Directive from genshi.template.interpolation import interpolate -__all__ = ['TextTemplate'] +__all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate'] __docformat__ = 'restructuredtext en' -class TextTemplate(Template): - """Implementation of a simple text-based template engine. +class NewTextTemplate(Template): + r"""Implementation of a simple text-based template engine. This class will + replace `OldTextTemplate` in a future release. - >>> tmpl = TextTemplate('''Dear $name, + It uses a more explicit delimiting style for directives: instead of the old + style which required putting directives on separate lines that were prefixed + with a ``#`` sign, directives and commenbtsr are enclosed in delimiter pairs + (by default ``{% ... %}`` and ``{# ... #}``, respectively). + + Variable substitution uses the same interpolation syntax as for markup + languages: simple references are prefixed with a dollar sign, more complex + expression enclosed in curly braces. + + >>> tmpl = NewTextTemplate('''Dear $name, + ... + ... {# This is a comment #} + ... We have the following items for you: + ... {% for item in items %} + ... * ${'Item %d' % item} + ... {% end %} + ... ''') + >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render() + Dear Joe, + + + We have the following items for you: + + * Item 1 + + * Item 2 + + * Item 3 + + + + By default, no spaces or line breaks are removed. If a line break should + not be included in the output, prefix it with a backslash: + + >>> tmpl = NewTextTemplate('''Dear $name, + ... + ... {# This is a comment #}\ + ... We have the following items for you: + ... {% for item in items %}\ + ... * $item + ... {% end %}\ + ... ''') + >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render() + Dear Joe, + + We have the following items for you: + * 1 + * 2 + * 3 + + + Backslashes are also used to escape the start delimiter of directives and + comments: + + >>> tmpl = NewTextTemplate('''Dear $name, + ... + ... \{# This is a comment #} + ... We have the following items for you: + ... {% for item in items %}\ + ... * $item + ... {% end %}\ + ... ''') + >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render() + Dear Joe, + + {# This is a comment #} + We have the following items for you: + * 1 + * 2 + * 3 + + + :since: version 0.5 + """ + directives = [('def', DefDirective), + ('when', WhenDirective), + ('otherwise', OtherwiseDirective), + ('for', ForDirective), + ('if', IfDirective), + ('choose', ChooseDirective), + ('with', WithDirective)] + serializer = 'text' + + _DIRECTIVE_RE = r'((? offset: + text = _escape_sub(_escape_repl, source[offset:start]) + for kind, data, pos in interpolate(text, self.filepath, lineno, + lookup=self.lookup): + stream.append((kind, data, pos)) + lineno += len(text.splitlines()) + + lineno += len(source[start:end].splitlines()) + command, value = mo.group(2, 3) + + if command == 'include': + pos = (self.filename, lineno, 0) + value = list(interpolate(value, self.filepath, lineno, 0, + lookup=self.lookup)) + if len(value) == 1 and value[0][0] is TEXT: + value = value[0][1] + stream.append((INCLUDE, (value, None, []), pos)) + + elif command == 'python': + if not self.allow_exec: + raise TemplateSyntaxError('Python code blocks not allowed', + self.filepath, lineno) + try: + suite = Suite(value, self.filepath, lineno, + lookup=self.lookup) + except SyntaxError, err: + raise TemplateSyntaxError(err, self.filepath, + lineno + (err.lineno or 1) - 1) + pos = (self.filename, lineno, 0) + stream.append((EXEC, suite, pos)) + + elif command == 'end': + depth -= 1 + if depth in dirmap: + directive, start_offset = dirmap.pop(depth) + substream = stream[start_offset:] + stream[start_offset:] = [(SUB, ([directive], substream), + (self.filepath, lineno, 0))] + + elif command: + cls = self.get_directive(command) + if cls is None: + raise BadDirectiveError(command) + directive = cls, value, None, (self.filepath, lineno, 0) + dirmap[depth] = (directive, len(stream)) + depth += 1 + + offset = end + + if offset < len(source): + text = _escape_sub(_escape_repl, source[offset:]) + for kind, data, pos in interpolate(text, self.filepath, lineno, + lookup=self.lookup): + stream.append((kind, data, pos)) + + return stream + + +class OldTextTemplate(Template): + """Legacy implementation of the old syntax text-based templates. This class + is provided in a transition phase for backwards compatibility. New code + should use the `NewTextTemplate` class and the improved syntax it provides. + + >>> tmpl = OldTextTemplate('''Dear $name, ... ... We have the following items for you: ... #for item in items @@ -36,7 +248,7 @@ ... ... All the best, ... Foobar''') - >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render('text') + >>> print tmpl.generate(name='Joe', items=[1, 2, 3]).render() Dear Joe, We have the following items for you: @@ -54,6 +266,7 @@ ('if', IfDirective), ('choose', ChooseDirective), ('with', WithDirective)] + serializer = 'text' _DIRECTIVE_RE = re.compile(r'(?:^[ \t]*(? offset: text = source[offset:start] - for kind, data, pos in interpolate(text, self.basedir, - self.filename, lineno, + for kind, data, pos in interpolate(text, self.filepath, lineno, lookup=self.lookup): stream.append((kind, data, pos)) lineno += len(text.splitlines()) @@ -98,9 +310,9 @@ (self.filepath, lineno, 0))] elif command == 'include': pos = (self.filename, lineno, 0) - stream.append((INCLUDE, (value.strip(), []), pos)) + stream.append((INCLUDE, (value.strip(), None, []), pos)) elif command != '#': - cls = self._dir_by_name.get(command) + cls = self.get_directive(command) if cls is None: raise BadDirectiveError(command) directive = cls, value, None, (self.filepath, lineno, 0) @@ -111,9 +323,11 @@ if offset < len(source): text = source[offset:].replace('\\#', '#') - for kind, data, pos in interpolate(text, self.basedir, - self.filename, lineno, + for kind, data, pos in interpolate(text, self.filepath, lineno, lookup=self.lookup): stream.append((kind, data, pos)) return stream + + +TextTemplate = OldTextTemplate diff --git a/genshi/tests/builder.py b/genshi/tests/builder.py --- a/genshi/tests/builder.py +++ b/genshi/tests/builder.py @@ -16,7 +16,7 @@ import unittest from genshi.builder import Element, tag -from genshi.core import Attrs, Stream +from genshi.core import Attrs, Markup, Stream from genshi.input import XML @@ -42,6 +42,15 @@ (None, -1, -1)), event) + def test_duplicate_attributes(self): + link = tag.a(href='#1', href_='#2')('Bar') + bits = iter(link.generate()) + self.assertEqual((Stream.START, + ('a', Attrs([('href', "#1")])), + (None, -1, -1)), bits.next()) + self.assertEqual((Stream.TEXT, u'Bar', (None, -1, -1)), bits.next()) + self.assertEqual((Stream.END, 'a', (None, -1, -1)), bits.next()) + def test_stream_as_child(self): xml = list(tag.span(XML('Foo')).generate()) self.assertEqual(5, len(xml)) @@ -51,6 +60,12 @@ self.assertEqual((Stream.END, 'b'), xml[3][:2]) self.assertEqual((Stream.END, 'span'), xml[4][:2]) + def test_markup_escape(self): + from genshi.core import Markup + m = Markup('See %s') % tag.a('genshi', + href='http://genshi.edgwall.org') + self.assertEqual(m, Markup('See ' + 'genshi')) def suite(): suite = unittest.TestSuite() diff --git a/genshi/tests/core.py b/genshi/tests/core.py --- a/genshi/tests/core.py +++ b/genshi/tests/core.py @@ -14,10 +14,14 @@ import doctest import pickle from StringIO import StringIO +try: + from cStringIO import StringIO as cStringIO +except ImportError: + cStringIO = StringIO import unittest from genshi import core -from genshi.core import Markup, Namespace, QName, escape, unescape +from genshi.core import Markup, Attrs, Namespace, QName, escape, unescape from genshi.input import XML, ParseError @@ -35,6 +39,18 @@ xml = XML('
    • Über uns
    • ') self.assertEqual('
    • Über uns
    • ', xml.render(encoding='ascii')) + def test_render_output_stream_utf8(self): + xml = XML('
    • Über uns
    • ') + strio = cStringIO() + self.assertEqual(None, xml.render(out=strio)) + self.assertEqual('
    • Über uns
    • ', strio.getvalue()) + + def test_render_output_stream_unicode(self): + xml = XML('
    • Über uns
    • ') + strio = StringIO() + self.assertEqual(None, xml.render(encoding=None, out=strio)) + self.assertEqual(u'
    • Über uns
    • ', strio.getvalue()) + def test_pickle(self): xml = XML('
    • Foo
    • ') buf = StringIO() @@ -46,6 +62,10 @@ class MarkupTestCase(unittest.TestCase): + def test_new_with_encoding(self): + markup = Markup('Döner', encoding='utf-8') + self.assertEquals("", repr(markup)) + def test_repr(self): markup = Markup('foo') self.assertEquals("", repr(markup)) @@ -91,6 +111,16 @@ assert type(markup) is Markup self.assertEquals('& boo', markup) + def test_mod_mapping(self): + markup = Markup('%(foo)s') % {'foo': '&'} + assert type(markup) is Markup + self.assertEquals('&', markup) + + def test_mod_noescape(self): + markup = Markup('%(amp)s') % {'amp': Markup('&')} + assert type(markup) is Markup + self.assertEquals('&', markup) + def test_mul(self): markup = Markup('foo') * 2 assert type(markup) is Markup @@ -134,6 +164,18 @@ self.assertEquals("", repr(pickle.load(buf))) +class AttrsTestCase(unittest.TestCase): + + def test_pickle(self): + attrs = Attrs([("attr1", "foo"), ("attr2", "bar")]) + buf = StringIO() + pickle.dump(attrs, buf, 2) + buf.seek(0) + unpickled = pickle.load(buf) + self.assertEquals("Attrs([('attr1', 'foo'), ('attr2', 'bar')])", + repr(unpickled)) + + class NamespaceTestCase(unittest.TestCase): def test_pickle(self): @@ -165,12 +207,18 @@ self.assertEqual("QName(u'http://www.example.org/namespace}elem')", repr(QName('http://www.example.org/namespace}elem'))) + def test_leading_curly_brace(self): + qname = QName('{http://www.example.org/namespace}elem') + self.assertEquals('http://www.example.org/namespace', qname.namespace) + self.assertEquals('elem', qname.localname) + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(StreamTestCase, 'test')) suite.addTest(unittest.makeSuite(MarkupTestCase, 'test')) suite.addTest(unittest.makeSuite(NamespaceTestCase, 'test')) + suite.addTest(unittest.makeSuite(AttrsTestCase, 'test')) suite.addTest(unittest.makeSuite(QNameTestCase, 'test')) suite.addTest(doctest.DocTestSuite(core)) return suite diff --git a/genshi/tests/input.py b/genshi/tests/input.py --- a/genshi/tests/input.py +++ b/genshi/tests/input.py @@ -28,8 +28,7 @@ kind, data, pos = events[1] self.assertEqual(Stream.TEXT, kind) self.assertEqual(u'foo bar', data) - if sys.version_info[:2] >= (2, 4): - self.assertEqual((None, 1, 6), pos) + self.assertEqual((None, 1, 6), pos) def test_text_node_pos_multi_line(self): text = '''foo @@ -38,8 +37,7 @@ kind, data, pos = events[1] self.assertEqual(Stream.TEXT, kind) self.assertEqual(u'foo\nbar', data) - if sys.version_info[:2] >= (2, 4): - self.assertEqual((None, 1, -1), pos) + self.assertEqual((None, 1, -1), pos) def test_element_attribute_order(self): text = '' @@ -123,8 +121,7 @@ kind, data, pos = events[1] self.assertEqual(Stream.TEXT, kind) self.assertEqual(u'foo bar', data) - if sys.version_info[:2] >= (2, 4): - self.assertEqual((None, 1, 6), pos) + self.assertEqual((None, 1, 6), pos) def test_text_node_pos_multi_line(self): text = '''foo @@ -133,8 +130,7 @@ kind, data, pos = events[1] self.assertEqual(Stream.TEXT, kind) self.assertEqual(u'foo\nbar', data) - if sys.version_info[:2] >= (2, 4): - self.assertEqual((None, 1, 6), pos) + self.assertEqual((None, 1, 6), pos) def test_input_encoding_text(self): text = u'
      \xf6
      '.encode('iso-8859-1') diff --git a/genshi/tests/output.py b/genshi/tests/output.py --- a/genshi/tests/output.py +++ b/genshi/tests/output.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006-2007 Edgewall Software +# Copyright (C) 2006-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -23,6 +23,15 @@ class XMLSerializerTestCase(unittest.TestCase): + def test_with_xml_decl(self): + stream = Stream([(Stream.XML_DECL, ('1.0', None, -1), (None, -1, -1))]) + output = stream.render(XMLSerializer, doctype='xhtml') + self.assertEqual('\n' + '\n', + output) + def test_doctype_in_stream(self): stream = Stream([(Stream.DOCTYPE, DocType.HTML_STRICT, (None, -1, -1))]) output = stream.render(XMLSerializer) @@ -194,6 +203,34 @@ class XHTMLSerializerTestCase(unittest.TestCase): + def test_xml_decl_dropped(self): + stream = Stream([(Stream.XML_DECL, ('1.0', None, -1), (None, -1, -1))]) + output = stream.render(XHTMLSerializer, doctype='xhtml') + self.assertEqual('\n', + output) + + def test_xml_decl_included(self): + stream = Stream([(Stream.XML_DECL, ('1.0', None, -1), (None, -1, -1))]) + output = stream.render(XHTMLSerializer, doctype='xhtml', + drop_xml_decl=False) + self.assertEqual('\n' + '\n', + output) + + def test_xml_lang(self): + text = '

      English text

      ' + output = XML(text).render(XHTMLSerializer) + self.assertEqual('

      English text

      ', output) + + def test_xml_lang_nodup(self): + text = '

      English text

      ' + output = XML(text).render(XHTMLSerializer) + self.assertEqual('

      English text

      ', output) + def test_textarea_whitespace(self): content = '\nHey there. \n\n I am indented.\n' stream = XML('' % content) @@ -209,7 +246,7 @@ def test_xml_space(self): text = ' Do not mess \n\n with me ' output = XML(text).render(XHTMLSerializer) - self.assertEqual(text, output) + self.assertEqual(' Do not mess \n\n with me ', output) def test_empty_script(self): text = """ @@ -324,6 +361,16 @@ class HTMLSerializerTestCase(unittest.TestCase): + def test_xml_lang(self): + text = '

      English text

      ' + output = XML(text).render(HTMLSerializer) + self.assertEqual('

      English text

      ', output) + + def test_xml_lang_nodup(self): + text = '

      English text

      ' + output = XML(text).render(HTMLSerializer) + self.assertEqual('

      English text

      ', output) + def test_textarea_whitespace(self): content = '\nHey there. \n\n I am indented.\n' stream = XML('' % content) @@ -343,7 +390,7 @@ def test_empty_script(self): text = '', output) def test_script_escaping(self): diff --git a/genshi/tests/path.py b/genshi/tests/path.py --- a/genshi/tests/path.py +++ b/genshi/tests/path.py @@ -15,11 +15,56 @@ import unittest from genshi.input import XML -from genshi.path import Path, PathSyntaxError +from genshi.path import Path, PathParser, PathSyntaxError, GenericStrategy, \ + SingleStepStrategy, SimplePathStrategy +class FakePath(Path): + def __init__(self, strategy): + self.strategy = strategy + def test(self, ignore_context = False): + return self.strategy.test(ignore_context) + class PathTestCase(unittest.TestCase): + strategies = [GenericStrategy, SingleStepStrategy, SimplePathStrategy] + def _create_path(self, expression, expected): + return path + + def _test_strategies(self, stream, path, render, + namespaces=None, variables=None): + for strategy in self.strategies: + if not strategy.supports(path): + continue + s = strategy(path) + rendered = FakePath(s).select(stream,namespaces=namespaces, + variables=variables).render() + msg = "Bad render using %s strategy"%str(strategy) + msg += "\nExpected:\t'%s'"%render + msg += "\nRendered:\t'%s'"%rendered + self.assertEqual(render, rendered, msg) + + def _test_expression(self, text, expected, stream=None, render="", + namespaces=None, variables=None): + path = Path(text) + if expected is not None: + self.assertEqual(expected, repr(path)) + + if stream is None: + return + + rendered = path.select(stream, namespaces=namespaces, + variables=variables).render() + msg = "Bad render using whole path" + msg += "\nExpected:\t'%s'"%render + msg += "\nRendered:\t'%s'"%rendered + self.assertEqual(render, rendered, msg) + + if len(path.paths) == 1: + self._test_strategies(stream, path.paths[0], render, + namespaces=namespaces, variables=variables) + + def test_error_no_absolute_path(self): self.assertRaises(PathSyntaxError, Path, '/root') @@ -30,413 +75,441 @@ def test_1step(self): xml = XML('') - path = Path('elem') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) - - path = Path('child::elem') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( 'elem', + '', + xml, + '') - path = Path('//elem') - self.assertEqual('', - repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( 'elem', + '', + xml, + '') - path = Path('descendant::elem') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( 'child::elem', + '', + xml, + '') + + self._test_expression( '//elem', + '', + xml, + '') + + self._test_expression( 'descendant::elem', + '', + xml, + '') def test_1step_self(self): xml = XML('') - path = Path('.') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( '.', + '', + xml, + '') - path = Path('self::node()') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( 'self::node()', + '', + xml, + '') def test_1step_wildcard(self): xml = XML('') - path = Path('*') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) - - path = Path('child::*') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( '*', + '', + xml, + '') - path = Path('child::node()') - self.assertEqual('', repr(path)) - self.assertEqual('', Path('child::node()').select(xml).render()) + self._test_expression( 'child::*', + '', + xml, + '') - path = Path('//*') - self.assertEqual('', - repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( 'child::node()', + '', + xml, + '') + + self._test_expression( '//*', + '', + xml, + '') def test_1step_attribute(self): - path = Path('@foo') - self.assertEqual('', repr(path)) - - xml = XML('') - self.assertEqual('', path.select(xml).render()) + self._test_expression( '@foo', + '', + XML(''), + '') xml = XML('') - self.assertEqual('bar', path.select(xml).render()) - path = Path('./@foo') - self.assertEqual('', repr(path)) - self.assertEqual('bar', path.select(xml).render()) + self._test_expression( '@foo', + '', + xml, + 'bar') + + self._test_expression( './@foo', + '', + xml, + 'bar') def test_1step_text(self): xml = XML('Hey') - path = Path('text()') - self.assertEqual('', repr(path)) - self.assertEqual('Hey', path.select(xml).render()) - - path = Path('./text()') - self.assertEqual('', repr(path)) - self.assertEqual('Hey', path.select(xml).render()) + self._test_expression( 'text()', + '', + xml, + 'Hey') - path = Path('//text()') - self.assertEqual('', - repr(path)) - self.assertEqual('Hey', path.select(xml).render()) + self._test_expression( './text()', + '', + xml, + 'Hey') - path = Path('.//text()') - self.assertEqual('', - repr(path)) - self.assertEqual('Hey', path.select(xml).render()) + self._test_expression( '//text()', + '', + xml, + 'Hey') + + self._test_expression( './/text()', + '', + xml, + 'Hey') def test_2step(self): xml = XML('') - self.assertEqual('', Path('*').select(xml).render()) - self.assertEqual('', Path('bar').select(xml).render()) - self.assertEqual('', Path('baz').select(xml).render()) + self._test_expression('*', None, xml, '') + self._test_expression('bar', None, xml, '') + self._test_expression('baz', None, xml, '') def test_2step_attribute(self): xml = XML('Hey Joe') - self.assertEqual('x', Path('@*').select(xml).render()) - self.assertEqual('x', Path('./@*').select(xml).render()) - self.assertEqual('xjoe', Path('.//@*').select(xml).render()) - self.assertEqual('joe', Path('*/@*').select(xml).render()) + self._test_expression('@*', None, xml, 'x') + self._test_expression('./@*', None, xml, 'x') + self._test_expression('.//@*', None, xml, 'xjoe') + self._test_expression('*/@*', None, xml, 'joe') xml = XML('') - self.assertEqual('', Path('@*').select(xml).render()) - self.assertEqual('12', Path('foo/@*').select(xml).render()) + self._test_expression('@*', None, xml, '') + self._test_expression('foo/@*', None, xml, '12') def test_2step_complex(self): xml = XML('') - path = Path('foo/bar') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( 'foo/bar', + '', + xml, + '') - path = Path('./bar') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( './bar', + '', + xml, + '') - path = Path('foo/*') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( 'foo/*', + '', + xml, + '') xml = XML('') - path = Path('./bar') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( './bar', + '', + xml, + '') def test_2step_text(self): xml = XML('Foo') - path = Path('item/text()') - self.assertEqual('', repr(path)) - self.assertEqual('Foo', path.select(xml).render()) - - path = Path('*/text()') - self.assertEqual('', repr(path)) - self.assertEqual('Foo', path.select(xml).render()) + self._test_expression( 'item/text()', + '', + xml, + 'Foo') - path = Path('//text()') - self.assertEqual('', - repr(path)) - self.assertEqual('Foo', path.select(xml).render()) + self._test_expression( '*/text()', + '', + xml, + 'Foo') - path = Path('./text()') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( '//text()', + '', + xml, + 'Foo') + + self._test_expression( './text()', + '', + xml, + '') xml = XML('FooBar') - path = Path('item/text()') - self.assertEqual('', repr(path)) - self.assertEqual('FooBar', path.select(xml).render()) - - xml = XML('FooBar') - self.assertEqual('FooBar', path.select(xml).render()) + self._test_expression( 'item/text()', + '', + xml, + 'FooBar') def test_3step(self): xml = XML('') - path = Path('foo/*') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( 'foo/*', + '', + xml, + '') def test_3step_complex(self): xml = XML('') - path = Path('*/bar') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( '*/bar', + '', + xml, + '') xml = XML('') - path = Path('//bar') - self.assertEqual('', - repr(path)) - self.assertEqual('', - path.select(xml).render()) + self._test_expression( '//bar', + '', + xml, + '') def test_node_type_comment(self): xml = XML('') - path = Path('comment()') - self.assertEqual('', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( 'comment()', + '', + xml, + '') def test_node_type_text(self): xml = XML('Some text
      in here.
      ') - path = Path('text()') - self.assertEqual('', repr(path)) - self.assertEqual('Some text in here.', path.select(xml).render()) + self._test_expression( 'text()', + '', + xml, + 'Some text in here.') def test_node_type_node(self): xml = XML('Some text
      in here.
      ') - path = Path('node()') - self.assertEqual('', repr(path)) - self.assertEqual('Some text
      in here.', path.select(xml).render()) + self._test_expression( 'node()', + '', + xml, + 'Some text
      in here.',) def test_node_type_processing_instruction(self): xml = XML('') - path = Path('processing-instruction()') - self.assertEqual('', - repr(path)) - self.assertEqual('', - path.select(xml).render()) + self._test_expression( '//processing-instruction()', + '', + xml, + '') - path = Path('processing-instruction("php")') - self.assertEqual('', - repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( 'processing-instruction()', + '', + xml, + '') + + self._test_expression( 'processing-instruction("php")', + '', + xml, + '') def test_simple_union(self): xml = XML("""1
      2
      3
      """) - path = Path('*|text()') - self.assertEqual('', repr(path)) - self.assertEqual('1
      2
      3
      ', path.select(xml).render()) + self._test_expression( '*|text()', + '', + xml, + '1
      2
      3
      ') def test_predicate_name(self): xml = XML('') - path = Path('*[name()="foo"]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[name()="foo"]', None, xml, '') def test_predicate_localname(self): xml = XML('') - path = Path('*[local-name()="foo"]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[local-name()="foo"]', None, xml, + '') def test_predicate_namespace(self): xml = XML('') - path = Path('*[namespace-uri()="NS"]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[namespace-uri()="NS"]', None, xml, + '') def test_predicate_not_name(self): xml = XML('') - path = Path('*[not(name()="foo")]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[not(name()="foo")]', None, xml, '') def test_predicate_attr(self): xml = XML('') - path = Path('item[@important]') - self.assertEqual('', path.select(xml).render()) - path = Path('item[@important="very"]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('item[@important]', None, xml, + '') + self._test_expression('item[@important="very"]', None, xml, + '') def test_predicate_attr_equality(self): xml = XML('') - path = Path('item[@important="very"]') - self.assertEqual('', path.select(xml).render()) - path = Path('item[@important!="very"]') - self.assertEqual('', - path.select(xml).render()) + self._test_expression('item[@important="very"]', None, xml, '') + self._test_expression('item[@important!="very"]', None, xml, + '') def test_predicate_attr_greater_than(self): xml = XML('') - path = Path('item[@priority>3]') - self.assertEqual('', path.select(xml).render()) - path = Path('item[@priority>2]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('item[@priority>3]', None, xml, '') + self._test_expression('item[@priority>2]', None, xml, + '') def test_predicate_attr_less_than(self): xml = XML('') - path = Path('item[@priority<3]') - self.assertEqual('', path.select(xml).render()) - path = Path('item[@priority<4]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('item[@priority<3]', None, xml, '') + self._test_expression('item[@priority<4]', None, xml, + '') def test_predicate_attr_and(self): xml = XML('') - path = Path('item[@important and @important="very"]') - self.assertEqual('', path.select(xml).render()) - path = Path('item[@important and @important="notso"]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('item[@important and @important="very"]', + None, xml, '') + self._test_expression('item[@important and @important="notso"]', + None, xml, '') def test_predicate_attr_or(self): xml = XML('') - path = Path('item[@urgent or @important]') - self.assertEqual('', path.select(xml).render()) - path = Path('item[@urgent or @notso]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('item[@urgent or @important]', None, xml, + '') + self._test_expression('item[@urgent or @notso]', None, xml, '') def test_predicate_boolean_function(self): xml = XML('bar') - path = Path('*[boolean("")]') - self.assertEqual('', path.select(xml).render()) - path = Path('*[boolean("yo")]') - self.assertEqual('bar', path.select(xml).render()) - path = Path('*[boolean(0)]') - self.assertEqual('', path.select(xml).render()) - path = Path('*[boolean(42)]') - self.assertEqual('bar', path.select(xml).render()) - path = Path('*[boolean(false())]') - self.assertEqual('', path.select(xml).render()) - path = Path('*[boolean(true())]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[boolean("")]', None, xml, '') + self._test_expression('*[boolean("yo")]', None, xml, 'bar') + self._test_expression('*[boolean(0)]', None, xml, '') + self._test_expression('*[boolean(42)]', None, xml, 'bar') + self._test_expression('*[boolean(false())]', None, xml, '') + self._test_expression('*[boolean(true())]', None, xml, + 'bar') def test_predicate_ceil_function(self): xml = XML('bar') - path = Path('*[ceiling("4.5")=5]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[ceiling("4.5")=5]', None, xml, + 'bar') def test_predicate_concat_function(self): xml = XML('bar') - path = Path('*[name()=concat("f", "oo")]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[name()=concat("f", "oo")]', None, xml, + 'bar') def test_predicate_contains_function(self): xml = XML('bar') - path = Path('*[contains(name(), "oo")]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[contains(name(), "oo")]', None, xml, + 'bar') + + def test_predicate_matches_function(self): + xml = XML('barfoo') + self._test_expression('*[matches(name(), "foo|bar")]', None, xml, + 'barfoo') def test_predicate_false_function(self): xml = XML('bar') - path = Path('*[false()]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[false()]', None, xml, '') def test_predicate_floor_function(self): xml = XML('bar') - path = Path('*[floor("4.5")=4]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[floor("4.5")=4]', None, xml, + 'bar') def test_predicate_normalize_space_function(self): xml = XML('bar') - path = Path('*[normalize-space(" foo bar ")="foo bar"]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[normalize-space(" foo bar ")="foo bar"]', + None, xml, 'bar') def test_predicate_number_function(self): xml = XML('bar') - path = Path('*[number("3.0")=3]') - self.assertEqual('bar', path.select(xml).render()) - path = Path('*[number("3.0")=3.0]') - self.assertEqual('bar', path.select(xml).render()) - path = Path('*[number("0.1")=.1]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[number("3.0")=3]', None, xml, + 'bar') + self._test_expression('*[number("3.0")=3.0]', None, xml, + 'bar') + self._test_expression('*[number("0.1")=.1]', None, xml, + 'bar') def test_predicate_round_function(self): xml = XML('bar') - path = Path('*[round("4.4")=4]') - self.assertEqual('bar', path.select(xml).render()) - path = Path('*[round("4.6")=5]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[round("4.4")=4]', None, xml, + 'bar') + self._test_expression('*[round("4.6")=5]', None, xml, + 'bar') def test_predicate_starts_with_function(self): xml = XML('bar') - path = Path('*[starts-with(name(), "f")]') - self.assertEqual('bar', path.select(xml).render()) - path = Path('*[starts-with(name(), "b")]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[starts-with(name(), "f")]', None, xml, + 'bar') + self._test_expression('*[starts-with(name(), "b")]', None, xml, '') def test_predicate_string_length_function(self): xml = XML('bar') - path = Path('*[string-length(name())=3]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[string-length(name())=3]', None, xml, + 'bar') def test_predicate_substring_function(self): xml = XML('bar') - path = Path('*[substring(name(), 1)="oo"]') - self.assertEqual('bar', path.select(xml).render()) - path = Path('*[substring(name(), 1, 1)="o"]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[substring(name(), 1)="oo"]', None, xml, + 'bar') + self._test_expression('*[substring(name(), 1, 1)="o"]', None, xml, + 'bar') def test_predicate_substring_after_function(self): xml = XML('bar') - path = Path('*[substring-after(name(), "f")="oo"]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[substring-after(name(), "f")="oo"]', None, xml, + 'bar') def test_predicate_substring_before_function(self): xml = XML('bar') - path = Path('*[substring-before(name(), "oo")="f"]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[substring-before(name(), "oo")="f"]', + None, xml, 'bar') def test_predicate_translate_function(self): xml = XML('bar') - path = Path('*[translate(name(), "fo", "ba")="baa"]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[translate(name(), "fo", "ba")="baa"]', + None, xml, 'bar') def test_predicate_true_function(self): xml = XML('bar') - path = Path('*[true()]') - self.assertEqual('bar', path.select(xml).render()) + self._test_expression('*[true()]', None, xml, 'bar') def test_predicate_variable(self): xml = XML('bar') - path = Path('*[name()=$bar]') variables = {'bar': 'foo'} - self.assertEqual('bar', - path.select(xml, variables=variables).render()) + self._test_expression('*[name()=$bar]', None, xml, 'bar', + variables = variables) def test_predicate_position(self): xml = XML('') - path = Path('*[2]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[2]', None, xml, '') def test_predicate_attr_and_position(self): xml = XML('') - path = Path('*[@id][2]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[@id][2]', None, xml, '') def test_predicate_position_and_attr(self): xml = XML('') - path = Path('*[1][@id]') - self.assertEqual('', path.select(xml).render()) - path = Path('*[2][@id]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[1][@id]', None, xml, '') + self._test_expression('*[2][@id]', None, xml, '') + + def test_predicate_advanced_position(self): + xml = XML('') + self._test_expression( 'descendant-or-self::*/' + 'descendant-or-self::*/' + 'descendant-or-self::*[2]/' + 'self::*/descendant::*[3]', None, xml, + '') + + def test_predicate_child_position(self): + xml = XML('\ +12345') + self._test_expression('//a/b[2]', None, xml, '25') + self._test_expression('//a/b[3]', None, xml, '3') def test_name_with_namespace(self): xml = XML('bar') - path = Path('f:foo') - self.assertEqual('', repr(path)) - namespaces = {'f': 'FOO'} - self.assertEqual('bar', - path.select(xml, namespaces=namespaces).render()) + self._test_expression('f:foo', '', xml, + 'bar', + namespaces = {'f': 'FOO'}) def test_wildcard_with_namespace(self): xml = XML('bar') - path = Path('f:*') - self.assertEqual('', repr(path)) - namespaces = {'f': 'FOO'} - self.assertEqual('bar', - path.select(xml, namespaces=namespaces).render()) + self._test_expression('f:*', '', xml, + 'bar', + namespaces = {'f': 'FOO'}) def test_predicate_termination(self): """ @@ -444,25 +517,70 @@ cause an infinite loop. See . """ xml = XML('
      • a
      • b
      ') - path = Path('.[@flag="1"]/*') - self.assertEqual('
    • a
    • b
    • ', path.select(xml).render()) + self._test_expression('.[@flag="1"]/*', None, xml, + '
    • a
    • b
    • ') xml = XML('
      • a
      • b
      ') - path = Path('.[@flag="0"]/*') - self.assertEqual('', path.select(xml).render()) + self._test_expression('.[@flag="0"]/*', None, xml, '') def test_attrname_with_namespace(self): xml = XML('') - path = Path('foo[@f:bar]') - self.assertEqual('', - path.select(xml, namespaces={'f': 'FOO'}).render()) + self._test_expression('foo[@f:bar]', None, xml, + '', + namespaces={'f': 'FOO'}) def test_attrwildcard_with_namespace(self): xml = XML('') - path = Path('foo[@f:*]') - self.assertEqual('', - path.select(xml, namespaces={'f': 'FOO'}).render()) + self._test_expression('foo[@f:*]', None, xml, + '', + namespaces={'f': 'FOO'}) + def test_self_and_descendant(self): + xml = XML('') + self._test_expression('self::root', None, xml, '') + self._test_expression('self::foo', None, xml, '') + self._test_expression('descendant::root', None, xml, '') + self._test_expression('descendant::foo', None, xml, '') + self._test_expression('descendant-or-self::root', None, xml, + '') + self._test_expression('descendant-or-self::foo', None, xml, '') + def test_long_simple_paths(self): + xml = XML('!' + '') + self._test_expression('//a/b/a/b/a/c', None, xml, '!') + self._test_expression('//a/b/a/c', None, xml, '!') + self._test_expression('//a/c', None, xml, '!') + self._test_expression('//c', None, xml, '!') + # Please note that a//b is NOT the same as a/descendant::b + # it is a/descendant-or-self::node()/b, which SimplePathStrategy + # does NOT support + self._test_expression('a/b/descendant::a/c', None, xml, '!') + self._test_expression('a/b/descendant::a/d/descendant::a/c', + None, xml, '!') + self._test_expression('a/b/descendant::a/d/a/c', None, xml, '') + self._test_expression('//d/descendant::b/descendant::b/descendant::b' + '/descendant::c', None, xml, '!') + self._test_expression('//d/descendant::b/descendant::b/descendant::b' + '/descendant::b/descendant::c', None, xml, '') + def _test_support(self, strategy_class, text): + path = PathParser(text, None, -1).parse()[0] + return strategy_class.supports(path) + def test_simple_strategy_support(self): + self.assert_(self._test_support(SimplePathStrategy, 'a/b')) + self.assert_(self._test_support(SimplePathStrategy, 'self::a/b')) + self.assert_(self._test_support(SimplePathStrategy, 'descendant::a/b')) + self.assert_(self._test_support(SimplePathStrategy, + 'descendant-or-self::a/b')) + self.assert_(self._test_support(SimplePathStrategy, '//a/b')) + self.assert_(self._test_support(SimplePathStrategy, 'a/@b')) + self.assert_(self._test_support(SimplePathStrategy, 'a/text()')) + + # a//b is a/descendant-or-self::node()/b + self.assert_(not self._test_support(SimplePathStrategy, 'a//b')) + self.assert_(not self._test_support(SimplePathStrategy, 'node()/@a')) + self.assert_(not self._test_support(SimplePathStrategy, '@a')) + self.assert_(not self._test_support(SimplePathStrategy, 'foo:bar')) + self.assert_(not self._test_support(SimplePathStrategy, 'a/@foo:bar')) def suite(): suite = unittest.TestSuite() diff --git a/genshi/util.py b/genshi/util.py --- a/genshi/util.py +++ b/genshi/util.py @@ -152,7 +152,7 @@ """ retval = [] for item in items: - if isinstance(item, (list, tuple)): + if isinstance(item, (frozenset, list, set, tuple)): retval += flatten(item) else: retval.append(item) @@ -198,7 +198,7 @@ If the `keepxmlentities` parameter is provided and is a truth value, the core XML entities (&, ', >, < and ") are left intact. - + >>> stripentities('1 < 2 …', keepxmlentities=True) u'1 < 2 \u2026' """ @@ -223,7 +223,7 @@ return ref return _STRIPENTITIES_RE.sub(_replace_entity, text) -_STRIPTAGS_RE = re.compile(r'<[^>]*?>') +_STRIPTAGS_RE = re.compile(r'(|<[^>]*>)') def striptags(text): """Return a copy of the text with any XML/HTML tags removed. @@ -234,6 +234,11 @@ >>> striptags('Foo
      ') 'Foo' + HTML/XML comments are stripped, too: + + >>> striptags('test') + 'test' + :param text: the string to remove tags from :return: the text with tags removed """ diff --git a/scripts/ast_generator.py b/scripts/ast_generator.py new file mode 100755 --- /dev/null +++ b/scripts/ast_generator.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python2.5 + +"""Script to that automatically generates genshi/templates/_astpy24.py. +Be sure to run this with a Python 2.5 interpreter. +""" + +import _ast + +done = set() + +IGNORE_ATTRS = ('__module__', '__dict__', '__weakref__', '__setattr__', + '__new__', '__getattribute__', '__reduce__', '__delattr__', + '__init__') + +def print_class(cls): + bnames = [] + for base in cls.__bases__: + if base.__module__ == '_ast': + if base not in done: + print_class(base) + bnames.append(base.__name__) + elif base.__module__ == '__builtin__': + bnames.append("%s"%base.__name__) + else: + bnames.append("%s.%s"%(base.__module__,base.__name__)) + print "class %s(%s):"%(cls.__name__, ", ".join(bnames)) + written = False + for attr in cls.__dict__: + if attr not in IGNORE_ATTRS: + written = True + print "\t%s = %s"%(attr, repr(cls.__dict__[attr]),) + if not written: + print "\tpass" + done.add(cls) + +print "# Generated automatically, please do not edit" +print "# Generator can be found in Genshi SVN, scripts/ast-generator.py" +print +print "__version__ = %s" % _ast.__version__ +print + +for name in dir(_ast): + cls = getattr(_ast, name) + if cls.__class__ is type: + print_class(cls) + print diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (C) 2006 Edgewall Software +# Copyright (C) 2006-2008 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -13,76 +13,82 @@ # history and logs, available at http://genshi.edgewall.org/log/. from distutils.cmd import Command +from distutils.command.build_ext import build_ext +from distutils.errors import CCompilerError, DistutilsPlatformError import doctest from glob import glob import os try: - from setuptools import setup + from setuptools import setup, Extension, Feature + from setuptools.command.bdist_egg import bdist_egg except ImportError: - from distutils.core import setup + from distutils.core import setup, Extension + Feature = None + bdist_egg = None import sys +sys.path.append(os.path.join('doc', 'common')) +try: + from doctools import build_doc, test_doc +except ImportError: + build_doc = test_doc = None + +_speedup_available = False + +class optional_build_ext(build_ext): + # This class allows C extension building to fail. + def run(self): + try: + build_ext.run(self) + except DistutilsPlatformError, e: + self._unavailable(e) + + def build_extension(self, ext): + try: + build_ext.build_extension(self, ext) + global _speedup_available + _speedup_available = True + except CCompilerError, e: + self._unavailable(e) + + def _unavailable(self, exc): + print '*' * 70 + print """WARNING: +An optional C extension could not be compiled, speedups will not be +available.""" + print '*' * 70 + print exc + -class build_doc(Command): - description = 'Builds the documentation' - user_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - from docutils.core import publish_cmdline - docutils_conf = os.path.join('doc', 'docutils.conf') - epydoc_conf = os.path.join('doc', 'epydoc.conf') - - for source in glob('doc/*.txt'): - dest = os.path.splitext(source)[0] + '.html' - if not os.path.exists(dest) or \ - os.path.getmtime(dest) < os.path.getmtime(source): - print 'building documentation file %s' % dest - publish_cmdline(writer_name='html', - argv=['--config=%s' % docutils_conf, source, - dest]) - - try: - from epydoc import cli - old_argv = sys.argv[1:] - sys.argv[1:] = [ - '--config=%s' % epydoc_conf, - '--no-private', # epydoc bug, not read from config - '--simple-term', - '--verbose' - ] - cli.cli() - sys.argv[1:] = old_argv - - except ImportError: - print 'epydoc not installed, skipping API documentation.' +if Feature: + speedups = Feature( + "optionial C speed-enhancements", + standard = True, + ext_modules = [ + Extension('genshi._speedups', ['genshi/_speedups.c']), + ], + ) +else: + speedups = None -class test_doc(Command): - description = 'Tests the code examples in the documentation' - user_options = [] - - def initialize_options(self): - pass +# Setuptools need some help figuring out if the egg is "zip_safe" or not +if bdist_egg: + class my_bdist_egg(bdist_egg): + def zip_safe(self): + return not _speedup_available and bdist_egg.zip_safe(self) - def finalize_options(self): - pass - def run(self): - for filename in glob('doc/*.txt'): - print 'testing documentation file %s' % filename - doctest.testfile(filename, False, optionflags=doctest.ELLIPSIS) +cmdclass = {'build_doc': build_doc, 'test_doc': test_doc, + 'build_ext': optional_build_ext} +if bdist_egg: + cmdclass['bdist_egg'] = my_bdist_egg setup( name = 'Genshi', - version = '0.5', - description = 'A toolkit for stream-based generation of output for the web', + version = '0.6', + description = 'A toolkit for generation of output for the web', long_description = \ """Genshi is a Python library that provides an integrated set of components for parsing, generating, and processing HTML, XML or @@ -93,7 +99,6 @@ license = 'BSD', url = 'http://genshi.edgewall.org/', download_url = 'http://genshi.edgewall.org/wiki/Download', - zip_safe = True, classifiers = [ 'Development Status :: 4 - Beta', @@ -111,13 +116,20 @@ packages = ['genshi', 'genshi.filters', 'genshi.template'], test_suite = 'genshi.tests.suite', - extras_require = {'plugin': ['setuptools>=0.6a2']}, + extras_require = { + 'i18n': ['Babel>=0.8'], + 'plugin': ['setuptools>=0.6a2'] + }, entry_points = """ + [babel.extractors] + genshi = genshi.filters.i18n:extract[i18n] + [python.templating.engines] genshi = genshi.template.plugin:MarkupTemplateEnginePlugin[plugin] genshi-markup = genshi.template.plugin:MarkupTemplateEnginePlugin[plugin] genshi-text = genshi.template.plugin:TextTemplateEnginePlugin[plugin] """, - cmdclass = {'build_doc': build_doc, 'test_doc': test_doc} + features = {'speedups': speedups}, + cmdclass = cmdclass )