From 05f7f938168f9b3195c9b101f4b98f298ab2e981 Mon Sep 17 00:00:00 2001 From: Santhosh Thottingal Date: Sun, 19 Jul 2009 18:22:04 +0530 Subject: Templating Library --- silpa/common/PyMeld.py | 1130 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1130 insertions(+) create mode 100644 silpa/common/PyMeld.py diff --git a/silpa/common/PyMeld.py b/silpa/common/PyMeld.py new file mode 100644 index 0000000..d5acf21 --- /dev/null +++ b/silpa/common/PyMeld.py @@ -0,0 +1,1130 @@ +#!/usr/bin/env python + +# +# PyMeld is released under the terms of the MIT License: +# +# Copyright (c) 2002-2009 Entrian Solutions Ltd. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +r"""A simple, lightweight system for manipulating HTML (and XML, informally) +using a Pythonic object model. `PyMeld` is a single Python module, +_PyMeld.py_. This beta version requires Python 2.2 or above, but the +final production version may work with previous releases of Python (with +slightly limited features) if there's sufficient demand. + +*Features:* + + o Allows program logic and HTML to be completely separated - a graphical + designer can design the HTML in a visual HTML editor, without needing to + deal with any non-standard syntax or non-standard attribute names. The + program code knows nothing about XML or HTML - it just deals with objects + and attributes like any other piece of Python code. + + o Designed with common HTML-application programming tasks in mind. + Populating an HTML form with a record from a database is a one-liner + (using the `%` operator - see below). Building an HTML table from a set + of records is just as easy, as shown in the example below. + + o No special requirements for the HTML/XML (or just one: attribute values + must be quoted) - so you can use any editor, and your HTML/XML doesn't + need to be strictly valid. + + o Works by string substitution, rather than by decomposing and rebuilding the + markup, hence has no impact on the parts of the page you don't manipulate. + + o Does nothing but manipulating HTML/XML, hence fits in with any other Web + toolkits you're using. + + o Tracebacks always point to the right place - many Python/HTML mixing + systems use exec or eval, making bugs hard to track down. + + +*Quick overview* + +A `PyMeld.Meld` object represents an XML document, or a piece of one. +All the elements in a document with `id=name` attributes are made available +by a Meld object as `object.name`. The attributes of elements are available +in the same way. A brief example is worth a thousand words: + +>>> from PyMeld import Meld +>>> xhtml = ''' +... +... ''' +>>> page = Meld(xhtml) # Create a Meld object from XHTML. +>>> print page.message # Access an element within the document. + +>>> print page.message.rows # Access an attribute of an element. +2 +>>> page.message = "New message." # Change the content of an element. +>>> page.message.rows = 4 # Change an attribute value. +>>> del page.message.wrap # Delete an attribute. +>>> print page # Print the resulting page. + + + + +So the program logic and the HTML are completely separated - a graphical +designer can design the HTML in a visual XHTML editor, without needing to +deal with any non-standard syntax or non-standard attribute names. The +program code knows nothing about XML or HTML - it just deals with objects and +attributes like any other piece of Python code. Populating an HTML form with +a record from a database is a one-liner (using the `%` operator - see below). +Building an HTML table from a set of records is just as easy, as shown in the +example below: + + +*Real-world example:* + +Here's a data-driven example populating a table from a data source, basing the +table on sample data put in by the page designer. Note that in the real world +the HTML would normally be a larger page read from an external file, keeping +the data and presentation separate, and the data would come from an external +source like an RDBMS. The HTML could be full of styles, images, anything you +like and it would all work just the same. + +>>> xhtml = ''' +... +... +...
Example name21
''' +>>> doc = Meld(xhtml) +>>> templateRow = doc.row.clone() # Take a copy of the template row, then +>>> del doc.row # delete it to make way for the real rows. +>>> for name, age in [("Richie", 30), ("Dave", 39), ("John", 78)]: +... newRow = templateRow.clone() +... newRow.name = name +... newRow.age = age +... doc.people += newRow +>>> print re.sub(r'\s*', '\n', str(doc)) # Prettify the output + + + + + +
Richie30
Dave39
John78
+ +Note that if you were going to subsequently manipulate the table, using +PyMeld or JavaScript for instance, you'd need to rename each `row`, `name` +and `age` element to have a unique name - you can do that by assigning +to the `id` attribute but I've skipped that to make the example simpler. + +As the example shows, the `+=` operator appends content to an element - +appending `` elements to a `` in this case. + + +*Shortcut: the % operator* + +Using the `object.id = value` syntax for every operation can get tedious, so +there are shortcuts you can take using the `%` operator. This works just like +the built-in `%` operator for strings. The example above could have been +written like this: + +>>> for name, age in [("Richie", 30), ("Dave", 39), ("John", 78)]: +... doc.people += templateRow % (name, age) + +The `%` operator, given a single value or a sequence, assigns values to +elements with `id`s in the order that they appear, just like the `%` operator +for strings. Note that there's no need to call `clone()` when you're using +`%`, as it automatically returns a modified clone (again, just like `%` does +for strings). You can also use a dictionary: + +>>> print templateRow % {'name': 'Frances', 'age': 39} + + +The `%` operator is really useful when you have a large number of data items +- for example, populating an HTML form with a record from an RDBMS becomes a +one-liner. + +Note that these examples are written for clarity rather than performance, and +don't necessarily scale very well - using `+=` to build up a result in a loop +is inefficient, and PyMeld's `%` operator is slower than Python's built-in +one. See `toFormatString()` in the reference manual for ways to speed up this +kind of code. + + +*Element content* + +When you refer to a named element in a document, you get a Meld object +representing that whole element: + +>>> page = Meld('Hello world') +>>> print page.x +Hello world + +If you just want to get the content of the element as string, use the +`_content` attribute: + +>>> print page.x._content +Hello world + +You can also assign to `_content`, though that's directly equivalent to +assigning to the tag itself: + +>>> page.x._content = "Hello again" +>>> print page +Hello again +>>> page.x = "Goodbye" +>>> print page +Goodbye + +The only time that you need to assign to `_content` is when you've taken a +reference to an element within a document: + +>>> x = page.x +>>> x._content = "I'm back" +>>> print page +I'm back + +Saying `x = "I'm back"` would simply re-bind `x` to the string `"I'm back"` +without affecting the document. + + +*Version and license* + +This is version 2.1.4 of _PyMeld.py_, Copyright (c) 2002-2009 Entrian +Solutions. It is Open Source software released under the terms of the MIT +License. +""" + +__version__ = "2.1.4" +__author__ = "Richie Hindle " + + +import sys, string, re +from types import StringType, UnicodeType + +# Entrian.Coverage: Pragma Stop +try: + True, False, bool +except NameError: + True = 1 + False = 0 + def bool(x): + return not not x +# Entrian.Coverage: Pragma Start + +# Regular expressions for tags and attributes. +openTagRE = re.compile(r"""(?ix) # Case-insensitive, verbose + <(?P[-a-z0-9_:.]+) # Tag opens; capture its name + (?:\s+[-a-z0-9_:.]+=(?P["']).*?(?P=quote1))* # Attributes + \s*/?> # Tag closes + """) + +openIDTagRE = r"""(?ix) # Case-insensitive, verbose + <(?P[-a-z0-9_:.]+) # Tag opens; capture its name + (?:\s+[-a-z0-9_:.]+=(?P["']).*?(?P=quote1))* # Attributes before id + \s+id=(?P["'])(?P%s)(?P=quote2) # The 'id' tag + (?:\s+[-a-z0-9_:.]+=(?P["']).*?(?P=quote3))* # Attributes after id + \s*/?> # Tag closes + """ + +attributeRE = r"""(?ix) + (?P\s+) + (?P%s)=(?P["'])(?P.*?)(?P=quote) + """ + +idRE = re.compile(r"""(?i)\s+id=(?P["'])(?P.*?)(?P=quote)""") + + +def _findIDMatch(id, text): + """Work around a possible RE bug: + > m = re.search(r"<\w+(?:\s\w+='.*?')*\sid='x'>", "") + > m.span() + (0, 25) + """ + + # To repeat the bug: return re.search(openIDTagRE % id, text) + # The fix: check whether you can match again, *within* the original match. + thisRE = openIDTagRE % id + start = 0 + match = re.search(thisRE, text) + prevMatch = match + while match: + prevMatch = match + start = prevMatch.span()[0] + 1 + match = re.search(thisRE, '.' * start + text[start:prevMatch.end()]) + return prevMatch + + +class _MarkupHolder: + """Keeps hold of the markup string, so that it can be shared between + multiple Meld objects.""" + def __init__(self, s): + self.count = 0 + self.s = s + + def __setattr__(self, name, value): + self.__dict__[name] = value + if name == 's': + self.__dict__['count'] = self.count + 1 + + +READ_ONLY_MESSAGE = "You can't modify this read-only Meld object" +class ReadOnlyError(Exception): + """Raised if you try to modify a readonly Meld object.""" + pass + + +class Meld: + """Represents an XML document, or a fragment of one. Pass XML/XHTML + source to the constructor. You can then access all the elements with + `id="name"` attributes as `object.name`, and all the attributes of the + outermost element as `object.attribute`.""" + + def __init__(self, source, + readonly=False, replaceUnderscoreWithDash=False): + """Creates a `Meld` from XML source. `readonly` does what it + says. replaceUnderscoreWithDash lets you write code like this: + + >>> html = '
xxx
' + >>> meld = Meld(html, replaceUnderscoreWithDash=True) + >>> meld.header_box = "Yay!" + >>> print meld.header_box +
Yay!
+ >>> del meld.header_box + >>> print meld + + """ + + # Store the options and the markup. + self._readonly = readonly + self._dashes = replaceUnderscoreWithDash + if source is not None: + # This is a container-style Meld, representing the whole thing. + if isinstance(source, StringType) or isinstance(source, UnicodeType): + self._markup = _MarkupHolder(source) + self._lastUpdate = -1 + self._name = None + # No call to `self._updatePositions()` 'cos it's done lazily. + else: + raise TypeError, "Melds must be constructed from strings" + + def _makeChild(self, name, start): + """Alternative constructor for internal use: makes a child Meld + for a named element. `start` is a shortcut - everywhere where this + is used, we've already found the starting position of the child + element as a side effect of determining that the element exists.""" + + newObject = Meld(None, self._readonly, self._dashes) + newObject._markup = self._markup + newObject._lastUpdate = -1 + newObject._name = name + newObject._updatePositions(start) + return newObject + + def _updatePositions(self, start=None): + """Finds the start and end positions of the start and end tag in + the markup. If the caller happens to know where the start tag starts, + he can pass it in to save time.""" + + if self._lastUpdate == self._markup.count: + return + + # Find the start tag. + if self._name is None: + # This is a container-style Meld, representing the whole thing. + # Look for the first opening element - we'll treat that as the + # defining element, in the absence of an `id`. + match = re.search(openTagRE, self._markup.s) + if not match: + raise ValueError, "This isn't any form of markup I recognize" + self._tagName = match.group('tag') + self._openStart = match.start() + self._openEnd = match.end() + else: + # Find the start tag. + if start is None: + match = _findIDMatch(self._name, self._markup.s) + self._tagName = match.group('tag') + self._openStart = match.start() + self._openEnd = match.end() + else: + match = re.search(openTagRE, self._markup.s[start:]) + self._tagName = match.group('tag') + self._openStart = start + match.start() + self._openEnd = start + match.end() + + # Now find the end tag in the rest of the HTML. Most of this code + # deals with nested tags - counting up nested opening tags and + # counting down the closing tags until it gets to zero. + rest = self._markup.s[self._openEnd:] + depth = 1 + pos = 0 + while 1: + openMatch = re.search('(?i)<%s(>|\s)' % self._tagName, rest[pos:]) + closeMatch = re.search('(?i)' % self._tagName, rest[pos:]) + if not closeMatch: + # There's no matching closing tag. + self._closeStart = self._closeEnd = self._openEnd + break + + elif not openMatch: + if depth == 1: + # We've found the matching closing tag. + self._closeStart = self._openEnd + pos + closeMatch.span()[0] + self._closeEnd = self._openEnd + pos + closeMatch.span()[1] + break + else: + # We've found a closing tag, but it's for a nested opening tag. + depth = depth - 1 + pos = pos + closeMatch.span()[1] + + elif openMatch.span()[0] < closeMatch.span()[0]: + # We've found a nested opening tag. + depth = depth + 1 + pos = pos + openMatch.span()[1] + + else: # closeMatch.span()[0] < openMatch.span()[0] + depth = depth - 1 + if depth == 0: + # We've found the matching closing tag. + self._closeStart = self._openEnd + pos + closeMatch.span()[0] + self._closeEnd = self._openEnd + pos + closeMatch.span()[1] + break + else: + # We've found a closing tag but it's for a nested opening tag. + pos = pos + closeMatch.span()[1] + + self._lastUpdate = self._markup.count + + def _findElementFromID(self, nodeID): + """Returns the start position of the element with the given ID, + or None.""" + self._updatePositions() + + # For the outermost element, include that element (otherwise you + # couldn't access it by ID). For all other elements, don't do that, + # because you couldn't access nested elements with the same name. + if self._name is None: + start = self._openStart + subset = self._markup.s[start:self._closeEnd] + else: + start = self._openEnd + subset = self._markup.s[start:self._closeStart] + match = _findIDMatch(nodeID, subset) + if match: + return start + match.start() + else: + return None + + def _quoteAttribute(self, value): + """Minimally quotes an attribute value, using `"`, `&`, + `<` and `>`.""" + value = value.replace('"', '"') + value = value.replace('<', '<').replace('>', '>') + value = re.sub(r'&(?![a-zA-Z0-9]+;)', '&', value) + return value + + def _unquoteAttribute(self, value): + """Unquotes an attribute value quoted by `_quoteAttribute()`.""" + value = value.replace('"', '"').replace('&', '&') + return value.replace('<', '<').replace('>', '>') + + def __getattr__(self, name): + """`object.`, if this Meld contains an element with an `id` + attribute of `name`, returns a Meld representing that element. + + Otherwise, `object.` returns the value of the attribute with + the given name, as a string. If no such attribute exists, an + AttributeError is raised. + + `object._content` returns the content of the Meld, not including + the enclosing ``, as a string. + + >>> p = Meld('

Hello World

') + >>> print p.who + World + >>> print p.style + one + >>> print p._content + Hello World + >>> print p.who._content + World + """ + + if name == '_content': + self._updatePositions() + return self._markup.s[self._openEnd:self._closeStart] + elif name[0] == '_': + try: + return self.__dict__[name] + except KeyError: + raise AttributeError, name + if self._dashes: + name = string.replace(name, '_', '-') + self._updatePositions() + start = self._findElementFromID(name) + if start is not None: + return self._makeChild(name, start) + openTag = self._markup.s[self._openStart:self._openEnd] + match = re.search(attributeRE % name, openTag) + if match: + return self._unquoteAttribute(match.group('value')) + else: + raise AttributeError, "No element or attribute named %r" % name + + def __setattr__(self, name, value): + """`object. = value` sets the XML content of the element with an + `id` of `name`, or if no such element exists, sets the value of the + `name` attribute on the outermost element. If the attribute is not + already there, a new attribute is created. + + >>> p = Meld('

Hello World

') + >>> p.who = "Richie" + >>> p.style = "two" + >>> p.align = "center" + >>> p.who.id = "newwho" + >>> print p +

Hello Richie

+ """ + + if name[0] == '_' and name != '_content': + self.__dict__[name] = value + return + if self._readonly: + raise ReadOnlyError, READ_ONLY_MESSAGE + if self._dashes: + name = string.replace(name, '_', '-') + self._updatePositions() + if not isinstance(value, StringType) or isinstance(value, UnicodeType): + value = str(value) + if name == '_content': + self._markup.s = self._markup.s[:self._openEnd] + \ + value + \ + self._markup.s[self._closeStart:] + return + start = self._findElementFromID(name) + if start is not None: + child = self._makeChild(name, start) + if self._markup.s[child._openStart:child._closeEnd] == value: + return # `x.y = x.y`, as happens via `x.y += z` + self._markup.s = self._markup.s[:child._openEnd] + \ + value + \ + self._markup.s[child._closeStart:] + else: + # Set the attribute value. + openTag = self._markup.s[self._openStart:self._openEnd] + attributeMatch = re.search(attributeRE % name, openTag) + escapedValue = self._quoteAttribute(value) + if attributeMatch: + # This is a change to an existing attribute. + attributeStart, attributeEnd = attributeMatch.span() + quote = attributeMatch.group('quote') + newOpenTag = openTag[:attributeStart] + \ + '%s%s=%s%s%s' % (attributeMatch.group('space'), + attributeMatch.group('name'), + quote, escapedValue, quote) + \ + openTag[attributeEnd:] + self._markup.s = self._markup.s[:self._openStart] + \ + newOpenTag + \ + self._markup.s[self._openEnd:] + else: + # This is introducing a new attribute. + newAttributePos = self._openStart + 1 + len(self._tagName) + newAttribute = ' %s="%s"' % (name, escapedValue) + self._markup.s = self._markup.s[:newAttributePos] + \ + newAttribute + \ + self._markup.s[newAttributePos:] + if string.lower(name) == 'id': + self._name = value + + def __delattr__(self, name): + """Deletes the named element or attribute from the `Meld`: + + >>> p = Meld('

Hello World

') + >>> del p.who + >>> del p.style + >>> print p +

Hello

+ """ + + if name == '_content': + self._updatePositions() + self._markup.s = self._markup.s[:self._openEnd] + \ + self._markup.s[self._closeStart:] + return + if name[0] == '_': + try: + del self.__dict__[name] + return + except KeyError: + raise AttributeError, name + if self._readonly: + raise ReadOnlyError, READ_ONLY_MESSAGE + if self._dashes: + name = string.replace(name, '_', '-') + self._updatePositions() + start = self._findElementFromID(name) + if start is not None: + child = self._makeChild(name, start) + self._markup.s = self._markup.s[:child._openStart] + \ + self._markup.s[child._closeEnd:] + return + + # Look for an attribute of this name. + openTag = self._markup.s[self._openStart:self._openEnd] + attributeMatch = re.search(attributeRE % name, openTag) + if attributeMatch: + attributeStart, attributeEnd = attributeMatch.span() + newOpenTag = openTag[:attributeStart] + openTag[attributeEnd:] + self._markup.s = self._markup.s[:self._openStart] + \ + newOpenTag + \ + self._markup.s[self._openEnd:] + else: + raise AttributeError, "No element or attribute named %r" % name + + def __mod__(self, values): + """`object % value`, `object % sequence`, or `object % dictionary` all + mimic the `%` operator for strings: + + >>> xml = 'Hello World' + >>> x = Meld(xml) + >>> print x % ("Howdy", "everybody") + Howdy everybody + >>> print x % {'who': 'all'} + Hello all + + Assignment for sequences happens in the same order that nodes with + 'id' attributes appear in the document, not including the top-level + node (because if the top-level node were included, you'd only ever + be able to assign to that and nothing else): + + >>> xml = '''
+ ... + ... First one + ... Second one + ... + ... Third one; the content includes 'f': + ... Removed when 'e' is assigned to + ... + ... ''' + >>> a = Meld(xml) + >>> print a % ('One, with a new node', 'Two', 'Three') + + + One, with a new node + Two + + Three + + + Giving the wrong number of elements to `%` raises the same exceptions + as the builtin string `%` operator. Unlike the builtin `%` operator, + dictionaries don't need to specify all the keys: + + >>> print x % "Howdy" + Traceback (most recent call last): + ... + TypeError: not enough arguments + >>> print x % ("Howdy", "everybody", "everywhere") + Traceback (most recent call last): + ... + TypeError: not all arguments converted + >>> print x % {"greeting": "Howdy"} + Howdy World + """ + + # Figure out whether we have a dictionary, a sequence, or a lone value. + new = self.clone() + new._updatePositions() + if hasattr(values, 'values') and callable(values.values): + # It's a dictionary. + keys = values.keys() + sequence = values.values() + elif hasattr(values, '__getitem__') and \ + not isinstance(values, StringType) or isinstance(values, UnicodeType): + # It's a sequence. + keys = None + sequence = list(values) + else: + # Assume it's a plain value. + keys = None + sequence = [values] + + # If we've derived a set of keys, just assign the values. + if keys: + for key, value in zip(keys, sequence): + if self._dashes: + key = string.replace(key, '_', '-') + if not isinstance(value, StringType) or isinstance(value, UnicodeType): + value = str(value) + start = new._findElementFromID(key) + if start is not None: + child = new._makeChild(key, start) + new._markup.s = new._markup.s[:child._openEnd] + \ + value + \ + new._markup.s[child._closeStart:] + else: + # No keys, so set the values in the order they appear. We + # reverse the sequence so we can use pop(). + sequence.reverse() + pos = new._openEnd + while sequence: + value = sequence.pop() + if not isinstance(value, StringType) or isinstance(value, UnicodeType): + value = str(value) + subset = new._markup.s[pos:new._closeStart] + match = _findIDMatch('[^\'"]*', subset) + if not match: + # We've run out of elements with `id` attributes. + raise TypeError, "not all arguments converted" + child = new._makeChild(match.group('id'), pos+match.start()) + new._markup.s = new._markup.s[:child._openEnd] + \ + value + \ + new._markup.s[child._closeStart:] + addedSize = len(value) - (child._closeStart - child._openEnd) + new._closeStart += addedSize + new._closeEnd += addedSize + pos = child._closeEnd + addedSize + subset = new._markup.s[pos:new._closeStart] + match = _findIDMatch('[^\'"]*', subset) + if match: + raise TypeError, "not enough arguments" + + return new + + def toFormatString(self, useDict=False): + r"""Converts a Meld object to a string, with the contents of any tags + with `id` attributes replaced with `%s` or `%(id)s`. This lets you + use Python's built-in `%` operator rather than PyMeld's, which can + speed things up considerably when you're looping over a lot of data. + Here's the example from the main documentation, speeded up by using + `toFormatString()` and by avoiding repeated use of the `+=` operator: + + >>> xhtml = '''
Frances39
+ ... + ... + ...
Example21
''' + >>> doc = Meld(xhtml) + >>> rowFormat = doc.row.toFormatString() + >>> rows = [] + >>> for name, age in [("Richie", 30), ("Dave", 39), ("John", 78)]: + ... rows.append(rowFormat % (name, age)) + >>> doc.people = '\n' + doc.header + ''.join(rows) + >>> print re.sub(r'\s*', '\n', str(doc)) # Prettify + + + + + +
Richie30
Dave39
John78
+ + So the inner loop no longer contains any PyMeld calls at all - it only + manipulates strings and lists. Here's what `doc.row.toFormatString()` + actually returns - note that this is a string, not a PyMeld object: + + >>> print doc.row.toFormatString() + %s%s + + You can ask for a format string that expects a dictionary rather than + a tuple using the `useDict` parameter: + + >>> print doc.row.toFormatString(useDict=True) + %(name)s%(age)s + + If your markup contains `%` symbols, they are correctly quoted in the + resulting format string: + + >>> doc = Meld("

10% gin.

") + >>> print doc.toFormatString() +

10%% %s.

+ >>> print doc.toFormatString() % 'vodka' +

10% vodka.

+ """ + + # Build a dictionary mapping from all the possible keys to special + # marker values. It doesn't matter if there's some text with `id='x'` + # in there, because it will just be ignored. + self._updatePositions() + content = self._markup.s[self._openEnd:self._closeStart] + quotesAndKeys = re.findall(r'\bid=(["\'])([^"\']+)\1', content) + keysToMarkers = {} + for unusedQuote, key in quotesAndKeys: + if self._dashes: + key = string.replace(key, '-', '_') + keysToMarkers[key] = ":PyMeldMarker'%s':" % key + + # Now use the PyMeld `%` operator to populate the tags. + format = str(self % keysToMarkers) + + # Convert the resulting marked-up string to a format string, by + # quoting all the % characters then inserting %s directives. + format = string.replace(format, '%', '%%') + if useDict: + return re.sub(r":PyMeldMarker'([^']+)':", r'%(\1)s', format) + else: + return re.sub(r":PyMeldMarker'[^']+':", r'%s', format) + + def clone(self, readonly=0, replaceUnderscoreWithDash=False): + """Creates a clone of a `Meld`, for instance to change an attribute + without affecting the original document: + + >>> p = Meld('

Hello World

') + >>> q = p.clone() + >>> q.who = "Richie" + >>> print q.who + Richie + >>> print p.who + World + """ + + self._updatePositions() + markup = self._markup.s[self._openStart:self._closeEnd] + return Meld(markup, readonly, replaceUnderscoreWithDash) + + def __add__(self, other): + """`object1 + object2` turns both objects into strings and returns the + concatenation of the strings: + + >>> a = Meld('1') + >>> b = Meld('2') + >>> c = Meld('3') + >>> print a + b + 12 + >>> print a.x + b.y + c.z + 123 + """ + + if isinstance(other, Meld): + other._updatePositions() + other = other._markup.s[other._openStart:other._closeEnd] + self._updatePositions() + return self._markup.s[self._openStart:self._closeEnd] + other + + def __radd__(self, other): + """See `__add__`""" + # The case where `other` is a Meld can never happen, because + # __add__ will be called instead. + self._updatePositions() + return other + self._markup.s[self._openStart:self._closeEnd] + + def __iadd__(self, other): + """`object1 += object2` appends a string or a clone of a Meld to + the end of another Meld's content. This is used to build things + like HTML tables, which are collections of other objects (eg. table + rows). See *Real-world example* in the main documentation.""" + + if self._readonly: + raise ReadOnlyError, READ_ONLY_MESSAGE + if isinstance(other, Meld): + other._updatePositions() + other = other._markup.s[other._openStart:other._closeEnd] + self._updatePositions() + self._markup.s = self._markup.s[:self._closeStart] + \ + other + \ + self._markup.s[self._closeStart:] + return self + + def __str__(self): + """Returns the XML that this `Meld` represents. Don't call + this directly - instead convert a `Meld` to a string using + `str(object)`. `print` does this automatically, which is why + none of the examples calls `str`.""" + + self._updatePositions() + if self._name is None: + return str(self._markup.s) + else: + return str(self._markup.s[self._openStart:self._closeEnd]) + + def __unicode__(self): + """Returns the XML that this `Meld` represents. Don't call + this directly - instead convert a `Meld` to unicode using + `unicode(object)`. `print` does this automatically, which is why + none of the examples calls `str`. Note that PyMeld's ability to + handle Unicode is largely untested.""" + + self._updatePositions() + return unicode(self._markup.s[self._openStart:self._closeEnd]) + + +# +# Extra tests, for features that aren't tested by the (visible) docstrings: +# +__test__ = { +'entities and charrefs': """ +>>> page = Meld('''• This "and that"... +... x''') +>>> print page.s.title +"Quoted" & Not +>>> page.s.title = page.s.title # Accept liberally, produce strictly. +>>> print page +• This "and that"... +x +>>> page.s.title = page.s.title + " <>" +>>> print page.s.title +"Quoted" & Not <> +>>> print page.s +x +""", + +'assigning to _content': """ +>>> page = Meld('''Old''') +>>> page.s._content = "New" +>>> print page +New +>>> page._content = "All new" +>>> print page +All new +""", + +'deleting _content': """ +>>> page = Meld('''Old''') +>>> del page.s._content +>>> print page + +""", + +'constructing from an unknown type': """ +>>> page = Meld(1) +Traceback (most recent call last): +... +TypeError: Melds must be constructed from strings +""", + +'accessing a non-existent attribute': """ +>>> page = Meld('') +>>> print page.spam +Traceback (most recent call last): +... +AttributeError: No element or attribute named 'spam' +>>> del page.spam +Traceback (most recent call last): +... +AttributeError: No element or attribute named 'spam' +>>> print page.body.spam # For non-container Melds +Traceback (most recent call last): +... +AttributeError: No element or attribute named 'spam' +>>> del page.body.spam # For non-container Melds +Traceback (most recent call last): +... +AttributeError: No element or attribute named 'spam' +""", + +'add new things':""" +>>> page = Meld('''''') +>>> page.empty = "Not any more" +>>> page.empty.cols = 60 +>>> print page + +""", + +'readonly': """ +>>> page = Meld('''No!''', readonly=True) +>>> page.no = "Yes?" +Traceback (most recent call last): +... +ReadOnlyError: You can't modify this read-only Meld object + +>>> page.no.attribute = "Yes?" +Traceback (most recent call last): +... +ReadOnlyError: You can't modify this read-only Meld object + +>>> page.no += "More?" +Traceback (most recent call last): +... +ReadOnlyError: You can't modify this read-only Meld object + +>>> del page.no +Traceback (most recent call last): +... +ReadOnlyError: You can't modify this read-only Meld object +""", + +'copy from one to another': """ +>>> a = Meld('One') +>>> b = Meld('Two') +>>> a.one = b.two +>>> print a +Two +>>> b.two = "New" +>>> print a # Checking for side-effects +Two +""", + +'mixed-type add, radd and iadd': """ +>>> a = Meld('1') +>>> print a.one + "x" +1x +>>> print "x" + a.one +x1 +>>> a.one += "y" +>>> print a +1y +""", + +'access top-level element': """ +>>> d = Meld("spam") +>>> print d.x +spam +""", + +'access nested element with same name': """ +>>> d = Meld("spam") +>>> print d.x.x +spam +>>> d = Meld("spam") +>>> print d.x.x +spam +""", + +# This is just a smoke-test; proper Unicode support is untested, though +# the code does attempt to be unicode-friendly. +'unicode': r""" +>>> u = Meld(u'One') +>>> a = Meld('Two') +>>> u.one = a.two +>>> print repr(unicode(u)) +u'Two' +>>> a.two = Meld(u'') +>>> print a + +""", + +'private attributes': """ +>>> page = Meld('x') +>>> page._private = "Spam" +>>> print repr(page._private) +'Spam' +>>> print page +x +>>> del page._private +>>> print repr(page._private) +Traceback (most recent call last): +... +AttributeError: _private +>>> del page._private +Traceback (most recent call last): +... +AttributeError: _private +>>> print page +x +""", + +'no markup': """ +>>> page = Meld("Hello world") +>>> print page.spam +Traceback (most recent call last): +... +ValueError: This isn't any form of markup I recognize +""", + +'nesting': """ +>>> page = Meld(''' +... Hello +... World +... and friends +... ! +... Goodbye +... +... ''') +>>> print page.all +Hello + World + and friends + ! + Goodbye + +>>> print page.extra +and friends +""", + +'re-bug': """ +>>> page = Meld("") +>>> print page.x # Was "" + +""", + +'underscores': """ +>>> html = '
xxx
' +>>> meld = Meld(html, replaceUnderscoreWithDash=True) +>>> print meld % {'header_box': 'Mod'} +
Mod
+>>> print meld.toFormatString(useDict=True) +
%(header_box)s
+>>> meld.header_box = 'yyy' +>>> meld.header_box.dash_attr = '___' +>>> print meld +
yyy
+""", + +'doctype': """ +>>> html = '\\nz' +>>> meld = Meld(html) +>>> meld.a = 'a' +>>> meld.y = 'b' +>>> print meld +\nb +""", + +'eichin-bug': """ +>>> page = Meld(''' +...
label Running
''') +>>> print page.Instance_1_4 +Running +""", +} + + +# Entrian.Coverage: Pragma Stop + +def test(): + """Tests the `PyMeld` module, performing code coverage analysis if + `Entrian.Coverage` is available. Returns `(failed, total)`, a la + `doctest.testmod`.""" + + import doctest + try: + from Entrian import Coverage + Coverage.start('PyMeld') + except ImportError: + Coverage = False + + ## # Profiling. + ## import PyMeld + ## import profile, pstats + ## profile.run("import doctest, PyMeld; result = doctest.testmod(PyMeld)", "rjh") + ## s = pstats.Stats("rjh") + ## s.sort_stats('cumulative').print_stats() + + ## # Cheap benchmark, for comparing new versions with old. + ## import PyMeld + ## for i in range(100): + ## reload(doctest) + ## result = doctest.testmod(PyMeld) + + import PyMeld + result = doctest.testmod(PyMeld) + + if Coverage: + analysis = Coverage.getAnalysis() + analysis.printAnalysis() + return result + +if __name__ == '__main__': + failed, total = test() + if failed == 0: # Else `doctest.testmod` prints the failures. + print "All %d tests passed." % total -- cgit