]> git.donarmstrong.com Git - neurodebian.git/commitdiff
Add RSS feed extension for sphinx.
authorMichael Hanke <michael.hanke@gmail.com>
Fri, 18 Mar 2011 19:14:20 +0000 (15:14 -0400)
committerMichael Hanke <michael.hanke@gmail.com>
Fri, 18 Mar 2011 19:14:20 +0000 (15:14 -0400)
Taken verbatim from https://bitbucket.org/birkenfeld/sphinx-contrib/src/813d6d58358d/feed/

sphinx/conf.py
sphinx/sphinxext/feed/__init__.py [new file with mode: 0644]
sphinx/sphinxext/feed/absolutify_urls.py [new file with mode: 0644]
sphinx/sphinxext/feed/django_support.py [new file with mode: 0644]
sphinx/sphinxext/feed/feedgenerator.py [new file with mode: 0644]
sphinx/sphinxext/feed/fsdict.py [new file with mode: 0644]
sphinx/sphinxext/feed/path.py [new file with mode: 0644]

index 1e8818de94ad3e22f1dc4533309369d325c92f43..5ec8c0f0f1ba417cc749f3d43361aca9c3b0c8a6 100644 (file)
@@ -44,6 +44,7 @@ def artworkdir():
 #extensions = []
 sys.path.append(os.path.abspath('.'))
 extensions = ['sphinxext.quote',
+              'sphinxext.feed',
               'sphinx.ext.todo']
 
 # show todo items
