Configuring Python in Emacs
Table of Contents
import re
A literate programming file for configuring Python.
Introduction
The critical part of Python integration with Emacs is running LSP in Python using . And the question to ask is if the Python we run it in Docker or in a virtual environment.
While Emacs supplies a Python editing environment, we’ll still use use-package to grab the latest:
(use-package python :after flycheck :mode (((rx ".flake8" eol) . conf-mode) ((rx "Pipfile" eol) . conf-mode) ((rx ".wsgi" eol) . python-mode)) :init (setq python-indent-guess-indent-offset-verbose nil flycheck-flake8-maximum-line-length 120) :config (setq python-shell-interpreter (or (executable-find "ipython") "python")) (flycheck-add-next-checker 'python-pylint 'python-pycompile 'append))
Keybindings
Instead of memorizing all the Emacs-specific keybindings, we use major-mode-hydra defined for python-mode:
(use-package major-mode-hydra :after python :config (defvar ha-python-eval-title (font-icons 'mdicon "run" :title "Python Evaluation")) (defvar ha-python-goto-title (font-icons 'faicon "python" :title "Python Symbol References")) (defvar ha-python-refactor-title (font-icons 'faicon "recycle" :title "Python Refactoring")) (pretty-hydra-define python-evaluate (:color blue :quit-key "C-g" :title ha-python-eval-title) ("Section" (("f" python-shell-send-defun "Function/class") ("e" python-shell-send-statement "Line") (";" python-shell-send-string "Expression")) "Entirety" (("f" python-shell-send-file "File") ("b" python-shell-send-buffer "Buffer") ("r" elpy-shell-send-region-or-buffer "Region")))) (pretty-hydra-define python-refactor (:color blue :quit-key "C-g" :title ha-python-refactor-title) ("Simple" (("r" iedit-mode "Rename")) "Imports" (("A" python-add-import "Add Import") ("a" python-import-symbol-at-point "Import Symbol") ("F" python-fix-imports "Fix Imports") ("S" python-sort-imports "Sort Imports")))) (pretty-hydra-define python-goto (:color pink :quit-key "C-g" :title ha-python-goto-title) ("Statements" (("s" xref-find-apropos "Find Symbol" :color blue) ("j" python-nav-forward-statement "Next") ("k" python-nav-backward-statement "Previous")) "Functions" (("F" imenu "Jump Function" :color blue) ("f" python-nav-forward-defun "Forward") ("d" python-nav-backward-defun "Backward") ("e" python-nav-end-of-defun "End of" :color blue)) "Blocks" (("u" python-nav-up-list "Up" :color blue) (">" python-nav-forward-block "Forward") ("<" python-nav-backward-block "Backward")))) (major-mode-hydra-define python-mode (:quit-key "C-g" :color blue) ("Server" (("S" run-python "Start Server") ("s" python-shell-switch-to-shell "Go to Server")) "Edit" (("r" python-refactor/body "Refactor...") (">" python-indent-shift-left "Shift Left") ("<" python-indent-shift-right "Shift Right")) "Navigate/Eval" (("e" python-evaluate/body "Eval...") ("g" python-goto/body "Go to...")) "Docs" (("d" python-eldoc-at-point "Docs on Symbol") ("D" python-describe-at-point "Describe Symbol")))))
Sections below can add to this with major-mode-hydra-define+.
Note: Install the following packages globally for Emacs:
pip install flake8 flake8-bugbear pylint pyright mypy pycompile black ruff ipython
But certainly add those to each project’s requirements-dev.txt file.
iPython has a feature of loading code on startup per profile. First, create it with:
ipython profile create
Next, after reading David Vujic’s Are We There Yet essay, I took a look at his Python configuration, and added the auto reloading feature to the iPython profile configuration:
c = get_config() #noqa %load_ext autoreload %autoreload 2 # c.InteractiveShellApp.extensions = ['autoreload'] # c.InteractiveShellApp.exec_lines = ['%autoreload 2']
Isolated Python Environments
While the Python community (and my work at my company) had difficulty transitioning from Python 2 to 3, I often run into issues needing a particular Python version and modules. After playing around with different approaches, I’m finding:
- Docker environments are nicely isolated, but annoying to work from outside the container
- The Builtin
venvis works well for different library modules, but not for different versions - The
pyenvdeals with different Python versions, but is overkill for library isolation
While the auto-virtualenv project attempts to resolve this, I’m using the direnv project abstraction for situations where I need project-specific isolation in more than just Python.
Virtual Environments
Use the built-in module, venv, to create isolated Python environments for specific projects, enabling you to manage dependencies separately.
Create a virtual environment, either in the project’s directory, or in a global spot:
python3 -m venv .venv
#
python3 -m venv ~/.venv/my_project/
And then activate it:
source ~/.venv/my_project/bin/activate
Or add that to a projects’ .envrc.
Now, do what you need to do with this isolation:
pip install -r test-requirements.txt
Managing Python Versions
Pyenv is a tool for managing multiple versions of Python on your machine, allowing you to switch between them easily (see this essay). On a Mac, installed it via Homebrew:
brew install readline xz brew install pyenv pyenv-virtualenv
Or on other systems, use the system Python to install pyenv globally:
pip install pyenv
Make sure we load this in the Zsh profile:
export PATH="$HOME/.pyenv/bin:$PATH" eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)"
Install the python versions you need, for instance:
pyenv install 3.9.23
Run pyenv versions to see what you have installed.
In any particular project directory, use a version you installed by creating a .python-version file, or call:
pyenv local 3.9.23
And have this in your .envrc file for use with direnv:
use python 3.7.1
Also, you need the following in your ~/.config/direnv/direnvrc file (which I have):
use_python() { local python_root=$(pyenv root)/versions/$1 load_prefix "$python_root" if [[ -x "$python_root/bin/python" ]]; then layout python "$python_root/bin/python" else echo "Error: $python_root/bin/python can't be executed." exit fi }
Tell Emacs about pyenv-mode:
(use-package pyenv-mode :config (defun setup-pyenv () "Pyenv." (setenv "WORKON_HOME" "~/.pyenv/versions") (pyenv-mode +1)))
Now specify the pyenv Python version by calling pyenv-mode-set:
M-x pyenv-mode-set
When you run inferior Python processes (like run-python), the process will start inside the specified Python installation. You can unset the current version with:
M-x pyenv-mode-unset
Or, we can do it automatically when we get into a project (if the project has a .python-version file):
(use-package pyenv-mode :config (defun project-pyenv-mode-set (&rest _) "Set pyenv version matching project name." (ignore-errors (let* ((filename (thread-first (project-current) (project-root) (file-name-concat ".python-version"))) (version (when (file-exists-p filename) (with-temp-buffer (insert-file-contents filename) (buffer-string))))) (when version (pyenv-mode-set version) (pyenv-mode-unset))))) ;; Either set/unset the pyenv version whenever changing tabs: (add-hook 'tab-bar-tab-post-select-functions 'project-pyenv-mode-set))
Docker Environment
Docker allows you to isolate your project’s environment. The downside is that you are using Docker and probably a bloated container. On my work laptop, a Mac, this creates a behemoth virtual machine that immediately spins the fans like a wind tunnel.
But, but… think of the dependencies!
Enough of the rant (I go back and forth), after getting Docker installed and running (ooo Podman … shiny), and you’ve created a Dockerfile for your project, let’s install container-env.
Your project’s .envrc file would contain something like:
CONTAINER_NAME=my-docker-container CONTAINER_WRAPPERS=(python3 pip3 yamllint) CONTAINER_EXTRA_ARGS="--env SOME_ENV_VAR=${SOME_ENV_VAR}" container_layout
Editing Python Code
Let’s integrate this Python support for evil-text-object project:
(when (fboundp 'evil-define-text-object) (use-package evil-text-object-python :hook (python-mode . evil-text-object-python-add-bindings)))
This allows me to delete a Python “block” using dal.
Unit Tests
(use-package python-pytest :after python :commands python-pytest-dispatch :init (use-package major-mode-hydra :config (defvar ha-python-tests-title (font-icons 'devicon "pytest" :title "Python Test Framework")) (pretty-hydra-define python-tests (:color blue :quit-key "q" :title ha-python-tests-title) ("Suite" (("a" python-pytest "All") ("f" python-pytest-file-dwim "File DWIM") ("F" python-pytest-file "File")) "Specific" (("d" python-pytest-function-dwim "Function DWIM") ("D" python-pytest-function "Function")) "Again" (("r" python-pytest-repeat "Repeat tests") ("p" python-pytest-dispatch "Dispatch")))) (major-mode-hydra-define+ python-mode (:quit-key "q" :color blue) ("Misc" (("t" python-tests/body "Tests..."))))))
Elpy
The Elpy Project expands on the python-mode.
(use-package elpy :init (advice-add 'python-mode :before 'elpy-enable) :config ;; (elpy-enable) (setq elpy-test-runner 'elpy-test-pytest-runner) (setq elpy-formatter 'black) (setq elpy-shell-echo-input nil) (setq elpy-modules (delq 'elpy-module-flymake elpy-modules)))
After we’ve loaded the Company section, we can add jedi to the list of completions:
(use-package elpy :after company-mode :config (add-to-list 'company-backends 'company-jedi))
Let’s expand our major-mode-hydra with some extras:
(use-package major-mode-hydra :after elpy :config (pretty-hydra-define python-evaluate (:color blue :quit-key "q" :title ha-python-eval-title) ("Section" (("F" elpy-shell-send-defun "Function") ("E" elpy-shell-send-statement "Statement") (";" python-shell-send-string "Expression")) "Entirety" (("B" elpy-shell-send-buffer "Buffer") ("r" elpy-shell-send-region-or-buffer "region")) "And Step..." (("f" elpy-shell-send-defun-and-step "Function" :color pink) ("e" elpy-shell-send-statement-and-step "Statement" :color pink)))) (pretty-hydra-define+ python-refactor nil ("Elpy" (("r" elpy-refactor-rename "Rename") ("i" elpy-refactor-inline "Inline var") ("v" elpy-refactor-extract-variable "To variable") ("f" elpy-refactor-extract-function "To function") ("a" elpy-refactor-mode "All...")))) (major-mode-hydra-define+ python-mode (:quit-key "q" :color blue) ("Server" (("s" elpy-shell-switch-to-shell "Go to Server") ("C" elpy-config "Config Elpy")) "Edit" (("f" elpy-black-fix-code "Fix/format code")) "Docs" (("d" elpy-eldoc-documentation "Describe Symbol") ("D" elpy-doc "Docs Symbol")))))
Anaconda
The anaconda-mode project seems as good as Elpy, but also include Evil keybindings.
(use-mode anaconda-mode :hook ((python-mode . anaconda-mode) (python-mode ../ anaconda-eldoc-mode)))
Since we are using
LSP Integration of Python
Dependencies
Each Python project’s requirements-dev.txt file would reference the python-lsp-server (not the unmaintained project, python-language-server):
python-lsp-server[all]
Note: This does mean, you would have a tox.ini with this line:
[tox] minversion = 1.6 skipsdist = True envlist = linters ignore_basepython_conflict = True [testenv] basepython = python3 install_command = pip install {opts} {packages} deps = -r{toxinidir}/test-requirements.txt commands = stestr run {posargs} stestr slowest # ...
Pyright
I’m using the Microsoft-supported pyright package instead. Adding this to my requirements.txt files:
pyright
The pyright package works with LSP.
(use-package lsp-pyright :hook (python-mode . (lambda () (require 'lsp-pyright))) :init (when (executable-find "python3") (setq lsp-pyright-python-executable-cmd "python3")))
Keybindings
Now that the LSP Integration is complete, we can stitch the two projects together, by calling lsp. I oscillate between automatically turning on LSP mode with every Python file, but I sometimes run into issues when starting, so I conditionally turn it on.
(defvar ha-python-lsp-title (font-icons 'faicon "python" :title "Python LSP")) (defun ha-setup-python-lsp () "Configure the keybindings for LSP in Python." (interactive) (pretty-hydra-define python-lsp (:color blue :quit-key "q" :title ha-python-lsp-title) ("Server" (("D" lsp-disconnect "Disconnect") ("R" lsp-workspace-restart "Restart") ("S" lsp-workspace-shutdown "Shutdown") ("?" lsp-describe-session "Describe")) "Refactoring" (("a" lsp-execute-code-action "Code Actions") ("o" lsp-organize-imports "Organize Imports") ("l" lsp-avy-lens "Avy Lens")) "Toggles" (("b" lsp-headerline-breadcrumb-mode "Breadcrumbs") ("d" lsp-ui-doc-mode "Documentation Popups") ("m" lsp-modeline-diagnostics-mode "Modeline Diagnostics") ("s" lsp-ui-sideline-mode "Sideline Mode")) "" (("t" lsp-toggle-on-type-formatting "Type Formatting") ("h" lsp-toggle-symbol-highlight "Symbol Highlighting") ("L" lsp-toggle-trace-io "Log I/O")))) (pretty-hydra-define+ python-goto (:quit-key "q") ("LSP" (("g" lsp-find-definition "Definition") ("d" lsp-find-declaration "Declaration") ("r" lsp-find-references "References") ("t" lsp-find-type-definition "Type Definition")) "Peek" (("D" lsp-ui-peek-find-definitions "Definitions") ("I" lsp-ui-peek-find-implementation "Implementations") ("R" lsp-ui-peek-find-references "References") ("S" lsp-ui-peek-find-workspace-symbol "Symbols")) "LSP+" (("u" lsp-ui-imenu "UI Menu") ("i" lsp-find-implementation "Implementations") ("h" lsp-treemacs-call-hierarchy "Hierarchy") ("E" lsp-treemacs-errors-list "Error List")))) (major-mode-hydra-define+ python-mode nil ("Server" (("l" python-lsp/body "LSP...")) "Edit" (("=" lsp-format-region "Format")) "Navigate" (("A" lsp-workspace-folders-add "Add Folder") ("R" lsp-workspace-folders-remove "Remove Folder")) "Docs" (("D" lsp-describe-thing-at-point "Describe LSP Symbol") ("h" lsp-ui-doc-glance "Glance Help") ("H" lsp-document-highlight "Highlight")))) (call-interactively 'lsp)) (use-package lsp-mode :config (major-mode-hydra-define+ python-mode (:quit-key "q") ("Server" (("L" ha-setup-python-lsp "Start LSP Server"))))) ;; ---------------------------------------------------------------------- ;; Missing Symbols to be integrated? ;; "0" '("treemacs" . lsp-treemacs-symbols) ;; "/" '("complete" . completion-at-point) ;; "k" '("check code" . python-check) ;; "Fb" '("un-blacklist folder" . lsp-workspace-blacklist-remove) ;; "hs" '("signature help" . lsp-signature-activate) ;; "tT" '("toggle treemacs integration" . lsp-treemacs-sync-mode) ;; "ta" '("toggle modeline code actions" . lsp-modeline-code-actions-mode) ;; "th" '("toggle highlighting" . lsp-toggle-symbol-highlight) ;; "tl" '("toggle lenses" . lsp-lens-mode) ;; "ts" '("toggle signature" . lsp-toggle-signature-auto-activate)
Project Configuration
I work with a lot of projects with my team where I need to configure the project such that LSP and my Emacs setup works. Let’s suppose I could point a function at a project directory, and have it set it up:
(defun ha-python-configure-project (proj-directory) "Configure PROJ-DIRECTORY for LSP and Python." (interactive "DPython Project: ") (let ((default-directory proj-directory)) (unless (f-exists? ".envrc") (message "Configuring direnv") (with-temp-file ".envrc" ;; (insert "use_python 3.7.4\n") (insert "layout_python3\n")) (direnv-allow)) (unless (f-exists? ".pip.conf") (message "Configuring pip") (with-temp-file ".pip.conf" (insert "[global]\n") (insert "index-url = https://pypi.python.org/simple\n")) (shell-command "pipconf --local") (shell-command "pip install --upgrade pip")) (message "Configuring pip for LSP") (with-temp-file "requirements-dev.txt" (insert "python-lsp-server[all]\n") ;; Let's install these extra packages individually ... (insert "pyls-flake8\n") ;; (insert "pylsp-mypy") ;; (insert "pyls-isort") ;; (insert "python-lsp-black") ;; (insert "pyls-memestra") (insert "pylsp-rope\n")) (shell-command "pip install -r requirements-dev.txt")))