;;; ob-ipython.el --- org-babel functions for IPython evaluation ;; Author: Greg Sexton ;; Keywords: literate programming, reproducible research ;; Homepage: http://www.gregsexton.org ;; Package-Requires: ((s "1.9.0") (dash "2.10.0") (dash-functional "1.2.0") (f "0.17.2") (emacs "24")) ;; The MIT License (MIT) ;; Copyright (c) 2015 Greg Sexton ;; Permission is hereby granted, free of charge, to any person obtaining a copy ;; of this software and associated documentation files (the "Software"), to deal ;; in the Software without restriction, including without limitation the rights ;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ;; copies of the Software, and to permit persons to whom the Software is ;; furnished to do so, subject to the following conditions: ;; The above copyright notice and this permission notice shall be included in ;; all copies or substantial portions of the Software. ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ;; THE SOFTWARE. ;;; Commentary: ;; Org-Babel support for evaluating Python source code using IPython. ;;; Code: (require 'ob) (require 'ob-python) (require 'dash) (require 'dash-functional) (require 's) (require 'f) (require 'json) (require 'python) (require 'cl) ;; variables (defcustom ob-ipython-kernel-extra-args '() "List of extra args to pass when creating a kernel." :group 'ob-ipython) (defcustom ob-ipython-client-path (f-expand "./client.py" (or (-when-let (f load-file-name) (f-dirname f)) default-directory)) "Path to the client script." :group 'ob-ipython) (defcustom ob-ipython-command "jupyter" "Command to launch ipython. Usually ipython or jupyter." :group 'ob-ipython) (defcustom ob-ipython-resources-dir "./obipy-resources/" "Directory where resources (e.g images) are stored so that they can be displayed.") ;; utils (defun ob-ipython--write-string-to-file (file string) (if string (with-temp-buffer (let ((require-final-newline nil)) (insert string) (write-file file))) (error "No output was produced to write to a file."))) (defun ob-ipython--write-base64-string (file b64-string) (if b64-string (with-temp-buffer (let ((buffer-file-coding-system 'binary) (require-final-newline nil)) (insert b64-string) (base64-decode-region (point-min) (point-max)) (write-file file))) (error "No output was produced to write to a file."))) (defun ob-ipython--create-traceback-buffer (traceback) (let ((buf (get-buffer-create "*ob-ipython-traceback*"))) (with-current-buffer buf (special-mode) (let ((inhibit-read-only t)) (erase-buffer) (-each traceback (lambda (line) (insert (format "%s\n" line)))) (ansi-color-apply-on-region (point-min) (point-max)))) (pop-to-buffer buf))) (defun ob-ipython--create-inspect-buffer (doc) (let ((buf (get-buffer-create "*ob-ipython-inspect*"))) (with-current-buffer buf (special-mode) (let ((inhibit-read-only t)) (erase-buffer) (insert doc) (ansi-color-apply-on-region (point-min) (point-max)) (whitespace-cleanup) (goto-char (point-min)))) (pop-to-buffer buf))) (defun ob-ipython--clear-output-buffer () (let ((buf (get-buffer-create "*ob-ipython-out*"))) (with-current-buffer buf (let ((inhibit-read-only t)) (erase-buffer))))) (defun ob-ipython--output (output append-p) (when (not (s-blank? output)) (let ((buf (get-buffer-create "*ob-ipython-out*"))) (with-current-buffer buf (special-mode) (let ((inhibit-read-only t)) (unless append-p (erase-buffer)) (when (s-blank? (buffer-string)) (pop-to-buffer buf)) (let ((p (point))) (if (= p (point-max)) ;allow tailing (progn (insert output) (-when-let (w (get-buffer-window buf 'visible)) (set-window-point w (point-max)))) (save-excursion (goto-char (point-max)) (insert output))) (ansi-color-apply-on-region p (point-max)) ;; this adds some support for control chars (comint-carriage-motion p (point-max))) (unless append-p (goto-char (point-min)))))))) (defun ob-ipython--dump-error (err-msg) (with-current-buffer (get-buffer-create "*ob-ipython-debug*") (special-mode) (let ((inhibit-read-only t)) (erase-buffer) (insert err-msg) (goto-char (point-min)))) (error "There was a fatal error trying to process the request. See *ob-ipython-debug*")) (defun ob-ipython--generate-file-name (suffix) (s-concat (make-temp-name ob-ipython-resources-dir) suffix)) ;; process management (defun ob-ipython--kernel-file (name) (if (s-ends-with-p ".json" name) name (format "emacs-%s.json" name))) (defun ob-ipython--kernel-repl-cmd (name) (list ob-ipython-command "console" "--simple-prompt" "--existing" (ob-ipython--kernel-file name))) ;;; TODO: could setup a default sentinel that outputs error on process ;;; early termination (defun ob-ipython--create-process (name cmd) (let ((buf (get-buffer-create (format "*ob-ipython-%s*" name)))) (with-current-buffer buf (erase-buffer)) (apply 'start-process name buf (car cmd) (cdr cmd)))) (defun ob-ipython--get-python () (locate-file (if (eq system-type 'windows-nt) "python.exe" (or python-shell-interpreter "python")) exec-path)) (defun ob-ipython--create-kernel (name &optional kernel) (when (and (not (ignore-errors (process-live-p (get-process (format "kernel-%s" name))))) (not (s-ends-with-p ".json" name))) (ob-ipython--create-process (format "kernel-%s" name) (append (list ob-ipython-command "console" "--simple-prompt") (list "-f" (ob-ipython--kernel-file name)) (if kernel (list "--kernel" kernel) '()) ;;should be last in the list of args ob-ipython-kernel-extra-args)) (sleep-for 1))) (defun ob-ipython--get-kernel-processes () (let ((procs (-filter (lambda (p) (s-starts-with? "kernel-" (process-name p))) (process-list)))) (-zip (-map (-compose (-partial 's-replace "kernel-" "") 'process-name) procs) procs))) (defun ob-ipython--create-repl (name) (let ((python-shell-completion-native-enable nil) (cmd (s-join " " (ob-ipython--kernel-repl-cmd name)))) (if (string= "default" name) (progn (run-python cmd nil nil) (format "*%s*" python-shell-buffer-name)) (let ((process-name (format "Python:%s" name))) (get-buffer-process (python-shell-make-comint cmd process-name nil)) (format "*%s*" process-name))))) ;; kernel management (defun ob-ipython--choose-kernel () (let ((procs (ob-ipython--get-kernel-processes))) (-> (ido-completing-read "kernel? " (-map 'car procs) nil t) (assoc procs) cdr list))) ;;; TODO: make this work on windows ;;; NOTE: interrupting remote kernel not currently possible, cf https://github.com/jupyter/jupyter_console/issues/150 (defun ob-ipython-interrupt-kernel (proc) "Interrupt a running kernel. Useful for terminating infinite loops etc. If things get really desparate try `ob-ipython-kill-kernel'." (interactive (ob-ipython--choose-kernel)) (when proc ;; send SIGINT to "python -m ipykernel_launcher", a child of proc (let ((proc-name (process-name proc))) (accept-process-output ;; get the child pid with pgrep -P ;; NOTE assumes proc has only 1 child (seems to be true always) (make-process :name (concat proc-name "-child") :command (list "pgrep" "-P" (number-to-string (process-id proc))) ;; send SIGINT to child-proc :filter (lambda (proc child-proc-id) (make-process :name (concat "interrupt-" proc-name) :command (list "kill" "-2" (string-trim child-proc-id))))))))) (defun ob-ipython-kill-kernel (proc) "Kill a kernel process. If you then re-evaluate a source block a new kernel will be started." (interactive (ob-ipython--choose-kernel)) (when proc (delete-process proc) (message (format "Killed %s" (process-name proc))))) ;; evaluation (defvar ob-ipython--async-queue nil) (defun ob-ipython--enqueue (q x) (set q (append (symbol-value q) (list x)))) (defun ob-ipython--dequeue (q) (let ((ret (car (symbol-value q)))) (set q (cdr (symbol-value q))) ret)) (defun ob-ipython--collect-json () ;; this function assumes that we're in a buffer with the json lines (let ((json-array-type 'list)) (let (acc) (while (not (= (point) (point-max))) (setq acc (cons (json-read) acc)) (forward-line)) (nreverse acc)))) (defun ob-ipython--running-p () (get-process "execute")) (defun ob-ipython--run-async (code name callback args) (let ((proc (ob-ipython--create-process "execute" (list (ob-ipython--get-python) "--" ob-ipython-client-path "--conn-file" name "--execute")))) ;; TODO: maybe add a way of disabling streaming output? ;; TODO: cleanup and break out - we parse twice, can we parse once? (set-process-filter proc (lexical-let ((parse-pos 0)) (lambda (proc output) ;; not guaranteed to be given lines - we need to handle buffering (with-current-buffer (process-buffer proc) (goto-char (point-max)) (insert output) (let ((json-array-type 'list)) (goto-char parse-pos) (while (not (= (point) (point-max))) (condition-case nil (progn (-> (json-read) list ob-ipython--extract-output (ob-ipython--output t)) (forward-line) (setq parse-pos (point))) (error (goto-char (point-max)))))))))) (set-process-sentinel proc (lexical-let ((callback callback) (args args)) (lambda (proc state) (when (not (process-live-p proc)) (with-current-buffer (process-buffer proc) (goto-char (point-min)) (apply callback (-> (ob-ipython--collect-json) ob-ipython--eval (cons args)))) (ob-ipython--maybe-run-async))))) (process-send-string proc code) (process-send-string proc "\n") (process-send-eof proc))) (defun ob-ipython--maybe-run-async () (when (not (ob-ipython--running-p)) (when-let (val (ob-ipython--dequeue 'ob-ipython--async-queue)) (cl-destructuring-bind (code name callback args) val (ob-ipython--run-async code name callback args))))) (defun ob-ipython--execute-request-async (code name callback args) (ob-ipython--enqueue 'ob-ipython--async-queue (list code name callback args)) (ob-ipython--maybe-run-async)) (defun ob-ipython--execute-request (code name) (with-temp-buffer (let ((ret (apply 'call-process-region code nil (ob-ipython--get-python) nil t nil (list "--" ob-ipython-client-path "--conn-file" name "--execute")))) (if (> ret 0) (ob-ipython--dump-error (buffer-string)) (goto-char (point-min)) (ob-ipython--collect-json))))) (defun ob-ipython--extract-output (msgs) (->> msgs (-filter (lambda (msg) (string= "stream" (cdr (assoc 'msg_type msg))))) (-filter (lambda (msg) (-contains? '("stdout" "stderr") (->> msg (assoc 'content) (assoc 'name) cdr)))) (-map (lambda (msg) (->> msg (assoc 'content) (assoc 'text) cdr))) (-reduce 's-concat))) (defun ob-ipython--extract-result (msgs) `((:value . ,(->> msgs (-filter (lambda (msg) (s-equals? "execute_result" (cdr (assoc 'msg_type msg))))) (-mapcat (lambda (msg) (->> msg (assoc 'content) (assoc 'data) cdr))))) (:display . ,(->> msgs (-filter (lambda (msg) (s-equals? "display_data" (cdr (assoc 'msg_type msg))))) (-mapcat (lambda (msg) (->> msg (assoc 'content) (assoc 'data) cdr))))))) (defun ob-ipython--extract-error (msgs) (let ((error-content (->> msgs (-filter (lambda (msg) (-contains? '("execute_reply" "inspect_reply") (cdr (assoc 'msg_type msg))))) car (assoc 'content) cdr))) ;; TODO: this doesn't belong in this abstraction (ob-ipython--create-traceback-buffer (cdr (assoc 'traceback error-content))) (format "%s: %s" (cdr (assoc 'ename error-content)) (cdr (assoc 'evalue error-content))))) (defun ob-ipython--extract-status (msgs) (->> msgs (-filter (lambda (msg) (-contains? '("execute_reply" "inspect_reply" "complete_reply") (cdr (assoc 'msg_type msg))))) car (assoc 'content) (assoc 'status) cdr)) (defun ob-ipython--extract-execution-count (msgs) (->> msgs (-filter (lambda (msg) (-contains? '("execute_reply") (cdr (assoc 'msg_type msg))))) car (assoc 'content) (assoc 'execution_count) cdr)) (defun ob-ipython--eval (service-response) (let ((status (ob-ipython--extract-status service-response))) (cond ((string= "ok" status) `((:result . ,(ob-ipython--extract-result service-response)) (:output . ,(ob-ipython--extract-output service-response)) (:exec-count . ,(ob-ipython--extract-execution-count service-response)))) ((string= "abort" status) (error "Kernel execution aborted.")) ((string= "error" status) (error (ob-ipython--extract-error service-response)))))) ;; inspection (defun ob-ipython--inspect-request (code &optional pos detail) (let ((input (json-encode `((code . ,code) (pos . ,(or pos (length code))) (detail . ,(or detail 0))))) (args (list "--" ob-ipython-client-path "--conn-file" (ob-ipython--get-session-from-edit-buffer (current-buffer)) "--inspect"))) (with-temp-buffer (let ((ret (apply 'call-process-region input nil (ob-ipython--get-python) nil t nil args))) (if (> ret 0) (ob-ipython--dump-error (buffer-string)) (goto-char (point-min)) (ob-ipython--collect-json)))))) (defun ob-ipython--inspect (code pos) "Given a piece of code and a point position, return inspection results." (let* ((resp (ob-ipython--inspect-request code pos 0)) (status (ob-ipython--extract-status resp))) (if (string= "ok" status) (->> resp (-filter (lambda (msg) (-contains? '("execute_result" "display_data" "inspect_reply") (cdr (assoc 'msg_type msg))))) (-mapcat (lambda (msg) (->> msg (assoc 'content) (assoc 'data) cdr)))) (error (ob-ipython--extract-error resp))))) (defun ob-ipython-inspect (buffer pos) "Ask a kernel for documentation on the thing at POS in BUFFER." (interactive (list (current-buffer) (point))) (let ((code (with-current-buffer buffer (buffer-substring-no-properties (point-min) (point-max))))) (-if-let (result (->> (ob-ipython--inspect code pos) (assoc 'text/plain) cdr)) (ob-ipython--create-inspect-buffer result) (message "No documentation was found.")))) ;; completion (defun ob-ipython--complete-request (code &optional pos) (let ((input (json-encode `((code . ,code) (pos . ,(or pos (length code)))))) (args (list "--" ob-ipython-client-path "--conn-file" (ob-ipython--get-session-from-edit-buffer (current-buffer)) "--complete"))) (with-temp-buffer (let ((ret (apply 'call-process-region input nil (ob-ipython--get-python) nil t nil args))) (if (> ret 0) (ob-ipython--dump-error (buffer-string)) (goto-char (point-min)) (ob-ipython--collect-json)))))) (defun ob-ipython-completions (buffer pos) "Ask a kernel for completions on the thing at POS in BUFFER." (let* ((code (with-current-buffer buffer (buffer-substring-no-properties (point-min) (point-max)))) (resp (ob-ipython--complete-request code pos)) (status (ob-ipython--extract-status resp))) (if (not (string= "ok" status)) '() (->> resp (-filter (lambda (msg) (-contains? '("complete_reply") (cdr (assoc 'msg_type msg))))) (-mapcat (lambda (msg) (->> msg (assoc 'content) cdr))))))) (defun ob-ipython--company-doc-buffer (doc) "Make company-suggested doc-buffer with ansi-color support." (let ((buf (company-doc-buffer doc))) (with-current-buffer buf (ansi-color-apply-on-region (point-min) (point-max))) buf)) (defun company-ob-ipython (command &optional arg &rest ignored) (interactive (list 'interactive)) (cl-case command (interactive (company-begin-backend 'company-ob-ipython)) (prefix (and ob-ipython-mode (let ((res (ob-ipython-completions (current-buffer) (1- (point))))) (substring-no-properties (buffer-string) (cdr (assoc 'cursor_start res)) (cdr (assoc 'cursor_end res)))))) (candidates (cons :async (lambda (cb) (let ((res (ob-ipython-completions (current-buffer) (1- (point))))) (funcall cb (cdr (assoc 'matches res))))))) (sorted t) (doc-buffer (ob-ipython--company-doc-buffer (cdr (assoc 'text/plain (ob-ipython--inspect arg (length arg)))))))) ;; mode (define-minor-mode ob-ipython-mode "" nil " ipy" '()) ;; babel framework (add-to-list 'org-src-lang-modes '("ipython" . python)) (add-hook 'org-mode-hook 'ob-ipython-auto-configure-kernels) (defvar ob-ipython-configured-kernels nil) (defun ob-ipython--get-kernels () "Return a list of available jupyter kernels and their corresponding languages. The elements of the list have the form (\"kernel\" \"language\")." (and ob-ipython-command (let ((kernelspecs (cdar (json-read-from-string (shell-command-to-string (s-concat ob-ipython-command " kernelspec list --json")))))) (-map (lambda (spec) (cons (symbol-name (car spec)) (->> (cdr spec) (assoc 'spec) cdr (assoc 'language) cdr))) kernelspecs)))) (defun ob-ipython--configure-kernel (kernel-lang) "Configure org mode to use specified kernel." (let* ((kernel (car kernel-lang)) (language (cdr kernel-lang)) (jupyter-lang (concat "jupyter-" language)) (mode (intern (or (cdr (assoc language org-src-lang-modes)) (replace-regexp-in-string "[0-9]*" "" language)))) (header-args (intern (concat "org-babel-default-header-args:" jupyter-lang)))) (add-to-list 'org-src-lang-modes `(,jupyter-lang . ,mode)) ;; Only set defaults if the corresponding variable is nil or does not ;; exist yet. (unless (and (boundp header-args) (symbol-value header-args)) (set (intern (concat "org-babel-default-header-args:" jupyter-lang)) `((:session . ,language) (:kernel . ,kernel)))) (defalias (intern (concat "org-babel-execute:" jupyter-lang)) 'org-babel-execute:ipython) (defalias (intern (concat "org-babel-" jupyter-lang "-initiate-session")) 'org-babel-ipython-initiate-session) kernel-lang)) (defun ob-ipython-auto-configure-kernels (&optional replace) "Auto-configure kernels for use with org-babel based on the available kernelspecs of the current jupyter installation. If REPLACE is non-nil, force configuring the kernels even if they have previously been configured." (interactive (list t)) (when (or replace (not ob-ipython-configured-kernels)) (setq ob-ipython-configured-kernels (-map 'ob-ipython--configure-kernel (ob-ipython--get-kernels))))) (defvar org-babel-default-header-args:ipython '()) (defun org-babel-edit-prep:ipython (info) ;; TODO: based on kernel, should change the major mode (ob-ipython--create-kernel (->> info (nth 2) (assoc :session) cdr ob-ipython--normalize-session) (->> info (nth 2) (assoc :kernel) cdr)) (ob-ipython-mode +1)) (defun ob-ipython--normalize-session (session) (if (string= "default" session) (error "default is reserved for when no name is provided. Please use a different session name.") (or session "default"))) (defun ob-ipython--get-session-from-edit-buffer (buffer) (with-current-buffer buffer (->> org-src--babel-info (nth 2) (assoc :session) cdr ob-ipython--normalize-session))) (defun org-babel-execute:ipython (body params) "Execute a block of IPython code with Babel. This function is called by `org-babel-execute-src-block'." (ob-ipython--clear-output-buffer) (if (cdr (assoc :async params)) (ob-ipython--execute-async body params) (ob-ipython--execute-sync body params))) (defun ob-ipython--execute-async (body params) (let* ((file (cdr (assoc :ipyfile params))) (session (cdr (assoc :session params))) (result-type (cdr (assoc :result-type params))) (sentinel (ipython--async-gen-sentinel))) (ob-ipython--create-kernel (ob-ipython--normalize-session session) (cdr (assoc :kernel params))) (ob-ipython--execute-request-async (org-babel-expand-body:generic (encode-coding-string body 'utf-8) params (org-babel-variable-assignments:python params)) (ob-ipython--normalize-session session) (lambda (ret sentinel buffer file result-type) (let ((replacement (ob-ipython--process-response ret file result-type))) (ipython--async-replace-sentinel sentinel buffer replacement))) (list sentinel (current-buffer) file result-type)) (format "%s - %s" (length ob-ipython--async-queue) sentinel))) (defun ob-ipython--execute-sync (body params) (let* ((file (cdr (assoc :ipyfile params))) (session (cdr (assoc :session params))) (result-type (cdr (assoc :result-type params)))) (ob-ipython--create-kernel (ob-ipython--normalize-session session) (cdr (assoc :kernel params))) (-when-let (ret (ob-ipython--eval (ob-ipython--execute-request (org-babel-expand-body:generic (encode-coding-string body 'utf-8) params (org-babel-variable-assignments:python params)) (ob-ipython--normalize-session session)))) (ob-ipython--process-response ret file result-type)))) (defun ob-ipython--process-response (ret file result-type) (let ((result (cdr (assoc :result ret))) (output (cdr (assoc :output ret)))) (if (eq result-type 'output) output (ob-ipython--output output nil) (s-concat (format "# Out[%d]:\n" (cdr (assoc :exec-count ret))) (s-join "\n" (->> (-map (-partial 'ob-ipython--render file) (list (cdr (assoc :value result)) (cdr (assoc :display result)))) (remove-if-not nil))))))) (defun ob-ipython--render (file-or-nil values) (let ((org (lambda (value) value)) (png (lambda (value) (let ((file (or file-or-nil (ob-ipython--generate-file-name ".png")))) (ob-ipython--write-base64-string file value) (format "[[file:%s]]" file)))) (svg (lambda (value) (let ((file (or file-or-nil (ob-ipython--generate-file-name ".svg")))) (ob-ipython--write-string-to-file file value) (format "[[file:%s]]" file)))) (html (lambda (value) ;; ((eq (car value) 'text/html) ;; (let ((pandoc (executable-find "pandoc"))) ;; (and pandoc (with-temp-buffer ;; (insert value) ;; (shell-command-on-region ;; (point-min) (point-max) ;; (format "%s -f html -t org" pandoc) t t) ;; (s-trim (buffer-string)))))) )) (txt (lambda (value) (let ((lines (s-lines value))) (if (cdr lines) (->> lines (-map 's-trim) (s-join "\n ") (s-concat " ") (format "#+BEGIN_EXAMPLE\n%s\n#+END_EXAMPLE")) (s-concat ": " (car lines))))))) (or (-when-let (val (cdr (assoc 'text/org values))) (funcall org val)) (-when-let (val (cdr (assoc 'image/png values))) (funcall png val)) (-when-let (val (cdr (assoc 'image/svg+xml values))) (funcall svg val)) (-when-let (val (cdr (assoc 'text/plain values))) (funcall txt val))))) (defun org-babel-prep-session:ipython (session params) "Prepare SESSION according to the header arguments in PARAMS. VARS contains resolved variable references" ;; c-u c-c c-v c-z (error "Currently unsupported.")) (defun org-babel-load-session:ipython (session body params) "Load BODY into SESSION." ;; c-c c-v c-l (error "Currently unsupported.")) (defun org-babel-ipython-initiate-session (&optional session params) "Create a session named SESSION according to PARAMS." (if (string= session "none") (error "ob-ipython currently only supports evaluation using a session. Make sure your src block has a :session param.") (when (not (s-ends-with-p ".json" session)) (ob-ipython--create-kernel (ob-ipython--normalize-session session) (cdr (assoc :kernel params)))) (ob-ipython--create-repl (ob-ipython--normalize-session session)))) ;; async (defun ipython--async-gen-sentinel () ;; lifted directly from org-id. thanks. (let ((rnd (md5 (format "%s%s%s%s%s%s%s" (random) (current-time) (user-uid) (emacs-pid) (user-full-name) user-mail-address (recent-keys))))) (format "%s-%s-4%s-%s%s-%s" (substring rnd 0 8) (substring rnd 8 12) (substring rnd 13 16) (format "%x" (logior #b10000000 (logand #b10111111 (string-to-number (substring rnd 16 18) 16)))) (substring rnd 18 20) (substring rnd 20 32)))) (defun ipython--async-replace-sentinel (sentinel buffer replacement) (save-window-excursion (save-excursion (save-restriction (with-current-buffer buffer (goto-char (point-min)) (re-search-forward sentinel) (re-search-backward "\\(call\\|src\\)_\\|^[ \t]*#\\+\\(BEGIN_SRC\\|CALL:\\)") (org-babel-remove-result) (org-babel-insert-result replacement (cdr (assoc :result-params (nth 2 (org-babel-get-src-block-info))))) (org-redisplay-inline-images)))))) ;; lib (provide 'ob-ipython) ;;; ob-ipython.el ends here