;;; 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")))) :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 stupidly make the unversioned one point at 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= (: "coding" (or ?: ?=) (* space) (group-n 1 (+ (or word ?-)))) ;; # -*- coding: -*- (: "-*-" (* space) "coding:" (* space) (group-n 1 (+ (or word ?-))) (* space) "-*-") ;; # vim: set fileencoding= : (: "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)) ;; Tree sitter (eval-when! (featurep! +tree-sitter) (add-hook! 'python-mode-local-vars-hook #'tree-sitter!))