diff --git a/cookiecutter.json b/cookiecutter.json
index 4b483bc..cb9d194 100644
--- a/cookiecutter.json
+++ b/cookiecutter.json
@@ -2,6 +2,7 @@
"project_name": "My Awesome Project",
"project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}",
"python_version": "3.11.1",
+ "python_version_slug": "py{{ cookiecutter.python_version.split('.')[:2] | join('') }}",
"_copy_without_render": [
"*.html",
"*/activation*.txt"
diff --git a/{{cookiecutter.project_slug}}/.github/workflows/test.yaml b/{{cookiecutter.project_slug}}/.github/workflows/test.yaml
index c4ada04..9632a20 100644
--- a/{{cookiecutter.project_slug}}/.github/workflows/test.yaml
+++ b/{{cookiecutter.project_slug}}/.github/workflows/test.yaml
@@ -24,6 +24,8 @@ jobs:
run: |
pip install pip-tools
pip-sync requirements.txt requirements-dev.txt
+ - name: Ruff
+ run: ruff --format=github .
- name: Test
run: pytest --cov=. --cov-branch --cov-report term-missing:skip-covered
working-directory: ./src/
diff --git a/{{cookiecutter.project_slug}}/.idea/watcherTasks.xml b/{{cookiecutter.project_slug}}/.idea/watcherTasks.xml
index 98b3257..e78ac61 100644
--- a/{{cookiecutter.project_slug}}/.idea/watcherTasks.xml
+++ b/{{cookiecutter.project_slug}}/.idea/watcherTasks.xml
@@ -21,7 +21,7 @@
-
+
@@ -41,5 +41,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml
index 69b3773..c6540bb 100644
--- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml
+++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml
@@ -27,40 +27,26 @@ repos:
rev: v3.3.1
hooks:
- id: pyupgrade
- args: [--py311-plus]
+ args: [--{{ cookiecutter.python_version_slug }}-plus]
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.12.0
hooks:
- id: django-upgrade
args: [--target-version, "4.1"]
- - repo: https://github.com/PyCQA/isort
- rev: 5.11.4
- hooks:
- - id: isort
- args: [--profile, black]
- repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- id: black
- args: [--target-version, py311]
+ args: [--target-version, {{ cookiecutter.python_version_slug }}]
- repo: https://github.com/rtts/djhtml
rev: v1.5.2
hooks:
- id: djhtml
- - repo: https://github.com/flakeheaven/flakeheaven
- rev: 3.2.1
+ - repo: https://github.com/charliermarsh/ruff-pre-commit
+ rev: 'v0.0.237'
hooks:
- - id: flakeheaven
- additional_dependencies:
- - flake8-annotations-complexity
- - flake8-bandit
- - flake8-builtins
- - flake8-bugbear
- - flake8-comprehensions
- - flake8-docstrings
- - flake8-eradicate
- - flake8-noqa
- - pep8-naming
+ - id: ruff
+ args: [--fix]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0-alpha.4
hooks:
diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml
index 805d1dd..d6a2075 100644
--- a/{{cookiecutter.project_slug}}/pyproject.toml
+++ b/{{cookiecutter.project_slug}}/pyproject.toml
@@ -4,7 +4,7 @@
[tool.pytest.ini_options]
addopts = """
--html=test_reports/pytest_result/pytest.html --color=yes --durations 20
---no-cov-on-fail --strict-markers
+--no-cov-on-fail --strict-markers --reuse-db
-W error
"""
markers = []
@@ -27,41 +27,77 @@ python_files = [
]
###############################################################################
-# flake8 / flakeheaven
+# ruff
###############################################################################
-[tool.flakeheaven]
-max_complexity = 10
-format = "grouped"
+[tool.ruff]
+src = ["src"]
+target-version = "{{ cookiecutter.python_version_slug }}"
+select = [
+ "F", # pyflakes
+ "E", "W", # pycodestyle
+ "C90", # mccabe
+ "I", # isort
+ "N", # pep8-naming
+ "D", # pydocstyle
+ "S", # flake8-bandit
+ "FBT", # flake8-boolean-trap
+ "B", # flake8-bugbear
+ "A", # flake8-builtins
+ "C4", # flake8-comprehensions
+ "DTZ", # flake8-datetimez
+ "T10", # flake8-debugger
+ "EXE", # flake8-executable
+ "ISC", # flake8-implicit-str-concat
+ "ICN", # flake8-import-conventions
+ "G", # flake8-logging-format
+ "INP", # flake8-no-pep420
+ "PIE", # flake8-pie
+ "T20", # flake8-print
+ "PT", # flake8-pytest-style
+ "RET", # flake8-return
+ "SIM", # flake8-simplify
+ "TID", # flake8-tidy-imports
+ "ARG", # flake8-unused-arguments
+ "PTH", # flake8-use-pathlib
+ "ERA", # eradicate
+ "PD", # pandas-vet
+ "PGH", # pygrep-hooks
+ "PL", # pylint
+ "TRY", # tryceratops
+ "RUF", # ruff-specific rules
+]
+unfixable = ["T20", "RUF001", "RUF002", "RUF003"]
-# Base rules
-#############################
-[tool.flakeheaven.plugins]
-"*" = [
- "+*",
- "-E501", # long lines
- "-E203", # conflict with black on PEP8 interpretation
- "-W503", # deprecated rule: https://www.flake8rules.com/rules/W503.html
-]
-flake8-builtins = [
- "+*",
- "-A003", # class attribute is shadowing a python builtin
-]
-flake8-docstrings = [
- "+*",
- "-D1??", # missing docstring
-]
-flake8-bandit = [
- "+*",
- "-S308", # Use of mark_safe() may expose cross-site scripting vulnerabilities and should be reviewed.
- "-S703", # Potential XSS on mark_safe function.
+ignore = [
+ "UP", # pyupgrade
+ "YTT", # flake8-2020
+ "ANN", # flake8-annotations
+ "BLE", # flake8-blind-except
+ "COM", # flake8-commas
+ "EM", # flake8-errmsg
+ "Q", # flake8-quotes
+ "TCH", # flake8-type-checking / TODO: revisit later ?
+
+ "E501", # long lines
+ "D1", # missing docstring
+ "TRY003", # Avoid specifying long messages outside the exception class
]
-# Exceptions
-#############################
-[tool.flakeheaven.exceptions."**/tests/*"]
-flake8-bandit = [
- "+*",
- "-S101", # Use of assert detected.
- "-S106", # Possible hardcoded password.
- "-S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes.
+[tool.ruff.per-file-ignores]
+"**/tests/*" = [
+ "S101", # Use of assert detected.
+ "S106", # Possible hardcoded password.
+ "B011", # Do not call assert False since python -O removes these calls.
+ "ARG001", # Unused function argument (mostly fixtures)
+ "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
]
+# File {name} is part of an implicit namespace package. Add an `__init__.py`.
+"tasks.py" = ["INP001"]
+"src/conftest.py" = ["INP001"]
+"src/manage.py" = ["INP001"]
+
+[tool.ruff.pydocstyle]
+convention = "pep257"
+
+[tool.ruff.mccabe]
+max-complexity = 10
diff --git a/{{cookiecutter.project_slug}}/requirements-dev.in b/{{cookiecutter.project_slug}}/requirements-dev.in
index c956029..25b2817 100644
--- a/{{cookiecutter.project_slug}}/requirements-dev.in
+++ b/{{cookiecutter.project_slug}}/requirements-dev.in
@@ -12,3 +12,6 @@ bpython>=0.22.1
invoke>=1.7.3
hypothesis>=6.56.4
django-browser-reload>=1.6.0
+black>=22.12.0
+pip-tools>=6.0
+ruff>=0.0.237
diff --git a/{{cookiecutter.project_slug}}/src/common/models.py b/{{cookiecutter.project_slug}}/src/common/models.py
index 8b6aec3..2bd30dd 100644
--- a/{{cookiecutter.project_slug}}/src/common/models.py
+++ b/{{cookiecutter.project_slug}}/src/common/models.py
@@ -3,5 +3,3 @@ from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
"""Default custom user model for {{ cookiecutter.project_name }}."""
-
- pass
diff --git a/{{cookiecutter.project_slug}}/src/common/tests/test_admin.py b/{{cookiecutter.project_slug}}/src/common/tests/test_admin.py
index ff9b60e..83942ba 100644
--- a/{{cookiecutter.project_slug}}/src/common/tests/test_admin.py
+++ b/{{cookiecutter.project_slug}}/src/common/tests/test_admin.py
@@ -1,3 +1,5 @@
+from http import HTTPStatus
+
import pytest
from django.contrib.auth import get_user_model
from django.urls import reverse
@@ -9,17 +11,17 @@ class TestUserAdmin:
def test_changelist(self, admin_client):
url = reverse("admin:common_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:common_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:common_user_add")
response = admin_client.get(url)
- assert response.status_code == 200
+ assert response.status_code == HTTPStatus.OK
response = admin_client.post(
url,
@@ -29,11 +31,11 @@ class TestUserAdmin:
"password2": "My_R@ndom-P@ssw0rd",
},
)
- assert response.status_code == 302
+ assert response.status_code == HTTPStatus.FOUND
assert get_user_model().objects.filter(username="test").exists()
def test_view_user(self, admin_client):
user = get_user_model().objects.get(username="admin")
url = reverse("admin:common_user_change", kwargs={"object_id": user.pk})
response = admin_client.get(url)
- assert response.status_code == 200
+ assert response.status_code == HTTPStatus.OK
diff --git a/{{cookiecutter.project_slug}}/src/common/views.py b/{{cookiecutter.project_slug}}/src/common/views.py
index 4dd684e..0dc09f4 100644
--- a/{{cookiecutter.project_slug}}/src/common/views.py
+++ b/{{cookiecutter.project_slug}}/src/common/views.py
@@ -6,7 +6,7 @@ from django.shortcuts import render
def hello_world(request: WSGIRequest) -> HttpResponse:
- context = {"value": random.randint(1, 1000)} # noqa: S311
+ context = {"value": random.randint(1, 1000)}
if request.htmx:
return render(request, "common/hello-random.html", context)
return render(request, "common/base.html", context)
diff --git a/{{cookiecutter.project_slug}}/src/conftest.py b/{{cookiecutter.project_slug}}/src/conftest.py
index bddc4e9..e020401 100644
--- a/{{cookiecutter.project_slug}}/src/conftest.py
+++ b/{{cookiecutter.project_slug}}/src/conftest.py
@@ -3,5 +3,5 @@ from django.core.management import call_command
@pytest.fixture(scope="session", autouse=True)
-def collectstatic():
+def _collectstatic():
call_command("collectstatic", "--clear", "--noinput", "--verbosity=0")
diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/context_processors.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/context_processors.py
index 18ab923..956ba17 100644
--- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/context_processors.py
+++ b/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/context_processors.py
@@ -1,5 +1,5 @@
from django.conf import settings
-def app(request):
+def app(_):
return settings.APP
diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware.py
index 873ca33..269825d 100644
--- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware.py
+++ b/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/middleware.py
@@ -1,7 +1,7 @@
from django.conf import settings
-def debug_toolbar_bypass_internal_ips(request) -> bool:
+def debug_toolbar_bypass_internal_ips(_) -> bool:
"""
Display debug toolbar according to the DEBUG_TOOLBAR setting only.
diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/settings.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/settings.py
index 6898f3c..89e667e 100644
--- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/settings.py
+++ b/{{cookiecutter.project_slug}}/src/{{cookiecutter.project_slug}}/settings.py
@@ -236,17 +236,17 @@ APP = {
}
}
try:
- with open("/app/git/build-date") as f:
+ with Path("/app/git/build-date").open() as f:
APP["build"]["date"] = f.read().strip()
-except Exception: # noqa: S110
- pass
+except Exception:
+ pass # noqa: S110
try:
- with open("/app/git/git-commit") as f:
+ with Path("/app/git/git-commit").open() as f:
APP["build"]["commit"] = f.read().strip()
-except Exception: # noqa: S110
- pass
+except Exception:
+ pass # noqa: S110
try:
- with open("/app/git/git-describe") as f:
+ with Path("/app/git/git-describe").open() as f:
APP["build"]["describe"] = f.read().strip()
-except Exception: # noqa: S110
- pass
+except Exception:
+ pass # noqa: S110
diff --git a/{{cookiecutter.project_slug}}/tasks.py b/{{cookiecutter.project_slug}}/tasks.py
index 3371e45..6d2129e 100644
--- a/{{cookiecutter.project_slug}}/tasks.py
+++ b/{{cookiecutter.project_slug}}/tasks.py
@@ -10,7 +10,7 @@ TEST_ENV = {"ENV_FILE": BASE_DIR / "envs" / "test-envs.env"}
@task
-def sync_dependencies(ctx: Context, update: bool = False):
+def sync_dependencies(ctx: Context, *, update: bool = False):
common_args = "-q --allow-unsafe --resolver=backtracking"
if update:
common_args += " --upgrade"