Update callback interface

- Rename `jupyter-received-message-types` to `jupyter-message-types` and add
  message types for requests as well. Also change the keys to keywords instead
  of symbols. Using plists with keywords is in line with `jupyter-messages` and
  the arguments of the jupyter request functions.

- Rename `jupyter-request-run-callbacks` to `jupyter--run-callbacks`. This is
  more of an internal function so mark it as such.

- Change the order of the first two arguments in `jupyter-add-callback` and
  `jupyter-wait-until`. In both cases you are adding a callback to a request or
  waiting for some condition to be satisfied on the request not on the message
  type. This is also the reason why `jupyter-wait-until-received` keeps the
  message type as the first argument. We are waiting until a message of a
  certain type is received for a request, but the more important object in this
  case is the message type.

- Update other files to take into account these changes.
This commit is contained in:
Nathaniel Nicandro 2018-01-06 15:31:39 -06:00
parent d1f57831fe
commit 4944a5d75a
4 changed files with 141 additions and 140 deletions

View file

@ -15,30 +15,37 @@
:control zmq-DEALER)
"The socket types for the various channels used by `jupyter'.")
(defconst jupyter-received-message-types
(list 'execute-result "execute_result"
'execute-reply "execute_reply"
'inspect-reply "inspect_reply"
'complete-reply "complete_reply"
'history-reply "history_reply"
'is-complete-reply "is_complete_reply"
'comm-info-reply "comm_info_reply"
'kernel-info-reply "kernel_info_reply"
'shutdown-reply "shutdown_reply"
'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"
'input-reply "input_reply")
"A plist mapping symbols to received message types.
This is used to give some protection against invalid message
types in `jupyter-add-callback'. If the MSG-TYPE argument of
`jupyter-add-callback' does not match one of the keys in this
plist, an error is thrown.")
(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"
: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"
:input-reply "input_reply")
"A plist mapping keywords to Jupyter message type strings.
The plist values are the message types either sent or received
from the kernel.")
;; https://tools.ietf.org/html/rfc4868
(defun sha256 (object)

View file

@ -357,8 +357,9 @@ for the heartbeat channel."
;;; Message callbacks
(defun jupyter-request-run-callbacks (req msg)
"Run the MSG callbacks of REQ."
(defun jupyter--run-callbacks (req msg)
"Run REQ's MSG callbacks.
See `jupyter-add-callback'."
(when req
(let* ((callbacks (jupyter-request-callbacks req))
(cb-all-types (cdr (assoc t callbacks)))
@ -366,80 +367,73 @@ for the heartbeat channel."
(and cb-all-types (funcall cb-all-types msg))
(and cb-for-type (funcall cb-for-type msg)))))
(defun jupyter-add-callback (msg-type req function)
"Add a callback FUNCTION to a kernel REQuest.
(defun jupyter--add-callback (req msg-type cb)
"Add REQ MSG-TYPE callback, CB."
(setq msg-type (or (plist-get jupyter-message-types msg-type)
;; A msg-type of t means that FUNCTION is run for all
;; messages associated with a request.
(eq msg-type t)))
(unless msg-type
(error "Not a valid message type (`%s')" msg-type))
(let ((callbacks (jupyter-request-callbacks req)))
(if (null callbacks)
(setf (jupyter-request-callbacks req)
(list (cons msg-type cb)))
(let ((cb-for-type (assoc msg-type callbacks)))
(if (not cb-for-type)
(nconc callbacks (list (cons msg-type cb)))
(setq cb (apply-partially
(lambda (cb1 cb2 msg)
(funcall cb1 msg)
(funcall cb2 msg))
(cdr cb-for-type)
cb))
(setcdr cb-for-type cb))))))
MSG-TYPE is a symbol representing which message type to run
FUNCTION on when messages of that type are received for REQ. The
symbol should be the same as one of the message types which are
expected to be received from a kernel where underscore characters
are replaced with hyphens, see
http://jupyter-client.readthedocs.io/en/latest/messaging.html.
(defun jupyter-add-callback (req msg-type cb &rest callbacks)
"Add a callback to run when a message is received for a request.
REQ is a `jupyter-request' returned by one of the request methods
of a `jupyter-kernel-client'. MSG-TYPE is a keyword corresponding
to one of the keys in `jupyter-message-types'. MSG-TYPE can also
be a list, in which case run CB for every MSG-TYPE in the list.
If MSG-TYPE is t, then run CB for every message received for REQ.
CB is the callback function which will run with a single
argument, a message whose `jupyter-message-parent-id' is the same
as the `jupyter-request-id' of REQ and whose
`jupyter-message-type' corresponds to the value of MSG-TYPE in
the `jupyter-message-types' plist. Any additional arguments to
`jupyter-add-callback' are interpreted as additional CALLBACKS to
add to REQ. So to add multiple callbacks to a request you would
do
So to add a callback which fires for an \"execute_reply\",
MSG-TYPE should be the symbol `execute-reply'. As a special case
if MSG-TYPE is t, FUNCTION is run for all received messages of
REQ. If MSG-TYPE is a list, then it should be a list of symbols
as described above. FUNCTION will then run for every type of
message in the list.
(jupyter-add-callback
(jupyter-execute-request client :code \"1 + 2\")
:status (lambda (msg) ...)
:execute-reply (lambda (msg) ...)
:execute-result (lambda (msg) ...))"
(declare (indent 1))
(if (jupyter-request-idle-received-p req)
(error "Request already received idle message")
(setq callbacks (append (list msg-type cb) callbacks))
(cl-loop for (msg-type cb) on callbacks by 'cddr
if (listp msg-type)
do (mapc (lambda (mt) (jupyter--add-callback req mt cb)) msg-type)
else do (jupyter--add-callback req msg-type cb))))
REQ is a `jupyter-request' object as returned by the kernel
request methods of a `jupyter-kernel-client'.
FUNCTION is the callback function to be run when a message with
MSG-TYPE is received for REQ. It should accept one argument which
will be the message that has type, MSG-TYPE, and is a message
associated with the request, REQ. Note that if multiple callbacks
are added for the same MSG-TYPE, they will be called in the order
in which they were added.
As an example, suppose you want to register a callback when you
recieve an `execute-reply' after sending an execute request. This
can be done like so:
(jupyter-add-callback 'execute-reply
(jupyter-request-execute client :code \"y = 1 + 2\")
(lambda (msg) ...))"
(declare (indent 2))
(if (listp msg-type)
(mapc (lambda (mt) (jupyter-add-callback mt req function)) msg-type)
(setq msg-type (or (plist-get jupyter--received-message-types msg-type)
;; A msg-type of t means that FUNCTION is run for all
;; messages associated with a request.
(eq msg-type t)))
(unless msg-type
(error "Not a valid received message type (`%s')" msg-type))
(if (jupyter-request-idle-received-p req)
(error "Request already received idle message.")
(let ((callbacks (jupyter-request-callbacks req)))
(if (null callbacks)
(setf (jupyter-request-callbacks req)
(list (cons msg-type function)))
(let ((cb-for-type (assoc msg-type callbacks)))
(if cb-for-type
(setcdr cb-for-type (apply-partially
(lambda (cb1 cb2 msg)
(funcall cb1 msg)
(funcall cb2 msg))
(cdr cb-for-type)
function))
(nconc callbacks
(list (cons msg-type function))))))))))
(defun jupyter-wait-until (msg-type req timeout fun)
(defun jupyter-wait-until (req msg-type fun &optional timeout)
"Wait until FUN returns non-nil for a received message.
FUN is run on every received message for request, REQ, that has
type, MSG-TYPE. If FUN does not return a non-nil value before
TIMEOUT, return nil. Otherwise return the message which caused
FUN to return a non-nil value. Note that if TIMEOUT is nil, it
defaults to `jupyter-default-timeout'."
(declare (indent 3))
(declare (indent 1))
(setq timeout (or timeout jupyter-default-timeout))
(cl-check-type timeout number)
(lexical-let ((msg nil)
(fun fun))
(jupyter-add-callback msg-type req
(lambda (m) (setq msg (when (funcall fun m) m))))
(jupyter-add-callback req
msg-type (lambda (m) (setq msg (when (funcall fun m) m))))
(with-timeout (timeout nil)
(while (null msg)
(sleep-for 0.01))
@ -448,7 +442,7 @@ defaults to `jupyter-default-timeout'."
(defun jupyter-wait-until-idle (req &optional timeout)
"Wait until TIMEOUT for REQ to receive an idle message.
If TIMEOUT is non-nil, it defaults to `jupyter-default-timeout'."
(jupyter-wait-until 'status req timeout #'jupyter-message-status-idle-p))
(jupyter-wait-until req :status #'jupyter-message-status-idle-p timeout))
(defun jupyter-wait-until-received (msg-type req &optional timeout)
"Wait for a message with MSG-TYPE to be received by CLIENT.
@ -474,7 +468,7 @@ sending one. For example you would not be expecting an
more info
http://jupyter-client.readthedocs.io/en/latest/messaging.html"
(declare (indent 1))
(jupyter-wait-until msg-type req timeout #'identity))
(jupyter-wait-until req msg-type #'identity timeout))
(cl-defmethod jupyter-handle-message ((client jupyter-kernel-client) channel)
"Process a message on CLIENT's CHANNEL.
@ -510,7 +504,7 @@ are taken:
(when (eq (oref channel type) :iopub)
(jupyter-handle-message channel client nil msg))
(unwind-protect
(jupyter-request-run-callbacks req msg)
(jupyter--run-callbacks req msg)
(unwind-protect
(when (jupyter-request-run-handlers-p req)
(jupyter-handle-message channel client req msg))

View file

@ -644,7 +644,7 @@ The first character of the cell code corresponds to position 1."
;; TODO: Not all kernels will respond to an is_complete_request. The
;; jupyter console will switch to its own internal handler when the
;; request times out.
(let ((res (jupyter-wait-until-received 'is-complete-reply
(let ((res (jupyter-wait-until-received :is-complete-reply
(jupyter-is-complete-request
jupyter-repl-current-client
:code (jupyter-repl-cell-code)))))
@ -717,7 +717,7 @@ The first character of the cell code corresponds to position 1."
(lambda (cb)
(let ((client jupyter-repl-current-client))
(with-jupyter-repl-buffer client
(jupyter-add-callback 'complete-reply
(jupyter-add-callback
(jupyter-complete-request
client
:code (jupyter-repl-cell-code)
@ -729,21 +729,22 @@ The first character of the cell code corresponds to position 1."
;; the cursor is at, but the cell code will only be 3
;; characters long.
:pos (1- (jupyter-repl-cell-code-position)))
:complete-reply
(apply-partially
(lambda (cb msg)
(cl-destructuring-bind (&key status matches &allow-other-keys)
(cl-destructuring-bind (&key status matches
&allow-other-keys)
(jupyter-message-content msg)
(if (equal status "ok") (funcall cb matches)
(funcall cb '()))))
cb))))))))
(sorted t)
(doc-buffer
(let* ((client jupyter-repl-current-client)
(msg (jupyter-wait-until-received 'inspect-reply
(let ((msg (jupyter-wait-until-received :inspect-reply
(jupyter-request-inhibit-handlers
(jupyter-inspect-request
client
:code arg :pos (length arg))))
(doc nil))
jupyter-repl-current-client
:code arg :pos (length arg))))))
(when msg
(cl-destructuring-bind (&key status found data &allow-other-keys)
(jupyter-message-content msg)
@ -797,15 +798,14 @@ it."
(let ((req (jupyter-execute-request jupyter-repl-current-client
:code "" :silent t)))
(jupyter-request-inhibit-handlers req)
(jupyter-add-callback 'execute-reply req
(apply-partially
(lambda (client msg)
(message "foo")
(oset client execution-count
(1+ (jupyter-message-get msg :execution_count)))
(with-jupyter-repl-buffer client
(jupyter-repl-insert-prompt 'in)))
jupyter-repl-current-client))
(jupyter-add-callback req
:execute-reply (apply-partially
(lambda (client msg)
(oset client execution-count
(1+ (jupyter-message-get msg :execution_count)))
(with-jupyter-repl-buffer client
(jupyter-repl-insert-prompt 'in)))
jupyter-repl-current-client))
(jupyter-wait-until-idle req)))
(defun run-jupyter-repl (kernel-name)

View file

@ -115,36 +115,36 @@ testing the callback functionality of a
(should (jupyter-wait-until-idle req))
(should (jupyter-request-idle-received-p req))
;; Can't add callbacks after an idle message has been received
(should-error (jupyter-add-callback 'status req #'identity))))
(should-error (jupyter-add-callback req :status #'identity))))
(ert-info ("Callback runs for the right message")
(lexical-let ((ran-callbacks nil)
(req1 (jupyter-execute-request client :code "foo"))
(req2 (jupyter-kernel-info-request client)))
;; callback for all message types received from a request
(jupyter-add-callback t req1
(lambda (msg)
(push 1 ran-callbacks)
(should (member (jupyter-message-type msg)
'("execute_reply" "status")))
(should (equal (jupyter-message-parent-id msg)
(jupyter-request-id req1)))))
(jupyter-add-callback t req2
(lambda (msg)
(push 2 ran-callbacks)
(should (member (jupyter-message-type msg)
'("kernel_info_reply" "status")))
(should (equal (jupyter-message-parent-id msg)
(jupyter-request-id req2)))))
(jupyter-add-callback req1
t (lambda (msg)
(push 1 ran-callbacks)
(should (member (jupyter-message-type msg)
'("execute_reply" "status")))
(should (equal (jupyter-message-parent-id msg)
(jupyter-request-id req1)))))
(jupyter-add-callback req2
t (lambda (msg)
(push 2 ran-callbacks)
(should (member (jupyter-message-type msg)
'("kernel_info_reply" "status")))
(should (equal (jupyter-message-parent-id msg)
(jupyter-request-id req2)))))
(should (jupyter-wait-until-idle req2))
(setq ran-callbacks (nreverse ran-callbacks))
(should (equal ran-callbacks '(1 1 1 2 2 2)))))
(ert-info ("Multiple callbacks for a single message type")
(lexical-let* ((ran-callbacks nil)
(req (jupyter-execute-request client :code "foo")))
(jupyter-add-callback 'execute-reply req
(lambda (msg) (push 1 ran-callbacks)))
(jupyter-add-callback 'execute-reply req
(lambda (msg) (push 2 ran-callbacks)))
(jupyter-add-callback req
:execute-reply (lambda (msg) (push 1 ran-callbacks)))
(jupyter-add-callback req
:execute-reply (lambda (msg) (push 2 ran-callbacks)))
(jupyter-wait-until-idle req)
(setq ran-callbacks (nreverse ran-callbacks))
(should (equal ran-callbacks '(1 2))))))))
@ -301,19 +301,19 @@ testing the callback functionality of a
(unwind-protect
(progn
(ert-info ("Kernel info")
(let ((res (jupyter-wait-until-received 'kernel-info-reply
(let ((res (jupyter-wait-until-received :kernel-info-reply
(jupyter-kernel-info-request client))))
(should-not (null res))
(should (json-plist-p res))
(should (equal (jupyter-message-type res) "kernel_info_reply"))))
(ert-info ("Comm info")
(let ((res (jupyter-wait-until-received 'comm-info-reply
(let ((res (jupyter-wait-until-received :comm-info-reply
(jupyter-comm-info-request client))))
(should-not (null res))
(should (json-plist-p res))
(should (equal (jupyter-message-type res) "comm_info_reply"))))
(ert-info ("Execute")
(let ((res (jupyter-wait-until-received 'execute-reply
(let ((res (jupyter-wait-until-received :execute-reply
(jupyter-execute-request client :code "y = 1 + 2"))))
(should-not (null res))
(should (json-plist-p res))
@ -321,7 +321,7 @@ testing the callback functionality of a
(ert-info ("Input")
(cl-letf (((symbol-function 'read-from-minibuffer)
(lambda (prompt &rest args) "foo")))
(let ((res (jupyter-wait-until-received 'execute-result
(let ((res (jupyter-wait-until-received :execute-result
(jupyter-execute-request client :code "input('')"))))
(should-not (null res))
(should (json-plist-p res))
@ -330,7 +330,7 @@ testing the callback functionality of a
(plist-get res :content)
(should (equal (plist-get data :text/plain) "'foo'"))))))
(ert-info ("Inspect")
(let ((res (jupyter-wait-until-received 'inspect-reply
(let ((res (jupyter-wait-until-received :inspect-reply
(jupyter-inspect-request
client
:code "list((1, 2, 3))"
@ -340,7 +340,7 @@ testing the callback functionality of a
(should (json-plist-p res))
(should (equal (jupyter-message-type res) "inspect_reply"))))
(ert-info ("Complete")
(let ((res (jupyter-wait-until-received 'complete-reply
(let ((res (jupyter-wait-until-received :complete-reply
(jupyter-complete-request
client
:code "foo = lis"
@ -349,14 +349,14 @@ testing the callback functionality of a
(should (json-plist-p res))
(should (equal (jupyter-message-type res) "complete_reply"))))
(ert-info ("History")
(let ((res (jupyter-wait-until-received 'history-reply
(let ((res (jupyter-wait-until-received :history-reply
(jupyter-history-request
client :hist-access-type "tail" :n 2))))
(should-not (null res))
(should (json-plist-p res))
(should (equal (jupyter-message-type res) "history_reply"))))
(ert-info ("Is Complete")
(let ((res (jupyter-wait-until-received 'is-complete-reply
(let ((res (jupyter-wait-until-received :is-complete-reply
(jupyter-is-complete-request
client :code "for i in range(5):"))))
(should-not (null res))
@ -365,14 +365,14 @@ testing the callback functionality of a
(ert-info ("Interrupt")
(lexical-let ((time (current-time))
(interrupt-time nil))
(jupyter-add-callback 'status
(jupyter-add-callback
(jupyter-execute-request
client :code "import time\ntime.sleep(2)")
(lambda (msg)
(when (jupyter-message-status-idle-p msg)
(setq interrupt-time (current-time)))))
:status (lambda (msg)
(when (jupyter-message-status-idle-p msg)
(setq interrupt-time (current-time)))))
(sleep-for 0.2)
(let ((res (jupyter-wait-until-received 'interrupt-reply
(let ((res (jupyter-wait-until-received :interrupt-reply
(jupyter-interrupt-request client))))
(should-not (null res))
(should (json-plist-p res))
@ -381,7 +381,7 @@ testing the callback functionality of a
(should (< (float-time (time-subtract interrupt-time time))
2)))))
(ert-info ("Shutdown")
(let ((res (jupyter-wait-until-received 'shutdown-reply
(let ((res (jupyter-wait-until-received :shutdown-reply
(jupyter-shutdown-request client))))
(should-not (null res))
(should (json-plist-p res))