From 17fa459dc3d84f7790b69487386c4e1ef7c5b95f Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Sat, 15 Apr 2023 15:43:04 +0100 Subject: [PATCH] Fix inconsistent line length and move configs to pyproject.toml (#4276) * Fix inconsistent line length and move config to pyproject.toml Fix #2720 * Fix running tox with AUTOFIXABLE_STYLES * Adjust some styles * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Adjust more styles * Split isort and flake8 tests --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .flake8 | 3 + docs/conf.py | 3 +- hooks/post_gen_project.py | 72 +++------------ hooks/pre_gen_project.py | 40 ++------ pyproject.toml | 29 ++++++ pytest.ini | 3 - requirements.txt | 1 - scripts/create_django_issue.py | 42 ++------- scripts/update_changelog.py | 8 +- scripts/update_contributors.py | 18 +--- setup.cfg | 7 -- setup.py | 5 +- tests/test_cookiecutter_generation.py | 28 +++--- tests/test_hooks.py | 4 +- tox.ini | 1 + .../.pre-commit-config.yaml | 2 - {{cookiecutter.project_slug}}/.pylintrc | 14 --- .../config/settings/base.py | 9 +- .../config/settings/local.py | 4 +- .../config/settings/production.py | 34 +++---- .../config/settings/test.py | 4 +- {{cookiecutter.project_slug}}/config/urls.py | 4 +- {{cookiecutter.project_slug}}/pyproject.toml | 92 +++++++++++++++++++ {{cookiecutter.project_slug}}/pytest.ini | 6 -- {{cookiecutter.project_slug}}/setup.cfg | 40 +------- .../{{cookiecutter.project_slug}}/__init__.py | 5 +- .../users/api/serializers.py | 4 +- .../users/tests/test_drf_urls.py | 10 +- .../users/tests/test_urls.py | 5 +- .../users/views.py | 4 +- 30 files changed, 213 insertions(+), 288 deletions(-) create mode 100644 .flake8 create mode 100644 pyproject.toml delete mode 100644 pytest.ini delete mode 100644 setup.cfg delete mode 100644 {{cookiecutter.project_slug}}/.pylintrc create mode 100644 {{cookiecutter.project_slug}}/pyproject.toml delete mode 100644 {{cookiecutter.project_slug}}/pytest.ini diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..84b4b441 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +exclude = docs +max-line-length = 119 diff --git a/docs/conf.py b/docs/conf.py index b1a97750..22e73e5d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -239,8 +239,7 @@ texinfo_documents = [ "Cookiecutter Django documentation", "Daniel Roy Greenfeld", "Cookiecutter Django", - "A Cookiecutter template for creating production-ready " - "Django projects quickly.", + "A Cookiecutter template for creating production-ready " "Django projects quickly.", "Miscellaneous", ) ] diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 22cda531..b79985a8 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -92,10 +92,7 @@ def remove_utility_files(): def remove_heroku_files(): file_names = ["Procfile", "runtime.txt", "requirements.txt"] for file_name in file_names: - if ( - file_name == "requirements.txt" - and "{{ cookiecutter.ci_tool }}".lower() == "travis" - ): + if file_name == "requirements.txt" and "{{ cookiecutter.ci_tool }}".lower() == "travis": # don't remove the file if we are using travisci but not using heroku continue os.remove(file_name) @@ -197,11 +194,7 @@ def handle_js_runner(choice, use_docker, use_async): "gulp-uglify-es", ] if not use_docker: - dev_django_cmd = ( - "uvicorn config.asgi:application --reload" - if use_async - else "python manage.py runserver" - ) + dev_django_cmd = "uvicorn config.asgi:application --reload" if use_async else "python manage.py runserver" scripts.update( { "dev": "concurrently npm:dev:*", @@ -219,9 +212,7 @@ def remove_celery_files(): file_names = [ os.path.join("config", "celery_app.py"), os.path.join("{{ cookiecutter.project_slug }}", "users", "tasks.py"), - os.path.join( - "{{ cookiecutter.project_slug }}", "users", "tests", "test_tasks.py" - ), + os.path.join("{{ cookiecutter.project_slug }}", "users", "tests", "test_tasks.py"), ] for file_name in file_names: os.remove(file_name) @@ -248,9 +239,7 @@ def remove_dotgithub_folder(): shutil.rmtree(".github") -def generate_random_string( - length, using_digits=False, using_ascii_letters=False, using_punctuation=False -): +def generate_random_string(length, using_digits=False, using_ascii_letters=False, using_punctuation=False): """ Example: opting out for 50 symbol-long, [a-z][A-Z][0-9] string @@ -344,9 +333,7 @@ def set_postgres_password(file_path, value=None): def set_celery_flower_user(file_path, value): - celery_flower_user = set_flag( - file_path, "!!!SET CELERY_FLOWER_USER!!!", value=value - ) + celery_flower_user = set_flag(file_path, "!!!SET CELERY_FLOWER_USER!!!", value=value) return celery_flower_user @@ -378,22 +365,14 @@ def set_flags_in_envs(postgres_user, celery_flower_user, debug=False): set_django_admin_url(production_django_envs_path) set_postgres_user(local_postgres_envs_path, value=postgres_user) - set_postgres_password( - local_postgres_envs_path, value=DEBUG_VALUE if debug else None - ) + set_postgres_password(local_postgres_envs_path, value=DEBUG_VALUE if debug else None) set_postgres_user(production_postgres_envs_path, value=postgres_user) - set_postgres_password( - production_postgres_envs_path, value=DEBUG_VALUE if debug else None - ) + set_postgres_password(production_postgres_envs_path, value=DEBUG_VALUE if debug else None) set_celery_flower_user(local_django_envs_path, value=celery_flower_user) - set_celery_flower_password( - local_django_envs_path, value=DEBUG_VALUE if debug else None - ) + set_celery_flower_password(local_django_envs_path, value=DEBUG_VALUE if debug else None) set_celery_flower_user(production_django_envs_path, value=celery_flower_user) - set_celery_flower_password( - production_django_envs_path, value=DEBUG_VALUE if debug else None - ) + set_celery_flower_password(production_django_envs_path, value=DEBUG_VALUE if debug else None) def set_flags_in_settings_files(): @@ -423,21 +402,9 @@ def remove_aws_dockerfile(): def remove_drf_starter_files(): os.remove(os.path.join("config", "api_router.py")) shutil.rmtree(os.path.join("{{cookiecutter.project_slug}}", "users", "api")) - os.remove( - os.path.join( - "{{cookiecutter.project_slug}}", "users", "tests", "test_drf_urls.py" - ) - ) - os.remove( - os.path.join( - "{{cookiecutter.project_slug}}", "users", "tests", "test_drf_views.py" - ) - ) - os.remove( - os.path.join( - "{{cookiecutter.project_slug}}", "users", "tests", "test_swagger.py" - ) - ) + os.remove(os.path.join("{{cookiecutter.project_slug}}", "users", "tests", "test_drf_urls.py")) + os.remove(os.path.join("{{cookiecutter.project_slug}}", "users", "tests", "test_drf_views.py")) + os.remove(os.path.join("{{cookiecutter.project_slug}}", "users", "tests", "test_swagger.py")) def remove_storages_module(): @@ -470,10 +437,7 @@ def main(): else: remove_docker_files() - if ( - "{{ cookiecutter.use_docker }}".lower() == "y" - and "{{ cookiecutter.cloud_provider}}" != "AWS" - ): + if "{{ cookiecutter.use_docker }}".lower() == "y" and "{{ cookiecutter.cloud_provider}}" != "AWS": remove_aws_dockerfile() if "{{ cookiecutter.use_heroku }}".lower() == "n": @@ -481,10 +445,7 @@ def main(): elif "{{ cookiecutter.frontend_pipeline }}" != "Django Compressor": remove_heroku_build_hooks() - if ( - "{{ cookiecutter.use_docker }}".lower() == "n" - and "{{ cookiecutter.use_heroku }}".lower() == "n" - ): + if "{{ cookiecutter.use_docker }}".lower() == "n" and "{{ cookiecutter.use_heroku }}".lower() == "n": if "{{ cookiecutter.keep_local_envs_in_vcs }}".lower() == "y": print( INFO + ".env(s) are only utilized when Docker Compose and/or " @@ -512,10 +473,7 @@ def main(): use_async=("{{ cookiecutter.use_async }}".lower() == "y"), ) - if ( - "{{ cookiecutter.cloud_provider }}" == "None" - and "{{ cookiecutter.use_docker }}".lower() == "n" - ): + if "{{ cookiecutter.cloud_provider }}" == "None" and "{{ cookiecutter.use_docker }}".lower() == "n": print( WARNING + "You chose to not use any cloud providers nor Docker, " "media files won't be served in production." + TERMINATOR diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index d067954d..33dc2e83 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -27,17 +27,11 @@ SUCCESS = "\x1b[1;32m [SUCCESS]: " project_slug = "{{ cookiecutter.project_slug }}" if hasattr(project_slug, "isidentifier"): - assert ( - project_slug.isidentifier() - ), "'{}' project slug is not a valid Python identifier.".format(project_slug) + assert project_slug.isidentifier(), "'{}' project slug is not a valid Python identifier.".format(project_slug) -assert ( - project_slug == project_slug.lower() -), "'{}' project slug should be all lowercase".format(project_slug) +assert project_slug == project_slug.lower(), "'{}' project slug should be all lowercase".format(project_slug) -assert ( - "\\" not in "{{ cookiecutter.author_name }}" -), "Don't include backslashes in author name." +assert "\\" not in "{{ cookiecutter.author_name }}", "Don't include backslashes in author name." if "{{ cookiecutter.use_docker }}".lower() == "n": python_major_version = sys.version_info[0] @@ -59,32 +53,16 @@ if "{{ cookiecutter.use_docker }}".lower() == "n": print( HINT + "Please respond with {} or {}: ".format( - ", ".join( - ["'{}'".format(o) for o in yes_options if not o == ""] - ), - ", ".join( - ["'{}'".format(o) for o in no_options if not o == ""] - ), + ", ".join(["'{}'".format(o) for o in yes_options if not o == ""]), + ", ".join(["'{}'".format(o) for o in no_options if not o == ""]), ) + TERMINATOR ) -if ( - "{{ cookiecutter.use_whitenoise }}".lower() == "n" - and "{{ cookiecutter.cloud_provider }}" == "None" -): - print( - "You should either use Whitenoise or select a " - "Cloud Provider to serve static files" - ) +if "{{ cookiecutter.use_whitenoise }}".lower() == "n" and "{{ cookiecutter.cloud_provider }}" == "None": + print("You should either use Whitenoise or select a " "Cloud Provider to serve static files") sys.exit(1) -if ( - "{{ cookiecutter.mail_service }}" == "Amazon SES" - and "{{ cookiecutter.cloud_provider }}" != "AWS" -): - print( - "You should either use AWS or select a different " - "Mail Service for sending emails." - ) +if "{{ cookiecutter.mail_service }}" == "Amazon SES" and "{{ cookiecutter.cloud_provider }}" != "AWS": + print("You should either use AWS or select a different " "Mail Service for sending emails.") sys.exit(1) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..2b4b9878 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +# ==== pytest ==== +[tool.pytest.ini_options] +addopts = "-v --tb=short" +norecursedirs = [ + ".tox", + ".git", + "*/migrations/*", + "*/static/*", + "docs", + "venv", + "*/{{cookiecutter.project_slug}}/*", +] + + +# ==== black ==== +[tool.black] +line-length = 119 +target-version = ['py311'] + + +# ==== isort ==== +[tool.isort] +profile = "black" +line_length = 119 +known_first_party = [ + "tests", + "scripts", + "hooks", +] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 52506f47..00000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -addopts = -v --tb=short -norecursedirs = .tox .git */migrations/* */static/* docs venv */{{cookiecutter.project_slug}}/* diff --git a/requirements.txt b/requirements.txt index cc729cf6..7ee3bbde 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,6 @@ binaryornot==0.4.4 black==23.3.0 isort==5.12.0 flake8==6.0.0 -flake8-isort==6.0.0 pre-commit==3.2.2 # Testing diff --git a/scripts/create_django_issue.py b/scripts/create_django_issue.py index 5809f393..d3b3c379 100644 --- a/scripts/create_django_issue.py +++ b/scripts/create_django_issue.py @@ -141,9 +141,7 @@ class GitHubManager: self.requirements_files = ["base", "local", "production"] # Format: # requirement file name: {package name: (master_version, package_info)} - self.requirements: dict[str, dict[str, tuple[str, dict]]] = { - x: {} for x in self.requirements_files - } + self.requirements: dict[str, dict[str, tuple[str, dict]]] = {x: {} for x in self.requirements_files} def setup(self) -> None: self.load_requirements() @@ -177,11 +175,7 @@ class GitHubManager: "is": "issue", "in": "title", } - issues = list( - self.github.search_issues( - "[Django Update]", "created", "desc", **qualifiers - ) - ) + issues = list(self.github.search_issues("[Django Update]", "created", "desc", **qualifiers)) print(f"Found {len(issues)} issues matching search") for issue in issues: matches = re.match(r"\[Update Django] Django (\d+.\d+)$", issue.title) @@ -194,9 +188,7 @@ class GitHubManager: else: self.existing_issues[issue_version] = issue - def get_compatibility( - self, package_name: str, package_info: dict, needed_dj_version: DjVersion - ): + def get_compatibility(self, package_name: str, package_info: dict, needed_dj_version: DjVersion): """ Verify compatibility via setup.py classifiers. If Django is not in the classifiers, then default compatibility is n/a and OK is ✅. @@ -209,9 +201,7 @@ class GitHubManager: # updated packages, or known releases that will happen but haven't yet if issue := self.existing_issues.get(needed_dj_version): if index := issue.body.find(package_name): - name, _current, prev_compat, ok = ( - s.strip() for s in issue.body[index:].split("|", 4)[:4] - ) + name, _current, prev_compat, ok = (s.strip() for s in issue.body[index:].split("|", 4)[:4]) if ok in ("✅", "❓", "🕒"): return prev_compat, ok @@ -248,9 +238,7 @@ class GitHubManager: ] def _get_md_home_page_url(self, package_info: dict): - urls = [ - package_info["info"].get(url_key) for url_key in self.HOME_PAGE_URL_KEYS - ] + urls = [package_info["info"].get(url_key) for url_key in self.HOME_PAGE_URL_KEYS] try: return f"[{{}}]({next(item for item in urls if item)})" except StopIteration: @@ -259,13 +247,9 @@ class GitHubManager: def generate_markdown(self, needed_dj_version: DjVersion): requirements = f"{needed_dj_version} requirements tables\n\n" for _file in self.requirements_files: - requirements += _TABLE_HEADER.format_map( - {"file": _file, "dj_version": needed_dj_version} - ) + requirements += _TABLE_HEADER.format_map({"file": _file, "dj_version": needed_dj_version}) for package_name, (version, info) in self.requirements[_file].items(): - compat_version, icon = self.get_compatibility( - package_name, info, needed_dj_version - ) + compat_version, icon = self.get_compatibility(package_name, info, needed_dj_version) requirements += ( f"| {self._get_md_home_page_url(info).format(package_name)} " f"| {version.strip()} " @@ -282,9 +266,7 @@ class GitHubManager: issue.edit(body=description) else: print(f"Creating new issue for Django {needed_dj_version}") - issue = self.repo.create_issue( - f"[Update Django] Django {needed_dj_version}", description - ) + issue = self.repo.create_issue(f"[Update Django] Django {needed_dj_version}", description) issue.add_to_labels(f"django{needed_dj_version}") def generate(self): @@ -297,9 +279,7 @@ class GitHubManager: def main(django_max_version=None) -> None: # Check if there are any djs - current_dj, latest_djs = get_all_latest_django_versions( - django_max_version=django_max_version - ) + current_dj, latest_djs = get_all_latest_django_versions(django_max_version=django_max_version) if not latest_djs: sys.exit(0) manager = GitHubManager(current_dj, latest_djs) @@ -309,9 +289,7 @@ def main(django_max_version=None) -> None: if __name__ == "__main__": if GITHUB_REPO is None: - raise RuntimeError( - "No github repo, please set the environment variable GITHUB_REPOSITORY" - ) + raise RuntimeError("No github repo, please set the environment variable GITHUB_REPOSITORY") max_version = None last_arg = sys.argv[-1] if CURRENT_FILE.name not in last_arg: diff --git a/scripts/update_changelog.py b/scripts/update_changelog.py index 57d915a4..7d43a0b5 100644 --- a/scripts/update_changelog.py +++ b/scripts/update_changelog.py @@ -154,11 +154,7 @@ def update_git_repo(paths: list[Path], release: str) -> None: if __name__ == "__main__": if GITHUB_REPO is None: - raise RuntimeError( - "No github repo, please set the environment variable GITHUB_REPOSITORY" - ) + raise RuntimeError("No github repo, please set the environment variable GITHUB_REPOSITORY") if GIT_BRANCH is None: - raise RuntimeError( - "No git branch set, please set the GITHUB_REF_NAME environment variable" - ) + raise RuntimeError("No git branch set, please set the GITHUB_REF_NAME environment variable") main() diff --git a/scripts/update_contributors.py b/scripts/update_contributors.py index 76ccf60a..09a7082c 100644 --- a/scripts/update_contributors.py +++ b/scripts/update_contributors.py @@ -44,15 +44,9 @@ def iter_recent_authors(): git CLI to work with Github usernames. """ repo = Github(login_or_token=GITHUB_TOKEN, per_page=5).get_repo(GITHUB_REPO) - recent_pulls = repo.get_pulls( - state="closed", sort="updated", direction="desc" - ).get_page(0) + recent_pulls = repo.get_pulls(state="closed", sort="updated", direction="desc").get_page(0) for pull in recent_pulls: - if ( - pull.merged - and pull.user.type == "User" - and pull.user.login not in BOT_LOGINS - ): + if pull.merged and pull.user.type == "User" and pull.user.login not in BOT_LOGINS: yield pull.user @@ -96,9 +90,7 @@ def write_md_file(contributors): core_contributors = [c for c in contributors if c.get("is_core", False)] other_contributors = (c for c in contributors if not c.get("is_core", False)) other_contributors = sorted(other_contributors, key=lambda c: c["name"].lower()) - content = template.render( - core_contributors=core_contributors, other_contributors=other_contributors - ) + content = template.render(core_contributors=core_contributors, other_contributors=other_contributors) file_path = ROOT / "CONTRIBUTORS.md" file_path.write_text(content) @@ -106,7 +98,5 @@ def write_md_file(contributors): if __name__ == "__main__": if GITHUB_REPO is None: - raise RuntimeError( - "No github repo, please set the environment variable GITHUB_REPOSITORY" - ) + raise RuntimeError("No github repo, please set the environment variable GITHUB_REPOSITORY") main() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index dd8f1ef3..00000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[flake8] -exclude = docs -max-line-length = 88 - -[isort] -profile = black -known_first_party = tests,scripts,hooks diff --git a/setup.py b/setup.py index 3f20e29c..eb7d14ff 100644 --- a/setup.py +++ b/setup.py @@ -13,10 +13,7 @@ with open("README.rst") as readme_file: setup( name="cookiecutter-django", version=version, - description=( - "A Cookiecutter template for creating production-ready " - "Django projects quickly." - ), + description=("A Cookiecutter template for creating production-ready " "Django projects quickly."), long_description=long_description, author="Daniel Roy Greenfeld", author_email="pydanny@gmail.com", diff --git a/tests/test_cookiecutter_generation.py b/tests/test_cookiecutter_generation.py index e4a03b3b..9837be3e 100755 --- a/tests/test_cookiecutter_generation.py +++ b/tests/test_cookiecutter_generation.py @@ -23,7 +23,7 @@ elif sys.platform.startswith("darwin") and os.getenv("CI"): # Run auto-fixable styles checks - skipped on CI by default. These can be fixed # automatically by running pre-commit after generation however they are tedious # to fix in the template, so we don't insist too much in fixing them. -AUTOFIXABLE_STYLES = os.getenv("AUTOFIXABLE_STYLES") == 1 +AUTOFIXABLE_STYLES = os.getenv("AUTOFIXABLE_STYLES") == "1" @pytest.fixture @@ -144,11 +144,7 @@ def _fixture_id(ctx): def build_files_list(base_dir): """Build a list containing absolute paths to the generated files.""" - return [ - os.path.join(dirpath, file_path) - for dirpath, subdirs, files in os.walk(base_dir) - for file_path in files - ] + return [os.path.join(dirpath, file_path) for dirpath, subdirs, files in os.walk(base_dir) for file_path in files] def check_paths(paths): @@ -208,6 +204,18 @@ def test_black_passes(cookies, context_override): pytest.fail(e.stdout.decode()) +@pytest.mark.skipif(not AUTOFIXABLE_STYLES, reason="isort is auto-fixable") +@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id) +def test_isort_passes(cookies, context_override): + """Check whether generated project passes isort style.""" + result = cookies.bake(extra_context=context_override) + + try: + sh.isort(_cwd=str(result.project_path)) + except sh.ErrorReturnCode as e: + pytest.fail(e.stdout.decode()) + + @pytest.mark.parametrize( ["use_docker", "expected_test_script"], [ @@ -240,9 +248,7 @@ def test_travis_invokes_pytest(cookies, context, use_docker, expected_test_scrip ("y", "docker-compose -f local.yml run django pytest"), ], ) -def test_gitlab_invokes_precommit_and_pytest( - cookies, context, use_docker, expected_test_script -): +def test_gitlab_invokes_precommit_and_pytest(cookies, context, use_docker, expected_test_script): context.update({"ci_tool": "Gitlab", "use_docker": use_docker}) result = cookies.bake(extra_context=context) @@ -269,9 +275,7 @@ def test_gitlab_invokes_precommit_and_pytest( ("y", "docker-compose -f local.yml run django pytest"), ], ) -def test_github_invokes_linter_and_pytest( - cookies, context, use_docker, expected_test_script -): +def test_github_invokes_linter_and_pytest(cookies, context, use_docker, expected_test_script): context.update({"ci_tool": "Github", "use_docker": use_docker}) result = cookies.bake(extra_context=context) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 7ca75272..6afdc400 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -22,7 +22,5 @@ def test_append_to_gitignore_file(working_directory): gitignore_file.write_text("node_modules/\n") append_to_gitignore_file(".envs/*") linesep = os.linesep.encode() - assert ( - gitignore_file.read_bytes() == b"node_modules/" + linesep + b".envs/*" + linesep - ) + assert gitignore_file.read_bytes() == b"node_modules/" + linesep + b".envs/*" + linesep assert gitignore_file.read_text() == "node_modules/\n.envs/*\n" diff --git a/tox.ini b/tox.ini index f0c22d48..b10d1642 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = py311,black-template [testenv] deps = -rrequirements.txt +passenv = AUTOFIXABLE_STYLES commands = pytest {posargs:./tests} [testenv:black-template] diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index a7c8fa58..5d8670e4 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -44,8 +44,6 @@ repos: rev: 6.0.0 hooks: - id: flake8 - args: ['--config=setup.cfg'] - additional_dependencies: [flake8-isort] # sets up .pre-commit-ci.yaml to ensure pre-commit dependencies stay up to date ci: diff --git a/{{cookiecutter.project_slug}}/.pylintrc b/{{cookiecutter.project_slug}}/.pylintrc deleted file mode 100644 index 9d604334..00000000 --- a/{{cookiecutter.project_slug}}/.pylintrc +++ /dev/null @@ -1,14 +0,0 @@ -[MASTER] -load-plugins=pylint_django{% if cookiecutter.use_celery == "y" %}, pylint_celery{% endif %} -django-settings-module=config.settings.local -[FORMAT] -max-line-length=120 - -[MESSAGES CONTROL] -disable=missing-docstring,invalid-name - -[DESIGN] -max-parents=13 - -[TYPECHECK] -generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",save,delete diff --git a/{{cookiecutter.project_slug}}/config/settings/base.py b/{{cookiecutter.project_slug}}/config/settings/base.py index dbb396c6..c0ca31f3 100644 --- a/{{cookiecutter.project_slug}}/config/settings/base.py +++ b/{{cookiecutter.project_slug}}/config/settings/base.py @@ -130,9 +130,7 @@ PASSWORD_HASHERS = [ ] # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" - }, + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, @@ -257,9 +255,8 @@ LOGGING = { "disable_existing_loggers": False, "formatters": { "verbose": { - "format": "%(levelname)s %(asctime)s %(module)s " - "%(process)d %(thread)d %(message)s" - } + "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s", + }, }, "handlers": { "console": { diff --git a/{{cookiecutter.project_slug}}/config/settings/local.py b/{{cookiecutter.project_slug}}/config/settings/local.py index 09d3bb9f..adab6087 100644 --- a/{{cookiecutter.project_slug}}/config/settings/local.py +++ b/{{cookiecutter.project_slug}}/config/settings/local.py @@ -37,9 +37,7 @@ EMAIL_HOST = "localhost" EMAIL_PORT = 1025 {%- else -%} # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -EMAIL_BACKEND = env( - "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" -) +EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend") {%- endif %} {%- if cookiecutter.use_whitenoise == 'y' %} diff --git a/{{cookiecutter.project_slug}}/config/settings/production.py b/{{cookiecutter.project_slug}}/config/settings/production.py index d5760147..e76b6399 100644 --- a/{{cookiecutter.project_slug}}/config/settings/production.py +++ b/{{cookiecutter.project_slug}}/config/settings/production.py @@ -56,15 +56,11 @@ CSRF_COOKIE_SECURE = True # TODO: set this to 60 seconds first and then to 518400 once you prove the former works SECURE_HSTS_SECONDS = 60 # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains -SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( - "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True -) +SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool("DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True) # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff -SECURE_CONTENT_TYPE_NOSNIFF = env.bool( - "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True -) +SECURE_CONTENT_TYPE_NOSNIFF = env.bool("DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True) {% if cookiecutter.cloud_provider != 'None' -%} # STORAGES @@ -85,7 +81,7 @@ AWS_QUERYSTRING_AUTH = False _AWS_EXPIRY = 60 * 60 * 24 * 7 # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings AWS_S3_OBJECT_PARAMETERS = { - "CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate" + "CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate", } # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings AWS_S3_MAX_MEMORY_SIZE = env.int( @@ -188,9 +184,7 @@ ANYMAIL = { EMAIL_BACKEND = "anymail.backends.mandrill.EmailBackend" ANYMAIL = { "MANDRILL_API_KEY": env("MANDRILL_API_KEY"), - "MANDRILL_API_URL": env( - "MANDRILL_API_URL", default="https://mandrillapp.com/api/1.0" - ), + "MANDRILL_API_URL": env("MANDRILL_API_URL", default="https://mandrillapp.com/api/1.0"), } {%- elif cookiecutter.mail_service == 'Postmark' %} # https://anymail.readthedocs.io/en/stable/esps/postmark/ @@ -211,18 +205,14 @@ ANYMAIL = { EMAIL_BACKEND = "anymail.backends.sendinblue.EmailBackend" ANYMAIL = { "SENDINBLUE_API_KEY": env("SENDINBLUE_API_KEY"), - "SENDINBLUE_API_URL": env( - "SENDINBLUE_API_URL", default="https://api.sendinblue.com/v3/" - ), + "SENDINBLUE_API_URL": env("SENDINBLUE_API_URL", default="https://api.sendinblue.com/v3/"), } {%- elif cookiecutter.mail_service == 'SparkPost' %} # https://anymail.readthedocs.io/en/stable/esps/sparkpost/ EMAIL_BACKEND = "anymail.backends.sparkpost.EmailBackend" ANYMAIL = { "SPARKPOST_API_KEY": env("SPARKPOST_API_KEY"), - "SPARKPOST_API_URL": env( - "SPARKPOST_API_URL", default="https://api.sparkpost.com/api/v1" - ), + "SPARKPOST_API_URL": env("SPARKPOST_API_URL", default="https://api.sparkpost.com/api/v1"), } {%- elif cookiecutter.mail_service == 'Other SMTP' %} # https://anymail.readthedocs.io/en/stable/esps @@ -278,9 +268,8 @@ LOGGING = { "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, "formatters": { "verbose": { - "format": "%(levelname)s %(asctime)s %(module)s " - "%(process)d %(thread)d %(message)s" - } + "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s", + }, }, "handlers": { "mail_admins": { @@ -314,9 +303,8 @@ LOGGING = { "disable_existing_loggers": True, "formatters": { "verbose": { - "format": "%(levelname)s %(asctime)s %(module)s " - "%(process)d %(thread)d %(message)s" - } + "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s", + }, }, "handlers": { "console": { @@ -376,7 +364,7 @@ sentry_sdk.init( # ------------------------------------------------------------------------------- # Tools that generate code samples can use SERVERS to point to the correct domain SPECTACULAR_SETTINGS["SERVERS"] = [ # noqa: F405 - {"url": "https://{{ cookiecutter.domain_name }}", "description": "Production server"} + {"url": "https://{{ cookiecutter.domain_name }}", "description": "Production server"}, ] {%- endif %} diff --git a/{{cookiecutter.project_slug}}/config/settings/test.py b/{{cookiecutter.project_slug}}/config/settings/test.py index 7941c741..92211ec7 100644 --- a/{{cookiecutter.project_slug}}/config/settings/test.py +++ b/{{cookiecutter.project_slug}}/config/settings/test.py @@ -32,9 +32,7 @@ TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore # noqa: F405 {%- if cookiecutter.frontend_pipeline == 'Webpack' %} # django-webpack-loader # ------------------------------------------------------------------------------ -WEBPACK_LOADER["DEFAULT"][ # noqa: F405 - "LOADER_CLASS" -] = "webpack_loader.loader.FakeWebpackLoader" +WEBPACK_LOADER["DEFAULT"]["LOADER_CLASS"] = "webpack_loader.loader.FakeWebpackLoader" # noqa: F405 {%- endif %} # Your stuff... diff --git a/{{cookiecutter.project_slug}}/config/urls.py b/{{cookiecutter.project_slug}}/config/urls.py index ab42cc10..7c5ad1a7 100644 --- a/{{cookiecutter.project_slug}}/config/urls.py +++ b/{{cookiecutter.project_slug}}/config/urls.py @@ -14,9 +14,7 @@ from rest_framework.authtoken.views import obtain_auth_token urlpatterns = [ path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), - path( - "about/", TemplateView.as_view(template_name="pages/about.html"), name="about" - ), + path("about/", TemplateView.as_view(template_name="pages/about.html"), name="about"), # Django Admin, use {% raw %}{% url 'admin:index' %}{% endraw %} path(settings.ADMIN_URL, admin.site.urls), # User management diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml new file mode 100644 index 00000000..6acac9b2 --- /dev/null +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -0,0 +1,92 @@ +# ==== pytest ==== +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "--ds=config.settings.test --reuse-db" +python_files = [ + "tests.py", + "test_*.py", +] +{%- if cookiecutter.frontend_pipeline == 'Gulp' %} +norecursedirs = ["node_modules"] +{%- endif %} + +# ==== Coverage ==== +[tool.coverage.run] +include = ["{{cookiecutter.project_slug}}/**"] +omit = ["*/migrations/*", "*/tests/*"] +plugins = ["django_coverage_plugin"] + + +# ==== black ==== +[tool.black] +line-length = 119 +target-version = ['py311'] + + +# ==== isort ==== +[tool.isort] +profile = "black" +line_length = 119 +known_first_party = [ + "{{cookiecutter.project_slug}}", + "config", +] +skip = ["venv/"] +skip_glob = ["**/migrations/*.py"] + + +# ==== mypy ==== +[tool.mypy] +python_version = "3.11" +check_untyped_defs = true +ignore_missing_imports = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true +plugins = [ + "mypy_django_plugin.main", +{%- if cookiecutter.use_drf == "y" %} + "mypy_drf_plugin.main", +{%- endif %} +] + +[[tool.mypy.overrides]] +# Django migrations should not produce any errors: +module = "*.migrations.*" +ignore_errors = true + +[tool.django-stubs] +django_settings_module = "config.settings.test" + + +# ==== PyLint ==== +[tool.pylint.MASTER] +load-plugins = [ + "pylint_django", +{%- if cookiecutter.use_celery == "y" %} + "pylint_celery", +{%- endif %} +] +django-settings-module = "config.settings.local" + +[tool.pylint.FORMAT] +max-line-length = 119 + +[tool.pylint."MESSAGES CONTROL"] +disable = [ + "missing-docstring", + "invalid-name", +] + +[tool.pylint.DESIGN] +max-parents = 13 + +[tool.pylint.TYPECHECK] +generated-members = [ + "REQUEST", + "acl_users", + "aq_parent", + "[a-zA-Z]+_set{1,2}", + "save", + "delete", +] diff --git a/{{cookiecutter.project_slug}}/pytest.ini b/{{cookiecutter.project_slug}}/pytest.ini deleted file mode 100644 index 969c7921..00000000 --- a/{{cookiecutter.project_slug}}/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -addopts = --ds=config.settings.test --reuse-db -python_files = tests.py test_*.py -{%- if cookiecutter.frontend_pipeline == 'Gulp' %} -norecursedirs = node_modules -{%- endif %} diff --git a/{{cookiecutter.project_slug}}/setup.cfg b/{{cookiecutter.project_slug}}/setup.cfg index ab191732..82906421 100644 --- a/{{cookiecutter.project_slug}}/setup.cfg +++ b/{{cookiecutter.project_slug}}/setup.cfg @@ -1,40 +1,10 @@ +# flake8 and pycodestyle don't support pyproject.toml +# https://github.com/PyCQA/flake8/issues/234 +# https://github.com/PyCQA/pycodestyle/issues/813 [flake8] -max-line-length = 120 +max-line-length = 119 exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv,.venv [pycodestyle] -max-line-length = 120 +max-line-length = 119 exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv,.venv - -[isort] -line_length = 88 -known_first_party = {{cookiecutter.project_slug}},config -multi_line_output = 3 -default_section = THIRDPARTY -skip = venv/ -skip_glob = **/migrations/*.py -include_trailing_comma = true -force_grid_wrap = 0 -use_parentheses = true - -[mypy] -python_version = 3.11 -check_untyped_defs = True -ignore_missing_imports = True -warn_unused_ignores = True -warn_redundant_casts = True -warn_unused_configs = True -plugins = mypy_django_plugin.main{% if cookiecutter.use_drf == "y" %}, mypy_drf_plugin.main{% endif %} - -[mypy.plugins.django-stubs] -django_settings_module = config.settings.test - -[mypy-*.migrations.*] -# Django migrations should not produce any errors: -ignore_errors = True - -[coverage:run] -include = {{cookiecutter.project_slug}}/** -omit = */migrations/*, */tests/* -plugins = - django_coverage_plugin diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py index fb653270..150a914e 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py @@ -1,5 +1,2 @@ __version__ = "{{ cookiecutter.version }}" -__version_info__ = tuple( - int(num) if num.isdigit() else num - for num in __version__.replace("-", ".", 1).split(".") -) +__version_info__ = tuple(int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")) 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 b497ddcf..6b26367d 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py @@ -11,12 +11,12 @@ class UserSerializer(serializers.ModelSerializer): fields = ["name", "url"] extra_kwargs = { - "url": {"view_name": "api:user-detail", "lookup_field": "pk"} + "url": {"view_name": "api:user-detail", "lookup_field": "pk"}, } {%- else %} fields = ["username", "name", "url"] extra_kwargs = { - "url": {"view_name": "api:user-detail", "lookup_field": "username"} + "url": {"view_name": "api:user-detail", "lookup_field": "username"}, } {%- endif %} diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_urls.py index 3fcfe4d0..334ab118 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_urls.py @@ -5,16 +5,10 @@ from {{ cookiecutter.project_slug }}.users.models import User def test_user_detail(user: User): {%- if cookiecutter.username_type == "email" %} - assert ( - reverse("api:user-detail", kwargs={"pk": user.pk}) - == f"/api/users/{user.pk}/" - ) + assert reverse("api:user-detail", kwargs={"pk": user.pk}) == f"/api/users/{user.pk}/" assert resolve(f"/api/users/{user.pk}/").view_name == "api:user-detail" {%- else %} - assert ( - reverse("api:user-detail", kwargs={"username": user.username}) - == f"/api/users/{user.username}/" - ) + assert reverse("api:user-detail", kwargs={"username": user.username}) == f"/api/users/{user.username}/" assert resolve(f"/api/users/{user.username}/").view_name == "api:user-detail" {%- endif %} 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 062d7dc8..a0d06889 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 @@ -8,10 +8,7 @@ def test_detail(user: User): assert reverse("users:detail", kwargs={"pk": user.pk}) == f"/users/{user.pk}/" assert resolve(f"/users/{user.pk}/").view_name == "users:detail" {%- else %} - assert ( - reverse("users:detail", kwargs={"username": user.username}) - == f"/users/{user.username}/" - ) + assert reverse("users:detail", kwargs={"username": user.username}) == f"/users/{user.username}/" assert resolve(f"/users/{user.username}/").view_name == "users:detail" {%- endif %} diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py index 8e868f78..82498e63 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py @@ -28,9 +28,7 @@ class UserUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): success_message = _("Information successfully updated") def get_success_url(self): - assert ( - self.request.user.is_authenticated - ) # for mypy to know that the user is authenticated + assert self.request.user.is_authenticated # for mypy to know that the user is authenticated return self.request.user.get_absolute_url() def get_object(self):