Skip to content

Instantly share code, notes, and snippets.

@doolio
Last active January 25, 2024 11:06
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save doolio/8c1768ebf33c483e6d26e5205896217f to your computer and use it in GitHub Desktop.
Save doolio/8c1768ebf33c483e6d26e5205896217f to your computer and use it in GitHub Desktop.
.dir-locals.el for use with the Emacs Eglot LSP client and python-lsp-server (pylsp) LSP server
;;; Directory Local Variables
;;; For more information see (info "(emacs) Directory Variables")
;;; Commentary:
;; .dir-locals.el for use with the Emacs Eglot LSP client and
;; python-lsp-server (pylsp) LSP server v1.10.0.
;; Default values in accordance with
;; https://github.com/python-lsp/python-lsp-server/blob/v1.10.0/CONFIGURATION.md
;; (or commit 2a5a953). A value of null means we do not set a value and
;; therefore use the plugin's default value.
;; The recommended format for the `eglot-workspace-configuration' variable
;; value is a property list (plist for short):
;;
;; (:server plist…)
;;
;; Here :server identifies a particular language server and plist is the
;; corresponding keyword-value property list of one or more parameter
;; settings for that server, serialized by Eglot as a JSON object.
;; plist may be arbitrarily complex, generally containing other
;; keyword-value property sublists corresponding to JSON subobjects.
;; The JSON values are represented by Emacs Lisp values as follows:
;; JSON | Emacs Lisp
;; ------------------------------------------------
;; :true i.e. true | t
;; :false i.e. false | :json-false
;; :null i.e. null | nil
;; :[] i.e. [] the empty array | []*
;; :{} i.e. {} the empty object | eglot-{}**
;; * Lisp array elements should not be comma separated as they are in a
;; JSON array.
;; ** Must be evaluated via a backquote or `list'
;; e.g. `(:pylsp (:plugins (:jedi (:env_vars ,eglot-{})))) or
;; (list :pylsp (list :plugins (list :jedi (list :env_vars eglot-{}))))
;;; How to Use:
;; 1. Place this file in your project directory.
;; 2. Adjust settings as desired. Inline comments below indicate the
;; expected type, possible values if appropriate and default pylsp value
;; for each configuration setting.
;;; Code:
((python-mode
. ((eglot-workspace-configuration
. (:pylsp (:configurationSources ["pycodestyle"] ; string array: ["flake8"] or ["pycodestyle"] (default)
:plugins (;; Note autopep uses some pycodestyle settings further down to avoid redefining things namely aggressive, exclude, hangClosing, ignore, maxLineLength and select
:autopep8
(:enabled t) ; boolean: true (default) or false
:flake8
(:config nil ; string: null (default)
:enabled :json-false ; boolean: true or false (default)
:exclude [] ; string array: [] (default)
:executable "flake8" ; string: "flake8" (default)
:extendIgnore [] ; string array: [] (default)
:filename nil ; string: null (default)
:hangClosing nil ; boolean: true or false; null (default)
:ignore [] ; string array: [] (default)
:indentSize nil ; integer: null (default)
:maxComplexity nil ; integer: null (default)
:maxLineLength nil ; integer: null (default)
:perFileIgnores [] ; string array: [] (default) e.g. ["file_path.py:W305,W304"]
:select nil) ; string array: null (default)
:jedi
(:auto_import_modules ["numpy"] ; string array: ["numpy"] (default)
:env_vars nil ; object: null (default)
:environment nil ; string: null (default)
:extra_paths []) ; string array: [] (default)
:jedi_completion
(:cache_for ["pandas" "numpy" "tensorflow" "matplotlib"] ; string array: ["pandas", "numpy", "tensorflow", "matplotlib"] (default)
:eager :json-false ; boolean: true or false (default)
:enabled t ; boolean: true (default) or false
:fuzzy :json-false ; boolean: true or false (default)
:include_class_objects :json-false ; boolean: true or false (default)
:include_function_objects :json-false ; boolean: true or false (default)
:include_params t ; boolean: true (default) or false
:resolve_at_most 25) ; integer: 25 (default)
:jedi_definition
(:enabled t ; boolean: true (default) or false
:follow_builtin_definitions t ; boolean: true (default) or false
:follow_builtin_imports t ; boolean: true (default) or false
:follow_imports t) ; boolean: true (default) or false
:jedit_hover
(:enabled t) ; boolean: true (default) or false
:jedi_references
(:enabled t) ; boolean: true (default) or false
:jedi_signature_help
(:enabled t) ; boolean: true (default) or false
:jedi_symbols
(:all_scopes t ; boolean: true (default) or false
:enabled t ; boolean: true (default) or false
:include_import_symbols t) ; boolean: true (default) or false
:mccabe
(:enabled t ; boolean: true (default) or false
:threshold 15) ; integer: 15 (default)
:preload
(:enabled t ; boolean: true (default) or false
:modules []) ; string array: [] (default)
:pycodestyle
(:enabled t ; boolean: true (default) or false
:exclude [] ; string array: [] (default)
:filename [] ; string array: [] (default)
:hangClosing nil ; boolean: true or false; null (default)
:ignore [] ; string array: [] (default)
:indentSize nil ; integer: null (default)
:maxLineLength nil ; integer: null (default)
:select nil) ; string array: null (default)
:pydocstyle
(:addIgnore [] ; string array: [] (default)
:addSelect [] ; string array: [] (default)
:convention nil ; string: "google", "numpy" or "pep257"; null (default)
:enabled :json-false ; boolean: true or false (default)
:ignore [] ; string array: [] (default)
:match "(?!test_).*\\.py" ; string: "(?!test_).*\\.py" (default)
:matchDir "[^\\.].*" ; string: "[^\\.].*" (default)
:select nil) ; string array: null (default)
:pyflakes
(:enabled t) ; boolean: true (default) or false
:pylint
(:args [] ; string array: [] (default)
:enabled :json-false ; boolean: true or false (default)
:executable nil) ; string: null (default)
:rope_autoimport
(:code_actions (:enabled t) ; boolean: true (default) or false
:completions (:enabled t) ; boolean: true (default) or false
:enabled :json-false ; boolean: true or false (default)
:memory :json-false) ; boolean: true or false (default)
:rope_completion
(:eager :json-false ; boolean: true or false (default)
:enabled :json-false) ; boolean: true or false (default)
:yapf
(:enabled t)) ; boolean: true (default) or false
:rope
(:extensionModules nil ; string: null (default)
:ropeFolder nil))))))) ; string array: null (default)
@david-vicente
Copy link

david-vicente commented Nov 28, 2022

Can you share an example of how to use eglot-{} in order to configure Jedi's :env_vars?

@doolio
Copy link
Author

doolio commented Nov 28, 2022

An example is provided in the commentary section already but it does not work but it should - see this discussion.

@david-vicente
Copy link

Thanks. Ignoring the fact that it is not working, do you happen to know how could we use the eglot-{} to write a json object?

Imagine I want to create the json object:

{"VIRTUAL_ENV": "/some/path"}

is it something like:

eglot-{"VIRTUAL_ENV" "/some/path"}

?

@david-vicente
Copy link

david-vicente commented Nov 28, 2022

This seems to work, even though eglot doesn't like it

(let ((myhash (make-hash-table :size 1)))
 (puthash "VIRTUAL_ENV" "/some/path" myhash)
(jsonrpc--json-encode `(:pylsp (:plugins (:jedi (:env_vars ,myhash :environment nil :extra_paths []))))))

