2018-01-08 21:38:32 -06:00
|
|
|
;;; jupyter-base.el --- Core definitions for Jupyter -*- lexical-binding: t -*-
|
|
|
|
|
|
|
|
;; Copyright (C) 2018 Nathaniel Nicandro
|
|
|
|
|
|
|
|
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
|
|
|
;; Created: 06 Jan 2018
|
2019-07-24 15:23:04 -05:00
|
|
|
;; Version: 0.8.1
|
2018-01-08 21:38:32 -06:00
|
|
|
;; Keywords: jupyter literate-programming
|
|
|
|
|
|
|
|
;; This program is free software; you can redistribute it and/or
|
|
|
|
;; modify it under the terms of the GNU General Public License as
|
2019-05-31 09:44:39 -05:00
|
|
|
;; published by the Free Software Foundation; either version 3, or (at
|
2018-01-08 21:38:32 -06:00
|
|
|
;; 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 GNU Emacs; see the file COPYING. If not, write to the
|
|
|
|
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
|
|
|
;; Boston, MA 02111-1307, USA.
|
|
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
|
|
|
|
;; This file holds the core requires, variables, and type definitions necessary
|
|
|
|
;; for jupyter.
|
|
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
2019-03-10 19:53:54 -05:00
|
|
|
(eval-when-compile (require 'subr-x))
|
2018-01-04 23:03:18 -06:00
|
|
|
(require 'cl-lib)
|
|
|
|
(require 'eieio)
|
2018-05-20 12:09:00 -05:00
|
|
|
(require 'eieio-base)
|
2018-01-04 23:03:18 -06:00
|
|
|
(require 'json)
|
|
|
|
|
2018-10-25 13:30:25 -05:00
|
|
|
(declare-function tramp-dissect-file-name "tramp" (name &optional nodefault))
|
|
|
|
(declare-function tramp-file-name-user "tramp")
|
|
|
|
(declare-function tramp-file-name-host "tramp")
|
2018-11-30 21:24:07 -06:00
|
|
|
(declare-function jupyter-message-content "jupyter-messages" (msg))
|
2018-05-22 21:57:38 -05:00
|
|
|
|
2018-11-12 14:55:02 -06:00
|
|
|
;;; Custom variables
|
|
|
|
|
2019-02-20 23:58:29 -06:00
|
|
|
(defcustom jupyter-pop-up-frame nil
|
|
|
|
"Whether or not buffers should be displayed in a new frame by default.
|
|
|
|
Note, this variable is only considered when evaluating code
|
|
|
|
interactively with functions like `jupyter-eval-line-or-region'.
|
|
|
|
|
|
|
|
If equal to nil, frames will never be popped up. When equal to t,
|
|
|
|
pop-up frames instead of windows.
|
|
|
|
|
|
|
|
`jupyter-pop-up-frame' can also be a list of message type
|
|
|
|
keywords for messages which will cause frames to be used. For any
|
|
|
|
message type not in the list, windows will be used instead.
|
|
|
|
Currently only `:execute-result', `:error', and `:stream'
|
|
|
|
messages consider this variable."
|
|
|
|
:group 'jupyter
|
|
|
|
:type '(choice (const :tag "Pop up frames" t)
|
|
|
|
(const :tag "Pop up windows" nil)
|
|
|
|
;; TODO: These are the only ones where `jupyter-pop-up-frame'
|
|
|
|
;; is checked at the moment.
|
|
|
|
(set (const :execute-result)
|
|
|
|
(const :error)
|
|
|
|
(const :stream))))
|
|
|
|
|
2018-05-20 12:09:00 -05:00
|
|
|
(defconst jupyter-root (file-name-directory load-file-name)
|
|
|
|
"Root directory containing emacs-jupyter.")
|
|
|
|
|
2018-01-04 23:03:18 -06:00
|
|
|
(defconst jupyter-protocol-version "5.3"
|
|
|
|
"The jupyter protocol version that is implemented.")
|
|
|
|
|
2018-01-06 15:31:39 -06:00
|
|
|
(defconst jupyter-message-types
|
|
|
|
(list :execute-result "execute_result"
|
|
|
|
:execute-request "execute_request"
|
|
|
|
:execute-reply "execute_reply"
|
|
|
|
:inspect-request "inspect_request"
|
|
|
|
:inspect-reply "inspect_reply"
|
|
|
|
:complete-request "complete_request"
|
|
|
|
:complete-reply "complete_reply"
|
|
|
|
:history-request "history_request"
|
|
|
|
:history-reply "history_reply"
|
|
|
|
:is-complete-request "is_complete_request"
|
|
|
|
:is-complete-reply "is_complete_reply"
|
|
|
|
:comm-info-request "comm_info_request"
|
|
|
|
:comm-info-reply "comm_info_reply"
|
2018-05-20 12:09:00 -05:00
|
|
|
:comm-open "comm_open"
|
|
|
|
:comm-msg "comm_msg"
|
|
|
|
:comm-close "comm_close"
|
2018-01-06 15:31:39 -06:00
|
|
|
:kernel-info-request "kernel_info_request"
|
|
|
|
:kernel-info-reply "kernel_info_reply"
|
|
|
|
:shutdown-request "shutdown_request"
|
|
|
|
:shutdown-reply "shutdown_reply"
|
|
|
|
:interupt-request "interrupt_request"
|
|
|
|
:interrupt-reply "interrupt_reply"
|
|
|
|
:stream "stream"
|
|
|
|
:display-data "display_data"
|
|
|
|
:update-display-data "update_display_data"
|
|
|
|
:execute-input "execute_input"
|
|
|
|
:error "error"
|
|
|
|
:status "status"
|
|
|
|
:clear-output "clear_output"
|
2018-10-10 00:12:56 -05:00
|
|
|
:input-reply "input_reply"
|
|
|
|
:input-request "input_request")
|
2018-01-06 15:31:39 -06:00
|
|
|
"A plist mapping keywords to Jupyter message type strings.
|
|
|
|
The plist values are the message types either sent or received
|
|
|
|
from the kernel.")
|
2018-01-04 23:03:18 -06:00
|
|
|
|
2018-11-09 12:20:38 -06:00
|
|
|
(defconst jupyter-mime-types '(:application/vnd.jupyter.widget-view+json
|
|
|
|
:text/html :text/markdown
|
|
|
|
:image/svg+xml :image/jpeg :image/png
|
|
|
|
:text/latex :text/plain)
|
|
|
|
"MIME types handled by Jupyter.")
|
|
|
|
|
|
|
|
(defconst jupyter-nongraphic-mime-types '(:application/vnd.jupyter.widget-view+json
|
|
|
|
:text/html :text/markdown
|
|
|
|
:text/plain)
|
|
|
|
"MIME types that can be used in terminal Emacs.")
|
|
|
|
|
2018-11-08 00:23:10 -06:00
|
|
|
(defvar jupyter--debug nil
|
|
|
|
"When non-nil, some parts of Jupyter will emit debug statements.")
|
|
|
|
|
2018-11-11 21:13:40 -06:00
|
|
|
|
2019-03-02 18:35:44 -06:00
|
|
|
(defvar jupyter-default-timeout 2.5
|
2018-11-11 21:13:40 -06:00
|
|
|
"The default timeout in seconds for `jupyter-wait-until'.")
|
|
|
|
|
|
|
|
(defvar jupyter-long-timeout 10
|
2019-03-02 18:01:22 -06:00
|
|
|
"A longer timeout than `jupyter-default-timeout' used for some operations.
|
2018-11-11 21:13:40 -06:00
|
|
|
A longer timeout is needed, for example, when retrieving the
|
|
|
|
`jupyter-kernel-info' to allow for the kernel to startup.")
|
|
|
|
|
|
|
|
;;; Macros
|
|
|
|
|
|
|
|
(defmacro jupyter-with-timeout (spec &rest wait-forms)
|
|
|
|
"Periodically evaluate WAIT-FORMS until timeout.
|
2018-12-01 00:21:36 -06:00
|
|
|
Or until WAIT-FORMS evaluates to a non-nil value.
|
2018-11-11 21:13:40 -06:00
|
|
|
|
|
|
|
Wait until timeout SECONDS, periodically evaluating WAIT-FORMS
|
|
|
|
until it returns non-nil. If WAIT-FORMS returns non-nil, stop
|
|
|
|
waiting and return its value. Otherwise if timeout SECONDS
|
|
|
|
elapses, evaluate TIMEOUT-FORMS and return its value.
|
|
|
|
|
2018-12-06 00:39:39 -06:00
|
|
|
If PROGRESS is non-nil and evaluates to a string, a progress
|
2018-12-01 00:21:36 -06:00
|
|
|
reporter will be used with PROGRESS as the message while waiting.
|
2018-11-11 21:13:40 -06:00
|
|
|
|
|
|
|
SPEC takes the form (PROGRESS SECONDS TIMEOUT-FORMS...).
|
|
|
|
|
|
|
|
\(fn (PROGRESS SECONDS TIMEOUT-FORMS...) WAIT-FORMS...)"
|
|
|
|
(declare (indent 1) (debug ((form form body) body)))
|
|
|
|
(let ((res (make-symbol "res"))
|
|
|
|
(prog (make-symbol "prog"))
|
2019-03-17 02:05:00 -05:00
|
|
|
(prog-msg (make-symbol "prog-msg"))
|
|
|
|
(timeout (make-symbol "timeout"))
|
|
|
|
(wait-time (make-symbol "wait-time")))
|
2018-11-11 21:13:40 -06:00
|
|
|
`(let* ((,res nil)
|
|
|
|
(,prog-msg ,(pop spec))
|
2019-03-17 02:05:00 -05:00
|
|
|
(,timeout ,(pop spec))
|
|
|
|
(,wait-time (/ ,timeout 10.0))
|
2018-11-11 21:13:40 -06:00
|
|
|
(,prog (and (stringp ,prog-msg)
|
|
|
|
(make-progress-reporter ,prog-msg))))
|
2019-03-17 02:05:00 -05:00
|
|
|
(with-timeout (,timeout ,@spec)
|
2018-11-11 21:13:40 -06:00
|
|
|
(while (not (setq ,res (progn ,@wait-forms)))
|
2019-03-17 02:05:00 -05:00
|
|
|
(accept-process-output nil ,wait-time)
|
2018-11-11 21:13:40 -06:00
|
|
|
(when ,prog (progress-reporter-update ,prog))))
|
|
|
|
(prog1 ,res
|
|
|
|
(when ,prog (progress-reporter-done ,prog))))))
|
|
|
|
|
2018-11-09 17:50:33 -06:00
|
|
|
(defmacro jupyter-with-insertion-bounds (beg end bodyform &rest afterforms)
|
|
|
|
"Bind BEG and END to `point-marker's, evaluate BODYFORM then AFTERFORMS.
|
|
|
|
The END marker will advance if BODYFORM inserts text in the
|
|
|
|
current buffer. Thus after BODYFORM is evaluated, AFTERFORMS will
|
|
|
|
have access to the bounds of the text inserted by BODYFORM in the
|
2018-12-06 00:39:39 -06:00
|
|
|
variables BEG and END. The result of evaluating BODYFORM is
|
|
|
|
returned."
|
2018-11-09 17:50:33 -06:00
|
|
|
(declare (indent 3) (debug (symbolp symbolp form body)))
|
|
|
|
`(let ((,beg (point-marker))
|
|
|
|
(,end (point-marker)))
|
2019-06-30 12:16:29 -05:00
|
|
|
(set-marker-insertion-type ,end t)
|
2018-11-09 17:50:33 -06:00
|
|
|
(unwind-protect
|
|
|
|
(prog1 ,bodyform ,@afterforms)
|
|
|
|
(set-marker ,beg nil)
|
|
|
|
(set-marker ,end nil))))
|
|
|
|
|
2018-11-10 13:09:34 -06:00
|
|
|
(defmacro jupyter-loop-over-mime (mime-order mime data metadata &rest bodyforms)
|
|
|
|
"Loop over MIME types in MIME-ORDER.
|
|
|
|
MIME-ORDER should evaluate to a list of MIME types to loop over.
|
|
|
|
|
|
|
|
MIME will be bound to the MIME type for the current iteration.
|
|
|
|
DATA and METADATA are variables that hold the property list of
|
|
|
|
MIME data to loop over and any associated metadata, respectively.
|
|
|
|
|
|
|
|
Evaluate BODYFORMS with DATA and METADATA temporarily bound to
|
|
|
|
the data and metadata of the MIME type for the current iteration.
|
|
|
|
If BODYFORMS returns non-nil, return its value. Otherwise loop
|
|
|
|
over the next MIME type in MIME-ORDER that has a non-nil value in
|
|
|
|
the DATA property list."
|
|
|
|
(declare (indent 4) (debug ([&or form symbolp listp]
|
|
|
|
symbolp symbolp symbolp body)))
|
|
|
|
`(cl-loop
|
|
|
|
for ,mime in ,mime-order
|
|
|
|
thereis (let ((,data (plist-get ,data ,mime))
|
|
|
|
(,metadata (plist-get ,metadata ,mime)))
|
|
|
|
(when ,data ,@bodyforms))))
|
|
|
|
|
2018-11-18 14:11:50 -06:00
|
|
|
;;;; Display buffers
|
2018-11-12 14:55:02 -06:00
|
|
|
|
2018-11-18 14:11:50 -06:00
|
|
|
(defvar-local jupyter-display-buffer-marker nil
|
2018-11-12 14:55:02 -06:00
|
|
|
"The marker to store the last output position of an output buffer.
|
2018-11-18 14:11:50 -06:00
|
|
|
See `jupyter-with-display-buffer'.")
|
2018-11-12 14:55:02 -06:00
|
|
|
|
2018-11-18 14:11:50 -06:00
|
|
|
(defvar-local jupyter-display-buffer-request-id nil
|
2018-11-12 14:55:02 -06:00
|
|
|
"The last `jupyter-request' message ID that generated output.")
|
|
|
|
|
|
|
|
(defun jupyter-get-buffer-create (name)
|
|
|
|
"Return a buffer with some special properties.
|
|
|
|
|
|
|
|
- The buffer's name is based on NAME, specifically it will be
|
|
|
|
\"*jupyter-NAME*\"
|
|
|
|
|
2018-11-17 11:53:05 -06:00
|
|
|
- Its `major-mode' will be `special-mode'."
|
2018-11-18 14:11:50 -06:00
|
|
|
(let* ((bname (format "*jupyter-%s*" name))
|
2018-11-12 14:55:02 -06:00
|
|
|
(buffer (get-buffer bname)))
|
|
|
|
(unless buffer
|
|
|
|
(setq buffer (get-buffer-create bname))
|
|
|
|
(with-current-buffer buffer
|
2019-04-06 17:39:00 +00:00
|
|
|
;; For buffers such as the jupyter REPL, showing trailing whitespaces
|
|
|
|
;; may be a nuisance (as evidenced by the Python banner).
|
|
|
|
(setq-local show-trailing-whitespace nil)
|
2018-11-17 11:53:05 -06:00
|
|
|
(unless (eq major-mode 'special-mode)
|
|
|
|
(special-mode))))
|
2018-11-12 14:55:02 -06:00
|
|
|
buffer))
|
|
|
|
|
2018-11-18 14:11:50 -06:00
|
|
|
(defun jupyter--reset-display-buffer-p (arg)
|
2018-11-12 14:55:02 -06:00
|
|
|
"Return non-nil if the current output buffer should be reset.
|
|
|
|
If ARG is a `jupyter-request', reset the buffer if ARG's
|
|
|
|
`jupyter-request-id' is no equal to the
|
|
|
|
`jupyter-buffer-last-request-id'. If ARG is not a
|
|
|
|
`jupyter-request-id', return ARG."
|
|
|
|
(if (jupyter-request-p arg)
|
|
|
|
;; Reset the output buffer is the last request ID does not
|
|
|
|
;; match the current request's ID.
|
|
|
|
(let ((id (jupyter-request-id arg)))
|
2018-11-18 14:11:50 -06:00
|
|
|
(and (not (equal id jupyter-display-buffer-request-id))
|
|
|
|
(setq jupyter-display-buffer-request-id id)
|
2018-11-12 14:55:02 -06:00
|
|
|
t))
|
|
|
|
;; Otherwise reset the output buffer if RESET evaluates to a
|
|
|
|
;; non-nil value
|
|
|
|
arg))
|
|
|
|
|
2018-11-18 14:11:50 -06:00
|
|
|
(defmacro jupyter-with-display-buffer (name reset &rest body)
|
2019-02-15 21:43:00 -06:00
|
|
|
"In a buffer with a name derived from NAME current, evaluate BODY.
|
|
|
|
The buffer's name is obtained by a call to
|
2019-05-19 00:40:12 -05:00
|
|
|
`jupyter-get-buffer-create'.
|
|
|
|
|
|
|
|
A display buffer is similar to a *Help* buffer, but maintains its
|
|
|
|
previous output on subsequent invocations that use the same NAME
|
|
|
|
and BODY is wrapped using `jupyter-with-control-code-handling' so
|
|
|
|
that any insertions into the buffer that contain ANSI escape
|
|
|
|
codes are properly handled.
|
|
|
|
|
|
|
|
Note, before BODY is evaluated, `point' is moved to the end of
|
|
|
|
the most recent output.
|
|
|
|
|
|
|
|
Also note, the `jupyter-current-client' variable in the buffer
|
|
|
|
that BODY is evaluated in is let bound to whatever value it has
|
|
|
|
before making that buffer current.
|
2019-02-15 21:43:00 -06:00
|
|
|
|
|
|
|
RESET is a form or symbol that determines if the buffer should be
|
|
|
|
erased before evaluating BODY. If RESET is nil, no erasing of the
|
|
|
|
buffer is ever performed. If RESET evaluates to a
|
2018-11-12 14:55:02 -06:00
|
|
|
`jupyter-request' object, reset the buffer if the previous
|
|
|
|
request that generated output in the buffer is not the same
|
|
|
|
request. Otherwise if RESET evaluates to any non-nil value, reset
|
|
|
|
the output buffer."
|
|
|
|
(declare (indent 2) (debug (stringp [&or atom form] body)))
|
2019-05-19 00:40:12 -05:00
|
|
|
(let ((buffer (make-symbol "buffer"))
|
|
|
|
(client (make-symbol "client")))
|
|
|
|
`(let ((,client jupyter-current-client)
|
|
|
|
(,buffer (jupyter-get-buffer-create ,name)))
|
2018-11-12 14:55:02 -06:00
|
|
|
(setq other-window-scroll-buffer ,buffer)
|
|
|
|
(with-current-buffer ,buffer
|
2019-02-15 21:47:00 -06:00
|
|
|
(unless jupyter-display-buffer-marker
|
|
|
|
(setq jupyter-display-buffer-marker (point-max-marker))
|
|
|
|
(set-marker-insertion-type jupyter-display-buffer-marker t))
|
2019-05-19 00:40:12 -05:00
|
|
|
(let ((inhibit-read-only t)
|
|
|
|
(jupyter-current-client ,client))
|
2018-11-18 14:11:50 -06:00
|
|
|
(when (jupyter--reset-display-buffer-p ,reset)
|
2018-11-12 14:55:02 -06:00
|
|
|
(erase-buffer)
|
2019-02-15 21:47:00 -06:00
|
|
|
(set-marker jupyter-display-buffer-marker (point)))
|
2018-11-18 14:11:50 -06:00
|
|
|
(goto-char jupyter-display-buffer-marker)
|
2019-02-15 21:43:00 -06:00
|
|
|
(jupyter-with-control-code-handling ,@body))))))
|
2018-11-12 14:55:02 -06:00
|
|
|
|
2019-02-20 23:58:29 -06:00
|
|
|
(defun jupyter-display-current-buffer-reuse-window (&optional msg-type alist &rest actions)
|
2019-02-11 16:01:41 -06:00
|
|
|
"Convenience function to call `display-buffer' on the `current-buffer'.
|
|
|
|
If a window showing the current buffer is already available,
|
2019-02-20 23:58:29 -06:00
|
|
|
re-use it.
|
|
|
|
|
|
|
|
If ALIST is non-nil it is used as the ACTION alist of
|
|
|
|
`display-buffer'.
|
|
|
|
|
|
|
|
If MSG-TYPE is specified, it should be one of the keywords in
|
|
|
|
`jupyter-message-types' and is used in setting `pop-up-frames'
|
|
|
|
and `pop-up-windows'. See `jupyter-pop-up-frame'.
|
|
|
|
|
|
|
|
The rest of the arguments are display ACTIONS tried after
|
|
|
|
attempting to re-use a window and before attempting to pop-up a
|
|
|
|
new window or frame."
|
|
|
|
(let* ((jupyter-pop-up-frame (jupyter-pop-up-frame-p msg-type))
|
|
|
|
(pop-up-frames (and jupyter-pop-up-frame 'graphic-only))
|
|
|
|
(pop-up-windows (not jupyter-pop-up-frame))
|
|
|
|
(display-buffer-base-action
|
|
|
|
(cons
|
|
|
|
(append '(display-buffer-reuse-window)
|
|
|
|
(delq nil actions))
|
|
|
|
alist)))
|
2019-02-12 01:42:12 -06:00
|
|
|
(display-buffer (current-buffer))))
|
2019-02-11 16:01:41 -06:00
|
|
|
|
2019-02-20 23:58:29 -06:00
|
|
|
(defun jupyter-pop-up-frame-p (msg-type)
|
|
|
|
"Return non-nil if a frame should be popped up for MSG-TYPE."
|
|
|
|
(or (eq jupyter-pop-up-frame t)
|
|
|
|
(memq msg-type jupyter-pop-up-frame)))
|
|
|
|
|
2019-05-20 23:13:00 -05:00
|
|
|
(defun jupyter-display-current-buffer-guess-where (msg-type)
|
|
|
|
"Display the current buffer in a window or frame depending on MSG-TYPE.
|
|
|
|
Call `jupyter-display-current-buffer-reuse-window' passing
|
|
|
|
MSG-TYPE as argument. If MSG-TYPE should be displayed in a window
|
|
|
|
and the current buffer is not already being displayed, display
|
|
|
|
the buffer below the selected window."
|
|
|
|
(jupyter-display-current-buffer-reuse-window
|
|
|
|
msg-type nil (unless (jupyter-pop-up-frame-p msg-type)
|
|
|
|
#'display-buffer-below-selected)))
|
|
|
|
|
2019-06-28 20:02:00 -05:00
|
|
|
;;; Some useful classes
|
2018-01-04 23:03:18 -06:00
|
|
|
|
2018-11-13 18:21:11 -06:00
|
|
|
(defclass jupyter-instance-tracker ()
|
|
|
|
((tracking-symbol :type symbol))
|
|
|
|
:documentation "Similar to `eieio-instance-tracker', but keeping weak references.
|
|
|
|
To access all the objects in TRACKING-SYMBOL, use
|
|
|
|
`jupyter-all-objects'."
|
|
|
|
:abstract t)
|
|
|
|
|
2019-05-09 13:29:01 -05:00
|
|
|
(cl-defmethod initialize-instance ((obj jupyter-instance-tracker) &optional _slots)
|
2018-11-13 18:21:11 -06:00
|
|
|
(cl-call-next-method)
|
|
|
|
(let ((sym (oref obj tracking-symbol)))
|
2018-11-20 13:44:10 -06:00
|
|
|
(unless (hash-table-p (symbol-value sym))
|
2018-11-13 18:21:11 -06:00
|
|
|
(put sym 'jupyter-instance-tracker t)
|
|
|
|
(set sym (make-hash-table :weakness 'key)))
|
|
|
|
(puthash obj t (symbol-value sym))))
|
|
|
|
|
|
|
|
(defun jupyter-all-objects (sym)
|
|
|
|
"Return all tracked objects in tracking SYM.
|
2019-02-07 13:44:14 -06:00
|
|
|
SYM is a symbol used for tracking objects that inherit from the
|
|
|
|
class corresponding to the symbol `jupyter-instance-tracker'."
|
2018-11-13 18:21:11 -06:00
|
|
|
(let ((table (symbol-value sym)))
|
|
|
|
(when (hash-table-p table)
|
|
|
|
(cl-assert (get sym 'jupyter-instance-tracker) t)
|
|
|
|
(hash-table-keys table))))
|
|
|
|
|
2018-11-12 10:22:00 -06:00
|
|
|
(defclass jupyter-finalized-object ()
|
|
|
|
((finalizers :type list :initform nil))
|
|
|
|
:documentation "A list of finalizers."
|
|
|
|
:documentation "A base class for cleaning up resources.
|
|
|
|
Adds the method `jupyter-add-finalizer' which maintains a list of
|
|
|
|
finalizer functions to be called when the object is garbage
|
|
|
|
collected.")
|
|
|
|
|
|
|
|
(cl-defgeneric jupyter-add-finalizer ((obj jupyter-finalized-object) finalizer)
|
|
|
|
"Cleanup resources automatically.
|
|
|
|
FINALIZER if a function to be added to a list of finalizers that
|
|
|
|
will be called when OBJ is garbage collected."
|
|
|
|
(declare (indent 1))
|
|
|
|
(cl-check-type finalizer function)
|
|
|
|
(push (make-finalizer finalizer) (oref obj finalizers)))
|
|
|
|
|
2018-01-04 23:03:18 -06:00
|
|
|
;;; Session object definition
|
|
|
|
|
2019-06-28 20:02:00 -05:00
|
|
|
(declare-function jupyter-new-uuid "jupyter-messages")
|
|
|
|
|
2018-01-04 23:03:18 -06:00
|
|
|
(cl-defstruct (jupyter-session
|
|
|
|
(:constructor nil)
|
|
|
|
(:constructor
|
|
|
|
jupyter-session
|
2018-05-06 23:38:09 -05:00
|
|
|
(&key
|
|
|
|
(conn-info nil)
|
|
|
|
(id (jupyter-new-uuid))
|
|
|
|
(key nil))))
|
2018-02-03 19:25:50 -06:00
|
|
|
"A `jupyter-session' holds the information needed to
|
2018-05-26 20:04:02 -05:00
|
|
|
authenticate messages. A `jupyter-session' contains the following
|
|
|
|
fields:
|
|
|
|
|
|
|
|
- CONN-INFO :: The connection info. property list of the kernel
|
|
|
|
this session is used to sign messages for.
|
|
|
|
|
2019-06-28 20:03:00 -05:00
|
|
|
- ID :: A string of bytes that uniquely identifies this session.
|
2018-05-26 20:04:02 -05:00
|
|
|
|
|
|
|
- KEY :: The key used when signing messages. If KEY is nil,
|
|
|
|
message signing is not performed."
|
2018-05-06 23:38:09 -05:00
|
|
|
(conn-info nil :read-only t)
|
2018-01-04 23:03:18 -06:00
|
|
|
(id nil :read-only t)
|
|
|
|
(key nil :read-only t))
|
|
|
|
|
Generalize communication with a kernel
The previous mechanism to communicate with a kernel was too low level from the
perspective of a client. The client interfaced directly with the subprocess
abstraction, `jupyter-ioloop`, and had to handle all "events" that occurred in
the `jupyter-ioloop`, e.g. when a channel was started or stopped. But in
reality such events should not be the concern of a client.
A client should only care about events that are directly related to kernel
messages and not events related to the implementation details of *how*
communication occurs.
This commit abstracts out the way in which a client communicates with its
kernel by introducing a new `jupyter-comm-layer` class. The
`jupyter-comm-layer` class takes care of managing the communication channel
between a kernel and its clients as well as sending events to all registered
clients. This way, clients operate solely at the level of events on the
communication layer. All a client does is register itself to receive events on
the communication layer and send events on the layer.
* jupyter-base.el (jupyter-session-endpoints): New function.
* jupyter-client.el (jupyter-kernel-client): Remove ioloop and channels slots.
Add kcomm slot.
(initialize-instance): Unconditionally stop channels.
(jupyter-initialize-connection): Change into a method call.
Call `jupyter-initialize-connection` on the `kcomm` slot.
(jupyter-with-client-buffer): Remove stale comment.
(jupyter-send): Call `jupyter-send` on the `kcomm` slot.
(jupyter-ioloop-handler): Remove all method definitions, replace `sent` and
`message` methods with their `jupyter-event-handler` equivalents.
(jupyter-hb-pause, jupyter-hb-unpause, jupyter-hb-beating):
(jupyter-channel-alive-p, jupyter-start-channel, jupyter-stop-channel):
(jupyter-start-channels, jupyter-stop-channels):
Replace with calls to their equivalents using the `kcomm` slot.
* jupyter-comm-layer.el: New file.
* jupyter-kernel-manager (jupyter-make-client): Set a client's `kcomm` slot to
`jupyter-channel-ioloop-comm`.
* jupyter-messages.el (jupyter-decode-message): Use `list` directly. There
seemed to be issues when using the new `jupyter-sync-channel-comm` due to
using quoted lists.
* test/jupyter-test.el: Add `jupyter-comm-layer` test. Update other tests.
* test/test-helper.el: Add `jupyter-comm-layer` mock objects. Update
`jupyter-echo-client`.
2019-04-08 11:42:00 -05:00
|
|
|
(cl-defmethod jupyter-session-endpoints ((session jupyter-session))
|
|
|
|
"Return a property list containing the endpoints from SESSION."
|
|
|
|
(cl-destructuring-bind
|
|
|
|
(&key shell_port iopub_port stdin_port hb_port ip transport
|
|
|
|
&allow-other-keys)
|
|
|
|
(jupyter-session-conn-info session)
|
|
|
|
(cl-assert (and transport ip))
|
|
|
|
(let ((addr (lambda (port) (format "%s://%s:%d" transport ip port))))
|
|
|
|
(cl-loop
|
|
|
|
for (channel . port) in `((:hb . ,hb_port)
|
|
|
|
(:stdin . ,stdin_port)
|
|
|
|
(:shell . ,shell_port)
|
|
|
|
(:iopub . ,iopub_port))
|
|
|
|
do (cl-assert port) and
|
|
|
|
collect channel and collect (funcall addr port)))))
|
|
|
|
|
2018-01-04 23:03:18 -06:00
|
|
|
;;; Request object definition
|
|
|
|
|
2018-11-15 18:49:55 -06:00
|
|
|
(cl-defstruct (jupyter-request
|
|
|
|
(:constructor nil)
|
|
|
|
(:constructor jupyter-request))
|
2018-02-03 19:25:50 -06:00
|
|
|
"A `jupyter-request' encapsulates the current status of a
|
|
|
|
request to a kernel. A `jupyter-request' consists of the
|
|
|
|
following fields:
|
|
|
|
|
2018-09-07 04:29:29 -05:00
|
|
|
- ID :: A UUID to match a `jupyter-request' to the received
|
|
|
|
messages of a kernel.
|
2018-02-03 19:25:50 -06:00
|
|
|
|
|
|
|
- TIME :: The time at which the request was made.
|
|
|
|
|
|
|
|
- IDLE-RECEIVED-P :: A flag variable that is set to t when a
|
|
|
|
`jupyter-kernel-client' has received the
|
|
|
|
status: idle message for the request.
|
|
|
|
|
2018-05-20 12:09:00 -05:00
|
|
|
- LAST-MESSAGE :: The raw message property list of the last
|
|
|
|
message received by the kernel in response to
|
|
|
|
this request.
|
|
|
|
|
2018-02-08 13:38:27 -06:00
|
|
|
- INHIBITED-HANDLERS :: A list of handler message types to
|
|
|
|
prevent the running of that particular
|
|
|
|
handler. If set to t, disable all
|
|
|
|
handlers for this request. Note this
|
|
|
|
should not be set directly, dynamically
|
|
|
|
bind `jupyter-inhibit-handlers' before
|
|
|
|
making the request.
|
2018-02-03 19:25:50 -06:00
|
|
|
|
|
|
|
- CALLBACKS :: An alist mapping message types to their
|
|
|
|
corresponding callbacks. This alist is modified
|
|
|
|
through calls to `jupyter-add-callback' on the request."
|
2018-09-07 04:29:29 -05:00
|
|
|
(id "")
|
2018-01-04 23:03:18 -06:00
|
|
|
(time (current-time))
|
|
|
|
(idle-received-p nil)
|
2018-05-20 12:09:00 -05:00
|
|
|
(last-message nil)
|
2018-02-08 13:38:27 -06:00
|
|
|
(inhibited-handlers nil)
|
2018-01-04 23:03:18 -06:00
|
|
|
(callbacks))
|
|
|
|
|
2018-05-06 23:38:09 -05:00
|
|
|
;;; Connecting to a kernel's channels
|
|
|
|
|
2019-02-18 10:03:18 -06:00
|
|
|
(eval-when-compile (require 'tramp))
|
|
|
|
|
2019-06-28 20:03:00 -05:00
|
|
|
(defun jupyter-available-local-ports (n)
|
|
|
|
"Return a list of N ports available on the localhost."
|
|
|
|
(let (servers)
|
|
|
|
(unwind-protect
|
|
|
|
(cl-loop
|
|
|
|
repeat n
|
|
|
|
do (push (make-network-process
|
|
|
|
:name "jupyter-available-local-ports"
|
|
|
|
:server t
|
|
|
|
:host "127.0.0.1"
|
|
|
|
:service t)
|
|
|
|
servers)
|
|
|
|
finally return (mapcar (lambda (p) (cadr (process-contact p))) servers))
|
|
|
|
(mapc #'delete-process servers))))
|
|
|
|
|
|
|
|
(defun jupyter-make-ssh-tunnel (lport rport server remoteip)
|
|
|
|
(or remoteip (setq remoteip "127.0.0.1"))
|
|
|
|
(start-process
|
|
|
|
"jupyter-ssh-tunnel" nil
|
|
|
|
"ssh"
|
|
|
|
;; Run in background
|
|
|
|
"-f"
|
|
|
|
;; Wait until the tunnel is open
|
|
|
|
"-o ExitOnForwardFailure=yes"
|
|
|
|
;; Local forward
|
|
|
|
"-L" (format "127.0.0.1:%d:%s:%d" lport remoteip rport)
|
|
|
|
server
|
|
|
|
;; Close the tunnel if no other connections are made within 60
|
|
|
|
;; seconds
|
|
|
|
"sleep 60"))
|
|
|
|
|
2018-05-15 19:25:03 -05:00
|
|
|
(defun jupyter-tunnel-connection (conn-file &optional server)
|
|
|
|
"Forward local ports to the remote ports in CONN-FILE.
|
|
|
|
CONN-FILE is the path to a Jupyter connection file, SERVER is the
|
|
|
|
host that the kernel connection in CONN-FILE is located. Return a
|
|
|
|
copy of the connection plist in CONN-FILE, but with the ports
|
|
|
|
replaced by the local ports used for the forwarding.
|
|
|
|
|
2019-02-17 23:06:00 -06:00
|
|
|
If CONN-FILE is a `tramp' file name, the SERVER argument will be
|
2018-05-15 19:25:03 -05:00
|
|
|
ignored and the host will be extracted from the information
|
|
|
|
contained in the file name.
|
|
|
|
|
2019-06-28 20:03:00 -05:00
|
|
|
Note only SSH tunnels are currently supported."
|
2019-03-19 00:38:00 -05:00
|
|
|
(catch 'no-tunnels
|
|
|
|
(let ((conn-info (jupyter-read-plist conn-file)))
|
|
|
|
(when (and (file-remote-p conn-file)
|
|
|
|
(functionp 'tramp-dissect-file-name))
|
|
|
|
(pcase-let (((cl-struct tramp-file-name method user host)
|
|
|
|
(tramp-dissect-file-name conn-file)))
|
|
|
|
(pcase method
|
|
|
|
("docker"
|
|
|
|
;; Assume docker is using the -p argument to publish its exposed
|
|
|
|
;; ports to the localhost. The ports used in the container should
|
|
|
|
;; be the same ports accessible on the local host. For example, if
|
|
|
|
;; the shell port is on 1234 in the container, the published port
|
|
|
|
;; flag should be "-p 1234:1234".
|
|
|
|
(throw 'no-tunnels conn-info))
|
|
|
|
(_
|
|
|
|
(setq server (if user (concat user "@" host)
|
|
|
|
host))))))
|
2019-06-28 20:03:00 -05:00
|
|
|
(let* ((keys '(:hb_port :shell_port :control_port
|
|
|
|
:stdin_port :iopub_port))
|
|
|
|
(lports (jupyter-available-local-ports (length keys))))
|
|
|
|
(cl-loop
|
|
|
|
with remoteip = (plist-get conn-info :ip)
|
|
|
|
for (key maybe-rport) on conn-info by #'cddr
|
|
|
|
collect key and if (memq key keys)
|
|
|
|
collect (let ((lport (pop lports)))
|
|
|
|
(prog1 lport
|
|
|
|
(jupyter-make-ssh-tunnel lport maybe-rport server remoteip)))
|
|
|
|
else collect maybe-rport)))))
|
2018-05-15 19:25:03 -05:00
|
|
|
|
2018-05-15 17:53:32 -05:00
|
|
|
;;; Helper functions
|
|
|
|
|
2019-01-15 16:08:58 -06:00
|
|
|
(defvar server-buffer)
|
|
|
|
(defvar jupyter-current-client)
|
|
|
|
(defvar jupyter-server-mode-client-timer nil
|
|
|
|
"Timer used to unset `jupyter-current-client' from `server-buffer'.")
|
|
|
|
|
|
|
|
;; FIXME: This works if we only consider a single send request that will also
|
|
|
|
;; finish within TIMEOUT which is probably 99% of the cases. It doesn't work
|
|
|
|
;; for multiple requests that have been sent using different clients where one
|
|
|
|
;; sets the client in `server-buffer' and, before a file is opened by the
|
|
|
|
;; underlying kernel, another sets the client in `server-buffer'.
|
|
|
|
|
|
|
|
(defun jupyter-server-mode-set-client (client &optional timeout)
|
|
|
|
"Set CLIENT as the `jupyter-current-client' in the `server-buffer'.
|
|
|
|
Kill `jupyter-current-client's local value in `server-buffer'
|
|
|
|
after TIMEOUT seconds, defaulting to `jupyter-long-timeout'.
|
|
|
|
|
|
|
|
If a function causes a buffer to be displayed through
|
|
|
|
emacsclient, e.g. when a function calls an external command that
|
|
|
|
invokes the EDITOR, we don't know when the buffer will be
|
|
|
|
displayed. All we know is that the buffer that will be current
|
|
|
|
before display will be the `server-buffer'. So we temporarily set
|
|
|
|
`jupyter-current-client' in `server-buffer' so that the client
|
|
|
|
gets a chance to be propagated to the displayed buffer, see
|
|
|
|
`jupyter-repl-persistent-mode'.
|
|
|
|
|
|
|
|
For this to work properly you should have something like the
|
|
|
|
following in your Emacs configuration
|
|
|
|
|
|
|
|
(server-mode 1)
|
|
|
|
(setenv \"EDITOR\" \"emacsclient\")
|
|
|
|
|
|
|
|
before starting any Jupyter kernels. The kernel also has to know
|
|
|
|
that it should use EDITOR to open files."
|
|
|
|
(when (bound-and-true-p server-mode)
|
|
|
|
(with-current-buffer (get-buffer-create server-buffer)
|
|
|
|
(setq jupyter-current-client client)
|
|
|
|
(jupyter-server-mode--unset-client-soon timeout))))
|
|
|
|
|
|
|
|
(defun jupyter-server-mode-unset-client ()
|
|
|
|
"Set `jupyter-current-client' to nil in `server-buffer'."
|
|
|
|
(when (and (bound-and-true-p server-mode)
|
|
|
|
(get-buffer server-buffer))
|
|
|
|
(with-current-buffer server-buffer
|
|
|
|
(setq jupyter-current-client nil))))
|
|
|
|
|
|
|
|
(defun jupyter-server-mode--unset-client-soon (&optional timeout)
|
|
|
|
(when (timerp jupyter-server-mode-client-timer)
|
|
|
|
(cancel-timer jupyter-server-mode-client-timer))
|
|
|
|
(setq jupyter-server-mode-client-timer
|
|
|
|
(run-at-time (or timeout jupyter-long-timeout)
|
|
|
|
nil #'jupyter-server-mode-unset-client)))
|
|
|
|
|
|
|
|
;; After switching to a server buffer, keep the client alive in `server-buffer'
|
|
|
|
;; to account for multiple files being opened by the server.
|
|
|
|
(add-hook 'server-switch-hook #'jupyter-server-mode--unset-client-soon)
|
|
|
|
|
2018-05-15 17:53:32 -05:00
|
|
|
(defun jupyter-read-plist (file)
|
|
|
|
"Read a JSON encoded FILE as a property list."
|
2018-05-22 21:43:08 -05:00
|
|
|
(let ((json-object-type 'plist))
|
2018-05-15 17:53:32 -05:00
|
|
|
(json-read-file file)))
|
|
|
|
|
2018-05-20 12:09:00 -05:00
|
|
|
(defun jupyter-read-plist-from-string (string)
|
2018-05-22 21:43:08 -05:00
|
|
|
"Read a property list from a JSON encoded STRING."
|
|
|
|
(let ((json-object-type 'plist))
|
2018-05-20 12:09:00 -05:00
|
|
|
(json-read-from-string string)))
|
|
|
|
|
2018-11-30 21:24:07 -06:00
|
|
|
(defun jupyter-normalize-data (plist &optional metadata)
|
|
|
|
"Return a list (DATA META) from PLIST.
|
|
|
|
DATA is a property list of mimetype data extracted from PLIST. If
|
|
|
|
PLIST is a message plist, then DATA will be the value of the
|
|
|
|
:data key in the messages contents. If PLIST is not a message
|
|
|
|
plist, then DATA is either the :data key of PLIST or PLIST
|
|
|
|
itself.
|
|
|
|
|
|
|
|
A similar extraction process is performed for the :metadata key
|
|
|
|
of PLIST which will be the META argument in the return value. If
|
|
|
|
no :metadata key can be found, then META will be METADATA."
|
|
|
|
(list
|
|
|
|
(or
|
|
|
|
;; Allow for passing message plists
|
|
|
|
(plist-get (jupyter-message-content plist) :data)
|
|
|
|
;; Allow for passing (jupyter-message-content msg)
|
|
|
|
(plist-get plist :data)
|
|
|
|
;; Otherwise assume the plist contains mimetypes
|
|
|
|
plist)
|
|
|
|
(or (plist-get (jupyter-message-content plist) :metadata)
|
|
|
|
(plist-get plist :metadata)
|
|
|
|
metadata)))
|
|
|
|
|
2019-06-27 16:53:00 -05:00
|
|
|
(defun jupyter-line-count-greater-p (str n)
|
|
|
|
"Return non-nil if STR has more than N lines."
|
|
|
|
(string-match-p
|
|
|
|
(format "^\\(?:[^\n]*\n\\)\\{%d,\\}" (1+ n))
|
|
|
|
str))
|
|
|
|
|
2019-04-08 11:34:00 -05:00
|
|
|
;;; Simple weak references
|
|
|
|
;; Thanks to Chris Wellon https://nullprogram.com/blog/2014/01/27/
|
|
|
|
|
|
|
|
(defun jupyter-weak-ref (object)
|
|
|
|
"Return a weak reference for OBJECT."
|
|
|
|
(let ((ref (make-hash-table :weakness 'value :size 1)))
|
|
|
|
(prog1 ref
|
|
|
|
(puthash t object ref))))
|
|
|
|
|
|
|
|
(defsubst jupyter-weak-ref-resolve (ref)
|
|
|
|
"Resolve a weak REF.
|
|
|
|
Return nil if the underlying object has been garbage collected,
|
|
|
|
otherwise return the underlying object."
|
|
|
|
(gethash t ref))
|
|
|
|
|
2019-04-08 11:35:00 -05:00
|
|
|
;;; Errors
|
|
|
|
|
2019-05-23 08:48:15 -05:00
|
|
|
(defun jupyter-error-if-not-client-class-p (class &optional check-class)
|
|
|
|
"Signal a wrong-type-argument error if CLASS is not a client class.
|
|
|
|
If CHECK-CLASS is provided check CLASS against it. CHECK-CLASS
|
|
|
|
defaults to `jupyter-kernel-client'."
|
|
|
|
(or check-class (setq check-class 'jupyter-kernel-client))
|
|
|
|
(cl-assert (class-p check-class))
|
|
|
|
(unless (child-of-class-p class check-class)
|
2019-04-08 11:35:00 -05:00
|
|
|
(signal 'wrong-type-argument
|
2019-05-23 08:48:15 -05:00
|
|
|
(list (list 'subclass check-class) class))))
|
2019-04-08 11:35:00 -05:00
|
|
|
|
2018-01-04 23:03:18 -06:00
|
|
|
(provide 'jupyter-base)
|
2018-01-08 21:38:32 -06:00
|
|
|
|
|
|
|
;;; jupyter-base.el ends here
|