@@ -209,3 +210,9 @@ latex_documents = [
 
 # If false, no module index is generated.
 #latex_use_modindex = True
+
+# RSS feed
+# --------
+feed_base_url = 'http://neuro.debian.net/blog'
+feed_description = "Debian for neuroscience and neuroscience in Debian"
+feed_filename = 'rss.xml'
diff --git a/sphinx/sphinxext/feed/__init__.py b/sphinx/sphinxext/feed/__init__.py
new file mode 100644 (file)
index 0000000..11a6004
--- /dev/null
@@ -0,0 +1,124 @@
+from fsdict import FSDict
+import feedgenerator
+from urllib import quote_plus
+import os.path
+
+#global
+feed_entries = None
+
+#constant unlikely to occur in a docname and legal as a filename
+MAGIC_SEPARATOR = '---###---'
+
+def setup(app):
+    """
+    see: http://sphinx.pocoo.org/ext/appapi.html
+    this is the primary extension point for Sphinx
+    """
+    from sphinx.application import Sphinx
+    if not isinstance(app, Sphinx): return
+    app.add_config_value('feed_base_url', '', 'html')
+    app.add_config_value('feed_description', '', 'html')
+    app.add_config_value('feed_filename', 'rss.xml', 'html')
+    
+    app.connect('html-page-context', create_feed_item)
+    app.connect('html-page-context', inject_feed_url)
+    app.connect('build-finished', emit_feed)
+    app.connect('builder-inited', create_feed_container)
+    app.connect('env-purge-doc', remove_dead_feed_item)
+    
+def create_feed_container(app):
+    """
+    create lazy filesystem stash for keeping RSS entry fragments, since we don't
+    want to store the entire site in the environment (in fact, even if we did,
+    it wasn't persisting for some reason.)
+    """
+    global feed_entries
+    rss_fragment_path = os.path.realpath(os.path.join(app.outdir, '..', 'rss_entry_fragments'))
+    feed_entries = FSDict(work_dir=rss_fragment_path)
+    app.builder.env.feed_url = app.config.feed_base_url + '/' + \
+        app.config.feed_filename
+    
+def inject_feed_url(app, pagename, templatename, ctx, doctree):
+    #We like to provide our templates with a way to link to the rss output file
+    ctx['rss_link'] = app.builder.env.feed_url #app.config.feed_base_url + '/' + app.config.feed_filename
+    
+def create_feed_item(app, pagename, templatename, ctx, doctree):
+    """
+    Here we have access to nice HTML fragments to use in, say, an RSS feed.
+    We serialize them to disk so that we get them preserved across builds.
+    """
+    global feed_entries
+    import dateutil.parser
+    from absolutify_urls import absolutify
+    date_parser = dateutil.parser.parser()
+    metadata = app.builder.env.metadata.get(pagename, {})
+    
+    if 'date' not in metadata:
+        return #don't index dateless articles
+    try:
+        pub_date = date_parser.parse(metadata['date'])
+    except ValueError, exc:
+        #probably a nonsensical date
+        app.builder.warn('date parse error: ' + str(exc) + ' in ' + pagename)
+        return
+        
+    # title, link, description, author_email=None,
+    #     author_name=None, author_link=None, pubdate=None, comments=None,
+    #     unique_id=None, enclosure=None, categories=(), item_copyright=None,
+    #     ttl=None,
+    link = app.config.feed_base_url + '/' + ctx['current_page_name'] + ctx['file_suffix']
+    item = {
+      'title': ctx.get('title'),
+      'link': link,
+      'unique_id': link,
+      'description': absolutify(ctx.get('body'), link),
+      'pubdate': pub_date
+    }
+    if 'author' in metadata:
+        item['author'] = metadata['author']
+    feed_entries[nice_name(pagename, pub_date)] = item    
+
+def remove_dead_feed_item(app, env, docname):
+    """
+    TODO:
+    purge unwanted crap
+    """
+    global feed_entries
+    munged_name = ''.join([MAGIC_SEPARATOR,quote_plus(docname)])
+    for name in feed_entries:
+        if name.endswith(munged_name):
+            del(feed_entries[name])
+
+def emit_feed(app, exc):
+    global feed_entries
+    import os.path
+    
+    feed_dict = {
+      'title': app.config.project,
+      'link': app.config.feed_base_url,
+      'feed_url': app.config.feed_base_url,
+      'description': app.config.feed_description
+    }
+    if app.config.language:
+        feed_dict['language'] = app.config.language
+    if app.config.copyright:
+        feed_dict['feed_copyright'] = app.config.copyright
+    feed = feedgenerator.Rss201rev2Feed(**feed_dict)
+    app.builder.env.feed_feed = feed
+    ordered_keys = feed_entries.keys()
+    ordered_keys.sort(reverse=True)
+    for key in ordered_keys:
+        feed.add_item(**feed_entries[key])     
+    outfilename = os.path.join(app.builder.outdir,
+      app.config.feed_filename)
+    fp = open(outfilename, 'w')
+    feed.write(fp, 'utf-8')
+    fp.close()
+
+def nice_name(docname, date):
+    """
+    we need convenient filenames which incorporate dates for ease of sorting and
+    guid for uniqueness, plus will work in the FS without inconvenient
+    characters. NB, at the moment, hour of publication is ignored.
+    """
+    return quote_plus(MAGIC_SEPARATOR.join([date.isoformat(), docname]))
diff --git a/sphinx/sphinxext/feed/absolutify_urls.py b/sphinx/sphinxext/feed/absolutify_urls.py
new file mode 100644 (file)
index 0000000..e892909
--- /dev/null
@@ -0,0 +1,96 @@
+# By Gareth Rees
+# http://gareth-rees.livejournal.com/27148.html
+
+import html5lib
+import html5lib.serializer
+import html5lib.treewalkers
+import urlparse
+
+# List of (ELEMENT, ATTRIBUTE) for HTML5 attributes which contain URLs.
+# Based on the list at http://www.feedparser.org/docs/resolving-relative-links.html
+url_attributes = [
+    ('a', 'href'),
+    ('applet', 'codebase'),
+    ('area', 'href'),
+    ('blockquote', 'cite'),
+    ('body', 'background'),
+    ('del', 'cite'),
+    ('form', 'action'),
+    ('frame', 'longdesc'),
+    ('frame', 'src'),
+    ('iframe', 'longdesc'),
+    ('iframe', 'src'),
+    ('head', 'profile'),
+    ('img', 'longdesc'),
+    ('img', 'src'),
+    ('img', 'usemap'),
+    ('input', 'src'),
+    ('input', 'usemap'),
+    ('ins', 'cite'),
+    ('link', 'href'),
+    ('object', 'classid'),
+    ('object', 'codebase'),
+    ('object', 'data'),
+    ('object', 'usemap'),
+    ('q', 'cite'),
+    ('script', 'src')]
+
+def absolutify(src, base_url):
+    """absolutify(SRC, BASE_URL): Resolve relative URLs in SRC.
+SRC is a string containing HTML. All URLs in SRC are resolved relative
+to BASE_URL. Return the body of the result as HTML."""
+
+    # Parse SRC as HTML.
+    tree_builder = html5lib.treebuilders.getTreeBuilder('dom')
+    parser = html5lib.html5parser.HTMLParser(tree = tree_builder)
+    dom = parser.parse(src)
+
+    # Handle <BASE> if any.
+    head = dom.getElementsByTagName('head')[0]
+    for b in head.getElementsByTagName('base'):
+        u = b.getAttribute('href')
+        if u:
+            base_url = urlparse.urljoin(base_url, u)
+            # HTML5 4.2.3 "if there are multiple base elements with href
+            # attributes, all but the first are ignored."
+            break
+
+    # Change all relative URLs to absolute URLs by resolving them
+    # relative to BASE_URL. Note that we need to do this even for URLs
+    # that consist only of a fragment identifier, because Google Reader
+    # changes href=#foo to href=http://site/#foo
+    for tag, attr in url_attributes:
+        for e in dom.getElementsByTagName(tag):
+            u = e.getAttribute(attr)
+            if u:
+                e.setAttribute(attr, urlparse.urljoin(base_url, u))
+
+    # Return the HTML5 serialization of the <BODY> of the result (we don't
+    # want the <HEAD>: this breaks feed readers).
+    body = dom.getElementsByTagName('body')[0]
+    tree_walker = html5lib.treewalkers.getTreeWalker('dom')
+    html_serializer = html5lib.serializer.htmlserializer.HTMLSerializer()
+    return u''.join(html_serializer.serialize(tree_walker(body)))
+    
+
+# Alternative option, from http://stackoverflow.com/questions/589833/how-to-find-a-relative-url-and-translate-it-to-an-absolute-url-in-python/589939#589939
+# 
+# import re, urlparse
+# 
+# find_re = re.compile(r'\bhref\s*=\s*("[^"]*"|\'[^\']*\'|[^"\'<>=\s]+)')
+# 
+# def fix_urls(document, base_url):
+#     ret = []
+#     last_end = 0
+#     for match in find_re.finditer(document):
+#         url = match.group(1)
+#         if url[0] in "\"'":
+#             url = url.strip(url[0])
+#         parsed = urlparse.urlparse(url)
+#         if parsed.scheme == parsed.netloc == '': #relative to domain
+#             url = urlparse.urljoin(base_url, url)
+#             ret.append(document[last_end:match.start(1)])
+#             ret.append('"%s"' % (url,))
+#             last_end = match.end(1)
+#     ret.append(document[last_end:])
+#     return ''.join(ret)
diff --git a/sphinx/sphinxext/feed/django_support.py b/sphinx/sphinxext/feed/django_support.py
new file mode 100644 (file)
index 0000000..1505004
--- /dev/null
@@ -0,0 +1,170 @@
+"""
+utils needed for django's feed generator
+"""
+
+"""
+Utilities for XML generation/parsing.
+from django.utils.xmlutils import SimplerXMLGenerator
+"""
+
+from xml.sax.saxutils import XMLGenerator
+
+class SimplerXMLGenerator(XMLGenerator):
+    def addQuickElement(self, name, contents=None, attrs=None):
+        "Convenience method for adding an element with no children"
+        if attrs is None: attrs = {}
+        self.startElement(name, attrs)
+        if contents is not None:
+            self.characters(contents)
+        self.endElement(name)
+        
+"""
+from django.utils.encoding import force_unicode, iri_to_uri
+"""
+import types
+import urllib
+import locale
+import datetime
+import codecs
+from decimal import Decimal
+
+class DjangoUnicodeDecodeError(UnicodeDecodeError):
+    def __init__(self, obj, *args):
+        self.obj = obj
+        UnicodeDecodeError.__init__(self, *args)
+
+    def __str__(self):
+        original = UnicodeDecodeError.__str__(self)
+        return '%s. You passed in %r (%s)' % (original, self.obj,
+                type(self.obj))
+
+class StrAndUnicode(object):
+    """
+    A class whose __str__ returns its __unicode__ as a UTF-8 bytestring.
+
+    Useful as a mix-in.
+    """
+    def __str__(self):
+        return self.__unicode__().encode('utf-8')
+
+def is_protected_type(obj):
+    """Determine if the object instance is of a protected type.
+
+    Objects of protected types are preserved as-is when passed to
+    force_unicode(strings_only=True).
+    """
+    return isinstance(obj, (
+        types.NoneType,
+        int, long,
+        datetime.datetime, datetime.date, datetime.time,
+        float, Decimal)
+    )
+
+def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'):
+    """
+    Similar to smart_unicode, except that lazy instances are resolved to
+    strings, rather than kept as lazy objects.
+
+    If strings_only is True, don't convert (some) non-string-like objects.
+    """
+    if strings_only and is_protected_type(s):
+        return s
+    try:
+        if not isinstance(s, basestring,):
+            if hasattr(s, '__unicode__'):
+                s = unicode(s)
+            else:
+                try:
+                    s = unicode(str(s), encoding, errors)
+                except UnicodeEncodeError:
+                    if not isinstance(s, Exception):
+                        raise
+                    # If we get to here, the caller has passed in an Exception
+                    # subclass populated with non-ASCII data without special
+                    # handling to display as a string. We need to handle this
+                    # without raising a further exception. We do an
+                    # approximation to what the Exception's standard str()
+                    # output should be.
+                    s = ' '.join([force_unicode(arg, encoding, strings_only,
+                            errors) for arg in s])
+        elif not isinstance(s, unicode):
+            # Note: We use .decode() here, instead of unicode(s, encoding,
+            # errors), so that if s is a SafeString, it ends up being a
+            # SafeUnicode at the end.
+            s = s.decode(encoding, errors)
+    except UnicodeDecodeError, e:
+        if not isinstance(s, Exception):
+            raise DjangoUnicodeDecodeError(s, *e.args)
+        else:
+            # If we get to here, the caller has passed in an Exception
+            # subclass populated with non-ASCII bytestring data without a
+            # working unicode method. Try to handle this without raising a
+            # further exception by individually forcing the exception args
+            # to unicode.
+            s = ' '.join([force_unicode(arg, encoding, strings_only,
+                    errors) for arg in s])
+    return s
+
+def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
+    """
+    Returns a bytestring version of 's', encoded as specified in 'encoding'.
+
+    If strings_only is True, don't convert (some) non-string-like objects.
+    """
+    if strings_only and isinstance(s, (types.NoneType, int)):
+        return s
+    elif not isinstance(s, basestring):
+        try:
+            return str(s)
+        except UnicodeEncodeError:
+            if isinstance(s, Exception):
+                # An Exception subclass containing non-ASCII data that doesn't
+                # know how to print itself properly. We shouldn't raise a
+                # further exception.
+                return ' '.join([smart_str(arg, encoding, strings_only,
+                        errors) for arg in s])
+            return unicode(s).encode(encoding, errors)
+    elif isinstance(s, unicode):
+        return s.encode(encoding, errors)
+    elif s and encoding != 'utf-8':
+        return s.decode('utf-8', errors).encode(encoding, errors)
+    else:
+        return s
+
+def iri_to_uri(iri):
+    """
+    Convert an Internationalized Resource Identifier (IRI) portion to a URI
+    portion that is suitable for inclusion in a URL.
+
+    This is the algorithm from section 3.1 of RFC 3987.  However, since we are
+    assuming input is either UTF-8 or unicode already, we can simplify things a
+    little from the full method.
+
+    Returns an ASCII string containing the encoded result.
+    """
+    # The list of safe characters here is constructed from the "reserved" and
+    # "unreserved" characters specified in sections 2.2 and 2.3 of RFC 3986:
+    #     reserved    = gen-delims / sub-delims
+    #     gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
+    #     sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
+    #                   / "*" / "+" / "," / ";" / "="
+    #     unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
+    # Of the unreserved characters, urllib.quote already considers all but
+    # the ~ safe.
+    # The % character is also added to the list of safe characters here, as the
+    # end of section 3.1 of RFC 3987 specifically mentions that % must not be
+    # converted.
+    if iri is None:
+        return iri
+    return urllib.quote(smart_str(iri), safe="/#%[]=:;$&()+,!?*@'~")
+
+
+# The encoding of the default system locale but falls back to the
+# given fallback encoding if the encoding is unsupported by python or could
+# not be determined.  See tickets #10335 and #5846
+try:
+    DEFAULT_LOCALE_ENCODING = locale.getdefaultlocale()[1] or 'ascii'
+    codecs.lookup(DEFAULT_LOCALE_ENCODING)
+except:
+    DEFAULT_LOCALE_ENCODING = 'ascii'
+
diff --git a/sphinx/sphinxext/feed/feedgenerator.py b/sphinx/sphinxext/feed/feedgenerator.py
new file mode 100644 (file)
index 0000000..de14b62
--- /dev/null
@@ -0,0 +1,351 @@
+"""
+Syndication feed generation library -- used for generating RSS, etc.
+Included from django http://djangoproject.org/
+
+Sample usage:
+
+>>> from django.utils import feedgenerator
+>>> feed = feedgenerator.Rss201rev2Feed(
+...     title=u"Poynter E-Media Tidbits",
+...     link=u"http://www.poynter.org/column.asp?id=31",
+...     description=u"A group weblog by the sharpest minds in online media/journalism/publishing.",
+...     language=u"en",
+... )
+>>> feed.add_item(title="Hello", link=u"http://www.holovaty.com/test/", description="Testing.")
+>>> fp = open('test.rss', 'w')
+>>> feed.write(fp, 'utf-8')
+>>> fp.close()
+
+For definitions of the different versions of RSS, see:
+http://diveintomark.org/archives/2004/02/04/incompatible-rss
+"""
+
+import re
+import datetime
+from django_support import SimplerXMLGenerator, iri_to_uri, force_unicode
+
+def rfc2822_date(date):
+    # We do this ourselves to be timezone aware, email.Utils is not tz aware.
+    if date.tzinfo:
+        time_str = date.strftime('%a, %d %b %Y %H:%M:%S ')
+        offset = date.tzinfo.utcoffset(date)
+        timezone = (offset.days * 24 * 60) + (offset.seconds / 60)
+        hour, minute = divmod(timezone, 60)
+        return time_str + "%+03d%02d" % (hour, minute)
+    else:
+        return date.strftime('%a, %d %b %Y %H:%M:%S -0000')
+
+def rfc3339_date(date):
+    if date.tzinfo:
+        time_str = date.strftime('%Y-%m-%dT%H:%M:%S')
+        offset = date.tzinfo.utcoffset(date)
+        timezone = (offset.days * 24 * 60) + (offset.seconds / 60)
+        hour, minute = divmod(timezone, 60)
+        return time_str + "%+03d:%02d" % (hour, minute)
+    else:
+        return date.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+def get_tag_uri(url, date):
+    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
+    tag = re.sub('^http://', '', url)
+    if date is not None:
+        tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
+    tag = re.sub('#', '/', tag)
+    return u'tag:' + tag
+
+class SyndicationFeed(object):
+    "Base class for all syndication feeds. Subclasses should provide write()"
+    def __init__(self, title, link, description, language=None, author_email=None,
+            author_name=None, author_link=None, subtitle=None, categories=None,
+            feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs):
+        to_unicode = lambda s: force_unicode(s, strings_only=True)
+        if categories:
+            categories = [force_unicode(c) for c in categories]
+        self.feed = {
+            'title': to_unicode(title),
+            'link': iri_to_uri(link),
+            'description': to_unicode(description),
+            'language': to_unicode(language),
+            'author_email': to_unicode(author_email),
+            'author_name': to_unicode(author_name),
+            'author_link': iri_to_uri(author_link),
+            'subtitle': to_unicode(subtitle),
+            'categories': categories or (),
+            'feed_url': iri_to_uri(feed_url),
+            'feed_copyright': to_unicode(feed_copyright),
+            'id': feed_guid or link,
+            'ttl': ttl,
+        }
+        self.feed.update(kwargs)
+        self.items = []
+
+    def add_item(self, title, link, description, author_email=None,
+        author_name=None, author_link=None, pubdate=None, comments=None,
+        unique_id=None, enclosure=None, categories=(), item_copyright=None,
+        ttl=None, **kwargs):
+        """
+        Adds an item to the feed. All args are expected to be Python Unicode
+        objects except pubdate, which is a datetime.datetime object, and
+        enclosure, which is an instance of the Enclosure class.
+        """
+        to_unicode = lambda s: force_unicode(s, strings_only=True)
+        if categories:
+            categories = [to_unicode(c) for c in categories]
+        item = {
+            'title': to_unicode(title),
+            'link': iri_to_uri(link),
+            'description': to_unicode(description),
+            'author_email': to_unicode(author_email),
+            'author_name': to_unicode(author_name),
+            'author_link': iri_to_uri(author_link),
+            'pubdate': pubdate,
+            'comments': to_unicode(comments),
+            'unique_id': to_unicode(unique_id),
+            'enclosure': enclosure,
+            'categories': categories or (),
+            'item_copyright': to_unicode(item_copyright),
+            'ttl': ttl,
+        }
+        item.update(kwargs)
+        self.items.append(item)
+
+    def num_items(self):
+        return len(self.items)
+
+    def root_attributes(self):
+        """
+        Return extra attributes to place on the root (i.e. feed/channel) element.
+        Called from write().
+        """
+        return {}
+
+    def add_root_elements(self, handler):
+        """
+        Add elements in the root (i.e. feed/channel) element. Called
+        from write().
+        """
+        pass
+
+    def item_attributes(self, item):
+        """
+        Return extra attributes to place on each item (i.e. item/entry) element.
+        """
+        return {}
+
+    def add_item_elements(self, handler, item):
+        """
+        Add elements on each item (i.e. item/entry) element.
+        """
+        pass
+
+    def write(self, outfile, encoding):
+        """
+        Outputs the feed in the given encoding to outfile, which is a file-like
+        object. Subclasses should override this.
+        """
+        raise NotImplementedError
+
+    def writeString(self, encoding):
+        """
+        Returns the feed in the given encoding as a string.
+        """
+        from StringIO import StringIO
+        s = StringIO()
+        self.write(s, encoding)
+        return s.getvalue()
+
+    def latest_post_date(self):
+        """
+        Returns the latest item's pubdate. If none of them have a pubdate,
+        this returns the current date/time.
+        """
+        updates = [i['pubdate'] for i in self.items if i['pubdate'] is not None]
+        if len(updates) > 0:
+            updates.sort()
+            return updates[-1]
+        else:
+            return datetime.datetime.now()
+
+class Enclosure(object):
+    "Represents an RSS enclosure"
+    def __init__(self, url, length, mime_type):
+        "All args are expected to be Python Unicode objects"
+        self.length, self.mime_type = length, mime_type
+        self.url = iri_to_uri(url)
+
+class RssFeed(SyndicationFeed):
+    mime_type = 'application/rss+xml'
+    def write(self, outfile, encoding):
+        handler = SimplerXMLGenerator(outfile, encoding)
+        handler.startDocument()
+        handler.startElement(u"rss", self.rss_attributes())
+        handler.startElement(u"channel", self.root_attributes())
+        self.add_root_elements(handler)
+        self.write_items(handler)
+        self.endChannelElement(handler)
+        handler.endElement(u"rss")
+
+    def rss_attributes(self):
+        return {u"version": self._version}
+
+    def write_items(self, handler):
+        for item in self.items:
+            handler.startElement(u'item', self.item_attributes(item))
+            self.add_item_elements(handler, item)
+            handler.endElement(u"item")
+
+    def add_root_elements(self, handler):
+        handler.addQuickElement(u"title", self.feed['title'])
+        handler.addQuickElement(u"link", self.feed['link'])
+        handler.addQuickElement(u"description", self.feed['description'])
+        if self.feed['language'] is not None:
+            handler.addQuickElement(u"language", self.feed['language'])
+        for cat in self.feed['categories']:
+            handler.addQuickElement(u"category", cat)
+        if self.feed['feed_copyright'] is not None:
+            handler.addQuickElement(u"copyright", self.feed['feed_copyright'])
+        handler.addQuickElement(u"lastBuildDate", rfc2822_date(self.latest_post_date()).decode('utf-8'))
+        if self.feed['ttl'] is not None:
+            handler.addQuickElement(u"ttl", self.feed['ttl'])
+
+    def endChannelElement(self, handler):
+        handler.endElement(u"channel")
+
+class RssUserland091Feed(RssFeed):
+    _version = u"0.91"
+    def add_item_elements(self, handler, item):
+        handler.addQuickElement(u"title", item['title'])
+        handler.addQuickElement(u"link", item['link'])
+        if item['description'] is not None:
+            handler.addQuickElement(u"description", item['description'])
+
+class Rss201rev2Feed(RssFeed):
+    # Spec: http://blogs.law.harvard.edu/tech/rss
+    _version = u"2.0"
+    def add_item_elements(self, handler, item):
+        handler.addQuickElement(u"title", item['title'])
+        handler.addQuickElement(u"link", item['link'])
+        if item['description'] is not None:
+            handler.addQuickElement(u"description", item['description'])
+
+        # Author information.
+        if item["author_name"] and item["author_email"]:
+            handler.addQuickElement(u"author", "%s (%s)" % \
+                (item['author_email'], item['author_name']))
+        elif item["author_email"]:
+            handler.addQuickElement(u"author", item["author_email"])
+        elif item["author_name"]:
+            handler.addQuickElement(u"dc:creator", item["author_name"], {"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
+
+        if item['pubdate'] is not None:
+            handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8'))
+        if item['comments'] is not None:
+            handler.addQuickElement(u"comments", item['comments'])
+        if item['unique_id'] is not None:
+            handler.addQuickElement(u"guid", item['unique_id'])
+        if item['ttl'] is not None:
+            handler.addQuickElement(u"ttl", item['ttl'])
+
+        # Enclosure.
+        if item['enclosure'] is not None:
+            handler.addQuickElement(u"enclosure", '',
+                {u"url": item['enclosure'].url, u"length": item['enclosure'].length,
+                    u"type": item['enclosure'].mime_type})
+
+        # Categories.
+        for cat in item['categories']:
+            handler.addQuickElement(u"category", cat)
+
+class Atom1Feed(SyndicationFeed):
+    # Spec: http://atompub.org/2005/07/11/draft-ietf-atompub-format-10.html
+    mime_type = 'application/atom+xml'
+    ns = u"http://www.w3.org/2005/Atom"
+
+    def write(self, outfile, encoding):
+        handler = SimplerXMLGenerator(outfile, encoding)
+        handler.startDocument()
+        handler.startElement(u'feed', self.root_attributes())
+        self.add_root_elements(handler)
+        self.write_items(handler)
+        handler.endElement(u"feed")
+
+    def root_attributes(self):
+        if self.feed['language'] is not None:
+            return {u"xmlns": self.ns, u"xml:lang": self.feed['language']}
+        else:
+            return {u"xmlns": self.ns}
+
+    def add_root_elements(self, handler):
+        handler.addQuickElement(u"title", self.feed['title'])
+        handler.addQuickElement(u"link", "", {u"rel": u"alternate", u"href": self.feed['link']})
+        if self.feed['feed_url'] is not None:
+            handler.addQuickElement(u"link", "", {u"rel": u"self", u"href": self.feed['feed_url']})
+        handler.addQuickElement(u"id", self.feed['id'])
+        handler.addQuickElement(u"updated", rfc3339_date(self.latest_post_date()).decode('utf-8'))
+        if self.feed['author_name'] is not None:
+            handler.startElement(u"author", {})
+            handler.addQuickElement(u"name", self.feed['author_name'])
+            if self.feed['author_email'] is not None:
+                handler.addQuickElement(u"email", self.feed['author_email'])
+            if self.feed['author_link'] is not None:
+                handler.addQuickElement(u"uri", self.feed['author_link'])
+            handler.endElement(u"author")
+        if self.feed['subtitle'] is not None:
+            handler.addQuickElement(u"subtitle", self.feed['subtitle'])
+        for cat in self.feed['categories']:
+            handler.addQuickElement(u"category", "", {u"term": cat})
+        if self.feed['feed_copyright'] is not None:
+            handler.addQuickElement(u"rights", self.feed['feed_copyright'])
+
+    def write_items(self, handler):
+        for item in self.items:
+            handler.startElement(u"entry", self.item_attributes(item))
+            self.add_item_elements(handler, item)
+            handler.endElement(u"entry")
+
+    def add_item_elements(self, handler, item):
+        handler.addQuickElement(u"title", item['title'])
+        handler.addQuickElement(u"link", u"", {u"href": item['link'], u"rel": u"alternate"})
+        if item['pubdate'] is not None:
+            handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('utf-8'))
+
+        # Author information.
+        if item['author_name'] is not None:
+            handler.startElement(u"author", {})
+            handler.addQuickElement(u"name", item['author_name'])
+            if item['author_email'] is not None:
+                handler.addQuickElement(u"email", item['author_email'])
+            if item['author_link'] is not None:
+                handler.addQuickElement(u"uri", item['author_link'])
+            handler.endElement(u"author")
+
+        # Unique ID.
+        if item['unique_id'] is not None:
+            unique_id = item['unique_id']
+        else:
+            unique_id = get_tag_uri(item['link'], item['pubdate'])
+        handler.addQuickElement(u"id", unique_id)
+
+        # Summary.
+        if item['description'] is not None:
+            handler.addQuickElement(u"summary", item['description'], {u"type": u"html"})
+
+        # Enclosure.
+        if item['enclosure'] is not None:
+            handler.addQuickElement(u"link", '',
+                {u"rel": u"enclosure",
+                 u"href": item['enclosure'].url,
+                 u"length": item['enclosure'].length,
+                 u"type": item['enclosure'].mime_type})
+
+        # Categories.
+        for cat in item['categories']:
+            handler.addQuickElement(u"category", u"", {u"term": cat})
+
+        # Rights.
+        if item['item_copyright'] is not None:
+            handler.addQuickElement(u"rights", item['item_copyright'])
+
+# This isolates the decision of what the system default is, so calling code can
+# do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
+DefaultFeed = Rss201rev2Feed
diff --git a/sphinx/sphinxext/feed/fsdict.py b/sphinx/sphinxext/feed/fsdict.py
new file mode 100644 (file)
index 0000000..04c9b84
--- /dev/null
@@ -0,0 +1,110 @@
+# âˆ’*− coding: UTF−8 âˆ’*−
+from path import path
+import os
+import pickle
+"""
+A class providing dictionary access to a folder.
+cribbed from http://bitbucket.org/howthebodyworks/fsdict
+"""
+
+def get_tmp_dir():
+    import tempfile
+    return tempfile.mkdtemp()
+    
+class FSDict(dict):
+    """
+    provide dictionary access to a temp dir. I don't know why i didn't just use
+    shelve. I think I forgot it existed.
+    
+    N.B. the keys ordering here is FS-dependent and thus unlike to be the same as
+    with a real dict. beware.
+    """
+    
+    unclean_dirs = []
+    
+    def __init__(self, initval=[], work_dir=None, *args, **kwargs):
+        if work_dir is None:
+            work_dir = get_tmp_dir()
+        self.work_dir = path(work_dir)
+        if not self.work_dir.exists():
+            self.work_dir.mkdir()
+        for key, val in getattr(initval, 'iteritems', initval.__iter__)():
+            self[key] = val
+        self.unclean_dirs.append(self.work_dir)
+        super(FSDict, self).__init__(*args, **kwargs)
+    
+    def __setitem__(self, key, val, *args, **kwargs):
+        pickle.dump(val, open(self.work_dir/key, 'w'))
+    
+    def __getitem__(self, key, *args, **kwargs):
+        return pickle.load(open(self.work_dir/key, 'r'))
+    
+    def __repr__(self):
+        """
+        a hardline list of everything in the dict. may be long.
+        """
+        return repr(dict([(k, v) for k, v in self.iteritems()]))
+        
+    def __str__(self):
+        """
+        str is truncated somewhat.
+        """
+        if len(self.keys()):
+            return '{' + repr(self.keys()[0]) + ':' + repr(self[self.keys()[0]]) + ', ...'
+        else:
+            return super(FSDict, self).__str__()
+    
+    def keys(self, *args, **kwargs):
+        return [key for key in self.iterkeys()]
+    
+    def iterkeys(self, *args, **kwargs):
+        for f in self.work_dir.files():
+            yield str(self.work_dir.relpathto(f))
+        
+    def iteritems(self):
+        for key in self.iterkeys():
+            yield key, self[key]
+            
+    def itervalues(self):
+        for key in self.iterkeys():
+            yield self[key]
+            
+    def __delitem__(self, key, *args, **kwargs):
+        (self.work_dir/key).unlink()
+    
+    def values(self, *args, **kwargs):
+        return [self[key] for key in self.keys()]
+        
+    def cleanup(self):
+        self.work_dir.rmtree()
+    
+    @classmethod
+    def cleanup_all(cls):
+        for fsd in cls.unclean_dirs:
+            try:
+                fsd.rmtree()
+            except OSError:
+                pass
+    
+    def move(self, new_dir):
+        
+        try:
+            self.work_dir.move(new_dir)
+        except Exception, e:
+            raise
+        else:
+            self.work_dir = new_dir
+    
+    def __eq__(self, other):
+        """
+        when compared to a dict, equate equal if all keys and vals are equal
+        note, this is potentially expensive.
+        """
+        #duck type our way to sanity:
+        if not hasattr(other, 'keys'): return False
+        #OK, it's a dict-ish thing
+        try:
+            return all([self[key]==other[key] for key in other]) and \
+              len(self.keys())==len(other.keys())
+        except KeyError:
+            return False
\ No newline at end of file
diff --git a/sphinx/sphinxext/feed/path.py b/sphinx/sphinxext/feed/path.py
new file mode 100644 (file)
index 0000000..3652963
--- /dev/null
@@ -0,0 +1,970 @@
+""" path.py - An object representing a path to a file or directory.\r
+\r
+Example:\r
+\r
+from path import path\r
+d = path('/home/guido/bin')\r
+for f in d.files('*.py'):\r
+    f.chmod(0755)\r
+\r
+This module requires Python 2.2 or later.\r
+\r
+\r
+URL:     http://www.jorendorff.com/articles/python/path\r
+Author:  Jason Orendorff <jason.orendorff\x40gmail\x2ecom> (and others - see the url!)\r
+Date:    9 Mar 2007\r
+"""\r
+\r
+\r
+# TODO\r
+#   - Tree-walking functions don't avoid symlink loops.  Matt Harrison\r
+#     sent me a patch for this.\r
+#   - Bug in write_text().  It doesn't support Universal newline mode.\r
+#   - Better error message in listdir() when self isn't a\r
+#     directory. (On Windows, the error message really sucks.)\r
+#   - Make sure everything has a good docstring.\r
+#   - Add methods for regex find and replace.\r
+#   - guess_content_type() method?\r
+#   - Perhaps support arguments to touch().\r
+\r
+from __future__ import generators\r
+\r
+import sys, warnings, os, fnmatch, glob, shutil, codecs, md5\r
+\r
+__version__ = '2.2'\r
+__all__ = ['path']\r
+\r
+# Platform-specific support for path.owner\r
+if os.name == 'nt':\r
+    try:\r
+        import win32security\r
+    except ImportError:\r
+        win32security = None\r
+else:\r
+    try:\r
+        import pwd\r
+    except ImportError:\r
+        pwd = None\r
+\r
+# Pre-2.3 support.  Are unicode filenames supported?\r
+_base = str\r
+_getcwd = os.getcwd\r
+try:\r
+    if os.path.supports_unicode_filenames:\r
+        _base = unicode\r
+        _getcwd = os.getcwdu\r
+except AttributeError:\r
+    pass\r
+\r
+# Pre-2.3 workaround for booleans\r
+try:\r
+    True, False\r
+except NameError:\r
+    True, False = 1, 0\r
+\r
+# Pre-2.3 workaround for basestring.\r
+try:\r
+    basestring\r
+except NameError:\r
+    basestring = (str, unicode)\r
+\r
+# Universal newline support\r
+_textmode = 'r'\r
+if hasattr(file, 'newlines'):\r
+    _textmode = 'U'\r
+\r
+\r
+class TreeWalkWarning(Warning):\r
+    pass\r
+\r
+class path(_base):\r
+    """ Represents a filesystem path.\r
+\r
+    For documentation on individual methods, consult their\r
+    counterparts in os.path.\r
+    """\r
+\r
+    # --- Special Python methods.\r
+\r
+    def __repr__(self):\r
+        return 'path(%s)' % _base.__repr__(self)\r
+\r
+    # Adding a path and a string yields a path.\r
+    def __add__(self, more):\r
+        try:\r
+            resultStr = _base.__add__(self, more)\r
+        except TypeError:  #Python bug\r
+            resultStr = NotImplemented\r
+        if resultStr is NotImplemented:\r
+            return resultStr\r
+        return self.__class__(resultStr)\r
+\r
+    def __radd__(self, other):\r
+        if isinstance(other, basestring):\r
+            return self.__class__(other.__add__(self))\r
+        else:\r
+            return NotImplemented\r
+\r
+    # The / operator joins paths.\r
+    def __div__(self, rel):\r
+        """ fp.__div__(rel) == fp / rel == fp.joinpath(rel)\r
+\r
+        Join two path components, adding a separator character if\r
+        needed.\r
+        """\r
+        return self.__class__(os.path.join(self, rel))\r
+\r
+    # Make the / operator work even when true division is enabled.\r
+    __truediv__ = __div__\r
+\r
+    def getcwd(cls):\r
+        """ Return the current working directory as a path object. """\r
+        return cls(_getcwd())\r
+    getcwd = classmethod(getcwd)\r
+\r
+\r
+    # --- Operations on path strings.\r
+\r
+    isabs = os.path.isabs\r
+    def abspath(self):       return self.__class__(os.path.abspath(self))\r
+    def normcase(self):      return self.__class__(os.path.normcase(self))\r
+    def normpath(self):      return self.__class__(os.path.normpath(self))\r
+    def realpath(self):      return self.__class__(os.path.realpath(self))\r
+    def expanduser(self):    return self.__class__(os.path.expanduser(self))\r
+    def expandvars(self):    return self.__class__(os.path.expandvars(self))\r
+    def dirname(self):       return self.__class__(os.path.dirname(self))\r
+    basename = os.path.basename\r
+\r
+    def expand(self):\r
+        """ Clean up a filename by calling expandvars(),\r
+        expanduser(), and normpath() on it.\r
+\r
+        This is commonly everything needed to clean up a filename\r
+        read from a configuration file, for example.\r
+        """\r
+        return self.expandvars().expanduser().normpath()\r
+\r
+    def _get_namebase(self):\r
+        base, ext = os.path.splitext(self.name)\r
+        return base\r
+\r
+    def _get_ext(self):\r
+        f, ext = os.path.splitext(_base(self))\r
+        return ext\r
+\r
+    def _get_drive(self):\r
+        drive, r = os.path.splitdrive(self)\r
+        return self.__class__(drive)\r
+\r
+    parent = property(\r
+        dirname, None, None,\r
+        """ This path's parent directory, as a new path object.\r
+\r
+        For example, path('/usr/local/lib/libpython.so').parent == path('/usr/local/lib')\r
+        """)\r
+\r
+    name = property(\r
+        basename, None, None,\r
+        """ The name of this file or directory without the full path.\r
+\r
+        For example, path('/usr/local/lib/libpython.so').name == 'libpython.so'\r
+        """)\r
+\r
+    namebase = property(\r
+        _get_namebase, None, None,\r
+        """ The same as path.name, but with one file extension stripped off.\r
+\r
+        For example, path('/home/guido/python.tar.gz').name     == 'python.tar.gz',\r
+        but          path('/home/guido/python.tar.gz').namebase == 'python.tar'\r
+        """)\r
+\r
+    ext = property(\r
+        _get_ext, None, None,\r
+        """ The file extension, for example '.py'. """)\r
+\r
+    drive = property(\r
+        _get_drive, None, None,\r
+        """ The drive specifier, for example 'C:'.\r
+        This is always empty on systems that don't use drive specifiers.\r
+        """)\r
+\r
+    def splitpath(self):\r
+        """ p.splitpath() -> Return (p.parent, p.name). """\r
+        parent, child = os.path.split(self)\r
+        return self.__class__(parent), child\r
+\r
+    def splitdrive(self):\r
+        """ p.splitdrive() -> Return (p.drive, <the rest of p>).\r
+\r
+        Split the drive specifier from this path.  If there is\r
+        no drive specifier, p.drive is empty, so the return value\r
+        is simply (path(''), p).  This is always the case on Unix.\r
+        """\r
+        drive, rel = os.path.splitdrive(self)\r
+        return self.__class__(drive), rel\r
+\r
+    def splitext(self):\r
+        """ p.splitext() -> Return (p.stripext(), p.ext).\r
+\r
+        Split the filename extension from this path and return\r
+        the two parts.  Either part may be empty.\r
+\r
+        The extension is everything from '.' to the end of the\r
+        last path segment.  This has the property that if\r
+        (a, b) == p.splitext(), then a + b == p.\r
+        """\r
+        filename, ext = os.path.splitext(self)\r
+        return self.__class__(filename), ext\r
+\r
+    def stripext(self):\r
+        """ p.stripext() -> Remove one file extension from the path.\r
+\r
+        For example, path('/home/guido/python.tar.gz').stripext()\r
+        returns path('/home/guido/python.tar').\r
+        """\r
+        return self.splitext()[0]\r
+\r
+    if hasattr(os.path, 'splitunc'):\r
+        def splitunc(self):\r
+            unc, rest = os.path.splitunc(self)\r
+            return self.__class__(unc), rest\r
+\r
+        def _get_uncshare(self):\r
+            unc, r = os.path.splitunc(self)\r
+            return self.__class__(unc)\r
+\r
+        uncshare = property(\r
+            _get_uncshare, None, None,\r
+            """ The UNC mount point for this path.\r
+            This is empty for paths on local drives. """)\r
+\r
+    def joinpath(self, *args):\r
+        """ Join two or more path components, adding a separator\r
+        character (os.sep) if needed.  Returns a new path\r
+        object.\r
+        """\r
+        return self.__class__(os.path.join(self, *args))\r
+\r
+    def splitall(self):\r
+        r""" Return a list of the path components in this path.\r
+\r
+        The first item in the list will be a path.  Its value will be\r
+        either os.curdir, os.pardir, empty, or the root directory of\r
+        this path (for example, '/' or 'C:\\').  The other items in\r
+        the list will be strings.\r
+\r
+        path.path.joinpath(*result) will yield the original path.\r
+        """\r
+        parts = []\r
+        loc = self\r
+        while loc != os.curdir and loc != os.pardir:\r
+            prev = loc\r
+            loc, child = prev.splitpath()\r
+            if loc == prev:\r
+                break\r
+            parts.append(child)\r
+        parts.append(loc)\r
+        parts.reverse()\r
+        return parts\r
+\r
+    def relpath(self):\r
+        """ Return this path as a relative path,\r
+        based from the current working directory.\r
+        """\r
+        cwd = self.__class__(os.getcwd())\r
+        return cwd.relpathto(self)\r
+\r
+    def relpathto(self, dest):\r
+        """ Return a relative path from self to dest.\r
+\r
+        If there is no relative path from self to dest, for example if\r
+        they reside on different drives in Windows, then this returns\r
+        dest.abspath().\r
+        """\r
+        origin = self.abspath()\r
+        dest = self.__class__(dest).abspath()\r
+\r
+        orig_list = origin.normcase().splitall()\r
+        # Don't normcase dest!  We want to preserve the case.\r
+        dest_list = dest.splitall()\r
+\r
+        if orig_list[0] != os.path.normcase(dest_list[0]):\r
+            # Can't get here from there.\r
+            return dest\r
+\r
+        # Find the location where the two paths start to differ.\r
+        i = 0\r
+        for start_seg, dest_seg in zip(orig_list, dest_list):\r
+            if start_seg != os.path.normcase(dest_seg):\r
+                break\r
+            i += 1\r
+\r
+        # Now i is the point where the two paths diverge.\r
+        # Need a certain number of "os.pardir"s to work up\r
+        # from the origin to the point of divergence.\r
+        segments = [os.pardir] * (len(orig_list) - i)\r
+        # Need to add the diverging part of dest_list.\r
+        segments += dest_list[i:]\r
+        if len(segments) == 0:\r
+            # If they happen to be identical, use os.curdir.\r
+            relpath = os.curdir\r
+        else:\r
+            relpath = os.path.join(*segments)\r
+        return self.__class__(relpath)\r
+\r
+    # --- Listing, searching, walking, and matching\r
+\r
+    def listdir(self, pattern=None):\r
+        """ D.listdir() -> List of items in this directory.\r
+\r
+        Use D.files() or D.dirs() instead if you want a listing\r
+        of just files or just subdirectories.\r
+\r
+        The elements of the list are path objects.\r
+\r
+        With the optional 'pattern' argument, this only lists\r
+        items whose names match the given pattern.\r
+        """\r
+        names = os.listdir(self)\r
+        if pattern is not None:\r
+            names = fnmatch.filter(names, pattern)\r
+        return [self / child for child in names]\r
+\r
+    def dirs(self, pattern=None):\r
+        """ D.dirs() -> List of this directory's subdirectories.\r
+\r
+        The elements of the list are path objects.\r
+        This does not walk recursively into subdirectories\r
+        (but see path.walkdirs).\r
+\r
+        With the optional 'pattern' argument, this only lists\r
+        directories whose names match the given pattern.  For\r
+        example, d.dirs('build-*').\r
+        """\r
+        return [p for p in self.listdir(pattern) if p.isdir()]\r
+\r
+    def files(self, pattern=None):\r
+        """ D.files() -> List of the files in this directory.\r
+\r
+        The elements of the list are path objects.\r
+        This does not walk into subdirectories (see path.walkfiles).\r
+\r
+        With the optional 'pattern' argument, this only lists files\r
+        whose names match the given pattern.  For example,\r
+        d.files('*.pyc').\r
+        """\r
+        \r
+        return [p for p in self.listdir(pattern) if p.isfile()]\r
+\r
+    def walk(self, pattern=None, errors='strict'):\r
+        """ D.walk() -> iterator over files and subdirs, recursively.\r
+\r
+        The iterator yields path objects naming each child item of\r
+        this directory and its descendants.  This requires that\r
+        D.isdir().\r
+\r
+        This performs a depth-first traversal of the directory tree.\r
+        Each directory is returned just before all its children.\r
+\r
+        The errors= keyword argument controls behavior when an\r
+        error occurs.  The default is 'strict', which causes an\r
+        exception.  The other allowed values are 'warn', which\r
+        reports the error via warnings.warn(), and 'ignore'.\r
+        """\r
+        if errors not in ('strict', 'warn', 'ignore'):\r
+            raise ValueError("invalid errors parameter")\r
+\r
+        try:\r
+            childList = self.listdir()\r
+        except Exception:\r
+            if errors == 'ignore':\r
+                return\r
+            elif errors == 'warn':\r
+                warnings.warn(\r
+                    "Unable to list directory '%s': %s"\r
+                    % (self, sys.exc_info()[1]),\r
+                    TreeWalkWarning)\r
+                return\r
+            else:\r
+                raise\r
+\r
+        for child in childList:\r
+            if pattern is None or child.fnmatch(pattern):\r
+                yield child\r
+            try:\r
+                isdir = child.isdir()\r
+            except Exception:\r
+                if errors == 'ignore':\r
+                    isdir = False\r
+                elif errors == 'warn':\r
+                    warnings.warn(\r
+                        "Unable to access '%s': %s"\r
+                        % (child, sys.exc_info()[1]),\r
+                        TreeWalkWarning)\r
+                    isdir = False\r
+                else:\r
+                    raise\r
+\r
+            if isdir:\r
+                for item in child.walk(pattern, errors):\r
+                    yield item\r
+\r
+    def walkdirs(self, pattern=None, errors='strict'):\r
+        """ D.walkdirs() -> iterator over subdirs, recursively.\r
+\r
+        With the optional 'pattern' argument, this yields only\r
+        directories whose names match the given pattern.  For\r
+        example, mydir.walkdirs('*test') yields only directories\r
+        with names ending in 'test'.\r
+\r
+        The errors= keyword argument controls behavior when an\r
+        error occurs.  The default is 'strict', which causes an\r
+        exception.  The other allowed values are 'warn', which\r
+        reports the error via warnings.warn(), and 'ignore'.\r
+        """\r
+        if errors not in ('strict', 'warn', 'ignore'):\r
+            raise ValueError("invalid errors parameter")\r
+\r
+        try:\r
+            dirs = self.dirs()\r
+        except Exception:\r
+            if errors == 'ignore':\r
+                return\r
+            elif errors == 'warn':\r
+                warnings.warn(\r
+                    "Unable to list directory '%s': %s"\r
+                    % (self, sys.exc_info()[1]),\r
+                    TreeWalkWarning)\r
+                return\r
+            else:\r
+                raise\r
+\r
+        for child in dirs:\r
+            if pattern is None or child.fnmatch(pattern):\r
+                yield child\r
+            for subsubdir in child.walkdirs(pattern, errors):\r
+                yield subsubdir\r
+\r
+    def walkfiles(self, pattern=None, errors='strict'):\r
+        """ D.walkfiles() -> iterator over files in D, recursively.\r
+\r
+        The optional argument, pattern, limits the results to files\r
+        with names that match the pattern.  For example,\r
+        mydir.walkfiles('*.tmp') yields only files with the .tmp\r
+        extension.\r
+        """\r
+        if errors not in ('strict', 'warn', 'ignore'):\r
+            raise ValueError("invalid errors parameter")\r
+\r
+        try:\r
+            childList = self.listdir()\r
+        except Exception:\r
+            if errors == 'ignore':\r
+                return\r
+            elif errors == 'warn':\r
+                warnings.warn(\r
+                    "Unable to list directory '%s': %s"\r
+                    % (self, sys.exc_info()[1]),\r
+                    TreeWalkWarning)\r
+                return\r
+            else:\r
+                raise\r
+\r
+        for child in childList:\r
+            try:\r
+                isfile = child.isfile()\r
+                isdir = not isfile and child.isdir()\r
+            except:\r
+                if errors == 'ignore':\r
+                    continue\r
+                elif errors == 'warn':\r
+                    warnings.warn(\r
+                        "Unable to access '%s': %s"\r
+                        % (self, sys.exc_info()[1]),\r
+                        TreeWalkWarning)\r
+                    continue\r
+                else:\r
+                    raise\r
+\r
+            if isfile:\r
+                if pattern is None or child.fnmatch(pattern):\r
+                    yield child\r
+            elif isdir:\r
+                for f in child.walkfiles(pattern, errors):\r
+                    yield f\r
+\r
+    def fnmatch(self, pattern):\r
+        """ Return True if self.name matches the given pattern.\r
+\r
+        pattern - A filename pattern with wildcards,\r
+            for example '*.py'.\r
+        """\r
+        return fnmatch.fnmatch(self.name, pattern)\r
+\r
+    def glob(self, pattern):\r
+        """ Return a list of path objects that match the pattern.\r
+\r
+        pattern - a path relative to this directory, with wildcards.\r
+\r
+        For example, path('/users').glob('*/bin/*') returns a list\r
+        of all the files users have in their bin directories.\r
+        """\r
+        cls = self.__class__\r
+        return [cls(s) for s in glob.glob(_base(self / pattern))]\r
+\r
+\r
+    # --- Reading or writing an entire file at once.\r
+\r
+    def open(self, mode='r'):\r
+        """ Open this file.  Return a file object. """\r
+        return file(self, mode)\r
+\r
+    def bytes(self):\r
+        """ Open this file, read all bytes, return them as a string. """\r
+        f = self.open('rb')\r
+        try:\r
+            return f.read()\r
+        finally:\r
+            f.close()\r
+\r
+    def write_bytes(self, bytes, append=False):\r
+        """ Open this file and write the given bytes to it.\r
+\r
+        Default behavior is to overwrite any existing file.\r
+        Call p.write_bytes(bytes, append=True) to append instead.\r
+        """\r
+        if append:\r
+            mode = 'ab'\r
+        else:\r
+            mode = 'wb'\r
+        f = self.open(mode)\r
+        try:\r
+            f.write(bytes)\r
+        finally:\r
+            f.close()\r
+\r
+    def text(self, encoding=None, errors='strict'):\r
+        r""" Open this file, read it in, return the content as a string.\r
+\r
+        This uses 'U' mode in Python 2.3 and later, so '\r\n' and '\r'\r
+        are automatically translated to '\n'.\r
+\r
+        Optional arguments:\r
+\r
+        encoding - The Unicode encoding (or character set) of\r
+            the file.  If present, the content of the file is\r
+            decoded and returned as a unicode object; otherwise\r
+            it is returned as an 8-bit str.\r
+        errors - How to handle Unicode errors; see help(str.decode)\r
+            for the options.  Default is 'strict'.\r
+        """\r
+        if encoding is None:\r
+            # 8-bit\r
+            f = self.open(_textmode)\r
+            try:\r
+                return f.read()\r
+            finally:\r
+                f.close()\r
+        else:\r
+            # Unicode\r
+            f = codecs.open(self, 'r', encoding, errors)\r
+            # (Note - Can't use 'U' mode here, since codecs.open\r
+            # doesn't support 'U' mode, even in Python 2.3.)\r
+            try:\r
+                t = f.read()\r
+            finally:\r
+                f.close()\r
+            return (t.replace(u'\r\n', u'\n')\r
+                     .replace(u'\r\x85', u'\n')\r
+                     .replace(u'\r', u'\n')\r
+                     .replace(u'\x85', u'\n')\r
+                     .replace(u'\u2028', u'\n'))\r
+\r
+    def write_text(self, text, encoding=None, errors='strict', linesep=os.linesep, append=False):\r
+        r""" Write the given text to this file.\r
+\r
+        The default behavior is to overwrite any existing file;\r
+        to append instead, use the 'append=True' keyword argument.\r
+\r
+        There are two differences between path.write_text() and\r
+        path.write_bytes(): newline handling and Unicode handling.\r
+        See below.\r
+\r
+        Parameters:\r
+\r
+          - text - str/unicode - The text to be written.\r
+\r
+          - encoding - str - The Unicode encoding that will be used.\r
+            This is ignored if 'text' isn't a Unicode string.\r
+\r
+          - errors - str - How to handle Unicode encoding errors.\r
+            Default is 'strict'.  See help(unicode.encode) for the\r
+            options.  This is ignored if 'text' isn't a Unicode\r
+            string.\r
+\r
+          - linesep - keyword argument - str/unicode - The sequence of\r
+            characters to be used to mark end-of-line.  The default is\r
+            os.linesep.  You can also specify None; this means to\r
+            leave all newlines as they are in 'text'.\r
+\r
+          - append - keyword argument - bool - Specifies what to do if\r
+            the file already exists (True: append to the end of it;\r
+            False: overwrite it.)  The default is False.\r
+\r
+\r
+        --- Newline handling.\r
+\r
+        write_text() converts all standard end-of-line sequences\r
+        ('\n', '\r', and '\r\n') to your platform's default end-of-line\r
+        sequence (see os.linesep; on Windows, for example, the\r
+        end-of-line marker is '\r\n').\r
+\r
+        If you don't like your platform's default, you can override it\r
+        using the 'linesep=' keyword argument.  If you specifically want\r
+        write_text() to preserve the newlines as-is, use 'linesep=None'.\r
+\r
+        This applies to Unicode text the same as to 8-bit text, except\r
+        there are three additional standard Unicode end-of-line sequences:\r
+        u'\x85', u'\r\x85', and u'\u2028'.\r
+\r
+        (This is slightly different from when you open a file for\r
+        writing with fopen(filename, "w") in C or file(filename, 'w')\r
+        in Python.)\r
+\r
+\r
+        --- Unicode\r
+\r
+        If 'text' isn't Unicode, then apart from newline handling, the\r
+        bytes are written verbatim to the file.  The 'encoding' and\r
+        'errors' arguments are not used and must be omitted.\r
+\r
+        If 'text' is Unicode, it is first converted to bytes using the\r
+        specified 'encoding' (or the default encoding if 'encoding'\r
+        isn't specified).  The 'errors' argument applies only to this\r
+        conversion.\r
+\r
+        """\r
+        if isinstance(text, unicode):\r
+            if linesep is not None:\r
+                # Convert all standard end-of-line sequences to\r
+                # ordinary newline characters.\r
+                text = (text.replace(u'\r\n', u'\n')\r
+                            .replace(u'\r\x85', u'\n')\r
+                            .replace(u'\r', u'\n')\r
+                            .replace(u'\x85', u'\n')\r
+                            .replace(u'\u2028', u'\n'))\r
+                text = text.replace(u'\n', linesep)\r
+            if encoding is None:\r
+                encoding = sys.getdefaultencoding()\r
+            bytes = text.encode(encoding, errors)\r
+        else:\r
+            # It is an error to specify an encoding if 'text' is\r
+            # an 8-bit string.\r
+            assert encoding is None\r
+\r
+            if linesep is not None:\r
+                text = (text.replace('\r\n', '\n')\r
+                            .replace('\r', '\n'))\r
+                bytes = text.replace('\n', linesep)\r
+\r
+        self.write_bytes(bytes, append)\r
+\r
+    def lines(self, encoding=None, errors='strict', retain=True):\r
+        r""" Open this file, read all lines, return them in a list.\r
+\r
+        Optional arguments:\r
+            encoding - The Unicode encoding (or character set) of\r
+                the file.  The default is None, meaning the content\r
+                of the file is read as 8-bit characters and returned\r
+                as a list of (non-Unicode) str objects.\r
+            errors - How to handle Unicode errors; see help(str.decode)\r
+                for the options.  Default is 'strict'\r
+            retain - If true, retain newline characters; but all newline\r
+                character combinations ('\r', '\n', '\r\n') are\r
+                translated to '\n'.  If false, newline characters are\r
+                stripped off.  Default is True.\r
+\r
+        This uses 'U' mode in Python 2.3 and later.\r
+        """\r
+        if encoding is None and retain:\r
+            f = self.open(_textmode)\r
+            try:\r
+                return f.readlines()\r
+            finally:\r
+                f.close()\r
+        else:\r
+            return self.text(encoding, errors).splitlines(retain)\r
+\r
+    def write_lines(self, lines, encoding=None, errors='strict',\r
+                    linesep=os.linesep, append=False):\r
+        r""" Write the given lines of text to this file.\r
+\r
+        By default this overwrites any existing file at this path.\r
+\r
+        This puts a platform-specific newline sequence on every line.\r
+        See 'linesep' below.\r
+\r
+        lines - A list of strings.\r
+\r
+        encoding - A Unicode encoding to use.  This applies only if\r
+            'lines' contains any Unicode strings.\r
+\r
+        errors - How to handle errors in Unicode encoding.  This\r
+            also applies only to Unicode strings.\r
+\r
+        linesep - The desired line-ending.  This line-ending is\r
+            applied to every line.  If a line already has any\r
+            standard line ending ('\r', '\n', '\r\n', u'\x85',\r
+            u'\r\x85', u'\u2028'), that will be stripped off and\r
+            this will be used instead.  The default is os.linesep,\r
+            which is platform-dependent ('\r\n' on Windows, '\n' on\r
+            Unix, etc.)  Specify None to write the lines as-is,\r
+            like file.writelines().\r
+\r
+        Use the keyword argument append=True to append lines to the\r
+        file.  The default is to overwrite the file.  Warning:\r
+        When you use this with Unicode data, if the encoding of the\r
+        existing data in the file is different from the encoding\r
+        you specify with the encoding= parameter, the result is\r
+        mixed-encoding data, which can really confuse someone trying\r
+        to read the file later.\r
+        """\r
+        if append:\r
+            mode = 'ab'\r
+        else:\r
+            mode = 'wb'\r
+        f = self.open(mode)\r
+        try:\r
+            for line in lines:\r
+                isUnicode = isinstance(line, unicode)\r
+                if linesep is not None:\r
+                    # Strip off any existing line-end and add the\r
+                    # specified linesep string.\r
+                    if isUnicode:\r
+                        if line[-2:] in (u'\r\n', u'\x0d\x85'):\r
+                            line = line[:-2]\r
+                        elif line[-1:] in (u'\r', u'\n',\r
+                                           u'\x85', u'\u2028'):\r
+                            line = line[:-1]\r
+                    else:\r
+                        if line[-2:] == '\r\n':\r
+                            line = line[:-2]\r
+                        elif line[-1:] in ('\r', '\n'):\r
+                            line = line[:-1]\r
+                    line += linesep\r
+                if isUnicode:\r
+                    if encoding is None:\r
+                        encoding = sys.getdefaultencoding()\r
+                    line = line.encode(encoding, errors)\r
+                f.write(line)\r
+        finally:\r
+            f.close()\r
+\r
+    def read_md5(self):\r
+        """ Calculate the md5 hash for this file.\r
+\r
+        This reads through the entire file.\r
+        """\r
+        f = self.open('rb')\r
+        try:\r
+            m = md5.new()\r
+            while True:\r
+                d = f.read(8192)\r
+                if not d:\r
+                    break\r
+                m.update(d)\r
+        finally:\r
+            f.close()\r
+        return m.digest()\r
+\r
+    # --- Methods for querying the filesystem.\r
+\r
+    exists = os.path.exists\r
+    isdir = os.path.isdir\r
+    isfile = os.path.isfile\r
+    islink = os.path.islink\r
+    ismount = os.path.ismount\r
+\r
+    if hasattr(os.path, 'samefile'):\r
+        samefile = os.path.samefile\r
+\r
+    getatime = os.path.getatime\r
+    atime = property(\r
+        getatime, None, None,\r
+        """ Last access time of the file. """)\r
+\r
+    getmtime = os.path.getmtime\r
+    mtime = property(\r
+        getmtime, None, None,\r
+        """ Last-modified time of the file. """)\r
+\r
+    if hasattr(os.path, 'getctime'):\r
+        getctime = os.path.getctime\r
+        ctime = property(\r
+            getctime, None, None,\r
+            """ Creation time of the file. """)\r
+\r
+    getsize = os.path.getsize\r
+    size = property(\r
+        getsize, None, None,\r
+        """ Size of the file, in bytes. """)\r
+\r
+    if hasattr(os, 'access'):\r
+        def access(self, mode):\r
+            """ Return true if current user has access to this path.\r
+\r
+            mode - One of the constants os.F_OK, os.R_OK, os.W_OK, os.X_OK\r
+            """\r
+            return os.access(self, mode)\r
+\r
+    def stat(self):\r
+        """ Perform a stat() system call on this path. """\r
+        return os.stat(self)\r
+\r
+    def lstat(self):\r
+        """ Like path.stat(), but do not follow symbolic links. """\r
+        return os.lstat(self)\r
+\r
+    def get_owner(self):\r
+        r""" Return the name of the owner of this file or directory.\r
+\r
+        This follows symbolic links.\r
+\r
+        On Windows, this returns a name of the form ur'DOMAIN\User Name'.\r
+        On Windows, a group can own a file or directory.\r
+        """\r
+        if os.name == 'nt':\r
+            if win32security is None:\r
+                raise Exception("path.owner requires win32all to be installed")\r
+            desc = win32security.GetFileSecurity(\r
+                self, win32security.OWNER_SECURITY_INFORMATION)\r
+            sid = desc.GetSecurityDescriptorOwner()\r
+            account, domain, typecode = win32security.LookupAccountSid(None, sid)\r
+            return domain + u'\\' + account\r
+        else:\r
+            if pwd is None:\r
+                raise NotImplementedError("path.owner is not implemented on this platform.")\r
+            st = self.stat()\r
+            return pwd.getpwuid(st.st_uid).pw_name\r
+\r
+    owner = property(\r
+        get_owner, None, None,\r
+        """ Name of the owner of this file or directory. """)\r
+\r
+    if hasattr(os, 'statvfs'):\r
+        def statvfs(self):\r
+            """ Perform a statvfs() system call on this path. """\r
+            return os.statvfs(self)\r
+\r
+    if hasattr(os, 'pathconf'):\r
+        def pathconf(self, name):\r
+            return os.pathconf(self, name)\r
+\r
+\r
+    # --- Modifying operations on files and directories\r
+\r
+    def utime(self, times):\r
+        """ Set the access and modified times of this file. """\r
+        os.utime(self, times)\r
+\r
+    def chmod(self, mode):\r
+        os.chmod(self, mode)\r
+\r
+    if hasattr(os, 'chown'):\r
+        def chown(self, uid, gid):\r
+            os.chown(self, uid, gid)\r
+\r
+    def rename(self, new):\r
+        os.rename(self, new)\r
+\r
+    def renames(self, new):\r
+        os.renames(self, new)\r
+\r
+\r
+    # --- Create/delete operations on directories\r
+\r
+    def mkdir(self, mode=0777):\r
+        os.mkdir(self, mode)\r
+\r
+    def makedirs(self, mode=0777):\r
+        os.makedirs(self, mode)\r
+\r
+    def rmdir(self):\r
+        os.rmdir(self)\r
+\r
+    def removedirs(self):\r
+        os.removedirs(self)\r
+\r
+\r
+    # --- Modifying operations on files\r
+\r
+    def touch(self):\r
+        """ Set the access/modified times of this file to the current time.\r
+        Create the file if it does not exist.\r
+        """\r
+        fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0666)\r
+        os.close(fd)\r
+        os.utime(self, None)\r
+\r
+    def remove(self):\r
+        os.remove(self)\r
+\r
+    def unlink(self):\r
+        os.unlink(self)\r
+\r
+\r
+    # --- Links\r
+\r
+    if hasattr(os, 'link'):\r
+        def link(self, newpath):\r
+            """ Create a hard link at 'newpath', pointing to this file. """\r
+            os.link(self, newpath)\r
+\r
+    if hasattr(os, 'symlink'):\r
+        def symlink(self, newlink):\r
+            """ Create a symbolic link at 'newlink', pointing here. """\r
+            os.symlink(self, newlink)\r
+\r
+    if hasattr(os, 'readlink'):\r
+        def readlink(self):\r
+            """ Return the path to which this symbolic link points.\r
+\r
+            The result may be an absolute or a relative path.\r
+            """\r
+            return self.__class__(os.readlink(self))\r
+\r
+        def readlinkabs(self):\r
+            """ Return the path to which this symbolic link points.\r
+\r
+            The result is always an absolute path.\r
+            """\r
+            p = self.readlink()\r
+            if p.isabs():\r
+                return p\r
+            else:\r
+                return (self.parent / p).abspath()\r
+\r
+\r
+    # --- High-level functions from shutil\r
+\r
+    copyfile = shutil.copyfile\r
+    copymode = shutil.copymode\r
+    copystat = shutil.copystat\r
+    copy = shutil.copy\r
+    copy2 = shutil.copy2\r
+    copytree = shutil.copytree\r
+    if hasattr(shutil, 'move'):\r
+        move = shutil.move\r
+    rmtree = shutil.rmtree\r
+\r
+\r
+    # --- Special stuff from os\r
+\r
+    if hasattr(os, 'chroot'):\r
+        def chroot(self):\r
+            os.chroot(self)\r
+\r
+    if hasattr(os, 'startfile'):\r
+        def startfile(self):\r
+            os.startfile(self)\r
+\r