Emacs & Python: My Setup 2018

Posted: | Updated:

Earlier this year I did an expansion to my entries on MPD, sort of a state of my setup piece, and I've decided to do the same thing for my Emacs and Python setups as well. If you write Python code and use Emacs, or are interested in either, read on!

The system: Void Linux

My operating system environment is Void Linux and has been for over four years now. At the OS-level, I've installed just a few Python 3 packages to get started:

python3             # Gives me the python interpreter
python3-pip         # Allows installation of packages
python3-setuptools  # Wanted by pip

Pip packages: local style

Personally, I don't love installing python packages I need for development through the system package manager. It's definitely appropriate for libraries needed by other system packages and things like that, but for my local dev environment I do something else.

For my login account, "hristos", I've added $HOME/.local/bin to my $PATH. Additionally, when I want to install a package via pip, I invoke it like this:

$ pip install --user ansible beets django httpie pyflakes flake8 ipython jedi selenium uWSGI

This --user flag installs the package to $HOME/.local so now all I need to do is configure my editor to look there for Python libraries. A nice benefit of doing things this way is you don't need superuser access, and you aren't messing with the system python in any way.

Anyways, in the manner seen above, you would install at minimum these packages: pyflakes, flake8, and jedi.

The editor: Emacs

Emacs being my editor of choice, I'll be using various plugins to get the functionality to where I want it. This includes:

Code completion + local libraries

Completion is enabled with two primary packages: Company mode and company-jedi. Company mode itself comes with Emacs, but follow the link to see an example of installing company-jedi with the excellent use-package.

But this isn't enough to enable completion for libraries you install with pip. To do this we need to tell Jedi about our local python libraries under $HOME/.local/lib, the below snippet is how this looks in my init.el at the time of this writing:

(defun use-system-python3 ()
    "Use the system python3."
    (interactive)
    (maybe-stop-jedi-server)
    (defvar python-check-command)
    (defvar python-shell-interpreter)
    (setq
       python-check-command "pyflakes"
       python-shell-interpreter "python3"
       flycheck-python-flake8-executable "flake8"
       jedi:environment-virtualenv (list "python -m venv")
       jedi:environment-root (concat dot-emacs "/.py/system3")
       jedi:server-args
       '("--sys-path" "/usr/lib/python3.6/site-packages"
         "--sys-path" "~/.local/lib/python3.6/site-packages"))
      (if (not (file-exists-p
                (concat jedi:environment-root
                        "/lib/python3.6/site-packages/jediepcserver.py")))
          (jedi:install-server)))

The above function is ran as a python-mode hook and enables several things:

You may notice that absolute paths are not used for the various binaries I've set, and that's deliberately. The idea is, let $PATH do it's job and find the right thing. This lets me use locally installed binaries ahead of anything that may be installed at the system-level.

Code navigation

It's often useful to go to the definition of a particular symbol to learn more about it and whatnot. The below function is mapped to Shift+left click to make this easy:

  (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)))

My use-package entry for python-mode then looks like this:

(use-package python-mode
  :bind
  ("<S-down-mouse-1>" . goto-definition-at-point)
  ... Truncated ...

Code documentation

In the spirit of empowering Emacs to give me everything I want, the below function, bound to shift+right click, will open a buffer with the PyDoc for the given symbol:

  (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)))

And again, my use-package for python mode looks like this:

(use-package python-mode
  :bind
  ("<S-down-mouse-1>" . goto-definition-at-point)
  ("<S-down-mouse-3>" . quick-pydoc)
  ... Truncated ...

Support for Django HTML templates

To enable auto-expanding of special HTML template characters like {{ and {% I use the excellent web-mode. But installing web-mode is not enough, and if you use smartparens some additional configuration is needed.

Because not every HTML file I edit is a Django template, I use a project-local .dir-locals.el file and set the following inside:

((web-mode (eval . (web-mode-set-engine "django"))))

This goes in the root of my project and automatically sets the web-mode engine to "django" for all html files in the project's sub-folders.

The Service

I won't go into much detail here, see this entry for more, but the final part of my setup involves an Emacs daemon runit service.

In my runit service file for Emacs, I set a few environment variables to ensure Emacs has the same $PATH as my user, and other things. Anything specific about one's environment needs to be set here since runit doesn't pass much of an environment to services (see here for more information on that.) Below is what my service file looks like:

#!/bin/sh
export EMACS_GO=true
export HOME=/home/hristos
export PATH=$HOME/.local/bin:$PATH
cd $HOME
exec chpst -u hristos:hristos /usr/bin/emacs --fg-daemon=hristos-emacsd 2>&1

Nothing special here, just setting up the environment as needed.

Bonus!

There's many more awesome tidbits that come along with this setup -- to see things like python-specific keybindings: with a python-mode buffer open, run M-x RET describe-mode RET and Emacs will show you some of those details and much more.

Conclusion

The tools described above help me be a more productive Python programmer. It's not a comprehensive writeup of my entire workflow, just the main parts that make writing Python more awesome.

One could take this a step further and utilize packages like python-django.el which provide much deeper project integration. I personally use a different pattern for this but it's a nice way to manage a project when you want to keep everything within Emacs.

In the future, I may do a more in depth entry on my Django workflow, or other more specific things. Until then, happy hacking!

This page was last modified on: 2020-07-26