Python
python-pet
Using https://github.com/wyuenho/emacs-pet. Switched from elpy, which is on life support, to a more modern stack with LSP. I don't fully understand how it works yet, but it seems to mostly just work.
Basically, for any Python project, pet detects whether and where the virtualenv is. I used to have a bunch of hacky functions to do this manually, but pet takes care of this now. Then, once this is figured out, it tries to find the language server and linter in the virtualenv (or system env if that is what we are working with).
The language server I am using is jedi-language-server using lsp-jedi. The main linter I am using is ruff using flycheck-ruff, but other linters (like flake8) are also supported with a little bit of massaging of the configuration. To set this up, the language server and linter must be present in the environment.
So for example, say we are working with uv, we could use the uv pip install or uv add (what's the difference?) commands to add the lang server and linter to the environment:
# using uv pip install jedi-language-server ruff
For system python, we can imagine installing these using the system package manager or pip. After this, everything should magically work.
(use-package lsp-jedi :ensure t :after lsp) (use-package lsp-pyright :ensure t :after lsp) (use-package ruff-format :ensure t) (use-package python-isort :ensure t) (use-package pet :ensure t :config (add-hook 'python-mode-hook (lambda () (setq-local python-shell-virtualenv-root (pet-virtualenv-root) python-shell-interpreter (if-let ((ipython (pet-executable-find "ipython"))) ipython (pet-executable-find "python"))) ;; flycheck (pet-flycheck-setup) (flycheck-mode) ;; lsp (setq-local lsp-jedi-executable-command (pet-executable-find "jedi-language-server")) (setq-local lsp-pyright-python-executable-cmd python-shell-interpreter lsp-pyright-venv-path python-shell-virtualenv-root) ;; for some reason, need to do this like this. I want the flake8 ;; checker to run. can add any other checkers after LSP like ;; this. (add-hook 'lsp-diagnostics-mode-hook (lambda () (flycheck-add-next-checker 'lsp 'python-ruff)) nil t) (lsp) ; start lsp (setq-local dap-python-executable python-shell-interpreter))) ;; format on save, but only if we explicitly allow it with a dir locals variable (add-hook 'hack-local-variables-hook (lambda () (when (equal major-mode 'python-mode) (unless (bound-and-true-p do.python/no-fmt-on-save) (when-let ((ruff-executable (pet-executable-find "ruff"))) (setq-local ruff-format-command ruff-executable) (ruff-format-on-save-mode)) (when-let ((black-executable (pet-executable-find "black"))) (setq-local python-black-command black-executable) (python-black-on-save-mode)) (when-let ((isort-executable (pet-executable-find "isort"))) (setq-local python-isort-command isort-executable) (python-isort-on-save-mode)))))))
The below sections are how I used to do these things, but I don't recommend using any of this stuff. Will be deleted soon.
elpy
THIS SECTION IS BEING DEPRECATED, THIS NO LONGER WORKS
Apparently, elpy is the way to go when I want to do Python development in emacs. We configure it here.
Elpy automatically activates highlight-identation-mode, which is quite ugly
in my opinion. It also uses flymake instead of flycheck by default. So let's
only activate the modules we actually want.
(use-package elpy :ensure t :commands (elpy-enable elpy-disable) :init (elpy-enable) ;; autoformat on save (defun do.python/toggle-autoformat () "Toggle yapf autoformat on save." (interactive) (make-variable-buffer-local 'before-save-hook) (if (member #'elpy-yapf-fix-code before-save-hook) (progn (remove-hook 'before-save-hook #'elpy-yapf-fix-code) (message "Disabled yapf autoformat.")) (add-hook 'before-save-hook #'elpy-yapf-fix-code) (message "Enabled yapf autoformat."))) (defun do.python/enable-autoformat () "Enable yapf autoformat on save" (make-variable-buffer-local 'before-save-hook) (add-hook 'before-save-hook #'elpy-yapf-fix-code) (message "Enabled yapf autoformat.")) (add-hook 'python-mode-hook #'do.python/enable-autoformat) (defun do.python/send-region-or-group (&optional arg) "Send the active region or the current group to the Python shell." (interactive "P") (if (use-region-p) (elpy-shell-send-region-or-buffer arg) (elpy-shell-send-group arg))) :bind (:map elpy-mode-map ("C-c C-c" . do.python/send-region-or-group)) :config (setq elpy-rpc-backend "jedi") (setq elpy-modules (quote (elpy-module-company elpy-module-eldoc elpy-module-pyvenv elpy-module-yasnippet elpy-module-sane-defaults))))
Windows
THIS SECTION IS BEING DEPRECATED, THIS NO LONGER WORKS
On windows, I use the conda package manager. To make this work with elpy, I need some extra configuration, including the conda.el package.
(when on-windows (defvar do.python.windows/anaconda-home "" "The path to the anaconda home directory. Usually C:\\Users\\username\\anaconda3.") (defvar do.python.windows/emacs-env "emacs" "The name of the default conda env to use for emacs. This cannot be 'base'. To make an env called 'emacs', do: conda create --name emacs python=3.8") ;; we use conda.el to set up PATH and enable the rest of the user-specified "emacs" environment. (use-package conda :ensure t :after elpy :init (setq conda-anaconda-home do.python.windows/anaconda-home) (setq conda-env-home-directory do.python.windows/anaconda-home) (conda-env-initialize-eshell) (conda-env-activate "emacs") (let ((emacs-venv-path (concat (file-name-as-directory do.python.windows/anaconda-home) "\\envs\\" do.python.windows/emacs-env))) ;; make this work with elpy (setq elpy-rpc-virtualenv-path emacs-venv-path) (pyvenv-activate emacs-venv-path))))
Inferior python
THIS SECTION IS BEING DEPRECATED, THIS NO LONGER WORKS
The inferior python shell is set in my site file. Here I just set the hooks and enable font locking.
(add-hook 'inferior-python-mode-hook 'no-trailing-whitespace) (add-hook 'inferior-python-mode-hook '(lambda () (setq-local ml-interactive? t))) ;; from https://elpy.readthedocs.io/en/latest/customization_tips.html#enable-full-font-locking-of-inputs-in-the-python-shell (advice-add 'elpy-shell--insert-and-font-lock :around (lambda (f string face &optional no-font-lock) (if (not (eq face 'comint-highlight-input)) (funcall f string face no-font-lock) (funcall f string face t) (python-shell-font-lock-post-command-hook)))) (advice-add 'comint-send-input :around (lambda (f &rest args) (if (eq major-mode 'inferior-python-mode) (cl-letf ((g (symbol-function 'add-text-properties)) ((symbol-function 'add-text-properties) (lambda (start end properties &optional object) (unless (eq (nth 3 properties) 'comint-highlight-input) (funcall g start end properties object))))) (apply f args)) (apply f args))))
Toggle Python versions (Linux only)
THIS SECTION IS BEING DEPRECATED, THIS NO LONGER WORKS
The following functions let me toggle between python 2 and 3 environments. I only use this on my linux machine. Also, this is probably a huge hack and will be removed soon.
(unless on-windows (defvar do.python/python-version "3" "The current python version we are using.") (defun do.python/set-python-version (ver) "Set the current python version." (elpy-disable) (setq do.python/python-version ver) (setq python-shell-interpreter (concat "ipython" do.python/python-version)) (setq elpy-rpc-python-command (concat "python" do.python/python-version)) (setenv "WORKON_HOME" (concat (file-name-as-directory (expand-file-name "~/python_venv")) "py" do.python/python-version)) (setenv "VIRTUALENVWRAPPER_PYTHON" (executable-find (concat "python" do.python/python-version))) (setenv "VIRTUALENVWRAPPER_VIRTUALENV" (concat "/usr/bin/virtualenv" do.python/python-version)) (setenv "VIRTUAL_ENV_DISABLE_PROMPT" "TRUE") (setenv "DO_PYTHON_VERSION" do.python/python-version) (elpy-enable)) (defun use-python2 () (interactive) (do.python/set-python-version "2")) (defun use-python3 () (interactive) (do.python/set-python-version "3")) (use-python3))
When I activate a virtualenv, I also want to set the LD_LIBRARY_PATH variable
(mainly for SWIG)
(unless on-windows (defun do.python/pyvenv-activate-and-set-ld-path (fun directory) "After activating the virtualenv DIRECTORY, set the environment variable LD_LIBRARY_PATH." (let ((ld_path (or (getenv "LD_LIBRARY_PATH") ""))) (apply fun (list directory)) (setq process-environment (append (list (concat "LD_LIBRARY_PATH=" ld_path ":" directory "/lib")) process-environment)))) (add-function :around (symbol-function #'pyvenv-activate) #'do.python/pyvenv-activate-and-set-ld-path))