This commit is contained in:
Bruno Alla 2025-03-20 14:50:10 +01:00 committed by GitHub
commit 5c9327c955
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 119 additions and 80 deletions

View File

@ -1,4 +0,0 @@
[flake8]
exclude = docs
max-line-length = 119
extend-ignore = E203

View File

@ -26,27 +26,12 @@ repos:
- id: prettier - id: prettier
args: ["--tab-width", "2"] args: ["--tab-width", "2"]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v3.19.1 rev: v0.11.0
hooks: hooks:
- id: pyupgrade - id: ruff
args: [--py312-plus] args: [--fix, --exit-non-zero-on-fix]
exclude: hooks/ - id: ruff-format
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 7.1.1
hooks:
- id: flake8
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.5.0" rev: "v2.5.0"

0
hooks/__init__.py Normal file
View File

View File

@ -1,3 +1,4 @@
# ruff: noqa: PLR0133
import json import json
import random import random
import shutil import shutil
@ -79,7 +80,7 @@ def remove_heroku_files():
file_names = ["Procfile", "requirements.txt"] file_names = ["Procfile", "requirements.txt"]
for file_name in file_names: for file_name in file_names:
if file_name == "requirements.txt" and "{{ cookiecutter.ci_tool }}".lower() == "travis": if file_name == "requirements.txt" and "{{ cookiecutter.ci_tool }}".lower() == "travis":
# don't remove the file if we are using travisci but not using heroku # Don't remove the file if we are using Travis CI but not using Heroku
continue continue
Path(file_name).unlink() Path(file_name).unlink()
shutil.rmtree("bin") shutil.rmtree("bin")
@ -179,7 +180,7 @@ def handle_js_runner(choice, use_docker, use_async):
"dev": "concurrently npm:dev:*", "dev": "concurrently npm:dev:*",
"dev:webpack": "webpack serve --config webpack/dev.config.js", "dev:webpack": "webpack serve --config webpack/dev.config.js",
"dev:django": dev_django_cmd, "dev:django": dev_django_cmd,
} },
) )
else: else:
remove_dev_deps.append("concurrently") remove_dev_deps.append("concurrently")
@ -239,7 +240,7 @@ def remove_dotdrone_file():
Path(".drone.yml").unlink() Path(".drone.yml").unlink()
def generate_random_string(length, using_digits=False, using_ascii_letters=False, using_punctuation=False): def generate_random_string(length, using_digits=False, using_ascii_letters=False, using_punctuation=False): # noqa: FBT002
""" """
Example: Example:
opting out for 50 symbol-long, [a-z][A-Z][0-9] string opting out for 50 symbol-long, [a-z][A-Z][0-9] string
@ -268,7 +269,7 @@ def set_flag(file_path: Path, flag, value=None, formatted=None, *args, **kwargs)
if random_string is None: if random_string is None:
print( print(
"We couldn't find a secure pseudo-random number generator on your " "We couldn't find a secure pseudo-random number generator on your "
"system. Please, make sure to manually {} later.".format(flag) f"system. Please, make sure to manually {flag} later.",
) )
random_string = flag random_string = flag
if formatted is not None: if formatted is not None:
@ -285,18 +286,17 @@ def set_flag(file_path: Path, flag, value=None, formatted=None, *args, **kwargs)
def set_django_secret_key(file_path: Path): def set_django_secret_key(file_path: Path):
django_secret_key = set_flag( return set_flag(
file_path, file_path,
"!!!SET DJANGO_SECRET_KEY!!!", "!!!SET DJANGO_SECRET_KEY!!!",
length=64, length=64,
using_digits=True, using_digits=True,
using_ascii_letters=True, using_ascii_letters=True,
) )
return django_secret_key
def set_django_admin_url(file_path: Path): def set_django_admin_url(file_path: Path):
django_admin_url = set_flag( return set_flag(
file_path, file_path,
"!!!SET DJANGO_ADMIN_URL!!!", "!!!SET DJANGO_ADMIN_URL!!!",
formatted="{}/", formatted="{}/",
@ -304,24 +304,22 @@ def set_django_admin_url(file_path: Path):
using_digits=True, using_digits=True,
using_ascii_letters=True, using_ascii_letters=True,
) )
return django_admin_url
def generate_random_user(): def generate_random_user():
return generate_random_string(length=32, using_ascii_letters=True) return generate_random_string(length=32, using_ascii_letters=True)
def generate_postgres_user(debug=False): def generate_postgres_user(debug=False): # noqa: FBT002
return DEBUG_VALUE if debug else generate_random_user() return DEBUG_VALUE if debug else generate_random_user()
def set_postgres_user(file_path, value): def set_postgres_user(file_path, value):
postgres_user = set_flag(file_path, "!!!SET POSTGRES_USER!!!", value=value) return set_flag(file_path, "!!!SET POSTGRES_USER!!!", value=value)
return postgres_user
def set_postgres_password(file_path, value=None): def set_postgres_password(file_path, value=None):
postgres_password = set_flag( return set_flag(
file_path, file_path,
"!!!SET POSTGRES_PASSWORD!!!", "!!!SET POSTGRES_PASSWORD!!!",
value=value, value=value,
@ -329,16 +327,14 @@ def set_postgres_password(file_path, value=None):
using_digits=True, using_digits=True,
using_ascii_letters=True, using_ascii_letters=True,
) )
return postgres_password
def set_celery_flower_user(file_path, value): def set_celery_flower_user(file_path, value):
celery_flower_user = set_flag(file_path, "!!!SET CELERY_FLOWER_USER!!!", value=value) return set_flag(file_path, "!!!SET CELERY_FLOWER_USER!!!", value=value)
return celery_flower_user
def set_celery_flower_password(file_path, value=None): def set_celery_flower_password(file_path, value=None):
celery_flower_password = set_flag( return set_flag(
file_path, file_path,
"!!!SET CELERY_FLOWER_PASSWORD!!!", "!!!SET CELERY_FLOWER_PASSWORD!!!",
value=value, value=value,
@ -346,7 +342,6 @@ def set_celery_flower_password(file_path, value=None):
using_digits=True, using_digits=True,
using_ascii_letters=True, using_ascii_letters=True,
) )
return celery_flower_password
def append_to_gitignore_file(ignored_line): def append_to_gitignore_file(ignored_line):
@ -355,7 +350,7 @@ def append_to_gitignore_file(ignored_line):
gitignore_file.write("\n") gitignore_file.write("\n")
def set_flags_in_envs(postgres_user, celery_flower_user, debug=False): def set_flags_in_envs(postgres_user, celery_flower_user, debug=False): # noqa: FBT002
local_django_envs_path = Path(".envs", ".local", ".django") local_django_envs_path = Path(".envs", ".local", ".django")
production_django_envs_path = Path(".envs", ".production", ".django") production_django_envs_path = Path(".envs", ".production", ".django")
local_postgres_envs_path = Path(".envs", ".local", ".postgres") local_postgres_envs_path = Path(".envs", ".local", ".postgres")
@ -405,7 +400,7 @@ def remove_drf_starter_files():
shutil.rmtree(Path("{{cookiecutter.project_slug}}", "users", "tests", "api")) shutil.rmtree(Path("{{cookiecutter.project_slug}}", "users", "tests", "api"))
def main(): def main(): # noqa: C901, PLR0912, PLR0915
debug = "{{ cookiecutter.debug }}".lower() == "y" debug = "{{ cookiecutter.debug }}".lower() == "y"
set_flags_in_envs( set_flags_in_envs(
@ -444,7 +439,7 @@ def main():
print( print(
INFO + ".env(s) are only utilized when Docker Compose and/or " INFO + ".env(s) are only utilized when Docker Compose and/or "
"Heroku support is enabled so keeping them does not make sense " "Heroku support is enabled so keeping them does not make sense "
"given your current setup." + TERMINATOR "given your current setup." + TERMINATOR,
) )
remove_envs_and_associated_files() remove_envs_and_associated_files()
else: else:
@ -471,7 +466,7 @@ def main():
if "{{ cookiecutter.cloud_provider }}" == "None" and "{{ cookiecutter.use_docker }}".lower() == "n": if "{{ cookiecutter.cloud_provider }}" == "None" and "{{ cookiecutter.use_docker }}".lower() == "n":
print( print(
WARNING + "You chose to not use any cloud providers nor Docker, " WARNING + "You chose to not use any cloud providers nor Docker, "
"media files won't be served in production." + TERMINATOR "media files won't be served in production." + TERMINATOR,
) )
if "{{ cookiecutter.use_celery }}".lower() == "n": if "{{ cookiecutter.use_celery }}".lower() == "n":

View File

@ -1,3 +1,4 @@
# ruff: noqa: PLR0133
import sys import sys
TERMINATOR = "\x1b[0m" TERMINATOR = "\x1b[0m"
@ -16,9 +17,9 @@ SUCCESS = "\x1b[1;32m [SUCCESS]: "
project_slug = "{{ cookiecutter.project_slug }}" project_slug = "{{ cookiecutter.project_slug }}"
if hasattr(project_slug, "isidentifier"): if hasattr(project_slug, "isidentifier"):
assert project_slug.isidentifier(), "'{}' project slug is not a valid Python identifier.".format(project_slug) assert project_slug.isidentifier(), f"'{project_slug}' project slug is not a valid Python identifier."
assert project_slug == project_slug.lower(), "'{}' project slug should be all lowercase".format(project_slug) assert project_slug == project_slug.lower(), f"'{project_slug}' project slug should be all lowercase"
assert "\\" not in "{{ cookiecutter.author_name }}", "Don't include backslashes in author name." assert "\\" not in "{{ cookiecutter.author_name }}", "Don't include backslashes in author name."

View File

@ -58,22 +58,83 @@ docs = [
"sphinx-rtd-theme>=3", "sphinx-rtd-theme>=3",
] ]
[tool.black] [tool.ruff]
target-version = "py39"
line-length = 119 line-length = 119
target-version = [ # Exclude the template content as most files aren't parseable
'py312', extend-exclude = [
"[{]{2}cookiecutter.project_slug[}]{2}/*",
"docs/*",
] ]
# ==== isort ==== lint.select = [
"A",
[tool.isort] # "ANN", # flake8-annotations: we should support this in the future but many errors atm
profile = "black" "ASYNC",
line_length = 119 "B",
known_first_party = [ "BLE",
"tests", "C4",
"scripts", "C90",
"hooks", "COM",
"DTZ",
"E",
"EM",
"ERA",
"EXE",
"F",
"FA",
"FBT",
"FLY",
"G",
"I",
"ICN",
"INP",
"INT",
"ISC",
"N",
"PD",
"PERF",
"PGH",
"PIE",
"PL",
"PT",
# "ARG", # Unused function argument
"PTH",
"PYI",
"Q",
"RET",
"RSE",
# "FURB",
# "LOG",
"RUF",
"S",
"SIM",
"SLF",
"SLOT",
"T10",
"TC",
"TID",
"TRY",
"UP",
"W",
"YTT",
] ]
lint.ignore = [
"EM101",
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
"S101", # Use of assert detected https://docs.astral.sh/ruff/rules/assert/
"SIM102", # sometimes it's better to nest
"TRY003", # Avoid specifying long messages outside the exception class
# Checks for uses of isinstance/issubclass that take a tuple of types for comparison.
# Deactivated because it can make the code slow: https://github.com/astral-sh/ruff/issues/7871
"UP038",
]
# The fixes in extend-unsafe-fixes will require
# provide the `--unsafe-fixes` flag when fixing.
lint.extend-unsafe-fixes = [
"UP038",
]
lint.isort.force-single-line = true
[tool.pyproject-fmt] [tool.pyproject-fmt]
keep_full_version = true keep_full_version = true

