Edgewall Software

source: trunk/genshi/builder.py

Last change on this file was 1191, checked in by hodgestar, 11 years ago

Fix a number of tests which Python's new hash randomization is causing to fail randomly.

  • Property svn:eol-style set to native
File size: 11.4 KB
RevLine 
[2]1# -*- coding: utf-8 -*-
2#
[1077]3# Copyright (C) 2006-2009 Edgewall Software
[2]4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
[287]8# are also available at http://genshi.edgewall.org/wiki/License.
[2]9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
[287]12# history and logs, available at http://genshi.edgewall.org/log/.
[2]13
[522]14"""Support for programmatically generating markup streams from Python code using
15a very simple syntax. The main entry point to this module is the `tag` object
16(which is actually an instance of the ``ElementFactory`` class). You should
17rarely (if ever) need to directly import and use any of the other classes in
18this module.
[517]19
[522]20Elements can be created using the `tag` object using attribute access. For
21example:
22
23>>> doc = tag.p('Some text and ', tag.a('a link', href='http://example.org/'), '.')
24>>> doc
25<Element "p">
26
27This produces an `Element` instance which can be further modified to add child
28nodes and attributes. This is done by "calling" the element: positional
29arguments are added as child nodes (alternatively, the `Element.append` method
30can be used for that purpose), whereas keywords arguments are added as
31attributes:
32
33>>> doc(tag.br)
34<Element "p">
[1076]35>>> print(doc)
[522]36<p>Some text and <a href="http://example.org/">a link</a>.<br/></p>
37
38If an attribute name collides with a Python keyword, simply append an underscore
39to the name:
40
41>>> doc(class_='intro')
42<Element "p">
[1076]43>>> print(doc)
[522]44<p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
45
46As shown above, an `Element` can easily be directly rendered to XML text by
47printing it or using the Python ``str()`` function. This is basically a
48shortcut for converting the `Element` to a stream and serializing that
49stream:
50
51>>> stream = doc.generate()
[523]52>>> stream #doctest: +ELLIPSIS
[522]53<genshi.core.Stream object at ...>
[1076]54>>> print(stream)
[522]55<p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
56
57
58The `tag` object also allows creating "fragments", which are basically lists
59of nodes (elements or text) that don't have a parent element. This can be useful
60for creating snippets of markup that are attached to a parent element later (for
61example in a template). Fragments are created by calling the `tag` object, which
62returns an object of type `Fragment`:
63
64>>> fragment = tag('Hello, ', tag.em('world'), '!')
65>>> fragment
66<Fragment>
[1076]67>>> print(fragment)
[522]68Hello, <em>world</em>!
69"""
70
[861]71from genshi.core import Attrs, Markup, Namespace, QName, Stream, \
72                        START, END, TEXT
[2]73
[525]74__all__ = ['Fragment', 'Element', 'ElementFactory', 'tag']
[517]75__docformat__ = 'restructuredtext en'
[2]76
77
78class Fragment(object):
[29]79    """Represents a markup fragment, which is basically just a list of element
80    or text nodes.
81    """
[2]82    __slots__ = ['children']
83
84    def __init__(self):
[525]85        """Create a new fragment."""
[2]86        self.children = []
87
[70]88    def __add__(self, other):
89        return Fragment()(self, other)
90
91    def __call__(self, *args):
[525]92        """Append any positional arguments as child nodes.
93       
94        :see: `append`
95        """
[1077]96        for arg in args:
97            self.append(arg)
[70]98        return self
99
100    def __iter__(self):
[103]101        return self._generate()
[70]102
103    def __repr__(self):
[1083]104        return '<%s>' % type(self).__name__
[70]105
106    def __str__(self):
107        return str(self.generate())
108
109    def __unicode__(self):
110        return unicode(self.generate())
111
[861]112    def __html__(self):
113        return Markup(self.generate())
114
[2]115    def append(self, node):
[525]116        """Append an element or string as child node.
117       
118        :param node: the node to append; can be an `Element`, `Fragment`, or a
119                     `Stream`, or a Python string or number
120        """
[461]121        if isinstance(node, (Stream, Element, basestring, int, float, long)):
[2]122            # For objects of a known/primitive type, we avoid the check for
123            # whether it is iterable for better performance
124            self.children.append(node)
125        elif isinstance(node, Fragment):
[170]126            self.children.extend(node.children)
[2]127        elif node is not None:
128            try:
[1077]129                for child in node:
130                    self.append(child)
[2]131            except TypeError:
132                self.children.append(node)
133
[99]134    def _generate(self):
135        for child in self.children:
136            if isinstance(child, Fragment):
137                for event in child._generate():
138                    yield event
[461]139            elif isinstance(child, Stream):
140                for event in child:
141                    yield event
[99]142            else:
143                if not isinstance(child, basestring):
144                    child = unicode(child)
[170]145                yield TEXT, child, (None, -1, -1)
[99]146
[2]147    def generate(self):
[600]148        """Return a markup event stream for the fragment.
149       
150        :rtype: `Stream`
151        """
[99]152        return Stream(self._generate())
[2]153
154
[424]155def _kwargs_to_attrs(kwargs):
[859]156    attrs = []
157    names = set()
[854]158    for name, value in kwargs.items():
159        name = name.rstrip('_').replace('_', '-')
[859]160        if value is not None and name not in names:
161            attrs.append((QName(name), unicode(value)))
162            names.add(name)
163    return Attrs(attrs)
[424]164
165
[2]166class Element(Fragment):
167    """Simple XML output generator based on the builder pattern.
168
169    Construct XML elements by passing the tag name to the constructor:
170
[1076]171    >>> print(Element('strong'))
[2]172    <strong/>
173
174    Attributes can be specified using keyword arguments. The values of the
175    arguments will be converted to strings and any special XML characters
176    escaped:
177
[1191]178    >>> print(Element('textarea', rows=10))
179    <textarea rows="10"/>
[1076]180    >>> print(Element('span', title='1 < 2'))
[2]181    <span title="1 &lt; 2"/>
[1076]182    >>> print(Element('span', title='"baz"'))
[2]183    <span title="&#34;baz&#34;"/>
184
185    The " character is escaped using a numerical entity.
186    The order in which attributes are rendered is undefined.
187
188    If an attribute value evaluates to `None`, that attribute is not included
189    in the output:
190
[1076]191    >>> print(Element('a', name=None))
[2]192    <a/>
193
194    Attribute names that conflict with Python keywords can be specified by
195    appending an underscore:
196
[1076]197    >>> print(Element('div', class_='warning'))
[2]198    <div class="warning"/>
199
200    Nested elements can be added to an element using item access notation.
201    The call notation can also be used for this and for adding attributes
202    using keyword arguments, as one would do in the constructor.
203
[1076]204    >>> print(Element('ul')(Element('li'), Element('li')))
[2]205    <ul><li/><li/></ul>
[1076]206    >>> print(Element('a')('Label'))
[2]207    <a>Label</a>
[1076]208    >>> print(Element('a')('Label', href="target"))
[2]209    <a href="target">Label</a>
210
211    Text nodes can be nested in an element by adding strings instead of
212    elements. Any special characters in the strings are escaped automatically:
213
[1076]214    >>> print(Element('em')('Hello world'))
[2]215    <em>Hello world</em>
[1076]216    >>> print(Element('em')(42))
[2]217    <em>42</em>
[1076]218    >>> print(Element('em')('1 < 2'))
[2]219    <em>1 &lt; 2</em>
220
221    This technique also allows mixed content:
222
[1076]223    >>> print(Element('p')('Hello ', Element('b')('world')))
[2]224    <p>Hello <b>world</b></p>
225
[35]226    Quotes are not escaped inside text nodes:
[1076]227    >>> print(Element('p')('"Hello"'))
[35]228    <p>"Hello"</p>
229
[2]230    Elements can also be combined with other elements or strings using the
231    addition operator, which results in a `Fragment` object that contains the
232    operands:
233   
[1076]234    >>> print(Element('br') + 'some text' + Element('br'))
[2]235    <br/>some text<br/>
236   
237    Elements with a namespace can be generated using the `Namespace` and/or
238    `QName` classes:
239   
[287]240    >>> from genshi.core import Namespace
[2]241    >>> xhtml = Namespace('http://www.w3.org/1999/xhtml')
[1076]242    >>> print(Element(xhtml.html, lang='en'))
[502]243    <html xmlns="http://www.w3.org/1999/xhtml" lang="en"/>
[2]244    """
245    __slots__ = ['tag', 'attrib']
246
247    def __init__(self, tag_, **attrib):
248        Fragment.__init__(self)
249        self.tag = QName(tag_)
[854]250        self.attrib = _kwargs_to_attrs(attrib)
[2]251
252    def __call__(self, *args, **kwargs):
[525]253        """Append any positional arguments as child nodes, and keyword arguments
254        as attributes.
255       
[600]256        :return: the element itself so that calls can be chained
257        :rtype: `Element`
[525]258        :see: `Fragment.append`
259        """
[854]260        self.attrib |= _kwargs_to_attrs(kwargs)
[103]261        Fragment.__call__(self, *args)
262        return self
[2]263
[70]264    def __repr__(self):
[1083]265        return '<%s "%s">' % (type(self).__name__, self.tag)
[70]266
[99]267    def _generate(self):
[170]268        yield START, (self.tag, self.attrib), (None, -1, -1)
[99]269        for kind, data, pos in Fragment._generate(self):
270            yield kind, data, pos
[170]271        yield END, self.tag, (None, -1, -1)
[99]272
[2]273    def generate(self):
[600]274        """Return a markup event stream for the fragment.
275       
276        :rtype: `Stream`
277        """
[99]278        return Stream(self._generate())
[2]279
280
281class ElementFactory(object):
[29]282    """Factory for `Element` objects.
283   
284    A new element is created simply by accessing a correspondingly named
285    attribute of the factory object:
286   
287    >>> factory = ElementFactory()
[1076]288    >>> print(factory.foo)
[29]289    <foo/>
[1076]290    >>> print(factory.foo(id=2))
[29]291    <foo id="2"/>
292   
[145]293    Markup fragments (lists of nodes without a parent element) can be created
294    by calling the factory:
295   
[1076]296    >>> print(factory('Hello, ', factory.em('world'), '!'))
[145]297    Hello, <em>world</em>!
298   
[29]299    A factory can also be bound to a specific namespace:
300   
301    >>> factory = ElementFactory('http://www.w3.org/1999/xhtml')
[1076]302    >>> print(factory.html(lang="en"))
[502]303    <html xmlns="http://www.w3.org/1999/xhtml" lang="en"/>
[29]304   
305    The namespace for a specific element can be altered on an existing factory
306    by specifying the new namespace using item access:
307   
308    >>> factory = ElementFactory()
[1076]309    >>> print(factory.html(factory['http://www.w3.org/2000/svg'].g(id=3)))
[502]310    <html><g xmlns="http://www.w3.org/2000/svg" id="3"/></html>
[29]311   
312    Usually, the `ElementFactory` class is not be used directly. Rather, the
313    `tag` instance should be used to create elements.
314    """
[2]315
[20]316    def __init__(self, namespace=None):
[29]317        """Create the factory, optionally bound to the given namespace.
318       
[517]319        :param namespace: the namespace URI for any created elements, or `None`
320                          for no namespace
[29]321        """
[21]322        if namespace and not isinstance(namespace, Namespace):
[20]323            namespace = Namespace(namespace)
324        self.namespace = namespace
[2]325
[145]326    def __call__(self, *args):
[525]327        """Create a fragment that has the given positional arguments as child
328        nodes.
329
330        :return: the created `Fragment`
[600]331        :rtype: `Fragment`
[525]332        """
[145]333        return Fragment()(*args)
334
[20]335    def __getitem__(self, namespace):
[525]336        """Return a new factory that is bound to the specified namespace.
337       
338        :param namespace: the namespace URI or `Namespace` object
339        :return: an `ElementFactory` that produces elements bound to the given
340                 namespace
[600]341        :rtype: `ElementFactory`
[525]342        """
[20]343        return ElementFactory(namespace)
[2]344
[20]345    def __getattr__(self, name):
[525]346        """Create an `Element` with the given name.
347       
348        :param name: the tag name of the element to create
349        :return: an `Element` with the specified name
[600]350        :rtype: `Element`
[525]351        """
[20]352        return Element(self.namespace and self.namespace[name] or name)
353
354
[2]355tag = ElementFactory()
[600]356"""Global `ElementFactory` bound to the default namespace.
357
358:type: `ElementFactory`
359"""
Note: See TracBrowser for help on using the repository browser.