Setup Emacs for Python development using Pyenv

Pyenv, as advertised, is a tool that lets you work with multiple versions of Python on the same machine. You could install different versions of Python such as 2.7.12 or 3.5.2 and define which one you'd like to use by executing:

> pyenv install 3.5.2
> cd /path/i/want/to/use/3.5.2/in
> pyenv local 3.5.2
> python -V

That would compile and install 3.5.2 on your machine and have it available wherever you'd like to use it. The local command tells Pyenv that you'd like to set the version of Python in the current working directory to that version. How does Pyenv do it? if you look at your directory structure

> ls -la
-rw-r--r--   1 rakan  staff     6B Sep 20  2016 .python-version

You'll notice that Pyenv places this ~.python-version~ file inside your current directory so that it can tell which python version this directory is using. If the file doesn't exist, then Pyenv will use the global Python version set.

Quite convenient isn't it? this means that you can use pip and possibly other tools to setup your project with that specific Python version. What if you had multiple projects that use the same version? you'd think you have to use virtualenv and you'd be right. Enter Pyenv-virtualenv, which will basically add an additional virtualenv command to your pyenv executable.

pyenv virtualenv 3.5.2 my-project-virtualenv-name

The above command will create a new virtualenv based on the specified Python version, but it wont set the current working directory to use that virtualenv, so you would have to be explicit by executing

pyenv local my-project-virtual-env

I am sure you noticed by now that Pyenv let's you deal with multiple Python versions and multiple virtualenvs as just different python installations that you could use each in a separate directory. So when you navigate to a directory which contains a ~.python-version~ file that contains a version number, you'll be using that specific Python installation. However, if ~.python-version~ of the current directory is a virtualenv, Pyenv will automatically activate it for you and any command you execute such as pip install X would affect that virtualenv.

Now that we covered basic Pyenv usage. How do we integrate this with Emacs? Well, let's start with setting up Python first: There are two packages on MELPA, which i know of, that provide Python-IDE experience: Elpy and Anaconda-mode. I use Elpy, so let's see how we can configure the package. I am going to show you use-package snippets and hopefully, you'll be using that already, otherwise, please consult the package documentation:

(use-package elpy
    (add-to-list 'auto-mode-alist '("\\.py$" . python-mode))
    :bind (:map elpy-mode-map
	      ("<M-left>" . nil)
	      ("<M-right>" . nil)
	      ("<M-S-left>" . elpy-nav-indent-shift-left)
	      ("<M-S-right>" . elpy-nav-indent-shift-right)
	      ("M-." . elpy-goto-definition)
	      ("M-," . pop-tag-mark))
    (setq elpy-rpc-backend "jedi"))

(use-package python
  :mode ("\\.py" . python-mode)
  (setq python-indent-offset 4)

The above elisp snippet should be straight forward. In the second use-package statement, I am loading the Python package, which is included with your Emacs installation. The only notable part about it is enabling elpy by calling that designated function. The first part is the interesting one where we load the elpy package, rebind a couple of key strokes and define some new ones, in addition to setting the rpc backend to jedi. Which means that jedi has to be installed. If you followed the above, you should be in a directory where you have a ~.python-version~ file which contains a name of a virtualenv you created, so go ahead and install the dependencies mentioned in the Quick Installation section of Elpy.

# Either of these
pip install rope
pip install jedi
# flake8 for code checks
pip install flake8
# importmagic for automatic imports
pip install importmagic
# and autopep8 for automatic PEP8 formatting
pip install autopep8
# and yapf for code formatting
pip install yapf

Great! Now if you try to use Emacs with Elpy, you'll notice that Elpy does not recognize that you've already installed the dependencies it requires into your current virtualenv because it doesn't see your virtualenv in the first place. This is where the package pyenv-mode comes into play, let's set it up:

(use-package pyenv-mode
  (add-to-list 'exec-path "~/.pyenv/shims")
  (setenv "WORKON_HOME" "~/.pyenv/versions/")
  ("C-x p e" . pyenv-activate-current-project))

use-package will make sure this package is installed for you if you don't already have it. The configuration of the package includes setting the WORKON_HOME environment variable to ~/.pyenv/versions. exec-path will also be updated to point to your ~~/.pyenv/shims~. What are those? Whenever you navigate to a directory with a ~.python-version~ file, Pyenv would read the file's content which tell Pyenv which version you are using. It will create symbolic links of the Python, Pip and potentially other executables you have installed in that specific version into those those two directories which makes it possible for us to just configure a single directory where those executables will be found.

Now what we'll need is a way to tell Emacs to update the currently activated pyenv version every time we switch projects. You'll notice in the :bind section of the above snippet that i configured C-x p e to activate current project's pyenv configuration.

(defun pyenv-activate-current-project ()
  "Automatically activates pyenv version if .python-version file exists."
   (lambda (path)
     (message path)
     (let ((pyenv-version-path (f-expand ".python-version" path)))
       (if (f-exists? pyenv-version-path)
            (let ((pyenv-current-version (s-trim (f-read-text pyenv-version-path 'utf-8))))
              (pyenv-mode-set pyenv-current-version)
              (message (concat "Setting virtualenv to " pyenv-current-version))))))))

The above code base, would traverse the directories starting from the current buffer's directory all the way up to root looking for the ~.python-version~ file. If it finds this file, it reads the content and set's both pyenv-mode and pyvenv mode to use that version. At the end, it'll emit a message saying that the virtualenv was set to the version found when you press that keystroke C-x p e.

In addition to that, we also need to activate the global version when we load Emacs.

(defvar pyenv-current-version nil nil)

(defun pyenv-init()
  "Initialize pyenv's current version to the global one."
  (let ((global-pyenv (replace-regexp-in-string "\n" "" (shell-command-to-string "pyenv global"))))
    (message (concat "Setting pyenv version to " global-pyenv))
    (pyenv-mode-set global-pyenv)
    (setq pyenv-current-version global-pyenv)))

(add-hook 'after-init-hook 'pyenv-init)

Which will initialize pyenv to use the global version at initialization.

Once you have this code in your Emacs configuration, You'll have a working setup for this amazing Pyenv package as well as Elpy.

Update(02.10.2017): As @componaut mentioned in the comment, the function locate-dominating-file can be used instead of f-traverse-upwards. This makes pyenv-activate-current-project look as follows:

(defun pyenv-activate-current-project ()
  "Automatically activates pyenv version if .python-version file exists."
  (let ((python-version-directory (locate-dominating-file (buffer-file-name) ".python-version")))
    (if python-version-directory
        (let* ((pyenv-version-path (f-expand ".python-version" python-version-directory))
               (pyenv-current-version (s-trim (f-read-text pyenv-version-path 'utf-8))))
          (pyenv-mode-set pyenv-current-version)
          (message (concat "Setting virtualenv to " pyenv-current-version))))))

Enjoy Emacs!

comments powered by Disqus