# pre-commit your Django projects

Updated 08/02/2024: Updated example dependencies, use Ruff instead of flake8 and pylint

[Pre-commit](https://pre-commit.com/) is a python based tool to enable easy integration of git hooks, and it's supported by plenty of tools like ruff, black, ...

While coding a Django project in a team, you would like to have unified rules and styles to avoid unnecessary changes on each Pull Request another team member will review. After setting up for the first time, this is for me a must for every project to ensure consistency and code quality.

After installing pre-commit, you will need to create a `.pre-commit-config.yaml` file and run `pre-commit install` inside the project to activate the git hooks.

## Default pre-commit hooks

```yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
    -   id: check-yaml
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
-   repo: https://github.com/psf/black
    rev: 24.1.1
    hooks:
    -   id: black
```

Check [this link](https://pre-commit.com/hooks.html) for a full list of builtins hooks from pre-commit.

## isort

The initial example contains configuration of python [Black](https://black.readthedocs.io/) formatting, on top of it I strongly recommend using consistent sorted import via [isort](https://github.com/pycqa/isort)

```yaml
  - repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks:
      - id: isort
```

If you use isort with black you will need to use black profile as follow in `pyproject.toml` in your root project directory.

```toml
[tool.black]
line-length = 88
include = '\.pyi?$'

[tool.isort]
profile = "black"
py_version=311
multi_line_output = 3
line_length = 88
default_section = "THIRDPARTY"
skip = ["migrations"]
```

## Pyupgrade

Another amazing tool you might need to use is [pyupgrade](https://github.com/asottile/pyupgrade), this one will ensure you will use new python feature like new style classes, removing unicode literals, use the python3 super() calls etc.

```yaml
- repo: https://github.com/asottile/pyupgrade
    rev: v3.15.0
    hooks:
      - id: pyupgrade
        args: ["--py3-plus", "--py311-plus"]
```

If you use python 3.10 you can change `--py39-plus` with `--py310plus`

## Djangoupgrade

Next similar tools is [djangoupgrade](https://github.com/adamchainz/django-upgrade), it will fix deprecated features of Django and make it easy to upgrade your Django projects, unfortunately is does only check against python files, for Django templates it's not doing anything, but I still find it useful.

```yaml
- repo: https://github.com/adamchainz/django-upgrade
    rev: "1.15.0"
    hooks:
      - id: django-upgrade
        args: [--target-version, "5.0"]
```

Note the target-version, this depend on the current supported Django version in the project.

## Bandit

Another great tool to use is [Bandit](https://github.com/pycqa/bandit), Bandit is designed to find common security issues in Python code. To do this Bandit processes each file, builds an AST from it, and runs appropriate plugins against the AST nodes. Once Bandit has finished scanning all the files it generates a report.

```yaml
- repo: https://github.com/pycqa/bandit
    rev: 1.7.7
    hooks:
      - id: bandit
        args: ["-iii", "-ll"]
```

## Ruff

[Ruff](https://docs.astral.sh/ruff/) is a powerful Python tool that consolidates various code quality checkers like pycodestyle, pyflakes, and mccabe, along with third-party plugins. It ensures the style and quality of Python code through a unified interface.

```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
    # Ruff version.
    rev: "v0.2.1"
    hooks:
      - id: ruff
```

Now we will need to setup Ruff config in `pyproject.toml`

```ini
[tool.ruff]
line-length = 88
target-version = "py311"
exclude = [
    ".git",
    ".mypy_cache",
    ".pre-commit-cache",
    ".ruff_cache",
    ".tox",
    ".venv",
    "venv",
    "docs",
    "__pycache",
    "**/migrations/*",
]
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.lint.mccabe]
max-complexity = 10
```

To run ruff manually, once installed (e.g `pip install ruff`)

```bash
ruff check .
```

# Final result

The final `.pre-commit-config.yaml`

```yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
    -   id: check-yaml
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
-   repo: https://github.com/psf/black
    rev: 22.8.0
    hooks:
    -   id: black
- repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks:
      - id: isort
- repo: https://github.com/asottile/pyupgrade
    rev: v3.15.0
    hooks:
      - id: pyupgrade
        args: ["--py3-plus", "--py39-plus"]
- repo: https://github.com/adamchainz/django-upgrade
    rev: "1.15.0"
    hooks:
      - id: django-upgrade
        args: [--target-version, "4.0"]
- repo: https://github.com/pycqa/bandit
    rev: 1.7.7
    hooks:
      - id: bandit
        args: ["-iii", "-ll"]
- repo: https://github.com/charliermarsh/ruff-pre-commit
    # Ruff version.
    rev: "v0.2.1"
    hooks:
      - id: ruff
```

## **Bonus: CI integration**

To make sure no one skip the pre-commit setup, either by intention of if someone forget to install the hooks locally after cloning the project for the first time, you can run `pre-commit run --all` in your CI.

Caching might be tricky and useful as initial environment setup take some time and therefore you can use the `PRE_COMMIT_HOME` for specifying which directory is used for the cache

### Gitlab CI

```yaml
precommit:
  image:
    name: python:3.10
  stage: test
  variables:
    PRE_COMMIT_HOME: ".cache/.pre-commit-cache/"
  script:
    - pip install pre-commit
    - pre-commit run --all
  rules:
    - exists:
        - ".pre-commit-config.yaml"
  interruptible: true
  cache:
    key: $CI_PROJECT_NAME-precommit-cache-v1
    paths:
      - .cache/.pre-commit-cache/
```

### Github Actions

```yaml
name: CI

on:
  create:
    tags:
      - "*" # run on all tags
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: 3.11
      - name: Install pre-commit
        run: pip install pre-commit
      - name: Cache pre-commit hooks
        id: pre-commit-cache
        uses: actions/cache@v2
        with:
          path: $HOME/.cache/pre-commit
          key: ${{ runner.os }}-pre-commit-hooks-${{ hashFiles('**/.pre-commit-config.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pre-commit-hooks-
      - name: Run pre-commit
        run: |
          pre-commit run --all-files
```
