python.el: Respect process environment for remote shells

* lisp/progmodes/python.el
(python-shell-calculate-process-environment): Calculate
process-environment or tramp-remote-process-environment depending
whether current file is remote.
(python-shell-calculate-exec-path): Calculate exec-path or
tramp-remote-path depending whether current file is remote.
(python-shell-with-environment): New macro.
(python-shell-prompt-detect, python-shell-calculate-command)
(python-shell-make-comint, python-check): Use it.
parent 8c81ac97
...@@ -180,6 +180,12 @@ ...@@ -180,6 +180,12 @@
;; shell so that relative imports work properly using the ;; shell so that relative imports work properly using the
;; `python-shell-package-enable' command. ;; `python-shell-package-enable' command.
;; Shell remote support: remote Python shells are started with the
;; correct environment for files opened remotely through tramp, also
;; respecting dir-local variables provided `enable-remote-dir-locals'
;; is non-nil. The logic for this is transparently handled by the
;; `python-shell-with-environment' macro.
;; Shell syntax highlighting: when enabled current input in shell is ;; Shell syntax highlighting: when enabled current input in shell is
;; highlighted. The variable `python-shell-font-lock-enable' controls ;; highlighted. The variable `python-shell-font-lock-enable' controls
;; activation of this feature globally when shells are started. ;; activation of this feature globally when shells are started.
...@@ -255,6 +261,7 @@ ...@@ -255,6 +261,7 @@
(require 'cl-lib) (require 'cl-lib)
(require 'comint) (require 'comint)
(require 'json) (require 'json)
(require 'tramp-sh)
;; Avoid compiler warnings ;; Avoid compiler warnings
(defvar view-return-to-alist) (defvar view-return-to-alist)
...@@ -2001,6 +2008,77 @@ virtualenv." ...@@ -2001,6 +2008,77 @@ virtualenv."
:type '(alist string) :type '(alist string)
:group 'python) :group 'python)
(defun python-shell-calculate-process-environment ()
"Calculate `process-environment' or `tramp-remote-process-environment'.
Pre-appends `python-shell-process-environment', sets extra
pythonpaths from `python-shell-extra-pythonpaths' and sets a few
virtualenv related vars. If `default-directory' points to a
remote machine, the returned value is intended for
`tramp-remote-process-environment'."
(let* ((remote-p (file-remote-p default-directory))
(process-environment (append
python-shell-process-environment
(if remote-p
tramp-remote-process-environment
process-environment) nil))
(virtualenv (if python-shell-virtualenv-root
(directory-file-name python-shell-virtualenv-root)
nil)))
(when python-shell-unbuffered
(setenv "PYTHONUNBUFFERED" "1"))
(when python-shell-extra-pythonpaths
(setenv "PYTHONPATH" (python-shell-calculate-pythonpath)))
(if (not virtualenv)
process-environment
(setenv "PYTHONHOME" nil)
(setenv "VIRTUAL_ENV" virtualenv))
process-environment))
(defun python-shell-calculate-exec-path ()
"Calculate `exec-path' or `tramp-remote-path'.
Pre-appends `python-shell-exec-path' and adds the binary
directory for virtualenv if `python-shell-virtualenv-root' is
set. If `default-directory' points to a remote machine, the
returned value is intended for `tramp-remote-path'."
(let ((path (append
;; Use nil as the tail so that the list is a full copy,
;; this is a paranoid safeguard for side-effects.
python-shell-exec-path
(if (file-remote-p default-directory)
tramp-remote-path
exec-path)
nil)))
(if (not python-shell-virtualenv-root)
path
(cons (expand-file-name "bin" python-shell-virtualenv-root)
path))))
(defmacro python-shell-with-environment (&rest body)
"Modify shell environment during execution of BODY.
Temporarily sets `process-environment' and `exec-path' during
execution of body. If `default-directory' points to a remote
machine then modifies `tramp-remote-process-environment' and
`tramp-remote-path' instead."
(declare (indent 0) (debug (body)))
(let ((remote-p (file-remote-p default-directory)))
`(let ((process-environment
(if ,remote-p
process-environment
(python-shell-calculate-process-environment)))
(tramp-remote-process-environment
(if ,remote-p
(python-shell-calculate-process-environment)
tramp-remote-process-environment))
(exec-path
(if ,remote-p
(python-shell-calculate-exec-path)
exec-path))
(tramp-remote-path
(if ,remote-p
(python-shell-calculate-exec-path)
tramp-remote-path)))
,(macroexp-progn body))))
(defvar python-shell--prompt-calculated-input-regexp nil (defvar python-shell--prompt-calculated-input-regexp nil
"Calculated input prompt regexp for inferior python shell. "Calculated input prompt regexp for inferior python shell.
Do not set this variable directly, instead use Do not set this variable directly, instead use
...@@ -2023,69 +2101,68 @@ shows a warning with instructions to avoid hangs and returns nil. ...@@ -2023,69 +2101,68 @@ shows a warning with instructions to avoid hangs and returns nil.
When `python-shell-prompt-detect-enabled' is nil avoids any When `python-shell-prompt-detect-enabled' is nil avoids any
detection and just returns nil." detection and just returns nil."
(when python-shell-prompt-detect-enabled (when python-shell-prompt-detect-enabled
(let* ((process-environment (python-shell-calculate-process-environment)) (python-shell-with-environment
(exec-path (python-shell-calculate-exec-path)) (let* ((code (concat
(code (concat "import sys\n"
"import sys\n" "ps = [getattr(sys, 'ps%s' % i, '') for i in range(1,4)]\n"
"ps = [getattr(sys, 'ps%s' % i, '') for i in range(1,4)]\n" ;; JSON is built manually for compatibility
;; JSON is built manually for compatibility "ps_json = '\\n[\"%s\", \"%s\", \"%s\"]\\n' % tuple(ps)\n"
"ps_json = '\\n[\"%s\", \"%s\", \"%s\"]\\n' % tuple(ps)\n" "print (ps_json)\n"
"print (ps_json)\n" "sys.exit(0)\n"))
"sys.exit(0)\n")) (output
(output (with-temp-buffer
(with-temp-buffer ;; TODO: improve error handling by using
;; TODO: improve error handling by using ;; `condition-case' and displaying the error message to
;; `condition-case' and displaying the error message to ;; the user in the no-prompts warning.
;; the user in the no-prompts warning. (ignore-errors
(ignore-errors (let ((code-file (python-shell--save-temp-file code)))
(let ((code-file (python-shell--save-temp-file code))) ;; Use `process-file' as it is remote-host friendly.
;; Use `process-file' as it is remote-host friendly. (process-file
(process-file python-shell-interpreter
python-shell-interpreter code-file
code-file '(t nil)
'(t nil) nil
nil python-shell-interpreter-interactive-arg)
python-shell-interpreter-interactive-arg) ;; Try to cleanup
;; Try to cleanup (delete-file code-file)))
(delete-file code-file))) (buffer-string)))
(buffer-string))) (prompts
(prompts (catch 'prompts
(catch 'prompts (dolist (line (split-string output "\n" t))
(dolist (line (split-string output "\n" t)) (let ((res
(let ((res ;; Check if current line is a valid JSON array
;; Check if current line is a valid JSON array (and (string= (substring line 0 2) "[\"")
(and (string= (substring line 0 2) "[\"") (ignore-errors
(ignore-errors ;; Return prompts as a list, not vector
;; Return prompts as a list, not vector (append (json-read-from-string line) nil)))))
(append (json-read-from-string line) nil))))) ;; The list must contain 3 strings, where the first
;; The list must contain 3 strings, where the first ;; is the input prompt, the second is the block
;; is the input prompt, the second is the block ;; prompt and the last one is the output prompt. The
;; prompt and the last one is the output prompt. The ;; input prompt is the only one that can't be empty.
;; input prompt is the only one that can't be empty. (when (and (= (length res) 3)
(when (and (= (length res) 3) (cl-every #'stringp res)
(cl-every #'stringp res) (not (string= (car res) "")))
(not (string= (car res) ""))) (throw 'prompts res))))
(throw 'prompts res)))) nil)))
nil))) (when (and (not prompts)
(when (and (not prompts) python-shell-prompt-detect-failure-warning)
python-shell-prompt-detect-failure-warning) (lwarn
(lwarn '(python python-shell-prompt-regexp)
'(python python-shell-prompt-regexp) :warning
:warning (concat
(concat "Python shell prompts cannot be detected.\n"
"Python shell prompts cannot be detected.\n" "If your emacs session hangs when starting python shells\n"
"If your emacs session hangs when starting python shells\n" "recover with `keyboard-quit' and then try fixing the\n"
"recover with `keyboard-quit' and then try fixing the\n" "interactive flag for your interpreter by adjusting the\n"
"interactive flag for your interpreter by adjusting the\n" "`python-shell-interpreter-interactive-arg' or add regexps\n"
"`python-shell-interpreter-interactive-arg' or add regexps\n" "matching shell prompts in the directory-local friendly vars:\n"
"matching shell prompts in the directory-local friendly vars:\n" " + `python-shell-prompt-regexp'\n"
" + `python-shell-prompt-regexp'\n" " + `python-shell-prompt-block-regexp'\n"
" + `python-shell-prompt-block-regexp'\n" " + `python-shell-prompt-output-regexp'\n"
" + `python-shell-prompt-output-regexp'\n" "Or alternatively in:\n"
"Or alternatively in:\n" " + `python-shell-prompt-input-regexps'\n"
" + `python-shell-prompt-input-regexps'\n" " + `python-shell-prompt-output-regexps'")))
" + `python-shell-prompt-output-regexps'"))) prompts))))
prompts)))
(defun python-shell-prompt-validate-regexps () (defun python-shell-prompt-validate-regexps ()
"Validate all user provided regexps for prompts. "Validate all user provided regexps for prompts.
...@@ -2181,14 +2258,12 @@ the `buffer-name'." ...@@ -2181,14 +2258,12 @@ the `buffer-name'."
(defun python-shell-calculate-command () (defun python-shell-calculate-command ()
"Calculate the string used to execute the inferior Python process." "Calculate the string used to execute the inferior Python process."
(let ((exec-path (python-shell-calculate-exec-path))) (python-shell-with-environment
;; `exec-path' gets tweaked so that virtualenv's specific ;; `exec-path' gets tweaked so that virtualenv's specific
;; `python-shell-interpreter' absolute path can be found by ;; `python-shell-interpreter' absolute path can be found by
;; `executable-find'. ;; `executable-find'.
(format "%s %s" (format "%s %s"
;; FIXME: Why executable-find? (shell-quote-argument python-shell-interpreter)
(shell-quote-argument
(executable-find python-shell-interpreter))
python-shell-interpreter-args))) python-shell-interpreter-args)))
(define-obsolete-function-alias (define-obsolete-function-alias
...@@ -2205,38 +2280,6 @@ the `buffer-name'." ...@@ -2205,38 +2280,6 @@ the `buffer-name'."
(concat extra path-separator pythonpath) (concat extra path-separator pythonpath)
extra))) extra)))
(defun python-shell-calculate-process-environment ()
"Calculate process environment given `python-shell-virtualenv-root'."
(let ((process-environment (append
python-shell-process-environment
process-environment nil))
(virtualenv (if python-shell-virtualenv-root
(directory-file-name python-shell-virtualenv-root)
nil)))
(when python-shell-unbuffered
(setenv "PYTHONUNBUFFERED" "1"))
(when python-shell-extra-pythonpaths
(setenv "PYTHONPATH" (python-shell-calculate-pythonpath)))
(if (not virtualenv)
process-environment
(setenv "PYTHONHOME" nil)
(setenv "PATH" (format "%s/bin%s%s"
virtualenv path-separator
(or (getenv "PATH") "")))
(setenv "VIRTUAL_ENV" virtualenv))
process-environment))
(defun python-shell-calculate-exec-path ()
"Calculate exec path given `python-shell-virtualenv-root'."
(let ((path (append
;; Use nil as the tail so that the list is a full copy,
;; this is a paranoid safeguard for side-effects.
python-shell-exec-path exec-path nil)))
(if (not python-shell-virtualenv-root)
path
(cons (expand-file-name "bin" python-shell-virtualenv-root)
path))))
(defvar python-shell--package-depth 10) (defvar python-shell--package-depth 10)
(defun python-shell-package-enable (directory package) (defun python-shell-package-enable (directory package)
...@@ -2561,31 +2604,30 @@ convention for temporary/internal buffers, and also makes sure ...@@ -2561,31 +2604,30 @@ convention for temporary/internal buffers, and also makes sure
the user is not queried for confirmation when the process is the user is not queried for confirmation when the process is
killed." killed."
(save-excursion (save-excursion
(let* ((proc-buffer-name (python-shell-with-environment
(format (if (not internal) "*%s*" " *%s*") proc-name)) (let* ((proc-buffer-name
(process-environment (python-shell-calculate-process-environment)) (format (if (not internal) "*%s*" " *%s*") proc-name)))
(exec-path (python-shell-calculate-exec-path))) (when (not (comint-check-proc proc-buffer-name))
(when (not (comint-check-proc proc-buffer-name)) (let* ((cmdlist (split-string-and-unquote cmd))
(let* ((cmdlist (split-string-and-unquote cmd)) (interpreter (car cmdlist))
(interpreter (car cmdlist)) (args (cdr cmdlist))
(args (cdr cmdlist)) (buffer (apply #'make-comint-in-buffer proc-name proc-buffer-name
(buffer (apply #'make-comint-in-buffer proc-name proc-buffer-name interpreter nil args))
interpreter nil args)) (python-shell--parent-buffer (current-buffer))
(python-shell--parent-buffer (current-buffer)) (process (get-buffer-process buffer))
(process (get-buffer-process buffer)) ;; Users can override the interpreter and args
;; Users can override the interpreter and args ;; interactively when calling `run-python', let-binding
;; interactively when calling `run-python', let-binding ;; these allows to have the new right values in all
;; these allows to have the new right values in all ;; setup code that is done in `inferior-python-mode',
;; setup code that is done in `inferior-python-mode', ;; which is important, especially for prompt detection.
;; which is important, especially for prompt detection. (python-shell--interpreter interpreter)
(python-shell--interpreter interpreter) (python-shell--interpreter-args
(python-shell--interpreter-args (mapconcat #'identity args " ")))
(mapconcat #'identity args " "))) (with-current-buffer buffer
(with-current-buffer buffer (inferior-python-mode))
(inferior-python-mode)) (when show (display-buffer buffer))
(when show (display-buffer buffer)) (and internal (set-process-query-on-exit-flag process nil))))
(and internal (set-process-query-on-exit-flag process nil)))) proc-buffer-name))))
proc-buffer-name)))
;;;###autoload ;;;###autoload
(defun run-python (&optional cmd dedicated show) (defun run-python (&optional cmd dedicated show)
...@@ -3984,8 +4026,7 @@ See `python-check-command' for the default." ...@@ -3984,8 +4026,7 @@ See `python-check-command' for the default."
""))))))) "")))))))
(setq python-check-custom-command command) (setq python-check-custom-command command)
(save-some-buffers (not compilation-ask-about-save) nil) (save-some-buffers (not compilation-ask-about-save) nil)
(let ((process-environment (python-shell-calculate-process-environment)) (python-shell-with-environment
(exec-path (python-shell-calculate-exec-path)))
(compilation-start command nil (compilation-start command nil
(lambda (_modename) (lambda (_modename)
(format python-check-buffer-name command))))) (format python-check-buffer-name command)))))
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment