Introduce ruff to cookiecutter template

This commit is contained in:
Gabriel Augendre 2023-01-29 13:54:51 +01:00
parent 5fee1369e8
commit a5dee60e98
14 changed files with 124 additions and 76 deletions

View file

@ -2,6 +2,7 @@
"project_name": "My Awesome Project", "project_name": "My Awesome Project",
"project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}", "project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}",
"python_version": "3.11.1", "python_version": "3.11.1",
"python_version_slug": "py{{ cookiecutter.python_version.split('.')[:2] | join('') }}",
"_copy_without_render": [ "_copy_without_render": [
"*.html", "*.html",
"*/activation*.txt" "*/activation*.txt"

View file

@ -24,6 +24,8 @@ jobs:
run: | run: |
pip install pip-tools pip install pip-tools
pip-sync requirements.txt requirements-dev.txt pip-sync requirements.txt requirements-dev.txt
- name: Ruff
run: ruff --format=github .
- name: Test - name: Test
run: pytest --cov=. --cov-branch --cov-report term-missing:skip-covered run: pytest --cov=. --cov-branch --cov-report term-missing:skip-covered
working-directory: ./src/ working-directory: ./src/

View file

@ -21,7 +21,7 @@
<option name="workingDir" value="" /> <option name="workingDir" value="" />
<envs /> <envs />
</TaskOptions> </TaskOptions>
<TaskOptions isEnabled="true"> <TaskOptions isEnabled="false">
<option name="arguments" value="run flakeheaven --file $FilePath$" /> <option name="arguments" value="run flakeheaven --file $FilePath$" />
<option name="checkSyntaxErrors" value="true" /> <option name="checkSyntaxErrors" value="true" />
<option name="description" /> <option name="description" />
@ -41,5 +41,25 @@
<option name="workingDir" value="" /> <option name="workingDir" value="" />
<envs /> <envs />
</TaskOptions> </TaskOptions>
<TaskOptions isEnabled="true">
<option name="arguments" value="run ruff --file $FilePath$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="py" />
<option name="immediateSync" value="false" />
<option name="name" value="ruff" />
<option name="output" value="$FilePath$" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="$USER_HOME$/.local/pipx/venvs/pre-commit/bin/pre-commit" />
<option name="runOnExternalChanges" value="false" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="false" />
<option name="workingDir" value="" />
<envs />
</TaskOptions>
</component> </component>
</project> </project>

View file

@ -27,40 +27,26 @@ repos:
rev: v3.3.1 rev: v3.3.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py311-plus] args: [--{{ cookiecutter.python_version_slug }}-plus]
- repo: https://github.com/adamchainz/django-upgrade - repo: https://github.com/adamchainz/django-upgrade
rev: 1.12.0 rev: 1.12.0
hooks: hooks:
- id: django-upgrade - id: django-upgrade
args: [--target-version, "4.1"] 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 - repo: https://github.com/psf/black
rev: 22.12.0 rev: 22.12.0
hooks: hooks:
- id: black - id: black
args: [--target-version, py311] args: [--target-version, {{ cookiecutter.python_version_slug }}]
- repo: https://github.com/rtts/djhtml - repo: https://github.com/rtts/djhtml
rev: v1.5.2 rev: v1.5.2
hooks: hooks:
- id: djhtml - id: djhtml
- repo: https://github.com/flakeheaven/flakeheaven - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 3.2.1 rev: 'v0.0.237'
hooks: hooks:
- id: flakeheaven - id: ruff
additional_dependencies: args: [--fix]
- flake8-annotations-complexity
- flake8-bandit
- flake8-builtins
- flake8-bugbear
- flake8-comprehensions
- flake8-docstrings
- flake8-eradicate
- flake8-noqa
- pep8-naming
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0-alpha.4 rev: v3.0.0-alpha.4
hooks: hooks:

View file

@ -4,7 +4,7 @@
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = """ addopts = """
--html=test_reports/pytest_result/pytest.html --color=yes --durations 20 --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 -W error
""" """
markers = [] markers = []
@ -27,41 +27,77 @@ python_files = [
] ]
############################################################################### ###############################################################################
# flake8 / flakeheaven # ruff
############################################################################### ###############################################################################
[tool.flakeheaven] [tool.ruff]
max_complexity = 10 src = ["src"]
format = "grouped" 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 ignore = [
############################# "UP", # pyupgrade
[tool.flakeheaven.plugins] "YTT", # flake8-2020
"*" = [ "ANN", # flake8-annotations
"+*", "BLE", # flake8-blind-except
"-E501", # long lines "COM", # flake8-commas
"-E203", # conflict with black on PEP8 interpretation "EM", # flake8-errmsg
"-W503", # deprecated rule: https://www.flake8rules.com/rules/W503.html "Q", # flake8-quotes
] "TCH", # flake8-type-checking / TODO: revisit later ?
flake8-builtins = [
"+*", "E501", # long lines
"-A003", # class attribute is shadowing a python builtin "D1", # missing docstring
] "TRY003", # Avoid specifying long messages outside the exception class
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.
] ]
# Exceptions [tool.ruff.per-file-ignores]
############################# "**/tests/*" = [
[tool.flakeheaven.exceptions."**/tests/*"] "S101", # Use of assert detected.
flake8-bandit = [ "S106", # Possible hardcoded password.
"+*", "B011", # Do not call assert False since python -O removes these calls.
"-S101", # Use of assert detected. "ARG001", # Unused function argument (mostly fixtures)
"-S106", # Possible hardcoded password. "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
"-S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes.
] ]
# 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