View File

@ -13,7 +13,9 @@ import os
import re import re
import sys import sys
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, NamedTuple from typing import TYPE_CHECKING
from typing import Any
from typing import NamedTuple
import requests import requests
from github import Github from github import Github
@ -59,7 +61,7 @@ class DjVersion(NamedTuple):
def get_package_info(package: str) -> dict: def get_package_info(package: str) -> dict:
"""Get package metadata using PyPI API.""" """Get package metadata using PyPI API."""
# "django" converts to "Django" on redirect # "django" converts to "Django" on redirect
r = requests.get(f"https://pypi.org/pypi/{package}/json", allow_redirects=True) r = requests.get(f"https://pypi.org/pypi/{package}/json", allow_redirects=True) # noqa: S113
if not r.ok: if not r.ok:
print(f"Couldn't find package: {package}") print(f"Couldn't find package: {package}")
sys.exit(1) sys.exit(1)
@ -214,9 +216,9 @@ class GitHubManager:
for classifier in package_info["info"]["classifiers"]: for classifier in package_info["info"]["classifiers"]:
# Usually in the form of "Framework :: Django :: 3.2" # Usually in the form of "Framework :: Django :: 3.2"
tokens = classifier.split(" ") tokens = classifier.split(" ")
if len(tokens) >= 5 and tokens[2].lower() == "django" and "." in tokens[4]: if len(tokens) >= 5 and tokens[2].lower() == "django" and "." in tokens[4]: # noqa: PLR2004
version = DjVersion.parse(tokens[4]) version = DjVersion.parse(tokens[4])
if len(version) == 2: if len(version) == 2: # noqa: PLR2004
supported_dj_versions.append(version) supported_dj_versions.append(version)
if supported_dj_versions: if supported_dj_versions:

