forked from emacs-jupyter/jupyter
-
Notifications
You must be signed in to change notification settings - Fork 0
/
jupyter-client.el
2140 lines (1861 loc) · 84.8 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-2020 Nathaniel Nicandro
;; Author: Nathaniel Nicandro <[email protected]>
;; Created: 06 Jan 2018
;; 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))
;;; Macros
(defmacro define-jupyter-client-handler (type &optional args doc &rest body)
"Define an implementation of jupyter-handle-TYPE, a Jupyter message handler.
ARGS is a three element argument specification, with the same
meaning as in `cl-defmethod', e.g.
((client jupyter-kernel-client) req msg)
When a message is handled by a client handler method, the first
element will be bound to a subclass of `jupyter-kernel-client',
the second to the `jupyter-request' that caused the message to be
handled, and MSG is the message property list.
DOC is an explanation of the handler and defaults to
\"A :TYPE handler.\"
BODY is the list of expressions to evaluate when the returned
method is called."
(declare (indent defun) (doc-string 2))
(when doc
(unless (stringp doc)
(setq body (cons doc body)
doc nil)))
(when (null body) (setq body (list nil)))
;; ARGS is only a list like (client msg)
(cl-assert (or (null args) (= (length args) 3)) t
"ARGS should be an argument list like (client req msg) or nil.")
`(cl-defmethod ,(intern (format "jupyter-handle-%s" type))
,(or args
;; Internal usage. Most default handlers are just stub
;; definitions that should not signal an error if called,
;; which is what would happen if no method was defined, so
;; reduce the amount of repetition.
'((_client jupyter-kernel-client) _req _msg))
,(or doc (format "A :%s handler." type))
,@body))
;; FIXME: Remove the need to call `jupyter-message-*' functions by
;; introducing some kind of property list defaults mechanism (e.g. by
;; appending the defaults to the property list passed to
;; `jupyter-send-*').
;;
;; - Document the :inhibit-handlers key
;; - Document that client is bound to the kernel client in BODY
(defmacro define--jupyter-client-sender (type &optional doc &rest body)
(declare (indent 2) (doc-string 2))
(when doc
(unless (stringp doc)
(setq body (cons doc body)
doc nil)))
(let (defaults)
(while (keywordp (car body))
(push (pop body) defaults)
(push (pop body) defaults))
(cl-callf nreverse defaults)
(cl-labels ((keyword-name (k) (intern (substring (symbol-name k) 1)))
(as-keyword
(k) (if (keywordp k) k
(intern (format ":%s" k)))))
`(cl-defgeneric ,(intern (format "jupyter-send-%s" type))
((client jupyter-kernel-client)
&key (inhibit-handlers nil)
,@(cl-loop for (k v) on defaults by #'cddr
collect (list (keyword-name k) v)))
,(or doc (format "Send an :%s message." type))
(declare (indent 1))
(let ((jupyter-inhibit-handlers
(or inhibit-handlers jupyter-inhibit-handlers))
(msg
(,(intern (format "jupyter-message-%s" type))
,@(cl-loop
for (k _) on defaults by #'cddr
nconc (list k (keyword-name k))))))
(prog1 (jupyter-send client :shell ,(as-keyword type) msg)
,@body))))))
;;; 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-comm-initialize'."
(let ((conn-info (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)))))))
;; Also validate the signature scheme here.
(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)))
conn-info))
;; FIXME: This requires that CLIENT is communicating with a kernel using a
;; `jupyter-channel-ioloop-comm' object.
(cl-defmethod jupyter-comm-initialize ((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)))
(oset client session
(or (copy-sequence session)
(jupyter-session
:key (plist-get conn-info :key)
:conn-info conn-info)))
(jupyter-comm-initialize
(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-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-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-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-comm-add-handler (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-comm-remove-handler (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
(defsubst 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-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."
(or (jupyter-request-idle-p req)
(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))
(defun jupyter-idle-sync (req)
"Return only when REQ has received a status: idle message."
(while (null (jupyter-wait-until-idle req jupyter-long-timeout))))
(defun jupyter-add-idle-sync-hook (hook req &optional append)
"Add a function to HOOK that waits until REQ receives a status: idle message.
The function will not return until either a status: idle message
has been received by REQ or an error is signaled. APPEND and has
the same meaning as in `add-hook'.
The function is added to the global value of HOOK. When the
function is evaluated, it removes itself from HOOK *before*
waiting."
(cl-check-type req jupyter-request)
(cl-labels
((sync-hook
()
(remove-hook hook #'sync-hook)
(jupyter-idle-sync req)))
(add-hook hook #'sync-hook append)))
;;; 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-p req)
(not (eq req last-sent)))
do (unwind-protect
(jupyter-drop-request client req)
(remhash (jupyter-request-id req) requests)))))
(defsubst jupyter--request-allows-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-handle-message-p (client channel msg)
"Return non-nil if CLIENT should handle a MSG received on CHANNEL.
Run CLIENT's CHANNEL hook, jupyter-CHANNEL-message-hook,
passing (CLIENT MSG) as arguments to the hook functions. If all
of the hook functions return nil, then MSG should be handled.
nil is returned otherwise."
(jupyter-with-client-buffer client
(let ((hook (pcase channel
(:iopub 'jupyter-iopub-message-hook)
(:shell 'jupyter-shell-message-hook)
(:stdin 'jupyter-stdin-message-hook)
(_ (error "Unhandled channel: %s" channel)))))
(when jupyter--debug
(message "RUN-HOOK: %s" hook))
(with-demoted-errors "Error in Jupyter message hook: %S"
(not (run-hook-with-args-until-success
hook client msg))))))
(defconst jupyter--client-handlers
(cl-labels
((handler-alist
(&rest msg-types)
(cl-loop
for mt in msg-types
collect (cons mt (intern
(format "jupyter-handle-%s"
(substring (symbol-name mt) 1)))))))
`((:iopub . ,(handler-alist
:shutdown-reply :stream :comm-open :comm-msg
:comm-close :execute-input :execute-result
:error :status :clear-output :display-data
:update-display-data))
(:shell . ,(handler-alist
:execute-reply :shutdown-reply :inspect-reply
:complete-reply :history-reply :is-complete-reply
:comm-info-reply :kernel-info-reply))
(:stdin . ,(handler-alist
:input-reply :input-request)))))
(defun jupyter--run-handler (client channel msg req)
(when (jupyter-handle-message-p client channel msg)
(let* ((msg-type (jupyter-message-type msg))
(channel-handlers
(or (alist-get channel jupyter--client-handlers)
(error "Unhandled channel: %s" channel)))
(handler (or (alist-get msg-type channel-handlers)
(error "Unhandled message type: %s" msg-type))))
(funcall handler client req msg))))
(defsubst jupyter--update-execution-state (client msg req)
(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))))))
(defsubst jupyter--message-completes-request-p (msg)
(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)))
(cl-defgeneric jupyter-handle-message ((client jupyter-kernel-client) channel msg)
"Process a message received on CLIENT's CHANNEL.
CHANNEL is the Jupyter channel that MSG was received on by
CLIENT. MSG is a message property list and is the Jupyter
message being handled.")
(cl-defmethod jupyter-handle-message ((client jupyter-kernel-client) channel msg)
"Run callbacks and handler method for MSG.
Before any handling of MSG takes place, update CLIENT's execution
status slots (execution-state, execution-count) based on MSG, let
bind `jupyter-current-client' to CLIENT, and, when there is a
`jupyter-request' sent by CLIENT associated with the
`jupyter-message-parent-id' of MSG, set the
`jupyter-request-last-message' of the request to MSG.
CLIENT may not have sent the request that generated MSG, e.g. if
MSG is an :execute-input request broadcasted to :iopub and not
sent by CLIENT. In this case, a message handler method is run,
without running any message callbacks, only if
`jupyter-include-other-output' is non-nil for CLIENT.
When MSG has an associated request generated by CLIENT, run the
`jupyter-request-callbacks', if any, for the message before
attempting to run the message handler. Then remove old,
completed, requests from CLIENT's request table."
(when msg
(let ((jupyter-current-client client)
(req (gethash (jupyter-message-parent-id msg) (oref client requests))))
(jupyter--update-execution-state client msg req)
(cond
(req
(setf (jupyter-request-last-message req) msg)
(unwind-protect
(jupyter--run-callbacks req msg)
(unwind-protect
(when (jupyter--request-allows-handler-p req msg)
(jupyter--run-handler client channel msg req))
(when (jupyter--message-completes-request-p msg)
;; 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-p req) t)))))
(t
(when (and (or (jupyter-get client 'jupyter-include-other-output)
;; Always handle a startup message
(jupyter-message-status-starting-p msg))
(jupyter--request-allows-handler-p req msg))
(jupyter--run-handler client channel msg req)))))))
;;; STDIN handlers
(define-jupyter-client-handler input-request ((client jupyter-kernel-client) _req msg)
"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."
(jupyter-with-message-content msg (prompt password)
(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 ""))))
(unwind-protect
(jupyter-send
client :stdin
:input-reply (jupyter-message-input-reply
:value value))
(when (eq password t)
(clear-string value)))
value)))
(defalias 'jupyter-handle-input-reply 'jupyter-handle-input-request)
;;;; 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