Ruff: Sorting Imports With pre-commit

Recently I’ve been updating one of my personal projects to a more modern linting & formatting solution. Namely, I’ve been trying out Ruff. I’ve heard a lot of good things about it (fast, easy to set-up, drop-in replacement for many tools), and it turns out for my use case they are completely valid.

Of course, my setup is not sophisticated, which makes things easier. I had only a few pre-commit hooks set up:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 22.6.0
hooks:
- id: black
files: "^backend/"
- repo: https://github.com/timothycrosley/isort
rev: 5.12.0
hooks:
- id: isort
files: "^backend/"
- repo: https://github.com/PyCQA/flake8
rev: 3.9.2
hooks:
- id: flake8
args: ["--config=backend/.flake8"]
files: "^backend/"

Aside from some flake8 rules that I like to ignore for some Django-specific files, migrating to ruff was equivalent to replacing the file with the following content:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
hooks:
- id: ruff
name: "ruff lint"
files: "^backend/"
- id: ruff-format
name: "ruff format"
files: "^backend/"

As you can see, it makes an already simple config even simpler - which I appreciate. The less lines there are, the less of them we have to maintain.

Sorting Imports

One thing that the above setup is missing, though, is import sorting. ruff format does not touch the imports, which for me was slightly disappointing. I was used to having them sorted by isort with black's profile, and I was hoping for something similar out of the box.

After some searching, I found that you could sort the imports with ruff as follows [1]:

ruff check --select I --fix
ruff format

Looks good! I’d have liked to have one command instead of two, and it would have been nice if I didn’t need to run check with a specific rule selected, but hey, this is still pretty easy to do. The only thing left was to figure out how to cajole it to a pre-commit hook.

After some experimentation, this is what I ended up with:

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.4
hooks:
- id: ruff
files: "^backend/"
###### Relevant part below ######
- id: ruff
args: ["check", "--select", "I", "--fix"]
files: "^backend/"
###### Relevant part above ######
- id: ruff-format
files: "^backend/"

This is decent! It does the job, is quite easy to read, and doesn't add anything too complicated. It is also possible to hack something like this so that there would be one hook responsible for formatting the files:

- repo: local
hooks:
- id: custom ruff format
name: ruff format & sort imports
entry: bash -c 'ruff check --select I --fix && ruff format'
language: system
types: [python]
pass_filenames: false

But I find it less readable, and I prefer to just use whatever ruff-pre-commit gives me out of the box. Again - the less custom code, the less I’ll have to worry about its maintenance.

Overall I’ve been quite happy with ruff. It truly is blazing fast, easy to start with and a delight to use. I’m looking forward to test it on more advanced use cases!

Sources

  1. https://docs.astral.sh/ruff/formatter/#sorting-imports