diff options
Diffstat (limited to 'lisp/progmodes/python.el')
-rw-r--r-- | lisp/progmodes/python.el | 286 |
1 files changed, 170 insertions, 116 deletions
diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 2d47cdc4068..091456aa89a 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -4,7 +4,7 @@ ;; Author: Fabián E. Gallina <fgallina@gnu.org> ;; URL: https://github.com/fgallina/python.el -;; Version: 0.26.1 +;; Version: 0.27 ;; Package-Requires: ((emacs "24.1") (cl-lib "1.0")) ;; Maintainer: emacs-devel@gnu.org ;; Created: Jul 2010 @@ -29,7 +29,7 @@ ;; Major mode for editing Python files with some fontification and ;; indentation bits extracted from original Dave Love's python.el -;; found in GNU/Emacs. +;; found in GNU Emacs. ;; Implements Syntax highlighting, Indentation, Movement, Shell ;; interaction, Shell completion, Shell virtualenv support, Shell @@ -135,7 +135,7 @@ ;; values enable completion for both CPython and IPython, and probably ;; any readline based shell (it's known to work with PyPy). If your ;; Python installation lacks readline (like CPython for Windows), -;; installing pyreadline (URL `http://ipython.org/pyreadline.html') +;; installing pyreadline (URL `https://ipython.org/pyreadline.html') ;; should suffice. To troubleshoot why you are not getting any ;; completions, you can try the following in your Python shell: @@ -247,13 +247,6 @@ ;; I'd recommend the first one since you'll get the same behavior for ;; all modes out-of-the-box. -;;; Installation: - -;; Add this to your .emacs: - -;; (add-to-list 'load-path "/folder/containing/file") -;; (require 'python) - ;;; TODO: ;;; Code: @@ -261,7 +254,6 @@ (require 'ansi-color) (require 'cl-lib) (require 'comint) -(require 'json) (require 'tramp-sh) ;; Avoid compiler warnings @@ -284,24 +276,6 @@ :link '(emacs-commentary-link "python")) -;;; 24.x Compat - - -(eval-and-compile - (unless (fboundp 'prog-first-column) - (defun prog-first-column () - 0)) - (unless (fboundp 'file-local-name) - (defun file-local-name (file) - "Return the local name component of FILE. -It returns a file name which can be used directly as argument of -`process-file', `start-file-process', or `shell-command'." - (or (file-remote-p file 'localname) file)))) - -;; In Emacs 24.3 and earlier, `define-derived-mode' does not define -;; the hook variable, it only puts documentation on the symbol. -(defvar inferior-python-mode-hook) - ;;; Bindings @@ -520,6 +494,52 @@ The type returned can be `comment', `string' or `paren'." font-lock-string-face) font-lock-comment-face)) +(defun python--f-string-p (ppss) + "Return non-nil if the pos where PPSS was found is inside an f-string." + (and (nth 3 ppss) + (let ((spos (1- (nth 8 ppss)))) + (and (memq (char-after spos) '(?f ?F)) + (or (< (point-min) spos) + (not (memq (char-syntax (char-before spos)) '(?w ?_)))))))) + +(defun python--font-lock-f-strings (limit) + "Mark {...} holes as being code. +Remove the (presumably `font-lock-string-face') `face' property from +the {...} holes that appear within f-strings." + ;; FIXME: This will fail to properly highlight strings appearing + ;; within the {...} of an f-string. + ;; We could presumably fix it by running + ;; `font-lock-fontify-syntactically-region' (as is done in + ;; `sm-c--cpp-fontify-syntactically', for example) after removing + ;; the `face' property, but I'm not sure it's worth the effort and + ;; the risks. + (let ((ppss (syntax-ppss))) + (while + (progn + (while (and (not (python--f-string-p ppss)) + (re-search-forward "\\<f['\"]" limit 'move)) + (setq ppss (syntax-ppss))) + (< (point) limit)) + (cl-assert (python--f-string-p ppss)) + (let ((send (save-excursion + (goto-char (nth 8 ppss)) + (condition-case nil + (progn (let ((forward-sexp-function nil)) + (forward-sexp 1)) + (min limit (1- (point)))) + (scan-error limit))))) + (while (re-search-forward "{" send t) + (if (eq ?\{ (char-after)) + (forward-char 1) ;Just skip over {{ + (let ((beg (match-beginning 0)) + (end (condition-case nil + (progn (up-list 1) (min send (point))) + (scan-error send)))) + (goto-char end) + (put-text-property beg end 'face nil)))) + (goto-char (min limit (1+ send))) + (setq ppss (syntax-ppss)))))) + (defvar python-font-lock-keywords-level-1 `((,(rx symbol-start "def" (1+ space) (group (1+ (or word ?_)))) (1 font-lock-function-name-face)) @@ -586,7 +606,8 @@ This is the medium decoration level, including everything in builtins.") (defvar python-font-lock-keywords-maximum-decoration - `(,@python-font-lock-keywords-level-2 + `((python--font-lock-f-strings) + ,@python-font-lock-keywords-level-2 ;; Constants (,(rx symbol-start (or @@ -594,7 +615,8 @@ builtins.") ;; copyright, license, credits, quit and exit are added by the site ;; module and they are not intended to be used in programs "copyright" "credits" "exit" "license" "quit") - symbol-end) . font-lock-constant-face) + symbol-end) + . font-lock-constant-face) ;; Decorators. (,(rx line-start (* (any " \t")) (group "@" (1+ (or word ?_)) (0+ "." (1+ (or word ?_))))) @@ -628,12 +650,16 @@ builtins.") ;; OS specific "VMSError" "WindowsError" ) - symbol-end) . font-lock-type-face) + symbol-end) + . font-lock-type-face) ;; assignments ;; support for a = b = c = 5 (,(lambda (limit) - (let ((re (python-rx (group (+ (any word ?. ?_))) - (? ?\[ (+ (not (any ?\]))) ?\]) (* space) + (let ((re (python-rx (group symbol-name) + ;; subscript, like "[5]" + (? ?\[ (+ (not ?\])) ?\]) (* space) + ;; type hint, like ": int" or ": Mapping[int, str]" + (? ?: (* space) (+ not-simple-operator) (* space)) assignment-operator)) (res nil)) (while (and (setq res (re-search-forward re limit t)) @@ -643,9 +669,9 @@ builtins.") (1 font-lock-variable-name-face nil nil)) ;; support for a, b, c = (1, 2, 3) (,(lambda (limit) - (let ((re (python-rx (group (+ (any word ?. ?_))) (* space) - (* ?, (* space) (+ (any word ?. ?_)) (* space)) - ?, (* space) (+ (any word ?. ?_)) (* space) + (let ((re (python-rx (group symbol-name) (* space) + (* ?, (* space) symbol-name (* space)) + ?, (* space) symbol-name (* space) assignment-operator)) (res nil)) (while (and (setq res (re-search-forward re limit t)) @@ -1993,7 +2019,7 @@ position, else returns nil." ;; IPython prompts activated, this adds some safeguard for that. "In : " "\\.\\.\\.: ") "List of regular expressions matching input prompts." - :type '(repeat string) + :type '(repeat regexp) :version "24.4") (defcustom python-shell-prompt-output-regexps @@ -2001,28 +2027,28 @@ position, else returns nil." "Out\\[[0-9]+\\]: " ; IPython "Out :") ; ipdb safeguard "List of regular expressions matching output prompts." - :type '(repeat string) + :type '(repeat regexp) :version "24.4") (defcustom python-shell-prompt-regexp ">>> " "Regular expression matching top level input prompt of Python shell. It should not contain a caret (^) at the beginning." - :type 'string) + :type 'regexp) (defcustom python-shell-prompt-block-regexp "\\.\\.\\.:? " "Regular expression matching block input prompt of Python shell. It should not contain a caret (^) at the beginning." - :type 'string) + :type 'regexp) (defcustom python-shell-prompt-output-regexp "" "Regular expression matching output prompt of Python shell. It should not contain a caret (^) at the beginning." - :type 'string) + :type 'regexp) (defcustom python-shell-prompt-pdb-regexp "[(<]*[Ii]?[Pp]db[>)]+ " "Regular expression matching pdb input prompt of Python shell. It should not contain a caret (^) at the beginning." - :type 'string) + :type 'regexp) (define-obsolete-variable-alias 'python-shell-enable-font-lock 'python-shell-font-lock-enable "25.1") @@ -2076,7 +2102,7 @@ that they are prioritized when looking for executables." When this variable is non-nil, values are exported into remote hosts PATH before starting processes. Values defined in `python-shell-exec-path' will take precedence to paths defined -here. Normally you wont use this variable directly unless you +here. Normally you won't use this variable directly unless you plan to ensure a particular set of paths to all Python shell executed through tramp connections." :version "25.1" @@ -2091,7 +2117,7 @@ executed through tramp connections." This variable, when set to a string, makes the environment to be modified such that shells are started within the specified virtualenv." - :type '(choice (const nil) string) + :type '(choice (const nil) directory) :group 'python) (defcustom python-shell-setup-codes nil @@ -2111,7 +2137,7 @@ virtualenv." "(" (group (1+ digit)) ")" (1+ (not (any "("))) "()") 1 2)) "`compilation-error-regexp-alist' for inferior Python." - :type '(alist string) + :type '(alist regexp) :group 'python) (defmacro python-shell--add-to-path-with-priority (pathvar paths) @@ -2276,6 +2302,18 @@ Do not set this variable directly, instead use Do not set this variable directly, instead use `python-shell-prompt-set-calculated-regexps'.") +(defalias 'python--parse-json-array + (if (fboundp 'json-parse-string) + (lambda (string) + (json-parse-string string :array-type 'list)) + (require 'json) + (defvar json-array-type) + (declare-function json-read-from-string "json" (string)) + (lambda (string) + (let ((json-array-type 'list)) + (json-read-from-string string)))) + "Parse the JSON array in STRING into a Lisp list.") + (defun python-shell-prompt-detect () "Detect prompts for the current `python-shell-interpreter'. When prompts can be retrieved successfully from the @@ -2324,11 +2362,11 @@ detection and just returns nil." (catch 'prompts (dolist (line (split-string output "\n" t)) (let ((res - ;; Check if current line is a valid JSON array - (and (string= (substring line 0 2) "[\"") + ;; Check if current line is a valid JSON array. + (and (string-prefix-p "[\"" line) (ignore-errors - ;; Return prompts as a list, not vector - (append (json-read-from-string line) nil))))) + ;; Return prompts as a list. + (python--parse-json-array line))))) ;; The list must contain 3 strings, where the first ;; is the input prompt, the second is the block ;; prompt and the last one is the output prompt. The @@ -2798,6 +2836,7 @@ variable. python-shell-comint-watch-for-first-prompt-output-filter python-comint-postoutput-scroll-to-bottom comint-watch-for-password-prompt)) + (setq-local comint-highlight-input nil) (set (make-local-variable 'compilation-error-regexp-alist) python-shell-compilation-regexp-alist) (add-hook 'completion-at-point-functions @@ -2876,7 +2915,7 @@ process buffer for a list of commands.)" (python-shell-make-comint (or cmd (python-shell-calculate-command)) (python-shell-get-process-name dedicated) show))) - (pop-to-buffer buffer) + (set-buffer buffer) (get-buffer-process buffer))) (defun run-python-internal () @@ -3080,7 +3119,7 @@ Returns the output. See `python-shell-send-string-no-output'." (define-obsolete-function-alias 'python-send-string 'python-shell-internal-send-string "24.3") -(defun python-shell-buffer-substring (start end &optional nomain) +(defun python-shell-buffer-substring (start end &optional nomain no-cookie) "Send buffer substring from START to END formatted for shell. This is a wrapper over `buffer-substring' that takes care of different transformations for the code sent to be evaluated in @@ -3094,9 +3133,16 @@ the python shell: 4. Wraps indented regions under an \"if True:\" block so the interpreter evaluates them correctly." (let* ((start (save-excursion - ;; Normalize start to the line beginning position. + ;; If we're at the start of the expression, and + ;; there's just blank space ahead of it, then expand + ;; the region to include the start of the line. + ;; This makes things work better with the rest of + ;; the data we're sending over. (goto-char start) - (line-beginning-position))) + (if (string-blank-p + (buffer-substring (line-beginning-position) start)) + (line-beginning-position) + start))) (substring (buffer-substring-no-properties start end)) (starts-at-point-min-p (save-restriction (widen) @@ -3106,12 +3152,13 @@ the python shell: (goto-char start) (python-util-forward-comment 1) (current-indentation)))) - (fillstr (when (not starts-at-point-min-p) - (concat - (format "# -*- coding: %s -*-\n" encoding) - (make-string - ;; Subtract 2 because of the coding cookie. - (- (line-number-at-pos start) 2) ?\n))))) + (fillstr (and (not no-cookie) + (not starts-at-point-min-p) + (concat + (format "# -*- coding: %s -*-\n" encoding) + (make-string + ;; Subtract 2 because of the coding cookie. + (- (line-number-at-pos start) 2) ?\n))))) (with-temp-buffer (python-mode) (when fillstr @@ -3150,7 +3197,8 @@ the python shell: (line-beginning-position) (line-end-position)))) (buffer-substring-no-properties (point-min) (point-max))))) -(defun python-shell-send-region (start end &optional send-main msg) +(defun python-shell-send-region (start end &optional send-main msg + no-cookie) "Send the region delimited by START and END to inferior Python process. When optional argument SEND-MAIN is non-nil, allow execution of code inside blocks delimited by \"if __name__== \\='__main__\\=':\". @@ -3160,7 +3208,8 @@ non-nil, forces display of a user-friendly message if there's no process running; defaults to t when called interactively." (interactive (list (region-beginning) (region-end) current-prefix-arg t)) - (let* ((string (python-shell-buffer-substring start end (not send-main))) + (let* ((string (python-shell-buffer-substring start end (not send-main) + no-cookie)) (process (python-shell-get-process-or-error msg)) (original-string (buffer-substring-no-properties start end)) (_ (string-match "\\`\n*\\(.*\\)" original-string))) @@ -3184,7 +3233,7 @@ interactively." (python-shell-send-region (save-excursion (python-nav-beginning-of-statement)) (save-excursion (python-nav-end-of-statement)) - send-main msg))) + send-main msg t))) (defun python-shell-send-buffer (&optional send-main msg) "Send the entire buffer to inferior Python process. @@ -3206,27 +3255,29 @@ optional argument MSG is non-nil, forces display of a user-friendly message if there's no process running; defaults to t when called interactively." (interactive (list current-prefix-arg t)) - (save-excursion - (python-shell-send-region - (progn - (end-of-line 1) - (while (and (or (python-nav-beginning-of-defun) - (beginning-of-line 1)) - (> (current-indentation) 0))) - (when (not arg) - (while (and - (eq (forward-line -1) 0) - (if (looking-at (python-rx decorator)) - t - (forward-line 1) - nil)))) - (point-marker)) - (progn - (or (python-nav-end-of-defun) - (end-of-line 1)) - (point-marker)) - nil ;; noop - msg))) + (let ((starting-pos (point))) + (save-excursion + (python-shell-send-region + (progn + (end-of-line 1) + (while (and (or (python-nav-beginning-of-defun) + (beginning-of-line 1)) + (> (current-indentation) 0))) + (when (not arg) + (while (and + (eq (forward-line -1) 0) + (if (looking-at (python-rx decorator)) + t + (forward-line 1) + nil)))) + (point-marker)) + (progn + (goto-char starting-pos) + (or (python-nav-end-of-defun) + (end-of-line 1)) + (point-marker)) + nil ;; noop + msg)))) (defun python-shell-send-file (file-name &optional process temp-file-name delete msg) @@ -3787,7 +3838,7 @@ the top stack frame has been reached. Filename is expected in the first parenthesized expression. Line number is expected in the second parenthesized expression." - :type 'string + :type 'regexp :version "27.1" :safe 'stringp) @@ -3802,7 +3853,7 @@ was `continue'. This behavior slightly differentiates the `continue' command from the `exit' command listed in `python-pdbtrack-exit-command'. See `python-pdbtrack-activate' for pdbtracking session overview." - :type 'list + :type '(repeat string) :version "27.1") (defcustom python-pdbtrack-exit-command '("q" "quit" "exit") @@ -3811,7 +3862,7 @@ After one of this commands is sent to pdb, pdbtracking session is considered over. See `python-pdbtrack-activate' for pdbtracking session overview." - :type 'list + :type '(repeat string) :version "27.1") (defcustom python-pdbtrack-kill-buffers t @@ -3954,8 +4005,8 @@ Argument OUTPUT is a string with the output from the comint process." "Setup pdb tracking in current buffer." (make-local-variable 'python-pdbtrack-buffers-to-kill) (make-local-variable 'python-pdbtrack-tracked-buffer) - (add-to-list (make-local-variable 'comint-input-filter-functions) - #'python-pdbtrack-comint-input-filter-function) + (add-hook 'comint-input-filter-functions + #'python-pdbtrack-comint-input-filter-function nil t) (add-to-list (make-local-variable 'comint-output-filter-functions) #'python-pdbtrack-comint-output-filter-function) (add-function :before (process-sentinel (get-buffer-process (current-buffer))) @@ -4136,7 +4187,7 @@ JUSTIFY should be used (if applicable) as in `fill-paragraph'." (goto-char (point-max))) (point-marker))) (multi-line-p - ;; Docstring styles may vary for oneliners and multi-liners. + ;; Docstring styles may vary for one-liners and multi-liners. (> (count-matches "\n" str-start-pos str-end-pos) 0)) (delimiters-style (pcase python-fill-docstring-style @@ -4562,7 +4613,7 @@ returns will be used. If not FORCE-PROCESS is passed what :type 'boolean :version "25.1") -(defun python-eldoc-function () +(defun python-eldoc-function (&rest _ignored) "`eldoc-documentation-function' for Python. For this to work as best as possible you should call `python-shell-send-buffer' from time to time so context in @@ -4591,9 +4642,7 @@ Interactively, prompt for symbol." (interactive (let ((symbol (python-eldoc--get-symbol-at-point)) (enable-recursive-minibuffers t)) - (list (read-string (if symbol - (format "Describe symbol (default %s): " symbol) - "Describe symbol: ") + (list (read-string (format-prompt "Describe symbol" symbol) nil nil symbol)))) (message (python-eldoc--get-doc-at-point symbol))) @@ -5137,21 +5186,22 @@ point's current `syntax-ppss'." (>= 2 (let (last-backward-sexp-point) - (while (save-excursion - (python-nav-backward-sexp) - (setq backward-sexp-point (point)) - (and (= indentation (current-indentation)) - ;; Make sure we're always moving point. - ;; If we get stuck in the same position - ;; on consecutive loop iterations, - ;; bail out. - (prog1 (not (eql last-backward-sexp-point - backward-sexp-point)) - (setq last-backward-sexp-point - backward-sexp-point)) - (looking-at-p - (concat "[uU]?[rR]?" - (python-rx string-delimiter))))) + (while (and (<= counter 2) + (save-excursion + (python-nav-backward-sexp) + (setq backward-sexp-point (point)) + (and (= indentation (current-indentation)) + ;; Make sure we're always moving point. + ;; If we get stuck in the same position + ;; on consecutive loop iterations, + ;; bail out. + (prog1 (not (eql last-backward-sexp-point + backward-sexp-point)) + (setq last-backward-sexp-point + backward-sexp-point)) + (looking-at-p + (concat "[uU]?[rR]?" + (python-rx string-delimiter)))))) ;; Previous sexp was a string, restore point. (goto-char backward-sexp-point) (cl-incf counter)) @@ -5343,7 +5393,7 @@ To use `flake8' you would set this to (\"flake8\" \"-\")." :group 'python-flymake :type '(repeat string)) -;; The default regexp accomodates for older pyflakes, which did not +;; The default regexp accommodates for older pyflakes, which did not ;; report the column number, and at the same time it's compatible with ;; flake8 output, although it may be redefined to explicitly match the ;; TYPE @@ -5542,12 +5592,16 @@ REPORT-FN is Flymake's callback function." (current-column)))) (^ '(- (1+ (current-indentation)))))) - (if (null eldoc-documentation-function) - ;; Emacs<25 - (set (make-local-variable 'eldoc-documentation-function) - #'python-eldoc-function) - (add-function :before-until (local 'eldoc-documentation-function) - #'python-eldoc-function)) + (with-no-warnings + ;; suppress warnings about eldoc-documentation-function being obsolete + (if (null eldoc-documentation-function) + ;; Emacs<25 + (set (make-local-variable 'eldoc-documentation-function) + #'python-eldoc-function) + (if (boundp 'eldoc-documentation-functions) + (add-hook 'eldoc-documentation-functions #'python-eldoc-function nil t) + (add-function :before-until (local 'eldoc-documentation-function) + #'python-eldoc-function)))) (add-to-list 'hs-special-modes-alist |