diff --git a/.travis.yml b/.travis.yml index f447312..c3c57cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python env: - TOXENV=py27 + - TOXENV=py33 - TOXENV=flake8 - TOXENV=sphinx - TOXENV=readme diff --git a/CHANGELOG b/CHANGELOG index 89f37c9..9a74610 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,8 @@ future releases, check `milestones`_ and :doc:`/about/vision`. 1.6 (unreleased) ---------------- +- Feature #46: introduced support for Python>=3.3. + - Feature #74: the Makefile in project's repository no longer creates a virtualenv. Developers setup the environment as they like, i.e. using virtualenv, virtualenvwrapper or whatever. Tests are run with tox. diff --git a/demo/demoproject/virtual/views.py b/demo/demoproject/virtual/views.py index 8bb7020..805651f 100644 --- a/demo/demoproject/virtual/views.py +++ b/demo/demoproject/virtual/views.py @@ -1,4 +1,4 @@ -from StringIO import StringIO +from six import StringIO from django.core.files.base import ContentFile @@ -10,7 +10,7 @@ from django_downloadview import StringIteratorIO class TextDownloadView(VirtualDownloadView): def get_file(self): """Return :class:`django.core.files.base.ContentFile` object.""" - return ContentFile(u"Hello world!\n", name='hello-world.txt') + return ContentFile(b"Hello world!\n", name='hello-world.txt') class StringIODownloadView(VirtualDownloadView): diff --git a/django_downloadview/apache/tests.py b/django_downloadview/apache/tests.py index b0ac5db..0aa17b0 100644 --- a/django_downloadview/apache/tests.py +++ b/django_downloadview/apache/tests.py @@ -1,3 +1,4 @@ +from six import iteritems from django_downloadview.apache.response import XSendfileResponse @@ -21,7 +22,7 @@ class XSendfileValidator(object): """ self.assert_x_sendfile_response(test_case, response) - for key, value in assertions.iteritems(): + for key, value in iteritems(assertions): assert_func = getattr(self, 'assert_%s' % key) assert_func(test_case, response, value) diff --git a/django_downloadview/files.py b/django_downloadview/files.py index 41ebb6b..e41534d 100644 --- a/django_downloadview/files.py +++ b/django_downloadview/files.py @@ -2,7 +2,7 @@ """File wrappers for use as exchange data between views and responses.""" from __future__ import absolute_import from io import BytesIO -from urlparse import urlparse +from six.moves.urllib.parse import urlparse from django.core.files.base import File from django.utils.encoding import force_bytes diff --git a/django_downloadview/nginx/tests.py b/django_downloadview/nginx/tests.py index 0cf7349..02bfec9 100644 --- a/django_downloadview/nginx/tests.py +++ b/django_downloadview/nginx/tests.py @@ -1,3 +1,4 @@ +from six import iteritems from django_downloadview.nginx.response import XAccelRedirectResponse @@ -35,7 +36,7 @@ class XAccelRedirectValidator(object): """ self.assert_x_accel_redirect_response(test_case, response) - for key, value in assertions.iteritems(): + for key, value in iteritems(assertions): assert_func = getattr(self, 'assert_%s' % key) assert_func(test_case, response, value) diff --git a/django_downloadview/response.py b/django_downloadview/response.py index 9cb7de7..c22775f 100644 --- a/django_downloadview/response.py +++ b/django_downloadview/response.py @@ -4,7 +4,8 @@ import os import mimetypes import re import unicodedata -import urllib +import six +from six.moves import urllib from django.conf import settings from django.http import HttpResponse, StreamingHttpResponse @@ -12,30 +13,27 @@ from django.utils.encoding import force_str def encode_basename_ascii(value): - """Return US-ASCII encoded ``value`` for use in Content-Disposition header. + u"""Return US-ASCII encoded ``value`` for Content-Disposition header. - >>> encode_basename_ascii(unicode('éà', 'utf-8')) - u'ea' + >>> print(encode_basename_ascii(u'éà')) + ea Spaces are converted to underscores. - >>> encode_basename_ascii(' ') - u'_' - - Text with non US-ASCII characters is expected to be unicode. - - >>> encode_basename_ascii('éà') # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - UnicodeDecodeError: \'ascii\' codec can\'t decode byte ... + >>> print(encode_basename_ascii(' ')) + _ Of course, ASCII values are not modified. - >>> encode_basename_ascii('ea') - u'ea' + >>> print(encode_basename_ascii('ea')) + ea + >>> print(encode_basename_ascii(b'ea')) + ea """ - ascii_basename = unicode(value) + if isinstance(value, six.binary_type): + value = value.decode('utf-8') + ascii_basename = six.text_type(value) ascii_basename = unicodedata.normalize('NFKD', ascii_basename) ascii_basename = ascii_basename.encode('ascii', 'ignore') ascii_basename = ascii_basename.decode('ascii') @@ -44,34 +42,34 @@ def encode_basename_ascii(value): def encode_basename_utf8(value): - """Return UTF-8 encoded ``value`` for use in Content-Disposition header. + u"""Return UTF-8 encoded ``value`` for use in Content-Disposition header. - >>> encode_basename_utf8(u' .txt') - '%20.txt' + >>> print(encode_basename_utf8(u' .txt')) + %20.txt - >>> encode_basename_utf8(unicode('éà', 'utf-8')) - '%C3%A9%C3%A0' + >>> print(encode_basename_utf8(u'éà')) + %C3%A9%C3%A0 """ - return urllib.quote(force_str(value)) + return urllib.parse.quote(force_str(value)) def content_disposition(filename): - """Return value of ``Content-Disposition`` header with 'attachment'. + u"""Return value of ``Content-Disposition`` header with 'attachment'. - >>> content_disposition('demo.txt') - 'attachment; filename=demo.txt' + >>> print(content_disposition('demo.txt')) + attachment; filename=demo.txt If filename is empty, only "attachment" is returned. - >>> content_disposition('') - 'attachment' + >>> print(content_disposition('')) + attachment If filename contains non US-ASCII characters, the returned value contains UTF-8 encoded filename and US-ASCII fallback. - >>> content_disposition(unicode('é.txt', 'utf-8')) - "attachment; filename=e.txt; filename*=UTF-8''%C3%A9.txt" + >>> print(content_disposition(u'é.txt')) + attachment; filename=e.txt; filename*=UTF-8''%C3%A9.txt """ if not filename: diff --git a/django_downloadview/test.py b/django_downloadview/test.py index e9fc454..714e8c2 100644 --- a/django_downloadview/test.py +++ b/django_downloadview/test.py @@ -1,5 +1,6 @@ """Testing utilities.""" import shutil +from six import iteritems import tempfile from django.conf import settings @@ -101,7 +102,7 @@ class DownloadResponseValidator(object): """ self.assert_download_response(test_case, response) - for key, value in assertions.iteritems(): + for key, value in iteritems(assertions): assert_func = getattr(self, 'assert_%s' % key) assert_func(test_case, response, value) @@ -138,7 +139,9 @@ class DownloadResponseValidator(object): test_case.assertTrue(response['Content-Type'].startswith(value)) def assert_content(self, test_case, response, value): - test_case.assertEqual(''.join(response.streaming_content), value) + test_case.assertEqual( + ''.join([s.decode('utf-8') for s in response.streaming_content]), + value) def assert_attachment(self, test_case, response, value): test_case.assertEqual('attachment;' in response['Content-Disposition'], diff --git a/django_downloadview/tests/api.py b/django_downloadview/tests/api.py index 4113043..ce2ebfc 100644 --- a/django_downloadview/tests/api.py +++ b/django_downloadview/tests/api.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Test suite around :mod:`django_downloadview.api` and deprecation plan.""" import unittest +from six.moves import reload_module as reload import warnings from django.core.exceptions import ImproperlyConfigured diff --git a/django_downloadview/utils.py b/django_downloadview/utils.py index f5b3326..b1facc1 100644 --- a/django_downloadview/utils.py +++ b/django_downloadview/utils.py @@ -26,8 +26,8 @@ def url_basename(url, content_type): If URL contains extension, it is kept as-is. - >>> url_basename(u'/path/to/somefile.rst', 'text/plain') - u'somefile.rst' + >>> print(url_basename(u'/path/to/somefile.rst', 'text/plain')) + somefile.rst """ return url.split('/')[-1] @@ -43,5 +43,5 @@ def import_member(import_string): """ module_name, factory_name = str(import_string).rsplit('.', 1) - module = __import__(module_name, globals(), locals(), [factory_name], -1) + module = __import__(module_name, globals(), locals(), [factory_name], 0) return getattr(module, factory_name) diff --git a/setup.py b/setup.py index 12809e6..d6f037b 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ KEYWORDS = ['file', 'mod_xsendfile', 'offload'] PACKAGES = [NAME.replace('-', '_')] -REQUIREMENTS = ['setuptools', 'Django>=1.5', 'requests'] +REQUIREMENTS = ['setuptools', 'Django>=1.5', 'requests', 'six'] if IS_PYTHON2: REQUIREMENTS.append('mock') ENTRY_POINTS = {} diff --git a/tests/packaging.py b/tests/packaging.py index 97b673e..7a97105 100644 --- a/tests/packaging.py +++ b/tests/packaging.py @@ -50,44 +50,3 @@ class VersionTestCase(unittest.TestCase): 'You may need to run ``make develop`` to update the ' 'installed version in development environment.' % (self.get_version(), file_version)) - - -class ReadMeTestCase(unittest.TestCase): - """Test suite around README file.""" - def test_readme_build(self): - """README builds to HTML without errors.""" - # Run build. - import docutils.core - import docutils.io - source = open(os.path.join(project_dir, 'README.rst')).read() - writer_name = 'html' - import sys - from StringIO import StringIO - stderr_backup = sys.stderr - sys.stderr = StringIO() - output, pub = docutils.core.publish_programmatically( - source=source, - source_class=docutils.io.StringInput, - source_path=None, - destination_class=docutils.io.StringOutput, - destination=None, - destination_path=None, - reader=None, - reader_name='standalone', - parser=None, - parser_name='restructuredtext', - writer=None, - writer_name=writer_name, - settings=None, - settings_spec=None, - settings_overrides=None, - config_section=None, - enable_exit_status=False) - sys.stderr = stderr_backup - errors = pub._stderr.stream.getvalue() - # Check result. - self.assertFalse(errors, "Docutils reported errors while building " - "readme content from reStructuredText to " - "HTML. So PyPI would display the readme as " - "text instead of HTML. Errors are:\n%s" - % errors) diff --git a/tox.ini b/tox.ini index 266a4d5..e64f108 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,flake8,sphinx,readme +envlist = py27,py33,flake8,sphinx,readme [testenv] deps = @@ -39,6 +39,6 @@ deps = pygments commands = mkdir -p var/docs - rst2html.py --exit-status=2 README.rst var/docs/README.html + rst2html.py --exit-status=2 README.rst var/docs/README.html whitelist_externals = mkdir