|
|
- ;;; elpy-shell.el --- Interactive Python support for elpy -*- lexical-binding: t -*-
- ;;
- ;; Copyright (C) 2012-2016 Jorgen Schaefer
- ;;
- ;; Author: Jorgen Schaefer <contact@jorgenschaefer.de>, Rainer Gemulla <rgemulla@gmx.de>
- ;; URL: https://github.com/jorgenschaefer/elpy
- ;;
- ;; This program is free software; you can redistribute it and/or
- ;; modify it under the terms of the GNU General Public License
- ;; as published by the Free Software Foundation; either version 3
- ;; of the License, or (at your option) any later version.
- ;;
- ;; This program is distributed in the hope that it will be useful,
- ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;; GNU General Public License for more details.
- ;;
- ;; You should have received a copy of the GNU General Public License
- ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
- ;;
- ;;; Commentary:
- ;;
- ;; Adds support for interactive Python to elpy
- ;;
- ;;; Code:
-
- (eval-when-compile (require 'subr-x))
- (require 'pyvenv)
- (require 'python)
-
- ;;;;;;;;;;;;;;;;;;;;;;
- ;;; User customization
-
- (defcustom elpy-dedicated-shells nil
- "Non-nil if Elpy should use dedicated shells.
-
- Elpy can use a unique Python shell for all buffers and support
- manually started dedicated shells. Setting this option to non-nil
- force the creation of dedicated shells for each buffers."
- :type 'boolean
- :group 'elpy)
- (make-obsolete-variable 'elpy-dedicated-shells
- "Dedicated shells are no longer supported by Elpy.
- You can use `(add-hook 'elpy-mode-hook (lambda () (elpy-shell-toggle-dedicated-shell 1)))' to achieve the same result."
- "1.17.0")
-
- (defcustom elpy-shell-display-buffer-after-send nil ;
- "Whether to display the Python shell after sending something to it."
- :type 'boolean
- :group 'elpy)
-
- (defcustom elpy-shell-echo-output 'when-shell-not-visible
- "Whether to echo the Python shell output in the echo area after input has been sent to the shell.
-
- Possible choices are nil (=never), `when-shell-not-visible', or
- t (=always)."
- :type '(choice (const :tag "Never" nil)
- (const :tag "When shell not visible" when-shell-not-visible)
- (const :tag "Always" t))
- :group 'elpy)
-
- (defcustom elpy-shell-capture-last-multiline-output t
- "Whether to capture the output of the last Python statement when sending multiple statements to the Python shell.
-
- If nil, no output is captured (nor echoed in the shell) when
- sending multiple statements. This is the default behavior of
- python.el. If non-nil and the last statement is an expression,
- captures its output so that it is echoed in the shell."
- :type 'boolean
- :group 'elpy)
- (make-obsolete-variable 'elpy-shell-capture-last-multiline-output
- "The last multiline output is now always captured."
- "February 2019")
-
- (defcustom elpy-shell-echo-input t
- "Whether to echo input sent to the Python shell as input in the
- shell buffer.
-
- Truncation of long inputs can be controlled via
- `elpy-shell-echo-input-lines-head' and
- `elpy-shell-echo-input-lines-tail'."
- :type 'boolean
- :group 'elpy)
-
- (defcustom elpy-shell-echo-input-cont-prompt t
- "Whether to show a continuation prompt when echoing multi-line
- input to the Python shell."
- :type 'boolean
- :group 'elpy)
-
- (defcustom elpy-shell-echo-input-lines-head 10
- "Maximum number of lines to show before truncating input echoed
- in the Python shell."
- :type 'integer
- :group 'elpy)
-
- (defcustom elpy-shell-echo-input-lines-tail 10
- "Maximum number of lines to show after truncating input echoed
- in the Python shell."
- :type 'integer
- :group 'elpy)
-
- (defcustom elpy-shell-use-project-root t
- "Whether to use project root as default directory when starting a Python shells.
-
- The project root is determined using `elpy-project-root`. If this variable is set to
- nil, the current directory is used instead."
- :type 'boolean
- :group 'elpy)
-
- (defcustom elpy-shell-cell-boundary-regexp
- (concat "^\\(?:"
- "##.*" "\\|"
- "#\\s-*<.+>" "\\|"
- "#\\s-*\\(?:In\\|Out\\)\\[.*\\]:"
- "\\)\\s-*$")
- "Regular expression for matching a line indicating the boundary
- of a cell (beginning or ending). By default, lines starting with
- ``##`` are treated as a cell boundaries, as are the boundaries in
- Python files exported from IPython or Jupyter notebooks (e.g.,
- ``# <markdowncell>``, ``# In[1]:'', or ``# Out[1]:``)."
- :type 'string
- :group 'elpy)
-
- (defcustom elpy-shell-codecell-beginning-regexp
- (concat "^\\(?:"
- "##.*" "\\|"
- "#\\s-*<codecell>" "\\|"
- "#\\s-*In\\[.*\\]:"
- "\\)\\s-*$")
- "Regular expression for matching a line indicating the
- beginning of a code cell. By default, lines starting with ``##``
- are treated as beginnings of a code cell, as are the code cell
- beginnings (and only the code cell beginnings) in Python files
- exported from IPython or Jupyter notebooks (e.g., ``#
- <codecell>`` or ``# In[1]:``).
-
- Note that `elpy-shell-cell-boundary-regexp' must also match
- the code cell beginnings defined here."
- :type 'string
- :group 'elpy)
-
-
- ;;;;;;;;;;;;;;;;;;
- ;;; Shell commands
-
- (defvar elpy--shell-last-py-buffer nil
- "Help keep track of python buffer when changing to pyshell.")
-
- (defun elpy-shell-display-buffer ()
- "Display inferior Python process buffer."
- (display-buffer (process-buffer (elpy-shell-get-or-create-process))
- nil
- 'visible))
-
- ;; better name would be pop-to-shell
- (defun elpy-shell-switch-to-shell ()
- "Switch to inferior Python process buffer."
- (interactive)
- (setq elpy--shell-last-py-buffer (buffer-name))
- (pop-to-buffer (process-buffer (elpy-shell-get-or-create-process))))
-
- (defun elpy-shell-switch-to-buffer ()
- "Switch from inferior Python process buffer to recent Python buffer."
- (interactive)
- (pop-to-buffer elpy--shell-last-py-buffer))
-
- (defun elpy-shell-switch-to-shell-in-current-window ()
- (interactive)
- (setq elpy--shell-last-py-buffer (buffer-name))
- (switch-to-buffer (process-buffer (elpy-shell-get-or-create-process))))
-
- (defun elpy-shell-switch-to-buffer-in-current-window ()
- (interactive)
- (switch-to-buffer elpy--shell-last-py-buffer))
-
- (defun elpy-shell-kill (&optional kill-buff)
- "Kill the current python shell.
-
- If KILL-BUFF is non-nil, also kill the associated buffer."
- (interactive)
- (let ((shell-buffer (python-shell-get-buffer)))
- (cond
- (shell-buffer
- (delete-process shell-buffer)
- (when kill-buff
- (kill-buffer shell-buffer))
- (message "Killed %s shell" shell-buffer))
- (t
- (message "No python shell to kill")))))
-
- (defun elpy-shell-kill-all (&optional kill-buffers ask-for-each-one)
- "Kill all active python shells.
-
- If KILL-BUFFERS is non-nil, also kill the associated buffers.
- If ASK-FOR-EACH-ONE is non-nil, ask before killing each python process."
- (interactive)
- (let ((python-buffer-list ()))
- ;; Get active python shell buffers and kill inactive ones (if asked)
- (cl-loop for buffer being the buffers do
- (when (and (buffer-name buffer)
- (string-match (rx bol "*Python" (opt "[" (* (not (any "]"))) "]") "*" eol)
- (buffer-name buffer)))
- (if (get-buffer-process buffer)
- (push buffer python-buffer-list)
- (when kill-buffers
- (kill-buffer buffer)))))
- (cond
- ;; Ask for each buffers and kill
- ((and python-buffer-list ask-for-each-one)
- (cl-loop for buffer in python-buffer-list do
- (when (y-or-n-p (format "Kill %s ? " buffer))
- (delete-process buffer)
- (when kill-buffers
- (kill-buffer buffer)))))
- ;; Ask and kill every buffers
- (python-buffer-list
- (if (y-or-n-p (format "Kill %s python shells ? " (length python-buffer-list)))
- (cl-loop for buffer in python-buffer-list do
- (delete-process buffer)
- (when kill-buffers
- (kill-buffer buffer)))))
- ;; No shell to close
- (t
- (message "No python shell to close")))))
-
- (defun elpy-shell-get-or-create-process (&optional sit)
- "Get or create an inferior Python process for current buffer and return it.
-
- If SIT is non-nil, sit for that many seconds after creating a
- Python process. This allows the process to start up."
- (let* ((bufname (format "*%s*" (python-shell-get-process-name nil)))
- (proc (get-buffer-process bufname)))
- (if proc
- proc
- (when (not (executable-find python-shell-interpreter))
- (error "Python shell interpreter `%s' cannot be found. Please set `python-shell-interpreter' to an valid python binary."
- python-shell-interpreter))
- (let ((default-directory (or (and elpy-shell-use-project-root
- (elpy-project-root))
- default-directory)))
- (run-python (python-shell-parse-command) nil t))
- (when sit (sit-for sit))
- (when (elpy-project-root)
- (python-shell-send-string
- (format "import sys;sys.path.append('%s')" (elpy-project-root))))
- (get-buffer-process bufname))))
-
- (defun elpy-shell-toggle-dedicated-shell (&optional arg)
- "Toggle the use of a dedicated python shell for the current buffer.
-
- if ARG is positive, enable the use of a dedicated shell.
- if ARG is negative or 0, disable the use of a dedicated shell."
- (interactive)
- (let ((arg (or arg
- (if (local-variable-p 'python-shell-buffer-name) 0 1))))
- (if (<= arg 0)
- (kill-local-variable 'python-shell-buffer-name)
- (setq-local python-shell-buffer-name
- (format "Python[%s]"
- (file-name-sans-extension
- (buffer-name)))))))
-
- (defun elpy-shell-set-local-shell (&optional shell-name)
- "Associate the current buffer to a specific shell.
-
- Meaning that the code from the current buffer will be sent to this shell.
-
- If SHELL-NAME is not specified, ask with completion for a shell name.
-
- If SHELL-NAME is \"Global\", associate the current buffer to the main python
- shell (often \"*Python*\" shell)."
- (interactive)
- (let* ((current-shell-name (if (local-variable-p 'python-shell-buffer-name)
- (progn
- (string-match "Python\\[\\(.*?\\)\\]"
- python-shell-buffer-name)
- (match-string 1 python-shell-buffer-name))
- "Global"))
- (shell-names (cl-loop
- for buffer in (buffer-list)
- for buffer-name = (file-name-sans-extension (substring-no-properties (buffer-name buffer)))
- if (string-match "\\*Python\\[\\(.*?\\)\\]\\*" buffer-name)
- collect (match-string 1 buffer-name)))
- (candidates (remove current-shell-name
- (delete-dups
- (append (list (file-name-sans-extension
- (buffer-name)) "Global")
- shell-names))))
- (prompt (format "Shell name (current: %s): " current-shell-name))
- (shell-name (or shell-name (completing-read prompt candidates))))
- (if (string= shell-name "Global")
- (kill-local-variable 'python-shell-buffer-name)
- (setq-local python-shell-buffer-name (format "Python[%s]" shell-name)))))
-
- (defun elpy-shell--ensure-shell-running ()
- "Ensure that the Python shell for the current buffer is running.
-
- If the shell is not running, waits until the first prompt is visible and
- commands can be sent to the shell."
- (with-current-buffer (process-buffer (elpy-shell-get-or-create-process))
- (let ((cumtime 0))
- (while (and (when (boundp 'python-shell--first-prompt-received)
- (not python-shell--first-prompt-received))
- (< cumtime 3))
- (sleep-for 0.1)
- (setq cumtime (+ cumtime 0.1)))))
- (elpy-shell-get-or-create-process))
-
- (defun elpy-shell--string-without-indentation (string)
- "Return the current string, but without indentation."
- (if (string-empty-p string)
- string
- (let ((indent-level nil)
- (indent-tabs-mode nil))
- (with-temp-buffer
- (insert string)
- (goto-char (point-min))
- (while (< (point) (point-max))
- (cond
- ((or (elpy-shell--current-line-only-whitespace-p)
- (python-info-current-line-comment-p)))
- ((not indent-level)
- (setq indent-level (current-indentation)))
- ((and indent-level
- (< (current-indentation) indent-level))
- (error (message "X%sX" (thing-at-point 'line)))))
- ;; (error "Can't adjust indentation, consecutive lines indented less than starting line")))
- (forward-line))
- (indent-rigidly (point-min)
- (point-max)
- (- indent-level))
- ;; 'indent-rigidly' introduces tabs despite the fact that 'indent-tabs-mode' is nil
- ;; 'untabify' fix that
- (untabify (point-min) (point-max))
- (buffer-string)))))
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;; Flash input sent to shell
-
- ;; functions for flashing a region; only flashes when package eval-sexp-fu is
- ;; loaded and its minor mode enabled
- (defun elpy-shell--flash-and-message-region (begin end)
- "Displays information about code fragments sent to the shell.
-
- BEGIN and END refer to the region of the current buffer containing the code being sent. Displays a message with the first line of that region. If `eval-sexp-fu-flash-mode' is active, additionally flashes that region briefly."
- (when (> end begin)
- (save-excursion
- (goto-char begin)
- (end-of-line)
- (if (<= end (point))
- (message "Sent: %s" (string-trim (thing-at-point 'line)))
- (message "Sent: %s..." (string-trim (thing-at-point 'line)))))
- (when (bound-and-true-p eval-sexp-fu-flash-mode)
- (multiple-value-bind (_bounds hi unhi _eflash)
- (eval-sexp-fu-flash (cons begin end))
- (eval-sexp-fu-flash-doit (lambda () t) hi unhi)))))
-
- ;;;;;;;;;;;;;;;;;;;
- ;; Helper functions
-
- (defun elpy-shell--current-line-else-or-elif-p ()
- (eq (string-match-p "\\s-*el\\(?:se:\\|if[^\w]\\)" (thing-at-point 'line)) 0))
-
- (defun elpy-shell--current-line-indented-p ()
- (eq (string-match-p "\\s-+[^\\s-]+" (thing-at-point 'line)) 0))
-
- (defun elpy-shell--current-line-only-whitespace-p ()
- "Whether the current line contains only whitespace characters (or is empty)."
- (eq (string-match-p "\\s-*$" (thing-at-point 'line)) 0))
-
- (defun elpy-shell--current-line-code-line-p ()
- (and (not (elpy-shell--current-line-only-whitespace-p))
- (not (python-info-current-line-comment-p))))
-
- (defun elpy-shell--current-line-defun-p ()
- "Whether a function definition starts at the current line."
- (eq (string-match-p
- "\\s-*\\(?:def\\|async\\s-+def\\)\\s\-"
- (thing-at-point 'line))
- 0))
-
- (defun elpy-shell--current-line-defclass-p ()
- "Whether a class definition starts at the current line."
- (eq (string-match-p
- "\\s-*class\\s\-"
- (thing-at-point 'line))
- 0))
-
- (defun elpy-shell--skip-to-next-code-line (&optional backwards)
- "Move the point to the next line containing code.
-
- If the current line has code, point is not moved. If BACKWARDS is
- non-nil, skips backwards."
- (if backwards
- (while (and (not (elpy-shell--current-line-code-line-p))
- (not (eq (point) (point-min))))
- (forward-line -1))
- (while (and (not (elpy-shell--current-line-code-line-p))
- (not (eq (point) (point-max))))
- (forward-line))))
-
- (defun elpy-shell--check-if-shell-available ()
- "Check if the associated python shell is available.
-
- Return non-nil is the shell is running and not busy, nil otherwise."
- (and (python-shell-get-process)
- (with-current-buffer (process-buffer (python-shell-get-process))
- (save-excursion
- (goto-char (point-max))
- (let ((inhibit-field-text-motion t))
- (python-shell-comint-end-of-output-p
- (buffer-substring (line-beginning-position)
- (line-end-position))))))))
- ;;;;;;;;;;
- ;; Echoing
-
- (defmacro elpy-shell--with-maybe-echo (body)
- ;; Echoing is apparently buggy for emacs < 25...
- (if (<= 25 emacs-major-version)
- `(elpy-shell--with-maybe-echo-output
- (elpy-shell--with-maybe-echo-input
- ,body))
- body))
-
-
- (defmacro elpy-shell--with-maybe-echo-input (body)
- "Run BODY so that it adheres `elpy-shell-echo-input' and `elpy-shell-display-buffer'."
- `(progn
- (elpy-shell--enable-echo)
- (prog1
- (if elpy-shell-display-buffer-after-send
- (prog1 (progn ,body)
- (elpy-shell-display-buffer))
- (cl-flet ((elpy-shell-display-buffer () ()))
- (progn ,body)))
- (elpy-shell--disable-echo))))
-
- (defvar-local elpy-shell--capture-output nil
- "Non-nil when the Python shell should capture output for display in the echo area.")
-
- (defvar-local elpy-shell--captured-output nil
- "Current captured output of the Python shell.")
-
- (defmacro elpy-shell--with-maybe-echo-output (body)
- "Run BODY and grab shell output according to `elpy-shell-echo-output'."
- `(cl-letf (((symbol-function 'python-shell-send-file)
- (if elpy-shell-echo-output
- (symbol-function 'elpy-shell-send-file)
- (symbol-function 'python-shell-send-file))))
- (let* ((process (elpy-shell--ensure-shell-running))
- (process-buf (process-buffer process))
- (shell-visible (or elpy-shell-display-buffer-after-send
- (get-buffer-window process-buf))))
- (with-current-buffer process-buf
- (setq-local elpy-shell--capture-output
- (and elpy-shell-echo-output
- (or (not (eq elpy-shell-echo-output 'when-shell-not-visible))
- (not shell-visible)))))
- (progn ,body))))
-
- (defun elpy-shell--enable-output-filter ()
- (add-hook 'comint-output-filter-functions 'elpy-shell--output-filter nil t))
-
- (defun elpy-shell--output-filter (string)
- "Filter used in `elpy-shell--with-maybe-echo-output' to grab output.
-
- No actual filtering is performed. STRING is the output received
- to this point from the process. If `elpy-shell--capture-output'
- is set, captures and messages shell output in the echo area (once
- complete). Otherwise, does nothing."
- ;; capture the output and message it when complete
- (when elpy-shell--capture-output
- ;; remember the new output
- (setq-local elpy-shell--captured-output
- (concat elpy-shell--captured-output (ansi-color-filter-apply string)))
-
- ;; Output ends when `elpy-shell--captured-output' contains
- ;; the prompt attached at the end of it. If so, message it.
- (when (python-shell-comint-end-of-output-p elpy-shell--captured-output)
- (let ((output (substring
- elpy-shell--captured-output
- 0 (match-beginning 0)))
- (message-log-max))
- (if (string-match-p "Traceback (most recent call last):" output)
- (message "Exception during evaluation.")
- (if (string-empty-p output)
- (message "No output was produced.")
- (message "%s" (replace-regexp-in-string "\n\\'" "" output))))
- (setq-local elpy-shell--captured-output nil))))
-
- ;; return input unmodified
- string)
-
- (defun elpy-shell--insert-and-font-lock (string face &optional no-font-lock)
- "Inject STRING into the Python shell buffer."
- (let ((from-point (point)))
- (insert string)
- (if (not no-font-lock)
- (add-text-properties from-point (point)
- (list 'front-sticky t 'font-lock-face face)))))
-
- (defun elpy-shell--append-to-shell-output (string &optional no-font-lock prepend-cont-prompt)
- "Append the given STRING to the output of the Python shell buffer.
-
- Unless NO-FONT-LOCK is set, formats STRING as shell input.
- Prepends a continuation promt if PREPEND-CONT-PROMPT is set."
- (when (not (string-empty-p string))
- (let* ((process (elpy-shell-get-or-create-process))
- (process-buf (process-buffer process))
- (mark-point (process-mark process)))
- (with-current-buffer process-buf
- (save-excursion
- (goto-char mark-point)
- (if prepend-cont-prompt
- (let* ((column (+ (- (point) (progn (forward-line -1) (end-of-line) (point))) 1))
- (prompt (concat (make-string (max 0 (- column 7)) ? ) "...: "))
- (lines (split-string string "\n")))
- (goto-char mark-point)
- (elpy-shell--insert-and-font-lock
- (car lines) 'comint-highlight-input no-font-lock)
- (when (cdr lines)
- ;; no additional newline at end for multiline
- (dolist (line (cdr lines))
- (insert "\n")
- (elpy-shell--insert-and-font-lock
- prompt 'comint-highlight-prompt no-font-lock)
- (elpy-shell--insert-and-font-lock
- line 'comint-highlight-input no-font-lock)))
- ;; but put one for single line
- (insert "\n"))
- (elpy-shell--insert-and-font-lock
- string 'comint-highlight-input no-font-lock))
- (set-marker (process-mark process) (point)))))))
-
- (defun elpy-shell--string-head-lines (string n)
- "Extract the first N lines from STRING."
- (let* ((line "\\(?:\\(?:.*\n\\)\\|\\(?:.+\\'\\)\\)")
- (lines (concat line "\\{" (number-to-string n) "\\}"))
- (regexp (concat "\\`" "\\(" lines "\\)")))
- (if (string-match regexp string)
- (match-string 1 string)
- string)))
-
- (defun elpy-shell--string-tail-lines (string n)
- "Extract the last N lines from STRING."
- (let* ((line "\\(?:\\(?:.*\n\\)\\|\\(?:.+\\'\\)\\)")
- (lines (concat line "\\{" (number-to-string n) "\\}"))
- (regexp (concat "\\(" lines "\\)" "\\'")))
- (if (string-match regexp string)
- (match-string 1 string)
- string)))
-
- (defun elpy-shell--python-shell-send-string-echo-advice (string &optional _process _msg)
- "Advice to enable echoing of input in the Python shell."
- (interactive)
- (let* ((append-string ; strip setup code from Elpy
- (if (string-match "import sys, codecs, os, ast;__pyfile = codecs.open.*$" string)
- (replace-match "" nil nil string)
- string))
- (append-string ; strip setup code from python.el
- (if (string-match "import codecs, os;__pyfile = codecs.open(.*;exec(compile(__code, .*$" append-string)
- (replace-match "" nil nil append-string)
- append-string))
- (append-string ; here too
- (if (string-match "^# -\\*- coding: utf-8 -\\*-\n*$" append-string)
- (replace-match "" nil nil append-string)
- append-string))
- (append-string ; Strip "if True:", added when sending regions
- (if (string-match "^if True:$" append-string)
- (replace-match "" nil nil append-string)
- append-string))
- (append-string ; strip newlines from beginning and white space from end
- (string-trim-right
- (if (string-match "\\`\n+" append-string)
- (replace-match "" nil nil append-string)
- append-string)))
- (append-string ; Dedent region
- (elpy-shell--string-without-indentation append-string))
- (head (elpy-shell--string-head-lines append-string elpy-shell-echo-input-lines-head))
- (tail (elpy-shell--string-tail-lines append-string elpy-shell-echo-input-lines-tail))
- (append-string (if (> (length append-string) (+ (length head) (length tail)))
- (concat head "...\n" tail)
- append-string)))
-
- ;; append the modified string to the shell output; prepend a newline for
- ;; multi-line strings
- (if elpy-shell-echo-input-cont-prompt
- (elpy-shell--append-to-shell-output append-string nil t)
- (elpy-shell--append-to-shell-output
- (concat (if (string-match "\n" append-string) "\n" "")
- append-string
- "\n")))))
-
- (defun elpy-shell--enable-echo ()
- "Enable input echoing when `elpy-shell-echo-input' is set."
- (when elpy-shell-echo-input
- (advice-add 'python-shell-send-string
- :before 'elpy-shell--python-shell-send-string-echo-advice)))
-
- (defun elpy-shell--disable-echo ()
- "Disable input echoing."
- (advice-remove 'python-shell-send-string
- 'elpy-shell--python-shell-send-string-echo-advice))
-
- (defun elpy-shell-send-file (file-name &optional process temp-file-name
- delete msg)
- "Like `python-shell-send-file' but evaluates last expression separately.
-
- See `python-shell-send-file' for a description of the
- arguments. This function differs in that it breaks up the
- Python code in FILE-NAME into statements. If the last statement
- is a Python expression, it is evaluated separately in 'eval'
- mode. This way, the interactive python shell can capture (and
- print) the output of the last expression."
- (interactive
- (list
- (read-file-name "File to send: ") ; file-name
- nil ; process
- nil ; temp-file-name
- nil ; delete
- t)) ; msg
- (let* ((process (or process (python-shell-get-process-or-error msg)))
- (encoding (with-temp-buffer
- (insert-file-contents
- (or temp-file-name file-name))
- (python-info-encoding)))
- (file-name (expand-file-name
- (or (file-remote-p file-name 'localname)
- file-name)))
- (temp-file-name (when temp-file-name
- (expand-file-name
- (or (file-remote-p temp-file-name 'localname)
- temp-file-name)))))
- (python-shell-send-string
- (format
- (concat
- "import sys, codecs, os, ast;"
- "__pyfile = codecs.open('''%s''', encoding='''%s''');"
- "__code = __pyfile.read().encode('''%s''');"
- "__pyfile.close();"
- (when (and delete temp-file-name)
- (format "os.remove('''%s''');" temp-file-name))
- "__block = ast.parse(__code, '''%s''', mode='exec');"
- ;; Has to ba a oneliner, which make conditionnal statements a bit complicated...
- " __block.body = (__block.body if not isinstance(__block.body[0], ast.If) else __block.body if not isinstance(__block.body[0].test, ast.Name) else __block.body if not __block.body[0].test.id == 'True' else __block.body[0].body) if sys.version_info[0] < 3 else (__block.body if not isinstance(__block.body[0], ast.If) else __block.body if not isinstance(__block.body[0].test, ast.NameConstant) else __block.body if not __block.body[0].test.value is True else __block.body[0].body);"
- "__last = __block.body[-1];" ;; the last statement
- "__isexpr = isinstance(__last,ast.Expr);" ;; is it an expression?
- "_ = __block.body.pop() if __isexpr else None;" ;; if so, remove it
- "exec(compile(__block, '''%s''', mode='exec'));" ;; execute everything else
- "eval(compile(ast.Expression(__last.value), '''%s''', mode='eval')) if __isexpr else None" ;; if it was an expression, it has been removed; now evaluate it
- )
- (or temp-file-name file-name) encoding encoding file-name file-name file-name)
- process)))
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;; Navigation commands for sending
-
- (defun elpy-shell--nav-beginning-of-statement ()
- "Move the point to the beginning of the current or next Python statement.
-
- If the current line starts with a statement, behaves exactly like
- `python-nav-beginning-of-statement'. If the line is part of a
- statement but not a statement itself, goes backwards to the
- beginning of the statement. If the current line is not a code
- line, skips forward to the next code line and navigates from
- there."
- (elpy-shell--skip-to-next-code-line)
- (python-nav-beginning-of-statement)
- (let ((p))
- (while (and (not (eq p (point)))
- (elpy-shell--current-line-else-or-elif-p))
- (elpy-nav-backward-block)
- (setq p (point)))))
-
- (defun elpy-shell--nav-end-of-statement ()
- "Move the point to the end of the current Python statement.
-
- Assumes that the point is precisely at the beginning of a
- statement (e.g., after calling
- `elpy-shell--nav-beginning-of-statement')."
- (let ((continue t)
- (p))
- (while (and (not (eq p (point)))
- continue)
- ;; check if there is a another block at the same indentation level
- (setq p (point))
- (elpy-nav-forward-block)
-
- ;; if not, go to the end of the block and done
- (if (eq p (point))
- (progn
- (python-nav-end-of-block)
- (setq continue nil))
- ;; otherwise check if its an else/elif clause
- (unless (elpy-shell--current-line-else-or-elif-p)
- (forward-line -1)
- (elpy-shell--skip-to-next-code-line t)
- (setq continue nil)))))
- (end-of-line))
-
- (defun elpy-shell--nav-beginning-of-top-statement ()
- "Move the point to the beginning of the current or next top-level statement.
-
- If the point is within a top-level statement, moves to its
- beginning. Otherwise, moves to the beginning of the next top-level
- statement."
- (interactive)
- (elpy-shell--nav-beginning-of-statement)
- (let ((p))
- (while (and (not (eq p (point)))
- (elpy-shell--current-line-indented-p))
- (forward-line -1)
- (elpy-shell--skip-to-next-code-line t)
- (elpy-shell--nav-beginning-of-statement))))
-
- (defun elpy-shell--nav-beginning-of-def (def-p)
- "Move point to the beginning of the current definition.
-
- DEF-P is a predicate function that decides whether the current
- line starts a definition.
-
- It the current line starts a definition, uses this definition. If
- the current line does not start a definition and is a code line,
- searches for the definition that contains the current line.
- Otherwise, searches for the definition that contains the next
- code line.
-
- If a definition is found, moves point to the start of the
- definition and returns t. Otherwise, retains point position and
- returns nil."
- (if (funcall def-p)
- (progn
- (python-nav-beginning-of-statement)
- t)
- (let ((beg-ts (save-excursion
- (elpy-shell--skip-to-next-code-line t)
- (elpy-shell--nav-beginning-of-top-statement)
- (point)))
- (orig-p (point))
- (max-indent (save-excursion
- (elpy-shell--skip-to-next-code-line)
- (- (current-indentation) 1)))
- (found))
- (while (and (not found)
- (>= (point) beg-ts))
- (if (and (funcall def-p)
- (<= (current-indentation) max-indent))
- (setq found t)
- (when (elpy-shell--current-line-code-line-p)
- (setq max-indent (min max-indent
- (- (current-indentation) 1))))
- (forward-line -1)))
- (if found
- (python-nav-beginning-of-statement)
- (goto-char orig-p))
- found)))
-
- (defun elpy-shell--nav-beginning-of-defun ()
- "Move point to the beginning of the current function definition.
-
- If a definition is found, moves point to the start of the
- definition and returns t. Otherwise, retains point position and
- returns nil.
-
- See `elpy-shell--nav-beginning-of-def' for details."
- (elpy-shell--nav-beginning-of-def 'elpy-shell--current-line-defun-p))
-
- (defun elpy-shell--nav-beginning-of-defclass ()
- "Move point to the beginning of the current class definition.
-
- If a definition is found, moves point to the start of the
- definition and returns t. Otherwise, retains point position and
- returns nil.
-
- See `elpy-shell--nav-beginning-of-def' for details."
- (elpy-shell--nav-beginning-of-def 'elpy-shell--current-line-defclass-p))
-
- (defun elpy-shell--nav-beginning-of-group ()
- "Move point to the beginning of the current or next group of top-level statements.
-
- A sequence of top-level statements is a group if they are not
- separated by empty lines. Empty lines within each top-level
- statement are ignored.
-
- If the point is within a top-level statement, moves to the
- beginning of the group containing this statement. Otherwise, moves
- to the first top-level statement below point."
- (elpy-shell--nav-beginning-of-top-statement)
- (while (not (or (elpy-shell--current-line-only-whitespace-p)
- (eq (point) (point-min))))
- (unless (python-info-current-line-comment-p)
- (elpy-shell--nav-beginning-of-top-statement))
- (forward-line -1)
- (beginning-of-line))
- (when (elpy-shell--current-line-only-whitespace-p)
- (forward-line 1)
- (beginning-of-line)))
-
- ;;;;;;;;;;;;;;;;;
- ;;; Send commands
-
- (defun elpy-shell-send-statement-and-step ()
- "Send current or next statement to Python shell and step.
-
- If the current line is part of a statement, sends this statement.
- Otherwise, skips forward to the next code line and sends the
- corresponding statement."
- (interactive)
- (elpy-shell--ensure-shell-running)
- (elpy-shell--nav-beginning-of-statement)
- ;; Make sure there is a statement to send
- (when (not (looking-at "[[:space:]]*$"))
- (when (not elpy-shell-echo-input) (elpy-shell--append-to-shell-output "\n"))
- (let ((beg (save-excursion (beginning-of-line) (point)))
- (end (progn (elpy-shell--nav-end-of-statement) (point))))
- (unless (eq beg end)
- (elpy-shell--flash-and-message-region beg end)
- (elpy-shell--with-maybe-echo
- (python-shell-send-string (python-shell-buffer-substring beg end)))))
- (python-nav-forward-statement)))
-
- (defun elpy-shell-send-top-statement-and-step ()
- "Send the current or next top-level statement to the Python shell and step.
-
- If the current line is part of a top-level statement, sends this
- top-level statement. Otherwise, skips forward to the next code
- line and sends the corresponding top-level statement."
- (interactive)
- (elpy-shell--ensure-shell-running)
- (let* ((beg (progn (elpy-shell--nav-beginning-of-top-statement) (point)))
- (end (progn (elpy-shell--nav-end-of-statement) (point))))
- (elpy-shell--flash-and-message-region beg end)
- (if (string-match-p "\\`[^\n]*\\'" (buffer-substring beg end))
- ;; single line
- (elpy-shell-send-statement-and-step)
- ;; multiple lines
- (elpy-shell--with-maybe-echo
- (python-shell-send-region beg end))
- (setq mark-active nil)
- (python-nav-forward-statement))))
-
- (defun elpy-shell-send-defun-and-step ()
- "Send the function definition that contains the current line
- to the Python shell and steps.
-
- See `elpy-shell--nav-beginning-of-def' for details."
- (interactive)
- (if (elpy-shell--nav-beginning-of-defun)
- (elpy-shell-send-statement-and-step)
- (message "There is no function definition that includes the current line.")))
-
- (defun elpy-shell-send-defclass-and-step ()
- "Send the class definition that contains the current line to
- the Python shell and steps.
-
- See `elpy-shell--nav-beginning-of-def' for details."
- (interactive)
- (if (elpy-shell--nav-beginning-of-defclass)
- (elpy-shell-send-statement-and-step)
- (message "There is no class definition that includes the current line.")))
-
- (defun elpy-shell-send-group-and-step ()
- "Send the current or next group of top-level statements to the Python shell and step.
-
- A sequence of top-level statements is a group if they are not
- separated by empty lines. Empty lines within each top-level
- statement are ignored.
-
- If the point is within a top-level statement, send the group
- around this statement. Otherwise, go to the top-level statement
- below point and send the group around this statement."
- (interactive)
- (elpy-shell--ensure-shell-running)
- (let* ((beg (progn (elpy-shell--nav-beginning-of-group) (point)))
- (end (progn
- ;; go forward to end of group
- (unless (python-info-current-line-comment-p)
- (elpy-shell--nav-end-of-statement))
- (let ((p))
- (while (not (eq p (point)))
- (setq p (point))
- (forward-line)
- (if (elpy-shell--current-line-only-whitespace-p)
- (goto-char p) ;; done
- (unless (python-info-current-line-comment-p)
- (elpy-shell--nav-end-of-statement)))))
- (point))))
- (if (> end beg)
- (progn
- (elpy-shell--flash-and-message-region beg end)
- ;; send the region and jump to next statement
- (if (string-match-p "\\`[^\n]*\\'" (buffer-substring beg end))
- ;; single line
- (elpy-shell-send-statement-and-step)
- ;; multiple lines
- (when (not elpy-shell-echo-input)
- (elpy-shell--append-to-shell-output "\n"))
- (elpy-shell--with-maybe-echo
- (python-shell-send-region beg end))
- (python-nav-forward-statement)))
- (goto-char (point-max)))
- (setq mark-active nil)))
-
- (defun elpy-shell-send-codecell-and-step ()
- "Send the current code cell to the Python shell and step.
-
- Signals an error if the point is not inside a code cell.
-
- Cell beginnings and cell boundaries can be customized via the
- variables `elpy-shell-cell-boundary-regexp' and
- `elpy-shell-codecell-beginning-regexp', which see."
- (interactive)
- (let ((beg (save-excursion
- (end-of-line)
- (re-search-backward elpy-shell-cell-boundary-regexp nil t)
- (beginning-of-line)
- (and (string-match-p elpy-shell-codecell-beginning-regexp
- (thing-at-point 'line))
- (point))))
- (end (save-excursion
- (forward-line)
- (if (re-search-forward elpy-shell-cell-boundary-regexp nil t)
- (forward-line -1)
- (goto-char (point-max)))
- (end-of-line)
- (point))))
- (if beg
- (progn
- (elpy-shell--flash-and-message-region beg end)
- (when (not elpy-shell-echo-input)
- (elpy-shell--append-to-shell-output "\n"))
- (elpy-shell--with-maybe-echo
- (python-shell-send-region beg end))
- (goto-char end)
- (python-nav-forward-statement))
- (message "Not in a codecell."))))
-
- (defun elpy-shell-send-region-or-buffer-and-step (&optional arg)
- "Send the active region or the buffer to the Python shell and step.
-
- If there is an active region, send that. Otherwise, send the
- whole buffer.
-
- In Emacs 24.3 and later, without prefix argument and when there
- is no active region, this will escape the Python idiom of if
- __name__ == '__main__' to be false to avoid accidental execution
- of code. With prefix argument, this code is executed."
- (interactive "P")
- (if (use-region-p)
- (elpy-shell--flash-and-message-region (region-beginning) (region-end))
- (elpy-shell--flash-and-message-region (point-min) (point-max)))
- (elpy-shell--with-maybe-echo
- (elpy-shell--send-region-or-buffer-internal arg))
- (if (use-region-p)
- (goto-char (region-end))
- (goto-char (point-max))))
-
- (defun elpy-shell--send-region-or-buffer-internal (&optional arg)
- "Send the active region or the buffer to the Python shell and step.
-
- If there is an active region, send that. Otherwise, send the
- whole buffer.
-
- In Emacs 24.3 and later, without prefix argument and when there
- is no active region, this will escape the Python idiom of if
- __name__ == '__main__' to be false to avoid accidental execution
- of code. With prefix argument, this code is executed."
- (interactive "P")
- (elpy-shell--ensure-shell-running)
- (when (not elpy-shell-echo-input) (elpy-shell--append-to-shell-output "\n"))
- (let ((if-main-regex "^if +__name__ +== +[\"']__main__[\"'] *:")
- (has-if-main-and-removed nil))
- (if (use-region-p)
- (let ((region (python-shell-buffer-substring
- (region-beginning) (region-end))))
- (when (string-match "\t" region)
- (message "Region contained tabs, this might cause weird errors"))
- (python-shell-send-string region))
- (unless arg
- (save-excursion
- (goto-char (point-min))
- (setq has-if-main-and-removed (re-search-forward if-main-regex nil t))))
- (python-shell-send-buffer arg))
- (when has-if-main-and-removed
- (message (concat "Removed if __name__ == '__main__' construct, "
- "use a prefix argument to evaluate.")))))
-
- (defun elpy-shell-send-buffer-and-step (&optional arg)
- "Send entire buffer to Python shell.
-
- In Emacs 24.3 and later, without prefix argument, this will
- escape the Python idiom of if __name__ == '__main__' to be false
- to avoid accidental execution of code. With prefix argument, this
- code is executed."
- (interactive "P")
- (let ((p))
- (save-mark-and-excursion
- (deactivate-mark)
- (elpy-shell-send-region-or-buffer-and-step arg)
- (setq p (point)))
- (goto-char p)))
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;; Send command variations (with/without step; with/without go)
-
- (defun elpy-shell--send-with-step-go (step-fun step go my-prefix-arg)
- "Run a function with STEP and/or GO.
-
- STEP-FUN should be a function that sends something to the shell
- and moves point to code position right after what has been sent.
-
- When STEP is nil, keeps point position. When GO is non-nil,
- switches focus to Python shell buffer."
- (let ((orig (point)))
- (setq current-prefix-arg my-prefix-arg)
- (call-interactively step-fun)
- (when (not step)
- (goto-char orig)))
- (when go
- (elpy-shell-switch-to-shell)))
-
- (defmacro elpy-shell--defun-step-go (fun-and-step)
- "Defines fun, fun-and-go, fun-and-step-and-go for the given FUN-AND-STEP function."
- (let ((name (string-remove-suffix "-and-step" (symbol-name fun-and-step))))
- (list
- 'progn
- (let ((fun (intern name)))
- `(defun ,fun (&optional arg)
- ,(concat "Run `" (symbol-name fun-and-step) "' but retain point position.")
- (interactive "P")
- (elpy-shell--send-with-step-go ',fun-and-step nil nil arg)))
- (let ((fun-and-go (intern (concat name "-and-go"))))
- `(defun ,fun-and-go (&optional arg)
- ,(concat "Run `" (symbol-name fun-and-step) "' but retain point position and switch to Python shell.")
- (interactive "P")
- (elpy-shell--send-with-step-go ',fun-and-step nil t arg)))
- (let ((fun-and-step-and-go (intern (concat name "-and-step-and-go"))))
- `(defun ,fun-and-step-and-go (&optional arg)
- ,(concat "Run `" (symbol-name fun-and-step) "' and switch to Python shell.")
- (interactive "P")
- (elpy-shell--send-with-step-go ',fun-and-step t t arg))))))
-
- (elpy-shell--defun-step-go elpy-shell-send-statement-and-step)
- (elpy-shell--defun-step-go elpy-shell-send-top-statement-and-step)
- (elpy-shell--defun-step-go elpy-shell-send-defun-and-step)
- (elpy-shell--defun-step-go elpy-shell-send-defclass-and-step)
- (elpy-shell--defun-step-go elpy-shell-send-group-and-step)
- (elpy-shell--defun-step-go elpy-shell-send-codecell-and-step)
- (elpy-shell--defun-step-go elpy-shell-send-region-or-buffer-and-step)
- (elpy-shell--defun-step-go elpy-shell-send-buffer-and-step)
-
-
- ;;;;;;;;;;;;;;;;;;;;;;;
- ;; Debugging features
-
- (when (version<= "25" emacs-version)
-
- (defun elpy-pdb--refresh-breakpoints (lines)
- "Add new breakpoints at lines LINES of the current buffer."
- ;; Forget old breakpoints
- (python-shell-send-string-no-output "import bdb as __bdb; __bdb.Breakpoint.bplist={}; __bdb.Breakpoint.next=1;__bdb.Breakpoint.bpbynumber=[None]")
- (python-shell-send-string-no-output "import pdb; __pdbi = pdb.Pdb()")
- (dolist (line lines)
- (python-shell-send-string-no-output
- (format "__pdbi.set_break('''%s''', %s)" (buffer-file-name) line))))
-
- (defun elpy-pdb--start-pdb (&optional output)
- "Start pdb on the current script.
-
- if OUTPUT is non-nil, display the prompt after execution."
- (let ((string (format "__pdbi._runscript('''%s''')" (buffer-file-name))))
- (if output
- (python-shell-send-string string)
- (python-shell-send-string-no-output string))))
-
- (defun elpy-pdb--get-breakpoint-positions ()
- "Return a list of lines with breakpoints."
- (let* ((overlays (overlay-lists))
- (overlays (append (car overlays) (cdr overlays)))
- (bp-lines '()))
- (dolist (ov overlays)
- (when (overlay-get ov 'elpy-breakpoint)
- (push (line-number-at-pos (overlay-start ov))
- bp-lines)))
- bp-lines))
-
- (defun elpy-pdb-debug-buffer (&optional arg)
- "Run pdb on the current buffer.
-
- If breakpoints are set in the current buffer, jump to the first one.
- If no breakpoints are set, debug from the beginning of the script.
-
- With a prefix argument, ignore the existing breakpoints."
- (interactive "P")
- (if (not (buffer-file-name))
- (error "Debugging only work for buffers visiting a file")
- (elpy-shell--ensure-shell-running)
- (save-buffer)
- (let ((bp-lines (elpy-pdb--get-breakpoint-positions)))
- (if (or arg (= 0 (length bp-lines)))
- (progn
- (elpy-pdb--refresh-breakpoints '())
- (elpy-pdb--start-pdb t))
- (elpy-pdb--refresh-breakpoints bp-lines)
- (elpy-pdb--start-pdb)
- (python-shell-send-string "continue")))
- (elpy-shell-display-buffer)))
-
- (defun elpy-pdb-break-at-point ()
- "Run pdb on the current buffer and break at the current line.
-
- Ignore the existing breakpoints.
- Pdb can directly exit if the current line is not a statement
- that is actually run (blank line, comment line, ...)."
- (interactive)
- (if (not (buffer-file-name))
- (error "Debugging only work for buffers visiting a file")
- (elpy-shell--ensure-shell-running)
- (save-buffer)
- (elpy-pdb--refresh-breakpoints (list (line-number-at-pos)))
- (elpy-pdb--start-pdb)
- (python-shell-send-string "continue")
- (elpy-shell-display-buffer)))
-
- (defun elpy-pdb-debug-last-exception ()
- "Run post-mortem pdb on the last exception."
- (interactive)
- (elpy-shell--ensure-shell-running)
- ;; check if there is a last exception
- (if (not (with-current-buffer (format "*%s*"
- (python-shell-get-process-name nil))
- (save-excursion
- (goto-char (point-max))
- (search-backward "Traceback (most recent call last):"
- nil t))))
- (error "No traceback on the current shell")
- (python-shell-send-string
- "import pdb as __pdb;__pdb.pm()"))
- (elpy-shell-display-buffer))
-
- ;; Fringe indicators
-
- (when (fboundp 'define-fringe-bitmap)
- (define-fringe-bitmap 'elpy-breakpoint-fringe-marker
- (vector
- #b00000000
- #b00111100
- #b01111110
- #b01111110
- #b01111110
- #b01111110
- #b00111100
- #b00000000)))
-
- (defcustom elpy-breakpoint-fringe-face 'elpy-breakpoint-fringe-face
- "Face for breakpoint bitmaps appearing on the fringe."
- :type 'face
- :group 'elpy)
-
- (defface elpy-breakpoint-fringe-face
- '((t (:foreground "red"
- :box (:line-width 1 :color "red" :style released-button))))
- "Face for breakpoint bitmaps appearing on the fringe."
- :group 'elpy)
-
- (defun elpy-pdb-toggle-breakpoint-at-point (&optional arg)
- "Add or remove a breakpoint at the current line.
-
- With a prefix argument, remove all the breakpoints from the current
- region or buffer."
- (interactive "P")
- (if arg
- (elpy-pdb-clear-breakpoints)
- (let ((overlays (overlays-in (line-beginning-position)
- (line-end-position)))
- bp-at-line)
- ;; Check if already a breakpoint
- (while overlays
- (let ((overlay (pop overlays)))
- (when (overlay-get overlay 'elpy-breakpoint)
- (setq bp-at-line t))))
- (if bp-at-line
- ;; If so, remove it
- (remove-overlays (line-beginning-position)
- (line-end-position)
- 'elpy-breakpoint t)
- ;; Check it the line is empty
- (if (not (save-excursion
- (beginning-of-line)
- (looking-at "[[:space:]]*$")))
- ;; Else add a new breakpoint
- (let* ((ov (make-overlay (line-beginning-position)
- (+ 1 (line-beginning-position))))
- (marker-string "*fringe-dummy*")
- (marker-length (length marker-string)))
- (put-text-property 0 marker-length
- 'display
- (list 'left-fringe
- 'elpy-breakpoint-fringe-marker
- 'elpy-breakpoint-fringe-face)
- marker-string)
- (overlay-put ov 'before-string marker-string)
- (overlay-put ov 'priority 200)
- (overlay-put ov 'elpy-breakpoint t)))))))
-
- (defun elpy-pdb-clear-breakpoints ()
- "Remove the breakpoints in the current region or buffer."
- (if (use-region-p)
- (remove-overlays (region-beginning) (region-end) 'elpy-breakpoint t)
- (remove-overlays (point-min) (point-max) 'elpy-breakpoint t))))
-
-
- ;;;;;;;;;;;;;;;;;;;;;;;
- ;; Deprecated functions
-
- (defun elpy-use-ipython (&optional _ipython)
- "Deprecated; see https://elpy.readthedocs.io/en/latest/ide.html#interpreter-setup"
- (error "elpy-use-ipython is deprecated; see https://elpy.readthedocs.io/en/latest/ide.html#interpreter-setup"))
- (make-obsolete 'elpy-use-ipython nil "Jan 2017")
-
- (defun elpy-use-cpython (&optional _cpython)
- "Deprecated; see https://elpy.readthedocs.io/en/latest/ide.html#interpreter-setup"
- (error "elpy-use-cpython is deprecated; see https://elpy.readthedocs.io/en/latest/ide.html#interpreter-setup"))
- (make-obsolete 'elpy-use-cpython nil "Jan 2017")
-
- (provide 'elpy-shell)
-
- ;;; elpy-shell.el ends here
|