forked from emacs-jupyter/jupyter
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjupyter-client.el
2292 lines (2018 loc) · 92.3 KB
/
jupyter-client.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; jupyter-client.el --- A Jupyter kernel client -*- lexical-binding: t -*-
;; Copyright (C) 2018 Nathaniel Nicandro
;; Author: Nathaniel Nicandro <[email protected]>
;; Created: 06 Jan 2018
;; Version: 0.8.1
;; This program 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, or (at
;; 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:
;; The default implementation of a Jupyter kernel client.
;;; Code:
(defgroup jupyter-client nil
"A Jupyter client."
:group 'jupyter)
(eval-when-compile (require 'subr-x))
(require 'jupyter-base)
(require 'jupyter-comm-layer)
(require 'jupyter-mime)
(require 'jupyter-messages)
(defface jupyter-eval-overlay
'((((class color) (min-colors 88) (background light))
:foreground "navy"
:weight bold)
(((class color) (min-colors 88) (background dark))
:foreground "dodger blue"
:weight bold))
"Face used for the input prompt."
:group 'jupyter-client)
(defcustom jupyter-eval-use-overlays nil
"Display evaluation results as overlays in the `current-buffer'.
If this variable is non-nil, evaluation results are displayed as
overlays at the end of the line if possible."
:group 'jupyter-client
:type 'boolean)
(defcustom jupyter-eval-overlay-prefix "=> "
"Evaluation result overlays will be prefixed with this string."
:group 'jupyter-client
:type 'string)
(defcustom jupyter-eval-short-result-display-function
(lambda (result) (message "%s" result))
"Function for displaying short evaluation results.
Evaluation results are considered short when they are less than
`jupyter-eval-short-result-max-lines' long.
The default function is `message', but any function that takes a
single string argument can be used. For example, to display the
result in a tooltip, the variable can be set to `popup-tip' from
the `popup' package."
:group 'jupyter-client
:type 'function)
(defcustom jupyter-eval-short-result-max-lines 10
"Maximum number of lines for short evaluation results.
Short evaluation results are displayed using
`jupyter-eval-short-result-display-function'. Longer results are
forwarded to a separate buffer."
:group 'jupyter-client
:type 'integer)
(defcustom jupyter-include-other-output nil
"Whether or not to handle IOPub messages from other clients.
A Jupyter client can receive messages from other clients
connected to the same kernel on the IOPub channel. You can choose
to ignore these messages by setting
`jupyter-include-other-output' to nil. If
`jupyter-include-other-output' is non-nil, then any messages that
are not associated with a request from a client are sent to the
client's handler methods with a nil value for the request
argument. To change the value of this variable for a particular
client use `jupyter-set'."
:group 'jupyter
:type 'boolean)
(defcustom jupyter-iopub-message-hook nil
"Hook run when a message is received on the IOPub channel.
The hook is called with two arguments, the Jupyter client and the
message it received.
Do not add to this hook variable directly, use
`jupyter-add-hook'. If any of the message hooks return a non-nil
value, the client handlers will be prevented from running for the
message."
:group 'jupyter
:type 'hook)
(put 'jupyter-iopub-message-hook 'permanent-local t)
(defcustom jupyter-shell-message-hook nil
"Hook run when a message is received on the SHELL channel.
The hook is called with two arguments, the Jupyter client and the
message it received.
Do not add to this hook variable directly, use
`jupyter-add-hook'. If any of the message hooks return a non-nil
value, the client handlers will be prevented from running for the
message."
:group 'jupyter
:type 'hook)
(put 'jupyter-shell-message-hook 'permanent-local t)
(defcustom jupyter-stdin-message-hook nil
"Hook run when a message is received on the STDIN channel.
The hook is called with two arguments, the Jupyter client and the
message it received.
Do not add to this hook variable directly,
use `jupyter-add-hook'. If any of the message hooks return a
non-nil value, the client handlers will be prevented from running
for the message."
:group 'jupyter
:type 'hook)
(put 'jupyter-stdin-message-hook 'permanent-local t)
(declare-function company-begin-backend "ext:company" (backend &optional callback))
(declare-function company-doc-buffer "ext:company" (&optional string))
(declare-function company-idle-begin "ext:company")
(declare-function yas-minor-mode "ext:yasnippet" (&optional arg))
(declare-function yas-expand-snippet "ext:yasnippet" (content &optional start end expand-env))
(declare-function jupyter-insert "jupyter-mime")
;; This is mainly used by the REPL code, but is also set by
;; the `org-mode' client whenever `point' is inside a code
;; block.
(defvar jupyter-current-client nil
"The kernel client for the `current-buffer'.
This is also let bound whenever a message is handled by a
kernel.")
(put 'jupyter-current-client 'permanent-local t)
(make-variable-buffer-local 'jupyter-current-client)
(defvar jupyter-inhibit-handlers nil
"Whether or not new requests inhibit client handlers.
If set to t, prevent new requests from running any of the client
handler methods. If set to a list of `jupyter-message-types',
prevent handler methods from running only for those message
types.
For example to prevent a client from calling its :execute-reply
handler:
(let ((jupyter-inhibit-handlers '(:execute-reply)))
(jupyter-send-execute-request client ...))
In addition, if the first element of the list is the symbol
`not', then inhibit handlers not in the list.
Do not set this variable directly, let bind it around specific
requests like the above example.")
(defvar jupyter--clients nil)
;; Define channel classes for method dispatching based on the channel type
(defclass jupyter-kernel-client (jupyter-finalized-object
jupyter-instance-tracker)
((tracking-symbol :initform 'jupyter--clients)
(execution-state
:type string
:initform "idle"
:documentation "The current state of the kernel. Can be
either \"idle\", \"busy\", or \"starting\".")
(execution-count
:type integer
:initform 1
:documentation "The *next* execution count of the kernel.
I.e., the execution count that will be assigned to the
next :execute-request sent to the kernel.")
(requests
:type hash-table
:initform (make-hash-table :test 'equal)
:documentation "A hash table with message ID's as keys.
This is used to register callback functions to run once a reply
from a previously sent request is received. See
`jupyter-add-callback'. Note that this is also used to filter
received messages that originated from a previous request by this
client. Whenever the client sends a message in which a reply is
expected, it sets an entry in this table to represent the fact
that the message has been sent. So if there is a non-nil value
for a message ID it means that a message has been sent and the
client is expecting a reply from the kernel.")
(kernel-info
:type json-plist
:initform nil
:documentation "The saved kernel info created when first
initializing this client. When `jupyter-start-channels' is
called, this will be set to the kernel info plist returned
from an initial `:kernel-info-request'.")
(kcomm
:type jupyter-comm-layer
:documentation "The process which receives events from channels.")
(session
:type jupyter-session
:documentation "The session for this client.")
(comms
:type hash-table
:initform (make-hash-table :test 'equal)
:documentation "A hash table with comm ID's as keys.
Contains all of the open comms. Each value is a cons cell (REQ .
DATA) which contains the generating `jupyter-request' that caused
the comm to open and the initial DATA passed to the comm for
initialization.")
(manager
:initform nil
:documentation "If this client was initialized using a
`jupyter-kernel-manager' this slot will hold the manager which
initialized the client.")
(-buffer
:type buffer
:documentation "An internal buffer used to store client local
variables.")))
;;; `jupyter-current-client' language method specializer
(defvar jupyter--generic-lang-used (make-hash-table :test #'eq))
(cl-generic-define-generalizer jupyter--generic-lang-generalizer
50 (lambda (name &rest _)
`(when (and ,name (object-of-class-p ,name 'jupyter-kernel-client))
(gethash (jupyter-kernel-language ,name) jupyter--generic-lang-used)))
(lambda (tag &rest _)
(and (eq (car-safe tag) 'jupyter-lang)
(list tag))))
(cl-generic-define-context-rewriter jupyter-lang (lang)
`(jupyter-current-client (jupyter-lang ,lang)))
(cl-defmethod cl-generic-generalizers ((specializer (head jupyter-lang)))
"Support for (jupyter-lang LANG) specializers.
Matches if the kernel language of the `jupyter-kernel-client'
passed as the argument has a language of LANG."
(puthash (cadr specializer) specializer jupyter--generic-lang-used)
(list jupyter--generic-lang-generalizer))
;;; Initializing a `jupyter-kernel-client'
(defun jupyter-client-has-manager-p (&optional client)
"Return non-nil if CLIENT's kernel has a kernel manager.
CLIENT defaults to `jupyter-current-client'."
(or client (setq client jupyter-current-client))
(when client
(cl-check-type client jupyter-kernel-client)
(and (oref client manager) t)))
(cl-defmethod initialize-instance ((client jupyter-kernel-client) &optional _slots)
(cl-call-next-method)
(let ((buffer (generate-new-buffer " *jupyter-kernel-client*")))
(oset client -buffer buffer)
(jupyter-add-finalizer client
(lambda ()
(when (buffer-live-p buffer)
(kill-buffer buffer))
(jupyter-stop-channels client)))))
(cl-defmethod jupyter-kernel-alive-p ((client jupyter-kernel-client))
"Return non-nil if the kernel CLIENT is connected to is alive."
(or (and (jupyter-client-has-manager-p client)
(jupyter-kernel-alive-p (oref client manager)))
(jupyter-hb-beating-p client)))
(defun jupyter-clients ()
"Return a list of all `jupyter-kernel-client' objects."
(jupyter-all-objects 'jupyter--clients))
(defun jupyter-find-client-for-session (session-id)
"Return the kernel client whose session has SESSION-ID."
(or (cl-find-if
(lambda (x) (string= (jupyter-session-id (oref x session)) session-id))
(jupyter-clients))
(error "No client found for session (%s)" session-id)))
(defun jupyter--connection-info (info-or-session)
"Return the connection plist according to INFO-OR-SESSION.
See `jupyter-initialize-connection'."
(cond
((jupyter-session-p info-or-session)
(jupyter-session-conn-info info-or-session))
((json-plist-p info-or-session) info-or-session)
((stringp info-or-session)
(if (file-remote-p info-or-session)
;; TODO: Don't tunnel if a tunnel already exists
(jupyter-tunnel-connection info-or-session)
(unless (file-exists-p info-or-session)
(error "File does not exist (%s)" info-or-session))
(jupyter-read-plist info-or-session)))
(t (signal 'wrong-type-argument
(list info-or-session
'(or jupyter-session-p json-plist-p stringp))))))
;; FIXME: This requires that CLIENT is communicating with a kernel using a
;; `jupyter-channel-ioloop-comm' object.
(cl-defmethod jupyter-initialize-connection ((client jupyter-kernel-client) info-or-session)
"Initialize CLIENT with connection INFO-OR-SESSION.
INFO-OR-SESSION can be a file name, a plist, or a
`jupyter-session' object that will be used to initialize CLIENT's
connection.
When INFO-OR-SESSION is a file name, read the contents of the
file as a JSON plist and create a new `jupyter-session' from it.
For remote files, create a new `jupyter-session' based on the
plist returned from `jupyter-tunnel-connection'.
When INFO-OR-SESSION is a plist, use it to create a new
`jupyter-session'.
Finally, when INFO-OR-SESSION is a `jupyter-session' it is used
as the session for CLIENT.
The session object will then be used to initialize the client
connection and will be accessible as the session slot of CLIENT.
The necessary keys and values to initialize a connection can be
found at
http://jupyter-client.readthedocs.io/en/latest/kernels.html#connection-files."
(let ((session (and (jupyter-session-p info-or-session) info-or-session))
(conn-info (jupyter--connection-info info-or-session)))
(cl-destructuring-bind (&key key signature_scheme &allow-other-keys)
conn-info
(when (and (> (length key) 0)
(not (functionp
(intern (concat "jupyter-" signature_scheme)))))
(error "Unsupported signature scheme: %s" signature_scheme)))
(oset client session
(or (copy-sequence session)
(jupyter-session
:key (plist-get conn-info :key)
:conn-info conn-info)))
(jupyter-initialize-connection
(oref client kcomm)
(oref client session))))
;;; Client local variables
(defmacro jupyter-with-client-buffer (client &rest body)
"Run a form inside CLIENT's IOloop subprocess buffer.
BODY is run with the current buffer set to CLIENT's IOloop
subprocess buffer."
(declare (indent 1))
`(progn
(cl-check-type ,client jupyter-kernel-client)
(with-current-buffer (oref ,client -buffer)
,@body)))
(defun jupyter-set (client symbol newval)
"Set CLIENT's local value for SYMBOL to NEWVAL."
(jupyter-with-client-buffer client
(set (make-local-variable symbol) newval)))
(defun jupyter-get (client symbol)
"Get CLIENT's local value of SYMBOL.
Return nil if SYMBOL is not bound for CLIENT."
(condition-case nil
(buffer-local-value symbol (oref client -buffer))
(void-variable nil)))
(gv-define-simple-setter jupyter-get jupyter-set)
;;; Hooks
(defun jupyter-add-hook (client hook function &optional append)
"Add to the CLIENT value of HOOK the function FUNCTION.
APPEND has the same meaning as in `add-hook' and FUNCTION is
added to HOOK using `add-hook', but local only to CLIENT. Note
that the CLIENT should have its channels already started before
this is called."
(declare (indent 2))
(jupyter-with-client-buffer client
(add-hook hook function append t)))
(defun jupyter-run-hook-with-args-until-success (client hook &rest args)
"Run CLIENT's value for HOOK with the arguments ARGS.
CLIENT is passed as the first argument and then ARGS."
(jupyter-with-client-buffer client
(when jupyter--debug
(message "RUN-HOOK: %s" hook))
(with-demoted-errors "Error in Jupyter message hook: %S"
(apply #'run-hook-with-args-until-success hook client args))))
(defun jupyter-remove-hook (client hook function)
"Remove from CLIENT's value of HOOK the function FUNCTION."
(jupyter-with-client-buffer client
(remove-hook hook function t)))
;;; Sending messages
(cl-defgeneric jupyter-generate-request ((_client jupyter-kernel-client) _msg)
"Generate a `jupyter-request' object for MSG.
This method gives an opportunity for subclasses to initialize a
`jupyter-request' based on the current context.
The default implementation returns a new `jupyter-request' with
the default value for all slots. Note, the `:id' and
`:inhibited-handlers' slots are overwritten by the caller of this
method."
(jupyter-request))
(defun jupyter-verify-inhibited-handlers ()
"Verify the value of `jupyter-inhibit-handlers'.
If it does not contain a valid value, raise an error."
(or (eq jupyter-inhibit-handlers t)
(cl-loop
for msg-type in (if (eq (car jupyter-inhibit-handlers) 'not)
(cdr jupyter-inhibit-handlers)
jupyter-inhibit-handlers)
unless (plist-member jupyter-message-types msg-type)
do (error "Not a valid message type (`%s')" msg-type))))
(cl-defmethod jupyter-send ((client jupyter-kernel-client)
channel
type
message
&optional msg-id)
"Send a message on CLIENT's CHANNEL.
Return a `jupyter-request' representing the sent message. CHANNEL
is one of the channel keywords, either (:stdin or :shell).
TYPE is one of the `jupyter-message-types'. MESSAGE is the
message sent on CHANNEL.
Note that you can manipulate how to handle messages received in
response to the sent message, see `jupyter-add-callback' and
`jupyter-request-inhibited-handlers'."
(declare (indent 1))
(jupyter-verify-inhibited-handlers)
(let ((msg-id (or msg-id (jupyter-new-uuid))))
(when jupyter--debug
;; The logging of messages is deferred until the next command loop for
;; security reasons. When sending :input-reply messages that read
;; passwords, clearing the password string using `clear-string' happens
;; *after* the call to `jupyter-send'.
(run-at-time 0 nil (lambda () (message "SENDING: %s %s %s" type msg-id message))))
(jupyter-send (oref client kcomm) 'send channel type message msg-id)
;; Anything sent to stdin is a reply not a request so don't add it as a
;; pending request
(unless (eq channel :stdin)
(let ((req (jupyter-generate-request client message))
(requests (oref client requests)))
(setf (jupyter-request-id req) msg-id)
(setf (jupyter-request-inhibited-handlers req) jupyter-inhibit-handlers)
(puthash msg-id req requests)
(puthash "last-sent" req requests)))))
;;; Pending requests
(defun jupyter-requests-pending-p (client)
"Return non-nil if CLIENT has open requests that the kernel has not handled.
Specifically, this returns non-nil if the last request message
sent to the kernel using CLIENT has not received an idle message
back."
(cl-check-type client jupyter-kernel-client)
(jupyter--drop-idle-requests client)
(with-slots (requests) client
;; If there are two requests, then there is really only one since
;; "last-sent" is an alias for the other.
(or (> (hash-table-count requests) 2)
(when-let* ((last-sent (gethash "last-sent" requests)))
(not (jupyter-request-idle-received-p last-sent))))))
(defsubst jupyter-last-sent-request (client)
"Return the most recent `jupyter-request' made by CLIENT."
(gethash "last-sent" (oref client requests)))
(defun jupyter-map-pending-requests (client function)
"Call FUNCTION for all pending requests of CLIENT."
(declare (indent 1))
(cl-check-type client jupyter-kernel-client)
(maphash (lambda (k v)
(unless (or (equal k "last-sent")
(jupyter-request-idle-received-p v))
(funcall function v)))
(oref client requests)))
;;; Event handlers
;;;; Sending/receiving
(defun jupyter--show-event (event)
(let ((event-name (upcase (symbol-name (car event))))
(repr (cl-case (car event)
(sent (format "%s" (cdr event)))
(message (cl-destructuring-bind (_ channel _idents . msg) event
(format "%s" (list
channel
(jupyter-message-type msg)
(jupyter-message-content msg)))))
(t nil))))
(when repr
(message "%s" (concat event-name ": " repr)))))
;; TODO: Get rid of this method
(cl-defmethod jupyter-event-handler ((_client jupyter-kernel-client)
(event (head sent)))
(when jupyter--debug
(jupyter--show-event event)))
(cl-defmethod jupyter-event-handler ((client jupyter-kernel-client)
(event (head message)))
(when jupyter--debug
(jupyter--show-event event))
(cl-destructuring-bind (_ channel _idents . msg) event
(jupyter-handle-message client channel msg)))
;;; Starting communication with a kernel
(cl-defmethod jupyter-start-channels ((client jupyter-kernel-client))
(jupyter-connect-client (oref client kcomm) client))
(cl-defmethod jupyter-stop-channels ((client jupyter-kernel-client))
"Stop any running channels of CLIENT."
(when (slot-boundp client 'kcomm)
(jupyter-disconnect-client (oref client kcomm) client)))
(cl-defmethod jupyter-channels-running-p ((client jupyter-kernel-client))
"Are any channels of CLIENT running?"
(jupyter-comm-alive-p (oref client kcomm)))
(cl-defmethod jupyter-channel-alive-p ((client jupyter-kernel-client) channel)
(jupyter-channel-alive-p (oref client kcomm) channel))
(cl-defmethod jupyter-hb-pause ((client jupyter-kernel-client))
(when (cl-typep (oref client kcomm) 'jupyter-hb-comm)
(jupyter-hb-pause (oref client kcomm))))
(cl-defmethod jupyter-hb-unpause ((client jupyter-kernel-client))
(when (cl-typep (oref client kcomm) 'jupyter-hb-comm)
(jupyter-hb-unpause (oref client kcomm))))
(cl-defmethod jupyter-hb-beating-p ((client jupyter-kernel-client))
"Is CLIENT still connected to its kernel?"
(or (null (cl-typep (oref client kcomm) 'jupyter-hb-comm))
(jupyter-hb-beating-p (oref client kcomm))))
;;; Message callbacks
(defun jupyter--run-callbacks (req msg)
"Run REQ's MSG callbacks.
See `jupyter-add-callback'."
(when-let (callbacks (and req (jupyter-request-callbacks req)))
;; Callback for all message types
(when-let (f (alist-get t callbacks))
(funcall f msg))
(when-let (f (alist-get (jupyter-message-type msg) callbacks))
(funcall f msg))))
(defun jupyter--add-callback (req msg-type cb)
"Helper function for `jupyter-add-callback'.
REQ is a `jupyter-request' object, MSG-TYPE is one of the
keywords corresponding to a received message type in
`jupyter-message-types', and CB is the callback that will be run
when MSG-TYPE is received for REQ."
(unless (or (plist-member 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))
(error "Not a valid message type (`%s')" msg-type))
(add-function
:after (alist-get msg-type (jupyter-request-callbacks req) #'identity)
cb))
(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 kernel client. MSG-TYPE is one of the keys in
`jupyter-message-types'. CB is the callback function to run when
a message with MSG-TYPE is received for REQ.
MSG-TYPE can also be a list, in which case run CB for every
MSG-TYPE in the list. If MSG-TYPE is t, run CB for every message
received for REQ.
Multiple callbacks can be added for the same MSG-TYPE. The
callbacks will be called in the order they were added.
Any additional arguments to `jupyter-add-callback' are
interpreted as additional CALLBACKS to add to REQ. So to add
multiple callbacks you would do
(jupyter-add-callback
(jupyter-send-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")
(while (and msg-type cb)
(cl-check-type cb function "Callback should be a function")
(if (listp msg-type)
(cl-loop for mt in msg-type
do (jupyter--add-callback req mt cb))
(jupyter--add-callback req msg-type cb))
(setq msg-type (pop callbacks)
cb (pop callbacks)))))
;;; Waiting for messages
(defvar jupyter--already-waiting-p nil)
(defun jupyter-wait-until (req msg-type cb &optional timeout progress-msg)
"Wait until conditions for a request are satisfied.
REQ, MSG-TYPE, and CB have the same meaning as in
`jupyter-add-callback'. If CB returns non-nil within TIMEOUT
seconds, return the message that caused CB to return non-nil. If
CB never returns a non-nil value within TIMEOUT, return nil. Note
that if no TIMEOUT is given, `jupyter-default-timeout' is used.
If PROGRESS-MSG is non-nil, it should be a message string to
display for reporting progress to the user while waiting."
(declare (indent 2))
(let (msg)
(jupyter-add-callback req
msg-type (lambda (m) (setq msg (when (funcall cb m) m))))
(let* ((timeout-spec (when jupyter--already-waiting-p
(with-timeout-suspend)))
(jupyter--already-waiting-p t))
(unwind-protect
(jupyter-with-timeout
(progress-msg (or timeout jupyter-default-timeout))
msg)
(when timeout-spec
(with-timeout-unsuspend timeout-spec))))))
(defun jupyter-wait-until-startup (client &optional timeout progress-msg)
"Wait for CLIENT to receive a status: startup message.
Return non-nil if CLIENT receives the message within TIMEOUT,
otherwise nil. TIMEOUT defaults to `jupyter-long-timeout'.
If PROGRESS-MSG is non-nil, it should be a message string to
display for reporting progress to the user while waiting."
(let* ((msg nil)
(check (lambda (_ m)
(when (jupyter-message-status-starting-p m)
(setq msg m)))))
(jupyter-add-hook client 'jupyter-iopub-message-hook check)
(unwind-protect
(jupyter-with-timeout
(progress-msg (or timeout jupyter-long-timeout))
msg)
(jupyter-remove-hook client 'jupyter-iopub-message-hook check))))
(defun jupyter-wait-until-idle (req &optional timeout progress-msg)
"Wait until a status: idle message is received for a request.
REQ has the same meaning as in `jupyter-add-callback'. If an idle
message for REQ is received within TIMEOUT seconds, return the
message. Otherwise return nil if the message was not received
within TIMEOUT. Note that if no TIMEOUT is given, it defaults to
`jupyter-default-timeout'.
If PROGRESS-MSG is non-nil, it is a message string to display for
reporting progress to the user while waiting."
(jupyter-wait-until req :status
#'jupyter-message-status-idle-p timeout progress-msg))
(defun jupyter-wait-until-received (msg-type req &optional timeout progress-msg)
"Wait until a message of a certain type is received for a request.
MSG-TYPE and REQ have the same meaning as their corresponding
arguments in `jupyter-add-callback'. If no message that matches
MSG-TYPE is received for REQ within TIMEOUT seconds, return nil.
Otherwise return the first message that matched MSG-TYPE. Note
that if no TIMEOUT is given, it defaults to
`jupyter-default-timeout'.
If PROGRESS-MSG is non-nil, it is a message string to display for
reporting progress to the user while waiting."
(declare (indent 1))
(jupyter-wait-until req msg-type #'identity timeout progress-msg))
;;; Client handlers
(cl-defgeneric jupyter-drop-request ((_client jupyter-kernel-client) _req)
"Called when CLIENT removes REQ, from its request table."
nil)
(cl-defmethod jupyter-drop-request :before ((_client jupyter-kernel-client) req)
(when jupyter--debug
(message "DROPPING-REQ: %s" (jupyter-request-id req))))
(defun jupyter--drop-idle-requests (client)
"Drop completed requests from CLIENT's request table.
A request is deemed complete when an idle message has been
received for it and it is not the most recently sent request."
(with-slots (requests) client
(cl-loop
with last-sent = (gethash "last-sent" requests)
for req in (hash-table-values requests)
when (and (jupyter-request-idle-received-p req)
(not (eq req last-sent)))
do (unwind-protect
(jupyter-drop-request client req)
(remhash (jupyter-request-id req) requests)))))
(defsubst jupyter--run-handler-p (req msg)
"Return non-nil if REQ doesn't inhibit the handler for MSG."
(let* ((ihandlers (and req (jupyter-request-inhibited-handlers req)))
(type (and (listp ihandlers)
(memq (jupyter-message-type msg) ihandlers))))
(not (or (eq ihandlers t)
(if (eq (car ihandlers) 'not) (not type) type)))))
(defun jupyter--run-handler-maybe (client channel req msg)
"Possibly run CLIENT's CHANNEL handler on REQ's received MSG."
(when (jupyter--run-handler-p req msg)
(jupyter-handle-message channel client req msg)))
(cl-defmethod jupyter-handle-message ((client jupyter-kernel-client) channel msg)
"Process a message on CLIENT's CHANNEL.
When a message is received from the kernel, the
`jupyter-handle-message' method is called on the client. The
client method runs any callbacks for the message and possibly
runs the client handler for the channel the message was received
on. The channel's `jupyter-handle-message' method will then pass
the message to the appropriate message handler based on the
message type.
So when a message is received from the kernel the following steps
are taken:
- `jupyter-handle-message' (client)
- Run callbacks for message
- Possibly run channel handlers
- `jupyter-handle-message' (channel)
- Based on message type, dispatch to
`jupyter-handle-execute-result',
`jupyter-handle-kernel-info-reply', ...
- Remove request from client request table when idle message is received"
(when msg
(let* ((jupyter-current-client client)
(pmsg-id (jupyter-message-parent-id msg))
(requests (oref client requests))
(req (gethash pmsg-id requests)))
;; Update the state of the client
(pcase (jupyter-message-type msg)
(:status
(oset client execution-state
(jupyter-message-get msg :execution_state)))
((or :execute-input (and (guard req) :execute-reply))
(oset client execution-count
(1+ (jupyter-message-get msg :execution_count)))))
(if (not req)
(when (or (jupyter-get client 'jupyter-include-other-output)
;; Always handle a startup message
(jupyter-message-status-starting-p msg))
(jupyter--run-handler-maybe client channel req msg))
(setf (jupyter-request-last-message req) msg)
(unwind-protect
(jupyter--run-callbacks req msg)
(unwind-protect
(jupyter--run-handler-maybe client channel req msg)
(when (or (jupyter-message-status-idle-p msg)
;; Jupyter protocol 5.1, IPython implementation 7.5.0
;; doesn't give status: busy or status: idle messages on
;; kernel-info-requests. Whereas IPython implementation
;; 6.5.0 does. Seen on Appveyor tests.
;;
;; TODO: May be related jupyter/notebook#3705 as the
;; problem does happen after a kernel restart when
;; testing.
(eq (jupyter-message-type msg) :kernel-info-reply)
;; No idle message is received after a shutdown reply so
;; consider REQ as having received an idle message in
;; this case.
(eq (jupyter-message-type msg) :shutdown-reply))
;; Order matters here. We want to remove idle requests *before*
;; setting another request idle to account for idle messages
;; coming in out of order, e.g. before their respective reply
;; messages.
(jupyter--drop-idle-requests client)
(setf (jupyter-request-idle-received-p req) t))))))))
;;; Channel handler macros
(defmacro jupyter-dispatch-message-cases (client req msg cases)
"Dispatch to CLIENT handler's based on REQ and MSG.
CASES defines the handlers to dispatch to based on the
`jupyter-message-type' of MSG and should be a list of lists, the
first element of each inner list being the name of the handler,
excluding the `jupyter-handle-' prefix. The rest of the elements
in the list are the name of the keys that will be extracted from
the `jupyter-message-content' of MSG and passed to the handler in
the same order as they appear. For example,
(jupyter-dispatch-message-cases client req msg
((shutdown-reply restart)
(stream name text)))
will be transformed to
(let ((content (jupyter-message-content msg)))
(pcase (jupyter-message-type msg)
(:shutdown-reply
(cl-destructuring-bind (&key restart &allow-other-keys)
content
(jupyter-handle-shutdown-reply client req restart)))
(:stream
(cl-destructuring-bind (&key name text &allow-other-keys)
content
(jupyter-handle-stream client req name text)))
(_ (warn \"Message type not handled (%s)\"
(jupyter-message-type msg)))))"
(declare (indent 3))
(let ((handlers nil)
(content (make-symbol "contentvar"))
(jclient (make-symbol "clientvar"))
(jreq (make-symbol "reqvar"))
(jmsg (make-symbol "msgvar")))
(dolist (case cases)
(cl-destructuring-bind (msg-type . keys) case
(let ((handler (intern (format "jupyter-handle-%s" msg-type)))
(msg-type (intern (concat ":" (symbol-name msg-type)))))
(push `(,msg-type
(cl-destructuring-bind (&key ,@keys &allow-other-keys)
,content
(,handler ,jclient ,jreq ,@keys)))
handlers))))
`(let* ((,jmsg ,msg)
(,jreq ,req)
(,jclient ,client)
(,content (jupyter-message-content ,jmsg)))
(pcase (jupyter-message-type ,jmsg)
,@handlers
(_ (warn "Message type not handled (%s)"
(jupyter-message-type msg)))))))
;;; STDIN handlers
(cl-defmethod jupyter-handle-message ((_channel (eql :stdin))
client
req
msg)
(unless (jupyter-run-hook-with-args-until-success
client 'jupyter-stdin-message-hook msg)
(jupyter-dispatch-message-cases client req msg
((input-reply prompt password)
(input-request prompt password)))))
(cl-defgeneric jupyter-handle-input-request ((client jupyter-kernel-client)
_req
prompt
password)
"Handle an input request from CLIENT's kernel.
PROMPT is the prompt the kernel would like to show the user. If
PASSWORD is t, then `read-passwd' is used to get input from the
user. Otherwise `read-from-minibuffer' is used."
(declare (indent 1))
(let* ((value (condition-case nil
;; Disallow any `with-timeout's from timing out. This
;; prevents any calls to `jupyter-wait-until-received' from
;; timing out when reading input. See #35.
(let ((timeout-spec (with-timeout-suspend)))
(unwind-protect
(if (eq password t) (read-passwd prompt)
(read-from-minibuffer prompt))
(with-timeout-unsuspend timeout-spec)))
(quit "")))
(msg (jupyter-message-input-reply :value value)))
(unwind-protect
(jupyter-send client :stdin :input-reply msg)
(when (eq password t)
(clear-string value)))
value))
(defalias 'jupyter-handle-input-reply 'jupyter-handle-input-request)
;;; SHELL handlers
;; http://jupyter-client.readthedocs.io/en/latest/messaging.html#messages-on-the-shell-router-dealer-channel
(cl-defmethod jupyter-handle-message ((_channel (eql :shell))
client
req
msg)
(unless (jupyter-run-hook-with-args-until-success
client 'jupyter-shell-message-hook msg)
(jupyter-dispatch-message-cases client req msg
((execute-reply status execution_count user_expressions payload)
(shutdown-reply restart)
(inspect-reply found data metadata)
(complete-reply matches cursor_start cursor_end metadata)
(history-reply history)
(is-complete-reply status indent)
(comm-info-reply comms)
(kernel-info-reply protocol_version implementation
implementation_version language_info
banner help_links)))))
;;;; Evaluation
(cl-defgeneric jupyter-load-file-code (_file)
"Return a string suitable to send as code to a kernel for loading FILE.
Use the jupyter-lang method specializer to add a method for a
particular language."
(error "Kernel language (%s) not supported yet"
(jupyter-kernel-language jupyter-current-client)))
;;;;; Evaluation routines
(defvar-local jupyter-eval-expression-history nil
"A client local variable to store the evaluation history.
The evaluation history is used when reading code to evaluate from
the minibuffer.")
(defun jupyter--teardown-minibuffer ()
"Remove Jupyter related variables and hooks from the minibuffer."
(setq jupyter-current-client nil)
(remove-hook 'completion-at-point-functions 'jupyter-completion-at-point t)
(remove-hook 'minibuffer-exit-hook 'jupyter--teardown-minibuffer t))
;; This is needed since `read-from-minibuffer' expects the history variable to
;; be a symbol whose value is `set' when adding a new history element. Since
;; `jupyter-eval-expression-history' is a buffer (client) local variable, it would be
;; set in the minibuffer which we don't want.
(defvar jupyter--read-expression-history nil
"A client's `jupyter-eval-expression-history' when reading an expression.
This variable is used as the history symbol when reading an
expression from the minibuffer. After an expression is read, the
`jupyter-eval-expression-history' of the client is updated to the
value of this variable.")
(cl-defgeneric jupyter-read-expression ()
"Read an expression using the `jupyter-current-client' for completion.
The expression is read from the minibuffer and the expression
history is obtained from the `jupyter-eval-expression-history'
client local variable.
Methods that extend this generic function should
`cl-call-next-method' as a last step."
(cl-check-type jupyter-current-client jupyter-kernel-client
"Need a client to read an expression")
(let* ((client jupyter-current-client)
(jupyter--read-expression-history
(jupyter-get client 'jupyter-eval-expression-history)))
(minibuffer-with-setup-hook
(lambda ()
(setq jupyter-current-client client)
(add-hook 'completion-at-point-functions
'jupyter-completion-at-point nil t)
(add-hook 'minibuffer-exit-hook
'jupyter--teardown-minibuffer nil t))
(prog1 (read-from-minibuffer
(format "Eval (%s): " (jupyter-kernel-language client))
nil read-expression-map
nil 'jupyter--read-expression-history)
(jupyter-set client 'jupyter-eval-expression-history
jupyter--read-expression-history)))))
(defun jupyter-eval (code &optional mime)
"Send an execute request for CODE, wait for the execute result.
The `jupyter-current-client' is used to send the execute request.
All client handlers are inhibited for the request. In addition,
the history of the request is not stored. Return the MIME
representation of the result. If MIME is nil, return the
text/plain representation."
(interactive (list (jupyter-read-expression) nil))
(let ((msg (jupyter-wait-until-received :execute-result
(let* ((jupyter-inhibit-handlers t)
(req (jupyter-send-execute-request jupyter-current-client
:code code :store-history nil)))
(prog1 req
(jupyter-add-callback req
:execute-reply
(jupyter-message-lambda (status evalue)
(unless (equal status "ok")
(error "%s" (ansi-color-apply evalue))))))))))
(when msg
(jupyter-message-data msg (or mime :text/plain)))))
(defun jupyter-eval-result-callbacks (req beg end)
"Return a plist containing callbacks used to display evaluation results.
The plist contains default callbacks for the :execute-reply,
:execute-result, and :display-data messages that may be used for
the messages received in response to REQ.
BEG and END are positions in the current buffer marking the
region of code evaluated.
The callbacks are designed to either display evaluation results
using overlays in the current buffer over the region between BEG
and END or in pop-up buffers/frames. See