diff options
| author | Augusto Stoffel | 2022-09-04 13:14:58 +0200 |
|---|---|---|
| committer | Lars Ingebrigtsen | 2022-09-04 13:14:58 +0200 |
| commit | 4d50d413e67dd8ae183af8b68f315a667ebf2add (patch) | |
| tree | fce67a6345d29ec4d1af8423de311354d83ca46b /lisp/progmodes/python.el | |
| parent | 4932d26b5df14af01ae757b2a5232d157df69008 (diff) | |
| download | emacs-4d50d413e67dd8ae183af8b68f315a667ebf2add.tar.gz emacs-4d50d413e67dd8ae183af8b68f315a667ebf2add.zip | |
Add Python import management commands
* lisp/progmodes/python.el (python-interpreter): New variable
(python-mode-map): Keybindings and menu entries for new commands
(python--list-imports, python-import-history, python--query-import)
(python--do-isort): New variables and helper functions.
(python-add-import, python-import-symbol-at-point)
(python-remove-import, python-sort-imports, python-fix-imports): New
interactive commands (bug#57574).
Diffstat (limited to 'lisp/progmodes/python.el')
| -rw-r--r-- | lisp/progmodes/python.el | 270 |
1 files changed, 265 insertions, 5 deletions
diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 6020d52b91f..147c5f248d2 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el | |||
| @@ -34,7 +34,8 @@ | |||
| 34 | ;; Implements Syntax highlighting, Indentation, Movement, Shell | 34 | ;; Implements Syntax highlighting, Indentation, Movement, Shell |
| 35 | ;; interaction, Shell completion, Shell virtualenv support, Shell | 35 | ;; interaction, Shell completion, Shell virtualenv support, Shell |
| 36 | ;; package support, Shell syntax highlighting, Pdb tracking, Symbol | 36 | ;; package support, Shell syntax highlighting, Pdb tracking, Symbol |
| 37 | ;; completion, Skeletons, FFAP, Code Check, ElDoc, Imenu. | 37 | ;; completion, Skeletons, FFAP, Code Check, ElDoc, Imenu, Flymake, |
| 38 | ;; Import management. | ||
| 38 | 39 | ||
| 39 | ;; Syntax highlighting: Fontification of code is provided and supports | 40 | ;; Syntax highlighting: Fontification of code is provided and supports |
| 40 | ;; python's triple quoted strings properly. | 41 | ;; python's triple quoted strings properly. |
| @@ -69,7 +70,7 @@ | |||
| 69 | ;; variables. This example enables IPython globally: | 70 | ;; variables. This example enables IPython globally: |
| 70 | 71 | ||
| 71 | ;; (setq python-shell-interpreter "ipython" | 72 | ;; (setq python-shell-interpreter "ipython" |
| 72 | ;; python-shell-interpreter-args "-i") | 73 | ;; python-shell-interpreter-args "--simple-prompt") |
| 73 | 74 | ||
| 74 | ;; Using the "console" subcommand to start IPython in server-client | 75 | ;; Using the "console" subcommand to start IPython in server-client |
| 75 | ;; mode is known to fail intermittently due a bug on IPython itself | 76 | ;; mode is known to fail intermittently due a bug on IPython itself |
| @@ -240,6 +241,21 @@ | |||
| 240 | ;; I'd recommend the first one since you'll get the same behavior for | 241 | ;; I'd recommend the first one since you'll get the same behavior for |
| 241 | ;; all modes out-of-the-box. | 242 | ;; all modes out-of-the-box. |
| 242 | 243 | ||
| 244 | ;; Flymake: A Flymake backend, using the pyflakes program by default, | ||
| 245 | ;; is provided. You can also use flake8 or pylint by customizing | ||
| 246 | ;; `python-flymake-command'. | ||
| 247 | |||
| 248 | ;; Import management: The commands `python-sort-imports', | ||
| 249 | ;; `python-add-import', `python-remove-import', and | ||
| 250 | ;; `python-fix-imports' automate the editing of import statements at | ||
| 251 | ;; the top of the buffer, which tend to be a tedious task in larger | ||
| 252 | ;; projects. These commands require that the isort library is | ||
| 253 | ;; available to the interpreter pointed at by `python-interpreter'. | ||
| 254 | ;; The last command also requires pyflakes. These dependencies can be | ||
| 255 | ;; installed, among other methods, with the following command: | ||
| 256 | ;; | ||
| 257 | ;; pip install isort pyflakes | ||
| 258 | |||
| 243 | ;;; Code: | 259 | ;;; Code: |
| 244 | 260 | ||
| 245 | (require 'ansi-color) | 261 | (require 'ansi-color) |
| @@ -268,6 +284,12 @@ | |||
| 268 | :version "24.3" | 284 | :version "24.3" |
| 269 | :link '(emacs-commentary-link "python")) | 285 | :link '(emacs-commentary-link "python")) |
| 270 | 286 | ||
| 287 | (defcustom python-interpreter "python" | ||
| 288 | "Python interpreter for noninteractive use. | ||
| 289 | To customize the Python shell, modify `python-shell-interpreter' | ||
| 290 | instead." | ||
| 291 | :version "29.1" | ||
| 292 | :type 'string) | ||
| 271 | 293 | ||
| 272 | 294 | ||
| 273 | ;;; Bindings | 295 | ;;; Bindings |
| @@ -306,6 +328,11 @@ | |||
| 306 | (define-key map "\C-c\C-v" #'python-check) | 328 | (define-key map "\C-c\C-v" #'python-check) |
| 307 | (define-key map "\C-c\C-f" #'python-eldoc-at-point) | 329 | (define-key map "\C-c\C-f" #'python-eldoc-at-point) |
| 308 | (define-key map "\C-c\C-d" #'python-describe-at-point) | 330 | (define-key map "\C-c\C-d" #'python-describe-at-point) |
| 331 | ;; Import management | ||
| 332 | (define-key map "\C-c\C-ia" #'python-add-import) | ||
| 333 | (define-key map "\C-c\C-if" #'python-fix-imports) | ||
| 334 | (define-key map "\C-c\C-ir" #'python-remove-import) | ||
| 335 | (define-key map "\C-c\C-is" #'python-sort-imports) | ||
| 309 | ;; Utilities | 336 | ;; Utilities |
| 310 | (substitute-key-definition #'complete-symbol #'completion-at-point | 337 | (substitute-key-definition #'complete-symbol #'completion-at-point |
| 311 | map global-map) | 338 | map global-map) |
| @@ -351,7 +378,17 @@ | |||
| 351 | ["Help on symbol" python-eldoc-at-point | 378 | ["Help on symbol" python-eldoc-at-point |
| 352 | :help "Get help on symbol at point"] | 379 | :help "Get help on symbol at point"] |
| 353 | ["Complete symbol" completion-at-point | 380 | ["Complete symbol" completion-at-point |
| 354 | :help "Complete symbol before point"])) | 381 | :help "Complete symbol before point"] |
| 382 | "-----" | ||
| 383 | ["Add import" python-add-import | ||
| 384 | :help "Add an import statement to the top of this buffer"] | ||
| 385 | ["Remove import" python-remove-import | ||
| 386 | :help "Remove an import statement from the top of this buffer"] | ||
| 387 | ["Sort imports" python-sort-imports | ||
| 388 | :help "Sort the import statements at the top of this buffer"] | ||
| 389 | ["Fix imports" python-fix-imports | ||
| 390 | :help "Add missing imports and remove unused ones from the current buffer"] | ||
| 391 | )) | ||
| 355 | map) | 392 | map) |
| 356 | "Keymap for `python-mode'.") | 393 | "Keymap for `python-mode'.") |
| 357 | 394 | ||
| @@ -5852,6 +5889,225 @@ REPORT-FN is Flymake's callback function." | |||
| 5852 | (process-send-eof python--flymake-proc)))) | 5889 | (process-send-eof python--flymake-proc)))) |
| 5853 | 5890 | ||
| 5854 | 5891 | ||
| 5892 | ;;; Import management | ||
| 5893 | (defconst python--list-imports "\ | ||
| 5894 | from isort import find_imports_in_stream, find_imports_in_paths | ||
| 5895 | from sys import argv, stdin | ||
| 5896 | |||
| 5897 | query, files, result = argv[1] or None, argv[2:], {} | ||
| 5898 | |||
| 5899 | if files: | ||
| 5900 | imports = find_imports_in_paths(files, top_only=True) | ||
| 5901 | else: | ||
| 5902 | imports = find_imports_in_stream(stdin, top_only=True) | ||
| 5903 | |||
| 5904 | for imp in imports: | ||
| 5905 | if query is None or query == (imp.alias or imp.attribute or imp.module): | ||
| 5906 | key = (imp.module, imp.attribute or '', imp.alias or '') | ||
| 5907 | if key not in result: | ||
| 5908 | result[key] = imp.statement() | ||
| 5909 | |||
| 5910 | for key in sorted(result): | ||
| 5911 | print(result[key]) | ||
| 5912 | " | ||
| 5913 | "Script to list import statements in Python code.") | ||
| 5914 | |||
| 5915 | (defvar python-import-history nil | ||
| 5916 | "History variable for `python-import' commands.") | ||
| 5917 | |||
| 5918 | (defun python--import-sources () | ||
| 5919 | "List files containing Python imports that may be useful in the current buffer." | ||
| 5920 | (if-let (((featurep 'project)) ;For compatibility with Emacs < 26 | ||
| 5921 | (proj (project-current))) | ||
| 5922 | (seq-filter (lambda (s) (string-match-p "\\.py[ciw]?\\'" s)) | ||
| 5923 | (project-files proj)) | ||
| 5924 | (list default-directory))) | ||
| 5925 | |||
| 5926 | (defun python--list-imports (name source) | ||
| 5927 | "List all Python imports matching NAME in SOURCE. | ||
| 5928 | If NAME is nil, list all imports. SOURCE can be a buffer or a | ||
| 5929 | list of file names or directories; the latter are searched | ||
| 5930 | recursively." | ||
| 5931 | (let ((buffer (current-buffer))) | ||
| 5932 | (with-temp-buffer | ||
| 5933 | (let* ((temp (current-buffer)) | ||
| 5934 | (status (if (bufferp source) | ||
| 5935 | (with-current-buffer source | ||
| 5936 | (call-process-region (point-min) (point-max) | ||
| 5937 | python-interpreter | ||
| 5938 | nil (list temp nil) nil | ||
| 5939 | "-c" python--list-imports | ||
| 5940 | (or name ""))) | ||
| 5941 | (with-current-buffer buffer | ||
| 5942 | (apply #'call-process | ||
| 5943 | python-interpreter | ||
| 5944 | nil (list temp nil) nil | ||
| 5945 | "-c" python--list-imports | ||
| 5946 | (or name "") | ||
| 5947 | (mapcar #'file-local-name source))))) | ||
| 5948 | lines) | ||
| 5949 | (unless (eq 0 status) | ||
| 5950 | (error "%s exited with status %s (maybe isort is missing?)" | ||
| 5951 | python-interpreter status)) | ||
| 5952 | (goto-char (point-min)) | ||
| 5953 | (while (not (eobp)) | ||
| 5954 | (push (buffer-substring-no-properties (point) (pos-eol)) | ||
| 5955 | lines) | ||
| 5956 | (forward-line 1)) | ||
| 5957 | (nreverse lines))))) | ||
| 5958 | |||
| 5959 | (defun python--query-import (name source prompt) | ||
| 5960 | "Read a Python import statement defining NAME. | ||
| 5961 | A list of candidates is produced by `python--list-imports' using | ||
| 5962 | the NAME and SOURCE arguments. An interactive query, using the | ||
| 5963 | PROMPT string, is made unless there is a single candidate." | ||
| 5964 | (let* ((cands (python--list-imports name source)) | ||
| 5965 | ;; Don't use DEF argument of `completing-read', so it is able | ||
| 5966 | ;; to return the empty string. | ||
| 5967 | (minibuffer-default-add-function | ||
| 5968 | (lambda () | ||
| 5969 | (setq minibuffer-default (with-minibuffer-selected-window | ||
| 5970 | (thing-at-point 'symbol))))) | ||
| 5971 | (statement (cond ((and name (length= cands 1)) | ||
| 5972 | (car cands)) | ||
| 5973 | (prompt | ||
| 5974 | (completing-read prompt | ||
| 5975 | (or cands python-import-history) | ||
| 5976 | nil nil nil | ||
| 5977 | 'python-import-history))))) | ||
| 5978 | (unless (string-empty-p statement) | ||
| 5979 | statement))) | ||
| 5980 | |||
| 5981 | (defun python--do-isort (&rest args) | ||
| 5982 | "Edit the current buffer using isort called with ARGS. | ||
| 5983 | Return non-nil if the buffer was actually modified." | ||
| 5984 | (let ((buffer (current-buffer))) | ||
| 5985 | (with-temp-buffer | ||
| 5986 | (let ((temp (current-buffer))) | ||
| 5987 | (with-current-buffer buffer | ||
| 5988 | (let ((status (apply #'call-process-region | ||
| 5989 | (point-min) (point-max) | ||
| 5990 | python-interpreter | ||
| 5991 | nil (list temp nil) nil | ||
| 5992 | "-m" "isort" "-" args)) | ||
| 5993 | (tick (buffer-chars-modified-tick))) | ||
| 5994 | (unless (eq 0 status) | ||
| 5995 | (error "%s exited with status %s (maybe isort is missing?)" | ||
| 5996 | python-interpreter status)) | ||
| 5997 | (replace-buffer-contents temp) | ||
| 5998 | (not (eq tick (buffer-chars-modified-tick))))))))) | ||
| 5999 | |||
| 6000 | ;;;###autoload | ||
| 6001 | (defun python-add-import (name) | ||
| 6002 | "Add an import statement to the current buffer. | ||
| 6003 | |||
| 6004 | Interactively, ask for an import statement using all imports | ||
| 6005 | found in the current project as suggestions. With a prefix | ||
| 6006 | argument, restrict the suggestions to imports defining the symbol | ||
| 6007 | at point. If there is only one such suggestion, act without | ||
| 6008 | asking. | ||
| 6009 | |||
| 6010 | When calling from Lisp, use a non-nil NAME to restrict the | ||
| 6011 | suggestions to imports defining NAME." | ||
| 6012 | (interactive (list (when current-prefix-arg (thing-at-point 'symbol)))) | ||
| 6013 | (when-let ((statement (python--query-import name | ||
| 6014 | (python--import-sources) | ||
| 6015 | "Add import: "))) | ||
| 6016 | (if (python--do-isort "--add" statement) | ||
| 6017 | (message "Added `%s'" statement) | ||
| 6018 | (message "(No changes in Python imports needed)")))) | ||
| 6019 | |||
| 6020 | ;;;###autoload | ||
| 6021 | (defun python-import-symbol-at-point () | ||
| 6022 | "Add an import statement for the symbol at point to the current buffer. | ||
| 6023 | This works like `python-add-import', but with the opposite | ||
| 6024 | behavior regarding the prefix argument." | ||
| 6025 | (interactive nil) | ||
| 6026 | (python-add-import (unless current-prefix-arg (thing-at-point 'symbol)))) | ||
| 6027 | |||
| 6028 | ;;;###autoload | ||
| 6029 | (defun python-remove-import (name) | ||
| 6030 | "Remove an import statement from the current buffer. | ||
| 6031 | |||
| 6032 | Interactively, ask for an import statement to remove, displaying | ||
| 6033 | the imports of the current buffer as suggestions. With a prefix | ||
| 6034 | argument, restrict the suggestions to imports defining the symbol | ||
| 6035 | at point. If there is only one such suggestion, act without | ||
| 6036 | asking." | ||
| 6037 | (interactive (list (when current-prefix-arg (thing-at-point 'symbol)))) | ||
| 6038 | (when-let ((statement (python--query-import name (current-buffer) | ||
| 6039 | "Remove import: "))) | ||
| 6040 | (if (python--do-isort "--rm" statement) | ||
| 6041 | (message "Removed `%s'" statement) | ||
| 6042 | (message "(No changes in Python imports needed)")))) | ||
| 6043 | |||
| 6044 | ;;;###autoload | ||
| 6045 | (defun python-sort-imports () | ||
| 6046 | "Sort Python imports in the current buffer." | ||
| 6047 | (interactive) | ||
| 6048 | (if (python--do-isort) | ||
| 6049 | (message "Sorted imports") | ||
| 6050 | (message "(No changes in Python imports needed)"))) | ||
| 6051 | |||
| 6052 | ;;;###autoload | ||
| 6053 | (defun python-fix-imports () | ||
| 6054 | "Add missing imports and remove unused ones from the current buffer." | ||
| 6055 | (interactive) | ||
| 6056 | (let ((buffer (current-buffer)) | ||
| 6057 | undefined unused add remove) | ||
| 6058 | ;; Compute list of undefined and unused names | ||
| 6059 | (with-temp-buffer | ||
| 6060 | (let ((temp (current-buffer))) | ||
| 6061 | (with-current-buffer buffer | ||
| 6062 | (call-process-region (point-min) (point-max) | ||
| 6063 | python-interpreter | ||
| 6064 | nil temp nil | ||
| 6065 | "-m" "pyflakes")) | ||
| 6066 | (goto-char (point-min)) | ||
| 6067 | (when (looking-at-p ".* No module named pyflakes$") | ||
| 6068 | (error "%s couldn't find pyflakes" python-interpreter)) | ||
| 6069 | (while (not (eobp)) | ||
| 6070 | (cond ((looking-at ".* undefined name '\\([^']+\\)'$") | ||
| 6071 | (push (match-string 1) undefined)) | ||
| 6072 | ((looking-at ".*'\\([^']+\\)' imported but unused$") | ||
| 6073 | (push (match-string 1) unused))) | ||
| 6074 | (forward-line 1)))) | ||
| 6075 | ;; Compute imports to be added | ||
| 6076 | (dolist (name (seq-uniq undefined)) | ||
| 6077 | (when-let ((statement (python--query-import name | ||
| 6078 | (python--import-sources) | ||
| 6079 | (format "\ | ||
| 6080 | Add import for undefined name `%s' (empty to skip): " | ||
| 6081 | name)))) | ||
| 6082 | (push statement add))) | ||
| 6083 | ;; Compute imports to be removed | ||
| 6084 | (dolist (name (seq-uniq unused)) | ||
| 6085 | ;; The unused imported names, as provided by pyflakes, are of | ||
| 6086 | ;; the form "module.var" or "module.var as alias", independently | ||
| 6087 | ;; of style of import statement used. | ||
| 6088 | (let* ((filter | ||
| 6089 | (lambda (statement) | ||
| 6090 | (string= name | ||
| 6091 | (thread-last | ||
| 6092 | statement | ||
| 6093 | (replace-regexp-in-string "^\\(from\\|import\\) " "") | ||
| 6094 | (replace-regexp-in-string " import " "."))))) | ||
| 6095 | (statements (seq-filter filter (python--list-imports nil buffer)))) | ||
| 6096 | (when (length= statements 1) | ||
| 6097 | (push (car statements) remove)))) | ||
| 6098 | ;; Edit buffer and say goodbye | ||
| 6099 | (if (not (or add remove)) | ||
| 6100 | (message "(No changes in Python imports needed)") | ||
| 6101 | (apply #'python--do-isort | ||
| 6102 | (append (mapcan (lambda (x) (list "--add" x)) add) | ||
| 6103 | (mapcan (lambda (x) (list "--rm" x)) remove))) | ||
| 6104 | (message "%s" (concat (when add "Added ") | ||
| 6105 | (when add (string-join add ", ")) | ||
| 6106 | (when remove (if add " and removed " "Removed ")) | ||
| 6107 | (when remove (string-join remove ", " ))))))) | ||
| 6108 | |||
| 6109 | |||
| 6110 | ;;; Major mode | ||
| 5855 | (defun python-electric-pair-string-delimiter () | 6111 | (defun python-electric-pair-string-delimiter () |
| 5856 | (when (and electric-pair-mode | 6112 | (when (and electric-pair-mode |
| 5857 | (memq last-command-event '(?\" ?\')) | 6113 | (memq last-command-event '(?\" ?\')) |
| @@ -5973,8 +6229,10 @@ REPORT-FN is Flymake's callback function." | |||
| 5973 | 6229 | ||
| 5974 | ;;; Completion predicates for M-x | 6230 | ;;; Completion predicates for M-x |
| 5975 | ;; Commands that only make sense when editing Python code | 6231 | ;; Commands that only make sense when editing Python code |
| 5976 | (dolist (sym '(python-check | 6232 | (dolist (sym '(python-add-import |
| 6233 | python-check | ||
| 5977 | python-fill-paragraph | 6234 | python-fill-paragraph |
| 6235 | python-fix-imports | ||
| 5978 | python-indent-dedent-line | 6236 | python-indent-dedent-line |
| 5979 | python-indent-dedent-line-backspace | 6237 | python-indent-dedent-line-backspace |
| 5980 | python-indent-guess-indent-offset | 6238 | python-indent-guess-indent-offset |
| @@ -5999,9 +6257,11 @@ REPORT-FN is Flymake's callback function." | |||
| 5999 | python-nav-forward-statement | 6257 | python-nav-forward-statement |
| 6000 | python-nav-if-name-main | 6258 | python-nav-if-name-main |
| 6001 | python-nav-up-list | 6259 | python-nav-up-list |
| 6260 | python-remove-import | ||
| 6002 | python-shell-send-buffer | 6261 | python-shell-send-buffer |
| 6003 | python-shell-send-defun | 6262 | python-shell-send-defun |
| 6004 | python-shell-send-statement)) | 6263 | python-shell-send-statement |
| 6264 | python-sort-imports)) | ||
| 6005 | (put sym 'completion-predicate #'python--completion-predicate)) | 6265 | (put sym 'completion-predicate #'python--completion-predicate)) |
| 6006 | 6266 | ||
| 6007 | (defun python-shell--completion-predicate (_ buffer) | 6267 | (defun python-shell--completion-predicate (_ buffer) |