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 a84612d0..3de15dc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,7 @@ binaryornot==0.4.4 # Code quality # ------------------------------------------------------------------------------ -black==24.2.0 -isort==5.13.2 -flake8==7.0.0 +ruff==0.2.1 django-upgrade==1.16.0 djlint==1.34.1 pre-commit==3.6.1 diff --git a/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json b/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json index c4158dc1..7fcd6287 100644 --- a/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json +++ b/{{cookiecutter.project_slug}}/.devcontainer/devcontainer.json @@ -37,22 +37,11 @@ "editor.codeActionsOnSave": { "source.organizeImports": true }, - // 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 diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml index 466cfece..7c9565e1 100644 --- a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml @@ -33,26 +33,15 @@ repos: - 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.1 hooks: - - id: pyupgrade - args: [--py311-plus] - - - repo: https://github.com/psf/black - rev: 24.2.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.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}}/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/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt index cad007f4..da0e0494 100644 --- a/{{cookiecutter.project_slug}}/requirements/local.txt +++ b/{{cookiecutter.project_slug}}/requirements/local.txt @@ -28,15 +28,9 @@ 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 +ruff==0.2.1 coverage==7.4.1 # https://github.com/nedbat/coveragepy -black==24.2.0 # https://github.com/psf/black 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.1 # https://github.com/pre-commit/pre-commit # Django 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}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_admin.py index 75917ab3..095b2cbb 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 @@ -58,7 +58,7 @@ class TestUserAdmin: 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: reload(users_admin)