Hook to switch the linter binaries in Emacs Lisp according to virtual environment

Hook to switch the linter binaries in Emacs Lisp according to virtual environment



Problem



I use Emacs for Python development along with several linters. When I activate a Python virtual environment (venv) from within Emacs, I would like to set the linter binaries according to the following rules:


nil



For example, let's say I activate a venv called my_venv that has pylint installed, but no flake8. flake8 is however installed in my default linters venv. After activating my_venv, the linters that will be used are


my_venv


pylint


flake8


flake8


linters


my_venv



The reason for implementing this is that I develop Python on multiple machines that share a single, version-controlled init.el file. I do not want to guarantee that I have the same Python binaries and venvs across these machines; this implementation helps decouple my Emacs setup from the Python venvs that are present on a machine.


init.el


init.el



Code


(defvar linter-execs '((flycheck-python-flake8-executable "bin/flake8")
(flycheck-python-pylint-executable "bin/pylint")
(flycheck-python-pycompile-executable "bin/python")))
(defvar default-linter-venv-path (concat (getenv "WORKON_HOME") "/linters/"))

(defun switch-linters ()
"Switch linter executables to those in the current venv.

If the venv does not have any linter packages, then they will be
set to those in the `default-linter-venv-path` venv. If these do
not exist, then no linter will be set."
(dolist (exec linter-execs)
(let ((venv-linter-bin (concat pyvenv-virtual-env (nth 1 exec)))
(default-linter-bin (concat default-linter-venv-path (nth 1 exec)))
(flycheck-var (nth 0 exec)))
(cond ((file-exists-p venv-linter-bin)
(set flycheck-var venv-linter-bin))
((file-exists-p default-linter-bin)
(set flycheck-var default-linter-bin))
(t (set flycheck-var nil))))))

(add-hook 'pyvenv-post-activate-hooks 'switch-linters)


linter-execs


flycheck


$WORKON_HOME/linters


switch-linters


pyvenv-post-activate-hooks


nil


(t (set flycheck-var nil))



Specific questions


linter-execs




1 Answer
1



It would be a good idea to write a docstring for the defvar'd variables like linter-execs. The explanations in the post would make an excellent start, for example:


defvar


linter-execs


(defvar linter-execs '((flycheck-python-flake8-executable "bin/flake8")
(flycheck-python-pylint-executable "bin/pylint")
(flycheck-python-pycompile-executable "bin/python"))
"The linter executables, as list of two-element lists. The
first element of an entry is the flycheck variable that contains
the path of a linter executable. The second element is the
relative path of the executable from within the venv.")



Writing (nth 0 exec) and (nth 1 exec) makes it hard to understand the meaning of these items of data. In Python you'd use tuple unpacking to give meaningful names to each element of the exec data structure, like this:


(nth 0 exec)


(nth 1 exec)


exec


flycheck_var, path = exec



In Emacs Lisp you can use cl-destructuring-bind in a similar way:


cl-destructuring-bind


(dolist (exec linter-execs)
(cl-destructuring-bind (flycheck-var path) exec
(let ((venv-linter-bin (concat pyvenv-virtual-env path))
;; etc



But exec comes from a list that you are looping over, so you could use the cl-loop macro instead of dolist, and cl-loop has destructuring built in:


exec


cl-loop


dolist


cl-loop


(cl-loop for (flycheck-var path) in linter-execs do
(let ((venv-linter-bin (concat pyvenv-virtual-env path))
;; etc



You'll need (require 'cl-macs) to use cl-destructuring-bind or cl-loop.


(require 'cl-macs)


cl-destructuring-bind


cl-loop



The two directories pyvenv-virtual-env and default-linter-venv-path are treated in exactly the same way: first we do (let ((Y (concat X path)) and then (file-exists-p Y) and finally (set flycheck-var Y). This repetition could be factored out into a loop:


pyvenv-virtual-env


default-linter-venv-path


(let ((Y (concat X path))


(file-exists-p Y)


(set flycheck-var Y)


(defun switch-linters ()
"Switch linter executables to those in the current venv.

If the venv does not have any linter packages, then they will be
set to those in the `default-linter-venv-path` venv. If these do
not exist, then the linter will be set to nil."
(cl-loop with dirs = (list pyvenv-virtual-env default-linter-venv-path)
for (flycheck-var path) in linter-execs
do (set flycheck-var
(cl-loop for directory in dirs
for executable = (concat directory path)
if (file-exists-p executable) return executable)))



Instead of file-exists-p, you probably want to use file-executable-p.


file-exists-p


file-executable-p



Looking for a file in a list of directories is built into Emacs as the function locate-file. Using this, we get:


locate-file


(defun switch-linters ()
"Switch linter executables to those in the current venv.

If the venv does not have any linter packages, then they will be
set to those in the `default-linter-venv-path` venv. If these do
not exist, then the linter will be set to nil."
(cl-loop with dirs = (list pyvenv-virtual-env default-linter-venv-path)
for (flycheck-var path) in linter-execs
do (set flycheck-var (locate-file path dirs nil 'file-executable-p))))



Thanks for contributing an answer to Code Review Stack Exchange!



But avoid



Use MathJax to format equations. MathJax reference.



To learn more, see our tips on writing great answers.



Required, but never shown



Required, but never shown




By clicking "Post Your Answer", you agree to our terms of service, privacy policy and cookie policy

Popular posts from this blog

𛂒𛀶,𛀽𛀑𛂀𛃧𛂓𛀙𛃆𛃑𛃷𛂟𛁡𛀢𛀟𛁤𛂽𛁕𛁪𛂟𛂯,𛁞𛂧𛀴𛁄𛁠𛁼𛂿𛀤 𛂘,𛁺𛂾𛃭𛃭𛃵𛀺,𛂣𛃍𛂖𛃶 𛀸𛃀𛂖𛁶𛁏𛁚 𛂢𛂞 𛁰𛂆𛀔,𛁸𛀽𛁓𛃋𛂇𛃧𛀧𛃣𛂐𛃇,𛂂𛃻𛃲𛁬𛃞𛀧𛃃𛀅 𛂭𛁠𛁡𛃇𛀷𛃓𛁥,𛁙𛁘𛁞𛃸𛁸𛃣𛁜,𛂛,𛃿,𛁯𛂘𛂌𛃛𛁱𛃌𛂈𛂇 𛁊𛃲,𛀕𛃴𛀜 𛀶𛂆𛀶𛃟𛂉𛀣,𛂐𛁞𛁾 𛁷𛂑𛁳𛂯𛀬𛃅,𛃶𛁼

Edmonton

Crossroads (UK TV series)