diff options
| author | Dirk Makowski <dmakowski@gmx.net> | 2012-08-25 23:14:28 +0200 |
|---|---|---|
| committer | Dirk Makowski <dmakowski@gmx.net> | 2012-08-25 23:14:28 +0200 |
| commit | 81c2749d1e42463c5fd7b987f570fab71d1cce38 (patch) | |
| tree | 5057de5fcc32e57818a95f9b549624379295ee42 /feedgenerator | |
| parent | 77dcfa6b2c0b8df13c34d0f2b76556b4c0569c8e (diff) | |
| download | feedgenerator-81c2749d1e42463c5fd7b987f570fab71d1cce38.tar.gz | |
Initial commit
Diffstat (limited to 'feedgenerator')
| -rw-r--r-- | feedgenerator/__init__.py | 24 | ||||
| -rw-r--r-- | feedgenerator/django/__init__.py | 0 | ||||
| -rw-r--r-- | feedgenerator/django/utils/__init__.py | 0 | ||||
| -rw-r--r-- | feedgenerator/django/utils/datetime_safe.py | 89 | ||||
| -rw-r--r-- | feedgenerator/django/utils/encoding.py | 256 | ||||
| -rw-r--r-- | feedgenerator/django/utils/feedgenerator.py | 389 | ||||
| -rw-r--r-- | feedgenerator/django/utils/functional.py | 370 | ||||
| -rw-r--r-- | feedgenerator/django/utils/six.py | 370 | ||||
| -rw-r--r-- | feedgenerator/django/utils/timezone.py | 292 | ||||
| -rw-r--r-- | feedgenerator/django/utils/xmlutils.py | 14 |
10 files changed, 1804 insertions, 0 deletions
diff --git a/feedgenerator/__init__.py b/feedgenerator/__init__.py new file mode 100644 index 0000000..2e40b8c --- /dev/null +++ b/feedgenerator/__init__.py @@ -0,0 +1,24 @@ +""" +Syndication feed generation library -- used for generating RSS, etc. + +Sample usage: + +>>> import feedgenerator +>>> feed = feedgenerator.Rss201rev2Feed( +... title="Poynter E-Media Tidbits", +... link="http://www.poynter.org/column.asp?id=31", +... description="A group Weblog by the sharpest minds in online media/journalism/publishing.", +... language="en", +... ) +>>> feed.add_item( +... title="Hello", +... link="http://www.holovaty.com/test/", +... description="Testing." +... ) +>>> with open('test.rss', 'w') as fp: +... feed.write(fp, 'utf-8') + +For definitions of the different versions of RSS, see: +http://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004/02/04/incompatible-rss +""" +from .django.utils.feedgenerator import * diff --git a/feedgenerator/django/__init__.py b/feedgenerator/django/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/feedgenerator/django/__init__.py diff --git a/feedgenerator/django/utils/__init__.py b/feedgenerator/django/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/feedgenerator/django/utils/__init__.py diff --git a/feedgenerator/django/utils/datetime_safe.py b/feedgenerator/django/utils/datetime_safe.py new file mode 100644 index 0000000..b634888 --- /dev/null +++ b/feedgenerator/django/utils/datetime_safe.py @@ -0,0 +1,89 @@ +# Python's datetime strftime doesn't handle dates before 1900. +# These classes override date and datetime to support the formatting of a date +# through its full "proleptic Gregorian" date range. +# +# Based on code submitted to comp.lang.python by Andrew Dalke +# +# >>> datetime_safe.date(1850, 8, 2).strftime("%Y/%m/%d was a %A") +# '1850/08/02 was a Friday' + +from datetime import date as real_date, datetime as real_datetime +import re +import time + +class date(real_date): + def strftime(self, fmt): + return strftime(self, fmt) + +class datetime(real_datetime): + def strftime(self, fmt): + return strftime(self, fmt) + + def combine(self, date, time): + return datetime(date.year, date.month, date.day, time.hour, time.minute, time.microsecond, time.tzinfo) + + def date(self): + return date(self.year, self.month, self.day) + +def new_date(d): + "Generate a safe date from a datetime.date object." + return date(d.year, d.month, d.day) + +def new_datetime(d): + """ + Generate a safe datetime from a datetime.date or datetime.datetime object. + """ + kw = [d.year, d.month, d.day] + if isinstance(d, real_datetime): + kw.extend([d.hour, d.minute, d.second, d.microsecond, d.tzinfo]) + return datetime(*kw) + +# This library does not support strftime's "%s" or "%y" format strings. +# Allowed if there's an even number of "%"s because they are escaped. +_illegal_formatting = re.compile(r"((^|[^%])(%%)*%[sy])") + +def _findall(text, substr): + # Also finds overlaps + sites = [] + i = 0 + while 1: + j = text.find(substr, i) + if j == -1: + break + sites.append(j) + i=j+1 + return sites + +def strftime(dt, fmt): + if dt.year >= 1900: + return super(type(dt), dt).strftime(fmt) + illegal_formatting = _illegal_formatting.search(fmt) + if illegal_formatting: + raise TypeError("strftime of dates before 1900 does not handle" + illegal_formatting.group(0)) + + year = dt.year + # For every non-leap year century, advance by + # 6 years to get into the 28-year repeat cycle + delta = 2000 - year + off = 6 * (delta // 100 + delta // 400) + year = year + off + + # Move to around the year 2000 + year = year + ((2000 - year) // 28) * 28 + timetuple = dt.timetuple() + s1 = time.strftime(fmt, (year,) + timetuple[1:]) + sites1 = _findall(s1, str(year)) + + s2 = time.strftime(fmt, (year+28,) + timetuple[1:]) + sites2 = _findall(s2, str(year+28)) + + sites = [] + for site in sites1: + if site in sites2: + sites.append(site) + + s = s1 + syear = "%04d" % (dt.year,) + for site in sites: + s = s[:site] + syear + s[site+4:] + return s diff --git a/feedgenerator/django/utils/encoding.py b/feedgenerator/django/utils/encoding.py new file mode 100644 index 0000000..f13412d --- /dev/null +++ b/feedgenerator/django/utils/encoding.py @@ -0,0 +1,256 @@ +from __future__ import unicode_literals + +import codecs +import datetime +from decimal import Decimal +import locale +try: + from urllib.parse import quote +except ImportError: # Python 2 + from urllib import quote +import warnings + +from .functional import Promise +from . import six + +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 that derives __str__ from __unicode__. + + On Python 2, __str__ returns the output of __unicode__ encoded as a UTF-8 + bytestring. On Python 3, __str__ returns the output of __unicode__. + + Useful as a mix-in. If you support Python 2 and 3 with a single code base, + you can inherit this mix-in and just define __unicode__. + """ + def __init__(self, *args, **kwargs): + warnings.warn("StrAndUnicode is deprecated. Define a __str__ method " + "and apply the @python_2_unicode_compatible decorator " + "instead.", PendingDeprecationWarning, stacklevel=2) + super(StrAndUnicode, self).__init__(*args, **kwargs) + + if six.PY3: + def __str__(self): + return self.__unicode__() + else: + def __str__(self): + return self.__unicode__().encode('utf-8') + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if not six.PY3: + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + +def smart_text(s, encoding='utf-8', strings_only=False, errors='strict'): + """ + Returns a text object representing 's' -- unicode on Python 2 and str on + Python 3. Treats bytestrings using the 'encoding' codec. + + If strings_only is True, don't convert (some) non-string-like objects. + """ + if isinstance(s, Promise): + # The input is the result of a gettext_lazy() call. + return s + return force_text(s, encoding, strings_only, errors) + +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_text(strings_only=True). + """ + return isinstance(obj, six.integer_types + (type(None), float, Decimal, + datetime.datetime, datetime.date, datetime.time)) + +def force_text(s, encoding='utf-8', strings_only=False, errors='strict'): + """ + Similar to smart_text, 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. + """ + # Handle the common case first, saves 30-40% when s is an instance of + # six.text_type. This function gets called often in that setting. + if isinstance(s, six.text_type): + return s + if strings_only and is_protected_type(s): + return s + try: + if not isinstance(s, six.string_types): + if hasattr(s, '__unicode__'): + s = s.__unicode__() + else: + try: + if six.PY3: + if isinstance(s, bytes): + s = six.text_type(s, encoding, errors) + else: + s = six.text_type(s) + else: + s = six.text_type(bytes(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_text(arg, encoding, strings_only, + errors) for arg in s]) + else: + # Note: We use .decode() here, instead of six.text_type(s, encoding, + # errors), so that if s is a SafeBytes, it ends up being a + # SafeText at the end. + s = s.decode(encoding, errors) + except UnicodeDecodeError as 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_text(arg, encoding, strings_only, + errors) for arg in s]) + return s + +def smart_bytes(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 isinstance(s, Promise): + # The input is the result of a gettext_lazy() call. + return s + return force_bytes(s, encoding, strings_only, errors) + + +def force_bytes(s, encoding='utf-8', strings_only=False, errors='strict'): + """ + Similar to smart_bytes, 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 isinstance(s, bytes): + if encoding == 'utf-8': + return s + else: + return s.decode('utf-8', errors).encode(encoding, errors) + if strings_only and (s is None or isinstance(s, int)): + return s + if isinstance(s, Promise): + return six.text_type(s).encode(encoding, errors) + if not isinstance(s, six.string_types): + try: + if six.PY3: + return six.text_type(s).encode(encoding) + else: + return bytes(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_bytes(arg, encoding, strings_only, + errors) for arg in s]) + return six.text_type(s).encode(encoding, errors) + else: + return s.encode(encoding, errors) + +if six.PY3: + smart_str = smart_text + force_str = force_text +else: + smart_str = smart_bytes + force_str = force_bytes + # backwards compatibility for Python 2 + smart_unicode = smart_text + force_unicode = force_text + +smart_str.__doc__ = """\ +Apply smart_text in Python 3 and smart_bytes in Python 2. + +This is suitable for writing to sys.stdout (for instance). +""" + +force_str.__doc__ = """\ +Apply force_text in Python 3 and force_bytes in Python 2. +""" + +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 quote(smart_bytes(iri), safe=b"/#%[]=:;$&()+,!?*@'~") + +def filepath_to_uri(path): + """Convert an file system path to a URI portion that is suitable for + inclusion in a URL. + + We are assuming input is either UTF-8 or unicode already. + + This method will encode certain chars that would normally be recognized as + special chars for URIs. Note that this method does not encode the ' + character, as it is a valid character within URIs. See + encodeURIComponent() JavaScript function for more details. + + Returns an ASCII string containing the encoded result. + """ + if path is None: + return path + # I know about `os.sep` and `os.altsep` but I want to leave + # some flexibility for hardcoding separators. + return quote(smart_bytes(path.replace("\\", "/")), safe=b"/~!*()'") + +# 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/feedgenerator/django/utils/feedgenerator.py b/feedgenerator/django/utils/feedgenerator.py new file mode 100644 index 0000000..54bc7ed --- /dev/null +++ b/feedgenerator/django/utils/feedgenerator.py @@ -0,0 +1,389 @@ +""" +Syndication feed generation library -- used for generating RSS, etc. + +Sample usage: + +>>> from django.utils import feedgenerator +>>> feed = feedgenerator.Rss201rev2Feed( +... title="Poynter E-Media Tidbits", +... link="http://www.poynter.org/column.asp?id=31", +... description="A group Weblog by the sharpest minds in online media/journalism/publishing.", +... language="en", +... ) +>>> feed.add_item( +... title="Hello", +... link="http://www.holovaty.com/test/", +... description="Testing." +... ) +>>> with open('test.rss', 'w') as fp: +... feed.write(fp, 'utf-8') + +For definitions of the different versions of RSS, see: +http://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004/02/04/incompatible-rss +""" +from __future__ import unicode_literals + +import datetime +try: + from urllib.parse import urlparse +except ImportError: # Python 2 + from urlparse import urlparse +from .xmlutils import SimplerXMLGenerator +from .encoding import force_text, iri_to_uri +from . import datetime_safe +from . import six +from .six import StringIO +from .timezone import is_aware + +def rfc2822_date(date): + # We can't use strftime() because it produces locale-dependant results, so + # we have to map english month and day names manually + months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',) + days = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') + # Support datetime objects older than 1900 + date = datetime_safe.new_datetime(date) + # We do this ourselves to be timezone aware, email.Utils is not tz aware. + dow = days[date.weekday()] + month = months[date.month - 1] + time_str = date.strftime('%s, %%d %s %%Y %%H:%%M:%%S ' % (dow, month)) + if not six.PY3: # strftime returns a byte string in Python 2 + time_str = time_str.decode('utf-8') + if is_aware(date): + 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 time_str + '-0000' + +def rfc3339_date(date): + # Support datetime objects older than 1900 + date = datetime_safe.new_datetime(date) + time_str = date.strftime('%Y-%m-%dT%H:%M:%S') + if not six.PY3: # strftime returns a byte string in Python 2 + time_str = time_str.decode('utf-8') + if is_aware(date): + 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 time_str + 'Z' + +def get_tag_uri(url, date): + """ + Creates a TagURI. + + See http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id + """ + bits = urlparse(url) + d = '' + if date is not None: + d = ',%s' % datetime_safe.new_datetime(date).strftime('%Y-%m-%d') + return 'tag:%s%s:%s/%s' % (bits.hostname, d, bits.path, bits.fragment) + +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_text(s, strings_only=True) + if categories: + categories = [force_text(c) for c in categories] + if ttl is not None: + # Force ints to unicode + ttl = force_text(ttl) + 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_text(s, strings_only=True) + if categories: + categories = [to_unicode(c) for c in categories] + if ttl is not None: + # Force ints to unicode + ttl = force_text(ttl) + 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. + """ + 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; charset=utf-8' + def write(self, outfile, encoding): + handler = SimplerXMLGenerator(outfile, encoding) + handler.startDocument() + handler.startElement("rss", self.rss_attributes()) + handler.startElement("channel", self.root_attributes()) + self.add_root_elements(handler) + self.write_items(handler) + self.endChannelElement(handler) + handler.endElement("rss") + + def rss_attributes(self): + return {"version": self._version, + "xmlns:atom": "http://www.w3.org/2005/Atom"} + + def write_items(self, handler): + for item in self.items: + handler.startElement('item', self.item_attributes(item)) + self.add_item_elements(handler, item) + handler.endElement("item") + + def add_root_elements(self, handler): + handler.addQuickElement("title", self.feed['title']) + handler.addQuickElement("link", self.feed['link']) + handler.addQuickElement("description", self.feed['description']) + if self.feed['feed_url'] is not None: + handler.addQuickElement("atom:link", None, + {"rel": "self", "href": self.feed['feed_url']}) + if self.feed['language'] is not None: + handler.addQuickElement("language", self.feed['language']) + for cat in self.feed['categories']: + handler.addQuickElement("category", cat) + if self.feed['feed_copyright'] is not None: + handler.addQuickElement("copyright", self.feed['feed_copyright']) + handler.addQuickElement("lastBuildDate", rfc2822_date(self.latest_post_date())) + if self.feed['ttl'] is not None: + handler.addQuickElement("ttl", self.feed['ttl']) + + def endChannelElement(self, handler): + handler.endElement("channel") + +class RssUserland091Feed(RssFeed): + _version = "0.91" + def add_item_elements(self, handler, item): + handler.addQuickElement("title", item['title']) + handler.addQuickElement("link", item['link']) + if item['description'] is not None: + handler.addQuickElement("description", item['description']) + +class Rss201rev2Feed(RssFeed): + # Spec: http://blogs.law.harvard.edu/tech/rss + _version = "2.0" + def add_item_elements(self, handler, item): + handler.addQuickElement("title", item['title']) + handler.addQuickElement("link", item['link']) + if item['description'] is not None: + handler.addQuickElement("description", item['description']) + + # Author information. + if item["author_name"] and item["author_email"]: + handler.addQuickElement("author", "%s (%s)" % \ + (item['author_email'], item['author_name'])) + elif item["author_email"]: + handler.addQuickElement("author", item["author_email"]) + elif item["author_name"]: + handler.addQuickElement("dc:creator", item["author_name"], {"xmlns:dc": "http://purl.org/dc/elements/1.1/"}) + + if item['pubdate'] is not None: + handler.addQuickElement("pubDate", rfc2822_date(item['pubdate'])) + if item['comments'] is not None: + handler.addQuickElement("comments", item['comments']) + if item['unique_id'] is not None: + handler.addQuickElement("guid", item['unique_id']) + if item['ttl'] is not None: + handler.addQuickElement("ttl", item['ttl']) + + # Enclosure. + if item['enclosure'] is not None: + handler.addQuickElement("enclosure", '', + {"url": item['enclosure'].url, "length": item['enclosure'].length, + "type": item['enclosure'].mime_type}) + + # Categories. + for cat in item['categories']: + handler.addQuickElement("category", cat) + +class Atom1Feed(SyndicationFeed): + # Spec: http://atompub.org/2005/07/11/draft-ietf-atompub-format-10.html + mime_type = 'application/atom+xml; charset=utf-8' + ns = "http://www.w3.org/2005/Atom" + + def write(self, outfile, encoding): + handler = SimplerXMLGenerator(outfile, encoding) + handler.startDocument() + handler.startElement('feed', self.root_attributes()) + self.add_root_elements(handler) + self.write_items(handler) + handler.endElement("feed") + + def root_attributes(self): + if self.feed['language'] is not None: + return {"xmlns": self.ns, "xml:lang": self.feed['language']} + else: + return {"xmlns": self.ns} + + def add_root_elements(self, handler): + handler.addQuickElement("title", self.feed['title']) + handler.addQuickElement("link", "", {"rel": "alternate", "href": self.feed['link']}) + if self.feed['feed_url'] is not None: + handler.addQuickElement("link", "", {"rel": "self", "href": self.feed['feed_url']}) + handler.addQuickElement("id", self.feed['id']) + handler.addQuickElement("updated", rfc3339_date(self.latest_post_date())) + if self.feed['author_name'] is not None: + handler.startElement("author", {}) + handler.addQuickElement("name", self.feed['author_name']) + if self.feed['author_email'] is not None: + handler.addQuickElement("email", self.feed['author_email']) + if self.feed['author_link'] is not None: + handler.addQuickElement("uri", self.feed['author_link']) + handler.endElement("author") + if self.feed['subtitle'] is not None: + handler.addQuickElement("subtitle", self.feed['subtitle']) + for cat in self.feed['categories']: + handler.addQuickElement("category", "", {"term": cat}) + if self.feed['feed_copyright'] is not None: + handler.addQuickElement("rights", self.feed['feed_copyright']) + + def write_items(self, handler): + for item in self.items: + handler.startElement("entry", self.item_attributes(item)) + self.add_item_elements(handler, item) + handler.endElement("entry") + + def add_item_elements(self, handler, item): + handler.addQuickElement("title", item['title']) + handler.addQuickElement("link", "", {"href": item['link'], "rel": "alternate"}) + if item['pubdate'] is not None: + handler.addQuickElement("updated", rfc3339_date(item['pubdate'])) + + # Author information. + if item['author_name'] is not None: + handler.startElement("author", {}) + handler.addQuickElement("name", item['author_name']) + if item['author_email'] is not None: + handler.addQuickElement("email", item['author_email']) + if item['author_link'] is not None: + handler.addQuickElement("uri", item['author_link']) + handler.endElement("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("id", unique_id) + + # Summary. + if item['description'] is not None: + handler.addQuickElement("summary", item['description'], {"type": "html"}) + + # Enclosure. + if item['enclosure'] is not None: + handler.addQuickElement("link", '', + {"rel": "enclosure", + "href": item['enclosure'].url, + "length": item['enclosure'].length, + "type": item['enclosure'].mime_type}) + + # Categories. + for cat in item['categories']: + handler.addQuickElement("category", "", {"term": cat}) + + # Rights. + if item['item_copyright'] is not None: + handler.addQuickElement("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/feedgenerator/django/utils/functional.py b/feedgenerator/django/utils/functional.py new file mode 100644 index 0000000..8d8f759 --- /dev/null +++ b/feedgenerator/django/utils/functional.py @@ -0,0 +1,370 @@ +import copy +import operator +from functools import wraps, update_wrapper +import sys + +from . import six + +# You can't trivially replace this `functools.partial` because this binds to +# classes and returns bound instances, whereas functools.partial (on CPython) +# is a type and its instances don't bind. +def curry(_curried_func, *args, **kwargs): + def _curried(*moreargs, **morekwargs): + return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs)) + return _curried + +def memoize(func, cache, num_args): + """ + Wrap a function so that results for any argument tuple are stored in + 'cache'. Note that the args to the function must be usable as dictionary + keys. + + Only the first num_args are considered when creating the key. + """ + @wraps(func) + def wrapper(*args): + mem_args = args[:num_args] + if mem_args in cache: + return cache[mem_args] + result = func(*args) + cache[mem_args] = result + return result + return wrapper + +class cached_property(object): + """ + Decorator that creates converts a method with a single + self argument into a property cached on the instance. + """ + def __init__(self, func): + self.func = func + + def __get__(self, instance, type): + res = instance.__dict__[self.func.__name__] = self.func(instance) + return res + +class Promise(object): + """ + This is just a base class for the proxy class created in + the closure of the lazy function. It can be used to recognize + promises in code. + """ + pass + +def lazy(func, *resultclasses): + """ + Turns any callable into a lazy evaluated callable. You need to give result + classes or types -- at least one is needed so that the automatic forcing of + the lazy evaluation code is triggered. Results are not memoized; the + function is evaluated on every access. + """ + + @total_ordering + class __proxy__(Promise): + """ + Encapsulate a function call and act as a proxy for methods that are + called on the result of that function. The function is not evaluated + until one of the methods on the result is called. + """ + __dispatch = None + + def __init__(self, args, kw): + self.__args = args + self.__kw = kw + if self.__dispatch is None: + self.__prepare_class__() + + def __reduce__(self): + return ( + _lazy_proxy_unpickle, + (func, self.__args, self.__kw) + resultclasses + ) + + def __prepare_class__(cls): + cls.__dispatch = {} + for resultclass in resultclasses: + cls.__dispatch[resultclass] = {} + for type_ in reversed(resultclass.mro()): + for (k, v) in type_.__dict__.items(): + # All __promise__ return the same wrapper method, but they + # also do setup, inserting the method into the dispatch + # dict. + meth = cls.__promise__(resultclass, k, v) + if hasattr(cls, k): + continue + setattr(cls, k, meth) + cls._delegate_bytes = bytes in resultclasses + cls._delegate_text = six.text_type in resultclasses + assert not (cls._delegate_bytes and cls._delegate_text), "Cannot call lazy() with both bytes and text return types." + if cls._delegate_text: + if six.PY3: + cls.__str__ = cls.__text_cast + else: + cls.__unicode__ = cls.__text_cast + elif cls._delegate_bytes: + if six.PY3: + cls.__bytes__ = cls.__bytes_cast + else: + cls.__str__ = cls.__bytes_cast + __prepare_class__ = classmethod(__prepare_class__) + + def __promise__(cls, klass, funcname, method): + # Builds a wrapper around some magic method and registers that magic + # method for the given type and method name. + def __wrapper__(self, *args, **kw): + # Automatically triggers the evaluation of a lazy value and + # applies the given magic method of the result type. + res = func(*self.__args, **self.__kw) + for t in type(res).mro(): + if t in self.__dispatch: + return self.__dispatch[t][funcname](res, *args, **kw) + raise TypeError("Lazy object returned unexpected type.") + + if klass not in cls.__dispatch: + cls.__dispatch[klass] = {} + cls.__dispatch[klass][funcname] = method + return __wrapper__ + __promise__ = classmethod(__promise__) + + def __text_cast(self): + return func(*self.__args, **self.__kw) + + def __bytes_cast(self): + return bytes(func(*self.__args, **self.__kw)) + + def __cast(self): + if self._delegate_bytes: + return self.__bytes_cast() + elif self._delegate_text: + return self.__text_cast() + else: + return func(*self.__args, **self.__kw) + + def __eq__(self, other): + if isinstance(other, Promise): + other = other.__cast() + return self.__cast() == other + + def __lt__(self, other): + if isinstance(other, Promise): + other = other.__cast() + return self.__cast() < other + + __hash__ = object.__hash__ + + def __mod__(self, rhs): + if self._delegate_bytes and not six.PY3: + return bytes(self) % rhs + elif self._delegate_text: + return six.text_type(self) % rhs + else: + raise AssertionError('__mod__ not supported for non-string types') + + def __deepcopy__(self, memo): + # Instances of this class are effectively immutable. It's just a + # collection of functions. So we don't need to do anything + # complicated for copying. + memo[id(self)] = self + return self + + @wraps(func) + def __wrapper__(*args, **kw): + # Creates the proxy object, instead of the actual value. + return __proxy__(args, kw) + + return __wrapper__ + +def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses): + return lazy(func, *resultclasses)(*args, **kwargs) + +def allow_lazy(func, *resultclasses): + """ + A decorator that allows a function to be called with one or more lazy + arguments. If none of the args are lazy, the function is evaluated + immediately, otherwise a __proxy__ is returned that will evaluate the + function when needed. + """ + @wraps(func) + def wrapper(*args, **kwargs): + for arg in list(args) + list(six.itervalues(kwargs)): + if isinstance(arg, Promise): + break + else: + return func(*args, **kwargs) + return lazy(func, *resultclasses)(*args, **kwargs) + return wrapper + +empty = object() +def new_method_proxy(func): + def inner(self, *args): + if self._wrapped is empty: + self._setup() + return func(self._wrapped, *args) + return inner + +class LazyObject(object): + """ + A wrapper for another class that can be used to delay instantiation of the + wrapped class. + + By subclassing, you have the opportunity to intercept and alter the + instantiation. If you don't need to do that, use SimpleLazyObject. + """ + def __init__(self): + self._wrapped = empty + + __getattr__ = new_method_proxy(getattr) + + def __setattr__(self, name, value): + if name == "_wrapped": + # Assign to __dict__ to avoid infinite __setattr__ loops. + self.__dict__["_wrapped"] = value + else: + if self._wrapped is empty: + self._setup() + setattr(self._wrapped, name, value) + + def __delattr__(self, name): + if name == "_wrapped": + raise TypeError("can't delete _wrapped.") + if self._wrapped is empty: + self._setup() + delattr(self._wrapped, name) + + def _setup(self): + """ + Must be implemented by subclasses to initialise the wrapped object. + """ + raise NotImplementedError + + # introspection support: + __dir__ = new_method_proxy(dir) + + +# Workaround for http://bugs.python.org/issue12370 +_super = super + +class SimpleLazyObject(LazyObject): + """ + A lazy object initialised from any function. + + Designed for compound objects of unknown type. For builtins or objects of + known type, use django.utils.functional.lazy. + """ + def __init__(self, func): + """ + Pass in a callable that returns the object to be wrapped. + + If copies are made of the resulting SimpleLazyObject, which can happen + in various circumstances within Django, then you must ensure that the + callable can be safely run more than once and will return the same + value. + """ + self.__dict__['_setupfunc'] = func + _super(SimpleLazyObject, self).__init__() + + def _setup(self): + self._wrapped = self._setupfunc() + + if six.PY3: + __bytes__ = new_method_proxy(bytes) + __str__ = new_method_proxy(str) + else: + __str__ = new_method_proxy(str) + __unicode__ = new_method_proxy(unicode) + + def __deepcopy__(self, memo): + if self._wrapped is empty: + # We have to use SimpleLazyObject, not self.__class__, because the + # latter is proxied. + result = SimpleLazyObject(self._setupfunc) + memo[id(self)] = result + return result + else: + return copy.deepcopy(self._wrapped, memo) + + # Because we have messed with __class__ below, we confuse pickle as to what + # class we are pickling. It also appears to stop __reduce__ from being + # called. So, we define __getstate__ in a way that cooperates with the way + # that pickle interprets this class. This fails when the wrapped class is a + # builtin, but it is better than nothing. + def __getstate__(self): + if self._wrapped is empty: + self._setup() + return self._wrapped.__dict__ + + # Need to pretend to be the wrapped class, for the sake of objects that care + # about this (especially in equality tests) + __class__ = property(new_method_proxy(operator.attrgetter("__class__"))) + __eq__ = new_method_proxy(operator.eq) + __hash__ = new_method_proxy(hash) + __bool__ = new_method_proxy(bool) # Python 3 + __nonzero__ = __bool__ # Python 2 + + +class lazy_property(property): + """ + A property that works with subclasses by wrapping the decorated + functions of the base class. + """ + def __new__(cls, fget=None, fset=None, fdel=None, doc=None): + if fget is not None: + @wraps(fget) + def fget(instance, instance_type=None, name=fget.__name__): + return getattr(instance, name)() + if fset is not None: + @wraps(fset) + def fset(instance, value, name=fset.__name__): + return getattr(instance, name)(value) + if fdel is not None: + @wraps(fdel) + def fdel(instance, name=fdel.__name__): + return getattr(instance, name)() + return property(fget, fset, fdel, doc) + +def partition(predicate, values): + """ + Splits the values into two sets, based on the return value of the function + (True/False). e.g.: + + >>> partition(lambda x: x > 3, range(5)) + [0, 1, 2, 3], [4] + """ + results = ([], []) + for item in values: + results[predicate(item)].append(item) + return results + +if sys.version_info >= (2,7,2): + from functools import total_ordering +else: + # For Python < 2.7.2. Python 2.6 does not have total_ordering, and + # total_ordering in 2.7 versions prior to 2.7.2 is buggy. See + # http://bugs.python.org/issue10042 for details. For these versions use + # code borrowed from Python 2.7.3. + def total_ordering(cls): + """Class decorator that fills in missing ordering methods""" + convert = { + '__lt__': [('__gt__', lambda self, other: not (self < other or self == other)), + ('__le__', lambda self, other: self < other or self == other), + ('__ge__', lambda self, other: not self < other)], + '__le__': [('__ge__', lambda self, other: not self <= other or self == other), + ('__lt__', lambda self, other: self <= other and not self == other), + ('__gt__', lambda self, other: not self <= other)], + '__gt__': [('__lt__', lambda self, other: not (self > other or self == other)), + ('__ge__', lambda self, other: self > other or self == other), + ('__le__', lambda self, other: not self > other)], + '__ge__': [('__le__', lambda self, other: (not self >= other) or self == other), + ('__gt__', lambda self, other: self >= other and not self == other), + ('__lt__', lambda self, other: not self >= other)] + } + roots = set(dir(cls)) & set(convert) + if not roots: + raise ValueError('must define at least one ordering operation: < > <= >=') + root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__ + for opname, opfunc in convert[root]: + if opname not in roots: + opfunc.__name__ = opname + opfunc.__doc__ = getattr(int, opname).__doc__ + setattr(cls, opname, opfunc) + return cls diff --git a/feedgenerator/django/utils/six.py b/feedgenerator/django/utils/six.py new file mode 100644 index 0000000..767fe0b --- /dev/null +++ b/feedgenerator/django/utils/six.py @@ -0,0 +1,370 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +import operator +import sys +import types + +__author__ = "Benjamin Peterson <benjamin@python.org>" +__version__ = "1.1.0" + + +# True if we are running on Python 3. +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules["django.utils.six.moves"] = _MovedItems("moves") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_code = "__code__" + _func_defaults = "__defaults__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_code = "func_code" + _func_defaults = "func_defaults" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + + +if PY3: + def get_unbound_function(unbound): + return unbound + + + advance_iterator = next + + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) +else: + def get_unbound_function(unbound): + return unbound.im_func + + + def advance_iterator(it): + return it.next() + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) + + +def iterkeys(d): + """Return an iterator over the keys of a dictionary.""" + return getattr(d, _iterkeys)() + +def itervalues(d): + """Return an iterator over the values of a dictionary.""" + return getattr(d, _itervalues)() + +def iteritems(d): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return getattr(d, _iteritems)() + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + int2byte = chr + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(code, globs=None, locs=None): + """Execute code in a namespace.""" + if globs is None: + frame = sys._getframe(1) + globs = frame.f_globals + if locs is None: + locs = frame.f_locals + del frame + elif locs is None: + locs = globs + exec("""exec code in globs, locs""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, base=object): + """Create a base class with a metaclass.""" + return meta("NewBase", (base,), {}) + + +### Additional customizations for Django ### + +if PY3: + _iterlists = "lists" +else: + _iterlists = "iterlists" + +def iterlists(d): + """Return an iterator over the values of a MultiValueDict.""" + return getattr(d, _iterlists)() + + +add_move(MovedModule("_dummy_thread", "dummy_thread")) +add_move(MovedModule("_thread", "thread")) diff --git a/feedgenerator/django/utils/timezone.py b/feedgenerator/django/utils/timezone.py new file mode 100644 index 0000000..c125487 --- /dev/null +++ b/feedgenerator/django/utils/timezone.py @@ -0,0 +1,292 @@ +"""Timezone helper functions. + +This module uses pytz when it's available and fallbacks when it isn't. +""" + +from datetime import datetime, timedelta, tzinfo +from threading import local +import time as _time + +try: + import pytz +except ImportError: + pytz = None + +# ### from django.conf import settings +from . import six + +__all__ = [ + 'utc', 'get_default_timezone', 'get_current_timezone', + 'activate', 'deactivate', 'override', + 'is_naive', 'is_aware', 'make_aware', 'make_naive', +] + + +# UTC and local time zones + +ZERO = timedelta(0) + +class UTC(tzinfo): + """ + UTC implementation taken from Python's docs. + + Used only when pytz isn't available. + """ + + def __repr__(self): + return "<UTC>" + + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO + +class LocalTimezone(tzinfo): + """ + Local time implementation taken from Python's docs. + + Used only when pytz isn't available, and most likely inaccurate. If you're + having trouble with this class, don't waste your time, just install pytz. + """ + + def __init__(self): + # This code is moved in __init__ to execute it as late as possible + # See get_default_timezone(). + self.STDOFFSET = timedelta(seconds=-_time.timezone) + if _time.daylight: + self.DSTOFFSET = timedelta(seconds=-_time.altzone) + else: + self.DSTOFFSET = self.STDOFFSET + self.DSTDIFF = self.DSTOFFSET - self.STDOFFSET + tzinfo.__init__(self) + + def __repr__(self): + return "<LocalTimezone>" + + def utcoffset(self, dt): + if self._isdst(dt): + return self.DSTOFFSET + else: + return self.STDOFFSET + + def dst(self, dt): + if self._isdst(dt): + return self.DSTDIFF + else: + return ZERO + + def tzname(self, dt): + return _time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + tt = (dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.weekday(), 0, 0) + stamp = _time.mktime(tt) + tt = _time.localtime(stamp) + return tt.tm_isdst > 0 + + +utc = pytz.utc if pytz else UTC() +"""UTC time zone as a tzinfo instance.""" + +# ### # In order to avoid accessing the settings at compile time, +# ### # wrap the expression in a function and cache the result. +# ### _localtime = None +# ### +# ### def get_default_timezone(): +# ### """ +# ### Returns the default time zone as a tzinfo instance. +# ### +# ### This is the time zone defined by settings.TIME_ZONE. +# ### +# ### See also :func:`get_current_timezone`. +# ### """ +# ### global _localtime +# ### if _localtime is None: +# ### if isinstance(settings.TIME_ZONE, six.string_types) and pytz is not None: +# ### _localtime = pytz.timezone(settings.TIME_ZONE) +# ### else: +# ### _localtime = LocalTimezone() +# ### return _localtime + +# This function exists for consistency with get_current_timezone_name +def get_default_timezone_name(): + """ + Returns the name of the default time zone. + """ + return _get_timezone_name(get_default_timezone()) + +_active = local() + +def get_current_timezone(): + """ + Returns the currently active time zone as a tzinfo instance. + """ + return getattr(_active, "value", get_default_timezone()) + +def get_current_timezone_name(): + """ + Returns the name of the currently active time zone. + """ + return _get_timezone_name(get_current_timezone()) + +def _get_timezone_name(timezone): + """ + Returns the name of ``timezone``. + """ + try: + # for pytz timezones + return timezone.zone + except AttributeError: + # for regular tzinfo objects + local_now = datetime.now(timezone) + return timezone.tzname(local_now) + +# Timezone selection functions. + +# These functions don't change os.environ['TZ'] and call time.tzset() +# because it isn't thread safe. + +def activate(timezone): + """ + Sets the time zone for the current thread. + + The ``timezone`` argument must be an instance of a tzinfo subclass or a + time zone name. If it is a time zone name, pytz is required. + """ + if isinstance(timezone, tzinfo): + _active.value = timezone + elif isinstance(timezone, six.string_types) and pytz is not None: + _active.value = pytz.timezone(timezone) + else: + raise ValueError("Invalid timezone: %r" % timezone) + +def deactivate(): + """ + Unsets the time zone for the current thread. + + Django will then use the time zone defined by settings.TIME_ZONE. + """ + if hasattr(_active, "value"): + del _active.value + +class override(object): + """ + Temporarily set the time zone for the current thread. + + This is a context manager that uses ``~django.utils.timezone.activate()`` + to set the timezone on entry, and restores the previously active timezone + on exit. + + The ``timezone`` argument must be an instance of a ``tzinfo`` subclass, a + time zone name, or ``None``. If is it a time zone name, pytz is required. + If it is ``None``, Django enables the default time zone. + """ + def __init__(self, timezone): + self.timezone = timezone + self.old_timezone = getattr(_active, 'value', None) + + def __enter__(self): + if self.timezone is None: + deactivate() + else: + activate(self.timezone) + + def __exit__(self, exc_type, exc_value, traceback): + if self.old_timezone is not None: + _active.value = self.old_timezone + else: + del _active.value + + +# ### # Templates +# ### +# ### def template_localtime(value, use_tz=None): +# ### """ +# ### Checks if value is a datetime and converts it to local time if necessary. +# ### +# ### If use_tz is provided and is not None, that will force the value to +# ### be converted (or not), overriding the value of settings.USE_TZ. +# ### +# ### This function is designed for use by the template engine. +# ### """ +# ### should_convert = (isinstance(value, datetime) +# ### and (settings.USE_TZ if use_tz is None else use_tz) +# ### and not is_naive(value) +# ### and getattr(value, 'convert_to_local_time', True)) +# ### return localtime(value) if should_convert else value + + +# Utilities + +def localtime(value, timezone=None): + """ + Converts an aware datetime.datetime to local time. + + Local time is defined by the current time zone, unless another time zone + is specified. + """ + if timezone is None: + timezone = get_current_timezone() + value = value.astimezone(timezone) + if hasattr(timezone, 'normalize'): + # available for pytz time zones + value = timezone.normalize(value) + return value + +# ### def now(): +# ### """ +# ### Returns an aware or naive datetime.datetime, depending on settings.USE_TZ. +# ### """ +# ### if settings.USE_TZ: +# ### # timeit shows that datetime.now(tz=utc) is 24% slower +# ### return datetime.utcnow().replace(tzinfo=utc) +# ### else: +# ### return datetime.now() + +# By design, these four functions don't perform any checks on their arguments. +# The caller should ensure that they don't receive an invalid value like None. + +def is_aware(value): + """ + Determines if a given datetime.datetime is aware. + + The logic is described in Python's docs: + http://docs.python.org/library/datetime.html#datetime.tzinfo + """ + return value.tzinfo is not None and value.tzinfo.utcoffset(value) is not None + +def is_naive(value): + """ + Determines if a given datetime.datetime is naive. + + The logic is described in Python's docs: + http://docs.python.org/library/datetime.html#datetime.tzinfo + """ + return value.tzinfo is None or value.tzinfo.utcoffset(value) is None + +def make_aware(value, timezone): + """ + Makes a naive datetime.datetime in a given time zone aware. + """ + if hasattr(timezone, 'localize'): + # available for pytz time zones + return timezone.localize(value, is_dst=None) + else: + # may be wrong around DST changes + return value.replace(tzinfo=timezone) + +def make_naive(value, timezone): + """ + Makes an aware datetime.datetime naive in a given time zone. + """ + value = value.astimezone(timezone) + if hasattr(timezone, 'normalize'): + # available for pytz time zones + value = timezone.normalize(value) + return value.replace(tzinfo=None) diff --git a/feedgenerator/django/utils/xmlutils.py b/feedgenerator/django/utils/xmlutils.py new file mode 100644 index 0000000..a1eb5fb --- /dev/null +++ b/feedgenerator/django/utils/xmlutils.py @@ -0,0 +1,14 @@ +""" +Utilities for XML generation/parsing. +""" + +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) |
