Skip to main content

Python LSP server Jedi environment in Emacs Eglot

I've moved onto using LSP in Emacs for all my programming stuff instead of individual packages for each language. I'm using eglot as the LSP client and python-lsp-server as the Python LSP server and it's been working good so far with a few gotchas.

One of the gotchas I've faced is in the jedi plugin of python-lsp-server, more specifically in the environment configuration key. This key is supposed to point to the virtual environment path to find the completion candidates.

In my workflow, I use separate virtual environments for every project and usually create them in the root of the project with name venv/. So, my understanding was that if I put value to the key pylsp.plugins.jedi.environment to the relative path"venv/", it should find the respective virtual enviroment for each project and everything will work as expected. This way I don't have to find the absolute path for each and, can also get away with just using the same relative path as all of the projects have the same virtual enviroment directory name. (I also have venv/ in my global .gitignore for this exact scenario.)

And that failed!

The failure was not specifically due to how python-lsp-server manages the value but one can easily argue that it's definitely something that should/could be handled by python-lsp-server (see the alternative option near the end).

Now, the imminent reason the above failed is due to how eglot (more specifically project.el used by eglot for this) finds the root of the project. In summary, it looks for different project root markers e.g. version control config dir (e.g. .git/, .svn/). Being a heavy user of git, most of my stuff are version-controlled. But in this case I was working on a short-lived throwaway test project to make something quick so I didn't bother about putting the project under git. As a result, after invoking eglot, got a whole bunch of errors on the python-lsp-server process. After debugging, it was clear that eglot was not using the project directory as the project root, rather the project root was set to my home directory. eglot also sets the project root (found by project.el) as the default-directory buffer-local variable and I found it to be pointing to ~.


The fix to this consists of two steps -- the first involves project.el and the second eglot. (The following changes are added to my Emacs init file.)

Step 1 (project.el):

We need to add some extra project-root marker(s) for project. This can be done by setting the project-vc-extra-root-markers variable to a list of desired values e.g. I've set it currently to:

(setq project-vc-extra-root-markers '("requirements.txt" "venv"))

So essentially, if there is a requirements.txt or venv file (directory is a file object as well in *nix), project.el will treat the containing (parent) directory as the project root and eglot in turn will set that project root as the value of variable default-directory.

Step 2 (eglot):

Next, we need to find a way to dynamically generate LSP workspace config. To do that, we can set the default value of the [eglot-workspace-configuration][12c] directory-local variable to a function that would be called to get the workspace config for the LSP server. It takes the LSP server instance as it's only argument.

Following is the truncated code containing only the jedi.enviroment config key only. Essentially, we're checking whether there's a venv directory. If exists, pass that absolute path to that as the value of jedi.enviroment.

(setq-default
 eglot-workspace-configuration
 (defun local-eglot-workspace-configuration-function (server)
   (let* ((project-root (expand-file-name default-directory))
      (venv-dir (file-name-concat project-root "venv"))
      (jedi-environment
       (cond
        ((file-directory-p venv-dir) venv-dir)
        (nil nil))))

     `(:pylsp
       (:plugins
        (:jedi (:environment ,jedi-environment)))))))

As an alternative approach, this can also be achieved through the python-lsp-server code. In the file pylsp/workspace.py, right after this line, we can have a check if the jedi.environment key points to a relative path. If so, prefix the path with the parent directory of the opened file. For example, something like the following should do:

if environment_path and os.name != "nt" and not environment_path.startswith("/"):
    environment_path = os.path.join(os.path.dirname(self.path), environment_path)

I was thinking about doing some ugly monkey-patching to do the above at one point but after finding the solution using project.el and eglot, there was only one way to go!


References:

  1. LSP
  2. eglot
  3. python-lsp-server
  4. jedi
  5. python-lsp-server configuration keys
  6. project.el
  7. git
  8. .gitignore
  9. pylsp/workspace.py
  10. default-directory
  11. project-vc-extra-root-markers
  12. eglot-workspace-configuration
  13. Emacs init file

Comments

Comments powered by Disqus