From 1ea2790ba9c0abff64f394f39018c72fa11b7190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20HUBSCHER?= Date: Sun, 16 Feb 2014 19:04:47 +0100 Subject: [PATCH 1/7] Refs #46 -- Add Py3 support. --- .travis.yml | 1 + demo/demoproject/virtual/views.py | 4 ++-- django_downloadview/apache/tests.py | 3 ++- django_downloadview/files.py | 2 +- django_downloadview/nginx/tests.py | 3 ++- django_downloadview/response.py | 24 ++++++++++++++---------- django_downloadview/test.py | 7 +++++-- django_downloadview/tests/api.py | 1 + django_downloadview/utils.py | 4 ++-- setup.py | 2 +- tox.ini | 4 +++- 11 files changed, 34 insertions(+), 21 deletions(-) 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/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..40f0a0e 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 @@ -14,28 +15,31 @@ from django.utils.encoding import force_str def encode_basename_ascii(value): """Return US-ASCII encoded ``value`` for use in Content-Disposition header. - >>> encode_basename_ascii(unicode('éà', 'utf-8')) + >>> encode_basename_ascii('éà') # doctest: +IGNORE_UNICODE u'ea' Spaces are converted to underscores. - >>> encode_basename_ascii(' ') + >>> encode_basename_ascii(' ') # doctest: +IGNORE_UNICODE u'_' Text with non US-ASCII characters is expected to be unicode. - >>> encode_basename_ascii('éà') # doctest: +ELLIPSIS + >>> from six import b + >>> encode_basename_ascii(b('éà')) # doctest: +ELLIPSIS Traceback (most recent call last): ... - UnicodeDecodeError: \'ascii\' codec can\'t decode byte ... + UnicodeDecodeError: ... Of course, ASCII values are not modified. - >>> encode_basename_ascii('ea') + >>> encode_basename_ascii('ea') # doctest: +IGNORE_UNICODE u'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') @@ -49,11 +53,11 @@ def encode_basename_utf8(value): >>> encode_basename_utf8(u' .txt') '%20.txt' - >>> encode_basename_utf8(unicode('éà', 'utf-8')) + >>> 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): @@ -70,7 +74,7 @@ def content_disposition(filename): 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')) + >>> content_disposition(u'é.txt') "attachment; filename=e.txt; filename*=UTF-8''%C3%A9.txt" """ 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..ea08f44 100644 --- a/django_downloadview/utils.py +++ b/django_downloadview/utils.py @@ -26,7 +26,7 @@ def url_basename(url, content_type): If URL contains extension, it is kept as-is. - >>> url_basename(u'/path/to/somefile.rst', 'text/plain') + >>> url_basename(u'/path/to/somefile.rst', 'text/plain') # doctest: +IGNORE_UNICODE u'somefile.rst' """ @@ -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/tox.ini b/tox.ini index 266a4d5..8bfcb8b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,12 @@ [tox] -envlist = py27,flake8,sphinx,readme +envlist = py27,py33,flake8,sphinx,readme [testenv] deps = nose rednose coverage + doctest-ignore-unicode commands = pip install -e ./ pip install -e demo/ @@ -28,6 +29,7 @@ deps = nose rednose Sphinx + doctest-ignore-unicode commands = make --directory=docs clean html doctest whitelist_externals = From 546bdc2461e07a77981c1e7e45ca67ad5850a9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Bryon?= Date: Tue, 25 Feb 2014 00:43:09 +0100 Subject: [PATCH 2/7] Test of README build to HTML is now done with 'rst2html' command via tox. --- tests/packaging.py | 41 ----------------------------------------- 1 file changed, 41 deletions(-) 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) From 70c6b00442b1daeccec94c9079efd2d6d4bc2613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Bryon?= Date: Tue, 25 Feb 2014 00:47:20 +0100 Subject: [PATCH 3/7] Refs #46 - Fixed Python 2 and Python 3 compatibility in doctests related to string/unicode. --- django_downloadview/response.py | 48 +++++++++++++++------------------ django_downloadview/utils.py | 4 +-- tox.ini | 2 -- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/django_downloadview/response.py b/django_downloadview/response.py index 40f0a0e..c22775f 100644 --- a/django_downloadview/response.py +++ b/django_downloadview/response.py @@ -13,28 +13,22 @@ 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('éà') # doctest: +IGNORE_UNICODE - u'ea' + >>> print(encode_basename_ascii(u'éà')) + ea Spaces are converted to underscores. - >>> encode_basename_ascii(' ') # doctest: +IGNORE_UNICODE - u'_' - - Text with non US-ASCII characters is expected to be unicode. - - >>> from six import b - >>> encode_basename_ascii(b('éà')) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - UnicodeDecodeError: ... + >>> print(encode_basename_ascii(' ')) + _ Of course, ASCII values are not modified. - >>> encode_basename_ascii('ea') # doctest: +IGNORE_UNICODE - u'ea' + >>> print(encode_basename_ascii('ea')) + ea + >>> print(encode_basename_ascii(b'ea')) + ea """ if isinstance(value, six.binary_type): @@ -48,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(u'éà') - '%C3%A9%C3%A0' + >>> print(encode_basename_utf8(u'éà')) + %C3%A9%C3%A0 """ 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(u'é.txt') - "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/utils.py b/django_downloadview/utils.py index ea08f44..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') # doctest: +IGNORE_UNICODE - u'somefile.rst' + >>> print(url_basename(u'/path/to/somefile.rst', 'text/plain')) + somefile.rst """ return url.split('/')[-1] diff --git a/tox.ini b/tox.ini index 8bfcb8b..bef2e6a 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,6 @@ deps = nose rednose coverage - doctest-ignore-unicode commands = pip install -e ./ pip install -e demo/ @@ -29,7 +28,6 @@ deps = nose rednose Sphinx - doctest-ignore-unicode commands = make --directory=docs clean html doctest whitelist_externals = From cfb6964af3724865e3e53ff70a55565a6a59eb9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Bryon?= Date: Tue, 25 Feb 2014 00:56:50 +0100 Subject: [PATCH 4/7] Minor changes in tox configuration file. --- tox.ini | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index bef2e6a..5f75b0b 100644 --- a/tox.ini +++ b/tox.ini @@ -6,10 +6,12 @@ deps = nose rednose coverage +setenv = + DJANGO_SETTINGS_MODULE = demoproject.settings commands = pip install -e ./ pip install -e demo/ - demo test --nose-verbosity=2 -c etc/nose/base.cfg -c etc/nose/django_downloadview.cfg django_downloadview + nosetests --verbosity=2 -c etc/nose/base.cfg -c etc/nose/django_downloadview.cfg django_downloadview/ demo test --nose-verbosity=2 demoproject rm .coverage pip freeze @@ -39,6 +41,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 From 2d4f8c977311ff72123066ce67e770d0618a2e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Bryon?= Date: Tue, 25 Feb 2014 00:57:07 +0100 Subject: [PATCH 5/7] Refs #46 - Updated changelog. --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) 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. From 5cde2b0d2dbcea475d05e5e3c08fbc3998a96ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Bryon?= Date: Tue, 25 Feb 2014 01:06:59 +0100 Subject: [PATCH 6/7] Revert "Minor changes in tox configuration file." This reverts commit cfb6964af3724865e3e53ff70a55565a6a59eb9d. --- tox.ini | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 5f75b0b..bef2e6a 100644 --- a/tox.ini +++ b/tox.ini @@ -6,12 +6,10 @@ deps = nose rednose coverage -setenv = - DJANGO_SETTINGS_MODULE = demoproject.settings commands = pip install -e ./ pip install -e demo/ - nosetests --verbosity=2 -c etc/nose/base.cfg -c etc/nose/django_downloadview.cfg django_downloadview/ + demo test --nose-verbosity=2 -c etc/nose/base.cfg -c etc/nose/django_downloadview.cfg django_downloadview demo test --nose-verbosity=2 demoproject rm .coverage pip freeze @@ -41,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 From 5850ffac81f770befc50b0dc6237e8769ed740b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Bryon?= Date: Tue, 25 Feb 2014 01:07:59 +0100 Subject: [PATCH 7/7] Fixed indentation in tox.ini file. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index bef2e6a..e64f108 100644 --- a/tox.ini +++ b/tox.ini @@ -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