View file

@ -12,3 +12,6 @@ bpython>=0.22.1
invoke>=1.7.3 invoke>=1.7.3
hypothesis>=6.56.4 hypothesis>=6.56.4
django-browser-reload>=1.6.0 django-browser-reload>=1.6.0
black>=22.12.0
pip-tools>=6.0
ruff>=0.0.237

View file

@ -3,5 +3,3 @@ from django.contrib.auth.models import AbstractUser
class User(AbstractUser): class User(AbstractUser):
"""Default custom user model for {{ cookiecutter.project_name }}.""" """Default custom user model for {{ cookiecutter.project_name }}."""
pass

View file

@ -1,3 +1,5 @@
from http import HTTPStatus
import pytest import pytest
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.urls import reverse from django.urls import reverse
@ -9,17 +11,17 @@ class TestUserAdmin:
def test_changelist(self, admin_client): def test_changelist(self, admin_client):
url = reverse("admin:common_user_changelist") url = reverse("admin:common_user_changelist")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK
def test_search(self, admin_client): def test_search(self, admin_client):
url = reverse("admin:common_user_changelist") url = reverse("admin:common_user_changelist")
response = admin_client.get(url, data={"q": "test"}) 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): def test_add(self, admin_client):
url = reverse("admin:common_user_add") url = reverse("admin:common_user_add")
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK
response = admin_client.post( response = admin_client.post(
url, url,
@ -29,11 +31,11 @@ class TestUserAdmin:
"password2": "My_R@ndom-P@ssw0rd", "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() assert get_user_model().objects.filter(username="test").exists()
def test_view_user(self, admin_client): def test_view_user(self, admin_client):
user = get_user_model().objects.get(username="admin") user = get_user_model().objects.get(username="admin")
url = reverse("admin:common_user_change", kwargs={"object_id": user.pk}) url = reverse("admin:common_user_change", kwargs={"object_id": user.pk})
response = admin_client.get(url) response = admin_client.get(url)
assert response.status_code == 200 assert response.status_code == HTTPStatus.OK

View file

@ -6,7 +6,7 @@ from django.shortcuts import render
def hello_world(request: WSGIRequest) -> HttpResponse: def hello_world(request: WSGIRequest) -> HttpResponse:
context = {"value": random.randint(1, 1000)} # noqa: S311 context = {"value": random.randint(1, 1000)}
if request.htmx: if request.htmx:
return render(request, "common/hello-random.html", context) return render(request, "common/hello-random.html", context)
return render(request, "common/base.html", context) return render(request, "common/base.html", context)

View file

@ -3,5 +3,5 @@ from django.core.management import call_command
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
def collectstatic(): def _collectstatic():
call_command("collectstatic", "--clear", "--noinput", "--verbosity=0") call_command("collectstatic", "--clear", "--noinput", "--verbosity=0")

View file

@ -1,5 +1,5 @@
from django.conf import settings from django.conf import settings
def app(request): def app(_):
return settings.APP return settings.APP

View file

@ -1,7 +1,7 @@
from django.conf import settings 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. Display debug toolbar according to the DEBUG_TOOLBAR setting only.

View file

@ -236,17 +236,17 @@ APP = {
} }
} }
try: try:
with open("/app/git/build-date") as f: with Path("/app/git/build-date").open() as f:
APP["build"]["date"] = f.read().strip() APP["build"]["date"] = f.read().strip()
except Exception: # noqa: S110 except Exception:
pass pass # noqa: S110
try: try:
with open("/app/git/git-commit") as f: with Path("/app/git/git-commit").open() as f:
APP["build"]["commit"] = f.read().strip() APP["build"]["commit"] = f.read().strip()
except Exception: # noqa: S110 except Exception:
pass pass # noqa: S110
try: try:
with open("/app/git/git-describe") as f: with Path("/app/git/git-describe").open() as f:
APP["build"]["describe"] = f.read().strip() APP["build"]["describe"] = f.read().strip()
except Exception: # noqa: S110 except Exception:
pass pass # noqa: S110

View file

@ -10,7 +10,7 @@ TEST_ENV = {"ENV_FILE": BASE_DIR / "envs" / "test-envs.env"}
@task @task
def sync_dependencies(ctx: Context, update: bool = False): def sync_dependencies(ctx: Context, *, update: bool = False):
common_args = "-q --allow-unsafe --resolver=backtracking" common_args = "-q --allow-unsafe --resolver=backtracking"
if update: if update:
common_args += " --upgrade" common_args += " --upgrade"