#+title: Configuring Python in Emacs #+author: Howard X. Abrams #+date: 2021-11-16 #+tags: emacs python programming A literate programming file for configuring Python. #+begin_src emacs-lisp :exports none ;;; ha-programming-python --- Python configuration. -*- lexical-binding: t; -*- ;; ;; © 2021-2023 Howard X. Abrams ;; Licensed under a Creative Commons Attribution 4.0 International License. ;; See http://creativecommons.org/licenses/by/4.0/ ;; ;; Author: Howard X. Abrams ;; Maintainer: Howard X. Abrams ;; Created: November 16, 2021 ;; ;; This file is not part of GNU Emacs. ;; ;; *NB:* Do not edit this file. Instead, edit the original literate file at: ;; ~/other/hamacs/ha-programming-python.org ;; And tangle the file to recreate this one. ;; ;;; Code: #+end_src * Introduction The critical part of Python integration with Emacs is running LSP in Python using [[file:ha-programming.org::*direnv][direnv]]. 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: #+begin_src emacs-lisp (use-package python :after projectile flycheck :mode ("[./]flake8\\'" . conf-mode) :mode ("/Pipfile\\'" . conf-mode) :init (setq python-indent-guess-indent-offset-verbose nil flycheck-flake8-maximum-line-length 120) :config (when (and (executable-find "python3") (string= python-shell-interpreter "python")) (setq python-shell-interpreter "python3")) ;; While `setup.py' and `requirements.txt' are already added, I often ;; create these files for my Python projects: (add-to-list 'projectile-project-root-files "requirements-dev.txt") (add-to-list 'projectile-project-root-files "requirements-test.txt") (flycheck-add-next-checker 'python-pylint 'python-pycompile 'append)) #+end_src Note: Install the following checks: #+begin_src sh pip install flake8 pylint pyright mypy pycompile #+end_src Or better yet, add those to the =requirements-dev.txt= file. ** Virtual Environment For a local virtual machine, put the following in your =.envrc= file: #+begin_src conf layout_python3 #+end_src That is pretty slick and simple. The old way, that we still use if you need a particular version of Python, is to install [[https://github.com/pyenv/pyenv][pyenv]] globally: #+begin_src sh pip install pyenv #+end_src And have this in your =.envrc= file: #+begin_src conf use python 3.7.1 #+end_src Also, you need the following in your =~/.config/direnv/direnvrc= file (which I have): #+begin_src shell 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 } #+end_src ** Editing Python Code Let’s integrate this [[https://github.com/wbolster/evil-text-object-python][Python support for evil-text-object]] project: #+begin_src emacs-lisp (when (fboundp 'evil-define-text-object) (use-package evil-text-object-python :hook (python-mode . evil-text-object-python-add-bindings))) #+end_src This allows me to delete a Python “block” using ~dal~. ** Docker Environment Docker really 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 [[https://github.com/snbuback/container-env][container-env]]. Your project's =.envrc= file would contain something like: #+begin_src shell CONTAINER_NAME=my-docker-container CONTAINER_WRAPPERS=(python3 pip3 yamllint) CONTAINER_EXTRA_ARGS="--env SOME_ENV_VAR=${SOME_ENV_VAR}" container_layout #+end_src ** Unit Tests #+begin_src emacs-lisp (use-package python-pytest :after python :commands python-pytest-dispatch :init (ha-local-leader :keymaps 'python-mode-map "t" '(:ignore t :which-key "tests") "t a" '("all" . python-pytest) "t f" '("file dwim" . python-pytest-file-dwim) "t F" '("file" . python-pytest-file) "t t" '("function-dwim" . python-pytest-function-dwim) "t T" '("function" . python-pytest-function) "t r" '("repeat" . python-pytest-repeat) "t p" '("dispatch" . python-pytest-dispatch))) #+end_src ** Python Dependencies Each Python project's =requirements-dev.txt= file would reference the [[https://pypi.org/project/python-lsp-server/][python-lsp-server]] (not the /unmaintained/ project, =python-language-server=): #+begin_src conf :tangle no python-lsp-server[all] #+end_src *Note:* This does mean, you would have a =tox.ini= with this line: #+begin_src conf [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 # ... #+end_src *** Pyright I’m using the Microsoft-supported [[https://github.com/Microsoft/pyright][pyright]] package instead. Adding this to my =requirements.txt= files: #+begin_src conf :tangle no pyright #+end_src The [[https://github.com/emacs-lsp/lsp-pyright][pyright package]] works with LSP. #+begin_src emacs-lisp :tangle no (use-package lsp-pyright :hook (python-mode . (lambda () (require 'lsp-pyright))) :init (when (executable-find "python3") (setq lsp-pyright-python-executable-cmd "python3"))) #+end_src * LSP Integration of Python Now that the [[file:ha-programming.org::*Language Server Protocol (LSP) Integration][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 turn it on with ~, w s~. #+begin_src emacs-lisp :tangle no (use-package lsp-mode ;; :hook ((python-mode . lsp))) :config (ha-local-leader :keymaps 'lsp-mode-map "0" '("treemacs" . lsp-treemacs-symbols) "/" '("complete" . completion-at-point) "k" '("check code" . python-check) "]" '("shift left" . python-indent-shift-left) "[" '("shift right" . python-indent-shift-right) ;; actions "a" '(:ignore t :which-key "code actions") "aa" '("code actions" . lsp-execute-code-action) "ah" '("highlight symbol" . lsp-document-highlight) "al" '("lens" . lsp-avy-lens) ;; formatting "=" '(:ignore t :which-key "formatting") "==" '("format buffer" . lsp-format-buffer) "=r" '("format region" . lsp-format-region) "e" '(:ignore t :which-key "eval") "e P" '("run python" . run-python) "e e" '("send statement" . python-shell-send-statement) "e b" '("send buffer" . python-shell-send-buffer) "e f" '("send defun" . python-shell-send-defun) "e F" '("send file" . python-shell-send-file) "e r" '("send region" . python-shell-send-region) "e ;" '("expression" . python-shell-send-string) "e p" '("switch-to-shell" . python-shell-switch-to-shell) ;; folders "F" '(:ignore t :which-key "folders") "Fa" '("add folder" . lsp-workspace-folders-add) "Fb" '("un-blacklist folder" . lsp-workspace-blacklist-remove) "Fr" '("remove folder" . lsp-workspace-folders-remove) ;; goto "g" '(:ignore t :which-key "goto") "ga" '("find symbol in workspace" . xref-find-apropos) "gd" '("find declarations" . lsp-find-declaration) "ge" '("show errors" . lsp-treemacs-errors-list) "gg" '("find definitions" . lsp-find-definition) "gh" '("call hierarchy" . lsp-treemacs-call-hierarchy) "gi" '("find implementations" . lsp-find-implementation) "gm" '("imenu" . lsp-ui-imenu) "gr" '("find references" . lsp-find-references) "gt" '("find type definition" . lsp-find-type-definition) ;; peeks "G" '(:ignore t :which-key "peek") "Gg" '("peek definitions" . lsp-ui-peek-find-definitions) "Gi" '("peek implementations" . lsp-ui-peek-find-implementation) "Gr" '("peek references" . lsp-ui-peek-find-references) "Gs" '("peek workspace symbol" . lsp-ui-peek-find-workspace-symbol) ;; help "h" '(:ignore t :which-key "help") "he" '("eldoc" . python-eldoc-at-point) "hg" '("glance symbol" . lsp-ui-doc-glance) "hh" '("describe symbol at point" . lsp-describe-thing-at-point) "gH" '("describe python symbol" . python-describe-at-point) "hs" '("signature help" . lsp-signature-activate) "i" 'imenu ;; refactoring "r" '(:ignore t :which-key "refactor") "ro" '("organize imports" . lsp-organize-imports) "rr" '("rename" . lsp-rename) ;; toggles "t" '(:ignore t :which-key "toggle") "tD" '("toggle modeline diagnostics" . lsp-modeline-diagnostics-mode) "tL" '("toggle log io" . lsp-toggle-trace-io) "tS" '("toggle sideline" . lsp-ui-sideline-mode) "tT" '("toggle treemacs integration" . lsp-treemacs-sync-mode) "ta" '("toggle modeline code actions" . lsp-modeline-code-actions-mode) "tb" '("toggle breadcrumb" . lsp-headerline-breadcrumb-mode) "td" '("toggle documentation popup" . lsp-ui-doc-mode) "tf" '("toggle on type formatting" . lsp-toggle-on-type-formatting) "th" '("toggle highlighting" . lsp-toggle-symbol-highlight) "tl" '("toggle lenses" . lsp-lens-mode) "ts" '("toggle signature" . lsp-toggle-signature-auto-activate) ;; workspaces "w" '(:ignore t :which-key "workspaces") "wD" '("disconnect" . lsp-disconnect) "wd" '("describe session" . lsp-describe-session) "wq" '("shutdown server" . lsp-workspace-shutdown) "wr" '("restart server" . lsp-workspace-restart) "ws" '("start server" . lsp))) #+end_src * 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/: #+begin_src emacs-lisp (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") (unless (f-exists? ".projectile") (with-temp-file ".projectile")) (unless (f-exists? ".dir-locals.el") (with-temp-file ".dir-locals.el" (insert "((nil . ((projectile-enable-caching . t))))"))))) #+end_src * Technical Artifacts :noexport: Let's =provide= a name so we can =require= this file: #+begin_src emacs-lisp :exports none (provide 'ha-programming-python) ;;; ha-programming-python.el ends here #+end_src #+DESCRIPTION: A literate programming file for configuring Python. #+PROPERTY: header-args:sh :tangle no #+PROPERTY: header-args:emacs-lisp :tangle yes #+PROPERTY: header-args :results none :eval no-export :comments no mkdirp yes #+OPTIONS: num:nil toc:t todo:nil tasks:nil tags:nil date:nil #+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil #+INFOJS_OPT: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js