Emacs & Python: My Setup 2018
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
- Expanding libraries I've installed locally
- Code navigation; Shift+left click on a symbol and be taken to its definition
- Code documentation; Shift+right click on a symbol and a PyDoc window will open.
- Support for Django HTML templates - not essential for everyone, but why not!
→ 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:
- If a Jedi server is running, it's stopped (via another function)
- Check commands are specified
- A path to a virtualenv is specified (this is required by Jedi, even if you don't use it.)
-
I tell Jedi to include my local Python library paths under
$HOME/.local/lib
in it's list of python paths so it will autocomplete and check those. - Finally, if it's not already there, the Jedi server is installed to it's virtualenv.
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!