diff --git a/.github/contributors.json b/.github/contributors.json index c95797b1..785ee4b3 100644 --- a/.github/contributors.json +++ b/.github/contributors.json @@ -1115,7 +1115,7 @@ "twitter_username": "Qoyyuum" }, { - "name": "mfosterw", + "name": "Matthew Foster Walsh", "github_login": "mfosterw", "twitter_username": "" }, @@ -1518,5 +1518,15 @@ "name": "henningbra", "github_login": "henningbra", "twitter_username": "" + }, + { + "name": "Paul Wulff", + "github_login": "mtmpaulwulff", + "twitter_username": "" + }, + { + "name": "Mounir", + "github_login": "mounirmesselmeni", + "twitter_username": "" } ] \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e6590469..3582a212 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,6 +3,22 @@ version: 2 updates: + # Update Python deps for the template (not the generated project) + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + labels: + - "project infrastructure" + + # Update Python deps for the documentation + - package-ecosystem: "pip" + directory: "docs/" + schedule: + interval: "daily" + labels: + - "project infrastructure" + # Update GitHub actions in workflows - package-ecosystem: "github-actions" directory: "/" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index baafb694..6355c5ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,14 +26,14 @@ repos: args: ["--tab-width", "2"] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.1 hooks: - id: pyupgrade args: [--py311-plus] exclude: hooks/ - repo: https://github.com/psf/black - rev: 24.1.1 + rev: 24.2.0 hooks: - id: black diff --git a/.pyup.yml b/.pyup.yml index e5d4752e..13d336d5 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -14,8 +14,6 @@ pin: True label_prs: update requirements: - - "requirements.txt" - - "docs/requirements.txt" - "{{cookiecutter.project_slug}}/requirements/base.txt" - "{{cookiecutter.project_slug}}/requirements/local.txt" - "{{cookiecutter.project_slug}}/requirements/production.txt" diff --git a/CHANGELOG.md b/CHANGELOG.md index f05a4c51..088e814c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,134 @@ All enhancements and patches to Cookiecutter Django will be documented in this f +## 2024.02.23 + + +### Changed + +- Switch to local imports within app ([#4883](https://github.com/cookiecutter/cookiecutter-django/pull/4883)) + +- Install ruff extension in `devcontainer.json` ([#4887](https://github.com/cookiecutter/cookiecutter-django/pull/4887)) + +### Updated + +- Bump webpack-dev-server to 5.0.2 ([#4875](https://github.com/cookiecutter/cookiecutter-django/pull/4875)) + +## 2024.02.21 + + +### Changed + +- Switch to `celery.shared_task` to define tasks ([#4881](https://github.com/cookiecutter/cookiecutter-django/pull/4881)) + +- Replace usages of `get_user_model` by importing model directly ([#4879](https://github.com/cookiecutter/cookiecutter-django/pull/4879)) + +### Updated + +- Auto-update pre-commit hooks ([#4873](https://github.com/cookiecutter/cookiecutter-django/pull/4873)) + +- Update pre-commit to 3.6.2 ([#4874](https://github.com/cookiecutter/cookiecutter-django/pull/4874)) + +- Update ruff to 0.2.2 ([#4871](https://github.com/cookiecutter/cookiecutter-django/pull/4871)) + +## 2024.02.19 + + +### Updated + +- Update sentry-sdk to 1.40.5 ([#4876](https://github.com/cookiecutter/cookiecutter-django/pull/4876)) + +## 2024.02.17 + + +### Updated + +- Update pytest to 8.0.1 ([#4870](https://github.com/cookiecutter/cookiecutter-django/pull/4870)) + +## 2024.02.16 + + +### Changed + +- Speed up GitHub CI for Docker setup ([#4863](https://github.com/cookiecutter/cookiecutter-django/pull/4863)) + +### Documentation + +- Add link to the ruff repository in requirements ([#4866](https://github.com/cookiecutter/cookiecutter-django/pull/4866)) + +## 2024.02.13 + + +### Changed + +- Ruff linting & formatting ([#4834](https://github.com/cookiecutter/cookiecutter-django/pull/4834)) + +### Updated + +- Update uvicorn to 0.27.1 ([#4848](https://github.com/cookiecutter/cookiecutter-django/pull/4848)) + +- Update sentry-sdk to 1.40.4 ([#4858](https://github.com/cookiecutter/cookiecutter-django/pull/4858)) + +- Bump traefik to 2.11.0 ([#4857](https://github.com/cookiecutter/cookiecutter-django/pull/4857)) + +- Auto-update pre-commit hooks ([#4855](https://github.com/cookiecutter/cookiecutter-django/pull/4855)) + +- Update black to 24.2.0 ([#4853](https://github.com/cookiecutter/cookiecutter-django/pull/4853)) + +## 2024.02.12 + + +### Updated + +- Update django-model-utils to 4.4.0 ([#4850](https://github.com/cookiecutter/cookiecutter-django/pull/4850)) + +- Update pre-commit to 3.6.1 ([#4849](https://github.com/cookiecutter/cookiecutter-django/pull/4849)) + +- Update django-upgrade to 1.16.0 ([#4851](https://github.com/cookiecutter/cookiecutter-django/pull/4851)) + +- Auto-update pre-commit hooks ([#4852](https://github.com/cookiecutter/cookiecutter-django/pull/4852)) + +## 2024.02.09 + + +### Updated + +- Update sentry-sdk to 1.40.3 ([#4847](https://github.com/cookiecutter/cookiecutter-django/pull/4847)) + +- Update django-allauth to 0.61.1 ([#4846](https://github.com/cookiecutter/cookiecutter-django/pull/4846)) + +- Update python-slugify to 8.0.4 ([#4844](https://github.com/cookiecutter/cookiecutter-django/pull/4844)) + +## 2024.02.08 + + +### Updated + +- Bump python to 3.11.8 in compose/local/docs ([#4840](https://github.com/cookiecutter/cookiecutter-django/pull/4840)) + +- Bump python to 3.11.8 in compose/local/django ([#4841](https://github.com/cookiecutter/cookiecutter-django/pull/4841)) + +- Bump python to 3.11.8 in compose/production/django ([#4842](https://github.com/cookiecutter/cookiecutter-django/pull/4842)) + +## 2024.02.07 + + +### Changed + +- Extend docker test with deploy check ([#4838](https://github.com/cookiecutter/cookiecutter-django/pull/4838)) + +- Generic UserManager ([#4836](https://github.com/cookiecutter/cookiecutter-django/pull/4836)) + +### Updated + +- Update django-allauth to 0.61.0 ([#4839](https://github.com/cookiecutter/cookiecutter-django/pull/4839)) + +- Bump gulp-postcss to 10.0.0 ([#4835](https://github.com/cookiecutter/cookiecutter-django/pull/4835)) + +- Update sentry-sdk to 1.40.2 ([#4837](https://github.com/cookiecutter/cookiecutter-django/pull/4837)) + +- Update django to 4.2.10 ([#4833](https://github.com/cookiecutter/cookiecutter-django/pull/4833)) + ## 2024.02.05 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 7b778f43..30064acb 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1454,6 +1454,13 @@ Listed in alphabetical order. + + Matthew Foster Walsh + + mfosterw + + + Matthew Sisley @@ -1489,13 +1496,6 @@ Listed in alphabetical order. - - mfosterw - - mfosterw - - - Michael Gecht @@ -1552,6 +1552,13 @@ Listed in alphabetical order. + + Mounir + + mounirmesselmeni + + + mozillazg @@ -1650,6 +1657,13 @@ Listed in alphabetical order. + + Paul Wulff + + mtmpaulwulff + + + Pawan Chaurasia diff --git a/docs/linters.rst b/docs/linters.rst index a4f60cc8..1fc44f30 100644 --- a/docs/linters.rst +++ b/docs/linters.rst @@ -4,40 +4,30 @@ Linters .. index:: linters -flake8 +ruff ------ -To run flake8: :: +Ruff is a Python linter and code formatter, written in Rust. +It is a aggregation of flake8, pylint, pyupgrade and many more. - $ flake8 +Ruff comes with a linter (``ruff check``) and a formatter (``ruff format``). +The linter is a wrapper around flake8, pylint, and other linters, +and the formatter is a wrapper around black, isort, and other formatters. -The config for flake8 is located in setup.cfg. It specifies: +To run ruff without modifying your files: :: -* Set max line length to 120 chars -* Exclude ``.tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules`` + $ ruff format --diff . + $ ruff check . -pylint ------- +Ruff is capable of fixing most of the problems it encounters. +Be sure you commit first before running `ruff` so you can restore to a savepoint (and amend afterwards to prevent a double commit. : :: -To run pylint: :: + $ ruff format . + $ ruff check --fix . + # be careful with the --unsafe-fixes option, it can break your code + $ ruff check --fix --unsafe-fixes . - $ pylint - -The config for pylint is located in .pylintrc. It specifies: - -* Use the pylint_django plugin. If using Celery, also use pylint_celery. -* Set max line length to 120 chars -* Disable linting messages for missing docstring and invalid name -* max-parents=13 - -pycodestyle ------------ - -This is included in flake8's checks, but you can also run it separately to see a more detailed report: :: - - $ pycodestyle - -The config for pycodestyle is located in setup.cfg. It specifies: - -* Set max line length to 120 chars -* Exclude ``.tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules`` +The config for ruff is located in pyproject.toml. +On of the most important option is `tool.ruff.lint.select`. +`select` determines which linters are run. In example, `DJ `_ refers to flake8-django. +For a full list of available linters, see `https://docs.astral.sh/ruff/rules/ `_ diff --git a/requirements.txt b/requirements.txt index b9ddeac0..138744a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,18 @@ -cookiecutter==2.5.0 +cookiecutter==2.6.0 sh==2.0.6; sys_platform != "win32" binaryornot==0.4.4 # Code quality # ------------------------------------------------------------------------------ -black==24.1.1 -isort==5.13.2 -flake8==7.0.0 -django-upgrade==1.15.0 +ruff==0.2.2 +django-upgrade==1.16.0 djlint==1.34.1 -pre-commit==3.6.0 +pre-commit==3.6.2 # Testing # ------------------------------------------------------------------------------ -tox==4.12.1 -pytest==8.0.0 +tox==4.13.0 +pytest==8.0.1 pytest-xdist==3.5.0 pytest-cookies==0.7.0 pytest-instafail==0.5.0 @@ -23,6 +21,6 @@ pyyaml==6.0.1 # Scripting # ------------------------------------------------------------------------------ PyGithub==2.2.0 -gitpython==3.1.41 +gitpython==3.1.42 jinja2==3.1.3 requests==2.31.0 diff --git a/setup.py b/setup.py index 554f649a..f7947b0e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ except ImportError: from distutils.core import setup # We use calendar versioning -version = "2024.02.05" +version = "2024.02.23" with open("README.md") as readme_file: long_description = readme_file.read() diff --git a/tests/test_cookiecutter_generation.py b/tests/test_cookiecutter_generation.py index 31d006be..b744a986 100755 --- a/tests/test_cookiecutter_generation.py +++ b/tests/test_cookiecutter_generation.py @@ -180,28 +180,25 @@ def test_project_generation(cookies, context, context_override): @pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id) -def test_flake8_passes(cookies, context_override): - """Generated project should pass flake8.""" +def test_ruff_check_passes(cookies, context_override): + """Generated project should pass ruff check.""" result = cookies.bake(extra_context=context_override) try: - sh.flake8(_cwd=str(result.project_path)) + sh.ruff("check", ".", _cwd=str(result.project_path)) except sh.ErrorReturnCode as e: pytest.fail(e.stdout.decode()) @auto_fixable @pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id) -def test_black_passes(cookies, context_override): - """Check whether generated project passes black style.""" +def test_ruff_format_passes(cookies, context_override): + """Check whether generated project passes ruff format.""" result = cookies.bake(extra_context=context_override) try: - sh.black( - "--check", - "--diff", - "--exclude", - "migrations", + sh.ruff( + "format", ".", _cwd=str(result.project_path), ) @@ -287,7 +284,7 @@ def test_travis_invokes_pytest(cookies, context, use_docker, expected_test_scrip with open(f"{result.project_path}/.travis.yml") as travis_yml: try: yml = yaml.safe_load(travis_yml)["jobs"]["include"] - assert yml[0]["script"] == ["flake8"] + assert yml[0]["script"] == ["ruff check ."] assert yml[1]["script"] == [expected_test_script] except yaml.YAMLError as e: pytest.fail(str(e)) diff --git a/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json b/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json index c4158dc1..e16d06a2 100644 --- a/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json +++ b/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json @@ -35,24 +35,13 @@ "analysis.typeCheckingMode": "basic", "defaultInterpreterPath": "/usr/local/bin/python", "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "always" }, - // Uncomment when fixed - // https://github.com/microsoft/vscode-remote-release/issues/8474 - // "editor.defaultFormatter": "ms-python.black-formatter", - "formatting.blackPath": "/usr/local/bin/black", - "formatting.provider": "black", + "editor.defaultFormatter": "charliermarsh.ruff", "languageServer": "Pylance", - // "linting.banditPath": "/usr/local/py-utils/bin/bandit", "linting.enabled": true, - "linting.flake8Enabled": true, - "linting.flake8Path": "/usr/local/bin/flake8", "linting.mypyEnabled": true, "linting.mypyPath": "/usr/local/bin/mypy", - "linting.pycodestylePath": "/usr/local/bin/pycodestyle", - // "linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", - "linting.pylintEnabled": true, - "linting.pylintPath": "/usr/local/bin/pylint" } }, // https://code.visualstudio.com/docs/remote/devcontainerjson-reference#_vs-code-specific-properties @@ -65,8 +54,7 @@ // python "ms-python.python", "ms-python.vscode-pylance", - "ms-python.isort", - "ms-python.black-formatter", + "charliermarsh.ruff", // django "batisteo.vscode-django" ] diff --git a/{{cookiecutter.project_slug}}/.github/workflows/ci.yml b/{{cookiecutter.project_slug}}/.github/workflows/ci.yml index e39933fe..414ee1e6 100644 --- a/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +++ b/{{cookiecutter.project_slug}}/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: - name: Run pre-commit uses: pre-commit/action@v3.0.0 - # With no caching at all the entire ci process takes 4m 30s to complete! + # With no caching at all the entire ci process takes 3m to complete! pytest: runs-on: ubuntu-latest {%- if cookiecutter.use_docker == 'n' %} @@ -69,7 +69,7 @@ jobs: {%- if cookiecutter.use_docker == 'y' %} - name: Build the Stack - run: docker compose -f local.yml build + run: docker compose -f local.yml build django - name: Run DB Migrations run: docker compose -f local.yml run --rm django python manage.py migrate diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index 12a1f5e8..1d06c042 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -28,31 +28,20 @@ repos: exclude: '{{cookiecutter.project_slug}}/templates/' - repo: https://github.com/adamchainz/django-upgrade - rev: '1.15.0' + rev: '1.16.0' hooks: - id: django-upgrade args: ['--target-version', '4.2'] - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + # Run the Ruff linter. + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.2 hooks: - - id: pyupgrade - args: [--py311-plus] - - - repo: https://github.com/psf/black - rev: 24.1.1 - hooks: - - id: black - - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - - - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 - hooks: - - id: flake8 + # Linter + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + # Formatter + - id: ruff-format - repo: https://github.com/Riverside-Healthcare/djLint rev: v1.34.1 diff --git a/{{cookiecutter.project_slug}}/.travis.yml b/{{cookiecutter.project_slug}}/.travis.yml index cd703d3a..78709191 100644 --- a/{{cookiecutter.project_slug}}/.travis.yml +++ b/{{cookiecutter.project_slug}}/.travis.yml @@ -10,9 +10,9 @@ jobs: include: - name: "Linter" before_script: - - pip install -q flake8 + - pip install -q ruff script: - - "flake8" + - ruff check . - name: "Django Test" {%- if cookiecutter.use_docker == 'y' %} @@ -24,7 +24,7 @@ jobs: - docker compose -f local.yml run --rm django python manage.py migrate - docker compose -f local.yml up -d script: - - "docker compose -f local.yml run django pytest" + - docker compose -f local.yml run django pytest after_failure: - docker compose -f local.yml logs {%- else %} @@ -41,5 +41,5 @@ jobs: install: - pip install -r requirements/local.txt script: - - "pytest" + - pytest {%- endif %} diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index ccf245a2..cb757689 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -3,7 +3,7 @@ {{ cookiecutter.description }} [![Built with Cookiecutter Django](https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg?logo=cookiecutter)](https://github.com/cookiecutter/cookiecutter-django/) -[![Black code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) {%- if cookiecutter.open_source_license != "Not open source" %} diff --git a/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile b/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile index 575a4518..75d5cbb9 100644 --- a/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/local/django/Dockerfile @@ -1,5 +1,5 @@ # define an alias for the specific python version used in this file. -FROM docker.io/python:3.11.7-slim-bookworm as python +FROM docker.io/python:3.11.8-slim-bookworm as python # Python build stage FROM docker.io/python as python-build-stage diff --git a/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile b/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile index 2b68c84b..87a1b246 100644 --- a/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/local/docs/Dockerfile @@ -1,5 +1,5 @@ # define an alias for the specific python version used in this file. -FROM docker.io/python:3.11.7-slim-bookworm as python +FROM docker.io/python:3.11.8-slim-bookworm as python # Python build stage diff --git a/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile index 079f6bd7..fb7fec50 100644 --- a/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/django/Dockerfile @@ -25,7 +25,7 @@ RUN npm run build {%- endif %} # define an alias for the specific python version used in this file. -FROM docker.io/python:3.11.7-slim-bookworm as python +FROM docker.io/python:3.11.8-slim-bookworm as python # Python build stage FROM docker.io/python as python-build-stage diff --git a/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile index c32b1587..ea918e91 100644 --- a/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/traefik/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/traefik:2.10.7 +FROM docker.io/traefik:2.11.0 RUN mkdir -p /etc/traefik/acme \ && touch /etc/traefik/acme/acme.json \ && chmod 600 /etc/traefik/acme/acme.json diff --git a/{{cookiecutter.project_slug}}/config/api_router.py b/{{cookiecutter.project_slug}}/config/api_router.py index 743069b2..d4de098f 100644 --- a/{{cookiecutter.project_slug}}/config/api_router.py +++ b/{{cookiecutter.project_slug}}/config/api_router.py @@ -1,12 +1,10 @@ from django.conf import settings -from rest_framework.routers import DefaultRouter, SimpleRouter +from rest_framework.routers import DefaultRouter +from rest_framework.routers import SimpleRouter from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet -if settings.DEBUG: - router = DefaultRouter() -else: - router = SimpleRouter() +router = DefaultRouter() if settings.DEBUG else SimpleRouter() router.register("users", UserViewSet) diff --git a/{{cookiecutter.project_slug}}/config/asgi.py b/{{cookiecutter.project_slug}}/config/asgi.py index c391bf87..edfffbbc 100644 --- a/{{cookiecutter.project_slug}}/config/asgi.py +++ b/{{cookiecutter.project_slug}}/config/asgi.py @@ -1,3 +1,4 @@ +# ruff: noqa """ ASGI config for {{ cookiecutter.project_name }} project. @@ -29,7 +30,7 @@ django_application = get_asgi_application() # application = HelloWorldApplication(application) # Import websocket application here, so apps from django_application are loaded first -from config.websocket import websocket_application # noqa isort:skip +from config.websocket import websocket_application async def application(scope, receive, send): @@ -38,4 +39,5 @@ async def application(scope, receive, send): elif scope["type"] == "websocket": await websocket_application(scope, receive, send) else: - raise NotImplementedError(f"Unknown scope type {scope['type']}") + msg = f"Unknown scope type {scope['type']}" + raise NotImplementedError(msg) diff --git a/{{cookiecutter.project_slug}}/config/settings/base.py b/{{cookiecutter.project_slug}}/config/settings/base.py index 9884814b..b7eb7e80 100644 --- a/{{cookiecutter.project_slug}}/config/settings/base.py +++ b/{{cookiecutter.project_slug}}/config/settings/base.py @@ -1,3 +1,4 @@ +# ruff: noqa: ERA001, E501 """Base settings to build other settings files upon.""" from pathlib import Path @@ -137,7 +138,9 @@ 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"}, @@ -210,7 +213,7 @@ TEMPLATES = [ "{{cookiecutter.project_slug}}.users.context_processors.allauth_settings", ], }, - } + }, ] # https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer @@ -274,7 +277,7 @@ LOGGING = { "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose", - } + }, }, "root": {"level": "INFO", "handlers": ["console"]}, } @@ -380,7 +383,7 @@ WEBPACK_LOADER = { "STATS_FILE": BASE_DIR / "webpack-stats.json", "POLL_INTERVAL": 0.1, "IGNORE": [r".+\.hot-update.js", r".+\.map"], - } + }, } {%- endif %} diff --git a/{{cookiecutter.project_slug}}/config/settings/local.py b/{{cookiecutter.project_slug}}/config/settings/local.py index 0304d6cd..f1edb514 100644 --- a/{{cookiecutter.project_slug}}/config/settings/local.py +++ b/{{cookiecutter.project_slug}}/config/settings/local.py @@ -1,4 +1,10 @@ -from .base import * # noqa +# ruff: noqa: E501 +from .base import * # noqa: F403 +from .base import INSTALLED_APPS +from .base import MIDDLEWARE +{%- if cookiecutter.frontend_pipeline == 'Webpack' %} +from .base import WEBPACK_LOADER +{%- endif %} from .base import env # GENERAL @@ -11,7 +17,7 @@ SECRET_KEY = env( default="!!!SET DJANGO_SECRET_KEY!!!", ) # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts -ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] +ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] # noqa: S104 # CACHES # ------------------------------------------------------------------------------ @@ -20,7 +26,7 @@ CACHES = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "", - } + }, } # EMAIL @@ -37,7 +43,9 @@ 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' %} @@ -45,15 +53,15 @@ EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.c # WhiteNoise # ------------------------------------------------------------------------------ # http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development -INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # noqa: F405 +INSTALLED_APPS = ["whitenoise.runserver_nostatic", *INSTALLED_APPS] {% endif %} # django-debug-toolbar # ------------------------------------------------------------------------------ # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites -INSTALLED_APPS += ["debug_toolbar"] # noqa: F405 +INSTALLED_APPS += ["debug_toolbar"] # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware -MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa: F405 +MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config DEBUG_TOOLBAR_CONFIG = { "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], @@ -80,7 +88,7 @@ if env("USE_DOCKER") == "yes": # django-extensions # ------------------------------------------------------------------------------ # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration -INSTALLED_APPS += ["django_extensions"] # noqa: F405 +INSTALLED_APPS += ["django_extensions"] {% if cookiecutter.use_celery == 'y' -%} # Celery @@ -96,7 +104,7 @@ CELERY_TASK_EAGER_PROPAGATES = True {%- if cookiecutter.frontend_pipeline == 'Webpack' %} # django-webpack-loader # ------------------------------------------------------------------------------ -WEBPACK_LOADER["DEFAULT"]["CACHE"] = not DEBUG # noqa: F405 +WEBPACK_LOADER["DEFAULT"]["CACHE"] = not DEBUG {%- endif %} # Your stuff... diff --git a/{{cookiecutter.project_slug}}/config/settings/production.py b/{{cookiecutter.project_slug}}/config/settings/production.py index 7f629357..0cebe1d9 100644 --- a/{{cookiecutter.project_slug}}/config/settings/production.py +++ b/{{cookiecutter.project_slug}}/config/settings/production.py @@ -1,3 +1,4 @@ +# ruff: noqa: E501 {% if cookiecutter.use_sentry == 'y' -%} import logging @@ -12,7 +13,12 @@ from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.redis import RedisIntegration {% endif -%} -from .base import * # noqa +from .base import * # noqa: F403 +from .base import DATABASES +from .base import INSTALLED_APPS +{%- if cookiecutter.use_drf == "y" %} +from .base import SPECTACULAR_SETTINGS +{%- endif %} from .base import env # GENERAL @@ -24,7 +30,7 @@ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["{{ cookiecutter.domai # DATABASES # ------------------------------------------------------------------------------ -DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa: F405 +DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # CACHES # ------------------------------------------------------------------------------ @@ -38,7 +44,7 @@ CACHES = { # https://github.com/jazzband/django-redis#memcached-exceptions-behavior "IGNORE_EXCEPTIONS": True, }, - } + }, } # SECURITY @@ -56,17 +62,23 @@ 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 # ------------------------------------------------------------------------------ # https://django-storages.readthedocs.io/en/latest/#installation -INSTALLED_APPS += ["storages"] # noqa: F405 +INSTALLED_APPS += ["storages"] {%- endif -%} {% if cookiecutter.cloud_provider == 'AWS' %} # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings @@ -197,7 +209,7 @@ ADMIN_URL = env("DJANGO_ADMIN_URL") # Anymail # ------------------------------------------------------------------------------ # https://anymail.readthedocs.io/en/stable/installation/#installing-anymail -INSTALLED_APPS += ["anymail"] # noqa: F405 +INSTALLED_APPS += ["anymail"] # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference {%- if cookiecutter.mail_service == 'Mailgun' %} @@ -273,7 +285,7 @@ COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage" COMPRESS_STORAGE = STORAGES["staticfiles"]["BACKEND"] {%- endif %} # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL -COMPRESS_URL = STATIC_URL{% if cookiecutter.use_whitenoise == 'y' or cookiecutter.cloud_provider == 'None' %} # noqa: F405{% endif %} +COMPRESS_URL = STATIC_URL{% if cookiecutter.use_whitenoise == 'y' or cookiecutter.cloud_provider == 'None' %}{% endif %} {%- if cookiecutter.use_whitenoise == 'y' %} # https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_OFFLINE COMPRESS_OFFLINE = True # Offline compression is required when using Whitenoise @@ -291,7 +303,7 @@ COMPRESS_FILTERS = { # Collectfast # ------------------------------------------------------------------------------ # https://github.com/antonagestam/collectfast#installation -INSTALLED_APPS = ["collectfast"] + INSTALLED_APPS # noqa: F405 +INSTALLED_APPS = ["collectfast", *INSTALLED_APPS] {% endif %} # LOGGING # ------------------------------------------------------------------------------ @@ -351,7 +363,7 @@ LOGGING = { "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose", - } + }, }, "root": {"level": "INFO", "handlers": ["console"]}, "loggers": { @@ -403,7 +415,7 @@ sentry_sdk.init( # django-rest-framework # ------------------------------------------------------------------------------- # Tools that generate code samples can use SERVERS to point to the correct domain -SPECTACULAR_SETTINGS["SERVERS"] = [ # noqa: F405 +SPECTACULAR_SETTINGS["SERVERS"] = [ {"url": "https://{{ cookiecutter.domain_name }}", "description": "Production server"}, ] diff --git a/{{cookiecutter.project_slug}}/config/settings/test.py b/{{cookiecutter.project_slug}}/config/settings/test.py index 7c54e021..696b4871 100644 --- a/{{cookiecutter.project_slug}}/config/settings/test.py +++ b/{{cookiecutter.project_slug}}/config/settings/test.py @@ -2,7 +2,8 @@ With these settings, tests run faster. """ -from .base import * # noqa +from .base import * # noqa: F403 +from .base import TEMPLATES from .base import env # GENERAL @@ -27,7 +28,7 @@ EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" # DEBUGGING FOR TEMPLATES # ------------------------------------------------------------------------------ -TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore # noqa: F405 +TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore[index] # MEDIA # ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/config/urls.py b/{{cookiecutter.project_slug}}/config/urls.py index 7c5ad1a7..5d9301b6 100644 --- a/{{cookiecutter.project_slug}}/config/urls.py +++ b/{{cookiecutter.project_slug}}/config/urls.py @@ -1,27 +1,37 @@ +# ruff: noqa from django.conf import settings from django.conf.urls.static import static from django.contrib import admin {%- if cookiecutter.use_async == 'y' %} from django.contrib.staticfiles.urls import staticfiles_urlpatterns {%- endif %} -from django.urls import include, path +from django.urls import include +from django.urls import path from django.views import defaults as default_views from django.views.generic import TemplateView {%- if cookiecutter.use_drf == 'y' %} -from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView +from drf_spectacular.views import SpectacularAPIView +from drf_spectacular.views import SpectacularSwaggerView from rest_framework.authtoken.views import obtain_auth_token {%- endif %} 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 path("users/", include("{{ cookiecutter.project_slug }}.users.urls", namespace="users")), path("accounts/", include("allauth.urls")), # Your stuff: custom urls includes go here -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + # ... + # Media files + *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), +] {%- if cookiecutter.use_async == 'y' %} if settings.DEBUG: # Static file serving when using Gunicorn + Uvicorn for local web socket development diff --git a/{{cookiecutter.project_slug}}/config/wsgi.py b/{{cookiecutter.project_slug}}/config/wsgi.py index 1dbd8a8d..73a6cddc 100644 --- a/{{cookiecutter.project_slug}}/config/wsgi.py +++ b/{{cookiecutter.project_slug}}/config/wsgi.py @@ -1,3 +1,4 @@ +# ruff: noqa """ WSGI config for {{ cookiecutter.project_name }} project. diff --git a/{{cookiecutter.project_slug}}/docs/conf.py b/{{cookiecutter.project_slug}}/docs/conf.py index c640e1c6..40d59dbb 100644 --- a/{{cookiecutter.project_slug}}/docs/conf.py +++ b/{{cookiecutter.project_slug}}/docs/conf.py @@ -1,3 +1,4 @@ +# ruff: noqa # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full diff --git a/{{cookiecutter.project_slug}}/manage.py b/{{cookiecutter.project_slug}}/manage.py index c44cc826..a3987181 100755 --- a/{{cookiecutter.project_slug}}/manage.py +++ b/{{cookiecutter.project_slug}}/manage.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# ruff: noqa import os import sys from pathlib import Path @@ -13,7 +14,7 @@ if __name__ == "__main__": # issue is really that Django is missing to avoid masking other # exceptions on Python 2. try: - import django # noqa + import django except ImportError: raise ImportError( "Couldn't import Django. Are you sure it's installed and " diff --git a/{{cookiecutter.project_slug}}/merge_production_dotenvs_in_dotenv.py b/{{cookiecutter.project_slug}}/merge_production_dotenvs_in_dotenv.py index 35139fb2..c83ed716 100644 --- a/{{cookiecutter.project_slug}}/merge_production_dotenvs_in_dotenv.py +++ b/{{cookiecutter.project_slug}}/merge_production_dotenvs_in_dotenv.py @@ -1,3 +1,4 @@ +# ruff: noqa import os from collections.abc import Sequence from pathlib import Path diff --git a/{{cookiecutter.project_slug}}/package.json b/{{cookiecutter.project_slug}}/package.json index efa2136e..9ca72820 100644 --- a/{{cookiecutter.project_slug}}/package.json +++ b/{{cookiecutter.project_slug}}/package.json @@ -31,7 +31,7 @@ "webpack": "^5.65.0", "webpack-bundle-tracker": "^3.0.1", "webpack-cli": "^5.0.1", - "webpack-dev-server": "^4.6.0", + "webpack-dev-server": "^5.0.2", "webpack-merge": "^5.8.0" }, "engines": { diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 7e4c9aa9..a056c71c 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -16,25 +16,6 @@ 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" @@ -58,40 +39,6 @@ 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", -] - - # ==== djLint ==== [tool.djlint] blank_line_after_tag = "load,extends" @@ -110,3 +57,112 @@ indent_size = 2 [tool.djlint.js] indent_size = 2 + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "*/migrations/*.py", + "staticfiles/*" +] +# Same as Django: https://github.com/cookiecutter/cookiecutter-django/issues/4792. +line-length = 88 +indent-width = 4 +target-version = "py311" + +[tool.ruff.lint] +select = [ + "F", + "E", + "W", + "C90", + "I", + "N", + "UP", + "YTT", + # "ANN", # flake8-annotations: we should support this in the future but 100+ errors atm + "ASYNC", + "S", + "BLE", + "FBT", + "B", + "A", + "COM", + "C4", + "DTZ", + "T10", + "DJ", + "EM", + "EXE", + "FA", + 'ISC', + "ICN", + "G", + 'INP', + 'PIE', + "T20", + 'PYI', + 'PT', + "Q", + "RSE", + "RET", + "SLF", + "SLOT", + "SIM", + "TID", + "TCH", + "INT", + # "ARG", # Unused function argument + "PTH", + "ERA", + "PD", + "PGH", + "PL", + "TRY", + "FLY", + # "NPY", + # "AIR", + "PERF", + # "FURB", + # "LOG", + "RUF" +] +ignore = [ + "S101", # Use of assert detected https://docs.astral.sh/ruff/rules/assert/ + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "SIM102" # sometimes it's better to nest +] +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.ruff.lint.isort] +force-single-line = true diff --git a/{{cookiecutter.project_slug}}/requirements/base.txt b/{{cookiecutter.project_slug}}/requirements/base.txt index 27d303cf..f3f1c693 100644 --- a/{{cookiecutter.project_slug}}/requirements/base.txt +++ b/{{cookiecutter.project_slug}}/requirements/base.txt @@ -1,4 +1,4 @@ -python-slugify==8.0.3 # https://github.com/un33k/python-slugify +python-slugify==8.0.4 # https://github.com/un33k/python-slugify Pillow==10.2.0 # https://github.com/python-pillow/Pillow {%- if cookiecutter.frontend_pipeline == 'Django Compressor' %} {%- if cookiecutter.windows == 'y' and cookiecutter.use_docker == 'n' %} @@ -23,15 +23,15 @@ flower==2.0.1 # https://github.com/mher/flower {%- endif %} {%- endif %} {%- if cookiecutter.use_async == 'y' %} -uvicorn[standard]==0.27.0.post1 # https://github.com/encode/uvicorn +uvicorn[standard]==0.27.1 # https://github.com/encode/uvicorn {%- endif %} # Django # ------------------------------------------------------------------------------ django==4.2.10 # pyup: < 5.0 # https://www.djangoproject.com/ django-environ==0.11.2 # https://github.com/joke2k/django-environ -django-model-utils==4.3.1 # https://github.com/jazzband/django-model-utils -django-allauth[mfa]==0.61.0 # https://github.com/pennersr/django-allauth +django-model-utils==4.4.0 # https://github.com/jazzband/django-model-utils +django-allauth[mfa]==0.61.1 # https://github.com/pennersr/django-allauth django-crispy-forms==2.1 # https://github.com/django-crispy-forms/django-crispy-forms crispy-bootstrap5==2023.10 # https://github.com/django-crispy-forms/crispy-bootstrap5 {%- if cookiecutter.frontend_pipeline == 'Django Compressor' %} diff --git a/{{cookiecutter.project_slug}}/requirements/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt index 0c1ebca2..618226f6 100644 --- a/{{cookiecutter.project_slug}}/requirements/local.txt +++ b/{{cookiecutter.project_slug}}/requirements/local.txt @@ -15,7 +15,7 @@ watchfiles==0.21.0 # https://github.com/samuelcolvin/watchfiles # ------------------------------------------------------------------------------ mypy==1.7.1 # https://github.com/python/mypy django-stubs[compatible-mypy]==4.2.7 # https://github.com/typeddjango/django-stubs -pytest==8.0.0 # https://github.com/pytest-dev/pytest +pytest==8.0.1 # https://github.com/pytest-dev/pytest pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar {%- if cookiecutter.use_drf == "y" %} djangorestframework-stubs[compatible-mypy]==3.14.5 # https://github.com/typeddjango/djangorestframework-stubs @@ -28,16 +28,10 @@ sphinx-autobuild==2024.2.4 # https://github.com/GaretJax/sphinx-autobuild # Code quality # ------------------------------------------------------------------------------ -flake8==7.0.0 # https://github.com/PyCQA/flake8 -flake8-isort==6.1.1 # https://github.com/gforcada/flake8-isort -coverage==7.4.1 # https://github.com/nedbat/coveragepy -black==24.1.1 # https://github.com/psf/black +ruff==0.2.2 # https://github.com/astral-sh/ruff +coverage==7.4.3 # https://github.com/nedbat/coveragepy djlint==1.34.1 # https://github.com/Riverside-Healthcare/djLint -pylint-django==2.5.5 # https://github.com/PyCQA/pylint-django -{%- if cookiecutter.use_celery == 'y' %} -pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery -{%- endif %} -pre-commit==3.6.0 # https://github.com/pre-commit/pre-commit +pre-commit==3.6.2 # https://github.com/pre-commit/pre-commit # Django # ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/requirements/production.txt b/{{cookiecutter.project_slug}}/requirements/production.txt index ae9a2dd0..4d96e86e 100644 --- a/{{cookiecutter.project_slug}}/requirements/production.txt +++ b/{{cookiecutter.project_slug}}/requirements/production.txt @@ -8,7 +8,7 @@ psycopg[c]==3.1.18 # https://github.com/psycopg/psycopg Collectfast==2.2.0 # https://github.com/antonagestam/collectfast {%- endif %} {%- if cookiecutter.use_sentry == "y" %} -sentry-sdk==1.40.2 # https://github.com/getsentry/sentry-python +sentry-sdk==1.40.5 # https://github.com/getsentry/sentry-python {%- endif %} {%- if cookiecutter.use_docker == "n" and cookiecutter.windows == "y" %} hiredis==2.3.2 # https://github.com/redis/hiredis-py diff --git a/{{cookiecutter.project_slug}}/setup.cfg b/{{cookiecutter.project_slug}}/setup.cfg deleted file mode 100644 index 2412f174..00000000 --- a/{{cookiecutter.project_slug}}/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -# 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 = 119 -exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv,.venv -extend-ignore = E203 - -[pycodestyle] -max-line-length = 119 -exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv,.venv diff --git a/{{cookiecutter.project_slug}}/tests/__init__.py b/{{cookiecutter.project_slug}}/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/{{cookiecutter.project_slug}}/webpack/dev.config.js b/{{cookiecutter.project_slug}}/webpack/dev.config.js index 8276c348..7c774185 100644 --- a/{{cookiecutter.project_slug}}/webpack/dev.config.js +++ b/{{cookiecutter.project_slug}}/webpack/dev.config.js @@ -6,13 +6,16 @@ module.exports = merge(commonConfig, { devtool: 'inline-source-map', devServer: { port: 3000, - proxy: { - {%- if cookiecutter.use_docker == 'n' %} - '/': 'http://0.0.0.0:8000', - {%- else %} - '/': 'http://django:8000', - {%- endif %} - }, + proxy: [ + { + context: ['/'], + {%- if cookiecutter.use_docker == 'n' %} + target: 'http://0.0.0.0:8000', + {%- else %} + target: 'http://django:8000', + {%- endif %} + }, + ], client: { overlay: { errors: true, diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py index 150a914e..fb653270 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py @@ -1,2 +1,5 @@ __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}}/conftest.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py index 7095a471..98efcd75 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py @@ -5,10 +5,10 @@ from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory @pytest.fixture(autouse=True) -def media_storage(settings, tmpdir): +def _media_storage(settings, tmpdir) -> None: settings.MEDIA_ROOT = tmpdir.strpath -@pytest.fixture +@pytest.fixture() def user(db) -> User: return UserFactory() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0001_initial.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0001_initial.py index 304cd6d7..fd76afb2 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0001_initial.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0001_initial.py @@ -1,6 +1,7 @@ import django.contrib.sites.models from django.contrib.sites.models import _simple_domain_name_validator -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): @@ -38,5 +39,5 @@ class Migration(migrations.Migration): }, bases=(models.Model,), managers=[("objects", django.contrib.sites.models.SiteManager())], - ) + ), ] diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0002_alter_domain_unique.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0002_alter_domain_unique.py index 2c8d6dac..4a44a6a9 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0002_alter_domain_unique.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0002_alter_domain_unique.py @@ -1,5 +1,6 @@ import django.contrib.sites.models -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py index e1822375..85ee2d9c 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py @@ -23,7 +23,7 @@ def _update_or_create_site_with_sequence(site_model, connection, domain, name): # site is created. # To avoid this, we need to manually update DB sequence and make sure it's # greater than the maximum value. - max_id = site_model.objects.order_by('-id').first().id + max_id = site_model.objects.order_by("-id").first().id with connection.cursor() as cursor: cursor.execute("SELECT last_value from django_site_id_seq") (current_id,) = cursor.fetchone() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py index f9ae43a8..484f686a 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/adapters.py @@ -5,10 +5,11 @@ import typing from allauth.account.adapter import DefaultAccountAdapter from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from django.conf import settings -from django.http import HttpRequest if typing.TYPE_CHECKING: from allauth.socialaccount.models import SocialLogin + from django.http import HttpRequest + from {{cookiecutter.project_slug}}.users.models import User @@ -18,10 +19,19 @@ class AccountAdapter(DefaultAccountAdapter): class SocialAccountAdapter(DefaultSocialAccountAdapter): - def is_open_for_signup(self, request: HttpRequest, sociallogin: SocialLogin) -> bool: + def is_open_for_signup( + self, + request: HttpRequest, + sociallogin: SocialLogin, + ) -> bool: return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) - def populate_user(self, request: HttpRequest, sociallogin: SocialLogin, data: dict[str, typing.Any]) -> User: + def populate_user( + self, + request: HttpRequest, + sociallogin: SocialLogin, + data: dict[str, typing.Any], + ) -> User: """ Populates user information from social provider info. diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py index 7fd49fa9..70f82925 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py @@ -1,17 +1,17 @@ from django.conf import settings from django.contrib import admin from django.contrib.auth import admin as auth_admin -from django.contrib.auth import get_user_model, decorators +from django.contrib.auth.decorators import login_required from django.utils.translation import gettext_lazy as _ -from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm, UserAdminCreationForm - -User = get_user_model() +from .forms import UserAdminChangeForm +from .forms import UserAdminCreationForm +from .models import User if settings.DJANGO_ADMIN_FORCE_ALLAUTH: # Force the `admin` sign in process to go through the `django-allauth` workflow: # https://docs.allauth.org/en/latest/common/admin.html#admin - admin.site.login = decorators.login_required(admin.site.login) # type: ignore[method-assign] + admin.site.login = login_required(admin.site.login) # type: ignore[method-assign] @admin.register(User) 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 0872d06f..51e0859f 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/serializers.py @@ -1,13 +1,9 @@ -from django.contrib.auth import get_user_model from rest_framework import serializers -from {{ cookiecutter.project_slug }}.users.models import User as UserType +from {{ cookiecutter.project_slug }}.users.models import User -User = get_user_model() - - -class UserSerializer(serializers.ModelSerializer[UserType]): +class UserSerializer(serializers.ModelSerializer[User]): class Meta: model = User {%- if cookiecutter.username_type == "email" %} 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 508431e4..7a521cdf 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/api/views.py @@ -1,13 +1,14 @@ -from django.contrib.auth import get_user_model from rest_framework import status from rest_framework.decorators import action -from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin +from rest_framework.mixins import ListModelMixin +from rest_framework.mixins import RetrieveModelMixin +from rest_framework.mixins import UpdateModelMixin from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet -from .serializers import UserSerializer +from {{ cookiecutter.project_slug }}.users.models import User -User = get_user_model() +from .serializers import UserSerializer class UserViewSet(RetrieveModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet): diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py index 92e7a74e..5c3d4fe0 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py @@ -1,3 +1,5 @@ +import contextlib + from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ @@ -7,7 +9,5 @@ class UsersConfig(AppConfig): verbose_name = _("Users") def ready(self): - try: + with contextlib.suppress(ImportError): import {{ cookiecutter.project_slug }}.users.signals # noqa: F401 - except ImportError: - pass diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py index ac5b647d..830fca60 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py @@ -1,13 +1,12 @@ from allauth.account.forms import SignupForm from allauth.socialaccount.forms import SignupForm as SocialSignupForm from django.contrib.auth import forms as admin_forms -from django.contrib.auth import get_user_model {%- if cookiecutter.username_type == "email" %} from django.forms import EmailField {%- endif %} from django.utils.translation import gettext_lazy as _ -User = get_user_model() +from .models import User class UserAdminChangeForm(admin_forms.UserChangeForm): diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/managers.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/managers.py index 03ac2954..d8beaa48 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/managers.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/managers.py @@ -4,7 +4,7 @@ from django.contrib.auth.hashers import make_password from django.contrib.auth.models import UserManager as DjangoUserManager if TYPE_CHECKING: - from {{ cookiecutter.project_slug }}.users.models import User # noqa: F401 + from .models import User # noqa: F401 class UserManager(DjangoUserManager["User"]): @@ -15,7 +15,8 @@ class UserManager(DjangoUserManager["User"]): Create and save a user with the given email and password. """ if not email: - raise ValueError("The given email must be set") + msg = "The given email must be set" + raise ValueError(msg) email = self.normalize_email(email) user = self.model(email=email, **extra_fields) user.password = make_password(password) @@ -32,8 +33,10 @@ class UserManager(DjangoUserManager["User"]): extra_fields.setdefault("is_superuser", True) if extra_fields.get("is_staff") is not True: - raise ValueError("Superuser must have is_staff=True.") + msg = "Superuser must have is_staff=True." + raise ValueError(msg) if extra_fields.get("is_superuser") is not True: - raise ValueError("Superuser must have is_superuser=True.") + msg = "Superuser must have is_superuser=True." + raise ValueError(msg) return self._create_user(email, password, **extra_fields) diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py index 58a439c5..cee6676b 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py @@ -1,7 +1,8 @@ import django.contrib.auth.models import django.contrib.auth.validators -from django.db import migrations, models import django.utils.timezone +from django.db import migrations +from django.db import models import {{cookiecutter.project_slug}}.users.models @@ -31,7 +32,7 @@ class Migration(migrations.Migration): ( "last_login", models.DateTimeField( - blank=True, null=True, verbose_name="last login" + blank=True, null=True, verbose_name="last login", ), ), ( @@ -61,14 +62,14 @@ class Migration(migrations.Migration): ( "email", models.EmailField( - blank=True, max_length=254, verbose_name="email address" + blank=True, max_length=254, verbose_name="email address", ), ), {%- else %} ( "email", models.EmailField( - unique=True, max_length=254, verbose_name="email address" + unique=True, max_length=254, verbose_name="email address", ), ), {%- endif %} @@ -91,13 +92,13 @@ class Migration(migrations.Migration): ( "date_joined", models.DateTimeField( - default=django.utils.timezone.now, verbose_name="date joined" + default=django.utils.timezone.now, verbose_name="date joined", ), ), ( "name", models.CharField( - blank=True, max_length=255, verbose_name="Name of User" + blank=True, max_length=255, verbose_name="Name of User", ), ), ( diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py index ccb6b78a..4a870cc2 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py @@ -1,14 +1,17 @@ {%- if cookiecutter.username_type == "email" %} from typing import ClassVar -{%- endif %} +{% endif -%} from django.contrib.auth.models import AbstractUser -from django.db.models import CharField{% if cookiecutter.username_type == "email" %}, EmailField{% endif %} +from django.db.models import CharField +{%- if cookiecutter.username_type == "email" %} +from django.db.models import EmailField +{%- endif %} from django.urls import reverse from django.utils.translation import gettext_lazy as _ {%- if cookiecutter.username_type == "email" %} -from {{ cookiecutter.project_slug }}.users.managers import UserManager +from .managers import UserManager {%- endif %} @@ -21,11 +24,11 @@ class User(AbstractUser): # First and last name do not cover name patterns around the globe name = CharField(_("Name of User"), blank=True, max_length=255) - first_name = None # type: ignore - last_name = None # type: ignore + first_name = None # type: ignore[assignment] + last_name = None # type: ignore[assignment] {%- if cookiecutter.username_type == "email" %} email = EmailField(_("email address"), unique=True) - username = None # type: ignore + username = None # type: ignore[assignment] USERNAME_FIELD = "email" REQUIRED_FIELDS = [] diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tasks.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tasks.py index c99341c5..ca51cd74 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tasks.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tasks.py @@ -1,11 +1,9 @@ -from django.contrib.auth import get_user_model +from celery import shared_task -from config import celery_app - -User = get_user_model() +from .models import User -@celery_app.task() +@shared_task() def get_users_count(): """A pointless Celery task to demonstrate usage.""" return User.objects.count() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py index bebd8adc..136d0b1d 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/factories.py @@ -1,10 +1,12 @@ from collections.abc import Sequence from typing import Any -from django.contrib.auth import get_user_model -from factory import Faker, post_generation +from factory import Faker +from factory import post_generation from factory.django import DjangoModelFactory +from {{ cookiecutter.project_slug }}.users.models import User + class UserFactory(DjangoModelFactory): {%- if cookiecutter.username_type == "username" %} @@ -14,7 +16,7 @@ class UserFactory(DjangoModelFactory): name = Faker("name") @post_generation - def password(self, create: bool, extracted: Sequence[Any], **kwargs): + def password(self, create: bool, extracted: Sequence[Any], **kwargs): # noqa: FBT001 password = ( extracted if extracted @@ -37,5 +39,5 @@ class UserFactory(DjangoModelFactory): instance.save() class Meta: - model = get_user_model() + model = User django_get_or_create = ["{{cookiecutter.username_type}}"] diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py index 75917ab3..f802b8ba 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py @@ -1,3 +1,5 @@ +import contextlib +from http import HTTPStatus from importlib import reload import pytest @@ -13,17 +15,17 @@ class TestUserAdmin: def test_changelist(self, admin_client): url = reverse("admin:users_user_changelist") response = admin_client.get(url) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK def test_search(self, admin_client): url = reverse("admin:users_user_changelist") response = admin_client.get(url, data={"q": "test"}) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK def test_add(self, admin_client): url = reverse("admin:users_user_add") response = admin_client.get(url) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK response = admin_client.post( url, @@ -37,7 +39,7 @@ class TestUserAdmin: "password2": "My_R@ndom-P@ssw0rd", }, ) - assert response.status_code == 302 + assert response.status_code == HTTPStatus.FOUND {%- if cookiecutter.username_type == "email" %} assert User.objects.filter(email="new-admin@example.com").exists() {%- else %} @@ -52,21 +54,19 @@ class TestUserAdmin: {%- endif %} url = reverse("admin:users_user_change", kwargs={"object_id": user.pk}) response = admin_client.get(url) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK - @pytest.fixture - def force_allauth(self, settings): + @pytest.fixture() + def _force_allauth(self, settings): settings.DJANGO_ADMIN_FORCE_ALLAUTH = True # Reload the admin module to apply the setting change - import {{ cookiecutter.project_slug }}.users.admin as users_admin # pylint: disable=import-outside-toplevel + import {{ cookiecutter.project_slug }}.users.admin as users_admin - try: + with contextlib.suppress(admin.sites.AlreadyRegistered): reload(users_admin) - except admin.sites.AlreadyRegistered: - pass - @pytest.mark.django_db - @pytest.mark.usefixtures("force_allauth") + @pytest.mark.django_db() + @pytest.mark.usefixtures("_force_allauth") def test_allauth_login(self, rf, settings): request = rf.get("/fake-url") request.user = AnonymousUser() 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 334ab118..b445b611 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 @@ -1,14 +1,20 @@ -from django.urls import resolve, reverse +from django.urls import resolve +from django.urls import reverse 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_drf_views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_views.py index 90e84dc7..955ebe4e 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_drf_views.py @@ -6,7 +6,7 @@ from {{ cookiecutter.project_slug }}.users.models import User class TestUserViewSet: - @pytest.fixture + @pytest.fixture() def api_rf(self) -> APIRequestFactory: return APIRequestFactory() @@ -26,7 +26,7 @@ class TestUserViewSet: view.request = request - response = view.me(request) # type: ignore + response = view.me(request) # type: ignore[call-arg, arg-type, misc] assert response.data == { {%- if cookiecutter.username_type == "email" %} diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py index ca624c89..17d0d72a 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py @@ -30,7 +30,7 @@ class TestUserAdminCreationForm: {%- endif %} "password1": user.password, "password2": user.password, - } + }, ) assert not form.is_valid() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_managers.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_managers.py index f25af4ee..e5e5f5a4 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_managers.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_managers.py @@ -6,12 +6,12 @@ from django.core.management import call_command from {{ cookiecutter.project_slug }}.users.models import User -@pytest.mark.django_db +@pytest.mark.django_db() class TestUserManager: def test_create_user(self): user = User.objects.create_user( email="john@example.com", - password="something-r@nd0m!", + password="something-r@nd0m!", # noqa: S106 ) assert user.email == "john@example.com" assert not user.is_staff @@ -22,7 +22,7 @@ class TestUserManager: def test_create_superuser(self): user = User.objects.create_superuser( email="admin@example.com", - password="something-r@nd0m!", + password="something-r@nd0m!", # noqa: S106 ) assert user.email == "admin@example.com" assert user.is_staff @@ -32,12 +32,12 @@ class TestUserManager: def test_create_superuser_username_ignored(self): user = User.objects.create_superuser( email="test@example.com", - password="something-r@nd0m!", + password="something-r@nd0m!", # noqa: S106 ) assert user.username is None -@pytest.mark.django_db +@pytest.mark.django_db() def test_createsuperuser_command(): """Ensure createsuperuser command works with our custom manager.""" out = StringIO() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_swagger.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_swagger.py index f97658b5..3081d1f6 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_swagger.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_swagger.py @@ -1,3 +1,5 @@ +from http import HTTPStatus + import pytest from django.urls import reverse @@ -5,17 +7,17 @@ from django.urls import reverse def test_swagger_accessible_by_admin(admin_client): url = reverse("api-docs") response = admin_client.get(url) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK -@pytest.mark.django_db +@pytest.mark.django_db() def test_swagger_ui_not_accessible_by_normal_user(client): url = reverse("api-docs") response = client.get(url) - assert response.status_code == 403 + assert response.status_code == HTTPStatus.FORBIDDEN def test_api_schema_generated_successfully(admin_client): url = reverse("api-schema") response = admin_client.get(url) - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK 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 41d5af29..d3f61013 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 @@ -9,8 +9,9 @@ pytestmark = pytest.mark.django_db def test_user_count(settings): """A basic test to execute the get_users_count Celery task.""" - UserFactory.create_batch(3) + batch_size = 3 + UserFactory.create_batch(batch_size) settings.CELERY_TASK_ALWAYS_EAGER = True task_result = get_users_count.delay() assert isinstance(task_result, EagerResult) - assert task_result.result == 3 + assert task_result.result == batch_size 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 a0d06889..aaacb05a 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,4 +1,5 @@ -from django.urls import resolve, reverse +from django.urls import resolve +from django.urls import reverse from {{ cookiecutter.project_slug }}.users.models import User @@ -8,7 +9,10 @@ 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/tests/test_views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py index 2c102703..136daa40 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py @@ -1,10 +1,13 @@ +from http import HTTPStatus + import pytest from django.conf import settings from django.contrib import messages from django.contrib.auth.models import AnonymousUser from django.contrib.messages.middleware import MessageMiddleware from django.contrib.sessions.middleware import SessionMiddleware -from django.http import HttpRequest, HttpResponseRedirect +from django.http import HttpRequest +from django.http import HttpResponseRedirect from django.test import RequestFactory from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -12,11 +15,9 @@ from django.utils.translation import gettext_lazy as _ from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory -from {{ cookiecutter.project_slug }}.users.views import ( - UserRedirectView, - UserUpdateView, - user_detail_view, -) +from {{ cookiecutter.project_slug }}.users.views import UserRedirectView +from {{ cookiecutter.project_slug }}.users.views import UserUpdateView +from {{ cookiecutter.project_slug }}.users.views import user_detail_view pytestmark = pytest.mark.django_db @@ -102,7 +103,7 @@ class TestUserDetailView: response = user_detail_view(request, username=user.username) {%- endif %} - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK def test_not_authenticated(self, user: User, rf: RequestFactory): request = rf.get("/fake-url/") @@ -116,5 +117,5 @@ class TestUserDetailView: login_url = reverse(settings.LOGIN_URL) assert isinstance(response, HttpResponseRedirect) - assert response.status_code == 302 + assert response.status_code == HTTPStatus.FOUND assert response.url == f"{login_url}?next=/fake-url/" diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py index 0ffca17a..74d65da1 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/urls.py @@ -1,10 +1,8 @@ from django.urls import path -from {{ cookiecutter.project_slug }}.users.views import ( - user_detail_view, - user_redirect_view, - user_update_view, -) +from .views import user_detail_view +from .views import user_redirect_view +from .views import user_update_view app_name = "users" urlpatterns = [ diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py index 82498e63..3f20f268 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/views.py @@ -1,11 +1,12 @@ -from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from django.views.generic import DetailView, RedirectView, UpdateView +from django.views.generic import DetailView +from django.views.generic import RedirectView +from django.views.generic import UpdateView -User = get_user_model() +from {{ cookiecutter.project_slug }}.users.models import User class UserDetailView(LoginRequiredMixin, DetailView): @@ -28,7 +29,8 @@ 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 + # for mypy to know that the user is authenticated + assert self.request.user.is_authenticated return self.request.user.get_absolute_url() def get_object(self):