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.
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):
|