#!/usr/bin/env python2.4

# Meld2
# by Paul Winkler, stuff@slinkp.com
# based on Richie Hindle's PyMeld and PyMeldLite, available from entrian.com.
# $Revision: 39 $
# $Author: pw $  $Date: 2005-11-28 00:50:55 -0500 (Mon, 28 Nov 2005) $
# License: Python License, see http://www.python.org/psf/license.html

from PyMeldLite import Meld, _fail, READ_ONLY_MESSAGE, ReadOnlyError

import sys
sys.setrecursionlimit(50)  # XXX makes blunders quicker to find.

class Meld2(Meld, object):

    """
    A Meld2 represents an XML document, or a fragment of one.

    Like PyMeld and PyMeldLite before it, the idea is that your
    templates are pure, clean x(h)tml. All the work is done in some
    accompanying python code that you write, in which you create a
    Meld (or Meld2) instance from that template, manipulate the Meld
    instance, and generate some text output.

    The idea of Meld2 is to enrich and improve the Meld API.

    Just like in PyMeld, we access and modify child nodes by using
    simple python attribute syntax, like myMeld.subnode.

    Unlike PyMeld, we use dictionary syntax when we want to
    access or modify XML attributes, so there's no chance of confusing
    child nodes and XML attributes.

    Also unlike PyMeld, we use a different identifier for finding
    nodes; the `id` attribute is too meaningful already, so we use
    `meld:id` instead.

    In addition, Meld2 provides the .repeat() method as a very
    convenient way to generate repetitive XML structures,
    since that's such a common use case.

    XXX TO DO:

    Make the node identifier configurable.

    Bundle this code with some real-world examples.

    """

    id_attr = 'meld:id'

    def __init__(self, *args, **kw):
        Meld.__init__(self, *args, **kw)

    def getElementNode(self):
        """Get the node that this Meld wraps.

        I got tired of calling self._tree.getElementNode().
        """
        return self._tree.getElementNode()

    def __cmp__(self, other):
        """Two Melds should be equal if they have the same text.
        This is really only intended to be useful for testing.

        >>> text = '<span><p meld:id="food">Cheese</p></span>'
        >>> p1 = Meld2(text)
        >>> p2 = Meld2(text)
        >>> p1 == p2
        True
        >>> p3 = Meld2('<html>hello</html>')
        >>> p3 == p1
        False
        >>> p3 != p1
        True

        It works for sub-nodes too:

        >>> p1.food == p2.food
        True
        >>> p1.food._content = 'Bananas'
        >>> print p1
        <span><p meld:id="food">Bananas</p></span>
        >>> p1 == p2
        False

        >>> p1 = Meld2('<p meld:id="food">Cheese</p>')
        >>> p2 = Meld2('<a meld:id="food">Cheese</a>')
        >>> p1 == p2
        False

        Only equality and inequality are really
        meaningful comparisons; magnitude comparisons
        should not be construed as meaningful.

        >>> Meld2("<x/>") != Meld2("<x/>")
        False
        >>> Meld2("<x/>") < Meld2("<x/>")
        False
        >>> Meld2("<x/>") > Meld2("<x/>")
        False

        """
        # The old PyMeld and PyMeldLite didn't implement this.
        # Only identity mattered. But equality can be very useful
        # for testing.
        if self is other:
            return 0
        if str(self) == str(other):
            return 0
        return 1


    def _findByID(self, node, name):
        """Returns the node with the given ID, or None.

        >>> m = Meld2('''<foo meld:id='bob'>Bob</foo>''')
        >>> print m._findByID(m._tree, 'bob') #doctest: +ELLIPSIS
        <PyMeldLite._ElementNode instance at 0x...>
        >>> print m._findByID(m._tree, 'Alice')
        None
        """
        if node.attributes.get(self.id_attr) == name:
            return node
        for child in node.children:
            result = self._findByID(child, name)
            if result:
                return result
        return None

    def getChild(self, name):
        """`object.getChild(<name>), if this Meld contains an element
        whose `id` is <name>, returns a Meld representing that element.

        If no such element exists, raises ValueError.

        >>> p = Meld2('<p style="one">Hello <b meld:id="who">World</b></p>')
        >>> print p.getChild('who')
        <b meld:id="who">World</b>
        >>> print p.getChild('style')
        Traceback (most recent call last):
          ...
        ValueError: No such child 'style'

        Nodes are only required to be unique in local scope.
        Outer nodes are returned before inner nodes of the same name.

        >>> nested = Meld2('''
        ...   <div meld:id="outer">
        ...     <span meld:id="orange" style="color: orange;">
        ...       <span meld:id="inner">
        ...         <p meld:id="orange">Fruity!</p>
        ...       </span>
        ...       <span meld:id="inner2">
        ...         <p meld:id="orange">Chewing gum</p>
        ...       </span>
        ...     </span>
        ...   </div>
        ... ''')
        >>> nested.getChild('outer').getChild('orange')['style']
        'color: orange;'
        >>> nested.getChild('orange')['style']
        'color: orange;'
        >>> nested.getChild('inner').getChild('orange')._content
        'Fruity!'
        >>> nested.getChild('inner2').getChild('orange')._content
        'Chewing gum'
        """
        node = self._findByID(self._tree, name)
        if node:
            return Meld2(node, self._readonly)
        else:
            raise ValueError, 'No such child %r' % name

    def __getattr__(self, name):
        """`object.<name>` is syntactic sugar for
        object.getChild(<name>).

        `object._content` returns the content of the Meld, not including
        the enclosing `<element></element>`, as a string.

        Children should not have names beginning with an underscore,
        as such attributes are assumed to be regular Python attributes
        rather than Meld nodes.

        >>> p = Meld2('<p style="one">Hello <b meld:id="who">World</b></p>')
        >>> print p.who
        <b meld:id="who">World</b>
        >>> p.who == p.getChild('who')
        True
        >>> p.bar
        Traceback (most recent call last):
          ...
        ValueError: No such child 'bar'

        >>> print p._content
        Hello <b meld:id="who">World</b>
        >>> print p.getChild('who')._content
        World
        """
        if name == '_content':
            return self.getElementNode().childrenToText()
        elif name.startswith('_'):
            return self.__dict__[name]
        return self.getChild(name)

    def __repr__(self):
        # XXX This should not be necessary, but __getattr__ isn't
        # handling it automatically... fix and delete this!
        return '<Meld2 instance at %d>' % id(self)

    def __setattr__(self, name, value):
        """
        `object.<name> = value` sets the XML content of the element with an
        `id` of `name`.

        >>> p = Meld2('<p style="one">Hello <b meld:id="who">World</b></p>')
        >>> p.who = "Richie"
        >>> print p
        <p style="one">Hello <b meld:id="who">Richie</b></p>
        >>> p.style = "two"
        Traceback (most recent call last):
          ...
        ValueError: No such child 'style'

        For changing attributes in an xml namespace, or with
        otherwise invalid python attribute names,
        you can resort to using setattr().

        >>> xml =  Meld2('<p>Hello <b meld:id="x:y">World</b></p>')
        >>> setattr(xml, 'x:y', 'Everybody')
        >>> print xml
        <p>Hello <b meld:id="x:y">Everybody</b></p>

        The _content attribute is special: it is not a specific
        sub-element, rather, setting it replaces all the XML content
        of the element.

        >>> xml._content = 'Totally changed'
        >>> print xml
        <p>Totally changed</p>

        Note that you can't have an element named 'class' as it's a
        reserved word in Python.

        >>> p.class = 'Special'
        Traceback (most recent call last):
         ...
        SyntaxError: invalid syntax
        """
        if name == '_content':
            node = self.getElementNode()
            self._replaceNodeContent(node, value)
            return
        elif name.startswith('_'):
            self.__dict__[name] = value
            return
        node = self.getChild(name)
        node._content = value

    def __delattr__(self, name):
        """Deletes the named element, or raises ValueError if it
        didn't exist.

        >>> p = Meld2('<p style="one">Hello <b meld:id="who">World</b></p>')
        >>> del p.who
        >>> print p
        <p style="one">Hello </p>
        >>> del p.style
        Traceback (most recent call last):
          ...
        ValueError: No subnode named 'style'

        You can also clear all content this way:

        >>> del p._content
        >>> print p
        <p style="one"/>

        Python special attributes still work as expected:

        >>> del p.__doc__  # it's a class attribute
        Traceback (most recent call last):
          ...
        AttributeError: __doc__
        >>> p.__doc__ = 'blah'
        >>> print p.__doc__
        blah
        >>> del p.__doc__

        """

        if self._readonly:
            raise ReadOnlyError, READ_ONLY_MESSAGE
        if name == '_content':
            self.getElementNode().children = []
            return
        elif name.startswith('_'):
            try:
                del self.__dict__[name]
                return
            except KeyError:
                raise AttributeError, name
        node = self._findByID(self._tree, name)
        if node:
            node.parent.children.remove(node)
            return
        else:
            raise ValueError, "No subnode named %r" % name

    def clone(self, readonly=False):
        return Meld2(self._tree.clone(), readonly)

    def __getitem__(self, name):
        """
        `object[<name>]` returns the value of the xml attribute with
        the given name, as a string.  If no such attribute exists, an
        AttributeError is raised.

        >>> p = Meld2('<p style="one">Hello <b meld:id="who">World</b></p>')
        >>> p['who']
        Traceback (innermost last):
            ...
        AttributeError: No xml attribute named 'who'
        >>> print p['style']
        one
        """

        try:
            return self.__dict__[name]
        except KeyError:
            pass
        attribute = self.getElementNode().attributes.get(name, _fail)
        if attribute is not _fail:
            return self._unquoteAttribute(attribute)
        raise AttributeError, "No xml attribute named %r" % name

    def __setitem__(self, name, value):
        """`object[<name>] = value` sets the value of the `name`
        xml attribute on the outermost element.  If the attribute is not
        already there, a new attribute is created.

        In output, attributes are sorted alphanumerically.

        >>> p = Meld2('<p style="one">Hello <b meld:id="who">World</b></p>')
        >>> p['who'] = "Richie"
        >>> print p
        <p style="one" who="Richie">Hello <b meld:id="who">World</b></p>
        >>> p['style'] = "two"
        >>> print p
        <p style="two" who="Richie">Hello <b meld:id="who">World</b></p>

        >>> p.who['meld:id'] = 'you'
        >>> print p
        <p style="two" who="Richie">Hello <b meld:id="you">World</b></p>

        """

        if self.__dict__.get('_readonly', None):
            raise ReadOnlyError, READ_ONLY_MESSAGE
        value = self._quoteAttribute(value)
        self.getElementNode().attributes[name] = value

    def __delitem__(self, name):
        """
        Deletes the named xml attribute from the Meld.

        >>> p = Meld2('<p style="one">Hello <b meld:id="who">World</b></p>')
        >>> del p['style']
        >>> print p
        <p>Hello <b meld:id="who">World</b></p>
        >>> del p['who']
        Traceback (most recent call last):
          ...
        AttributeError: No xml attribute 'who'

        You can remove an element from the meld tree by deleting its
        identifier.

        >>> del p.who['meld:id']
        >>> print p
        <p>Hello <b>World</b></p>
        >>> print p.who
        Traceback (most recent call last):
          ...
        ValueError: No such child 'who'

        """
        # First handle some special cases.


        if self._readonly:
            raise ReadOnlyError, READ_ONLY_MESSAGE
        # Delete the attribute from the element.
        node = self.getElementNode()
        attribute = node.attributes.get(name, _fail)
        if attribute is not _fail:
            del node.attributes[name]
        else:
            msg = "No xml attribute %r" % name
            raise AttributeError, msg

    def repeat(self, iterable):
        """
        More convenient interface than Meld's way of doing repeats.
        The problem with Meld is twofold:  1) you have to explicitly id
        the parent so you can assign new children to it;
        2) once you've added to the parent, the children are fixed
        since they are merged by value, not kept around by reference.

        Returns a generator which first removes the template element
        from the parent, then yields tuples of '(element.clone(),
        iterable.next())'. Each time a cloned element is yielded, it
        is inserted in the document right after the last element (the
        first element is inserted where the original element was). The
        result is similar to "tal:repeat", especially if you iterate it in a
        for-loop. Finally, the meld:id for each clone is munged to
        end with an index number so that the output is still a valid
        Meld2 template.

        Here's an example.

        >>> page = Meld2('''<table>
        ... <tr>
        ...   <th>Title</th>
        ...   <th>Description</th>
        ... </tr>
        ... <tr meld:id="doc_item">
        ...   <td meld:id="title">Document Title</td>
        ...   <td meld:id="description">Document Description</td>
        ... </tr>
        ... </table>''')

        And here's an example of calling it:

        >>> contents = [{'Title': 'Foo Title',
        ...              'Descr': 'Foo Description',
        ...             },
        ...             {'Title': 'Bar Title',
        ...              'Descr': 'Bar Description',
        ...             }]
        ...
        >>> for tr, info in page.doc_item.repeat(contents):
        ...     tr.title = info['Title']
        ...     tr.title['class'] = 'funky'
        ...     tr.description = info['Descr']
        ...     print tr
        ...
        <tr meld:id="doc_item/0">
          <td class="funky" meld:id="title">Foo Title</td>
          <td meld:id="description">Foo Description</td>
        </tr>
        <tr meld:id="doc_item/1">
          <td class="funky" meld:id="title">Bar Title</td>
          <td meld:id="description">Bar Description</td>
        </tr>
        >>>
        >>> print page
        <table>
        <tr>
          <th>Title</th>
          <th>Description</th>
        </tr>
        <tr meld:id="doc_item/0">
          <td class="funky" meld:id="title">Foo Title</td>
          <td meld:id="description">Foo Description</td>
        </tr>
        <tr meld:id="doc_item/1">
          <td class="funky" meld:id="title">Bar Title</td>
          <td meld:id="description">Bar Description</td>
        </tr>
        </table>
        """

        parent = self._tree.parent
        separator = None
        template = self
        if parent is None:
            raise RuntimeError, "XXX when is a node's parent None??"
        index = self._findIndexInChildrenOf(parent)
        if index is not None:
            del(parent.children[index])
        # Get any whitespace that was after it,
        # so we can follow each repetition with it.
        if len(parent.children) > index:
            separator = parent.children[index]
            maybe_whitespace = separator.toText()
            if maybe_whitespace.strip():
                # It's not whitespace, don't use it.
                separator = None
            else:
                del(parent.children[index])
        for suffix, value in enumerate(iterable):
            myclone = template.clone()
            myclone[self.id_attr] += '/%d' % suffix
            self._insertNodeContent(parent, myclone, index, clone=False)
            index += 1
            if separator is not None:
                self._insertNodeContent(parent, separator, index)
                index += 1
            yield (myclone, value)

    def _insertNodeContent(self, node, meld_or_node, index, clone=True):
        """Inserts the content of `meld_or_node` in the children of
        the given node.  If `clone` is true (the default), the meld is
        cloned first; if false, it is inserted as-is so you can modify
        it later.
        """
        try:
            child_node = meld_or_node._tree
        except AttributeError:
            child_node = meld_or_node
        child_node.parent = node
        child = child_node.getElementNode()
        if clone:
            child = child.clone()
        node.children.insert(index, child)

    def _findIndexInChildrenOf(self, node):
        """Return the index at which self lives in node's children,
        or None if it's not in there.
        """
        # XXX afaict there's no more efficient way to do this w/ meld's API.
        for i, child in enumerate(node.children):
            if child is self.getElementNode():
                return i
        return None

    def replaceNode(self, name, newmeld_or_text, keepId=False):
        """
        Replace the child node `name` with the new meld (or text).
        If keepId is true, the new meld gets the old id.

        >>> m1 = Meld2('''<a><b meld:id="foo" /></a>''')
        >>> m2 = Meld2('''<h1 meld:id="bar">the new guy</h1>''')
        >>> m1.replaceNode('foo', m2)
        >>> print m1
        <a><h1 meld:id="bar">the new guy</h1></a>
        >>> m1.replaceNode('bar', '<span> potatos </span>')
        >>> print m1
        <a><span> potatos </span></a>
        >>> m1.replaceNode('baz', m2)
        Traceback (most recent call last):
          ...
        ValueError: No such child 'baz'
        >>> m3 = Meld2('<p><a meld:id="baz" /></p>')
        >>> m3.replaceNode('baz', '<b/>', keepId=True)
        >>> print m3
        <p><b meld:id="baz"/></p>
        """
        if isinstance(newmeld_or_text, str):
            newmeld = Meld2(newmeld_or_text)
        else:
            newmeld = newmeld_or_text
        if keepId:
            newmeld[self.id_attr] =  name
        old = getattr(self, name)
        parent = old._tree.parent
        index = old._findIndexInChildrenOf(parent)
        if index is None:
            # XXX this happens if elNode is not old's immediate parent.
            raise ValueError, 'No such child %r' % name
        newnode = newmeld.getElementNode()
        newnode.parent = parent
        parent.children[index] = newnode

    def getId(self):
        """Get the value of this meld's id attribute, or None.
        """
        attrs = self.getElementNode().attributes
        return attrs.get(self.id_attr, None)


    def nodeKeys(self):
        """Get a sorted list of names of sub-nodes.

        >>> m = Meld2('''
        ... <a>
        ...  <b meld:id='apples'/>
        ...  <b meld:id='pears'>
        ...    <c meld:id='bartlett'>A child of a child</c>
        ...  </b>
        ...  <b>This doesn't show up, no id.</b>
        ...  <b meld:id='bananas' />
        ... </a>''')
        >>> sorted(m.nodeKeys())
        ['apples', 'bananas', 'pears']
        >>> m.pears.nodeKeys()
        ['bartlett']
        >>> m.apples.nodeKeys()
        []
        """
        names = [item[0] for item in self.nodeItems()]
        return names

    def nodeItems(self):
        """Get a list of sub-nodes as (name, meld) pairs,
        sorted by  name.

        >>> m = Meld2('''
        ... <a>
        ...  <b meld:id='apples'/>
        ...  <b meld:id='pears'>
        ...    <c meld:id='bartlett'>A child of a child</c>
        ...  </b>
        ...  <b>This doesn't show up, no id.</b>
        ...  <b meld:id='bananas' />
        ... </a>''')
        >>> items = m.nodeItems()
        >>> for item in items:
        ...     print item[0] == item[1].getId()
        True
        True
        True
        >>> for item in items: print item[0]
        apples
        bananas
        pears
        >>> m.pears.nodeItems()  # doctest: +ELLIPSIS
        [('bartlett', <...Meld2 instance at...>)]
        >>> m.apples.nodeItems()
        []
        """
        result = [(c.getId(), c) for c in self.getAllChildren()
                  if c.getId() is not None]
        result.sort()
        return result

    def nodeValues(self):
        """Get a list of sub-nodes as meld objects, sorted
        by their corresponding node name.

        >>> m = Meld2('''
        ... <a>
        ...  <b meld:id='apples'/>
        ...  <b meld:id='pears'>
        ...    <c meld:id='bartlett'>A child of a child</c>
        ...  </b>
        ...  <b>This doesn't show up, no id.</b>
        ...  <b meld:id='bananas' />
        ... </a>''')
        >>> for v in m.nodeValues(): print v.getId()
        apples
        bananas
        pears
        >>> m.pears.nodeValues() # doctest: +ELLIPSIS
        [<...Meld2 instance at...>]
        >>> m.apples.nodeValues()
        []
        """
        values = [item[1] for item in self.nodeItems()]
        return values

    def getAllChildren(self):
        """Return an ordered list of all immediate children
        as Melds, whether they have ids or not.

        XXX add tests
        """
        n = self.getElementNode()
        return [Meld2(c) for c in n.children]

    # Introspection methods for attributes.

    def attrItems(self):
        """
        Return a list of (name, value) tuples for all
        XML attributes of this node, sorted by name.

        >>> xml = Meld2('<p/>')
        >>> xml.attrItems()
        []
        >>> xml = Meld2('<p meld:id="a" style="color: red" class="bold" />')
        >>> xml.a.attrItems()
        [('class', 'bold'), ('meld:id', 'a'), ('style', 'color: red')]
        """
        attrs = self.getElementNode().attributes
        result = attrs.items()
        result.sort()
        return result

    def attrKeys(self):
        """
        Return a sorted list of names of all XML attributes of this node.

        >>> xml = Meld2('<p/>')
        >>> xml.attrKeys()
        []
        >>> xml = Meld2('<p meld:id="a" style="color: red" class="bold" />')
        >>> xml.a.attrKeys()
        ['class', 'meld:id', 'style']
        """
        return [item[0] for item in self.attrItems()]

    def attrValues(self):
        """
        Return a list of values for all XML attributes of this node,
        sorted by their corresponding key.

        >>> xml = Meld2('<p/>')
        >>> xml.attrValues()
        []
        >>> xml = Meld2('<p meld:id="a" style="color: red" class="bold" />')
        >>> xml.a.attrValues()
        ['bold', 'a', 'color: red']
        """
        return [item[1] for item in self.attrItems()]

def _test():
    """Tests this module.
    """
    import doctest
    doctest.testmod()

if __name__ == '__main__':
    _test()
    print "DONE"
