emacs-ipython-notebook/lisp/ob-ein.el
John Miller af45786726 ob-ein: Enable asynchronous execution of source blocks.
Can be enabled or disabled via `ein:org-async-p'.

Greatly inspired by ob-ipython-async and scimax (see the source code, Luke!).
2018-03-08 09:07:31 -06:00

298 lines
12 KiB
EmacsLisp

;; -*- lexical-binding: t -*-
;;; ob-ein.el --- org-babel functions for template evaluation
;; Copyright (C) John M. Miller
;; Author: John M. Miller <millejoh at mac.com>
;;
;;; License:
;; This file is NOT part of GNU Emacs.
;; ob-ein.el 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.
;; ob-ein.el 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 ein-notebooklist.el. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Support executing org-babel source blocks using EIN worksheets.
;; Async support based on work by @khinsen on github in ob-ipython-async: https://github.com/khinsen/ob-ipython-async/blob/master/ob-ipython-async.el
;; which was in turn inspired by the scimax starter kit by @jkitchin: https://github.com/jkitchin/scimax
;;; Code:
(require 'ob)
(require 'ob-python)
(require 'cl)
(require 'ein-notebook)
(require 'ein-shared-output)
(require 'ein-utils)
(require 'python)
(defcustom ein:org-async-p t
"If non-nil run ein org-babel source blocks asyncronously."
:group 'ein
:type 'boolean)
(defcustom ein:org-inline-image-directory "ein-images"
"Default directory where to save images generated from ein
org-babel source blocks."
:group 'ein
:type '(directory))
;; declare default header arguments for this language
(defvar org-babel-default-header-args:ein '())
(add-to-list 'org-src-lang-modes '("ein" . python))
;; Handling source block execution results
(defun ein:temp-inline-image-info (value)
(let* ((f (md5 value))
(d ein:org-inline-image-directory)
(tf (concat d "/ob-ein-" f ".png")))
(unless (file-directory-p d)
(make-directory d))
tf))
(defun ein:write-base64-image (img-string file)
(with-temp-file file
(let ((buffer-read-only nil)
(buffer-file-coding-system 'binary)
(require-final-newline nil)
(file-precious-flag t))
(insert img-string)
(base64-decode-region (point-min) (point-max)))))
(defun ein:return-mime-type (json file)
(loop
for key in (cond
((functionp ein:output-type-preference)
(funcall ein:output-type-preference json))
(t ein:output-type-preference))
for type = (intern (format ":%s" key)) ; something like `:text'
for value = (plist-get json type) ; FIXME: optimize
when (plist-member json type)
return
(case key
((svg image/svg)
(let ((file (or file (ein:temp-inline-image-info value))))
(ein:write-base64-decoded-image value file)
(format "[[file:%s]]" file)))
((png image/png jpeg image/jpeg)
(let ((file (or file (ein:temp-inline-image-info value))))
(ein:write-base64-image value file)
(format "[[file:%s]]" file)))
(t (plist-get json type)))))
(defun org-babel-ein-process-outputs (outputs params)
(let ((file (cdr (assoc :image params))))
(ein:join-str "\n"
(loop for o in outputs
collecting (ein:return-mime-type o file)))))
;; Asynchronous execution requires each source code block to
;; be named. For blocks that have no name, an automatically
;; generated name is added. This variable holds the function
;; that generates the name.
(defvar *ein:org-name-generator* 'ein:uuid-generator
"Function to generate a name for a src block.
The default is `ein:uuid-generator'.")
(defun ein:uuid-generator ()
(org-id-new 'none))
(defun ein:org-get-name-create ()
"Get the name of a src block or add a uuid as the name."
(if-let* (name (fifth (org-babel-get-src-block-info)))
name
(save-excursion
(let ((el (org-element-context))
(id (funcall *ein:org-name-generator*)))
(goto-char (org-element-property :begin el))
(insert (format "#+NAME: %s\n" id))
id))))
(defcustom ein:org-execute-timeout 30
"Query timeout, in seconds, for executing ein source blocks in
org files."
:type 'number
:group 'ein)
;; This is the main function which is called to evaluate a code
;; block.
;;
;; This function will evaluate the body of the source code and
;; return the results as emacs-lisp depending on the value of the
;; :results header argument
;; - output means that the output to STDOUT will be captured and
;; returned
;; - value means that the value of the last statement in the
;; source code block will be returned
;;
(defun org-babel-execute:ein (body params)
"Execute a block of python code with org-babel by way of
emacs-ipython-notebook's facilities for communicating with
jupyter kernels.
This function is called by `org-babel-execute-src-block'"
(let* ((processed-params (org-babel-process-params params))
(kernelspec (cdr (assoc :kernelspec params)))
;; set the session if the session variable is non-nil
(session-kernel (org-babel-ein-initiate-session
(cdr (assoc :session processed-params))
kernelspec))
;; either OUTPUT or VALUE which should behave as described above
(result-type (cdr (assoc :result-type processed-params)))
;; expand the body with `org-babel-expand-body:template'
(full-body (org-babel-expand-body:generic (encode-coding-string body 'utf-8)
params
(org-babel-variable-assignments:python params))))
(if ein:org-async-p
(ein:ob-ein--execute-async full-body session-kernel processed-params (ein:org-get-name-create))
(ein:ob-ein--execute full-body session-kernel processed-params))))
(defun ein:ob-ein--execute-async (body kernel params name)
(lexical-let ((buffer (current-buffer))
(name name)
(body body)
(kernel kernel)
(params params))
(deferred:$
(deferred:next
(lambda ()
(message "Starting deferred ein execution: %s" name)
(ein:shared-output-eval-string body nil nil kernel)))
(deferred:nextc it
(deferred:lambda ()
(let ((cell (ein:shared-output-get-cell)))
(if (not (null (slot-value cell 'running)))
(deferred:nextc (deferred:wait 50) self)))))
(deferred:nextc it
(lambda ()
(let ((cell (ein:shared-output-get-cell)))
(if (and (slot-boundp cell 'traceback)
(slot-value cell 'traceback))
(ansi-color-apply (apply #'concat (mapcar #'(lambda (s)
(format "%s\n" s))
(slot-value cell 'traceback))))
(org-babel-ein-process-outputs (slot-value cell 'outputs) params)))))
(deferred:nextc it
(lambda (formatted-result)
(message "Finished deferred ein execution: %s" name)
(with-current-buffer buffer
(save-excursion
(org-babel-goto-named-result name)
(search-forward (format "[[ob-ein-async-running: %s]]" name))
(replace-match formatted-result)
(org-redisplay-inline-images)
;; (when (member "drawer" (cdr (assoc :result-params params)))
;; ;; open the results drawer
;; (org-babel-goto-named-result name)
;; (forward-line)
;; (org-flag-drawer nil))
)))))
(format "[[ob-ein-async-running: %s]]" name)))
(defun ein:ob-ein--execute (full-body session-kernel processed-params)
(ein:shared-output-eval-string full-body nil nil session-kernel)
(let ((cell (ein:shared-output-get-cell)))
(ein:wait-until #'(lambda ()
(null (slot-value cell 'running)))
nil ein:org-execute-timeout)
(if (and (slot-boundp cell 'traceback)
(slot-value cell 'traceback))
(ansi-color-apply (apply #'concat (mapcar #'(lambda (s)
(format "%s\n" s))
(slot-value cell 'traceback))))
(org-babel-ein-process-outputs (slot-value cell 'outputs) processed-params))))
(defun ein:org-find-or-open-session (session &optional kernelspec)
(multiple-value-bind (url-or-port path) (ein:org-babel-parse-session session)
(setf kernelspec (or kernelspec (ein:get-kernelspec url-or-port "default")))
(let ((nb (or (ein:notebook-get-opened-notebook url-or-port path)
(ein:notebook-open url-or-port path kernelspec
(lambda (nb param packed)
(multiple-value-bind (session kernelspec) packed
(org-babel-ein-initiate-session session kernelspec)))
(list session kernelspec)))))
(loop do (sit-for 1.0)
until (ein:kernel-live-p (ein:$notebook-kernel nb)))
nb)))
(defun org-babel-edit-prep:ein (babel-info)
"Set up source code completion for editing an EIN source block."
(let ((nb (ein:org-find-or-open-session (cdr (assoc :session (third babel-info))))))
(ein:connect-buffer-to-notebook nb (current-buffer) t)))
;; This function should be used to assign any variables in params in
;; the context of the session environment.
(defun org-babel-prep-session:ein (session params)
"Prepare SESSION according to the header arguments specified in PARAMS."
)
(defun org-babel-ein-var-to-template (var)
"Convert an elisp var into a string of template source code
specifying a var of the same value."
(format "%S" var))
(defun org-babel-ein-table-or-string (results)
"If the results look like a table, then convert them into an
Emacs-lisp table, otherwise return the results as a string."
)
(defun ein:org-babel-clean-url (url-or-port)
(if (search ":" url-or-port)
url-or-port
(string-to-number url-or-port)))
(defun ein:org-babel-parse-session (session)
(if (numberp session)
(values (format "http://localhost:%s" session) nil)
(let ((session-uri (url-generic-parse-url session)))
(cond ((url-fullness session-uri)
(values (format "%s://%s:%s" (url-type session-uri) (url-host session-uri) (url-port session-uri))
(url-filename session-uri)))
(t (let* ((url-or-port (ein:org-babel-clean-url (car (split-string session "/"))))
(path (ein:join-str "/" (rest (split-string session "/")))))
(values (format "http://localhost:%s" url-or-port) path)))))))
(defcustom ein:org-babel-default-session-name "ein_babel_session.ipynb"
"Default name for org babel sessions running ein environments.
This is the name of the notebook used when no notebook path is
given in the session parameter."
:type '(string :tag "Format string")
:group 'ein)
(defun org-babel-ein-initiate-session (&optional session kernelspec)
"If there is not a current inferior-process-buffer in SESSION then create.
Return the initialized session."
(when (and (stringp session) (string= session "none"))
(error "You must specify a notebook or kernelspec as the session variable for ein code blocks."))
(multiple-value-bind (url-or-port path) (ein:org-babel-parse-session session)
(if (null (gethash url-or-port ein:available-kernelspecs))
(ein:query-kernelspecs url-or-port))
(if (null kernelspec)
(setq kernelspec (ein:get-kernelspec url-or-port "default")))
(cond ((null path)
(let* ((name ein:org-babel-default-session-name)
(new-session (format "%s/%s" url-or-port name)))
(ein:notebooklist-new-notebook-with-name name kernelspec url-or-port)
(org-babel-ein-initiate-session new-session kernelspec)))
(t (let ((nb (ein:org-find-or-open-session session kernelspec)))
(ein:$notebook-kernel nb))))))
(provide 'ob-ein)
;;; ob-ein.el ends here