From 7608d4f5061e954e17753b4ff3997356a1ffde5d Mon Sep 17 00:00:00 2001 From: Anton Agestam Date: Tue, 22 Oct 2019 20:54:55 +0200 Subject: [PATCH] switch to pytest and fix #157 - Fix #157: catch pydoc.ErrorDuringImport - Remove runtests.py and use pytest instead - Add Python 3.8 to test matrix - Update copyright note in readme - Add deprecation warning for using BotoStrategy --- .travis.yml | 7 +- LICENSE | 2 +- Makefile | 4 +- README.rst | 2 +- collectfast/strategies/base.py | 13 ++-- collectfast/strategies/boto.py | 6 ++ .../strategies/test_caching_hash_strategy.py | 6 +- .../tests/strategies/test_guess_strategy.py | 14 ++-- .../tests/strategies/test_hash_strategy.py | 6 +- collectfast/tests/test_command.py | 24 +++---- collectfast/tests/utils.py | 6 +- conftest.py | 18 ++++++ runtests.py | 64 ------------------- setup.cfg | 3 + test-requirements.txt | 2 + 15 files changed, 74 insertions(+), 103 deletions(-) create mode 100644 conftest.py delete mode 100755 runtests.py diff --git a/.travis.yml b/.travis.yml index 7b59ec2..d87ce8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ python: - '3.5' - '3.6' - '3.7' + - '3.8' install: - pip install --upgrade pip setuptools - pip install django=="$DJANGO" @@ -26,7 +27,7 @@ script: - 'if [[ $(python --version) == "Python 3.7."* ]]; then black --check .; fi' - 'if [[ $(python --version) == "Python 3.7."* ]]; then sorti --check .; fi' - 'if [[ $(python --version) == "Python 3.7."* && "$DJANGO" != "1.11" ]]; then pip install . && mypy .; fi' - - coverage run --source collectfast ./runtests.py + - coverage run --source collectfast -m pytest after_script: - coveralls matrix: @@ -35,5 +36,9 @@ matrix: python: '3.4' - env: DJANGO=2.2 python: '3.4' + - env: DJANGO=1.11 + python: '3.8' + - env: DJANGO=2.1 + python: '3.8' notifications: email: false diff --git a/LICENSE b/LICENSE index d98e358..2cea426 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2013-2017 Anton Agestam +Copyright (c) 2013-2019 Anton Agestam Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 46ea9ab..dbcbcb9 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ SHELL := /usr/bin/env bash test: - . storage-credentials && ./runtests.py + . storage-credentials && pytest test-coverage: - . storage-credentials && coverage run --source collectfast ./runtests.py + . storage-credentials && coverage run --source collectfast -m pytest distribute: pip install --upgrade wheel twine setuptools diff --git a/README.rst b/README.rst index 9855f54..f9962de 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ A faster collectstatic command. **Supported Storage Backends** -- ``storages.backends.s3boto.S3BotoStorage`` (deprecated) +- ``storages.backends.s3boto.S3BotoStorage`` (deprecated, will be removed in 2.0) - ``storages.backends.s3boto3.S3Boto3Storage`` - ``storages.backends.gcloud.GoogleCloudStorage`` - ``django.core.files.storage.FileSystemStorage`` diff --git a/collectfast/strategies/base.py b/collectfast/strategies/base.py index 3b0ca0e..1705b3f 100644 --- a/collectfast/strategies/base.py +++ b/collectfast/strategies/base.py @@ -3,10 +3,10 @@ import hashlib import logging import mimetypes +import pydoc import warnings from functools import lru_cache from io import BytesIO -from pydoc import locate from typing import ClassVar from typing import Generic from typing import Optional @@ -140,7 +140,7 @@ def get_gzipped_local_file_hash(self, uncompressed_file_hash, path, contents): def load_strategy(klass: Union[str, type, object]) -> Type[Strategy[Storage]]: if isinstance(klass, str): - klass = locate(klass) + klass = pydoc.locate(klass) if not isinstance(klass, type) or not issubclass(klass, Strategy): raise ImproperlyConfigured( "Configured strategies must be subclasses of %s.%s" @@ -157,11 +157,11 @@ def load_strategy(klass: Union[str, type, object]) -> Type[Strategy[Storage]]: def _resolves_to_subclass(subclass_ref: str, superclass_ref: str) -> bool: try: - subclass = locate(subclass_ref) + subclass = pydoc.locate(subclass_ref) assert isinstance(subclass, type) - superclass = locate(superclass_ref) + superclass = pydoc.locate(superclass_ref) assert isinstance(superclass, type) - except (ImportError, AssertionError) as e: + except (ImportError, AssertionError, pydoc.ErrorDuringImport) as e: logger.debug("Failed to import %s: %s" % (superclass_ref, e)) return False return issubclass(subclass, superclass) @@ -171,7 +171,8 @@ def guess_strategy(storage: str) -> str: warnings.warn( "Falling back to guessing strategy for backwards compatibility. This " "is deprecated and will be removed in a future release. Explicitly " - "set COLLECTFAST_STRATEGY to silence this warning." + "set COLLECTFAST_STRATEGY to silence this warning.", + DeprecationWarning, ) if storage == _BOTO_STORAGE: return _BOTO_STRATEGY diff --git a/collectfast/strategies/boto.py b/collectfast/strategies/boto.py index 90573cd..dd817d8 100644 --- a/collectfast/strategies/boto.py +++ b/collectfast/strategies/boto.py @@ -1,4 +1,5 @@ import logging +import warnings from typing import Optional import boto.exception @@ -14,6 +15,11 @@ class BotoStrategy(CachingHashStrategy[S3BotoStorage]): def __init__(self, remote_storage): # type: (S3BotoStorage) -> None + warnings.warn( + "The BotoStrategy class is deprecated and will be removed in Collectfast " + "2.0.", + DeprecationWarning, + ) super().__init__(remote_storage) self.remote_storage.preload_metadata = True self.use_gzip = settings.aws_is_gzipped diff --git a/collectfast/tests/strategies/test_caching_hash_strategy.py b/collectfast/tests/strategies/test_caching_hash_strategy.py index c15eacd..9fdc691 100644 --- a/collectfast/tests/strategies/test_caching_hash_strategy.py +++ b/collectfast/tests/strategies/test_caching_hash_strategy.py @@ -6,7 +6,7 @@ from collectfast import settings from collectfast.strategies.base import CachingHashStrategy -from collectfast.tests.utils import test +from collectfast.tests.utils import make_test hash_characters = string.ascii_letters + string.digits @@ -22,7 +22,7 @@ def get_remote_file_hash(self, prefixed_path): pass -@test +@make_test def test_get_cache_key(case): # type: (TestCase) -> None strategy = Strategy() @@ -35,7 +35,7 @@ def test_get_cache_key(case): case.assertIn(c, expected_chars) -@test +@make_test def test_gets_and_invalidates_hash(case): # type: (TestCase) -> None strategy = Strategy() diff --git a/collectfast/tests/strategies/test_guess_strategy.py b/collectfast/tests/strategies/test_guess_strategy.py index a1b4433..3a69156 100644 --- a/collectfast/tests/strategies/test_guess_strategy.py +++ b/collectfast/tests/strategies/test_guess_strategy.py @@ -7,22 +7,22 @@ from collectfast.strategies.base import _BOTO_STORAGE from collectfast.strategies.base import _BOTO_STRATEGY from collectfast.strategies.base import guess_strategy -from collectfast.tests.utils import test +from collectfast.tests.utils import make_test -@test +@make_test def test_guesses_boto_from_exact(case): # type: (TestCase) -> None case.assertEqual(guess_strategy(_BOTO_STORAGE), _BOTO_STRATEGY) -@test +@make_test def test_guesses_boto3_from_exact(case): # type: (TestCase) -> None case.assertEqual(guess_strategy(_BOTO3_STORAGE), _BOTO3_STRATEGY) -@test +@make_test def test_guesses_boto_from_subclass(case): # type: (TestCase) -> None case.assertEqual( @@ -31,7 +31,7 @@ def test_guesses_boto_from_subclass(case): ) -@test +@make_test def test_guesses_boto3_from_subclass(case): # type: (TestCase) -> None case.assertEqual( @@ -40,14 +40,14 @@ def test_guesses_boto3_from_subclass(case): ) -@test +@make_test def test_raises_improperly_configured_for_unguessable_class(case): # type: (TestCase) -> None with case.assertRaises(ImproperlyConfigured): guess_strategy("collectfast.tests.test_storages.unguessable.UnguessableStorage") -@test +@make_test def test_raises_improperly_configured_for_invalid_type(case): # type: (TestCase) -> None with case.assertRaises(ImproperlyConfigured): diff --git a/collectfast/tests/strategies/test_hash_strategy.py b/collectfast/tests/strategies/test_hash_strategy.py index cfdbb4e..d49c413 100644 --- a/collectfast/tests/strategies/test_hash_strategy.py +++ b/collectfast/tests/strategies/test_hash_strategy.py @@ -7,7 +7,7 @@ from django.core.files.storage import FileSystemStorage from collectfast.strategies.base import HashStrategy -from collectfast.tests.utils import test +from collectfast.tests.utils import make_test class Strategy(HashStrategy[FileSystemStorage]): @@ -20,7 +20,7 @@ def get_remote_file_hash(self, prefixed_path): pass -@test +@make_test def test_get_file_hash(case): # type: (TestCase) -> None strategy = Strategy() @@ -32,7 +32,7 @@ def test_get_file_hash(case): case.assertTrue(re.fullmatch(r"^[A-z0-9]{32}$", hash_) is not None) -@test +@make_test def test_should_copy_file(case): # type: (TestCase) -> None strategy = Strategy() diff --git a/collectfast/tests/test_command.py b/collectfast/tests/test_command.py index f61573a..c63908d 100644 --- a/collectfast/tests/test_command.py +++ b/collectfast/tests/test_command.py @@ -8,9 +8,9 @@ from .utils import clean_static_dir from .utils import create_static_file +from .utils import make_test from .utils import override_setting from .utils import override_storage_attr -from .utils import test from .utils import test_many from collectfast.management.commands.collectstatic import Command @@ -39,8 +39,8 @@ } ) -test_aws_backends = test_many(**aws_backend_confs) -test_all_backends = test_many(**all_backend_confs) +make_test_aws_backends = test_many(**aws_backend_confs) +make_test_all_backends = test_many(**all_backend_confs) def call_collectstatic(*args, **kwargs): @@ -52,7 +52,7 @@ def call_collectstatic(*args, **kwargs): return out.getvalue() -@test_all_backends +@make_test_all_backends def test_basics(case): # type: (TestCase) -> None clean_static_dir() @@ -62,7 +62,7 @@ def test_basics(case): case.assertIn("0 static files copied.", call_collectstatic()) -@test_all_backends +@make_test_all_backends @override_setting("threads", 5) def test_threads(case): # type: (TestCase) -> None @@ -73,7 +73,7 @@ def test_threads(case): case.assertIn("0 static files copied.", call_collectstatic()) -@test +@make_test @override_django_settings( STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage" ) @@ -84,7 +84,7 @@ def test_disable_collectfast_with_default_storage(case): case.assertIn("1 static file copied.", call_collectstatic(disable_collectfast=True)) -@test +@make_test def test_disable_collectfast(case): # type: (TestCase) -> None clean_static_dir() @@ -92,7 +92,7 @@ def test_disable_collectfast(case): case.assertIn("1 static file copied.", call_collectstatic(disable_collectfast=True)) -@test +@make_test def test_dry_run(case): # type: (TestCase) -> None clean_static_dir() @@ -106,7 +106,7 @@ def test_dry_run(case): case.assertTrue("Pretending to delete", result) -@test_aws_backends +@make_test_aws_backends @override_storage_attr("gzip", True) @override_setting("aws_is_gzipped", True) def test_aws_is_gzipped(case): @@ -118,7 +118,7 @@ def test_aws_is_gzipped(case): case.assertIn("0 static files copied.", call_collectstatic()) -@test +@make_test @override_django_settings( STATICFILES_STORAGE="storages.backends.s3boto.S3BotoStorage", COLLECTFAST_STRATEGY=None, @@ -128,7 +128,7 @@ def test_recognizes_boto_storage(case): case.assertEqual(Command._load_strategy().__name__, "BotoStrategy") -@test +@make_test @override_django_settings( STATICFILES_STORAGE="storages.backends.s3boto3.S3Boto3Storage", COLLECTFAST_STRATEGY=None, @@ -138,7 +138,7 @@ def test_recognizes_boto3_storage(case): case.assertEqual(Command._load_strategy().__name__, "Boto3Strategy") -@test +@make_test @override_django_settings(STATICFILES_STORAGE=None, COLLECTFAST_STRATEGY=None) def test_raises_for_unrecognized_storage(case): # type: (TestCase) -> None diff --git a/collectfast/tests/utils.py b/collectfast/tests/utils.py index 2cb804a..15df38a 100644 --- a/collectfast/tests/utils.py +++ b/collectfast/tests/utils.py @@ -20,14 +20,14 @@ static_dir = pathlib.Path(django_settings.STATICFILES_DIRS[0]) # type: Final -def test(func): +def make_test(func): # type: (F) -> Type[unittest.TestCase] """ Creates a class that inherits from `unittest.TestCase` with the decorated function as a method. Create tests like this: >>> fn = lambda x: 1337 - >>> @test + >>> @make_test ... def test_fn(case): ... case.assertEqual(fn(), 1337) """ @@ -45,7 +45,7 @@ def test(func): function as a method. Create tests like this: >>> fn = lambda x: 1337 - >>> @test + >>> @make_test ... def test_fn(case): ... case.assertEqual(fn(), 1337) """ diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..8a4f616 --- /dev/null +++ b/conftest.py @@ -0,0 +1,18 @@ +import os +import shutil + +import pytest +from django.conf import settings + + +@pytest.fixture(autouse=True) +def create_test_directories(): + paths = (settings.STATICFILES_DIRS[0], settings.STATIC_ROOT, settings.MEDIA_ROOT) + for path in paths: + if not os.path.exists(path): + os.makedirs(path) + try: + yield + finally: + for path in paths: + shutil.rmtree(path) diff --git a/runtests.py b/runtests.py deleted file mode 100755 index a3253b2..0000000 --- a/runtests.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python -import os -import shutil -import sys -from optparse import OptionParser - -import django -from django.conf import settings -from django.core.management import call_command - -from collectfast.tests import settings as test_settings - - -def configure_django(): - # type: () -> None - settings.configure(**vars(test_settings)) - django.setup() - - -def main(): - # type: () -> None - configure_django() - - parser = OptionParser() - parser.add_option("--target", dest="target", default=None) - parser.add_option( - "--skip-cleanup", dest="skip_cleanup", default=False, action="store_true" - ) - - options, args = parser.parse_args() - - app_path = "collectfast" - parent_dir, app_name = os.path.split(app_path) - sys.path.insert(0, parent_dir) - - # create static dirs - staticfiles_dir = test_settings.STATICFILES_DIRS[0] - staticroot_dir = test_settings.STATIC_ROOT - fs_remote = test_settings.MEDIA_ROOT - if not os.path.exists(staticfiles_dir): - os.makedirs(staticfiles_dir) - if not os.path.exists(staticroot_dir): - os.makedirs(staticroot_dir) - if not os.path.exists(fs_remote): - os.makedirs(fs_remote) - - if options.target is not None: - test_arg = "%s.%s" % (app_name, options.target) - else: - test_arg = app_name - - try: - call_command("test", test_arg) - finally: - if options.skip_cleanup: - return - # delete static dirs - shutil.rmtree(staticfiles_dir) - shutil.rmtree(staticroot_dir) - shutil.rmtree(fs_remote) - - -if __name__ == "__main__": - main() diff --git a/setup.cfg b/setup.cfg index ebe4b25..e71dc5b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,6 @@ +[tool:pytest] +DJANGO_SETTINGS_MODULE = collectfast.tests.settings + [flake8] exclude = appveyor, .idea, .git, .venv, .tox, __pycache__, *.egg-info, build max-complexity = 8 diff --git a/test-requirements.txt b/test-requirements.txt index d079706..6d52baf 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,3 +6,5 @@ django-storages boto3 boto google-cloud-storage +pytest +pytest-django