#+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 <http://gitlab.com/howardabrams>
  ;; 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:
  ;;            ~/src/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 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"))

    (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")))
#+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
