diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 3a87b269b..000000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -exclude = docs -max-line-length = 119 -extend-ignore = E203 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9cb8e4b36..0b8ac5775 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,27 +26,12 @@ repos: - id: prettier args: ["--tab-width", "2"] - - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.0 hooks: - - id: pyupgrade - args: [--py312-plus] - exclude: hooks/ - - - 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 + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt rev: "v2.5.0" diff --git a/hooks/__init__.py b/hooks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 318c5beb7..2de3612b2 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -1,3 +1,4 @@ +# ruff: noqa: PLR0133 import json import random import shutil @@ -79,7 +80,7 @@ def remove_heroku_files(): file_names = ["Procfile", "requirements.txt"] for file_name in file_names: 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 Path(file_name).unlink() shutil.rmtree("bin") @@ -179,7 +180,7 @@ def handle_js_runner(choice, use_docker, use_async): "dev": "concurrently npm:dev:*", "dev:webpack": "webpack serve --config webpack/dev.config.js", "dev:django": dev_django_cmd, - } + }, ) else: remove_dev_deps.append("concurrently") @@ -239,7 +240,7 @@ def remove_dotdrone_file(): 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: 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: print( "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 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): - django_secret_key = set_flag( + return set_flag( file_path, "!!!SET DJANGO_SECRET_KEY!!!", length=64, using_digits=True, using_ascii_letters=True, ) - return django_secret_key def set_django_admin_url(file_path: Path): - django_admin_url = set_flag( + return set_flag( file_path, "!!!SET DJANGO_ADMIN_URL!!!", formatted="{}/", @@ -304,24 +304,22 @@ def set_django_admin_url(file_path: Path): using_digits=True, using_ascii_letters=True, ) - return django_admin_url def generate_random_user(): 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() def set_postgres_user(file_path, value): - postgres_user = set_flag(file_path, "!!!SET POSTGRES_USER!!!", value=value) - return postgres_user + return set_flag(file_path, "!!!SET POSTGRES_USER!!!", value=value) def set_postgres_password(file_path, value=None): - postgres_password = set_flag( + return set_flag( file_path, "!!!SET POSTGRES_PASSWORD!!!", value=value, @@ -329,16 +327,14 @@ def set_postgres_password(file_path, value=None): using_digits=True, using_ascii_letters=True, ) - return postgres_password def set_celery_flower_user(file_path, value): - celery_flower_user = set_flag(file_path, "!!!SET CELERY_FLOWER_USER!!!", value=value) - return celery_flower_user + return set_flag(file_path, "!!!SET CELERY_FLOWER_USER!!!", value=value) def set_celery_flower_password(file_path, value=None): - celery_flower_password = set_flag( + return set_flag( file_path, "!!!SET CELERY_FLOWER_PASSWORD!!!", value=value, @@ -346,7 +342,6 @@ def set_celery_flower_password(file_path, value=None): using_digits=True, using_ascii_letters=True, ) - return celery_flower_password def append_to_gitignore_file(ignored_line): @@ -355,7 +350,7 @@ def append_to_gitignore_file(ignored_line): 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") production_django_envs_path = Path(".envs", ".production", ".django") 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")) -def main(): +def main(): # noqa: C901, PLR0912, PLR0915 debug = "{{ cookiecutter.debug }}".lower() == "y" set_flags_in_envs( @@ -444,7 +439,7 @@ def main(): print( INFO + ".env(s) are only utilized when Docker Compose and/or " "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() else: @@ -471,7 +466,7 @@ def main(): if "{{ cookiecutter.cloud_provider }}" == "None" and "{{ cookiecutter.use_docker }}".lower() == "n": print( 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": diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index 1e0c591ef..de2f87e3e 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -1,3 +1,4 @@ +# ruff: noqa: PLR0133 import sys TERMINATOR = "\x1b[0m" @@ -16,9 +17,9 @@ SUCCESS = "\x1b[1;32m [SUCCESS]: " project_slug = "{{ cookiecutter.project_slug }}" 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." diff --git a/pyproject.toml b/pyproject.toml index 348219a24..a25362490 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,22 +58,83 @@ docs = [ "sphinx-rtd-theme>=3", ] -[tool.black] +[tool.ruff] +target-version = "py39" line-length = 119 -target-version = [ - 'py312', +# Exclude the template content as most files aren't parseable +extend-exclude = [ + "[{]{2}cookiecutter.project_slug[}]{2}/*", + "docs/*", ] -# ==== isort ==== - -[tool.isort] -profile = "black" -line_length = 119 -known_first_party = [ - "tests", - "scripts", - "hooks", +lint.select = [ + "A", + # "ANN", # flake8-annotations: we should support this in the future but many errors atm + "ASYNC", + "B", + "BLE", + "C4", + "C90", + "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] keep_full_version = true diff --git a/scripts/create_django_issue.py b/scripts/create_django_issue.py index e728da798..bbc897f5c 100644 --- a/scripts/create_django_issue.py +++ b/scripts/create_django_issue.py @@ -13,7 +13,9 @@ import os import re import sys 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 from github import Github @@ -59,7 +61,7 @@ class DjVersion(NamedTuple): def get_package_info(package: str) -> dict: """Get package metadata using PyPI API.""" # "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: print(f"Couldn't find package: {package}") sys.exit(1) @@ -214,9 +216,9 @@ class GitHubManager: for classifier in package_info["info"]["classifiers"]: # Usually in the form of "Framework :: Django :: 3.2" 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]) - if len(version) == 2: + if len(version) == 2: # noqa: PLR2004 supported_dj_versions.append(version) if supported_dj_versions: diff --git a/scripts/ruff_version.py b/scripts/ruff_version.py index 4b0ab965c..ef50ce323 100644 --- a/scripts/ruff_version.py +++ b/scripts/ruff_version.py @@ -1,12 +1,14 @@ from __future__ import annotations import subprocess -import tomllib from pathlib import Path +import tomllib + ROOT = Path(__file__).parent.parent TEMPLATED_ROOT = ROOT / "{{cookiecutter.project_slug}}" 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" PYPROJECT_TOML = ROOT / "pyproject.toml" @@ -18,7 +20,7 @@ def main() -> None: return 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: @@ -44,12 +46,13 @@ def update_ruff_version(old_version: str, new_version: str) -> None: f"ruff=={new_version}", ) PYPROJECT_TOML.write_text(new_content) - # Update pre-commit config - new_content = PRE_COMMIT_CONFIG.read_text().replace( - 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) + # Update pre-commit configs + for config_file in [PRE_COMMIT_CONFIG, TEMPLATE_PRE_COMMIT_CONFIG]: + new_content = config_file.read_text().replace( + 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}", + ) + config_file.write_text(new_content) if __name__ == "__main__": diff --git a/scripts/update_changelog.py b/scripts/update_changelog.py index 1bedebf5a..7118b5a05 100644 --- a/scripts/update_changelog.py +++ b/scripts/update_changelog.py @@ -23,7 +23,7 @@ def main() -> None: Script entry point. """ # 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) merged_pulls = list(iter_pulls(repo, merged_date)) print(f"Merged pull requests: {merged_pulls}") @@ -54,7 +54,7 @@ def main() -> None: # Run 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 update_git_repo([changelog_path, setup_py_path, uv_lock_path], release) diff --git a/tests/test_cookiecutter_generation.py b/tests/test_cookiecutter_generation.py index 609e0d672..bbd0c209a 100755 --- a/tests/test_cookiecutter_generation.py +++ b/tests/test_cookiecutter_generation.py @@ -1,4 +1,4 @@ -import glob +import glob # noqa: EXE002 import os import re import sys @@ -227,7 +227,7 @@ def test_django_upgrade_passes(cookies, context_override): python_files = [ 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: sh.django_upgrade( diff --git a/tox.ini b/tox.ini index 70cde339f..43cb6baa9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,7 @@ [tox] skipsdist = true -envlist = py312,black-template +envlist = py312 [testenv] passenv = AUTOFIXABLE_STYLES commands = pytest -n auto {posargs:./tests} - -[testenv:black-template] -deps = black -commands = black --check hooks tests docs scripts