Spacemacs and ruff to sort Python imports on save
Table of Contents
Recently I introduced ruff in a Python project of a client to format the code base and to sort the imports. Most of the developers there use PyCharm, which is easy to configure to use ruff. Surprisingly, it turned out to be more difficult in Spacemacs. To cut a long story short, Spacemacs1 does not support the use of ruff to sort Python imports on save. For this it relies on the LSP server but the official ruff LSP server also does not support it.
Existing support in Spacemacs
In Spacemacs, if you set boolean variable python-sort-imports-on-save
to t
,
it will automatically sort the imports of a Python file when you save that file.
This functionality comes with two caveats. First, Spacemacs uses isort to sort
the imports, you cannot configure the use of ruff to sort them. Now isort has a
lot of options to tweak its behavior, but I could not configure it to
sufficiently emulate the ruff configuration in-use.
I use the ruff LSP server to format Python code and this brings me to the second caveat: Spacemacs sorts the imports after the LSP servers have formatted the code2. According to the ruff documentation that is the wrong order: you should first sort the imports and then format the code.
If you want to use ruff to sort the Python imports on save, you either have to rely on a LSP server that supports this, or write the Emacs Lisp code yourself. I use 2 Python language servers:
- python-lsp-server, or pylsp for short, which I (mostly) use for completions, find-references and go-to-definition;
- ruff “as a server”, which I use for format-on-save and sometimes its code actions.
The ruff server does support “(code) format on save”, but in ruff, sorting imports is something else than a code format. This is reflected in the language server: it can format on save but sorting imports is only available as a code action. This means you have to manually trigger the code action to sort the imports. Of course, you can also write some Emacs Lisp to find the appriopriate code action and execute it on save. I use (the LSP clients) of lsp-mode and this does not seem to be that straightforward.
Back to python-lsp-server, out-of-the-box it does not support sorting imports on save. That server has a ruff plugin that can sort the imports on save. Its “format document” code actually calls ruff twice: first to format the code and then to sort the imports3. I tried it and it works but there is a snag: in Emacs the code actions provided by this plugin interfere with the ones provided by the ruff server. Emacs shows you the same code action twice and it is unclear which action comes from which server. What’s worse is that selecting one sometimes resulted in a Lisp error. Unfortunately neither the servers nor lsp-mode seem to give you the option to ignore (certain) code actions.
Finally, python-lsp-server also has an isort plugin plugin that can sort imports on save. As mentioned before, isort cannot emulate the ruff configuration in use.
Workaround time
An obvious workaround is to just use isort. Both isort and ruff have a lot of options to configure their sorting behavior and for the current code I can get them “close enough”. I will need to change a ruff sorting option and this will modify the current code. This is not a no-go, but it’s also not something I can do immediately.
The workaround I currently use is to call ruff from a Git pre-commit hook. So Emacs calls ruff to format the file on save and Git updates it when you commmit the change (and only when necessary). I’m already using pre-commit to configure the Git hooks and the following config is what you need:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.5
hooks:
- id: ruff
args:
- --select
- I
- --fix
stages:
- pre-commit
In practice this works satisfactory because you know how to keep ruff satisfied ;). It’s not ideal though as it can make (and sometimes does make) your commit a two-step process.
A better option would be to add a before-save-hook
in Emacs that calls ruff to
sort the imports before the LSP client kicks in. I’ve googled a bit and this
should be easy to do using the Emacs reformatter package, which lets you easily
define the Lisp commands to run a code formatter. But that’s for another day.
Spacemacs commit hash abba23b6 installed on December 25, 2024. ↩︎
The function that runs isort is on the global
before-save-hook
. The function that triggers the LSP servers to format the code is on the localbefore-save-hook
. The functions on the local hook run before the ones on the global hook. ↩︎As mentioned earlier, this is the wrong order. You should use ruff to first sort the imports and then to format the code. ↩︎