doomemacs/modules/lang/python/config.el
Henrik Lissner 3239ab8b2e
nit(mu4e): neutralize comments
Some of our comments/docs can come off as disparaging or snide. They're
glimpses of unfiltered frustration or snarky rubber ducking gone too
far, something I can totally sympathize with, as a scatterbrained
tinkerer, unwittingly made responsible for a lot of work that isn't mine
because of Doom's position as a middleman. But now that Doom has a
veritable userbase, I'd like to hold it to a higher standard.

Light-hearted banter and aired grievances in our source code,
documentation, or community are fine if focused on the problem or the
personal/shared experiences of the community (things that offer value or
amusement to others), but it is never acceptable to attack people or
their efforts. Especially not the very people on whose shoulders Doom
stands.

I sincerely apologize if these have offended you.

Amend: b07614037f
2022-08-14 20:36:42 +02:00

433 lines
17 KiB
EmacsLisp

;;; lang/python/config.el -*- lexical-binding: t; -*-
(defvar +python-ipython-command '("ipython" "-i" "--simple-prompt" "--no-color-info")
"Command to initialize the ipython REPL for `+python/open-ipython-repl'.")
(defvar +python-jupyter-command '("jupyter" "console" "--simple-prompt")
"Command to initialize the jupyter REPL for `+python/open-jupyter-repl'.")
(after! projectile
(pushnew! projectile-project-root-files "pyproject.toml" "requirements.txt" "setup.py"))
;;
;;; Packages
(use-package! python
:mode ("[./]flake8\\'" . conf-mode)
:mode ("/Pipfile\\'" . conf-mode)
:init
(setq python-environment-directory doom-cache-dir
python-indent-guess-indent-offset-verbose nil)
(when (featurep! +lsp)
(add-hook 'python-mode-local-vars-hook #'lsp! 'append)
;; Use "mspyls" in eglot if in PATH
(when (executable-find "Microsoft.Python.LanguageServer")
(set-eglot-client! 'python-mode '("Microsoft.Python.LanguageServer"))))
(when (featurep! +tree-sitter)
(add-hook 'python-mode-local-vars-hook #'tree-sitter! 'append))
:config
(set-repl-handler! 'python-mode #'+python/open-repl
:persist t
:send-region #'python-shell-send-region
:send-buffer #'python-shell-send-buffer)
(set-docsets! '(python-mode inferior-python-mode) "Python 3" "NumPy" "SciPy" "Pandas")
(set-ligatures! 'python-mode
;; Functional
:def "def"
:lambda "lambda"
;; Types
:null "None"
:true "True" :false "False"
:int "int" :str "str"
:float "float"
:bool "bool"
:tuple "tuple"
;; Flow
:not "not"
:in "in" :not-in "not in"
:and "and" :or "or"
:for "for"
:return "return" :yield "yield")
;; Stop the spam!
(setq python-indent-guess-indent-offset-verbose nil)
;; Default to Python 3. Prefer the versioned Python binaries since some
;; systems link the unversioned one to Python 2.
(when (and (executable-find "python3")
(string= python-shell-interpreter "python"))
(setq python-shell-interpreter "python3"))
(add-hook! 'python-mode-hook
(defun +python-use-correct-flycheck-executables-h ()
"Use the correct Python executables for Flycheck."
(let ((executable python-shell-interpreter))
(save-excursion
(goto-char (point-min))
(save-match-data
(when (or (looking-at "#!/usr/bin/env \\(python[^ \n]+\\)")
(looking-at "#!\\([^ \n]+/python[^ \n]+\\)"))
(setq executable (substring-no-properties (match-string 1))))))
;; Try to compile using the appropriate version of Python for
;; the file.
(setq-local flycheck-python-pycompile-executable executable)
;; We might be running inside a virtualenv, in which case the
;; modules won't be available. But calling the executables
;; directly will work.
(setq-local flycheck-python-pylint-executable "pylint")
(setq-local flycheck-python-flake8-executable "flake8"))))
;; Affects pyenv and conda
(when (featurep! :ui modeline)
(advice-add #'pythonic-activate :after-while #'+modeline-update-env-in-all-windows-h)
(advice-add #'pythonic-deactivate :after #'+modeline-clear-env-in-all-windows-h))
(setq-hook! 'python-mode-hook tab-width python-indent-offset)
;; HACK Fix syntax highlighting on Emacs 28.1
;; DEPRECATED Remove when 28.1 support is dropped
;; REVIEW Revisit if a 28.2 is released with a fix
(when (= emacs-major-version 28)
(defadvice! +python--font-lock-assignment-matcher-a (regexp)
:override #'python-font-lock-assignment-matcher
(lambda (limit)
(cl-loop while (re-search-forward regexp limit t)
unless (or (python-syntax-context 'paren)
(equal (char-after) ?=))
return t)))
(defadvice! +python--rx-a (&rest regexps)
:override #'python-rx
`(rx-let ((block-start (seq symbol-start
(or "def" "class" "if" "elif" "else" "try"
"except" "finally" "for" "while" "with"
;; Python 3.5+ PEP492
(and "async" (+ space)
(or "def" "for" "with")))
symbol-end))
(dedenter (seq symbol-start
(or "elif" "else" "except" "finally")
symbol-end))
(block-ender (seq symbol-start
(or
"break" "continue" "pass" "raise" "return")
symbol-end))
(decorator (seq line-start (* space) ?@ (any letter ?_)
(* (any word ?_))))
(defun (seq symbol-start
(or "def" "class"
;; Python 3.5+ PEP492
(and "async" (+ space) "def"))
symbol-end))
(if-name-main (seq line-start "if" (+ space) "__name__"
(+ space) "==" (+ space)
(any ?' ?\") "__main__" (any ?' ?\")
(* space) ?:))
(symbol-name (seq (any letter ?_) (* (any word ?_))))
(assignment-target (seq (? ?*)
(* symbol-name ?.) symbol-name
(? ?\[ (+ (not ?\])) ?\])))
(grouped-assignment-target (seq (? ?*)
(* symbol-name ?.) (group symbol-name)
(? ?\[ (+ (not ?\])) ?\])))
(open-paren (or "{" "[" "("))
(close-paren (or "}" "]" ")"))
(simple-operator (any ?+ ?- ?/ ?& ?^ ?~ ?| ?* ?< ?> ?= ?%))
(not-simple-operator (not (or simple-operator ?\n)))
(operator (or "==" ">=" "is" "not"
"**" "//" "<<" ">>" "<=" "!="
"+" "-" "/" "&" "^" "~" "|" "*" "<" ">"
"=" "%"))
(assignment-operator (or "+=" "-=" "*=" "/=" "//=" "%=" "**="
">>=" "<<=" "&=" "^=" "|="
"="))
(string-delimiter (seq
;; Match even number of backslashes.
(or (not (any ?\\ ?\' ?\")) point
;; Quotes might be preceded by an
;; escaped quote.
(and (or (not (any ?\\)) point) ?\\
(* ?\\ ?\\) (any ?\' ?\")))
(* ?\\ ?\\)
;; Match single or triple quotes of any kind.
(group (or "\"\"\"" "\"" "'''" "'"))))
(coding-cookie (seq line-start ?# (* space)
(or
;; # coding=<encoding name>
(: "coding" (or ?: ?=) (* space)
(group-n 1 (+ (or word ?-))))
;; # -*- coding: <encoding name> -*-
(: "-*-" (* space) "coding:" (* space)
(group-n 1 (+ (or word ?-)))
(* space) "-*-")
;; # vim: set fileencoding=<encoding name> :
(: "vim:" (* space) "set" (+ space)
"fileencoding" (* space) ?= (* space)
(group-n 1 (+ (or word ?-)))
(* space) ":")))))
(rx ,@regexps)))))
(use-package! anaconda-mode
:defer t
:init
(setq anaconda-mode-installation-directory (concat doom-etc-dir "anaconda/")
anaconda-mode-eldoc-as-single-line t)
(add-hook! 'python-mode-local-vars-hook :append
(defun +python-init-anaconda-mode-maybe-h ()
"Enable `anaconda-mode' if `lsp-mode' is absent and
`python-shell-interpreter' is present."
(unless (or (bound-and-true-p lsp-mode)
(bound-and-true-p eglot--managed-mode)
(bound-and-true-p lsp--buffer-deferred)
(not (executable-find python-shell-interpreter t)))
(anaconda-mode +1))))
:config
(set-company-backend! 'anaconda-mode '(company-anaconda))
(set-lookup-handlers! 'anaconda-mode
:definition #'anaconda-mode-find-definitions
:references #'anaconda-mode-find-references
:documentation #'anaconda-mode-show-doc)
(set-popup-rule! "^\\*anaconda-mode" :select nil)
(add-hook 'anaconda-mode-hook #'anaconda-eldoc-mode)
(defun +python-auto-kill-anaconda-processes-h ()
"Kill anaconda processes if this buffer is the last python buffer."
(when (and (eq major-mode 'python-mode)
(not (delq (current-buffer)
(doom-buffers-in-mode 'python-mode (buffer-list)))))
(anaconda-mode-stop)))
(add-hook! 'python-mode-hook
(add-hook 'kill-buffer-hook #'+python-auto-kill-anaconda-processes-h
nil 'local))
(when (featurep 'evil)
(add-hook 'anaconda-mode-hook #'evil-normalize-keymaps))
(map! :localleader
:map anaconda-mode-map
:prefix "g"
"d" #'anaconda-mode-find-definitions
"h" #'anaconda-mode-show-doc
"a" #'anaconda-mode-find-assignments
"f" #'anaconda-mode-find-file
"u" #'anaconda-mode-find-references))
(use-package! pyimport
:defer t
:init
(map! :after python
:map python-mode-map
:localleader
(:prefix ("i" . "imports")
:desc "Insert missing imports" "i" #'pyimport-insert-missing
:desc "Remove unused imports" "R" #'pyimport-remove-unused
:desc "Optimize imports" "o" #'+python/optimize-imports)))
(use-package! py-isort
:defer t
:init
(map! :after python
:map python-mode-map
:localleader
(:prefix ("i" . "imports")
:desc "Sort imports" "s" #'py-isort-buffer
:desc "Sort region" "r" #'py-isort-region)))
(use-package! nose
:commands nose-mode
:preface (defvar nose-mode-map (make-sparse-keymap))
:minor ("/test_.+\\.py$" . nose-mode)
:config
(set-popup-rule! "^\\*nosetests" :size 0.4 :select nil)
(set-yas-minor-mode! 'nose-mode)
(when (featurep 'evil)
(add-hook 'nose-mode-hook #'evil-normalize-keymaps))
(map! :localleader
:map nose-mode-map
:prefix "t"
"r" #'nosetests-again
"a" #'nosetests-all
"s" #'nosetests-one
"v" #'nosetests-module
"A" #'nosetests-pdb-all
"O" #'nosetests-pdb-one
"V" #'nosetests-pdb-module))
(use-package! python-pytest
:commands python-pytest-dispatch
:init
(map! :after python
:localleader
:map python-mode-map
:prefix ("t" . "test")
"a" #'python-pytest
"f" #'python-pytest-file-dwim
"F" #'python-pytest-file
"t" #'python-pytest-function-dwim
"T" #'python-pytest-function
"r" #'python-pytest-repeat
"p" #'python-pytest-dispatch))
;;
;;; Environment management
(use-package! pipenv
:commands pipenv-project-p
:hook (python-mode . pipenv-mode)
:init (setq pipenv-with-projectile nil)
:config
(set-eval-handler! 'python-mode
'((:command . (lambda () python-shell-interpreter))
(:exec (lambda ()
(if-let* ((bin (executable-find "pipenv" t))
(_ (pipenv-project-p)))
(format "PIPENV_MAX_DEPTH=9999 %s run %%c %%o %%s %%a" bin)
"%c %o %s %a")))
(:description . "Run Python script")))
(map! :map python-mode-map
:localleader
:prefix "e"
:desc "activate" "a" #'pipenv-activate
:desc "deactivate" "d" #'pipenv-deactivate
:desc "install" "i" #'pipenv-install
:desc "lock" "l" #'pipenv-lock
:desc "open module" "o" #'pipenv-open
:desc "run" "r" #'pipenv-run
:desc "shell" "s" #'pipenv-shell
:desc "uninstall" "u" #'pipenv-uninstall))
(use-package! pyvenv
:after python
:init
(when (featurep! :ui modeline)
(add-hook 'pyvenv-post-activate-hooks #'+modeline-update-env-in-all-windows-h)
(add-hook 'pyvenv-pre-deactivate-hooks #'+modeline-clear-env-in-all-windows-h))
:config
(add-hook 'python-mode-local-vars-hook #'pyvenv-track-virtualenv)
(add-to-list 'global-mode-string
'(pyvenv-virtual-env-name (" venv:" pyvenv-virtual-env-name " "))
'append))
(use-package! pyenv-mode
:when (featurep! +pyenv)
:after python
:config
(when (executable-find "pyenv")
(pyenv-mode +1)
(add-to-list 'exec-path (expand-file-name "shims" (or (getenv "PYENV_ROOT") "~/.pyenv"))))
(add-hook 'python-mode-local-vars-hook #'+python-pyenv-mode-set-auto-h)
(add-hook 'doom-switch-buffer-hook #'+python-pyenv-mode-set-auto-h))
(use-package! conda
:when (featurep! +conda)
:after python
:config
;; The location of your anaconda home will be guessed from a list of common
;; possibilities, starting with `conda-anaconda-home''s default value (which
;; will consult a ANACONDA_HOME envvar, if it exists).
;;
;; If none of these work for you, `conda-anaconda-home' must be set
;; explicitly. Afterwards, run M-x `conda-env-activate' to switch between
;; environments
(or (cl-loop for dir in (list conda-anaconda-home
"~/.anaconda"
"~/.miniconda"
"~/.miniconda3"
"~/.miniforge3"
"~/anaconda3"
"~/miniconda3"
"~/miniforge3"
"~/opt/miniconda3"
"/usr/bin/anaconda3"
"/usr/local/anaconda3"
"/usr/local/miniconda3"
"/usr/local/Caskroom/miniconda/base"
"~/.conda")
if (file-directory-p dir)
return (setq conda-anaconda-home (expand-file-name dir)
conda-env-home-directory (expand-file-name dir)))
(message "Cannot find Anaconda installation"))
;; integration with term/eshell
(conda-env-initialize-interactive-shells)
(after! eshell (conda-env-initialize-eshell))
(add-to-list 'global-mode-string
'(conda-env-current-name (" conda:" conda-env-current-name " "))
'append))
(use-package! poetry
:when (featurep! +poetry)
:after python
:init
(setq poetry-tracking-strategy 'switch-buffer)
(add-hook 'python-mode-hook #'poetry-tracking-mode))
(use-package! cython-mode
:when (featurep! +cython)
:mode "\\.p\\(yx\\|x[di]\\)\\'"
:config
(setq cython-default-compile-format "cython -a %s")
(map! :map cython-mode-map
:localleader
:prefix "c"
:desc "Cython compile buffer" "c" #'cython-compile))
(use-package! flycheck-cython
:when (featurep! +cython)
:when (featurep! :checkers syntax)
:after cython-mode)
(use-package! pip-requirements
:defer t
:config
;; HACK `pip-requirements-mode' performs a sudden HTTP request to
;; https://pypi.org/simple, which causes unexpected hangs (see #5998). This
;; advice defers this behavior until the first time completion is invoked.
;; REVIEW More sensible behavior should be PRed upstream.
(defadvice! +python--init-completion-a (&rest args)
"Call `pip-requirements-fetch-packages' first time completion is invoked."
:before #'pip-requirements-complete-at-point
(unless pip-packages (pip-requirements-fetch-packages)))
(defadvice! +python--inhibit-pip-requirements-fetch-packages-a (fn &rest args)
"No-op `pip-requirements-fetch-packages', which can be expensive."
:around #'pip-requirements-mode
(letf! ((#'pip-requirements-fetch-packages #'ignore))
(apply fn args))))
;;
;;; LSP
(eval-when! (and (featurep! +lsp)
(not (featurep! :tools lsp +eglot)))
(use-package! lsp-python-ms
:unless (featurep! +pyright)
:after lsp-mode
:preface
(after! python
(setq lsp-python-ms-python-executable-cmd python-shell-interpreter)))
(use-package! lsp-pyright
:when (featurep! +pyright)
:after lsp-mode))