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 | |
| parent | 77dcfa6b2c0b8df13c34d0f2b76556b4c0569c8e (diff) | |
| download | feedgenerator-81c2749d1e42463c5fd7b987f570fab71d1cce38.tar.gz | |
Initial commit
| -rw-r--r-- | PKG-INFO | 20 | ||||
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | README.rst | 13 | ||||
| -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 (renamed from feedgenerator.py) | 205 | ||||
| -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 | ||||
| -rw-r--r-- | setup.py | 31 | ||||
| -rw-r--r-- | test.rss | 6 | ||||
| -rw-r--r-- | tests/test_001.py | 20 | ||||
| -rw-r--r-- | tox.ini | 15 |
17 files changed, 1549 insertions, 180 deletions
diff --git a/PKG-INFO b/PKG-INFO deleted file mode 100644 index b87ae71..0000000 --- a/PKG-INFO +++ /dev/null @@ -1,20 +0,0 @@ -Metadata-Version: 1.0 -Name: feedgenerator -Version: 1.2.1 -Summary: Standalone version of django.utils.feedgenerator -Home-page: http://hg.lolnet.org/feedgenerator/ -Author: Alexis Metaireau -Author-email: alexis@notmyidea.org -License: UNKNOWN -Description: UNKNOWN -Keywords: feed,atom,rss -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Environment :: Web Environment -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Topic :: Internet :: WWW/HTTP -Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content -Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/README.md b/README.md deleted file mode 100644 index 96a3321..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -feedgenerator-py3k -================== - -Port of feedgenerator to Python 3
\ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..2b826d8 --- /dev/null +++ b/README.rst @@ -0,0 +1,13 @@ +feedgenerator-py3k +================== + +Feedgenerator-py3k is a standalone version of Django's feedgenerator. +It is based on the current Django Version 1.5.dev20120824122350. + +The previous feedgenerator 1.2.1 is based on rather old code, and +during the port to Python 3 it became obvious that (at least) the handling +of unicode strings has to be refactored. + +Django has evolved since, so I decided to create a new standalone version +which is based upon modern code. + 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.py b/feedgenerator/django/utils/feedgenerator.py index 3d1a7db..54bc7ed 100644 --- a/feedgenerator.py +++ b/feedgenerator/django/utils/feedgenerator.py @@ -3,186 +3,96 @@ Syndication feed generation library -- used for generating RSS, etc. Sample usage: ->>> import feedgenerator +>>> from django.utils import feedgenerator >>> feed = feedgenerator.Rss201rev2Feed( -... title=u"Poynter E-Media Tidbits", -... link=u"http://www.poynter.org/column.asp?id=31", -... feed_url=u"http://test.org/rss", -... description=u"A group weblog by the sharpest minds in online media/journalism/publishing.", -... language=u"en", +... 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=u"http://www.holovaty.com/test/", +... link="http://www.holovaty.com/test/", ... description="Testing." ... ) ->>> fp = open('test.rss', 'w') ->>> feed.write(fp, 'utf-8') ->>> fp.close() +>>> with open('test.rss', 'w') as fp: +... feed.write(fp, 'utf-8') For definitions of the different versions of RSS, see: -http://diveintomark.org/archives/2004/02/04/incompatible-rss +http://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004/02/04/incompatible-rss """ +from __future__ import unicode_literals import datetime -import urllib.parse -import urllib.request, urllib.parse, urllib.error -import types -from decimal import Decimal -from xml.sax.saxutils import XMLGenerator - -# -- Imported from others modules ---------------------------------- - -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) - -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, (type(None), int)): - return s - elif not isinstance(s, str): - 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 str(s).encode(encoding, errors) - elif isinstance(s, str): - 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): - if iri is None: - return iri - return urllib.parse.quote(smart_str(iri), safe="/#%[]=:;$&()+,!?*@'~") - -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, ( - type(None), - int, - 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, str,): - if hasattr(s, '__unicode__'): - s = str(s) - else: - try: - s = str(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, str): - # 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 as e: - if not isinstance(s, Exception): - raise UnicodeDecodeError(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 - -# -- Start of the feedgenerator module -------------------------------------- +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. - if date.tzinfo: - time_str = date.strftime('%a, %d %b %Y %H:%M:%S ') + 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) + timezone = (offset.days * 24 * 60) + (offset.seconds // 60) hour, minute = divmod(timezone, 60) - return time_str + "%+03d%02d" % (hour, minute) + return time_str + '%+03d%02d' % (hour, minute) else: - return date.strftime('%a, %d %b %Y %H:%M:%S -0000') + return time_str + '-0000' def rfc3339_date(date): - if date.tzinfo: - time_str = date.strftime('%Y-%m-%dT%H:%M:%S') + # 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) + timezone = (offset.days * 24 * 60) + (offset.seconds // 60) hour, minute = divmod(timezone, 60) - return time_str + "%+03d:%02d" % (hour, minute) + return time_str + '%+03d:%02d' % (hour, minute) else: - return date.strftime('%Y-%m-%dT%H:%M:%SZ') + return time_str + 'Z' def get_tag_uri(url, date): """ Creates a TagURI. - See http://diveintomark.org/archives/2004/05/28/howto-atom-id + See http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id """ - url_split = urllib.parse.urlparse(url) - - # Python 2.4 didn't have named attributes on split results or the hostname. - hostname = getattr(url_split, 'hostname', url_split[1].split(':')[0]) - path = url_split[2] - fragment = url_split[5] - + bits = urlparse(url) d = '' if date is not None: - d = ',%s' % date.strftime('%Y-%m-%d') - return 'tag:%s%s:%s/%s' % (hostname, d, path, fragment) + 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_unicode(s, strings_only=True) + to_unicode = lambda s: force_text(s, strings_only=True) if categories: - categories = [force_unicode(c) for c in categories] + categories = [force_text(c) for c in categories] if ttl is not None: # Force ints to unicode - ttl = force_unicode(ttl) + ttl = force_text(ttl) self.feed = { 'title': to_unicode(title), 'link': iri_to_uri(link), @@ -210,12 +120,12 @@ class SyndicationFeed(object): 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) + 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_unicode(ttl) + ttl = force_text(ttl) item = { 'title': to_unicode(title), 'link': iri_to_uri(link), @@ -274,7 +184,6 @@ class SyndicationFeed(object): """ Returns the feed in the given encoding as a string. """ - from io import StringIO s = StringIO() self.write(s, encoding) return s.getvalue() @@ -299,7 +208,7 @@ class Enclosure(object): self.url = iri_to_uri(url) class RssFeed(SyndicationFeed): - mime_type = 'application/rss+xml' + mime_type = 'application/rss+xml; charset=utf-8' def write(self, outfile, encoding): handler = SimplerXMLGenerator(outfile, encoding) handler.startDocument() @@ -324,7 +233,9 @@ class RssFeed(SyndicationFeed): handler.addQuickElement("title", self.feed['title']) handler.addQuickElement("link", self.feed['link']) handler.addQuickElement("description", self.feed['description']) - handler.addQuickElement("atom:link", None, {"rel": "self", "href": self.feed['feed_url']}) + 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']: @@ -385,7 +296,7 @@ class Rss201rev2Feed(RssFeed): class Atom1Feed(SyndicationFeed): # Spec: http://atompub.org/2005/07/11/draft-ietf-atompub-format-10.html - mime_type = 'application/atom+xml' + mime_type = 'application/atom+xml; charset=utf-8' ns = "http://www.w3.org/2005/Atom" def write(self, outfile, encoding): 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) @@ -1,19 +1,24 @@ #! /usr/bin/python + +# Using setuptools rather than distutils to get the `develop` command from setuptools import setup -NAME = 'feedgenerator' -MODULES = ['feedgenerator'] -DESCRIPTION = 'Standalone version of django.utils.feedgenerator' +NAME = 'feedgenerator-py3k' +PACKAGES = ['feedgenerator'] +DESCRIPTION = 'Standalone version of django.utils.feedgenerator, compatible with Py3k' +LONG_DESCRIPTION = open('README.rst').read(), -URL = "http://hg.lolnet.org/feedgenerator/" +URL = "https://github.com/dmdm/feedgenerator-py3k.git" -CLASSIFIERS = ['Development Status :: 5 - Production/Stable', +CLASSIFIERS = ['Development Status :: 3 - Alpha', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.2', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules', @@ -21,15 +26,19 @@ CLASSIFIERS = ['Development Status :: 5 - Production/Stable', AUTHOR = 'Django Software Foundation' AUTHOR_EMAIL = 'foundation@djangoproject.com' -MAINTAINER = 'Alexis Metaireau' -MAINTAINER_EMAIL= 'alexis@notmyidea.org' +MAINTAINER = 'Dirk Makowski' +MAINTAINER_EMAIL= 'dm@parenchym.com' KEYWORDS = "feed atom rss".split(' ') -VERSION = '1.2.1' +VERSION = '1.5.dev' + +TEST_SUITE = 'tests' + +REQUIRES = ['pytz', 'six'] setup( name=NAME, version=VERSION, - py_modules=MODULES, + packages=PACKAGES, # metadata for upload to PyPI author=AUTHOR, @@ -37,7 +46,11 @@ setup( maintainer=MAINTAINER, maintainer_email=MAINTAINER_EMAIL, description=DESCRIPTION, + long_description=LONG_DESCRIPTION, keywords=KEYWORDS, url=URL, classifiers=CLASSIFIERS, + test_suite=TEST_SUITE, + install_requires=REQUIRES, + ) diff --git a/test.rss b/test.rss new file mode 100644 index 0000000..0518bdb --- /dev/null +++ b/test.rss @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Poynter E-Media Tidbits</title><link>http://www.poynter.org/column.asp?id=31</link><description>A group Weblog by the sharpest minds in online media/journalism/publishing. + Umlauts: äöüßÄÖÜ + Chinese: 老师是四十四,是不是? + Finnish: Mustan kissan paksut posket. (ah, no special chars) Kärpänen sanoi kärpäselle: tuu kattoon kattoon ku kaveri tapettiin tapettiin. + </description><language>en</language><lastBuildDate>Sat, 25 Aug 2012 23:03:26 -0000</lastBuildDate><item><title>Hello</title><link>http://www.holovaty.com/test/</link><description>Testing.</description></item></channel></rss>
\ No newline at end of file diff --git a/tests/test_001.py b/tests/test_001.py new file mode 100644 index 0000000..e074148 --- /dev/null +++ b/tests/test_001.py @@ -0,0 +1,20 @@ +# -*- encoding: utf-8 -*- +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. + Umlauts: äöüßÄÖÜ + Chinese: 老师是四十四,是不是? + Finnish: Mustan kissan paksut posket. (ah, no special chars) Kärpänen sanoi kärpäselle: tuu kattoon kattoon ku kaveri tapettiin tapettiin. + """, + 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') + @@ -0,0 +1,15 @@ +[tox] +envlist = py26,py27 + +[testenv] +commands = nosetests -s tests +deps = + nose + Jinja2 + Pygments + docutils + feedgenerator + unittest2 + mock + Markdown + BeautifulSoup |
