-
-
Notifications
You must be signed in to change notification settings - Fork 94
/
jupyter-env.el
195 lines (171 loc) · 8.14 KB
/
jupyter-env.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
;;; jupyter-env.el --- Query the jupyter shell command for information -*- lexical-binding: t -*-
;; Copyright (C) 2019-2024 Nathaniel Nicandro
;; Author: Nathaniel Nicandro <[email protected]>
;; Created: 27 Jun 2019
;; 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:
;; Custom variables and functions related to calling the jupyter shell command
;; and its sub-commands for information.
;;; Code:
(require 'jupyter-base)
(eval-when-compile (require 'subr-x))
(defvar jupyter-runtime-directory nil
"The Jupyter runtime directory.
When a new kernel is started through `jupyter-start-kernel', this
directory is where kernel connection files are written to.
This variable should not be used. To obtain the runtime directory
call the function `jupyter-runtime-directory'.")
(defcustom jupyter-executable "jupyter"
"The `jupyter` command executable."
:type 'string
:group 'jupyter)
(defun jupyter-command (&rest args)
"Run a Jupyter shell command synchronously, return its output.
The shell command run is
jupyter ARGS...
If the command fails or the jupyter shell command doesn't exist,
return nil."
(let ((stderr-file (make-temp-file "jupyter"))
(stdout (get-buffer-create " *jupyter-command-stdout*")))
(unwind-protect
(let* ((status (apply #'process-file
jupyter-executable
nil
(list stdout stderr-file)
nil
args))
(buffer (find-file-noselect stderr-file)))
(unwind-protect
(with-current-buffer buffer
(unless (eq (point-min) (point-max))
(message "jupyter-command: Content written to stderr stream")
(while (not (eq (point) (point-max)))
(message " %s" (buffer-substring (line-beginning-position)
(line-end-position)))
(forward-line))))
(kill-buffer buffer))
(when (zerop status)
(with-current-buffer stdout
(string-trim-right (buffer-string)))))
(delete-file stderr-file)
(kill-buffer stdout))))
(defun jupyter-runtime-directory ()
"Return the runtime directory used by Jupyter.
Create the directory if necessary. If `default-directory' is a
remote directory, return the runtime directory on that remote.
As a side effect, the variable `jupyter-runtime-directory' is set
to the local runtime directory if it is nil."
(unless jupyter-runtime-directory
(setq jupyter-runtime-directory
(let ((default-directory (expand-file-name "~" user-emacs-directory)))
(file-name-as-directory (jupyter-command "--runtime-dir")))))
(let ((dir (if (file-remote-p default-directory)
(jupyter-command "--runtime-dir")
jupyter-runtime-directory)))
(unless dir
(error "Can't obtain runtime directory from jupyter shell command"))
(setq dir (concat (file-remote-p default-directory) dir))
(make-directory dir 'parents)
(file-name-as-directory dir)))
(defun jupyter-locate-python ()
"Return the path to the python executable in use by Jupyter.
If the `default-directory' is a remote directory, search on that
remote. Raise an error if the executable could not be found.
The paths examined are the data paths of \"jupyter --paths\" in
the order specified.
This function always returns the `file-local-name' of the path."
(let* ((remote (file-remote-p default-directory))
(paths (mapcar (lambda (x) (concat remote x))
(or (plist-get
(jupyter-read-plist-from-string
(jupyter-command "--paths" "--json"))
:data)
(error "Can't get search paths"))))
(path nil))
(cl-loop
with programs = '("bin/python3" "bin/python"
;; Need to also check Windows since paths can be
;; pointing to local or remote files.
"python3.exe" "python.exe")
with pred = (lambda (dir)
(cl-loop
for program in programs
for spath = (expand-file-name program dir)
thereis (setq path (and (file-exists-p spath) spath))))
for path in paths
thereis (locate-dominating-file path pred)
finally (error "No `python' found in search paths"))
(file-local-name path)))
(defun jupyter-write-connection-file (session)
"Write a connection file based on SESSION to `jupyter-runtime-directory'.
Return the path to the connection file."
(cl-check-type session jupyter-session)
(let* ((temporary-file-directory (jupyter-runtime-directory))
(json-encoding-pretty-print t)
(file (make-temp-file "emacs-kernel-" nil ".json")))
(prog1 file
(with-temp-file file
(insert (json-encode-plist
(jupyter-session-conn-info session)))))))
(defun jupyter-session-with-random-ports ()
"Return a `jupyter-session' with random channel ports.
The session can be used to write a connection file, see
`jupyter-write-connection-file'."
;; The actual work of making the connection file is left up to the
;; `jupyter kernel` shell command. This is done to support
;; launching remote kernels via TRAMP. The Jupyter suite of shell
;; commands probably exist on the remote system, so we rely on them
;; to figure out a set of open ports on the remote.
(with-temp-buffer
;; NOTE: On Windows, apparently the "jupyter kernel" command uses something
;; like an exec shell command to start the process which launches the kernel,
;; but exec like commands on Windows start a new process instead of replacing
;; the current one which results in the process we start here exiting after
;; the new process is launched. We call python directly to avoid this.
(let ((process (start-file-process
"jupyter-session-with-random-ports" (current-buffer)
(jupyter-locate-python) "-c"
"from jupyter_client.kernelapp import main; main()")))
(set-process-query-on-exit-flag process nil)
(jupyter-with-timeout
(nil jupyter-long-timeout
(error "`jupyter kernel` failed to show connection file path"))
(and (process-live-p process)
(goto-char (point-min))
(re-search-forward (rx "Connection file: "
(group (+ any) ".json")
(* whitespace) line-end)
nil t)))
(let* ((conn-file (concat
(save-match-data
(file-remote-p default-directory))
(match-string 1)))
(conn-info (jupyter-read-connection conn-file)))
;; Tell the `jupyter kernel` process to shutdown itself and
;; the launched kernel.
(interrupt-process process)
;; Wait until the connection file is cleaned up before
;; forgetting about the process completely.
(jupyter-with-timeout
(nil jupyter-default-timeout
(delete-file conn-file))
(not (file-exists-p conn-file)))
(delete-process process)
(let ((new-key (jupyter-new-uuid)))
(plist-put conn-info :key new-key)
(jupyter-session
:conn-info conn-info
:key new-key))))))
(provide 'jupyter-env)
;;; jupyter-env.el ends here