X-Git-Url: https://code.delx.au/gnu-emacs/blobdiff_plain/ef9bcf3b409648f36c5745e22d147f50a144524f..d7f413b893012eb5c9c93cd724008c2c1faae56f:/lisp/progmodes/python.el diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index aa9d9b10db..f6e1021c86 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -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. @@ -170,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. @@ -178,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 @@ -278,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 @@ -1102,12 +1111,10 @@ 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 right. COUNT defaults to `python-indent-offset'. If region isn't @@ -1757,6 +1764,7 @@ position, else returns nil." (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 : " "\\.\\.\\.: ") @@ -1792,7 +1800,10 @@ It should not contain a caret (^) at the beginning." It should not contain a caret (^) at the beginning." :type 'string) -(defcustom python-shell-enable-font-lock t +(define-obsolete-variable-alias + 'python-shell-enable-font-lock 'python-shell-font-lock-enable "25.1") + +(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 @@ -1928,7 +1939,9 @@ detection and just returns nil." nil))) (when (and (not prompts) python-shell-prompt-detect-failure-warning) - (warn + (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" @@ -2065,6 +2078,16 @@ uniqueness for different types of configurations." (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 @@ -2074,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) @@ -2099,26 +2116,242 @@ uniqueness for different types of configurations." (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. @@ -2129,13 +2362,17 @@ 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-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. @@ -2153,50 +2390,31 @@ variable. (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) + (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) + 'python-shell-completion-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) (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. @@ -2232,7 +2450,6 @@ killed." (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))) @@ -2255,7 +2472,7 @@ 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))) @@ -2275,10 +2492,10 @@ 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 @@ -2286,16 +2503,19 @@ startup." (python-shell-internal-get-process-name) nil t)))) (defun python-shell-get-buffer () - "Return inferior Python buffer for 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)))) + "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 () "Return inferior Python process for current buffer." @@ -2307,25 +2527,14 @@ 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* ((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 (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)) - (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)))) + (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. @@ -2343,13 +2552,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 @@ -2399,16 +2602,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. - (concat - "\r?\n" - ;; Remove initial caret from calculated regexp - (replace-regexp-in-string - (rx string-start ?^) "" - python-shell--prompt-calculated-input-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. @@ -2608,18 +2802,24 @@ 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) @@ -2678,88 +2878,80 @@ the full statement in the case of imports." "24.4" "Completion string code must also autocomplete modules.") -(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-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 line input) - "Do completion at point for PROCESS. -LINE is used to detect the context on how to complete given INPUT." +(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 + (let ((prompt-boundaries (python-util-comint-last-prompt))) (buffer-substring-no-properties - (line-beginning-position) ;End of prompt. - (progn - (re-search-backward "^") - (python-util-forward-comment) ;FIXME: Why? - (point))))) + (car prompt-boundaries) (cdr prompt-boundaries)))) (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 (> (length python-shell-completion-pdb-string-code) 0) - (string-match + (cond ((and (string-match (concat "^" python-shell-prompt-pdb-regexp) prompt)) - python-shell-completion-pdb-string-code) + ;; 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))) - (input - (if (string-match - (python-rx (+ space) (or "from" "import") space) - line) - line - input))) + (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 input) process)))) + (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. @@ -2867,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. +(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." (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 @@ -3218,6 +3411,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 @@ -3244,7 +3443,7 @@ The skeleton will be bound to python-skeleton-NAME." "class " str "(" ("Inheritance, %s: " (unless (equal ?\( (char-before)) ", ") str) - & ")" | -2 + & ")" | -1 ":" \n "\"\"\"" - "\"\"\"" \n > _ \n) @@ -3392,8 +3591,7 @@ 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 @@ -3420,9 +3618,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 @@ -3916,6 +4111,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." @@ -3927,6 +4134,68 @@ 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 negative its opposite is used. @@ -3943,6 +4212,23 @@ 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 @@ -4012,7 +4298,7 @@ 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)