Python with Emacs: py(v)env and lsp-mode

2020-02-18

I have an old post describing how to spin up an IDE-like Python development environment in Emacs with Eglot and some .dir-locals.el help. Now a year later, I've converged on what I think is a better setup.

pyenv

My main driver for installing different versions of Python and spinning up virtual environments is pyenv. I use the automatic installer on all machines where I install pyenv, and I manually modify my shell's initialization such that I have to execute a setupPyenv function to enable its usage (I also give myself the ability to activate an environment via a single argument):

function setupPyenv() {
    VENV=$1
    export PATH="$HOME/.pyenv/bin:$PATH"
    eval "$(pyenv init -)"
    eval "$(pyenv virtualenv-init -)"
    [ -n "$VENV" ] && pyenv activate $VENV
}

pyvenv

To activate various Python environments in Emacs I turn to pyvenv. Since the pyenv installer puts itself in the user's home directory, we can configure pyvenv to find virtual environments in ~/.pyenv/versions via the WORKON_ON environment variable. I lean on use-package to initialize pyvenv and set the environment variable:

(use-package pyvenv
  :ensure t
  :init
  (setenv "WORKON_HOME" "~/.pyenv/versions"))

By setting the WORKON_HOME environment variable we can select which pyenv virtual environment we want to use by calling M-x pyvenv-workon. One can also call M-x pyvenv-activate to choose an environment via manual filesystem navigation.

lsp-mode

With a pyvenv environment activated in Emacs, all we have to do is call M-x lsp (after setting it up of course); lsp-mode can be configured in an init.el file with something as simple as:

(use-package lsp-mode
  :ensure t
  :commands lsp)

See the GitHub project for more details. Completion (with company-mode) and static checks (with Flymake, an Emacs builtin, or Flycheck) are easy to setup with lsp-mode.

The working virtual environment will have to have a language server installed. The easiest and fastest way to get started (a simple pip install) is to use pyls. Alternatively, one can use Microsoft's python-language-server with lsp-mode via lsp-python-ms; upon first use a prompt will ask if the user would like to download mspyls. I've used both; while mspyls has better performance, pyls support is built into lsp-mode and the server can be installed like any other Python package (the Microsoft implementation is a C# program). In my opinion those pros neutralize the performance con (which is not too bad).

Automated helper

Just about all of my Python development happens inside of a projectile project. I have a simple interactive function that will automatically activate the environment associated with a project and spin up lsp-mode. I bind this helper function to C-c C-a in the python-mode-map.

(defun dd/py-workon-project-venv ()
  "Call pyenv-workon with the current projectile project name.
This will return the full path of the associated virtual
environment found in $WORKON_HOME, or nil if the environment does
not exist."
  (let ((pname (projectile-project-name)))
    (pyvenv-workon pname)
    (if (file-directory-p pyvenv-virtual-env)
        pyvenv-virtual-env
      (pyvenv-deactivate))))

(defun dd/py-auto-lsp ()
  "Turn on lsp mode in a Python project with some automated logic.
Try to automatically determine which pyenv virtual environment to
activate based on the project name, using
`dd/py-workon-project-venv'. If successful, call `lsp'. If we
cannot determine the virtualenv automatically, first call the
interactive `pyvenv-workon' function before `lsp'"
  (interactive)
  (let ((pvenv (dd/py-workon-project-venv)))
    (if pvenv
        (lsp)
      (progn
        (call-interactively #'pyvenv-workon)
        (lsp)))))

(bind-key (kbd "C-c C-a") #'dd/py-auto-lsp python-mode-map)