View File

@ -1,12 +1,14 @@
from __future__ import annotations from __future__ import annotations
import subprocess import subprocess
import tomllib
from pathlib import Path from pathlib import Path
import tomllib
ROOT = Path(__file__).parent.parent ROOT = Path(__file__).parent.parent
TEMPLATED_ROOT = ROOT / "{{cookiecutter.project_slug}}" TEMPLATED_ROOT = ROOT / "{{cookiecutter.project_slug}}"
REQUIREMENTS_LOCAL_TXT = TEMPLATED_ROOT / "requirements" / "local.txt" REQUIREMENTS_LOCAL_TXT = TEMPLATED_ROOT / "requirements" / "local.txt"
TEMPLATE_PRE_COMMIT_CONFIG = ROOT / ".pre-commit-config.yaml"
PRE_COMMIT_CONFIG = TEMPLATED_ROOT / ".pre-commit-config.yaml" PRE_COMMIT_CONFIG = TEMPLATED_ROOT / ".pre-commit-config.yaml"
PYPROJECT_TOML = ROOT / "pyproject.toml" PYPROJECT_TOML = ROOT / "pyproject.toml"
@ -18,7 +20,7 @@ def main() -> None:
return return
update_ruff_version(old_version, new_version) update_ruff_version(old_version, new_version)
subprocess.run(["uv", "lock", "--no-upgrade"], cwd=ROOT) subprocess.run(["uv", "lock", "--no-upgrade"], cwd=ROOT, check=False) # noqa: S603,S607
def get_requirements_txt_version() -> str: def get_requirements_txt_version() -> str:
@ -44,12 +46,13 @@ def update_ruff_version(old_version: str, new_version: str) -> None:
f"ruff=={new_version}", f"ruff=={new_version}",
) )
PYPROJECT_TOML.write_text(new_content) PYPROJECT_TOML.write_text(new_content)
# Update pre-commit config # Update pre-commit configs
new_content = PRE_COMMIT_CONFIG.read_text().replace( for config_file in [PRE_COMMIT_CONFIG, TEMPLATE_PRE_COMMIT_CONFIG]:
f"repo: https://github.com/astral-sh/ruff-pre-commit\n rev: v{old_version}", new_content = config_file.read_text().replace(
f"repo: https://github.com/astral-sh/ruff-pre-commit\n rev: v{new_version}", f"repo: https://github.com/astral-sh/ruff-pre-commit\n rev: v{old_version}",
) f"repo: https://github.com/astral-sh/ruff-pre-commit\n rev: v{new_version}",
PRE_COMMIT_CONFIG.write_text(new_content) )
config_file.write_text(new_content)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -23,7 +23,7 @@ def main() -> None:
Script entry point. Script entry point.
""" """
# Generate changelog for PRs merged yesterday # Generate changelog for PRs merged yesterday
merged_date = dt.date.today() - dt.timedelta(days=1) merged_date = dt.date.today() - dt.timedelta(days=1) # noqa: DTZ011
repo = Github(login_or_token=GITHUB_TOKEN).get_repo(GITHUB_REPO) repo = Github(login_or_token=GITHUB_TOKEN).get_repo(GITHUB_REPO)
merged_pulls = list(iter_pulls(repo, merged_date)) merged_pulls = list(iter_pulls(repo, merged_date))
print(f"Merged pull requests: {merged_pulls}") print(f"Merged pull requests: {merged_pulls}")
@ -54,7 +54,7 @@ def main() -> None:
# Run uv lock # Run uv lock
uv_lock_path = ROOT / "uv.lock" uv_lock_path = ROOT / "uv.lock"
subprocess.run(["uv", "lock", "--no-upgrade"], cwd=ROOT) subprocess.run(["uv", "lock", "--no-upgrade"], cwd=ROOT, check=False) # noqa: S603, S607
# Commit changes, create tag and push # Commit changes, create tag and push
update_git_repo([changelog_path, setup_py_path, uv_lock_path], release) update_git_repo([changelog_path, setup_py_path, uv_lock_path], release)

View File

@ -1,4 +1,4 @@
import glob import glob # noqa: EXE002
import os import os
import re import re
import sys import sys
@ -227,7 +227,7 @@ def test_django_upgrade_passes(cookies, context_override):
python_files = [ python_files = [
file_path.removeprefix(f"{result.project_path}/") file_path.removeprefix(f"{result.project_path}/")
for file_path in glob.glob(str(result.project_path / "**" / "*.py"), recursive=True) for file_path in glob.glob(str(result.project_path / "**" / "*.py"), recursive=True) # noqa: PTH207
] ]
try: try:
sh.django_upgrade( sh.django_upgrade(

View File

@ -1,11 +1,7 @@
[tox] [tox]
skipsdist = true skipsdist = true
envlist = py312,black-template envlist = py312
[testenv] [testenv]
passenv = AUTOFIXABLE_STYLES passenv = AUTOFIXABLE_STYLES
commands = pytest -n auto {posargs:./tests} commands = pytest -n auto {posargs:./tests}
[testenv:black-template]
deps = black
commands = black --check hooks tests docs scripts