X-Git-Url: https://code.delx.au/gnu-emacs/blobdiff_plain/01633a17e74e638f31ec71c3587481f0084574f2..b1d5ab0352b6132eee4feaf9091ce7dc7a3e6a84:/lisp/progmodes/python.el diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 33039a4d08..b230992fc9 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -1,11 +1,11 @@ -;;; python.el --- Python's flying circus support for Emacs +;;; python.el --- Python's flying circus support for Emacs -*- lexical-binding: t -*- -;; Copyright (C) 2003-2013 Free Software Foundation, Inc. +;; Copyright (C) 2003-2014 Free Software Foundation, Inc. ;; Author: Fabián E. Gallina ;; URL: https://github.com/fgallina/python.el -;; Version: 0.24.2 -;; Maintainer: FSF +;; Version: 0.24.4 +;; Maintainer: emacs-devel@gnu.org ;; Created: Jul 2010 ;; Keywords: languages @@ -31,9 +31,9 @@ ;; found in GNU/Emacs. ;; Implements Syntax highlighting, Indentation, Movement, Shell -;; interaction, Shell completion, Shell virtualenv support, Pdb -;; tracking, Symbol completion, Skeletons, FFAP, Code Check, Eldoc, -;; Imenu. +;; interaction, Shell completion, Shell virtualenv support, Shell +;; package support, Shell syntax highlighting, Pdb tracking, Symbol +;; completion, Skeletons, FFAP, Code Check, Eldoc, Imenu. ;; Syntax highlighting: Fontification of code is provided and supports ;; python's triple quoted strings properly. @@ -62,57 +62,80 @@ ;; (add-hook 'python-mode-hook ;; (lambda () (setq forward-sexp-function nil))) -;; Shell interaction: is provided and allows you to execute easily any -;; block of code of your current buffer in an inferior Python process. +;; Shell interaction: is provided and allows opening Python shells +;; inside Emacs and executing any block of code of your current buffer +;; in that inferior Python process. + +;; Besides that only the standard CPython (2.x and 3.x) shell and +;; IPython are officially supported out of the box, the interaction +;; should support any other readline based Python shells as well +;; (e.g. Jython and Pypy have been reported to work). You can change +;; your default interpreter and commandline arguments by setting the +;; `python-shell-interpreter' and `python-shell-interpreter-args' +;; variables. This example enables IPython globally: + +;; (setq python-shell-interpreter "ipython" +;; python-shell-interpreter-args "-i") + +;; Using the "console" subcommand to start IPython in server-client +;; mode is known to fail intermittently due a bug on IPython itself +;; (see URL `http://debbugs.gnu.org/cgi/bugreport.cgi?bug=18052#27'). +;; There seems to be a race condition in the IPython server (A.K.A +;; kernel) when code is sent while it is still initializing, sometimes +;; causing the shell to get stalled. With that said, if an IPython +;; kernel is already running, "console --existing" seems to work fine. + +;; Running IPython on Windows needs more tweaking. The way you should +;; set `python-shell-interpreter' and `python-shell-interpreter-args' +;; is as follows (of course you need to modify the paths according to +;; your system): + +;; (setq python-shell-interpreter "C:\\Python27\\python.exe" +;; python-shell-interpreter-args +;; "-i C:\\Python27\\Scripts\\ipython-script.py") + +;; If you are experiencing missing or delayed output in your shells, +;; that's likely caused by your Operating System's pipe buffering +;; (e.g. this is known to happen running CPython 3.3.4 in Windows 7. +;; See URL `http://debbugs.gnu.org/cgi/bugreport.cgi?bug=17304'). To +;; fix this, using CPython's "-u" commandline argument or setting the +;; "PYTHONUNBUFFERED" environment variable should help: See URL +;; `https://docs.python.org/3/using/cmdline.html#cmdoption-u'. + +;; The interaction relies upon having prompts for input (e.g. ">>> " +;; and "... " in standard Python shell) and output (e.g. "Out[1]: " in +;; IPython) detected properly. Failing that Emacs may hang but, in +;; the case that happens, you can recover with \\[keyboard-quit]. To +;; avoid this issue, a two-step prompt autodetection mechanism is +;; provided: the first step is manual and consists of a collection of +;; regular expressions matching common prompts for Python shells +;; stored in `python-shell-prompt-input-regexps' and +;; `python-shell-prompt-output-regexps', and dir-local friendly vars +;; `python-shell-prompt-regexp', `python-shell-prompt-block-regexp', +;; `python-shell-prompt-output-regexp' which are appended to the +;; former automatically when a shell spawns; the second step is +;; automatic and depends on the `python-shell-prompt-detect' helper +;; function. See its docstring for details on global variables that +;; modify its behavior. ;; Shell completion: hitting tab will try to complete the current -;; word. Shell completion is implemented in a manner that if you -;; change the `python-shell-interpreter' to any other (for example -;; IPython) it should be easy to integrate another way to calculate -;; completions. You just need to specify your custom -;; `python-shell-completion-setup-code' and -;; `python-shell-completion-string-code'. - -;; Here is a complete example of the settings you would use for -;; iPython 0.11: - -;; (setq -;; python-shell-interpreter "ipython" -;; python-shell-interpreter-args "" -;; python-shell-prompt-regexp "In \\[[0-9]+\\]: " -;; python-shell-prompt-output-regexp "Out\\[[0-9]+\\]: " -;; python-shell-completion-setup-code -;; "from IPython.core.completerlib import module_completion" -;; python-shell-completion-module-string-code -;; "';'.join(module_completion('''%s'''))\n" -;; python-shell-completion-string-code -;; "';'.join(get_ipython().Completer.all_completions('''%s'''))\n") - -;; For iPython 0.10 everything would be the same except for -;; `python-shell-completion-string-code' and -;; `python-shell-completion-module-string-code': - -;; (setq python-shell-completion-string-code -;; "';'.join(__IP.complete('''%s'''))\n" -;; python-shell-completion-module-string-code "") - -;; Unfortunately running iPython on Windows needs some more tweaking. -;; The way you must set `python-shell-interpreter' and -;; `python-shell-interpreter-args' is as follows: - -;; (setq -;; python-shell-interpreter "C:\\Python27\\python.exe" -;; python-shell-interpreter-args -;; "-i C:\\Python27\\Scripts\\ipython-script.py") - -;; That will spawn the iPython process correctly (Of course you need -;; to modify the paths according to your system). - -;; Please note that the default completion system depends on the +;; word. Shell completion is implemented in such way that if you +;; change the `python-shell-interpreter' it should be possible to +;; integrate custom logic to calculate completions. To achieve this +;; you just need to set `python-shell-completion-setup-code' and +;; `python-shell-completion-string-code'. The default provided code, +;; enables autocompletion for both CPython and IPython (and ideally +;; any readline based Python shell). This code depends on the ;; readline module, so if you are using some Operating System that -;; bundles Python without it (like Windows) just install the -;; pyreadline from http://ipython.scipy.org/moin/PyReadline/Intro and -;; you should be good to go. +;; bundles Python without it (like Windows), installing pyreadline +;; from URL `http://ipython.scipy.org/moin/PyReadline/Intro' should +;; suffice. To troubleshoot why you are not getting any completions +;; you can try the following in your Python shell: + +;; >>> import readline, rlcompleter + +;; If you see an error, then you need to either install pyreadline or +;; setup custom code that avoids that dependency. ;; Shell virtualenv support: The shell also contains support for ;; virtualenvs and other special environment modifications thanks to @@ -147,6 +170,16 @@ ;; introduced as simple way of adding paths to the PYTHONPATH without ;; affecting existing values. +;; Shell package support: you can enable a package in the current +;; shell so that relative imports work properly using the +;; `python-shell-package-enable' command. + +;; Shell syntax highlighting: when enabled current input in shell is +;; highlighted. The variable `python-shell-font-lock-enable' controls +;; activation of this feature globally when shells are started. +;; Activation/deactivation can be also controlled on the fly via the +;; `python-shell-font-lock-toggle' command. + ;; Pdb tracking: when you execute a block of code that contains some ;; call to pdb (or ipdb) it will prompt the block of code and will ;; follow the execution of pdb marking the current line with an arrow. @@ -155,15 +188,13 @@ ;; the shell completion in background so you should run ;; `python-shell-send-buffer' from time to time to get better results. -;; Skeletons: 6 skeletons are provided for simple inserting of class, -;; def, for, if, try and while. These skeletons are integrated with -;; abbrev. If you have `abbrev-mode' activated and +;; Skeletons: skeletons are provided for simple inserting of things like class, +;; def, for, import, if, try, and while. These skeletons are +;; integrated with abbrev. If you have `abbrev-mode' activated and ;; `python-skeleton-autoinsert' is set to t, then whenever you type ;; the name of any of those defined and hit SPC, they will be ;; automatically expanded. As an alternative you can use the defined -;; skeleton commands: `python-skeleton-class', `python-skeleton-def' -;; `python-skeleton-for', `python-skeleton-if', `python-skeleton-try' -;; and `python-skeleton-while'. +;; skeleton commands: `python-skeleton-'. ;; FFAP: You can find the filename for a given module when using ffap ;; out of the box. This feature needs an inferior python shell @@ -180,7 +211,7 @@ ;; Imenu: There are two index building functions to be used as ;; `imenu-create-index-function': `python-imenu-create-index' (the ;; default one, builds the alist in form of a tree) and -;; `python-imenu-create-flat-index'. See also +;; `python-imenu-create-flat-index'. See also ;; `python-imenu-format-item-label-function', ;; `python-imenu-format-parent-item-label-function', ;; `python-imenu-format-parent-item-jump-label-function' variables for @@ -213,7 +244,9 @@ ;;; Code: (require 'ansi-color) +(require 'cl-lib) (require 'comint) +(require 'json) ;; Avoid compiler warnings (defvar view-return-to-alist) @@ -253,6 +286,7 @@ (define-key map "\C-c\C-td" 'python-skeleton-def) (define-key map "\C-c\C-tf" 'python-skeleton-for) (define-key map "\C-c\C-ti" 'python-skeleton-if) + (define-key map "\C-c\C-tm" 'python-skeleton-import) (define-key map "\C-c\C-tt" 'python-skeleton-try) (define-key map "\C-c\C-tw" 'python-skeleton-while) ;; Shell interaction @@ -322,6 +356,13 @@ (or "def" "class" "if" "elif" "else" "try" "except" "finally" "for" "while" "with") symbol-end)) + (dedenter . ,(rx symbol-start + (or "elif" "else" "except" "finally") + symbol-end)) + (block-ender . ,(rx symbol-start + (or + "break" "continue" "pass" "raise" "return") + symbol-end)) (decorator . ,(rx line-start (* space) ?@ (any letter ?_) (* (any word ?_)))) (defun . ,(rx symbol-start (or "def" "class") symbol-end)) @@ -357,7 +398,7 @@ (defmacro python-rx (&rest regexps) "Python mode specialized rx macro. -This variant of `rx' supports common python named REGEXPS." +This variant of `rx' supports common Python named REGEXPS." (let ((rx-constituents (append python-rx-constituents rx-constituents))) (cond ((null regexps) (error "No regexp")) @@ -504,7 +545,7 @@ The type returned can be `comment', `string' or `paren'." (res nil)) (while (and (setq res (re-search-forward re limit t)) (or (python-syntax-context 'paren) - (equal (char-after (point-marker)) ?=)))) + (equal (char-after (point)) ?=)))) res)) (1 font-lock-variable-name-face nil nil)) ;; support for a, b, c = (1, 2, 3) @@ -528,7 +569,7 @@ The type returned can be `comment', `string' or `paren'." (defsubst python-syntax-count-quotes (quote-char &optional point limit) "Count number of quotes around point (max is 3). QUOTE-CHAR is the quote char to count. Optional argument POINT is -the point where scan starts (defaults to current point) and LIMIT +the point where scan starts (defaults to current point), and LIMIT is used to limit the scan." (let ((i 0)) (while (and (< i 3) @@ -631,18 +672,6 @@ It makes underscores and dots word constituent chars.") (defvar python-indent-levels '(0) "Levels of indentation available for `python-indent-line-function'.") -(defvar python-indent-dedenters '("else" "elif" "except" "finally") - "List of words that should be dedented. -These make `python-indent-calculate-indentation' subtract the value of -`python-indent-offset'.") - -(defvar python-indent-block-enders - '("break" "continue" "pass" "raise" "return") - "List of words that mark the end of a block. -These make `python-indent-calculate-indentation' subtract the -value of `python-indent-offset' when `python-indent-context' is -AFTER-LINE.") - (defun python-indent-guess-indent-offset () "Guess and set `python-indent-offset' for the current buffer." (interactive) @@ -683,7 +712,7 @@ AFTER-LINE.") (defun python-indent-context () "Get information on indentation context. Context information is returned with a cons with the form: - \(STATUS . START) + (STATUS . START) Where status can be any of the following symbols: @@ -693,6 +722,7 @@ Where status can be any of the following symbols: * after-backslash: Previous line ends in a backslash * after-beginning-of-block: Point is after beginning of block * after-line: Point is after normal line + * dedenter-statement: Point is on a dedenter statement. * no-indent: Point is at beginning of buffer or other special case START is the buffer position where the sexp starts." (save-restriction @@ -747,6 +777,8 @@ START is the buffer position where the sexp starts." (when (looking-at (python-rx block-start)) (point-marker))))) 'after-beginning-of-block) + ((when (setq start (python-info-dedenter-statement-p)) + 'dedenter-statement)) ;; After normal line ((setq start (save-excursion (back-to-indentation) @@ -777,22 +809,35 @@ START is the buffer position where the sexp starts." (goto-char context-start) (+ (current-indentation) python-indent-offset)) ;; When after a simple line just use previous line - ;; indentation, in the case current line starts with a - ;; `python-indent-dedenters' de-indent one level. + ;; indentation. (`after-line - (- - (save-excursion - (goto-char context-start) - (current-indentation)) - (if (or (save-excursion - (back-to-indentation) - (looking-at (regexp-opt python-indent-dedenters))) - (save-excursion - (python-util-forward-comment -1) - (python-nav-beginning-of-statement) - (looking-at (regexp-opt python-indent-block-enders)))) - python-indent-offset - 0))) + (let* ((pair (save-excursion + (goto-char context-start) + (cons + (current-indentation) + (python-info-beginning-of-block-p)))) + (context-indentation (car pair)) + ;; TODO: Separate block enders into its own case. + (adjustment + (if (save-excursion + (python-util-forward-comment -1) + (python-nav-beginning-of-statement) + (looking-at (python-rx block-ender))) + python-indent-offset + 0))) + (- context-indentation adjustment))) + ;; When point is on a dedenter statement, search for the + ;; opening block that corresponds to it and use its + ;; indentation. If no opening block is found just remove + ;; indentation as this is an invalid python file. + (`dedenter-statement + (let ((block-start-point + (python-info-dedenter-opening-block-position))) + (save-excursion + (if (not block-start-point) + 0 + (goto-char block-start-point) + (current-indentation))))) ;; When inside of a string, do nothing. just use the current ;; indentation. XXX: perhaps it would be a good idea to ;; invoke standard text indentation here @@ -919,16 +964,25 @@ START is the buffer position where the sexp starts." (defun python-indent-calculate-levels () "Calculate `python-indent-levels' and reset `python-indent-current-level'." - (let* ((indentation (python-indent-calculate-indentation)) - (remainder (% indentation python-indent-offset)) - (steps (/ (- indentation remainder) python-indent-offset))) - (setq python-indent-levels (list 0)) - (dotimes (step steps) - (push (* python-indent-offset (1+ step)) python-indent-levels)) - (when (not (eq 0 remainder)) - (push (+ (* python-indent-offset steps) remainder) python-indent-levels)) - (setq python-indent-levels (nreverse python-indent-levels)) - (setq python-indent-current-level (1- (length python-indent-levels))))) + (if (not (python-info-dedenter-statement-p)) + (let* ((indentation (python-indent-calculate-indentation)) + (remainder (% indentation python-indent-offset)) + (steps (/ (- indentation remainder) python-indent-offset))) + (setq python-indent-levels (list 0)) + (dotimes (step steps) + (push (* python-indent-offset (1+ step)) python-indent-levels)) + (when (not (eq 0 remainder)) + (push (+ (* python-indent-offset steps) remainder) python-indent-levels))) + (setq python-indent-levels + (or + (mapcar (lambda (pos) + (save-excursion + (goto-char pos) + (current-indentation))) + (python-info-dedenter-opening-block-positions)) + (list 0)))) + (setq python-indent-current-level (1- (length python-indent-levels)) + python-indent-levels (nreverse python-indent-levels))) (defun python-indent-toggle-levels () "Toggle `python-indent-current-level' over `python-indent-levels'." @@ -951,11 +1005,11 @@ variable `python-indent-current-level'. When the variable `last-command' is not equal to one of the symbols inside `python-indent-trigger-commands' and FORCE-TOGGLE -is nil it calculates possible indentation levels and saves it in -the variable `python-indent-levels'. Afterwards it sets the +is nil it calculates possible indentation levels and saves them +in the variable `python-indent-levels'. Afterwards it sets the variable `python-indent-current-level' correctly so offset is -equal to (`nth' `python-indent-current-level' -`python-indent-levels')" +equal to + (nth python-indent-current-level python-indent-levels)" (or (and (or (and (memq this-command python-indent-trigger-commands) (eq last-command this-command)) @@ -977,7 +1031,7 @@ equal to (`nth' `python-indent-current-level' (indent-to next-indent) (goto-char starting-pos)) (and follow-indentation-p (back-to-indentation))) - (python-info-closing-block-message)) + (python-info-dedenter-opening-block-message)) (defun python-indent-line-function () "`indent-line-function' for Python mode. @@ -988,9 +1042,9 @@ See `python-indent-line' for details." "De-indent current line." (interactive "*") (when (and (not (python-syntax-comment-or-string-p)) - (<= (point-marker) (save-excursion + (<= (point) (save-excursion (back-to-indentation) - (point-marker))) + (point))) (> (current-column) 0)) (python-indent-line t) t)) @@ -998,14 +1052,14 @@ See `python-indent-line' for details." (defun python-indent-dedent-line-backspace (arg) "De-indent current line. Argument ARG is passed to `backward-delete-char-untabify' when -point is not in between the indentation." +point is not in between the indentation." (interactive "*p") (when (not (python-indent-dedent-line)) (backward-delete-char-untabify arg))) (put 'python-indent-dedent-line-backspace 'delete-selection 'supersede) (defun python-indent-region (start end) - "Indent a python region automagically. + "Indent a Python region automagically. Called from a program, START and END specify the region to indent." (let ((deactivate-mark nil)) @@ -1057,14 +1111,12 @@ any lines in the region are indented less than COUNT columns." (while (< (point) end) (if (and (< (current-indentation) count) (not (looking-at "[ \t]*$"))) - (error "Can't shift all lines enough")) + (user-error "Can't shift all lines enough")) (forward-line)) (indent-rigidly start end (- count)))))) -(add-to-list 'debug-ignored-errors "^Can't shift all lines enough") - (defun python-indent-shift-right (start end &optional count) - "Shift lines contained in region START END by COUNT columns to the left. + "Shift lines contained in region START END by COUNT columns to the right. COUNT defaults to `python-indent-offset'. If region isn't active, the current line is shifted. The shifted region includes the lines in which START and END lie." @@ -1079,37 +1131,47 @@ the lines in which START and END lie." (defun python-indent-post-self-insert-function () "Adjust indentation after insertion of some characters. -This function is intended to be added to the -`post-self-insert-hook.' If a line renders a paren alone, after -adding a char before it, the line will be re-indented -automatically if needed." +This function is intended to be added to `post-self-insert-hook.' +If a line renders a paren alone, after adding a char before it, +the line will be re-indented automatically if needed." (when (and electric-indent-mode (eq (char-before) last-command-event)) (cond - ((and (not (bolp)) - (memq (char-after) '(?\) ?\] ?\}))) + ;; Electric indent inside parens + ((and + (not (bolp)) + (let ((paren-start (python-syntax-context 'paren))) + ;; Check that point is inside parens. + (when paren-start + (not + ;; Filter the case where input is happening in the same + ;; line where the open paren is. + (= (line-number-at-pos) + (line-number-at-pos paren-start))))) + ;; When content has been added before the closing paren or a + ;; comma has been inserted, it's ok to do the trick. + (or + (memq (char-after) '(?\) ?\] ?\})) + (eq (char-before) ?,))) (save-excursion (goto-char (line-beginning-position)) - ;; If after going to the beginning of line the point - ;; is still inside a paren it's ok to do the trick - (when (python-syntax-context 'paren) - (let ((indentation (python-indent-calculate-indentation))) - (when (< (current-indentation) indentation) - (indent-line-to indentation)))))) + (let ((indentation (python-indent-calculate-indentation))) + (when (< (current-indentation) indentation) + (indent-line-to indentation))))) + ;; Electric colon ((and (eq ?: last-command-event) (memq ?: electric-indent-chars) (not current-prefix-arg) + ;; Trigger electric colon only at end of line (eolp) + ;; Avoid re-indenting on extra colon (not (equal ?: (char-before (1- (point))))) - (not (python-syntax-comment-or-string-p))) - (let ((indentation (current-indentation)) - (calculated-indentation (python-indent-calculate-indentation))) - (python-info-closing-block-message) - (when (> indentation calculated-indentation) - (save-excursion - (indent-line-to calculated-indentation) - (when (not (python-info-closing-block-message)) - (indent-line-to indentation))))))))) + (not (python-syntax-comment-or-string-p)) + ;; Never re-indent at beginning of defun + (not (save-excursion + (python-nav-beginning-of-statement) + (python-info-looking-at-beginning-of-defun)))) + (python-indent-line))))) ;;; Navigation @@ -1164,20 +1226,20 @@ With positive ARG search backwards, else search forwards." (defun python-nav-beginning-of-defun (&optional arg) "Move point to `beginning-of-defun'. -With positive ARG search backwards else search forward. When ARG -is nil or 0 defaults to 1. When searching backwards nested -defuns are handled with care depending on current point -position. Return non-nil if point is moved to +With positive ARG search backwards else search forward. +ARG nil or 0 defaults to 1. When searching backwards, +nested defuns are handled with care depending on current +point position. Return non-nil if point is moved to `beginning-of-defun'." (when (or (null arg) (= arg 0)) (setq arg 1)) (let ((found)) - (cond ((and (eq this-command 'mark-defun) - (python-info-looking-at-beginning-of-defun))) - (t - (dotimes (i (if (> arg 0) arg (- arg))) - (when (and (python-nav--beginning-of-defun arg) - (not found)) - (setq found t))))) + (while (and (not (= arg 0)) + (let ((keep-searching-p + (python-nav--beginning-of-defun arg))) + (when (and keep-searching-p (null found)) + (setq found t)) + keep-searching-p)) + (setq arg (if (> arg 0) (1- arg) (1+ arg)))) found)) (defun python-nav-end-of-defun () @@ -1267,15 +1329,21 @@ nested definitions." (defun python-nav-beginning-of-statement () "Move to start of current statement." (interactive "^") - (while (and (or (back-to-indentation) t) - (not (bobp)) - (when (or - (save-excursion - (forward-line -1) - (python-info-line-ends-backslash-p)) - (python-syntax-context 'string) - (python-syntax-context 'paren)) - (forward-line -1)))) + (back-to-indentation) + (let* ((ppss (syntax-ppss)) + (context-point + (or + (python-syntax-context 'paren ppss) + (python-syntax-context 'string ppss)))) + (cond ((bobp)) + (context-point + (goto-char context-point) + (python-nav-beginning-of-statement)) + ((save-excursion + (forward-line -1) + (python-info-line-ends-backslash-p)) + (forward-line -1) + (python-nav-beginning-of-statement)))) (point-marker)) (defun python-nav-end-of-statement (&optional noend) @@ -1406,25 +1474,36 @@ backward to previous block." (and (goto-char starting-pos) nil) (and (not (= (point) starting-pos)) (point-marker))))) -(defun python-nav-lisp-forward-sexp-safe (&optional arg) - "Safe version of standard `forward-sexp'. -When ARG > 0 move forward, else if ARG is < 0." - (or arg (setq arg 1)) +(defun python-nav--lisp-forward-sexp (&optional arg) + "Standard version `forward-sexp'. +It ignores completely the value of `forward-sexp-function' by +setting it to nil before calling `forward-sexp'. With positive +ARG move forward only one sexp, else move backwards." (let ((forward-sexp-function) - (paren-regexp - (if (> arg 0) (python-rx close-paren) (python-rx open-paren))) - (search-fn - (if (> arg 0) #'re-search-forward #'re-search-backward))) + (arg (if (or (not arg) (> arg 0)) 1 -1))) + (forward-sexp arg))) + +(defun python-nav--lisp-forward-sexp-safe (&optional arg) + "Safe version of standard `forward-sexp'. +When at end of sexp (i.e. looking at a opening/closing paren) +skips it instead of throwing an error. With positive ARG move +forward only one sexp, else move backwards." + (let* ((arg (if (or (not arg) (> arg 0)) 1 -1)) + (paren-regexp + (if (> arg 0) (python-rx close-paren) (python-rx open-paren))) + (search-fn + (if (> arg 0) #'re-search-forward #'re-search-backward))) (condition-case nil - (forward-sexp arg) + (python-nav--lisp-forward-sexp arg) (error (while (and (funcall search-fn paren-regexp nil t) (python-syntax-context 'paren))))))) -(defun python-nav--forward-sexp (&optional dir) +(defun python-nav--forward-sexp (&optional dir safe) "Move to forward sexp. -With positive Optional argument DIR direction move forward, else -backwards." +With positive optional argument DIR direction move forward, else +backwards. When optional argument SAFE is non-nil do not throw +errors when at end of sexp, skip it instead." (setq dir (or dir 1)) (unless (= dir 0) (let* ((forward-p (if (> dir 0) @@ -1442,7 +1521,9 @@ backwards." (eq (syntax-class (syntax-after (1- (point)))) (car (string-to-syntax ")"))))) ;; Inside a paren or looking at it, lisp knows what to do. - (python-nav-lisp-forward-sexp-safe dir)) + (if safe + (python-nav--lisp-forward-sexp-safe dir) + (python-nav--lisp-forward-sexp dir))) (t ;; This part handles the lispy feel of ;; `python-nav-forward-sexp'. Knowing everything about the @@ -1456,7 +1537,9 @@ backwards." ((python-info-end-of-statement-p) 'statement-end))) (next-sexp-pos (save-excursion - (python-nav-lisp-forward-sexp-safe dir) + (if safe + (python-nav--lisp-forward-sexp-safe dir) + (python-nav--lisp-forward-sexp dir)) (point))) (next-sexp-context (save-excursion @@ -1510,23 +1593,48 @@ backwards." (python-nav-beginning-of-statement)) (t (goto-char next-sexp-pos)))))))))) -(defun python-nav--backward-sexp () - "Move to backward sexp." - (python-nav--forward-sexp -1)) - (defun python-nav-forward-sexp (&optional arg) - "Move forward across one block of code. -With ARG, do it that many times. Negative arg -N means -move backward N times." + "Move forward across expressions. +With ARG, do it that many times. Negative arg -N means move +backward N times." (interactive "^p") (or arg (setq arg 1)) (while (> arg 0) - (python-nav--forward-sexp) + (python-nav--forward-sexp 1) (setq arg (1- arg))) (while (< arg 0) - (python-nav--backward-sexp) + (python-nav--forward-sexp -1) (setq arg (1+ arg)))) +(defun python-nav-backward-sexp (&optional arg) + "Move backward across expressions. +With ARG, do it that many times. Negative arg -N means move +forward N times." + (interactive "^p") + (or arg (setq arg 1)) + (python-nav-forward-sexp (- arg))) + +(defun python-nav-forward-sexp-safe (&optional arg) + "Move forward safely across expressions. +With ARG, do it that many times. Negative arg -N means move +backward N times." + (interactive "^p") + (or arg (setq arg 1)) + (while (> arg 0) + (python-nav--forward-sexp 1 t) + (setq arg (1- arg))) + (while (< arg 0) + (python-nav--forward-sexp -1 t) + (setq arg (1+ arg)))) + +(defun python-nav-backward-sexp-safe (&optional arg) + "Move backward safely across expressions. +With ARG, do it that many times. Negative arg -N means move +forward N times." + (interactive "^p") + (or arg (setq arg 1)) + (python-nav-forward-sexp-safe (- arg))) + (defun python-nav--up-list (&optional dir) "Internal implementation of `python-nav-up-list'. DIR is always 1 or -1 and comes sanitized from @@ -1582,7 +1690,7 @@ This command assumes point is not in a string or comment." (defun python-nav-backward-up-list (&optional arg) "Move backward out of one level of parentheses (or blocks). With ARG, do this that many times. -A negative argument means move backward but still to a less deep spot. +A negative argument means move forward but still to a less deep spot. This command assumes point is not in a string or comment." (interactive "^p") (or arg (setq arg 1)) @@ -1636,37 +1744,68 @@ position, else returns nil." :type 'string :group 'python) +(defcustom python-shell-interpreter-interactive-arg "-i" + "Interpreter argument to force it to run interactively." + :type 'string + :version "24.4") + +(defcustom python-shell-prompt-detect-enabled t + "Non-nil enables autodetection of interpreter prompts." + :type 'boolean + :safe 'booleanp + :version "24.4") + +(defcustom python-shell-prompt-detect-failure-warning t + "Non-nil enables warnings when detection of prompts fail." + :type 'boolean + :safe 'booleanp + :version "24.4") + +(defcustom python-shell-prompt-input-regexps + '(">>> " "\\.\\.\\. " ; Python + "In \\[[0-9]+\\]: " ; IPython + " \\.\\.\\.: " ; IPython + ;; Using ipdb outside IPython may fail to cleanup and leave static + ;; IPython prompts activated, this adds some safeguard for that. + "In : " "\\.\\.\\.: ") + "List of regular expressions matching input prompts." + :type '(repeat string) + :version "24.4") + +(defcustom python-shell-prompt-output-regexps + '("" ; Python + "Out\\[[0-9]+\\]: " ; IPython + "Out :") ; ipdb safeguard + "List of regular expressions matching output prompts." + :type '(repeat string) + :version "24.4") + (defcustom python-shell-prompt-regexp ">>> " - "Regular Expression matching top\-level input prompt of python shell. + "Regular expression matching top level input prompt of Python shell. It should not contain a caret (^) at the beginning." - :type 'string - :group 'python - :safe 'stringp) + :type 'string) -(defcustom python-shell-prompt-block-regexp "[.][.][.] " - "Regular Expression matching block input prompt of python shell. +(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 - :group 'python - :safe 'stringp) + :type 'string) (defcustom python-shell-prompt-output-regexp "" - "Regular Expression matching output prompt of python shell. + "Regular expression matching output prompt of Python shell. It should not contain a caret (^) at the beginning." - :type 'string - :group 'python - :safe 'stringp) + :type 'string) (defcustom python-shell-prompt-pdb-regexp "[(<]*[Ii]?[Pp]db[>)]+ " - "Regular Expression matching pdb input prompt of python shell. + "Regular expression matching pdb input prompt of Python shell. It should not contain a caret (^) at the beginning." - :type 'string - :group 'python - :safe 'stringp) + :type 'string) + +(define-obsolete-variable-alias + 'python-shell-enable-font-lock 'python-shell-font-lock-enable "25.1") -(defcustom python-shell-enable-font-lock t - "Should syntax highlighting be enabled in the python shell buffer? -Restart the python shell after changing this variable for it to take effect." +(defcustom python-shell-font-lock-enable t + "Should syntax highlighting be enabled in the Python shell buffer? +Restart the Python shell after changing this variable for it to take effect." :type 'boolean :group 'python :safe 'booleanp) @@ -1732,6 +1871,169 @@ virtualenv." :type '(alist string) :group 'python) +(defvar python-shell--prompt-calculated-input-regexp nil + "Calculated input prompt regexp for inferior python shell. +Do not set this variable directly, instead use +`python-shell-prompt-set-calculated-regexps'.") + +(defvar python-shell--prompt-calculated-output-regexp nil + "Calculated output prompt regexp for inferior python shell. +Do not set this variable directly, instead use +`python-shell-set-prompt-regexp'.") + +(defun python-shell-prompt-detect () + "Detect prompts for the current `python-shell-interpreter'. +When prompts can be retrieved successfully from the +`python-shell-interpreter' run with +`python-shell-interpreter-interactive-arg', returns a list of +three elements, where the first two are input prompts and the +last one is an output prompt. When no prompts can be detected +and `python-shell-prompt-detect-failure-warning' is non-nil, +shows a warning with instructions to avoid hangs and returns nil. +When `python-shell-prompt-detect-enabled' is nil avoids any +detection and just returns nil." + (when python-shell-prompt-detect-enabled + (let* ((process-environment (python-shell-calculate-process-environment)) + (exec-path (python-shell-calculate-exec-path)) + (code (concat + "import sys\n" + "ps = [getattr(sys, 'ps%s' % i, '') for i in range(1,4)]\n" + ;; JSON is built manually for compatibility + "ps_json = '\\n[\"%s\", \"%s\", \"%s\"]\\n' % tuple(ps)\n" + "print (ps_json)\n" + "sys.exit(0)\n")) + (output + (with-temp-buffer + ;; TODO: improve error handling by using + ;; `condition-case' and displaying the error message to + ;; the user in the no-prompts warning. + (ignore-errors + (let ((code-file (python-shell--save-temp-file code))) + ;; Use `process-file' as it is remote-host friendly. + (process-file + python-shell-interpreter + code-file + '(t nil) + nil + python-shell-interpreter-interactive-arg) + ;; Try to cleanup + (delete-file code-file))) + (buffer-string))) + (prompts + (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) "[\"") + (ignore-errors + ;; Return prompts as a list, not vector + (append (json-read-from-string line) nil))))) + ;; 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 + ;; input prompt is the only one that can't be empty. + (when (and (= (length res) 3) + (cl-every #'stringp res) + (not (string= (car res) ""))) + (throw 'prompts res)))) + nil))) + (when (and (not prompts) + python-shell-prompt-detect-failure-warning) + (lwarn + '(python python-shell-prompt-regexp) + :warning + (concat + "Python shell prompts cannot be detected.\n" + "If your emacs session hangs when starting python shells\n" + "recover with `keyboard-quit' and then try fixing the\n" + "interactive flag for your interpreter by adjusting the\n" + "`python-shell-interpreter-interactive-arg' or add regexps\n" + "matching shell prompts in the directory-local friendly vars:\n" + " + `python-shell-prompt-regexp'\n" + " + `python-shell-prompt-block-regexp'\n" + " + `python-shell-prompt-output-regexp'\n" + "Or alternatively in:\n" + " + `python-shell-prompt-input-regexps'\n" + " + `python-shell-prompt-output-regexps'"))) + prompts))) + +(defun python-shell-prompt-validate-regexps () + "Validate all user provided regexps for prompts. +Signals `user-error' if any of these vars contain invalid +regexps: `python-shell-prompt-regexp', +`python-shell-prompt-block-regexp', +`python-shell-prompt-pdb-regexp', +`python-shell-prompt-output-regexp', +`python-shell-prompt-input-regexps', +`python-shell-prompt-output-regexps'." + (dolist (symbol (list 'python-shell-prompt-input-regexps + 'python-shell-prompt-output-regexps + 'python-shell-prompt-regexp + 'python-shell-prompt-block-regexp + 'python-shell-prompt-pdb-regexp + 'python-shell-prompt-output-regexp)) + (dolist (regexp (let ((regexps (symbol-value symbol))) + (if (listp regexps) + regexps + (list regexps)))) + (when (not (python-util-valid-regexp-p regexp)) + (user-error "Invalid regexp %s in `%s'" + regexp symbol))))) + +(defun python-shell-prompt-set-calculated-regexps () + "Detect and set input and output prompt regexps. +Build and set the values for `python-shell-input-prompt-regexp' +and `python-shell-output-prompt-regexp' using the values from +`python-shell-prompt-regexp', `python-shell-prompt-block-regexp', +`python-shell-prompt-pdb-regexp', +`python-shell-prompt-output-regexp', +`python-shell-prompt-input-regexps', +`python-shell-prompt-output-regexps' and detected prompts from +`python-shell-prompt-detect'." + (when (not (and python-shell--prompt-calculated-input-regexp + python-shell--prompt-calculated-output-regexp)) + (let* ((detected-prompts (python-shell-prompt-detect)) + (input-prompts nil) + (output-prompts nil) + (build-regexp + (lambda (prompts) + (concat "^\\(" + (mapconcat #'identity + (sort prompts + (lambda (a b) + (let ((length-a (length a)) + (length-b (length b))) + (if (= length-a length-b) + (string< a b) + (> (length a) (length b)))))) + "\\|") + "\\)")))) + ;; Validate ALL regexps + (python-shell-prompt-validate-regexps) + ;; Collect all user defined input prompts + (dolist (prompt (append python-shell-prompt-input-regexps + (list python-shell-prompt-regexp + python-shell-prompt-block-regexp + python-shell-prompt-pdb-regexp))) + (cl-pushnew prompt input-prompts :test #'string=)) + ;; Collect all user defined output prompts + (dolist (prompt (cons python-shell-prompt-output-regexp + python-shell-prompt-output-regexps)) + (cl-pushnew prompt output-prompts :test #'string=)) + ;; Collect detected prompts if any + (when detected-prompts + (dolist (prompt (butlast detected-prompts)) + (setq prompt (regexp-quote prompt)) + (cl-pushnew prompt input-prompts :test #'string=)) + (cl-pushnew (regexp-quote + (car (last detected-prompts))) + output-prompts :test #'string=)) + ;; Set input and output prompt regexps from collected prompts + (setq python-shell--prompt-calculated-input-regexp + (funcall build-regexp input-prompts) + python-shell--prompt-calculated-output-regexp + (funcall build-regexp output-prompts))))) + (defun python-shell-get-process-name (dedicated) "Calculate the appropriate process name for inferior Python process. If DEDICATED is t and the variable `buffer-file-name' is non-nil @@ -1754,10 +2056,10 @@ uniqueness for different types of configurations." python-shell-internal-buffer-name (md5 (concat - (python-shell-parse-command) - python-shell-prompt-regexp - python-shell-prompt-block-regexp - python-shell-prompt-output-regexp + python-shell-interpreter + python-shell-interpreter-args + python-shell--prompt-calculated-input-regexp + python-shell--prompt-calculated-output-regexp (mapconcat #'symbol-value python-shell-setup-codes "") (mapconcat #'identity python-shell-process-environment "") (mapconcat #'identity python-shell-extra-pythonpaths "") @@ -1765,14 +2067,27 @@ uniqueness for different types of configurations." (or python-shell-virtualenv-path "") (mapconcat #'identity python-shell-exec-path ""))))) -(defun python-shell-parse-command () +(defun python-shell-parse-command () ;FIXME: why name it "parse"? "Calculate the string used to execute the inferior Python process." + ;; FIXME: process-environment doesn't seem to be used anywhere within + ;; this let. (let ((process-environment (python-shell-calculate-process-environment)) (exec-path (python-shell-calculate-exec-path))) (format "%s %s" + ;; FIXME: Why executable-find? (executable-find python-shell-interpreter) python-shell-interpreter-args))) +(defun python-new-pythonpath () + "Calculate the new PYTHONPATH value from `python-shell-extra-pythonpaths'." + (let ((pythonpath (getenv "PYTHONPATH")) + (extra (mapconcat 'identity + python-shell-extra-pythonpaths + path-separator))) + (if pythonpath + (concat extra path-separator pythonpath) + extra))) + (defun python-shell-calculate-process-environment () "Calculate process environment given `python-shell-virtualenv-path'." (let ((process-environment (append @@ -1782,13 +2097,7 @@ uniqueness for different types of configurations." (directory-file-name python-shell-virtualenv-path) nil))) (when python-shell-extra-pythonpaths - (setenv "PYTHONPATH" - (format "%s%s%s" - (mapconcat 'identity - python-shell-extra-pythonpaths - path-separator) - path-separator - (or (getenv "PYTHONPATH") "")))) + (setenv "PYTHONPATH" (python-new-pythonpath))) (if (not virtualenv) process-environment (setenv "PYTHONHOME" nil) @@ -1801,109 +2110,311 @@ uniqueness for different types of configurations." (defun python-shell-calculate-exec-path () "Calculate exec path given `python-shell-virtualenv-path'." (let ((path (append python-shell-exec-path - exec-path nil))) + exec-path nil))) ;FIXME: Why nil? (if (not python-shell-virtualenv-path) path - (cons (format "%s/bin" - (directory-file-name python-shell-virtualenv-path)) + (cons (expand-file-name "bin" python-shell-virtualenv-path) path)))) -(defun python-comint-output-filter-function (output) - "Hook run after content is put into comint buffer. -OUTPUT is a string with the contents of the buffer." - (ansi-color-filter-apply output)) +(defvar python-shell--package-depth 10) + +(defun python-shell-package-enable (directory package) + "Add DIRECTORY parent to $PYTHONPATH and enable PACKAGE." + (interactive + (let* ((dir (expand-file-name + (read-directory-name + "Package root: " + (file-name-directory + (or (buffer-file-name) default-directory))))) + (name (completing-read + "Package: " + (python-util-list-packages + dir python-shell--package-depth)))) + (list dir name))) + (python-shell-send-string + (format + (concat + "import os.path;import sys;" + "sys.path.append(os.path.dirname(os.path.dirname('''%s''')));" + "__package__ = '''%s''';" + "import %s") + directory package package) + (python-shell-get-process))) + +(defun python-shell-accept-process-output (process &optional timeout regexp) + "Accept PROCESS output with TIMEOUT until REGEXP is found. +Optional argument TIMEOUT is the timeout argument to +`accept-process-output' calls. Optional argument REGEXP +overrides the regexp to match the end of output, defaults to +`comint-prompt-regexp.'. Returns non-nil when output was +properly captured. + +This utility is useful in situations where the output may be +received in chunks, since `accept-process-output' gives no +guarantees they will be grabbed in a single call. An example use +case for this would be the CPython shell start-up, where the +banner and the initial prompt are received separately." + (let ((regexp (or regexp comint-prompt-regexp))) + (catch 'found + (while t + (when (not (accept-process-output process timeout)) + (throw 'found nil)) + (when (looking-back regexp) + (throw 'found t)))))) + +(defun python-shell-comint-end-of-output-p (output) + "Return non-nil if OUTPUT is ends with input prompt." + (string-match + ;; XXX: It seems on OSX an extra carriage return is attached + ;; at the end of output, this handles that too. + (concat + "\r?\n?" + ;; Remove initial caret from calculated regexp + (replace-regexp-in-string + (rx string-start ?^) "" + python-shell--prompt-calculated-input-regexp) + (rx eos)) + output)) + +(define-obsolete-function-alias + 'python-comint-output-filter-function + 'ansi-color-filter-apply + "25.1") + +(defun python-comint-postoutput-scroll-to-bottom (output) + "Faster version of `comint-postoutput-scroll-to-bottom'. +Avoids `recenter' calls until OUTPUT is completely sent." + (when (and (not (string= "" output)) + (python-shell-comint-end-of-output-p + (ansi-color-filter-apply output))) + (comint-postoutput-scroll-to-bottom output)) + output) (defvar python-shell--parent-buffer nil) -(defvar python-shell-output-syntax-table - (let ((table (make-syntax-table python-dotty-syntax-table))) - (modify-syntax-entry ?\' "." table) - (modify-syntax-entry ?\" "." table) - (modify-syntax-entry ?\( "." table) - (modify-syntax-entry ?\[ "." table) - (modify-syntax-entry ?\{ "." table) - (modify-syntax-entry ?\) "." table) - (modify-syntax-entry ?\] "." table) - (modify-syntax-entry ?\} "." table) - table) - "Syntax table for shell output. -It makes parens and quotes be treated as punctuation chars.") +(defmacro python-shell-with-shell-buffer (&rest body) + "Execute the forms in BODY with the shell buffer temporarily current. +Signals an error if no shell buffer is available for current buffer." + (declare (indent 0) (debug t)) + (let ((shell-buffer (make-symbol "shell-buffer"))) + `(let ((,shell-buffer (python-shell-get-buffer))) + (when (not ,shell-buffer) + (error "No inferior Python buffer available.")) + (with-current-buffer ,shell-buffer + ,@body)))) + +(defvar python-shell--font-lock-buffer nil) + +(defun python-shell-font-lock-get-or-create-buffer () + "Get or create a font-lock buffer for current inferior process." + (python-shell-with-shell-buffer + (if python-shell--font-lock-buffer + python-shell--font-lock-buffer + (let ((process-name + (process-name (get-buffer-process (current-buffer))))) + (generate-new-buffer + (format "*%s-font-lock*" process-name)))))) + +(defun python-shell-font-lock-kill-buffer () + "Kill the font-lock buffer safely." + (python-shell-with-shell-buffer + (when (and python-shell--font-lock-buffer + (buffer-live-p python-shell--font-lock-buffer)) + (kill-buffer python-shell--font-lock-buffer) + (when (eq major-mode 'inferior-python-mode) + (setq python-shell--font-lock-buffer nil))))) + +(defmacro python-shell-font-lock-with-font-lock-buffer (&rest body) + "Execute the forms in BODY in the font-lock buffer. +The value returned is the value of the last form in BODY. See +also `with-current-buffer'." + (declare (indent 0) (debug t)) + `(python-shell-with-shell-buffer + (save-current-buffer + (when (not (and python-shell--font-lock-buffer + (get-buffer python-shell--font-lock-buffer))) + (setq python-shell--font-lock-buffer + (python-shell-font-lock-get-or-create-buffer))) + (set-buffer python-shell--font-lock-buffer) + (set (make-local-variable 'delay-mode-hooks) t) + (let ((python-indent-guess-indent-offset nil)) + (when (not (eq major-mode 'python-mode)) + (python-mode)) + ,@body)))) + +(defun python-shell-font-lock-cleanup-buffer () + "Cleanup the font-lock buffer. +Provided as a command because this might be handy if something +goes wrong and syntax highlighting in the shell gets messed up." + (interactive) + (python-shell-with-shell-buffer + (python-shell-font-lock-with-font-lock-buffer + (delete-region (point-min) (point-max))))) + +(defun python-shell-font-lock-comint-output-filter-function (output) + "Clean up the font-lock buffer after any OUTPUT." + (when (and (not (string= "" output)) + ;; Is end of output and is not just a prompt. + (not (member + (python-shell-comint-end-of-output-p + (ansi-color-filter-apply output)) + '(nil 0)))) + ;; If output is other than an input prompt then "real" output has + ;; been received and the font-lock buffer must be cleaned up. + (python-shell-font-lock-cleanup-buffer)) + output) + +(defun python-shell-font-lock-post-command-hook () + "Fontifies current line in shell buffer." + (if (eq this-command 'comint-send-input) + ;; Add a newline when user sends input as this may be a block. + (python-shell-font-lock-with-font-lock-buffer + (goto-char (line-end-position)) + (newline)) + (when (and (python-util-comint-last-prompt) + (> (point) (cdr (python-util-comint-last-prompt)))) + (let ((input (buffer-substring-no-properties + (cdr (python-util-comint-last-prompt)) (point-max))) + (old-input (python-shell-font-lock-with-font-lock-buffer + (buffer-substring-no-properties + (line-beginning-position) (point-max)))) + (current-point (point)) + (buffer-undo-list t)) + ;; When input hasn't changed, do nothing. + (when (not (string= input old-input)) + (delete-region (cdr (python-util-comint-last-prompt)) (point-max)) + (insert + (python-shell-font-lock-with-font-lock-buffer + (delete-region (line-beginning-position) + (line-end-position)) + (insert input) + ;; Ensure buffer is fontified, keeping it + ;; compatible with Emacs < 24.4. + (if (fboundp 'font-lock-ensure) + (funcall 'font-lock-ensure) + (font-lock-default-fontify-buffer)) + ;; Replace FACE text properties with FONT-LOCK-FACE so + ;; they are not overwritten by comint buffer's font lock. + (python-util-text-properties-replace-name + 'face 'font-lock-face) + (buffer-substring (line-beginning-position) + (line-end-position)))) + (goto-char current-point)))))) + +(defun python-shell-font-lock-turn-on (&optional msg) + "Turn on shell font-lock. +With argument MSG show activation message." + (interactive "p") + (python-shell-with-shell-buffer + (python-shell-font-lock-kill-buffer) + (set (make-local-variable 'python-shell--font-lock-buffer) nil) + (add-hook 'post-command-hook + #'python-shell-font-lock-post-command-hook nil 'local) + (add-hook 'kill-buffer-hook + #'python-shell-font-lock-kill-buffer nil 'local) + (add-hook 'comint-output-filter-functions + #'python-shell-font-lock-comint-output-filter-function + 'append 'local) + (when msg + (message "Shell font-lock is enabled")))) + +(defun python-shell-font-lock-turn-off (&optional msg) + "Turn off shell font-lock. +With argument MSG show deactivation message." + (interactive "p") + (python-shell-with-shell-buffer + (python-shell-font-lock-kill-buffer) + (when (python-util-comint-last-prompt) + ;; Cleanup current fontification + (remove-text-properties + (cdr (python-util-comint-last-prompt)) + (line-end-position) + '(face nil font-lock-face nil))) + (set (make-local-variable 'python-shell--font-lock-buffer) nil) + (remove-hook 'post-command-hook + #'python-shell-font-lock-post-command-hook'local) + (remove-hook 'kill-buffer-hook + #'python-shell-font-lock-kill-buffer 'local) + (remove-hook 'comint-output-filter-functions + #'python-shell-font-lock-comint-output-filter-function + 'local) + (when msg + (message "Shell font-lock is disabled")))) + +(defun python-shell-font-lock-toggle (&optional msg) + "Toggle font-lock for shell. +With argument MSG show activation/deactivation message." + (interactive "p") + (python-shell-with-shell-buffer + (set (make-local-variable 'python-shell-font-lock-enable) + (not python-shell-font-lock-enable)) + (if python-shell-font-lock-enable + (python-shell-font-lock-turn-on msg) + (python-shell-font-lock-turn-off msg)) + python-shell-font-lock-enable)) (define-derived-mode inferior-python-mode comint-mode "Inferior Python" "Major mode for Python inferior process. Runs a Python interpreter as a subprocess of Emacs, with Python -I/O through an Emacs buffer. Variables -`python-shell-interpreter' and `python-shell-interpreter-args' -controls which Python interpreter is run. Variables +I/O through an Emacs buffer. Variables `python-shell-interpreter' +and `python-shell-interpreter-args' control which Python +interpreter is run. Variables `python-shell-prompt-regexp', `python-shell-prompt-output-regexp', `python-shell-prompt-block-regexp', -`python-shell-enable-font-lock', +`python-shell-font-lock-enable', `python-shell-completion-setup-code', `python-shell-completion-string-code', -`python-shell-completion-module-string-code', `python-eldoc-setup-code', `python-eldoc-string-code', `python-ffap-setup-code' and `python-ffap-string-code' can customize this mode for different Python interpreters. +This mode resets `comint-output-filter-functions' locally, so you +may want to re-add custom functions to it using the +`inferior-python-mode-hook'. + You can also add additional setup code to be run at initialization of the interpreter via `python-shell-setup-codes' variable. \(Type \\[describe-mode] in the process buffer for a list of commands.)" - (and python-shell--parent-buffer - (python-util-clone-local-variables python-shell--parent-buffer)) - (setq comint-prompt-regexp (format "^\\(?:%s\\|%s\\|%s\\)" - python-shell-prompt-regexp - python-shell-prompt-block-regexp - python-shell-prompt-pdb-regexp)) + (let ((interpreter python-shell-interpreter) + (args python-shell-interpreter-args)) + (when python-shell--parent-buffer + (python-util-clone-local-variables python-shell--parent-buffer)) + ;; Users can override default values for these vars when calling + ;; `run-python'. This ensures new values let-bound in + ;; `python-shell-make-comint' are locally set. + (set (make-local-variable 'python-shell-interpreter) interpreter) + (set (make-local-variable 'python-shell-interpreter-args) args)) + (set (make-local-variable 'python-shell--prompt-calculated-input-regexp) nil) + (set (make-local-variable 'python-shell--prompt-calculated-output-regexp) nil) + (python-shell-prompt-set-calculated-regexps) + (setq comint-prompt-regexp python-shell--prompt-calculated-input-regexp + comint-prompt-read-only t) (setq mode-line-process '(":%s")) - (make-local-variable 'comint-output-filter-functions) - (add-hook 'comint-output-filter-functions - 'python-comint-output-filter-function) - (add-hook 'comint-output-filter-functions - 'python-pdbtrack-comint-output-filter-function) + (set (make-local-variable 'comint-output-filter-functions) + '(ansi-color-process-output + python-pdbtrack-comint-output-filter-function + python-comint-postoutput-scroll-to-bottom)) (set (make-local-variable 'compilation-error-regexp-alist) python-shell-compilation-regexp-alist) - (define-key inferior-python-mode-map [remap complete-symbol] - 'completion-at-point) (add-hook 'completion-at-point-functions - 'python-shell-completion-complete-at-point nil 'local) - (add-to-list (make-local-variable 'comint-dynamic-complete-functions) - 'python-shell-completion-complete-at-point) + #'python-shell-completion-at-point nil 'local) (define-key inferior-python-mode-map "\t" 'python-shell-completion-complete-or-indent) (make-local-variable 'python-pdbtrack-buffers-to-kill) (make-local-variable 'python-pdbtrack-tracked-buffer) (make-local-variable 'python-shell-internal-last-output) - (when python-shell-enable-font-lock - (set-syntax-table python-mode-syntax-table) - (set (make-local-variable 'font-lock-defaults) - '(python-font-lock-keywords nil nil nil nil)) - (set (make-local-variable 'syntax-propertize-function) - (eval - ;; XXX: Unfortunately eval is needed here to make use of the - ;; dynamic value of `comint-prompt-regexp'. - `(syntax-propertize-rules - (,comint-prompt-regexp - (0 (ignore - (put-text-property - comint-last-input-start end 'syntax-table - python-shell-output-syntax-table) - ;; XXX: This might look weird, but it is the easiest - ;; way to ensure font lock gets cleaned up before the - ;; current prompt, which is needed for unclosed - ;; strings to not mess up with current input. - (font-lock-unfontify-region comint-last-input-start end)))) - (,(python-rx string-delimiter) - (0 (ignore - (and (not (eq (get-text-property start 'field) 'output)) - (python-syntax-stringify))))))))) - (compilation-shell-minor-mode 1)) + (when python-shell-font-lock-enable + (python-shell-font-lock-turn-on)) + (compilation-shell-minor-mode 1) + (python-shell-accept-process-output + (get-buffer-process (current-buffer)))) (defun python-shell-make-comint (cmd proc-name &optional pop internal) - "Create a python shell comint buffer. -CMD is the python command to be executed and PROC-NAME is the + "Create a Python shell comint buffer. +CMD is the Python command to be executed and PROC-NAME is the process name the comint buffer will get. After the comint buffer is created the `inferior-python-mode' is activated. When optional argument POP is non-nil the buffer is shown. When @@ -1919,13 +2430,22 @@ killed." (exec-path (python-shell-calculate-exec-path))) (when (not (comint-check-proc proc-buffer-name)) (let* ((cmdlist (split-string-and-unquote cmd)) + (interpreter (car cmdlist)) + (args (cdr cmdlist)) (buffer (apply #'make-comint-in-buffer proc-name proc-buffer-name - (car cmdlist) nil (cdr cmdlist))) + interpreter nil args)) (python-shell--parent-buffer (current-buffer)) - (process (get-buffer-process buffer))) + (process (get-buffer-process buffer)) + ;; As the user may have overridden default values for + ;; these vars on `run-python', let-binding them allows + ;; to have the new right values in all setup code + ;; that's is done in `inferior-python-mode', which is + ;; important, especially for prompt detection. + (python-shell-interpreter interpreter) + (python-shell-interpreter-args + (mapconcat #'identity args " "))) (with-current-buffer buffer (inferior-python-mode)) - (accept-process-output process) (and pop (pop-to-buffer buffer t)) (and internal (set-process-query-on-exit-flag process nil)))) proc-buffer-name))) @@ -1942,13 +2462,13 @@ command used to call the interpreter and define DEDICATED, so a dedicated process for the current buffer is open. When numeric prefix arg is other than 0 or 4 do not SHOW. -Runs the hook `inferior-python-mode-hook' (after the -`comint-mode-hook' is run). \(Type \\[describe-mode] in the +Runs the hook `inferior-python-mode-hook' after +`comint-mode-hook' is run. (Type \\[describe-mode] in the process buffer for a list of commands.)" (interactive (if current-prefix-arg (list - (read-string "Run Python: " (python-shell-parse-command)) + (read-shell-command "Run Python: " (python-shell-parse-command)) (y-or-n-p "Make dedicated process? ") (= (prefix-numeric-value current-prefix-arg) 4)) (list (python-shell-parse-command) nil t))) @@ -1963,15 +2483,15 @@ Input and output via buffer named after `python-shell-internal-get-process-name' returns. This new kind of shell is intended to be used for generic -communication related to defined configurations, the main +communication related to defined configurations; the main difference with global or dedicated shells is that these ones are attached to a configuration, not a buffer. This means that can be used for example to retrieve the sys.path and other stuff, without messing with user shells. Note that -`python-shell-enable-font-lock' and `inferior-python-mode-hook' +`python-shell-font-lock-enable' and `inferior-python-mode-hook' are set to nil for these shells, so setup codes are not sent at startup." - (let ((python-shell-enable-font-lock nil) + (let ((python-shell-font-lock-enable nil) (inferior-python-mode-hook nil)) (get-buffer-process (python-shell-make-comint @@ -1979,38 +2499,38 @@ startup." (python-shell-internal-get-process-name) nil t)))) (defun python-shell-get-buffer () - "Get inferior Python buffer for current buffer and return it." - (let* ((dedicated-proc-name (python-shell-get-process-name t)) - (dedicated-proc-buffer-name (format "*%s*" dedicated-proc-name)) - (global-proc-name (python-shell-get-process-name nil)) - (global-proc-buffer-name (format "*%s*" global-proc-name)) - (dedicated-running (comint-check-proc dedicated-proc-buffer-name)) - (global-running (comint-check-proc global-proc-buffer-name))) - ;; Always prefer dedicated - (or (and dedicated-running dedicated-proc-buffer-name) - (and global-running global-proc-buffer-name)))) + "Return inferior Python buffer for current buffer. +If current buffer is in `inferior-python-mode', return it." + (if (eq major-mode 'inferior-python-mode) + (current-buffer) + (let* ((dedicated-proc-name (python-shell-get-process-name t)) + (dedicated-proc-buffer-name (format "*%s*" dedicated-proc-name)) + (global-proc-name (python-shell-get-process-name nil)) + (global-proc-buffer-name (format "*%s*" global-proc-name)) + (dedicated-running (comint-check-proc dedicated-proc-buffer-name)) + (global-running (comint-check-proc global-proc-buffer-name))) + ;; Always prefer dedicated + (or (and dedicated-running dedicated-proc-buffer-name) + (and global-running global-proc-buffer-name))))) (defun python-shell-get-process () - "Get inferior Python process for current buffer and return it." + "Return inferior Python process for current buffer." (get-buffer-process (python-shell-get-buffer))) -(defun python-shell-get-or-create-process () - "Get or create an inferior Python process for current buffer and return it." - (let* ((dedicated-proc-name (python-shell-get-process-name t)) - (dedicated-proc-buffer-name (format "*%s*" dedicated-proc-name)) - (global-proc-name (python-shell-get-process-name nil)) - (global-proc-buffer-name (format "*%s*" global-proc-name)) - (dedicated-running (comint-check-proc dedicated-proc-buffer-name)) - (global-running (comint-check-proc global-proc-buffer-name)) - (current-prefix-arg 16)) - (when (and (not dedicated-running) (not global-running)) - (if (call-interactively 'run-python) - (setq dedicated-running t) - (setq global-running t))) - ;; Always prefer dedicated - (get-buffer-process (if dedicated-running - dedicated-proc-buffer-name - global-proc-buffer-name)))) +(defun python-shell-get-or-create-process (&optional cmd dedicated show) + "Get or create an inferior Python process for current buffer and return it. +Arguments CMD, DEDICATED and SHOW are those of `run-python' and +are used to start the shell. If those arguments are not +provided, `run-python' is called interactively and the user will +be asked for their values." + (let ((shell-process (python-shell-get-process))) + (when (not shell-process) + (if (not cmd) + ;; XXX: Refactor code such that calling `run-python' + ;; interactively is not needed anymore. + (call-interactively 'run-python) + (run-python cmd dedicated show))) + (or shell-process (python-shell-get-process)))) (defvar python-shell-internal-buffer nil "Current internal shell buffer for the current buffer. @@ -2028,13 +2548,7 @@ there for compatibility with CEDET.") (proc-buffer-name (format " *%s*" proc-name))) (when (not (process-live-p proc-name)) (run-python-internal) - (setq python-shell-internal-buffer proc-buffer-name) - ;; XXX: Why is this `sit-for' needed? - ;; `python-shell-make-comint' calls `accept-process-output' - ;; already but it is not helping to get proper output on - ;; 'gnu/linux when the internal shell process is not running and - ;; a call to `python-shell-internal-send-string' is issued. - (sit-for 0.1 t)) + (setq python-shell-internal-buffer proc-buffer-name)) (get-buffer-process proc-buffer-name))) (define-obsolete-function-alias @@ -2060,8 +2574,7 @@ there for compatibility with CEDET.") temp-file-name)) (defun python-shell-send-string (string &optional process) - "Send STRING to inferior Python PROCESS. -When MSG is non-nil messages the first line of STRING." + "Send STRING to inferior Python PROCESS." (interactive "sPython command: ") (let ((process (or process (python-shell-get-or-create-process)))) (if (string-match ".\n+." string) ;Multiline. @@ -2085,13 +2598,7 @@ detecting a prompt at the end of the buffer." string (ansi-color-filter-apply string) python-shell-output-filter-buffer (concat python-shell-output-filter-buffer string)) - (when (string-match - ;; XXX: It seems on OSX an extra carriage return is attached - ;; at the end of output, this handles that too. - (format "\r?\n\\(?:%s\\|%s\\|%s\\)$" - python-shell-prompt-regexp - python-shell-prompt-block-regexp - python-shell-prompt-pdb-regexp) + (when (python-shell-comint-end-of-output-p python-shell-output-filter-buffer) ;; Output ends when `python-shell-output-filter-buffer' contains ;; the prompt attached at the end of it. @@ -2099,10 +2606,10 @@ detecting a prompt at the end of the buffer." python-shell-output-filter-buffer (substring python-shell-output-filter-buffer 0 (match-beginning 0))) - (when (and (> (length python-shell-prompt-output-regexp) 0) - (string-match (concat "^" python-shell-prompt-output-regexp) - python-shell-output-filter-buffer)) - ;; Some shells, like iPython might append a prompt before the + (when (string-match + python-shell--prompt-calculated-output-regexp + python-shell-output-filter-buffer) + ;; Some shells, like IPython might append a prompt before the ;; output, clean that. (setq python-shell-output-filter-buffer (substring python-shell-output-filter-buffer (match-end 0))))) @@ -2110,8 +2617,7 @@ detecting a prompt at the end of the buffer." (defun python-shell-send-string-no-output (string &optional process) "Send STRING to PROCESS and inhibit output. -When MSG is non-nil messages the first line of STRING. Return -the output." +Return the output." (let ((process (or process (python-shell-get-or-create-process))) (comint-preoutput-filter-functions '(python-shell-output-filter)) @@ -2160,7 +2666,7 @@ source file.") This is a wrapper over `buffer-substring' that takes care of different transformations for the code sent to be evaluated in the python shell: - 1. When Optional Argument NOMAIN is non-nil everything under an + 1. When optional argument NOMAIN is non-nil everything under an \"if __name__ == '__main__'\" block will be removed. 2. When a subregion of the buffer is sent, it takes care of appending extra empty lines so tracebacks are correct. @@ -2194,7 +2700,7 @@ the python shell: (save-excursion (when (python-nav-if-name-main) (cons (point) - (progn (python-nav-forward-sexp) + (progn (python-nav-forward-sexp-safe) (point))))))) ;; Oh destructuring bind, how I miss you. (if-name-main-start (car if-name-main-start-end)) @@ -2232,7 +2738,7 @@ the python shell: (defun python-shell-send-buffer (&optional arg) "Send the entire buffer to inferior Python process. With prefix ARG allow execution of code inside blocks delimited -by \"if __name__== '__main__':\"" +by \"if __name__== '__main__':\"." (interactive "P") (save-restriction (widen) @@ -2263,8 +2769,8 @@ When argument ARG is non-nil do not include decorators." delete) "Send FILE-NAME to inferior Python PROCESS. If TEMP-FILE-NAME is passed then that file is used for processing -instead, while internally the shell will continue to use -FILE-NAME. If DELETE is non-nil, delete the file afterwards." +instead, while internally the shell will continue to use FILE-NAME. +If DELETE is non-nil, delete the file afterwards." (interactive "fFile to send: ") (let* ((process (or process (python-shell-get-or-create-process))) (temp-file-name (when temp-file-name @@ -2292,18 +2798,24 @@ FILE-NAME. If DELETE is non-nil, delete the file afterwards." (defun python-shell-switch-to-shell () "Switch to inferior Python process buffer." (interactive) - (pop-to-buffer (process-buffer (python-shell-get-or-create-process)) t)) + (process-buffer (python-shell-get-or-create-process)) t) (defun python-shell-send-setup-code () "Send all setup code for shell. This function takes the list of setup code to send from the `python-shell-setup-codes' list." - (let ((process (get-buffer-process (current-buffer)))) - (dolist (code python-shell-setup-codes) - (when code - (message "Sent %s" code) - (python-shell-send-string - (symbol-value code) process))))) + (let ((process (python-shell-get-process)) + (code (concat + (mapconcat + (lambda (elt) + (cond ((stringp elt) elt) + ((symbolp elt) (symbol-value elt)) + (t ""))) + python-shell-setup-codes + "\n\n") + "\n\nprint ('python.el: sent setup code')"))) + (python-shell-send-string code process) + (python-shell-accept-process-output process))) (add-hook 'inferior-python-mode-hook #'python-shell-send-setup-code) @@ -2313,23 +2825,35 @@ This function takes the list of setup code to send from the (defcustom python-shell-completion-setup-code "try: - import readline + import readline, rlcompleter except ImportError: - def __COMPLETER_all_completions(text): [] + def __PYTHON_EL_get_completions(text): + return [] else: - import rlcompleter - readline.set_completer(rlcompleter.Completer().complete) - def __COMPLETER_all_completions(text): - import sys + def __PYTHON_EL_get_completions(text): completions = [] try: - i = 0 - while True: - res = readline.get_completer()(text, i) - if not res: break - i += 1 - completions.append(res) - except NameError: + splits = text.split() + is_module = splits and splits[0] in ('from', 'import') + is_ipython = getattr( + __builtins__, '__IPYTHON__', + getattr(__builtins__, '__IPYTHON__active', False)) + if is_module: + from IPython.core.completerlib import module_completion + completions = module_completion(text.strip()) + elif is_ipython and getattr(__builtins__, '__IP', None): + completions = __IP.complete(text) + elif is_ipython and getattr(__builtins__, 'get_ipython', None): + completions = get_ipython().Completer.all_completions(text) + else: + i = 0 + while True: + res = readline.get_completer()(text, i) + if not res: + break + i += 1 + completions.append(res) + except: pass return completions" "Code used to setup completion in inferior Python processes." @@ -2337,126 +2861,106 @@ else: :group 'python) (defcustom python-shell-completion-string-code - "';'.join(__COMPLETER_all_completions('''%s'''))\n" - "Python code used to get a string of completions separated by semicolons." + "';'.join(__PYTHON_EL_get_completions('''%s'''))\n" + "Python code used to get a string of completions separated by semicolons. +The string passed to the function is the current python name or +the full statement in the case of imports." :type 'string :group 'python) -(defcustom python-shell-completion-module-string-code "" - "Python code used to get completions separated by semicolons for imports. - -For IPython v0.11, add the following line to -`python-shell-completion-setup-code': - -from IPython.core.completerlib import module_completion - -and use the following as the value of this variable: - -';'.join(module_completion('''%s'''))\n" - :type 'string - :group 'python) - -(defcustom python-shell-completion-pdb-string-code - "';'.join(globals().keys() + locals().keys())" - "Python code used to get completions separated by semicolons for [i]pdb." - :type 'string - :group 'python) +(define-obsolete-variable-alias + 'python-shell-completion-module-string-code + 'python-shell-completion-string-code + "24.4" + "Completion string code must also autocomplete modules.") -(defun python-shell-completion-get-completions (process line input) - "Do completion at point for PROCESS. -LINE is used to detect the context on how to complete given -INPUT." - (let* ((prompt - ;; Get last prompt of the inferior process buffer (this - ;; intentionally avoids using `comint-last-prompt' because - ;; of incompatibilities with Emacs 24.x). - (with-current-buffer (process-buffer process) +(define-obsolete-variable-alias + 'python-shell-completion-pdb-string-code + 'python-shell-completion-string-code + "25.1" + "Completion string code must work for (i)pdb.") + +(defun python-shell-completion-get-completions (process import input) + "Do completion at point using PROCESS for IMPORT or INPUT. +When IMPORT is non-nil takes precedence over INPUT for +completion." + (with-current-buffer (process-buffer process) + (let* ((prompt + ;; Get last prompt of the inferior process buffer (this + ;; intentionally avoids using `comint-last-prompt' because + ;; of incompatibilities with Emacs 24.x). (save-excursion (buffer-substring-no-properties - (- (point) (length line)) - (progn - (re-search-backward "^") - (python-util-forward-comment) - (point)))))) - (completion-context - ;; Check whether a prompt matches a pdb string, an import - ;; statement or just the standard prompt and use the - ;; correct python-shell-completion-*-code string - (cond ((and (> (length python-shell-completion-pdb-string-code) 0) - (string-match - (concat "^" python-shell-prompt-pdb-regexp) prompt)) - 'pdb) - ((and (> - (length python-shell-completion-module-string-code) 0) - (string-match - (concat "^" python-shell-prompt-regexp) prompt) - (string-match "^[ \t]*\\(from\\|import\\)[ \t]" line)) - 'import) - ((string-match - (concat "^" python-shell-prompt-regexp) prompt) - 'default) - (t nil))) - (completion-code - (pcase completion-context - (`pdb python-shell-completion-pdb-string-code) - (`import python-shell-completion-module-string-code) - (`default python-shell-completion-string-code) - (_ nil))) - (input - (if (eq completion-context 'import) - (replace-regexp-in-string "^[ \t]+" "" line) - input))) - (and completion-code - (> (length input) 0) - (with-current-buffer (process-buffer process) - (let ((completions (python-shell-send-string-no-output - (format completion-code input) process))) + (line-beginning-position) ;End of prompt. + (re-search-backward "^")))) + (completion-code + ;; Check whether a prompt matches a pdb string, an import + ;; statement or just the standard prompt and use the + ;; correct python-shell-completion-*-code string + (cond ((and (string-match + (concat "^" python-shell-prompt-pdb-regexp) prompt)) + ;; Since there are no guarantees the user will remain + ;; in the same context where completion code was sent + ;; (e.g. user steps into a function), safeguard + ;; resending completion setup continuously. + (concat python-shell-completion-setup-code + "\nprint (" python-shell-completion-string-code ")")) + ((string-match + python-shell--prompt-calculated-input-regexp prompt) + python-shell-completion-string-code) + (t nil))) + (subject (or import input))) + (and completion-code + (> (length input) 0) + (let ((completions + (python-util-strip-string + (python-shell-send-string-no-output + (format completion-code subject) process)))) (and (> (length completions) 2) (split-string completions "^'\\|^\"\\|;\\|'$\\|\"$" t))))))) -(defun python-shell-completion-complete-at-point (&optional process) - "Perform completion at point in inferior Python. +(defun python-shell-completion-at-point (&optional process) + "Function for `completion-at-point-functions' in `inferior-python-mode'. Optional argument PROCESS forces completions to be retrieved using that one instead of current buffer's process." (setq process (or process (get-buffer-process (current-buffer)))) - (let* ((start + (let* ((last-prompt-end (cdr (python-util-comint-last-prompt))) + (import-statement + (when (string-match-p + (rx (* space) word-start (or "from" "import") word-end space) + (buffer-substring-no-properties last-prompt-end (point))) + (buffer-substring-no-properties last-prompt-end (point)))) + (start (save-excursion - (with-syntax-table python-dotty-syntax-table - (let* ((paren-depth (car (syntax-ppss))) - (syntax-string "w_") - (syntax-list (string-to-syntax syntax-string))) - ;; Stop scanning for the beginning of the completion - ;; subject after the char before point matches a - ;; delimiter - (while (member - (car (syntax-after (1- (point)))) syntax-list) - (skip-syntax-backward syntax-string) - (when (or (equal (char-before) ?\)) - (equal (char-before) ?\")) - (forward-char -1)) - (while (or - ;; honor initial paren depth - (> (car (syntax-ppss)) paren-depth) - (python-syntax-context 'string)) - (forward-char -1))) - (point))))) + (if (not (re-search-backward + (python-rx + (or whitespace open-paren close-paren string-delimiter)) + last-prompt-end + t 1)) + last-prompt-end + (forward-char (length (match-string-no-properties 0))) + (point)))) (end (point))) (list start end (completion-table-dynamic (apply-partially #'python-shell-completion-get-completions - process (buffer-substring-no-properties - (line-beginning-position) end)))))) + process import-statement))))) + +(define-obsolete-function-alias + 'python-shell-completion-complete-at-point + 'python-shell-completion-at-point + "25.1") (defun python-shell-completion-complete-or-indent () "Complete or indent depending on the context. -If content before pointer is all whitespace indent. If not try -to complete." +If content before pointer is all whitespace, indent. +If not try to complete." (interactive) (if (string-match "^[[:space:]]*$" (buffer-substring (comint-line-beginning-position) - (point-marker))) + (point))) (indent-for-tab-command) (completion-at-point))) @@ -2464,14 +2968,14 @@ to complete." ;;; PDB Track integration (defcustom python-pdbtrack-activate t - "Non-nil makes python shell enable pdbtracking." + "Non-nil makes Python shell enable pdbtracking." :type 'boolean :group 'python :safe 'booleanp) (defcustom python-pdbtrack-stacktrace-info-regexp - "^> \\([^\"(<]+\\)(\\([0-9]+\\))\\([?a-zA-Z0-9_<>]+\\)()" - "Regular Expression matching stacktrace information. + "> \\([^\"(<]+\\)(\\([0-9]+\\))\\([?a-zA-Z0-9_<>]+\\)()" + "Regular expression matching stacktrace information. Used to extract the current line and module being inspected." :type 'string :group 'python @@ -2489,7 +2993,9 @@ Never set this variable directly, use "Set the buffer for FILE-NAME as the tracked buffer. Internally it uses the `python-pdbtrack-tracked-buffer' variable. Returns the tracked buffer." - (let ((file-buffer (get-file-buffer file-name))) + (let ((file-buffer (get-file-buffer + (concat (file-remote-p default-directory) + file-name)))) (if file-buffer (setq python-pdbtrack-tracked-buffer file-buffer) (setq file-buffer (find-file-noselect file-name)) @@ -2553,18 +3059,19 @@ Argument OUTPUT is a string with the output from the comint process." ;;; Symbol completion -(defun python-completion-complete-at-point () - "Complete current symbol at point. -For this to work the best as possible you should call +(defun python-completion-at-point () + "Function for `completion-at-point-functions' in `python-mode'. +For this to work as best as possible you should call `python-shell-send-buffer' from time to time so context in -inferior python process is updated properly." +inferior Python process is updated properly." (let ((process (python-shell-get-process))) - (if (not process) - (error "Completion needs an inferior Python process running") - (python-shell-completion-complete-at-point process)))) + (when process + (python-shell-completion-at-point process)))) -(add-to-list 'debug-ignored-errors - "^Completion needs an inferior Python process running.") +(define-obsolete-function-alias + 'python-completion-complete-at-point + 'python-completion-at-point + "25.1") ;;; Fill paragraph @@ -2602,12 +3109,12 @@ fill parens." This affects `python-fill-string' behavior with regards to triple quotes positioning. -Possible values are DJANGO, ONETWO, PEP-257, PEP-257-NN, -SYMMETRIC, and NIL. A value of NIL won't care about quotes +Possible values are `django', `onetwo', `pep-257', `pep-257-nn', +`symmetric', and nil. A value of nil won't care about quotes position and will treat docstrings a normal string, any other value may result in one of the following docstring styles: -DJANGO: +`django': \"\"\" Process foo, return bar. @@ -2619,7 +3126,7 @@ DJANGO: If processing fails throw ProcessingError. \"\"\" -ONETWO: +`onetwo': \"\"\"Process foo, return bar.\"\"\" @@ -2630,7 +3137,7 @@ ONETWO: \"\"\" -PEP-257: +`pep-257': \"\"\"Process foo, return bar.\"\"\" @@ -2640,7 +3147,7 @@ PEP-257: \"\"\" -PEP-257-NN: +`pep-257-nn': \"\"\"Process foo, return bar.\"\"\" @@ -2649,7 +3156,7 @@ PEP-257-NN: If processing fails throw ProcessingError. \"\"\" -SYMMETRIC: +`symmetric': \"\"\"Process foo, return bar.\"\"\" @@ -2784,17 +3291,17 @@ JUSTIFY should be used (if applicable) as in `fill-paragraph'." (save-restriction (narrow-to-region (progn (while (python-syntax-context 'paren) - (goto-char (1- (point-marker)))) - (point-marker) + (goto-char (1- (point)))) (line-beginning-position)) (progn (when (not (python-syntax-context 'paren)) (end-of-line) (when (not (python-syntax-context 'paren)) (skip-syntax-backward "^)"))) - (while (python-syntax-context 'paren) - (goto-char (1+ (point-marker)))) - (point-marker))) + (while (and (python-syntax-context 'paren) + (not (eobp))) + (goto-char (1+ (point)))) + (point))) (let ((paragraph-start "\f\\|[ \t]*$") (paragraph-separate ",") (fill-paragraph-function)) @@ -2803,7 +3310,8 @@ JUSTIFY should be used (if applicable) as in `fill-paragraph'." (while (not (eobp)) (forward-line 1) (python-indent-line) - (goto-char (line-end-position)))) t) + (goto-char (line-end-position)))) + t) ;;; Skeletons @@ -2902,6 +3410,12 @@ The skeleton will be bound to python-skeleton-NAME." > _ \n '(python-skeleton--else) | ^) +(python-skeleton-define import nil + "Import from module: " + "from " str & " " | -5 + "import " + ("Identifier: " str ", ") -2 \n _) + (python-skeleton-define try nil nil "try:" \n @@ -2928,7 +3442,7 @@ The skeleton will be bound to python-skeleton-NAME." "class " str "(" ("Inheritance, %s: " (unless (equal ?\( (char-before)) ", ") str) - & ")" | -2 + & ")" | -1 ":" \n "\"\"\"" - "\"\"\"" \n > _ \n) @@ -3006,8 +3520,8 @@ The skeleton will be bound to python-skeleton-NAME." (defun python-check (command) "Check a Python file (default current buffer's file). -Runs COMMAND, a shell command, as if by `compile'. See -`python-check-command' for the default." +Runs COMMAND, a shell command, as if by `compile'. +See `python-check-command' for the default." (interactive (list (read-string "Check command: " (or python-check-custom-command @@ -3072,13 +3586,11 @@ Runs COMMAND, a shell command, as if by `compile'. See (defun python-eldoc--get-doc-at-point (&optional force-input force-process) "Internal implementation to get documentation at point. -If not FORCE-INPUT is passed then what -`python-info-current-symbol' returns will be used. If not -FORCE-PROCESS is passed what `python-shell-get-process' returns -is used." +If not FORCE-INPUT is passed then what `python-info-current-symbol' +returns will be used. If not FORCE-PROCESS is passed what +`python-shell-get-process' returns is used." (let ((process (or force-process (python-shell-get-process)))) - (if (not process) - (error "Eldoc needs an inferior Python process running") + (when process (let ((input (or force-input (python-info-current-symbol t)))) (and input @@ -3088,9 +3600,9 @@ is used." (defun python-eldoc-function () "`eldoc-documentation-function' for Python. -For this to work the best as possible you should call +For this to work as best as possible you should call `python-shell-send-buffer' from time to time so context in -inferior python process is updated properly." +inferior Python process is updated properly." (python-eldoc--get-doc-at-point)) (defun python-eldoc-at-point (symbol) @@ -3105,9 +3617,6 @@ Interactively, prompt for symbol." nil nil symbol)))) (message (python-eldoc--get-doc-at-point symbol))) -(add-to-list 'debug-ignored-errors - "^Eldoc needs an inferior Python process running.") - ;;; Imenu @@ -3127,15 +3636,15 @@ It must be a function with two arguments: TYPE and NAME.") It must be a function with two arguments: TYPE and NAME.") (defun python-imenu-format-item-label (type name) - "Return imenu label for single node using TYPE and NAME." + "Return Imenu label for single node using TYPE and NAME." (format "%s (%s)" name type)) (defun python-imenu-format-parent-item-label (type name) - "Return imenu label for parent node using TYPE and NAME." + "Return Imenu label for parent node using TYPE and NAME." (format "%s..." (python-imenu-format-item-label type name))) (defun python-imenu-format-parent-item-jump-label (type _name) - "Return imenu label for parent node jump using TYPE and NAME." + "Return Imenu label for parent node jump using TYPE and NAME." (if (string= type "class") "*class definition*" "*function definition*")) @@ -3152,7 +3661,7 @@ It must be a function with two arguments: TYPE and NAME.") (defun python-imenu--build-tree (&optional min-indent prev-indent tree) "Recursively build the tree of nested definitions of a node. -Arguments MIN-INDENT PREV-INDENT and TREE are internal and should +Arguments MIN-INDENT, PREV-INDENT and TREE are internal and should not be passed explicitly unless you know what you are doing." (setq min-indent (or min-indent 0) prev-indent (or prev-indent python-indent-offset)) @@ -3193,7 +3702,7 @@ not be passed explicitly unless you know what you are doing." tree))))))) (defun python-imenu-create-index () - "Return tree Imenu alist for the current python buffer. + "Return tree Imenu alist for the current Python buffer. Change `python-imenu-format-item-label-function', `python-imenu-format-parent-item-label-function', `python-imenu-format-parent-item-jump-label-function' to @@ -3206,18 +3715,19 @@ customize how labels are formatted." index)) (defun python-imenu-create-flat-index (&optional alist prefix) - "Return flat outline of the current python buffer for Imenu. -Optional Argument ALIST is the tree to be flattened, when nil + "Return flat outline of the current Python buffer for Imenu. +Optional argument ALIST is the tree to be flattened; when nil `python-imenu-build-index' is used with `python-imenu-format-parent-item-jump-label-function' `python-imenu-format-parent-item-label-function' -`python-imenu-format-item-label-function' set to (lambda (type -name) name). Optional Argument PREFIX is used in recursive calls -and should not be passed explicitly. +`python-imenu-format-item-label-function' set to + (lambda (type name) name) +Optional argument PREFIX is used in recursive calls and should +not be passed explicitly. Converts this: - \((\"Foo\" . 103) + ((\"Foo\" . 103) (\"Bar\" . 138) (\"decorator\" (\"decorator\" . 173) @@ -3227,7 +3737,7 @@ Converts this: To this: - \((\"Foo\" . 103) + ((\"Foo\" . 103) (\"Bar\" . 138) (\"decorator\" . 173) (\"decorator.wrap\" . 353) @@ -3260,9 +3770,8 @@ To this: (defun python-info-current-defun (&optional include-type) "Return name of surrounding function with Python compatible dotty syntax. Optional argument INCLUDE-TYPE indicates to include the type of the defun. -This function is compatible to be used as -`add-log-current-defun-function' since it returns nil if point is -not inside a defun." +This function can be used as the value of `add-log-current-defun-function' +since it returns nil if point is not inside a defun." (save-restriction (widen) (save-excursion @@ -3381,49 +3890,88 @@ parent defun name." (and (python-info-end-of-statement-p) (python-info-statement-ends-block-p))) -(defun python-info-closing-block () - "Return the point of the block the current line closes." - (let ((closing-word (save-excursion - (back-to-indentation) - (current-word))) - (indentation (current-indentation))) - (when (member closing-word python-indent-dedenters) +(define-obsolete-function-alias + 'python-info-closing-block + 'python-info-dedenter-opening-block-position "24.4") + +(defun python-info-dedenter-opening-block-position () + "Return the point of the closest block the current line closes. +Returns nil if point is not on a dedenter statement or no opening +block can be detected. The latter case meaning current file is +likely an invalid python file." + (let ((positions (python-info-dedenter-opening-block-positions)) + (indentation (current-indentation)) + (position)) + (while (and (not position) + positions) (save-excursion - (forward-line -1) - (while (and (> (current-indentation) indentation) - (not (bobp)) - (not (back-to-indentation)) - (forward-line -1))) - (back-to-indentation) - (cond - ((not (equal indentation (current-indentation))) nil) - ((string= closing-word "elif") - (when (member (current-word) '("if" "elif")) - (point-marker))) - ((string= closing-word "else") - (when (member (current-word) '("if" "elif" "except" "for" "while")) - (point-marker))) - ((string= closing-word "except") - (when (member (current-word) '("try")) - (point-marker))) - ((string= closing-word "finally") - (when (member (current-word) '("except" "else")) - (point-marker)))))))) - -(defun python-info-closing-block-message (&optional closing-block-point) - "Message the contents of the block the current line closes. -With optional argument CLOSING-BLOCK-POINT use that instead of -recalculating it calling `python-info-closing-block'." - (let ((point (or closing-block-point (python-info-closing-block)))) + (goto-char (car positions)) + (if (<= (current-indentation) indentation) + (setq position (car positions)) + (setq positions (cdr positions))))) + position)) + +(defun python-info-dedenter-opening-block-positions () + "Return points of blocks the current line may close sorted by closer. +Returns nil if point is not on a dedenter statement or no opening +block can be detected. The latter case meaning current file is +likely an invalid python file." + (save-excursion + (let ((dedenter-pos (python-info-dedenter-statement-p))) + (when dedenter-pos + (goto-char dedenter-pos) + (let* ((pairs '(("elif" "elif" "if") + ("else" "if" "elif" "except" "for" "while") + ("except" "except" "try") + ("finally" "else" "except" "try"))) + (dedenter (match-string-no-properties 0)) + (possible-opening-blocks (cdr (assoc-string dedenter pairs))) + (collected-indentations) + (opening-blocks)) + (catch 'exit + (while (python-nav--syntactically + (lambda () + (re-search-backward (python-rx block-start) nil t)) + #'<) + (let ((indentation (current-indentation))) + (when (and (not (memq indentation collected-indentations)) + (or (not collected-indentations) + (< indentation (apply #'min collected-indentations)))) + (setq collected-indentations + (cons indentation collected-indentations)) + (when (member (match-string-no-properties 0) + possible-opening-blocks) + (setq opening-blocks (cons (point) opening-blocks)))) + (when (zerop indentation) + (throw 'exit nil))))) + ;; sort by closer + (nreverse opening-blocks)))))) + +(define-obsolete-function-alias + 'python-info-closing-block-message + 'python-info-dedenter-opening-block-message "24.4") + +(defun python-info-dedenter-opening-block-message () + "Message the first line of the block the current statement closes." + (let ((point (python-info-dedenter-opening-block-position))) (when point (save-restriction (widen) (message "Closes %s" (save-excursion (goto-char point) - (back-to-indentation) (buffer-substring (point) (line-end-position)))))))) +(defun python-info-dedenter-statement-p () + "Return point if current statement is a dedenter. +Sets `match-data' to the keyword that starts the dedenter +statement." + (save-excursion + (python-nav-beginning-of-statement) + (when (and (not (python-syntax-context-type)) + (looking-at (python-rx dedenter))) + (point)))) + (defun python-info-line-ends-backslash-p (&optional line-number) "Return non-nil if current line ends with backslash. With optional argument LINE-NUMBER, check that line instead." @@ -3525,13 +4073,13 @@ operator." (looking-at python-nav-beginning-of-defun-regexp)))) (defun python-info-current-line-comment-p () - "Check if current line is a comment line." + "Return non-nil if current line is a comment line." (char-equal (or (char-after (+ (line-beginning-position) (current-indentation))) ?_) ?#)) (defun python-info-current-line-empty-p () - "Check if current line is empty, ignoring whitespace." + "Return non-nil if current line is empty, ignoring whitespace." (save-excursion (beginning-of-line 1) (looking-at @@ -3562,6 +4110,18 @@ to \"^python-\"." (cdr pair)))) (buffer-local-variables from-buffer))) +(defvar comint-last-prompt-overlay) ; Shut up, byte compiler. + +(defun python-util-comint-last-prompt () + "Return comint last prompt overlay start and end. +This is for compatibility with Emacs < 24.4." + (cond ((bound-and-true-p comint-last-prompt-overlay) + (cons (overlay-start comint-last-prompt-overlay) + (overlay-end comint-last-prompt-overlay))) + ((bound-and-true-p comint-last-prompt) + comint-last-prompt) + (t nil))) + (defun python-util-forward-comment (&optional direction) "Python mode specific version of `forward-comment'. Optional argument DIRECTION defines the direction to move to." @@ -3573,11 +4133,73 @@ Optional argument DIRECTION defines the direction to move to." (goto-char comment-start)) (forward-comment factor))) +(defun python-util-list-directories (directory &optional predicate max-depth) + "List DIRECTORY subdirs, filtered by PREDICATE and limited by MAX-DEPTH. +Argument PREDICATE defaults to `identity' and must be a function +that takes one argument (a full path) and returns non-nil for +allowed files. When optional argument MAX-DEPTH is non-nil, stop +searching when depth is reached, else don't limit." + (let* ((dir (expand-file-name directory)) + (dir-length (length dir)) + (predicate (or predicate #'identity)) + (to-scan (list dir)) + (tally nil)) + (while to-scan + (let ((current-dir (car to-scan))) + (when (funcall predicate current-dir) + (setq tally (cons current-dir tally))) + (setq to-scan (append (cdr to-scan) + (python-util-list-files + current-dir #'file-directory-p) + nil)) + (when (and max-depth + (<= max-depth + (length (split-string + (substring current-dir dir-length) + "/\\|\\\\" t)))) + (setq to-scan nil)))) + (nreverse tally))) + +(defun python-util-list-files (dir &optional predicate) + "List files in DIR, filtering with PREDICATE. +Argument PREDICATE defaults to `identity' and must be a function +that takes one argument (a full path) and returns non-nil for +allowed files." + (let ((dir-name (file-name-as-directory dir))) + (apply #'nconc + (mapcar (lambda (file-name) + (let ((full-file-name (expand-file-name file-name dir-name))) + (when (and + (not (member file-name '("." ".."))) + (funcall (or predicate #'identity) full-file-name)) + (list full-file-name)))) + (directory-files dir-name))))) + +(defun python-util-list-packages (dir &optional max-depth) + "List packages in DIR, limited by MAX-DEPTH. +When optional argument MAX-DEPTH is non-nil, stop searching when +depth is reached, else don't limit." + (let* ((dir (expand-file-name dir)) + (parent-dir (file-name-directory + (directory-file-name + (file-name-directory + (file-name-as-directory dir))))) + (subpath-length (length parent-dir))) + (mapcar + (lambda (file-name) + (replace-regexp-in-string + (rx (or ?\\ ?/)) "." (substring file-name subpath-length))) + (python-util-list-directories + (directory-file-name dir) + (lambda (dir) + (file-exists-p (expand-file-name "__init__.py" dir))) + max-depth)))) + (defun python-util-popn (lst n) "Return LST first N elements. -N should be an integer, when it's a natural negative number its -opposite is used. When N is bigger than the length of LST, the -list is returned as is." +N should be an integer, when negative its opposite is used. +When N is bigger than the length of LST, the list is +returned as is." (let* ((n (min (abs n))) (len (length lst)) (acc)) @@ -3589,6 +4211,35 @@ list is returned as is." n (1- n))) (reverse acc)))) +(defun python-util-text-properties-replace-name + (from to &optional start end) + "Replace properties named FROM to TO, keeping its value. +Arguments START and END narrow the buffer region to work on." + (save-excursion + (goto-char (or start (point-min))) + (while (not (eobp)) + (let ((plist (text-properties-at (point))) + (next-change (or (next-property-change (point) (current-buffer)) + (or end (point-max))))) + (when (plist-get plist from) + (let* ((face (plist-get plist from)) + (plist (plist-put plist from nil)) + (plist (plist-put plist to face))) + (set-text-properties (point) next-change plist (current-buffer)))) + (goto-char next-change))))) + +(defun python-util-strip-string (string) + "Strip STRING whitespace and newlines from end and beginning." + (replace-regexp-in-string + (rx (or (: string-start (* (any whitespace ?\r ?\n))) + (: (* (any whitespace ?\r ?\n)) string-end))) + "" + string)) + +(defun python-util-valid-regexp-p (regexp) + "Return non-nil if REGEXP is valid." + (ignore-errors (string-match regexp "") t)) + (defun python-electric-pair-string-delimiter () (when (and electric-pair-mode @@ -3596,8 +4247,9 @@ list is returned as is." (let ((count 0)) (while (eq (char-before (- (point) count)) last-command-event) (cl-incf count)) - (= count 3))) - (save-excursion (insert (make-string 3 last-command-event))))) + (= count 3)) + (eq (char-after) last-command-event)) + (save-excursion (insert (make-string 2 last-command-event))))) (defvar electric-indent-inhibit) @@ -3645,7 +4297,7 @@ list is returned as is." #'python-nav-end-of-defun) (add-hook 'completion-at-point-functions - #'python-completion-complete-at-point nil 'local) + #'python-completion-at-point nil 'local) (add-hook 'post-self-insert-hook #'python-indent-post-self-insert-function 'append 'local) @@ -3670,13 +4322,12 @@ list is returned as is." (add-to-list 'hs-special-modes-alist `(python-mode "^\\s-*\\(?:def\\|class\\)\\>" nil "#" ,(lambda (_arg) - (python-nav-end-of-defun)) nil)) - - (set (make-local-variable 'mode-require-final-newline) t) + (python-nav-end-of-defun)) + nil)) (set (make-local-variable 'outline-regexp) (python-rx (* space) block-start)) - (set (make-local-variable 'outline-heading-end-regexp) ":\\s-*\n") + (set (make-local-variable 'outline-heading-end-regexp) ":[^\n]*\n") (set (make-local-variable 'outline-level) #'(lambda () "`outline-level' function for Python mode."