featurep! will be renamed modulep! in the future, so it's been deprecated. They have identical interfaces, and can be replaced without issue. featurep! was never quite the right name for this macro. It implied that it had some connection to featurep, which it doesn't (only that it was similar in purpose; still, Doom modules are not features). To undo such implications and be consistent with its namespace (and since we're heading into a storm of breaking changes with the v3 release anyway), now was the best opportunity to begin the transition.
433 lines
17 KiB
EmacsLisp
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 (modulep! +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 (modulep! +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 (modulep! :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 (modulep! :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 (modulep! +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 (modulep! +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 (modulep! +poetry)
|
|
:after python
|
|
:init
|
|
(setq poetry-tracking-strategy 'switch-buffer)
|
|
(add-hook 'python-mode-hook #'poetry-tracking-mode))
|
|
|
|
|
|
(use-package! cython-mode
|
|
:when (modulep! +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 (modulep! +cython)
|
|
:when (modulep! :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 (modulep! +lsp)
|
|
(not (modulep! :tools lsp +eglot)))
|
|
|
|
(use-package! lsp-python-ms
|
|
:unless (modulep! +pyright)
|
|
:after lsp-mode
|
|
:preface
|
|
(after! python
|
|
(setq lsp-python-ms-python-executable-cmd python-shell-interpreter)))
|
|
|
|
(use-package! lsp-pyright
|
|
:when (modulep! +pyright)
|
|
:after lsp-mode))
|