For years, I have struggled to get elpy
running with direnv
- and have recently found the missing link, which was hiding in the setting elpy-rpc-virtualenv-path
. If you set it to 'current
, life is good.
direnv + pyenv
First of all, why would you want to use direnv
for your development purposes? I primarily use it as a nice tool to help me manage virtual environments, and it also allows me to develop using a specific Python version.
For example, this is what I currently have in .envrc
:
layout pyenv 3.10.3
This sets up a virtualenv using pyenv
, containing a Python 3.10.3 interpreter so I am able to use spiffy language features like PEP-604’s writing union types as X | Y
.
If you are on a Mac, direnv
and pyenv
can be installed using the usual homebrew
commands. Follow the instructions on how to integrate it into your shell.
You will also need some configuration to use direnv
in Emacs, for example, if you are using use-package
, the following snippet should be enough to get you going:
(use-package exec-path-from-shell
:config (progn (exec-path-from-shell-initialize)))
(use-package direnv
:after exec-path-from-shell
:config (direnv-mode))
Note that if you use direnv
, every project has its own virtualenv that is no longer shared with the rest of the system. In this case, elpy
needs the jedi
package to be installed, which means you’ll need to install it in every virtualenv you want to use elpy
in. It is easy though:
$ python -m pip install jedi
Collecting jedi
Using cached jedi-0.18.1-py2.py3-none-any.whl (1.6 MB)
Collecting parso<0.9.0,>=0.8.0
Using cached parso-0.8.3-py2.py3-none-any.whl (100 kB)
Installing collected packages: parso, jedi
Successfully installed jedi-0.18.1 parso-0.8.3
$
elpy
Now it is time to set up elpy
! Again, this is easy – once you know how. Just add something like the following to your Emacs configuration:
(use-package elpy
:custom
(elpy-rpc-virtualenv-path 'current) ;; this makes elpy use direnv venvs
(elpy-modules '(elpy-module-company elpy-module-eldoc elpy-module-flymake elpy-module-pyvenv elpy-module-yasnippet elpy-module-django elpy-module-sane-defaults)) ;; removes elpy-highlight-indentation
:config
(elpy-enable))
The most important part is the elpy-rpc-virtualenv-path
setting, which will make elpy
use the current path, which in this case will have been set by the direnv
integration in Emacs.
bonus contents: darker
black
is a combination of a tool and a somewhat ossified coding style that aspires to improve the legibility of Python code. Unfortunately, it fails to reach that goal every now and then, and I prefer that black
does not touch my artisanal code in those cases. Therefore, I have been using a wrapper called black-macchiato
that makes it possible to let black
selectively reformat a portion of the code.
However, there is something nicer out there that automates running black
but prevents it from reformatting code that was not touched since the last merge: darker
. This great piece of software can be installed in your virtualenv using pip:
$ python -m pip install darker[isort]
Collecting darker
Using cached darker-1.4.2-py3-none-any.whl (84 kB)
[...]
$
Note that by supplying [isort]
it will also automatically pull in dependencies to run isort
from darker
, which should help you to keep your import statements tidy.
I have the following snippet in my emacs-configuration to get darker to run automatically on save (most of the time) using reformatter
:
(use-package reformatter
:hook ((python-mode . darker-reformat-on-save-mode))
:config
(reformatter-define darker-reformat
:program "darker"
:stdin nil
:stdout nil
:args (list "-q" "-i" "-S" input-file))
:bind
(:map python-mode-map
("<f2>" . darker-reformat-region)
("<M-f2>" . darker-reformat-on-save-mode))) ;; to disable temporarily
Note that darker
also supports pyproject.toml
integration, so instead of putting arguments like "-i"
and "-S"
in the reformatter
-configuration, you can also put it there and share it with other people working on this project.
[tool.darker]
line-length = 104 # let's compromise between black's 88 and a sensible 120 🤦♂️
target-version = ["py310"]
include = '\.pyi?$'
skip-string-normalization = 1
isort = 1
Unfortunately this duplicates bits of the configuration from black
but that is a small price to pay – in the end, they’re both configured in the same file. Just be sure to keep them in sync.