home

Emacs, py(v)env, and lsp-mode

tags: python emacs

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 to ability to activate an environment via a single argument):

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

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
  :config
  (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. 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 personally use mspyls because it has better performance.

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.

(defun ddavis/get-pyvenv-name ()
  "grab the name of the active pyvenv (nil if not defined)"
  (when pyvenv-virtual-env
    (car (last (split-string (directory-file-name pyvenv-virtual-env) "/")))))

(defun ddavis/py-auto-lsp ()
  "turn on lsp mode in a Python project by trying to
automatically determine which pyenv virtual environment to
activate based on the project name"
  (interactive)
  (if (and pyvenv-virtual-env
           (file-directory-p pyvenv-virtual-env)
           (string= projectile-project-name (ddavis/get-pyvenv-name)))
      (lsp)
    (pyvenv-workon (projectile-project-name))
    (if (file-directory-p pyvenv-virtual-env)
        (lsp)
      (progn
        (message (format "%s does not exist, set env manually"
                         pyvenv-virtual-env))
        (call-interactively #'pyvenv-workon)
        (lsp)))))