-
-
Notifications
You must be signed in to change notification settings - Fork 93
/
jupyter-client.el
1810 lines (1571 loc) · 71.5 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-2024 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-mime)
(require 'jupyter-messages)
(require 'jupyter-kernel)
(require 'jupyter-kernelspec)
(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 client \"execute_request\" ...)))
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-instance-tracker
jupyter-finalized-object)
((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.")
(kernel-info
:type json-plist
:initform nil
:documentation "The saved kernel info created when first
initializing 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.")
(io
:type list
:initarg :io
:documentation "The I/O context kernel messages are communicated on.")
(-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 jupyter-with-client (client &rest body)
"Set CLIENT as the `jupyter-current-client', evaluate BODY.
In addition, set `jupyter-current-io' to the value of CLIENT's IO
slot."
(declare (indent 1))
`(let ((jupyter-current-client ,client))
,@body))
(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))
;;; Initializing a `jupyter-kernel-client'
(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))))))
(cl-defmethod jupyter-kernel-alive-p ((client jupyter-kernel-client))
"Return non-nil if the kernel CLIENT is connected to is alive."
(and (jupyter-connected-p client)
(jupyter-kernel-action client #'jupyter-alive-p)))
(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-all-objects 'jupyter--clients))
(error "No client found for session (%s)" session-id)))
;;; Client local variables
(defmacro jupyter-with-client-buffer (client &rest body)
"Run a form inside CLIENT's internal state buffer.
This buffer has all of the variables of CLIENT that were set
using `jupyter-set' and `jupyter-add-hook'."
(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 depth)
"Add to the CLIENT value of HOOK the function FUNCTION.
DEPTH has the same meaning as in `add-hook' and FUNCTION is added
to HOOK using `add-hook', but local only to CLIENT."
(declare (indent 2))
(jupyter-with-client-buffer client
(add-hook hook function depth 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 &rest slots)
"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."
(apply #'make-jupyter-request slots))
(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 (member msg-type jupyter-message-types)
do (error "Not a valid message type (`%s')" msg-type))))
;;; Starting communication with a kernel
(cl-defmethod jupyter-alive-p ((client jupyter-kernel-client) &optional _channel)
(when-let* ((kernel (jupyter-kernel client)))
(and (jupyter-alive-p kernel)
(jupyter-alive-p (jupyter-io kernel)))))
(cl-defmethod jupyter-hb-pause ((_client jupyter-kernel-client))
;; (when-let* ((kernel (jupyter-kernel client))
;; (hb (jupyter-send (jupyter-io kernel) 'hb)))
;; (jupyter-hb-pause hb))
)
(cl-defmethod jupyter-hb-unpause ((_client jupyter-kernel-client))
;; (when-let* ((kernel (jupyter-kernel client))
;; (hb (jupyter-send (jupyter-io kernel) 'hb)))
;; (jupyter-hb-unpause hb))
)
(cl-defmethod jupyter-hb-beating-p ((_client jupyter-kernel-client))
"Is CLIENT still connected to its kernel?"
t
;; (when-let* ((kernel (jupyter-kernel client)))
;; (let ((hb (jupyter-send (jupyter-io kernel) 'hb)))
;; (or (null hb) (jupyter-hb-beating-p hb))))
)
;;; Mapping kernelspecs to connected clients
(cl-defgeneric jupyter-client (kernel &optional client-class)
"Return a client connected to KERNEL.
The returned client will be an instance of CLIENT-CLASS. The
default class is `jupyter-kernel-client'.")
(cl-defmethod jupyter-client ((kernel string) &optional client-class)
"Return a client connected to KERNEL.
KERNEL is the name of the kernelspec as returned by the
jupyter kernelspec list
shell command."
(jupyter-client (jupyter-get-kernelspec kernel) client-class))
(cl-defmethod jupyter-disconnect ((client jupyter-kernel-client))
(slot-makeunbound client 'io))
(cl-defmethod jupyter-client ((spec jupyter-kernelspec) &optional client-class)
"Return a client connected to kernel created from SPEC.
SPEC is a kernelspec that will be used to initialize a new
kernel whose kernelspec if SPEC."
(jupyter-client (jupyter-kernel :spec spec) client-class))
(cl-defmethod jupyter-connected-p ((client jupyter-kernel-client))
"Return non-nil if CLIENT is connected to a kernel."
(slot-boundp client 'io))
(cl-defmethod jupyter-client ((kernel jupyter-kernel) &optional client-class)
(or client-class (setq client-class 'jupyter-kernel-client))
(cl-assert (child-of-class-p client-class 'jupyter-kernel-client))
(let ((client (make-instance client-class)))
(oset client io (jupyter-io kernel))
(unless (jupyter-kernel-info client)
(error "Kernel did not respond to kernel_info_request"))
;; If the connection can resolve the kernel's heartbeat channel,
;; start monitoring it now.
(jupyter-hb-unpause client)
client))
(defun jupyter-kernel-io (client)
"Return the I/O function of the kernel CLIENT is connected to."
;; TODO: Mention the messages that can be sent to the
;; `jupyter-publisher'. See `jupyter-websocket-io'.
(or (car-safe (oref client io))
(error "Invalid value of a client's IO slot.")))
(defun jupyter-kernel-action-subscriber (client)
"Return a `jupyter-subscriber' used to modify CLIENT's kernel's state."
(or (car (cdr-safe (oref client io)))
(error "Invalid value of a client's IO slot.")))
(defun jupyter-kernel-action (client fn)
"Evaluate FN on the kernel CLIENT is connected to.
FN takes a single argument which will be the kernel object."
(declare (indent 1))
(let ((kaction-sub (jupyter-kernel-action-subscriber client))
(res nil))
(jupyter-run-with-io kaction-sub
(jupyter-publish
(list 'action
(lambda (kernel)
(setq res (funcall fn kernel))))))
res))
;;; Shutdown and interrupt a kernel
(cl-defmethod jupyter-shutdown-kernel ((client jupyter-kernel-client))
"Shutdown the kernel CLIENT is connected to.
After CLIENT shuts down the kernel it is connected to, it is no
longer connected to a kernel."
(let ((kaction-sub (jupyter-kernel-action-subscriber client)))
(jupyter-run-with-io kaction-sub
(jupyter-publish 'shutdown))))
(cl-defmethod jupyter-restart-kernel ((client jupyter-kernel-client))
"Restart the kernel CLIENT is connected to."
(let ((kaction-sub (jupyter-kernel-action-subscriber client)))
(jupyter-run-with-io kaction-sub
(jupyter-publish 'restart))))
(cl-defmethod jupyter-interrupt-kernel ((client jupyter-kernel-client))
"Interrupt the kernel CLIENT is connected to."
(let ((interrupt-mode (jupyter-kernel-action client
(lambda (kernel)
(plist-get
(jupyter-kernelspec-plist
(jupyter-kernel-spec kernel))
:interrupt_mode)))))
(if (equal interrupt-mode "message")
(jupyter-run-with-client client
(jupyter-sent (jupyter-interrupt-request)))
(let ((kaction-sub (jupyter-kernel-action-subscriber client)))
(jupyter-run-with-io kaction-sub
(jupyter-publish 'interrupt))))))
;;; 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 is a `jupyter-request', MSG-TYPE is the message type for
which to wait on and CB is a callback function. 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-run-with-io (jupyter-request-message-publisher req)
(jupyter-subscribe
(jupyter-subscriber
(lambda (req-msg)
(when (equal (jupyter-message-type req-msg) msg-type)
(setq msg (when (funcall cb req-msg) req-msg))
(when msg
(jupyter-unsubscribe)))))))
(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-idle (req &optional timeout progress-msg)
"Wait until a status: idle message is received for a request.
REQ is a `jupyter-request'. 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-with-timeout
(progress-msg (or timeout jupyter-default-timeout))
(jupyter-request-idle-p req))))
(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 depth)
"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. DEPTH 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 depth)))
;;; Client handlers
(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)
(member (jupyter-message-type msg) ihandlers))))
(not (or (eq ihandlers t)
(if (eq (car ihandlers) 'not) (not type) type)))))
(defsubst jupyter--channel-hook-allows-handler-p (client channel msg)
(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)))))
(jupyter-debug "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"
(replace-regexp-in-string
"_" "-" mt)))))))
`(("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-maybe (client channel msg req)
(when (and (jupyter--request-allows-handler-p req msg)
(jupyter--channel-hook-allows-handler-p client channel msg))
(let* ((msg-type (jupyter-message-type msg))
(channel-handlers
(or (alist-get channel jupyter--client-handlers nil nil #'string=)
(error "Unhandled channel: %s" channel)))
(handler (or (alist-get msg-type channel-handlers nil nil #'string=)
(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))))))
(cl-defmethod 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."
(when msg
(let ((print-length 10))
(jupyter-debug "Got MSG: %s %S"
(jupyter-message-type msg)
(jupyter-message-content msg)))
(let ((jupyter-current-client client)
(req (plist-get msg :parent-request)))
(jupyter--update-execution-state client msg req)
(cond
(req (jupyter--run-handler-maybe client channel msg req))
((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 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."
;; TODO: with-message-content -> with-content
(jupyter-with-message-content msg (prompt password)
(let ((value (condition-case nil
;; Disallow any `with-timeout's from timing out.
;; 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-run-with-client client
(jupyter-sent (jupyter-input-reply :value value)))
(when (eq password t)
(clear-string value)))
value)))
;;;; 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))
(jupyter-run-with-client jupyter-current-client
(jupyter-mlet*
((res (jupyter-result
(jupyter-message-subscribed
(jupyter-execute-request
:code code
:store-history nil
:handlers nil)
`(("execute_reply"
,(jupyter-message-lambda (status evalue)
(unless (equal status "ok")
(error "%s" (ansi-color-apply evalue))))))))))
(jupyter-return (jupyter-message-data res (or mime :text/plain))))))
(defun jupyter-eval-result-callbacks (insert 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
`jupyter-eval-use-overlays'."
(let ((buffer (current-buffer))
(use-overlays-p (and beg end (jupyter-eval-display-with-overlay-p))))
(when use-overlays-p
;; NOTE: It would make sense to set these markers to nil, e.g. at the end
;; of the execute-result or idle messages, but there is no guarantee on
;; message order so it may be the case that those message types are
;; received before the callbacks that use these markers have fired.
;;
;; TODO: Do something with finalizers?
(setq beg (set-marker (make-marker) beg))
(setq end (set-marker (make-marker) end)))
(let ((display-overlay
(if use-overlays-p
(lambda (val)
(when (buffer-live-p buffer)
(prog1 t
(with-current-buffer buffer
(jupyter-eval-display-overlay beg end val)))))
#'ignore))
had-result)
`(("execute_reply"
,(jupyter-message-lambda (status ename evalue)
(cond
((equal status "ok")
(unless had-result
(unless (funcall display-overlay "✔")
(message "jupyter: eval done"))))
(t
(setq ename (ansi-color-apply ename))
(setq evalue (ansi-color-apply evalue))
(unless
;; Happens in IJulia
(> (+ (length ename) (length evalue)) 250)
(if (string-prefix-p ename evalue)
;; Also happens in IJulia
(message evalue)
(message "%s: %s" ename evalue)))))))
("execute_result"
,(if insert
(let ((pos (point-marker))
(region (unless (and (= beg (line-beginning-position))
(= end (line-end-position)))
(cons beg end))))
(jupyter-message-lambda ((res text/plain))
(when res
(setq res (ansi-color-apply res))
(with-current-buffer (marker-buffer pos)
(save-excursion
(cond
(region
(goto-char (car region))
(delete-region (car region) (cdr region)))
(t
(goto-char pos)
(end-of-line)
(insert "\n")))
(set-marker pos nil)
(insert res)
(when region (push-mark)))))))
(lambda (msg)
(setq had-result t)
(jupyter-with-message-data msg
((res text/plain)
;; Prefer to display the markdown representation if available. The
;; IJulia kernel will return both plain text and markdown.
(md text/markdown))
(let ((jupyter-pop-up-frame (jupyter-pop-up-frame-p "execute_result")))
(cond
((or md (null res))
(jupyter-with-display-buffer "result" 'reset
(jupyter-with-message-content msg (data metadata)
(jupyter-insert data metadata))
(goto-char (point-min))
(jupyter-display-current-buffer-reuse-window)))
(res
(setq res (ansi-color-apply res))
(cond
((funcall display-overlay res))
((jupyter-line-count-greater-p
res jupyter-eval-short-result-max-lines)
(jupyter-with-display-buffer "result" 'reset
(insert res)
(goto-char (point-min))
(jupyter-display-current-buffer-reuse-window)))
(t
(funcall jupyter-eval-short-result-display-function
(format "%s" res)))))))))))
("display_data"
,(lambda (msg)
(jupyter-with-message-content msg (data metadata)
(setq had-result t)
(jupyter-with-display-buffer "display"
(plist-get msg :parent-request)
(jupyter-insert data metadata)
;; Don't pop-up the display when it's empty (e.g. jupyter-R
;; will open some HTML results in an external browser)
(when (and (/= (point-min) (point-max)))
(jupyter-display-current-buffer-guess-where :display-data)))
;; TODO: Also inline images?
(funcall display-overlay "✔"))))))))
(defun jupyter-eval-callbacks (&optional insert beg end)
"Return evaluation callbacks.
The callbacks are designed to handle the various message types
that can be generated by an execute-request to, e.g. display the
results of evaluation in a popup buffer or indicate that an error
occurred during evaluation.
The message types that will have callbacks added are
:execute-reply, :execute-result, :display-data, :error, :stream.
BEG and END are positions that mark the region of the current
buffer corresponding to the evaluated code.
See `jupyter-eval-short-result-max-lines' and
`jupyter-eval-use-overlays'."
(nconc
(jupyter-eval-result-callbacks insert beg end)
`(("error"
,(jupyter-message-lambda (traceback)
;; FIXME: Assumes the error in the
;; execute-reply is good enough
(when (> (apply #'+ (mapcar #'length traceback)) 250)
(jupyter-display-traceback traceback))))
("stream"
(lambda (msg)
(jupyter-with-message-content msg (name text)
(jupyter-with-display-buffer (pcase name
("stderr" "error")
(_ "output"))
;; TODO: Is there a better solution than just having the
;; request be a part of the message property list?
(plist-get msg :parent-request)
(jupyter-insert-ansi-coded-text text)
(when-let* ((window (jupyter-display-current-buffer-guess-where :stream)))
(set-window-point window (point-max))))))))))
(cl-defgeneric jupyter-eval-string (str &optional beg end)
"Evaluate STR using the `jupyter-current-client'.
Return the `jupyter-request' object for the evaluation.
If BEG and END are non-nil they correspond to the region of the
current buffer that STR was extracted from.")
(cl-defmethod jupyter-eval-string (str &optional insert beg end)
"Evaluate STR using the `jupyter-current-client'."
(cl-check-type jupyter-current-client jupyter-kernel-client
"Not a valid client")
(jupyter-run-with-client jupyter-current-client
(jupyter-sent
(jupyter-message-subscribed
(jupyter-execute-request
:code str
:store-history nil
:handlers '("input_request"))
(jupyter-eval-callbacks insert beg end)))))
(defun jupyter-eval-string-command (str)
"Evaluate STR using the `jupyter-current-client'.
If the result of evaluation is more than
`jupyter-eval-short-result-max-lines' long, a buffer displaying
the results is shown. For less lines, the result is displayed
with `jupyter-eval-short-result-display-function'.
If `jupyter-eval-use-overlays' is non-nil, evaluation results
are displayed in the current buffer instead."
(interactive (list (jupyter-read-expression)))
(jupyter-eval-string str))
(defun jupyter-eval-region (insert beg end)
"Evaluate a region with the `jupyter-current-client'.
If INSERT is non-nil, insert the result of evaluation in the
buffer. BEG and END are the beginning and end of the region to
evaluate.
If the result of evaluation is more than
`jupyter-eval-short-result-max-lines' long, a buffer displaying
the results is shown. For less lines, the result is displayed
with `jupyter-eval-short-result-display-function'.
If `jupyter-eval-use-overlays' is non-nil, evaluation results
are displayed in the current buffer instead."
(interactive "Pr")
(jupyter-eval-string
(buffer-substring-no-properties beg end)
insert beg end))
(defun jupyter-eval-line-or-region (insert)
"Evaluate the current line or region with the `jupyter-current-client'.
If the current region is active send it using
`jupyter-eval-region', otherwise send the current line.
With a prefix argument, evaluate and INSERT the text/plain
representation of the results in the current buffer."
(interactive "P")
(let* ((region (when (use-region-p) (car (region-bounds)))))
(jupyter-eval-region insert
(or (car region) (line-beginning-position))
(or (cdr region) (line-end-position)))))
(defun jupyter-load-file (file)
"Send the contents of FILE using `jupyter-current-client'."
(interactive
(list (read-file-name "File name: " nil nil nil
(file-name-nondirectory
(or (buffer-file-name) "")))))
(message "Evaluating %s..." file)
(setq file (expand-file-name file))
(if (file-exists-p file)
(jupyter-eval-string (jupyter-load-file-code file))
(error "Not a file (%s)" file)))
(defun jupyter-eval-buffer (buffer)
"Send the contents of BUFFER using `jupyter-current-client'."
(interactive (list (current-buffer)))
(jupyter-eval-string (with-current-buffer buffer (buffer-string))))
(defun jupyter-eval-defun ()
"Evaluate the function at `point'."
(interactive)
(when-let* ((bounds (bounds-of-thing-at-point 'defun)))
(cl-destructuring-bind (beg . end) bounds
(jupyter-eval-region nil beg end))))
;;;;;; Evaluation overlays