@doolio
Copy link
Author

doolio commented Nov 28, 2022

My understanding is that eglot-{} is the Lisp object representation of the empty JSON object i.e. {}. To set a non-empty JSON object I believe we just continue to add nested plists (untested).

(:pylsp (:plugins (:jedi (:env_vars (:VIRTUAL_ENV "/some/path")))))

(From our other discussions elsewhere, perhaps you would be better served by exporting this environment variable via direnv since you use that too.)

@david-vicente
Copy link

david-vicente commented Nov 29, 2022

To set a non-empty JSON object I believe we just continue to add nested plists (untested).

You are right. This worked:

((python-mode
  . ((eglot-workspace-configuration
      . (:pylsp (:configurationSources ["pycodestyle"] 
                                       :plugins (:jedi
                                                 (:env_vars (:SOME_ENV_VAR "/some/path")
							     :environment "/env/path" ))))))))

Thanks!

@doolio
Copy link
Author

doolio commented Nov 29, 2022

Glad to hear it.

@paaguti
Copy link

paaguti commented Feb 24, 2023

A simple question:
I installed pylsp with [all] and enabled auto import with

                                                 :rope_autoimport
                                                 (:enabled t ; boolean: true or false (default)
                                                           :memory :json-false) ; boolean: true or false (default)

If I include

_,ext = os.path.splitext(file)

without import osin my code, the error is shown, but when i click on os.path it tells me that there are no code actions on the error. I was expecting auto_import to kick in.

@doolio
Copy link
Author

doolio commented Feb 24, 2023

I don't use rope myself but

I installed pylsp with [all]

Perhaps confusingly this does not install rope. See here. Are you having this issue with rope installed?

@paaguti
Copy link

paaguti commented Feb 25, 2023 via email

@doolio
Copy link
Author

doolio commented Feb 27, 2023

Right, but have you installed rope into the environment where you installed pylsp? It is not installed by default with "python-lsp-server[all]". You need to also run:

pip3 install "python-lsp-server[rope]"

@paaguti
Copy link

paaguti commented Feb 27, 2023 via email

@fast-90
Copy link

fast-90 commented May 10, 2023

For some reason using this file I get the following error:

error in process filter: run-hook-with-args: Wrong type argument: listp, :pylsp
error in process filter: Wrong type argument: listp, :pylsp

and it doesn't seem to be reading my pylsp config options. However, if I slightly change the syntax it seems to work, like so

((python-mode
  . ((eglot-workspace-configuration
      . ((:pylsp :configurationSources ["flake8"]
                                       :plugins (:jedi
                                                 (:environment "./.venv/"))))))))

