Greatings fellow Pythonistas and Emacs users!
Have you ever worked on a project that uses one of the many Python package managers and/or virtual environments, where all the linters, formatters and commit hooks are set up meticulously, and then when you fire up Emacs, packages like flycheck or lsp-mode are either unable to find the binary in your virtualenv, or are using a wrong one?
Have you ever tried one of the 11+ Emacs virtualenv packages to help you fix this problem, but are still at a lost at why your other favorite Emacs packages still can't find the right binaries, or they stop working when you switch to a different project using a different flavor of virtualenv?
If you answer "yes" for any of these questions, you've come to the right place.
The first key insight is to recognize the paths to executables of many of these linting and formatting Emacs packages rely on are configurable.
The second key insight is Emacs allows you to setup a different value for the exectuable path on a per buffer basis, and that these packages work with these buffer-local values.
The hardest problem is finding the correct executable, this is what pet
tries to solve.
As long as you use one of the supported Python virtualenv tools, pet
will be
able to find the virtualenv root and binary you ask for, with zero Emacs
configuration necessary.
pet
works well with popular source code project management packages such as
Projectile and the
built-in project.el
. The first time you call one the few pet
helper
functions, it will use Projectile or project.el to detect the root of your
project, search for the configuration files for the many supported Python
virtualenv tools, and then lookup the location of the virtualenv based on the
content of the configuration files. Once a virtualenv is found, all executables
are found by looking into its bin
directory.
- pre-commit
- poetry
- pipenv
- direnv
- venv or virtualenv
- pdm
- pipx
- pyenv (very poorly maintained, don't use it unless you are using Homebrew on macOS)
- docker
- Whatever is on your
VIRTUAL_ENV
environment variable
- Built-in project.el
- projectile
- envrc (direnv caveats)
- eglot
- flycheck
- lsp-jedi
- lsp-pyright
- dap-python
- blacken
- yapfify
- python-black
- python-isort
- python-pytest
Currently pet
requires a program to convert TOML to JSON, a program to
convert YAML to JSON, and if you are using Emacs < 29, the sqlite3
command
to be installed on your system.
By default, both the TOML to JSON and YAML to JSON converters are configured to use dasel. If you are on Linux, it may be more convenient to use tomljson and yq since both of which are likely to be available from the system package management system.
When a suitable Emacs Lisp YAML and TOML parser becomes available, dasel
will be made optional.
If you are using Emacs on macOS, to get the most out of pet
, it is best to
install exec-path-from-shell first to ensure all of the
Supported Python Virtual Environment Tools are available in your
exec-path
. Once your exec-path
is synced up to your shell's $PATH
environment variable, you can use the following ways to help you setup the rest
of your Emacs packages properly.
Generally, the following snippet is all you'll need:
(require 'pet)
;; Emacs < 26
;; You have to make sure this function is added to the hook last so it's
;; called first
(add-hook 'python-mode-hook 'pet-mode)
;; Emacs 27+
;; The -10 tells `add-hook' to makes sure the function is called as early as
;; possible whenever it is added to the hook variable
(add-hook 'python-mode-hook 'pet-mode -10)
;; Emacs 29+
;; This will turn on `pet-mode' on `python-mode' and `python-ts-mode'
(add-hook 'python-base-mode-hook 'pet-mode -10)
Or, if you use use-package:
(use-package pet
:config
(add-hook 'python-base-mode-hook 'pet-mode -10))
This will setup the buffer local variables for all of the Supported Emacs Packages.
If you need to configure a package that pet
doesn't support, or only want to
configure a couple of packages instead of all the supported one, pet
offers
2 autoloaded functions to help you find the correct path to the executable and
virtualenv directory:
(pet-executable-find EXECUTABLE)
(pet-virtualenv-root)
For example, to set up python-mode
to use the correct interpreter when you
execute M-x run-python
:
(add-hook 'python-mode-hook
(lambda ()
(setq-local python-shell-interpreter (pet-executable-find "python")
python-shell-virtualenv-root (pet-virtualenv-root))))
For flycheck
, due to its complexity, pet
also comes with another
autoloaded function to help you setup the flake8
, pylint
and mypy
checkers:
(add-hook 'python-mode-hook 'pet-flycheck-setup)
(use-package exec-path-from-shell
:if (memq (window-system) '(mac ns))
:config (exec-path-from-shell-initialize))
(use-package flycheck)
(use-package lsp)
(use-package lsp-jedi
:after lsp)
(use-package lsp-pyright
:after lsp)
(use-package dap-python
:after lsp)
(use-package eglot)
(use-package python-pytest)
(use-package python-black)
(use-package python-isort)
(use-package pet
:ensure-system-package (dasel sqlite3)
:config
(add-hook 'python-mode-hook
(lambda ()
(setq-local python-shell-interpreter (pet-executable-find "python")
python-shell-virtualenv-root (pet-virtualenv-root))
;; (pet-eglot-setup)
;; (eglot-ensure)
(pet-flycheck-setup)
(flycheck-mode)
(setq-local lsp-jedi-executable-command
(pet-executable-find "jedi-language-server"))
(setq-local lsp-pyright-python-executable-cmd python-shell-interpreter
lsp-pyright-venv-path python-shell-virtualenv-root)
(lsp)
(setq-local dap-python-executable python-shell-interpreter)
(setq-local python-pytest-executable (pet-executable-find "pytest"))
(when-let ((black-executable (pet-executable-find "black")))
(setq-local python-black-command black-executable)
(python-black-on-save-mode))
(when-let ((isort-executable (pet-executable-find "isort")))
(setq-local python-isort-command isort-executable)
(python-isort-on-save-mode)))))
Short answer:
Use envrc.
(require 'envrc)
(add-hook 'change-major-mode-after-body-hook 'envrc-mode)
Longer answer:
There are a number of packages similar to envrc
such as direnv
and
buffer-env
that claim to be able to configure direnv
in Emacs. However,
they all suffer from various problems such as changing the environment and
exec-path
for the entire Emacs process, unable to activate early enough or
being too general to support direnv tightly.
Because pet
needs to be able to configure the buffer local variables
before the rest of the minor modes are activated, but after
exec-path
has been set up by direnv, one must take care of choosing a minor
mode package that allows the user to customize when it takes effect. This
requirement rules our direnv.el
[1].
[1] | Earlier versions of pet suggested direnv.el as a solution, it is
no longer recommended due to this reason. |
Pet
does not automatically create virtualenvs for you. If you have a fresh
clone, you must create the virtualenv and install your development dependencies
into it first. Once it is done, the next time you open a Python file buffer
pet
will automatically set up the executable variables for you.
To find out how to do it, please find the virtualenv tool in question from Supported Python Virtual Environment Tools, and visit its documentation for details.
The reason is mainly due to the fact that many Python projects use development
tools located in different virtualenvs. This means exec-path
needs to be
prepended with all of the virtualenvs for all of the dev tools, and always kept
in the correct order. An example where this approach may cause issues is dealing
with projects that use pre-commit
and direnv
. A typical pre-commit
configuration may include many "hooks", where each of them is isolated in its
own virtualenv. While prepending many directories to exec-path
is not
problematic in itself, playing well with other Emacs packages that mutate
exec-path
reliably is non-trivial. Providing an absolute path to executable
variables conveniently sidesteps this complexity, while being slightly more
performant.
In addition, there are Emacs packages, most prominantly flycheck
that by
default require dev tools to be installed into the same virtualenv as the first
python
executable found on exec-path
. Changing this behavior requires
setting the corresponding flycheck
checker executable variable to the
intended absolute path.
You can turn on pet-debug
and watch what comes out in the *Messages*
buffer. In addition, you can use M-x pet-verify-setup
in your Python buffers
to find out what was detected.
For lsp
, use lsp-describe-session
.
For eglot
, use eglot-show-workspace-configuration
.
For flycheck
, use flycheck-verify-setup
.
Nope. You can uninstall them all. This is the raison d'être of this package.