diff --git a/.github/actions/test-build/action.yml b/.github/actions/test-build/action.yml index 2db1d79..1a07522 100644 --- a/.github/actions/test-build/action.yml +++ b/.github/actions/test-build/action.yml @@ -18,7 +18,7 @@ runs: steps: - name: Create version tag id: version-tag - uses: nationalarchives/ds-docker-actions/.github/actions/get-version-tag@main # TODO: Could replace the "Prepare image tag" step below? + uses: nationalarchives/ds-docker-actions/.github/actions/get-version-tag@main # TODO: Could replace the "Prepare image tag" step below? - name: Test new image tag run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b97fd8..e68a6a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/nationalarchives/docker/compare/v1.11.1...HEAD) ### Added + +- Added Ruff as a replacement for isort, Flake8 and Black + ### Changed - Update default isort config to ignore `node_modules` diff --git a/docker/tna-python-dev/Dockerfile b/docker/tna-python-dev/Dockerfile index 0cb98bc..8b68fa3 100644 --- a/docker/tna-python-dev/Dockerfile +++ b/docker/tna-python-dev/Dockerfile @@ -45,9 +45,7 @@ RUN apt-get update; \ # Set the versions for various tools that we # want to install to help with development # ========================================== -ENV BLACK_VERSION=26.3.1 \ - FLAKE8_VERSION=7.1.1 \ - ISORT_VERSION=8.0.1 \ +ENV RUFF_VERSION=0.15.12 \ DJLINT_VERSION=1.36.4 \ DJHTML_VERSION=3.0.11 \ DJANGO_DEBUG_TOOLBAR_VERSION=6.3.0 \ @@ -85,7 +83,7 @@ ENV PATH="/home/app/.local/bin/tasks:/home/app/.local/bin/dev:$PATH" # ========================================== # hadolint ignore=SC1091 RUN install-format-dependencies; \ - python -m pip install --no-cache-dir --quiet black=="$BLACK_VERSION" flake8=="$FLAKE8_VERSION" isort=="$ISORT_VERSION" djlint=="$DJLINT_VERSION" djhtml=="$DJHTML_VERSION" + python -m pip install --no-cache-dir --quiet ruff=="$RUFF_VERSION" djlint=="$DJLINT_VERSION" djhtml=="$DJHTML_VERSION" # ========================================== # Copy any configuration files into the main diff --git a/docker/tna-python-dev/README.md b/docker/tna-python-dev/README.md index 3832424..73bdc0c 100644 --- a/docker/tna-python-dev/README.md +++ b/docker/tna-python-dev/README.md @@ -4,9 +4,9 @@ This image extends `tna-python` and can be used for local development **ONLY**. It adds: -- `black`, `flake8` and `isort` for formatting Python code +- `ruff` for formatting and linting Python - `prettier`, `eslint` and `stylelint` for formatting JavaScript and CSS -- scripts for formatting code +- scripts for formatting - `django-debug-toolbar` for debugging Django applications - `git` @@ -92,30 +92,92 @@ The process for these commands is: ### `format` -1. Run `isort` -1. Run `black` -1. Run `flake8` +1. Run `ruff` 1. Run `djlint` to check HTML templates for Jinja compliance 1. Apply `prettier` to all files in the `/app` directory 1. Run `stylelint` against all SCSS files in the `/app` directory 1. Run `eslint` against all JavaScript files in the `/app` directory -#### How to override the default configurations +#### Ruff + +By default, Ruff is only configured to check the following when running `format` and `checkformat`: + +| Code | Purpose | Defined by | +| ---------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| `E4`, `E7`, `E9` | [Subset of pycodestyle errors](https://pypi.org/project/pycodestyle/) | [Ruff default configuration](https://docs.astral.sh/ruff/configuration/) | +| `F` | [Pyflakes](https://pypi.org/project/pyflakes/) | [Ruff default configuration](https://docs.astral.sh/ruff/configuration/) | +| `W` | [pycodestyle warnings](https://pypi.org/project/pycodestyle/) | [`/home/app/ruff.toml`](./lib/ruff.toml) | +| `C901` | [mccabe "Function is too complex"](https://www.flake8rules.com/rules/C901.html) | [`/home/app/ruff.toml`](./lib/ruff.toml) | +| `B` | [flake8-bugbear](https://pypi.org/project/flake8-bugbear/) | [`/home/app/ruff.toml`](./lib/ruff.toml) | +| `I` | [isort](https://pypi.org/project/isort/) | [`/home/app/ruff.toml`](./lib/ruff.toml) | +| `Q` | [flake8-quotes](https://pypi.org/project/flake8-quotes/) | [`/home/app/ruff.toml`](./lib/ruff.toml) | + +Running `format --strict` or `checkformat --strict` will apply some extra rules defined in [`/home/app/ruff-strict.toml`](./lib/ruff-strict.toml): + +| Code | Purpose | +| ------ | --------------------------------------------------------------------- | +| `A` | [flake8-builtins](https://pypi.org/project/flake8-builtins/) | +| `DJ` | [flake8-django](https://pypi.org/project/flake8-django/) | +| `ERA` | [eradicate](https://pypi.org/project/eradicate/) | +| `FAST` | [fastapi](https://pypi.org/project/fastapi/) | +| `FIX` | [flake8-fixme](https://github.com/tommilligan/flake8-fixme) | +| `LOG` | [flake8-logging](https://pypi.org/project/flake8-logging/) | +| `N` | [pep8-naming](https://pypi.org/project/pep8-naming/) | +| `PL` | [pylint](https://pypi.org/project/pylint/) | +| `RET` | [flake8-return](https://pypi.org/project/flake8-return/) | +| `RSE` | [flake8-raise](https://pypi.org/project/flake8-raise/) | +| `RUF` | [Ruff-specific rules](https://docs.astral.sh/ruff/settings/#lintruff) | +| `SIM` | [flake8-simplify](https://pypi.org/project/flake8-simplify/) | +| `T20` | [flake8-print](https://pypi.org/project/flake8-print/) | +| `TD` | [flake8-todos](https://github.com/orsinium-labs/flake8-todos/) | +| `TRY` | [tryceratops](https://pypi.org/project/tryceratops/) | +| `UP` | [pyupgrade](https://pypi.org/project/pyupgrade/) | + +##### How to override Ruff configuration + +Create a `ruff.toml` file in your project root. If this file exists, the `--strict` parameter will be ignored on `format --strict` and `checkformat --strict`. + +```toml +# Extend the default Ruff configuration +extend = "/home/app/ruff.toml" + +# ...or extend the strict ruleset +# extend = "/home/app/ruff-strict.toml" + +extend-exclude = [ + # "migrations" # Exclude the "migrations" directory +] + +[lint] +ignore = [ + # Add rules to ignore in your project +] +``` + +##### Ignoring code -| Tool | Overwrite solution | More information | -| ----------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `isort` | `.isort.cfg` file in the project root | https://pycqa.github.io/isort/docs/configuration/config_files.html#isortcfg-preferred-format | -| `black` | Add `[tool.black]` config to the `pyproject.toml` | https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file | -| `flake8` | `.flake8` file in the project root | https://flake8.pycqa.org/en/latest/user/configuration.html#configuration-locations | -| `djlint` | `.djlintrc` file in the project root | https://djlint.com/docs/configuration/ | -| `prettier` | `.prettierignore` file in the project root | https://prettier.io/docs/en/ignore.html | -| `stylelint` | `.stylelintrc` file in the project root | https://stylelint.io/user-guide/configure/ | -| `eslint` | `.eslintrc.js` file in the project root | https://eslint.org/docs/latest/use/configure/configuration-files#using-configuration-files | +Use the `# noqa` annotation from [In-line Ignoring Errorsin Flake8](https://flake8.pycqa.org/en/latest/user/violations.html#in-line-ignoring-errors) to ignore failing lines. + +```python +def my_overly_complex_function(): # noqa: C901 + pass +``` + +#### How to override other default configurations + +| Tool | Overwrite solution | More information | +| ----------- | ------------------------------------------ | ------------------------------------------------------------------------------------------ | +| `djlint` | `.djlintrc` file in the project root | https://djlint.com/docs/configuration/ | +| `prettier` | `.prettierignore` file in the project root | https://prettier.io/docs/en/ignore.html | +| `stylelint` | `.stylelintrc` file in the project root | https://stylelint.io/user-guide/configure/ | +| `eslint` | `.eslintrc.js` file in the project root | https://eslint.org/docs/latest/use/configure/configuration-files#using-configuration-files | ### `checkformat` Runs the same tests as `format` but doesn't fix issues. Can be used in CI/CD pipelines to check formatting. +Like `format --strict`, `checkformat --strict` uses an [expanded set of Ruff rules](#ruff). + ### `outdated` 1. Shows outdated Poetry dependencies diff --git a/docker/tna-python-dev/bin/dev/checkformat b/docker/tna-python-dev/bin/dev/checkformat index 2440cd8..3b630c8 100755 --- a/docker/tna-python-dev/bin/dev/checkformat +++ b/docker/tna-python-dev/bin/dev/checkformat @@ -1,3 +1,9 @@ #!/bin/bash -FIX=false format +STRICT=false + +if [[ "$1" == "--strict" ]]; then + STRICT=true +fi + +STRICT=$STRICT FIX=false format diff --git a/docker/tna-python-dev/bin/dev/format b/docker/tna-python-dev/bin/dev/format index 8b037d8..e994682 100755 --- a/docker/tna-python-dev/bin/dev/format +++ b/docker/tna-python-dev/bin/dev/format @@ -2,8 +2,6 @@ set -e -cd /app || return - [[ -z $FIX ]] && FIX=true if [ "$FIX" = true ] @@ -14,42 +12,39 @@ else fi echo -echo "Running isort..." -if [ -f "/app/.isort.cfg" ] -then - ISORT_CONFIG="/app/.isort.cfg" - echo "Using app config ($ISORT_CONFIG)" -else - ISORT_CONFIG="/home/app/.isort.cfg" - echo "Using default config ($ISORT_CONFIG)" -fi -if [ "$FIX" = true ] -then - isort --settings-file "$ISORT_CONFIG" /app --overwrite-in-place -else - isort --settings-file "$ISORT_CONFIG" /app --diff +[[ -z $STRICT ]] && STRICT=false +if [[ "$1" == "--strict" ]]; then + STRICT=true fi -echo -echo "Running black..." -if [ "$FIX" = true ] +cd /app || return + +echo "Running ruff..." +if [ -f "/app/ruff.toml" ] then - black -t py310 -t py311 -t py312 -t py313 -t py314 /app + RUFF_CONFIG="/app/ruff.toml" + if [ "$STRICT" = true ] + then + echo "Using app config ($RUFF_CONFIG - ignoring strict mode)" + else + echo "Using app config ($RUFF_CONFIG)" + fi +elif [ "$STRICT" = true ] +then + RUFF_CONFIG="/home/app/ruff-strict.toml" + echo "Using default strict config ($RUFF_CONFIG)" else - black -t py310 -t py311 -t py312 -t py313 -t py314 --check /app + RUFF_CONFIG="/home/app/ruff.toml" + echo "Using default config ($RUFF_CONFIG)" fi -echo - -echo "Running flake8..." -if [ -f "/app/.flake8" ] +if [ "$FIX" = true ] then - FLAKE8_CONFIG="/app/.flake8" - echo "Using app config ($FLAKE8_CONFIG)" + ruff check /app --fix --config "$RUFF_CONFIG" + ruff format /app --config "$RUFF_CONFIG" else - FLAKE8_CONFIG="/home/app/.flake8" - echo "Using default config ($FLAKE8_CONFIG)" + ruff check /app --config "$RUFF_CONFIG" + ruff format --check /app --config "$RUFF_CONFIG" fi -flake8 --config="$FLAKE8_CONFIG" /app echo if [ -d "/app/app/templates" ] diff --git a/docker/tna-python-dev/lib/.flake8 b/docker/tna-python-dev/lib/.flake8 deleted file mode 100644 index a24cadd..0000000 --- a/docker/tna-python-dev/lib/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -ignore = E203, W503, E501 -exclude = venv*,__pycache__,node_modules,migrations,.git -max-line-length = 88 -max-complexity = 12 diff --git a/docker/tna-python-dev/lib/.isort.cfg b/docker/tna-python-dev/lib/.isort.cfg deleted file mode 100644 index 1c9f724..0000000 --- a/docker/tna-python-dev/lib/.isort.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[settings] -profile = black -skip = migrations,node_modules \ No newline at end of file diff --git a/docker/tna-python-dev/lib/ruff-strict.toml b/docker/tna-python-dev/lib/ruff-strict.toml new file mode 100644 index 0000000..7d6cdc4 --- /dev/null +++ b/docker/tna-python-dev/lib/ruff-strict.toml @@ -0,0 +1,24 @@ +extend = "./ruff.toml" + +[lint] +extend-select = [ + "A", + "DJ", + "ERA", + "FAST", + "FIX", + "LOG", + "N", + "PL", + "RET", + "RSE", + "RUF", + "SIM", + "T20", + "TD", + "TRY", + "UP" +] + +[lint.mccabe] +max-complexity = 12 \ No newline at end of file diff --git a/docker/tna-python-dev/lib/ruff.toml b/docker/tna-python-dev/lib/ruff.toml new file mode 100644 index 0000000..4e1b9b4 --- /dev/null +++ b/docker/tna-python-dev/lib/ruff.toml @@ -0,0 +1,23 @@ +[lint] +extend-select = [ + "W", + "C901", + "B", + "I", + "Q" +] +allowed-confusables = [ + "—", + "–", + "’" +] + +[lint.per-file-ignores] +"__init__.py" = [ + "E402", + "F401", + "PLC0415" +] + +[lint.mccabe] +max-complexity = 20 \ No newline at end of file