From cc4ae4f19b391ff309584b49fbc9b3bb1f92428d Mon Sep 17 00:00:00 2001 From: Valentin Boettcher Date: Thu, 16 May 2024 10:50:33 -0400 Subject: [PATCH] implement running general scripts and detecting ipython --- python-vterm.el | 113 +++++++++++++++++++++++++++++++----------- scripts/get_env.py | 14 +++--- scripts/is_ipython.py | 5 ++ scripts/utility.py | 5 ++ 4 files changed, 100 insertions(+), 37 deletions(-) create mode 100644 scripts/is_ipython.py create mode 100644 scripts/utility.py diff --git a/python-vterm.el b/python-vterm.el index bdf1273..0c57214 100644 --- a/python-vterm.el +++ b/python-vterm.el @@ -77,6 +77,10 @@ parameters may be used, like python -q") "If non-nil, the PYTHON-VTERM-SEND-CURRENT-CELL will use ipythons `%run` magic to run a code cell.") (defvar-local python-vterm-repl-script-buffer nil) +(defvar-local python-vterm-repl-interpreter :python + "Reflects whether the inferior Python REPL is a Python or IPython shell. + +If in doubt, set this to :python.") (defvar python-vterm-repl-mode-map (let ((map (make-sparse-keymap))) @@ -110,6 +114,9 @@ If SESSION-NAME is not given, the default session name `main' is assumed." (defun python-vterm--launch (ses-name env context) + "Launch a new Python REPL buffer with SES-NAME and ENV. + +If CONTEXT is given, it is used to set the working directory and the script buffer." (let ((new-buffer (generate-new-buffer (python-vterm-repl-buffer-name ses-name))) (vterm-shell python-vterm-repl-program) @@ -119,6 +126,16 @@ If SESSION-NAME is not given, the default session name `main' is assumed." (setq default-directory (plist-get context :cwd)) (setq python-vterm-repl-script-buffer (plist-get context :script-buffer))) (python-vterm-repl-mode) + (run-with-timer .5 nil + (lambda (buffer) + (with-current-buffer buffer + (while (not (python-vterm-repl-prompt-status)) + (sit-for 0.1)) + (setq python-vterm-repl-interpreter + (if (python-vterm--execute-script "is_ipython") + :ipython :python)))) + new-buffer) + (add-function :filter-args (process-filter vterm--process) (python-vterm-repl-run-filter-functions-func ses-name))) new-buffer)) @@ -233,14 +250,61 @@ Return a corresponding symbol or nil if not ready for input." (defun python-vterm--get-script-file (name) "Return the full path of the script file with NAME." - (expand-file-name (concat "./scripts/" name) + (expand-file-name (concat "./scripts/" name ".py") (file-name-directory (symbol-file 'python-vterm-mode)))) -(defun python-vterm--execute-script (name) - "Execute the python script file with NAME." - (let ((script-file (python-vterm--get-script-file name))) - (python-vterm-paste-string (format "exec(open(\"%s\").read())" script-file)) - (if (not python-vterm-paste-with-return) (vterm-send-return)))) +(defun python-vterm--execute-script (name &rest args) + "Load the script with file NAME and call the eponymous function with ARGS. + +The script file is expected to be in the `scripts' directory of +the package and must contain exactly one function with the same +name as the file without the extension. The function must return +a JSON-serializable object. The function is called with the +arguments ARGS and the result is returned as a parsed JSON object +in the plist format. The functions from the utility script are +loaded into the repl as well. All loaded functions and modules +will be cleaned up afterwards." + (let ((utility-file (python-vterm--get-script-file name)) + (script-file (python-vterm--get-script-file "utility")) + (tmpfile (make-temp-file "python-vterm--" nil ".json")) + (python-vterm-paste-with-return nil) + (python-vterm-paste-with-clear nil) + (arglist (concat + (seq-reduce + (lambda (el rest) + (format "%s, \"%s" el rest)) + args "") + (if (seq-empty-p args) "" "\""))) + (up-to-now (buffer-string)) + (result nil)) + + (python-vterm-clear-line) + (python-vterm-paste-string (format "exec(open(\"%s\").read());" utility-file)) + (python-vterm-paste-string (format "exec(open(\"%s\").read());" script-file)) + (python-vterm-paste-string (format "%s(\"%s\"%s);" name tmpfile arglist)) + + ;; clean up all utility stuff + (python-vterm-paste-string "del dump_json;") + (python-vterm-paste-string (format "del %s" name)) + (python-vterm-send-return-key) + + (while + (progn (with-temp-buffer + (insert-file-contents tmpfile) + (buffer-string) + (if (= (buffer-size) 0) + (progn + (sleep-for 0.1) + t) + (progn + (delete-file tmpfile) + (setq result (json-parse-buffer :object-type 'plist :array-type 'list)) + nil))))) + (let ((inhibit-read-only t)) + (python-vterm-repl-clear-buffer) + (insert up-to-now)) + result)) + (defun python-vterm--read-script (name) "Read the content of the script file with NAME. @@ -255,27 +319,8 @@ The path of the script is expanded relative to the `scripts' directory." This returns a list of the current working directory of teh inferior Python process and the current active environment." (with-current-buffer buf - (let ((tmpfile (make-temp-file "python-vterm-context-" nil ".json"))) - (let ((uid (concat "python-vterm:" (org-id-uuid)))) - (python-vterm--execute-script "get_env.py") - (python-vterm-paste-string (concat "python_vterm__get_context(\"" tmpfile "\")\n")) - (if (not python-vterm-paste-with-return) (vterm-send-return)) - (let ((context-json "")) - (while - (progn (setq context-json - (with-temp-buffer - (insert-file-contents tmpfile) - (buffer-string))) - (if (string= context-json "") - (progn (sleep-for 0.1) - t) - nil))) - (delete-file tmpfile) - (let ((context - (json-parse-string (string-replace "\n" "" context-json) - :object-type 'plist - :array-type 'list))) - (plist-put context :script-buffer python-vterm-repl-script-buffer))))))) + (let ((context (python-vterm--execute-script "get_env"))) + (plist-put context :script-buffer python-vterm-repl-script-buffer)))) (defvar python-vterm-repl-copy-mode-map (let ((map (make-sparse-keymap))) @@ -322,6 +367,9 @@ inferior Python process and the current active environment." (defvar-local python-vterm-paste-with-return t "Whether to send a return key after pasting a string to the Python REPL.") +(defvar-local python-vterm-paste-with-clear t + "Whether to clear the line before pasting a string to the Python REPL.") + (defvar-local python-vterm-fellow-repl-buffer nil) (defvar-local python-vterm-session nil) (defvar-local python-vterm-context nil) @@ -387,12 +435,19 @@ If SESSION-NAME is given, the REPL with the session name, otherwise the main REPL, is used." (with-current-buffer (python-vterm-fellow-repl-buffer session-name) (goto-char (point-max)) - (vterm-send-key (kbd "C-a")) - (vterm-send-key (kbd "C-k")) + (when python-vterm-paste-with-clear + (python-vterm-clear-line session-name)) (vterm-send-string string t) (if python-vterm-paste-with-return (python-vterm-send-return-key)))) +(defun python-vterm-clear-line (&optional session-name) + "Clear the current line in the Python REPL buffer." + (with-current-buffer (python-vterm-fellow-repl-buffer session-name) + (goto-char (point-max)) + (vterm-send-key (kbd "C-a")) + (vterm-send-key (kbd "C-k")))) + (defun python-vterm-ensure-newline (str) "Add a newline at the end of STR if the last character is not a newline." (concat str (if (string= (substring str -1 nil) "\n") "" "\n"))) diff --git a/scripts/get_env.py b/scripts/get_env.py index 857d841..3222abb 100644 --- a/scripts/get_env.py +++ b/scripts/get_env.py @@ -1,12 +1,10 @@ -def python_vterm__get_context(tempfile): - import json +def get_env(tempfile): import os - with open(tempfile, "w") as f: - context = dict( + dump_json( + tempfile, + dict( env=["=".join(el) for el in list(os.environ.items())], cwd=os.getcwd(), - ) - json.dump(context, f) - - del os, json + ), + ) diff --git a/scripts/is_ipython.py b/scripts/is_ipython.py new file mode 100644 index 0000000..ef92fc4 --- /dev/null +++ b/scripts/is_ipython.py @@ -0,0 +1,5 @@ +def is_ipython(tempfile): + try: + return dump_json(tempfile, __IPYTHON__) + except NameError: + return dump_json(tempfile, False) diff --git a/scripts/utility.py b/scripts/utility.py new file mode 100644 index 0000000..ff9a261 --- /dev/null +++ b/scripts/utility.py @@ -0,0 +1,5 @@ +def dump_json(tempfile, value): + import json + + with open(tempfile, "w") as f: + json.dump(value, f)