Editing with Emacs: Python - Part 2

Posted:

This is a follow-up to my earlier post about editing python with Emacs, as well as the start of a series on using Emacs effectively with a number of languages. I'll go into what I use for editing python with Emacs and why, as well as how I set it all up. Let's go!

If you want the satisfaction of putting together a fine python editor, that is also capable of serving as a sophisticated editor for many many other languages, with the added benefit of having at least some control over how it all works, then read on! You just might learn something ...

Goals

Before I get into too many specifics, let's look at what we actually want to achieve here:

These things are already provided out of the box by other editors/IDEs and frankly some of them do it quite well. So why bother?

As with many things that I do you really must care about DIY to get the most out of it; if you're looking to just edit python efficiently as quickly as possible, then this post might not be for you.

A quick note about installing Emacs packages ...

I use, greatly prefer, and strongly recommend John Weigley's excellent use-package macro for installing and managing my Emacs packages. It helps me keep configurations clean, prevents Emacs from loading all-the-things, all the time, and just feels better than a massive cluttered init.el.

Of course, it's 100% optional, but this and every other one of my writings will be written as a use-package user! Keep that in mind as you read on, as the below Emacs-lisp snippets assume you are one too!

Syntax highlighting

This one's debatable; There are several pieces I've read that suggest the author prefers and is more productive with the coloring off [1] [2]. I won't editorialize here, skip ahead if need be.

This one's also subjective. The general look and coloring of your editor is something that belongs totally to you, and you should spend some time checking out your options. MELPA has a ton of themes - start there! I personally use the Lush theme after going through most if not all of the themes on MELPA at some point or another.

Syntax checking

With syntax checking, you'll receive warning and error notifications in-line in your code - updated when you save your file.

Flycheck

Flycheck is a general syntax checker for Emacs. Just by installing it, you will not only get your desired python syntax checking - you'll get it for a host of other languages as well [3]. One of the single most useful packages for Emacs, in my opinion.

The following snippet will get Flycheck installed and set up with some handy yet optional keybindings:

(use-package flycheck
  :ensure t
  :bind
  (("C-c e n" . flycheck-next-error)
   ("C-c e p" . flycheck-previous-error))
  :config
  (add-hook 'after-init-hook #'global-flycheck-mode))

Code completion

Code completion is another feature that some folks don't care for; I find it extremely useful, and it is supported quite well with python in Emacs once you get a few packages to handle the task.

Company mode

Company is a general completion framework for Emacs, and is compatible with many languages via backends [4]. It should be perfectly usable out of the box, but I've made a few tweaks that you can apply with the below snippet:

