From 95b36fc8436f18aab7cb91249735d9de5e23713b Mon Sep 17 00:00:00 2001 From: Peter Marheine Date: Thu, 23 Dec 2021 10:27:14 +1100 Subject: [PATCH 1/8] Import ABCs from collections.abc, not collections The types in collections.abc were moved from just collections in Python 3.3, and Python 3.10 removed the old aliases. We no longer support Python versions earlier than 3.3 and need to support 3.10, so update the import. --- django_downloadview/middlewares.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django_downloadview/middlewares.py b/django_downloadview/middlewares.py index 557498d..51888b2 100644 --- a/django_downloadview/middlewares.py +++ b/django_downloadview/middlewares.py @@ -4,7 +4,7 @@ Download middlewares capture :py:class:`django_downloadview.DownloadResponse` responses and may replace them with optimized download responses. """ -import collections +import collections.abc import copy import os @@ -160,7 +160,7 @@ class SmartDownloadMiddleware(BaseDownloadMiddleware): for key, options in enumerate(options_list): args = [] kwargs = {} - if isinstance(options, collections.Mapping): # Using kwargs. + if isinstance(options, collections.abc.Mapping): # Using kwargs. kwargs = options else: args = options From cb3ec3a09137a94307a58f53bf33b96b33e4d66c Mon Sep 17 00:00:00 2001 From: Peter Marheine Date: Thu, 23 Dec 2021 10:33:08 +1100 Subject: [PATCH 2/8] Stop using nosetests Nose is no longer maintained and is incompatible with Python 3.10, so can no longer be used. This change runs `coverage` manually to collect coverage and uses `pytest` to run doctests, collectively covering what was tested using django_nose. --- demo/demoproject/settings.py | 11 ----------- demo/setup.py | 2 +- tox.ini | 16 ++++++++++++++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/demo/demoproject/settings.py b/demo/demoproject/settings.py index 6e690f1..87e4bc4 100644 --- a/demo/demoproject/settings.py +++ b/demo/demoproject/settings.py @@ -53,8 +53,6 @@ INSTALLED_APPS = ( "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", - # Stuff that must be at the end. - "django_nose", ) @@ -111,15 +109,6 @@ DOWNLOADVIEW_RULES += [ # Test/development settings. DEBUG = True -TEST_RUNNER = "django_nose.NoseTestSuiteRunner" -NOSE_ARGS = [ - "--verbosity=2", - "--no-path-adjustment", - "--nocapture", - "--all-modules", - "--with-coverage", - "--with-doctest", -] TEMPLATES = [ diff --git a/demo/setup.py b/demo/setup.py index 1055fa7..3b9a946 100644 --- a/demo/setup.py +++ b/demo/setup.py @@ -21,6 +21,6 @@ setup( packages=["demoproject"], include_package_data=True, zip_safe=False, - install_requires=["django-downloadview", "django-nose"], + install_requires=["django-downloadview", "pytest-django"], entry_points={"console_scripts": ["demo = demoproject.manage:main"]}, ) diff --git a/tox.ini b/tox.ini index e228195..8a1fbf3 100644 --- a/tox.ini +++ b/tox.ini @@ -27,11 +27,16 @@ deps = dj31: Django>=3.1,<3.2 dj32: Django>=3.2,<3.3 djmain: https://github.com/django/django/archive/main.tar.gz - nose + pytest + pytest-cov commands = pip install -e . pip install -e demo - python -Wd {envbindir}/demo test --cover-package=django_downloadview --cover-package=demoproject --cover-xml {posargs: tests demoproject} + # doctests + pytest --cov=django_downloadview --cov=demoproject {posargs} + # all other test cases + coverage run --append {envbindir}/demo test {posargs: tests demoproject} + coverage xml pip freeze ignore_outcome = djmain: True @@ -65,3 +70,10 @@ commands = [flake8] max-line-length = 88 ignore = E203, W503 + +[coverage:run] +source = django_downloadview,demo + +[pytest] +DJANGO_SETTINGS_MODULE = demoproject.settings +addopts = --doctest-modules --ignore=docs/ From 2524668e865fdaf66089e656ef673de92e2b3656 Mon Sep 17 00:00:00 2001 From: Peter Marheine Date: Thu, 23 Dec 2021 10:40:19 +1100 Subject: [PATCH 3/8] Test on Python 3.10 --- .github/workflows/test.yml | 13 ++++++++++++- tox.ini | 5 +++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce0df1c..f5e25e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,8 +10,19 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.6', '3.7', '3.8', '3.9'] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] django-version: ['2.2', '3.1', '3.2', 'main'] + exclude: + # Django prior to 3.2 does not support Python 3.10 + - django-version: '2.2' + python-version: '3.10' + - django-version: '3.1' + python-version: '3.10' + # Django after 3.2 drop support for Python prior to 3.8 + - django-version: 'main' + python-version: '3.6' + - django-version: 'main' + python-version: '3.7' steps: - uses: actions/checkout@v2 diff --git a/tox.ini b/tox.ini index 8a1fbf3..72c46c4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = - py{36,37,38,39}-dj{22,31,32} - py{38,39}-djmain + py{36,37,38,39,310}-dj{22,31,32} + py{38,39,310}-djmain lint sphinx readme @@ -12,6 +12,7 @@ python = 3.7: py37 3.8: py38, lint, sphinx, readme 3.9: py39 + 3.10: py310 [gh-actions:env] DJANGO = From 0ab8aa3e8f42cebb266ee1d7d256563964d872e0 Mon Sep 17 00:00:00 2001 From: Peter Marheine Date: Thu, 23 Dec 2021 11:22:52 +1100 Subject: [PATCH 4/8] Stop using django.util.deprecation.MiddlewareMixin That class is intended primarily for compatibility with Pre-1.10 middleware, and recently gained a check that get_response is not None. This package ensures an unexpecified `get_response` function is never called on its own, so it's simplest to manually implement the middleware API. --- django_downloadview/middlewares.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/django_downloadview/middlewares.py b/django_downloadview/middlewares.py index 51888b2..ff9df36 100644 --- a/django_downloadview/middlewares.py +++ b/django_downloadview/middlewares.py @@ -14,14 +14,6 @@ from django.core.exceptions import ImproperlyConfigured from django_downloadview.response import DownloadResponse from django_downloadview.utils import import_member -try: - from django.utils.deprecation import MiddlewareMixin -except ImportError: - - class MiddlewareMixin(object): - def __init__(self, get_response=None): - super(MiddlewareMixin, self).__init__() - #: Sentinel value to detect whether configuration is to be loaded from Django #: settings or not. @@ -38,12 +30,18 @@ def is_download_response(response): return isinstance(response, DownloadResponse) -class BaseDownloadMiddleware(MiddlewareMixin): +class BaseDownloadMiddleware: """Base (abstract) Django middleware that handles download responses. Subclasses **must** implement :py:meth:`process_download_response` method. """ + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + return self.process_response(request, response) def is_download_response(self, response): """Return True if ``response`` can be considered as a file download. From a64a0e8c33e7e91cc5cabd208b12498d601dfd98 Mon Sep 17 00:00:00 2001 From: Peter Marheine Date: Thu, 23 Dec 2021 11:24:48 +1100 Subject: [PATCH 5/8] Split DownloadDispatcherMiddleware into two classes Instantiating a middleware but not using it as a middleware was a strange behavior, so this change splits the dispatching out to another class with a more specialized API and uses that middleware. --- django_downloadview/middlewares.py | 35 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/django_downloadview/middlewares.py b/django_downloadview/middlewares.py index ff9df36..782917b 100644 --- a/django_downloadview/middlewares.py +++ b/django_downloadview/middlewares.py @@ -85,11 +85,8 @@ class RealDownloadMiddleware(BaseDownloadMiddleware): return False -class DownloadDispatcherMiddleware(BaseDownloadMiddleware): - "Download middleware that dispatches job to several middleware instances." - - def __init__(self, get_response=None, middlewares=AUTO_CONFIGURE): - super(DownloadDispatcherMiddleware, self).__init__(get_response) +class DownloadDispatcher: + def __init__(self, middlewares=AUTO_CONFIGURE): #: List of children middlewares. self.middlewares = middlewares if self.middlewares is AUTO_CONFIGURE: @@ -105,27 +102,35 @@ class DownloadDispatcherMiddleware(BaseDownloadMiddleware): middleware = factory(**kwargs) self.middlewares.append((key, middleware)) - def process_download_response(self, request, response): + def dispatch(self, request, response): """Dispatches job to children middlewares.""" for (key, middleware) in self.middlewares: response = middleware.process_response(request, response) return response -class SmartDownloadMiddleware(BaseDownloadMiddleware): +class DownloadDispatcherMiddleware(BaseDownloadMiddleware): + "Download middleware that dispatches job to several middleware instances." + + def __init__(self, get_response, middlewares=AUTO_CONFIGURE): + super(DownloadDispatcherMiddleware, self).__init__(get_response) + self.dispatcher = DownloadDispatcher(middlewares) + + def process_download_response(self, request, response): + return self.dispatcher.dispatch(request, response) + + +class SmartDownloadMiddleware(DownloadDispatcherMiddleware): """Easy to configure download middleware.""" def __init__( self, - get_response=None, + get_response, backend_factory=AUTO_CONFIGURE, backend_options=AUTO_CONFIGURE, ): """Constructor.""" - super(SmartDownloadMiddleware, self).__init__(get_response) - #: :class:`DownloadDispatcher` instance that can hold multiple - #: backend instances. - self.dispatcher = DownloadDispatcherMiddleware(middlewares=[]) + super(SmartDownloadMiddleware, self).__init__(get_response, middlewares=[]) #: Callable (typically a class) to instantiate backend (typically a #: :class:`DownloadMiddleware` subclass). self.backend_factory = backend_factory @@ -170,10 +175,6 @@ class SmartDownloadMiddleware(BaseDownloadMiddleware): middleware_instance = factory(*args, **kwargs) self.dispatcher.middlewares.append((key, middleware_instance)) - def process_download_response(self, request, response): - """Use :attr:`dispatcher` to process download response.""" - return self.dispatcher.process_download_response(request, response) - class NoRedirectionMatch(Exception): """Response object does not match redirection rules.""" @@ -183,7 +184,7 @@ class ProxiedDownloadMiddleware(RealDownloadMiddleware): """Base class for middlewares that use optimizations of reverse proxies.""" def __init__( - self, get_response=None, source_dir=None, source_url=None, destination_url=None + self, get_response, source_dir=None, source_url=None, destination_url=None ): """Constructor.""" super(ProxiedDownloadMiddleware, self).__init__(get_response) From 198f6a32950b5193009bb34e2bc0d0c9cdee0f5f Mon Sep 17 00:00:00 2001 From: Peter Marheine Date: Thu, 9 Dec 2021 11:10:01 +1100 Subject: [PATCH 6/8] Update compatibility for Django 4.0 The only meaningful change is removing use of `force_text` (which was deprecated in Django 3.0) in favor of `force_str` (which has existed since before Django 1.11). On Python 3 there is no functional difference between the two. --- django_downloadview/io.py | 4 ++-- setup.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/django_downloadview/io.py b/django_downloadview/io.py index f619813..b1d1778 100644 --- a/django_downloadview/io.py +++ b/django_downloadview/io.py @@ -1,7 +1,7 @@ """Low-level IO operations, for use with file wrappers.""" import io -from django.utils.encoding import force_bytes, force_text +from django.utils.encoding import force_bytes, force_str class TextIteratorIO(io.TextIOBase): @@ -32,7 +32,7 @@ class TextIteratorIO(io.TextIOBase): break else: # Make sure we handle text. - self._left = force_text(self._left) + self._left = force_str(self._left) ret = self._left[:n] self._left = self._left[len(ret) :] return ret diff --git a/setup.py b/setup.py index b23326c..1175853 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ setup( 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.1', 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.0', ], keywords=" ".join( [ From 6381dc94f1aeb110320f67e5bfb882fe894a4848 Mon Sep 17 00:00:00 2001 From: Peter Marheine Date: Tue, 21 Dec 2021 07:49:35 +1100 Subject: [PATCH 7/8] Include Django 4.0 in the test matrix --- .github/workflows/test.yml | 8 ++++++-- tox.ini | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f5e25e6..93d4cce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,16 +11,20 @@ jobs: max-parallel: 5 matrix: python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] - django-version: ['2.2', '3.1', '3.2', 'main'] + django-version: ['2.2', '3.1', '3.2', '4.0', 'main'] exclude: # Django prior to 3.2 does not support Python 3.10 - django-version: '2.2' python-version: '3.10' - django-version: '3.1' python-version: '3.10' - # Django after 3.2 drop support for Python prior to 3.8 + # Django after 3.2 dropped support for Python prior to 3.8 + - django-version: '4.0' + python-version: '3.6' - django-version: 'main' python-version: '3.6' + - django-version: '4.0' + python-version: '3.7' - django-version: 'main' python-version: '3.7' diff --git a/tox.ini b/tox.ini index 72c46c4..9078c64 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = py{36,37,38,39,310}-dj{22,31,32} - py{38,39,310}-djmain + py{38,39,310}-dj{40,main} lint sphinx readme @@ -19,6 +19,7 @@ DJANGO = 2.2: dj22 3.1: dj31 3.2: dj32 + 4.0: dj40 main: djmain [testenv] @@ -27,6 +28,7 @@ deps = dj22: Django>=2.2,<3.0 dj31: Django>=3.1,<3.2 dj32: Django>=3.2,<3.3 + dj40: Django>=4.0,<4.1 djmain: https://github.com/django/django/archive/main.tar.gz pytest pytest-cov From e9fbb74b2cce0b7e287a842c1e0bef6e205b9909 Mon Sep 17 00:00:00 2001 From: Peter Marheine Date: Thu, 23 Dec 2021 20:27:29 +1100 Subject: [PATCH 8/8] Uncap Github Actions parallelism There seems to be no reason to limit tests to 5 jobs at a time; removing the limit should allow tests to finish faster. --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93d4cce..80c55e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,6 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - max-parallel: 5 matrix: python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] django-version: ['2.2', '3.1', '3.2', '4.0', 'main']