summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk Makowski <dmakowski@gmx.net>2012-08-25 23:14:28 +0200
committerDirk Makowski <dmakowski@gmx.net>2012-08-25 23:14:28 +0200
commit81c2749d1e42463c5fd7b987f570fab71d1cce38 (patch)
tree5057de5fcc32e57818a95f9b549624379295ee42
parent77dcfa6b2c0b8df13c34d0f2b76556b4c0569c8e (diff)
downloadfeedgenerator-81c2749d1e42463c5fd7b987f570fab71d1cce38.tar.gz
Initial commit
-rw-r--r--PKG-INFO20
-rw-r--r--README.md4
-rw-r--r--README.rst13
-rw-r--r--feedgenerator/__init__.py24
-rw-r--r--feedgenerator/django/__init__.py0
-rw-r--r--feedgenerator/django/utils/__init__.py0
-rw-r--r--feedgenerator/django/utils/datetime_safe.py89
-rw-r--r--feedgenerator/django/utils/encoding.py256
-rw-r--r--feedgenerator/django/utils/feedgenerator.py (renamed from feedgenerator.py)205
-rw-r--r--feedgenerator/django/utils/functional.py370
-rw-r--r--feedgenerator/django/utils/six.py370
-rw-r--r--feedgenerator/django/utils/timezone.py292
-rw-r--r--feedgenerator/django/utils/xmlutils.py14
-rw-r--r--setup.py31
-rw-r--r--test.rss6
-rw-r--r--tests/test_001.py20
-rw-r--r--tox.ini15
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)
diff --git a/setup.py b/setup.py
index 263db20..5316808 100644
--- a/setup.py
+++ b/setup.py
@@ -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')
+
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..a7dc9ec
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,15 @@
+[tox]
+envlist = py26,py27
+
+[testenv]
+commands = nosetests -s tests
+deps =
+ nose
+ Jinja2
+ Pygments
+ docutils
+ feedgenerator
+ unittest2
+ mock
+ Markdown
+ BeautifulSoup