(use-package company
  :ensure t
  :config
  (add-hook 'after-init-hook 'global-company-mode)
  (setq
    company-echo-delay 0
    company-idle-delay 0.2
    company-minimum-prefix-length 1
    company-tooltip-align-annotations t
    company-tooltip-limit 20)
    ;; Default colors are awful - borrowed these from gocode (thanks!):
    ;; https://github.com/nsf/gocode/tree/master/emacs-company#color-customization
    (set-face-attribute
     'company-preview nil :foreground "black" :underline t)
    (set-face-attribute
     'company-preview-common nil :inherit 'company-preview)
    (set-face-attribute
     'company-tooltip nil :background "lightgray" :foreground "black")
    (set-face-attribute
     'company-tooltip-selection nil :background "steelblue" :foreground "white")
    (set-face-attribute
     'company-tooltip-common nil :foreground "darkgreen" :weight 'bold)
    (set-face-attribute
     'company-tooltip-common-selection nil :foreground "black" :weight 'bold))

Although the (add-hook) call is necessary for the functionality we want, the above (setq)'d variables are optional; feel free to tweak them to your liking.

Jedi

The last piece we need is Jedi, which provides rich autocmplete and static analysis (to compliment Flycheck.) First, make sure you've got virtualenv installed. I don't use this, but Jedi wants it. On Void Linux:

# xbps-install python-virtualenv

The # at the start means this should be run with elevated privileges. Note that the process for installing virtualenv may vary on your OS.

Now, use pip to install some packages that will be needed:

$ pip install flake8 jedi

If you've got Python installed via pyenv, then pip can be ran as your normal user, without elevated privileges. Hence the $ at the start of the above snippte. Note that system-installed pips may require elevated privileges to install these.

Per the Jedi readme [5], we won't be using the jedi package. We'll instead use the company-jedi package to add jedi as a company backend. Add this to your init.el:

(use-package company-jedi
  :defer t
  :ensure t
  :init
  (setq jedi:complete-on-dot t)
  (setq jedi:get-in-function-call-delay 0.2))

This will ensure you've got company-jedi, as well as configure Jedi a bit. Now we just need to hook it into python-mode:

(use-package python-mode
  :bind
  ("<S-down-mouse-1>" . goto-definition-at-point)
  ("<S-down-mouse-3>" . quick-pydoc)
  :functions jedi:goto-definition
  :init
  (setq-default python-shell-completion-native-enable nil)

  (defun goto-definition-at-point (event)
    "Move the point to the clicked position
     and jedi:goto-definition the thing at point."
    (interactive "e")
    (let ((es (event-start event)))
      (select-window (posn-window es))
      (goto-char (posn-point es))
      (jedi:goto-definition)))

  (defun quick-pydoc (event)
    "Move the point to the clicked position
     and pydoc the thing at point."
    (interactive "e")
    (let ((es (event-start event)))
      (select-window (posn-window es))
      (goto-char (posn-point es))
      (pydoc-at-point)))

  (add-hook 'python-mode-hook
            (lambda ()
              (when (derived-mode-p 'python-mode)
                (add-to-list 'company-backends 'company-jedi)))))

This gives us a few things:

  1. Shift-left click to go to the definition of what's at point.
  2. Shift-right click to get the Python documentation for what's at point.
  3. When python-mode starts, it'll also start up Jedi.el (our auto-complete.)

Support multiple Python versions

Jedi.el is now set up, but it would be great if one could easily swap between Python versions installed with pyenv. This can be achieved by adding a few functions. The updated python-mode section now looks like:

(use-package python-mode
  :bind
  ("<S-down-mouse-1>" . goto-definition-at-point)
  ("<S-down-mouse-3>" . quick-pydoc)
  :functions jedi:goto-definition jedi:stop-server maybe-stop-jedi-server
  :init
  (setq-default python-shell-completion-native-enable nil)

  (defun goto-definition-at-point (event)
    "Move the point to the clicked position
     and jedi:goto-definition the thing at point."
    (interactive "e")
    (let ((es (event-start event)))
      (select-window (posn-window es))
      (goto-char (posn-point es))
      (jedi:goto-definition)))

  (defun maybe-stop-jedi-server ()
    "Stop the Jedi server, if need me."
    (if (boundp 'jedi:stop-server)
        (jedi:stop-server)))

  (defun quick-pydoc (event)
    "Move the point to the clicked position
     and pydoc the thing at point."
    (interactive "e")
    (let ((es (event-start event)))
      (select-window (posn-window es))
      (goto-char (posn-point es))
      (pydoc-at-point)))

  (defun use-pyenv352 ()
    "Configure Jedi to use a pyenv-provided Python 3.5.2."
    (interactive)
    (maybe-stop-jedi-server)
    (let ((pyenv352 (concat my-home "/.pyenv/versions/3.5.2")))
      (setq
       jedi:environment-virtualenv (list (concat pyenv352 "/bin/pyvenv-3.5"))
       jedi:environment-root (concat dot-emacs "/.py/352")
       jedi:server-args
       '("--sys-path" "~/.pyenv/versions/3.5.2/lib/python3.5/site-packages"))
      (if (not (file-exists-p
                (concat jedi:environment-root
                        "/lib/python3.5/site-packages/jediepcserver.py")))
          (jedi:install-server))))

  (defun use-pyenv2712 ()
    "Configure Jedi to use a pyenv-provided Python 2.7.12."
    (interactive)
    (maybe-stop-jedi-server)
    (let ((pyenv2712 (concat my-home "/.pyenv/versions/2.7.12")))
      (setq
       jedi:environment-virtualenv (list (concat pyenv2712 "/bin/virtualenv"))
       jedi:environment-root (concat dot-emacs "/.py/2712")
       jedi:server-args
       '("--sys-path" "~/.pyenv/versions/2.7.12/lib/python2.7/site-packages"))
      (if (not (file-exists-p
                (concat jedi:environment-root
                        "/lib/python2.7/site-packages/jediepcserver.py")))
          (jedi:install-server))))

  (add-hook 'python-mode-hook 'use-pyenv352)
  (add-hook 'python-mode-hook
            (lambda ()
              (when (derived-mode-p 'python-mode)
                (add-to-list 'company-backends 'company-jedi)))))

And these defined earlier in the file:

(defconst dot-emacs "~/.emacs.d")
(defconst my-home (getenv "HOME"))
(defconst my-bin (concat my-home "/bin"))
(defconst my-src (concat my-home "/src"))

This enables a few things:

  1. The jedi server will be automatically installed the first time a python file is opened, if it is not already.
  2. Python versions can be switched, on the fly, with a simple M-x use-pyenvN.

Now we've got all features available to any Python version we need.

Initiate a build from within Emacs

I love Makefiles, and in this case they are a handy tool for hooking build capabilities into Emacs. Given the following helper function and binding in your init.el:

(defun build-project ()
  "Compile the current project."
  (interactive)
  (setq-local compilation-read-command nil)
  (call-interactively 'compile))

(global-set-key (kbd "<f5>") 'build-project)

... and a properly-configured Makefile, you can simply hit F5 within Emacs to fire off your build (or do any other arbitrary task!)

Conclusion

This is technically a "repost" for me, but I really wanted to go into more detail and correct some mistakes/oversights from my last post - an errata of sorts but more of a part deux.

All of the above and more are fully laid out in my personal init.el, so be sure to check that out. I plan to put up a few asciinema clips of all this in action, so stay tuned...

References

  1. https://www.robertmelton.com/2016/04/10/syntax-highlighting-off/
  2. http://www.linusakesson.net/programming/syntaxhighlighting
  3. http://www.flycheck.org/
  4. https://company-mode.github.io/
  5. https://github.com/tkf/emacs-jedi/#company-users
This page was last modified on: 2020-07-26