diff --git a/.travis.yml b/.travis.yml index 925d82e7b..52786a17f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,10 +15,6 @@ matrix: include: - name: Test results script: tox -e py37 - - name: Run flake8 on result - script: tox -e flake8 - - name: Run black on result - script: tox -e black - name: Black template script: tox -e black-template - name: Basic Docker diff --git a/pytest.ini b/pytest.ini index 89aeb302c..03ca13891 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,4 @@ [pytest] -addopts = -x --tb=short +addopts = -v --tb=short python_paths = . norecursedirs = .tox .git */migrations/* */static/* docs venv */{{cookiecutter.project_slug}}/* -markers = - flake8: Run flake8 on all possible template combinations - black: Run black on all possible template combinations diff --git a/requirements.txt b/requirements.txt index 06f5a80ed..140dc2cdd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,12 +6,12 @@ binaryornot==0.4.4 # ------------------------------------------------------------------------------ black==19.10b0 flake8==3.7.9 +flake8-isort==2.8.0 # Testing # ------------------------------------------------------------------------------ tox==3.14.5 pytest==5.3.5 -pytest_cases==1.12.2 pytest-cookies==0.5.1 -pytest-xdist==1.31.0 +pytest-instafail==0.4.1.post0 pyyaml==5.3 diff --git a/tests/test_cookiecutter_generation.py b/tests/test_cookiecutter_generation.py index 39b05d7c6..e51e4b9fa 100755 --- a/tests/test_cookiecutter_generation.py +++ b/tests/test_cookiecutter_generation.py @@ -3,7 +3,6 @@ import re import pytest from cookiecutter.exceptions import FailedHookException -from pytest_cases import fixture_plus import sh import yaml from binaryornot.check import is_binary @@ -26,49 +25,62 @@ def context(): } -@fixture_plus -@pytest.mark.parametrize("windows", ["y", "n"], ids=lambda yn: f"win:{yn}") -@pytest.mark.parametrize("use_docker", ["y", "n"], ids=lambda yn: f"docker:{yn}") -@pytest.mark.parametrize("use_celery", ["y", "n"], ids=lambda yn: f"celery:{yn}") -@pytest.mark.parametrize("use_mailhog", ["y", "n"], ids=lambda yn: f"mailhog:{yn}") -@pytest.mark.parametrize("use_sentry", ["y", "n"], ids=lambda yn: f"sentry:{yn}") -@pytest.mark.parametrize("use_compressor", ["y", "n"], ids=lambda yn: f"cmpr:{yn}") -@pytest.mark.parametrize("use_drf", ["y", "n"], ids=lambda yn: f"drf:{yn}") -@pytest.mark.parametrize( - "use_whitenoise,cloud_provider", - [ - ("y", "AWS"), - ("y", "GCP"), - ("y", "None"), - ("n", "AWS"), - ("n", "GCP"), - # no whitenoise + no cloud provider is not supported - ], - ids=lambda id: f"wnoise:{id[0]}-cloud:{id[1]}", -) -def context_combination( - windows, - use_docker, - use_celery, - use_mailhog, - use_sentry, - use_compressor, - use_whitenoise, - use_drf, - cloud_provider, -): - """Fixture that parametrize the function where it's used.""" - return { - "windows": windows, - "use_docker": use_docker, - "use_compressor": use_compressor, - "use_celery": use_celery, - "use_mailhog": use_mailhog, - "use_sentry": use_sentry, - "use_whitenoise": use_whitenoise, - "use_drf": use_drf, - "cloud_provider": cloud_provider, - } +SUPPORTED_COMBINATIONS = [ + {"open_source_license": "MIT"}, + {"open_source_license": "BSD"}, + {"open_source_license": "GPLv3"}, + {"open_source_license": "Apache Software License 2.0"}, + {"open_source_license": "Not open source"}, + {"windows": "y"}, + {"windows": "n"}, + {"use_pycharm": "y"}, + {"use_pycharm": "n"}, + {"use_docker": "y"}, + {"use_docker": "n"}, + {"postgresql_version": "11.3"}, + {"postgresql_version": "10.8"}, + {"postgresql_version": "9.6"}, + {"postgresql_version": "9.5"}, + {"postgresql_version": "9.4"}, + {"cloud_provider": "AWS", "use_whitenoise": "y"}, + {"cloud_provider": "AWS", "use_whitenoise": "n"}, + {"cloud_provider": "GCP", "use_whitenoise": "y"}, + {"cloud_provider": "GCP", "use_whitenoise": "n"}, + {"cloud_provider": "None", "use_whitenoise": "y"}, + # Note: cloud_provider=None AND use_whitenoise=n is not supported + {"use_drf": "y"}, + {"use_drf": "n"}, + {"js_task_runner": "None"}, + {"js_task_runner": "Gulp"}, + {"custom_bootstrap_compilation": "y"}, + {"custom_bootstrap_compilation": "n"}, + {"use_compressor": "y"}, + {"use_compressor": "n"}, + {"use_celery": "y"}, + {"use_celery": "n"}, + {"use_mailhog": "y"}, + {"use_mailhog": "n"}, + {"use_sentry": "y"}, + {"use_sentry": "n"}, + {"use_whitenoise": "y"}, + {"use_whitenoise": "n"}, + {"use_heroku": "y"}, + {"use_heroku": "n"}, + {"ci_tool": "None"}, + {"ci_tool": "Travis"}, + {"ci_tool": "Gitlab"}, + {"keep_local_envs_in_vcs": "y"}, + {"keep_local_envs_in_vcs": "n"}, + {"debug": "y"}, + {"debug": "n"}, +] + +UNSUPPORTED_COMBINATIONS = [{"cloud_provider": "None", "use_whitenoise": "n"}] + + +def _fixture_id(ctx): + """Helper to get a user friendly test name from the parametrized context.""" + return "-".join(f"{key}:{value}" for key, value in ctx.items()) def build_files_list(root_dir): @@ -81,9 +93,7 @@ def build_files_list(root_dir): def check_paths(paths): - """Method to check all paths have correct substitutions, - used by other tests cases - """ + """Method to check all paths have correct substitutions.""" # Assert that no match is found in any of the files for path in paths: if is_binary(path): @@ -95,13 +105,10 @@ def check_paths(paths): assert match is None, msg.format(path) -def test_project_generation(cookies, context, context_combination): - """ - Test that project is generated and fully rendered. - - This is parametrized for each combination from ``context_combination`` fixture - """ - result = cookies.bake(extra_context={**context, **context_combination}) +@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id) +def test_project_generation(cookies, context, context_override): + """Test that project is generated and fully rendered.""" + result = cookies.bake(extra_context={**context, **context_override}) assert result.exit_code == 0 assert result.exception is None assert result.project.basename == context["project_slug"] @@ -112,34 +119,26 @@ def test_project_generation(cookies, context, context_combination): check_paths(paths) -@pytest.mark.flake8 -def test_flake8_passes(cookies, context_combination): - """ - Generated project should pass flake8. - - This is parametrized for each combination from ``context_combination`` fixture - """ - result = cookies.bake(extra_context=context_combination) +@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id) +def test_flake8_passes(cookies, context_override): + """Generated project should pass flake8.""" + result = cookies.bake(extra_context=context_override) try: sh.flake8(str(result.project)) except sh.ErrorReturnCode as e: - pytest.fail(e) + pytest.fail(e.stdout.decode()) -@pytest.mark.black -def test_black_passes(cookies, context_combination): - """ - Generated project should pass black. - - This is parametrized for each combination from ``context_combination`` fixture - """ - result = cookies.bake(extra_context=context_combination) +@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id) +def test_black_passes(cookies, context_override): + """Generated project should pass black.""" + result = cookies.bake(extra_context=context_override) try: sh.black("--check", "--diff", "--exclude", "migrations", f"{result.project}/") except sh.ErrorReturnCode as e: - pytest.fail(e) + pytest.fail(e.stdout.decode()) def test_travis_invokes_pytest(cookies, context): @@ -187,9 +186,10 @@ def test_invalid_slug(cookies, context, slug): assert isinstance(result.exception, FailedHookException) -def test_no_whitenoise_and_no_cloud_provider(cookies, context): - """It should not generate project if neither whitenoise or cloud provider are set""" - context.update({"use_whitenoise": "n", "cloud_provider": "None"}) +@pytest.mark.parametrize("invalid_context", UNSUPPORTED_COMBINATIONS) +def test_error_if_incompatible(cookies, context, invalid_context): + """It should not generate project an incompatible combination is selected.""" + context.update(invalid_context) result = cookies.bake(extra_context=context) assert result.exit_code != 0 diff --git a/tox.ini b/tox.ini index 1c83465cc..737b26e73 100644 --- a/tox.ini +++ b/tox.ini @@ -1,18 +1,10 @@ [tox] skipsdist = true -envlist = py37,flake8,black,black-template +envlist = py37,black-template [testenv] deps = -rrequirements.txt -commands = pytest -m "not flake8" -m "not black" {posargs:./tests} - -[testenv:flake8] -deps = -rrequirements.txt -commands = pytest -m flake8 {posargs:./tests} - -[testenv:black] -deps = -rrequirements.txt -commands = pytest -m black {posargs:./tests} +commands = pytest {posargs:./tests} [testenv:black-template] deps = black diff --git a/{{cookiecutter.project_slug}}/.editorconfig b/{{cookiecutter.project_slug}}/.editorconfig index 792dd3b06..39c15f079 100644 --- a/{{cookiecutter.project_slug}}/.editorconfig +++ b/{{cookiecutter.project_slug}}/.editorconfig @@ -13,8 +13,8 @@ indent_style = space indent_size = 4 [*.py] -line_length = 120 -known_first_party = {{ cookiecutter.project_slug }} +line_length = 88 +known_first_party = {{cookiecutter.project_slug}},config multi_line_output = 3 default_section = THIRDPARTY recursive = true diff --git a/{{cookiecutter.project_slug}}/Procfile b/{{cookiecutter.project_slug}}/Procfile index 5b8e9eaf8..3838eafd7 100644 --- a/{{cookiecutter.project_slug}}/Procfile +++ b/{{cookiecutter.project_slug}}/Procfile @@ -2,4 +2,5 @@ release: python manage.py migrate web: gunicorn config.wsgi:application {% if cookiecutter.use_celery == "y" -%} worker: celery worker --app=config.celery_app --loglevel=info +beat: celery beat --app=config.celery_app --loglevel=info {%- endif %} diff --git a/{{cookiecutter.project_slug}}/config/api_router.py b/{{cookiecutter.project_slug}}/config/api_router.py index 46a797a74..743069b2c 100644 --- a/{{cookiecutter.project_slug}}/config/api_router.py +++ b/{{cookiecutter.project_slug}}/config/api_router.py @@ -1,5 +1,6 @@ -from rest_framework.routers import DefaultRouter, SimpleRouter from django.conf import settings +from rest_framework.routers import DefaultRouter, SimpleRouter + from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet if settings.DEBUG: diff --git a/{{cookiecutter.project_slug}}/config/celery_app.py b/{{cookiecutter.project_slug}}/config/celery_app.py index e275f054f..0728a649e 100644 --- a/{{cookiecutter.project_slug}}/config/celery_app.py +++ b/{{cookiecutter.project_slug}}/config/celery_app.py @@ -1,4 +1,5 @@ import os + from celery import Celery # set the default Django settings module for the 'celery' program. diff --git a/{{cookiecutter.project_slug}}/config/settings/production.py b/{{cookiecutter.project_slug}}/config/settings/production.py index 36667b33a..73ce4d648 100644 --- a/{{cookiecutter.project_slug}}/config/settings/production.py +++ b/{{cookiecutter.project_slug}}/config/settings/production.py @@ -1,3 +1,4 @@ +"""isort:skip_file""" {% if cookiecutter.use_sentry == 'y' -%} import logging diff --git a/{{cookiecutter.project_slug}}/config/urls.py b/{{cookiecutter.project_slug}}/config/urls.py index 382bf8951..818609061 100644 --- a/{{cookiecutter.project_slug}}/config/urls.py +++ b/{{cookiecutter.project_slug}}/config/urls.py @@ -1,10 +1,10 @@ from django.conf import settings -from django.urls import include, path from django.conf.urls.static import static from django.contrib import admin -from django.views.generic import TemplateView +from django.urls import include, path from django.views import defaults as default_views -{% if cookiecutter.use_drf == 'y' -%} +from django.views.generic import TemplateView +{%- if cookiecutter.use_drf == 'y' %} from rest_framework.authtoken.views import obtain_auth_token {%- endif %} diff --git a/{{cookiecutter.project_slug}}/requirements/base.txt b/{{cookiecutter.project_slug}}/requirements/base.txt index a53af9296..2981cfb80 100644 --- a/{{cookiecutter.project_slug}}/requirements/base.txt +++ b/{{cookiecutter.project_slug}}/requirements/base.txt @@ -10,7 +10,7 @@ whitenoise==5.0.1 # https://github.com/evansd/whitenoise {%- endif %} redis==3.4.1 # https://github.com/andymccurdy/redis-py {%- if cookiecutter.use_celery == "y" %} -celery==4.4.0 # pyup: < 5.0 # https://github.com/celery/celery +celery==4.4.1 # pyup: < 5.0 # https://github.com/celery/celery django-celery-beat==2.0.0 # https://github.com/celery/django-celery-beat {%- if cookiecutter.use_docker == 'y' %} flower==0.9.3 # https://github.com/mher/flower @@ -23,7 +23,7 @@ django==3.0.4 # pyup: < 3.1 # https://www.djangoproject.com/ django-environ==0.4.5 # https://github.com/joke2k/django-environ django-model-utils==4.0.0 # https://github.com/jazzband/django-model-utils django-allauth==0.41.0 # https://github.com/pennersr/django-allauth -django-crispy-forms==1.8.1 # https://github.com/django-crispy-forms/django-crispy-forms +django-crispy-forms==1.9.0 # https://github.com/django-crispy-forms/django-crispy-forms {%- if cookiecutter.use_compressor == "y" %} django-compressor==2.4 # https://github.com/django-compressor/django-compressor {%- endif %} diff --git a/{{cookiecutter.project_slug}}/requirements/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt index 194e25d58..80558f5c7 100644 --- a/{{cookiecutter.project_slug}}/requirements/local.txt +++ b/{{cookiecutter.project_slug}}/requirements/local.txt @@ -1,7 +1,7 @@ -r ./base.txt Werkzeug==1.0.0 # https://github.com/pallets/werkzeug -ipdb==0.12.3 # https://github.com/gotcha/ipdb +ipdb==0.13.2 # https://github.com/gotcha/ipdb Sphinx==2.4.3 # https://github.com/sphinx-doc/sphinx {%- if cookiecutter.use_docker == 'y' %} psycopg2==2.8.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 @@ -19,6 +19,7 @@ pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar # Code quality # ------------------------------------------------------------------------------ flake8==3.7.9 # https://github.com/PyCQA/flake8 +flake8-isort==2.8.0 # https://github.com/gforcada/flake8-isort coverage==5.0.3 # https://github.com/nedbat/coveragepy black==19.10b0 # https://github.com/ambv/black pylint-django==2.0.14 # https://github.com/PyCQA/pylint-django diff --git a/{{cookiecutter.project_slug}}/requirements/production.txt b/{{cookiecutter.project_slug}}/requirements/production.txt index 62fe3e40c..f0c4fed12 100644 --- a/{{cookiecutter.project_slug}}/requirements/production.txt +++ b/{{cookiecutter.project_slug}}/requirements/production.txt @@ -5,7 +5,7 @@ gunicorn==20.0.4 # https://github.com/benoitc/gunicorn psycopg2==2.8.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 {%- if cookiecutter.use_whitenoise == 'n' %} -Collectfast==2.0.1 # https://github.com/antonagestam/collectfast +Collectfast==2.1.0 # https://github.com/antonagestam/collectfast {%- endif %} {%- if cookiecutter.use_sentry == "y" %} sentry-sdk==0.14.2 # https://github.com/getsentry/sentry-python diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py index bb52738b7..04bf4c85e 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers + from {{ cookiecutter.project_slug }}.users.models import User diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py index 7b5af999b..288ea7ab2 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from rest_framework import status from rest_framework.decorators import action -from rest_framework.mixins import RetrieveModelMixin, ListModelMixin, UpdateModelMixin +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py index 250cc9040..25065419a 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py @@ -1,4 +1,4 @@ -from django.contrib.auth import get_user_model, forms +from django.contrib.auth import forms, get_user_model from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_tasks.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_tasks.py index addb091db..61586b5ca 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_tasks.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_tasks.py @@ -1,7 +1,6 @@ import pytest from celery.result import EagerResult - from {{ cookiecutter.project_slug }}.users.tasks import get_users_count from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py index 933396ba0..aab6d0a87 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_urls.py @@ -1,5 +1,5 @@ import pytest -from django.urls import reverse, resolve +from django.urls import resolve, reverse from {{ cookiecutter.project_slug }}.users.models import User diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py index eff24dd04..8c8c7e2ea 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py @@ -1,9 +1,9 @@ from django.urls import path from {{ cookiecutter.project_slug }}.users.views import ( + user_detail_view, user_redirect_view, user_update_view, - user_detail_view, ) app_name = "users" diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py index 5c0d5b5c2..f0e81dffc 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py @@ -1,9 +1,9 @@ +from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse -from django.views.generic import DetailView, RedirectView, UpdateView -from django.contrib import messages from django.utils.translation import ugettext_lazy as _ +from django.views.generic import DetailView, RedirectView, UpdateView User = get_user_model()