aboutsummaryrefslogtreecommitdiffstats
path: root/lisp/progmodes/python.el
diff options
context:
space:
mode:
authorAugusto Stoffel2022-09-04 13:14:58 +0200
committerLars Ingebrigtsen2022-09-04 13:14:58 +0200
commit4d50d413e67dd8ae183af8b68f315a667ebf2add (patch)
treefce67a6345d29ec4d1af8423de311354d83ca46b /lisp/progmodes/python.el
parent4932d26b5df14af01ae757b2a5232d157df69008 (diff)
downloademacs-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.el270
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.
289To customize the Python shell, modify `python-shell-interpreter'
290instead."
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 "\
5894from isort import find_imports_in_stream, find_imports_in_paths
5895from sys import argv, stdin
5896
5897query, files, result = argv[1] or None, argv[2:], {}
5898
5899if files:
5900 imports = find_imports_in_paths(files, top_only=True)
5901else:
5902 imports = find_imports_in_stream(stdin, top_only=True)
5903
5904for 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
5910for 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.
5928If NAME is nil, list all imports. SOURCE can be a buffer or a
5929list of file names or directories; the latter are searched
5930recursively."
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.
5961A list of candidates is produced by `python--list-imports' using
5962the NAME and SOURCE arguments. An interactive query, using the
5963PROMPT 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.
5983Return 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
6004Interactively, ask for an import statement using all imports
6005found in the current project as suggestions. With a prefix
6006argument, restrict the suggestions to imports defining the symbol
6007at point. If there is only one such suggestion, act without
6008asking.
6009
6010When calling from Lisp, use a non-nil NAME to restrict the
6011suggestions 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.
6023This works like `python-add-import', but with the opposite
6024behavior 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
6032Interactively, ask for an import statement to remove, displaying
6033the imports of the current buffer as suggestions. With a prefix
6034argument, restrict the suggestions to imports defining the symbol
6035at point. If there is only one such suggestion, act without
6036asking."
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 "\
6080Add 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)