Any idea why? (I'm not too familiar with elisp, my main knowledge is from looking at other configs to build my own config)

@doolio
Copy link
Author

doolio commented May 11, 2023

Any idea why?

There was an option (namely :follow_builtin_definitions) with a missing value which may have been the cause. I've updated the file to correct this oversight and to further reflect the latest version of the server (v1.7.2). Could you try again to see if that resolved your issue?

As to why your version works I can only assume it is (another) form acceptable to eglot. There was a time eglot accepted many different forms but the one presented here is now the agreed upon form to use. See for example this discussion on this topic.

@fast-90
Copy link

fast-90 commented May 12, 2023

There was an option (namely :follow_builtin_definitions) with a missing value which may have been the cause. I've updated the file to correct this oversight and to further reflect the latest version of the server (v1.7.2). Could you try again to see if that resolved your issue?

Thanks, I still have this issue although I noticed I only have the issue with python-ts-mode and not with python-mode (so your snippet works just fine as is now). Probably has something to do with how I set up eglot for python-ts-mode:

(add-to-list 'eglot-server-programs
               `(python-ts-mode . ,(eglot-alternatives
                               '(("pylsp")
                                 ("pyright-langserver" "--stdio")))))

@doolio
Copy link
Author

doolio commented May 12, 2023

Interesting, I'm still on Emacs 27.1 so haven't tried the new python-ts-mode yet. So thanks for the heads up. It may be worth opening an issue on the eglot github repository as I'm sure they would be interested to understand why this does not work for the new modes. It's likely other ts-modes are affected also. Eglot already knows about the most popular python language servers such as pylsp and pyright so there should be no need to configure that yourself. You might trying the following to configure Emacs to use the tree-sitter mode when opening a python file and then replace any python-mode-hooks you may have with python-ts-mode-hooks. So you could do away with what you have above.

(add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode))

@fast-90
Copy link

fast-90 commented Jan 8, 2024

Hey, I'm back again with a question.

I am trying to get rope_autoimport working. I just briefly tried your gist and it worked for like a while, however it suddenly stopped working for me. Everything else is working, it's just that the rope_autoimport is not working. I have rope and python-lsp-server[rope] installed in my venv, and am using the same .dir-locals.el file in your gist.

As far as I can see, I also don't see any mention of "autoimport" in my Eglot events buffers anymore (it used to be there before).

Is it working correctly for you? Do you know how I can debug this in my case?

@doolio
Copy link
Author

doolio commented Jan 8, 2024

Is it working correctly for you? Do you know how I can debug this in my case?

I don't use rope and never have. I presume you have the third party plugin pylsp-rope installed in the same venv. Is other rope functionality working as you expect?

am using the same .dir-locals.el file in your gist

In the gist rope-autoimport is not enabled as by default it is disabled in python-lsp.

:rope_autoimport
(:code_actions (:enabled t) ; boolean: true (default) or false
               :completions (:enabled t) ; boolean: true (default) or false
               :enabled t ; boolean: true or false (default)
               :memory t) ; boolean: true or false (default)

Please confirm your settings are as follows. If not update your settings as above and test again.

Can you give me a minimal example to test if it is working for me?

@fast-90
Copy link

fast-90 commented Jan 8, 2024

@doolio sure, please see below:

Virtual environment

Install the following packages in your venv: python-lsp-server[rope] rope.

.dir-locals.el

I am using your snippet with the following amendments:

                                                 :rope_autoimport
                                                 (:code_actions (:enabled t) ; boolean: true (default) or false
                                                                :completions (:enabled t) ; boolean: true (default) or false
                                                                :enabled t ; boolean: true or false (default)
                                                                :memory t) ; boolean: true or false (default)
                                                 :rope_completion
                                                 (:eager :json-false ; boolean: true or false (default)
                                                         :enabled t) ; boolean: true or false (default)

I am using python-ts-mode instead of python-mode as well, but I don't think that should matter.

script.py

In an empty file, when you type something like mat, I would expect it to show a completion menu with the built-in package math as a suggestion to auto-import. Here is an example with some screenshots.

@doolio
Copy link
Author

doolio commented Jan 8, 2024

This is what I see with your settings. There is no other text in the test file.

image

Preceded by import I see:

image

and then later

image

These screenshots suggest it is working on my side.

I see several mentions (13) of "autoimport" in my *EGLOT events* buffer.

What is it that you see?

@fast-90
Copy link

fast-90 commented Jan 8, 2024

Yes, it seems to be working for you!

My Eglot events buffer does not mention anything about rope, let alone autoimport. I’m not sure why, as I’m sure the package and plugins are installed.

Copying over my whole events buffer might be oo much, but is there anything else I could do to find out what the issue is with my setup?

@doolio
Copy link
Author

doolio commented Jan 9, 2024

OK, so I'm still on Emacs 27.1 so am not using python-ts-mode. Maybe, that is the reason we are seeing different behaviours? What is your python and eglot configurations?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment