diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce0df1c..80c55e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,10 +8,24 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - max-parallel: 5 matrix: - python-version: ['3.6', '3.7', '3.8', '3.9'] - django-version: ['2.2', '3.1', '3.2', 'main'] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + 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 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' steps: - uses: actions/checkout@v2 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/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/django_downloadview/middlewares.py b/django_downloadview/middlewares.py index 557498d..782917b 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 @@ -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. @@ -87,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: @@ -107,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 @@ -160,7 +163,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 @@ -172,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.""" @@ -185,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) 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( [ diff --git a/tox.ini b/tox.ini index e228195..9078c64 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}-dj{40,main} lint sphinx readme @@ -12,12 +12,14 @@ python = 3.7: py37 3.8: py38, lint, sphinx, readme 3.9: py39 + 3.10: py310 [gh-actions:env] DJANGO = 2.2: dj22 3.1: dj31 3.2: dj32 + 4.0: dj40 main: djmain [testenv] @@ -26,12 +28,18 @@ 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 - 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 +73,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/