emacs-jupyter/jupyter-kernelspec.el
Nathaniel Nicandro 5857352135 Add the jupyter-kernelspecs method
Replaces both `jupyter-available-kernelspecs` and
`jupyter-server-kernelspecs`.
2023-02-13 20:25:05 -06:00

215 lines
8.2 KiB
EmacsLisp

;;; jupyter-kernelspec.el --- Jupyter kernelspecs -*- lexical-binding: t -*-
;; Copyright (C) 2018-2020 Nathaniel Nicandro
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
;; Created: 17 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:
;; Functions to work with kernelspecs found by the shell command
;;
;; jupyter kernelspec list
;;; Code:
(require 'json)
(require 'jupyter-env)
(defgroup jupyter-kernelspec nil
"Jupyter kernelspecs"
:group 'jupyter)
(declare-function jupyter-read-plist "jupyter-base" (file))
(declare-function jupyter-read-plist-from-string "jupyter-base" (file))
(cl-defstruct jupyter-kernelspec
(name "python"
:type string
:documentation "The name of the kernelspec."
:read-only t)
(plist nil
:type list
:documentation "The kernelspec as a property list."
:read-only t)
(resource-directory nil
:type (or null string)
:documentation "The resource directory."
:read-only t))
(defvar jupyter--kernelspecs (make-hash-table :test #'equal :size 5)
"A hash table mapping hosts to the kernelspecs available on them.")
(defun jupyter-available-kernelspecs (&optional refresh)
"Return the available kernelspecs.
Return a list of `jupyter-kernelspec's available on the host
associated with the `default-directory'. If `default-directory'
is a remote file name, return the list of available kernelspecs
on the remote system. The kernelspecs on the local system are
returned otherwise.
On any system, the list is formed by parsing the output of the
shell command
jupyter kernelspec list --json
By default the available kernelspecs are cached. To force an
update of the cached kernelspecs, give a non-nil value to
REFRESH."
(let* ((host (or (file-remote-p default-directory) "local"))
(kernelspecs
(or (and (not refresh) (gethash host jupyter--kernelspecs))
(let ((specs
(plist-get
(jupyter-read-plist-from-string
(or (jupyter-command "kernelspec" "list" "--json" "--log-level" "ERROR")
(error "Can't obtain kernelspecs from jupyter shell command")))
:kernelspecs)))
(puthash
host (cl-loop
for (kname spec) on specs by #'cddr
for name = (substring (symbol-name kname) 1)
for dir = (plist-get spec :resource_dir)
collect (make-jupyter-kernelspec
:name name
:resource-directory (concat
(unless (string= host "local") host)
dir)
:plist (plist-get spec :spec)))
jupyter--kernelspecs)))))
kernelspecs))
(cl-defgeneric jupyter-kernelspecs (host &optional refresh)
"Return a list of kernelspecs on HOST.")
(cl-defmethod jupyter-kernelspecs ((host string) &optional refresh)
(let ((default-directory host))
(jupyter-available-kernelspecs refresh)))
(defun jupyter-get-kernelspec (name &optional specs refresh)
"Get the kernelspec for a kernel named NAME.
If no kernelspec is found, return nil. Otherwise return the
kernelspec for the kernel named NAME.
If SPECS is provided, it is a list of kernelspecs that will be
searched. Otherwise the kernelspecs associated with the
`default-directory' are used.
Optional argument REFRESH has the same meaning as in
`jupyter-kernelspecs'."
(cl-loop
for kernelspec in (or specs (jupyter-kernelspecs default-directory refresh))
thereis (when (string= (jupyter-kernelspec-name kernelspec) name)
kernelspec)))
(defun jupyter-find-kernelspecs (re &optional specs refresh)
"Find all specs of kernels that have names matching RE.
RE is a regular expression use to match the name of a kernel.
Return a list of `jupyter-kernelspec' objects.
If SPECS is non-nil search SPECS, otherwise search the
kernelspecs associated with the `default-directory'.
Optional argument REFRESH has the same meaning as in
`jupyter-kernelspecs'."
(cl-remove-if-not
(lambda (kernelspec)
(string-match-p re (jupyter-kernelspec-name kernelspec)))
(or specs (jupyter-kernelspecs default-directory refresh))))
(defun jupyter-guess-kernelspec (name &optional specs refresh)
"Return the first kernelspec starting with NAME.
Raise an error if no kernelspec could be found.
SPECS and REFRESH have the same meaning as in
`jupyter-find-kernelspecs'."
(or (car (jupyter-find-kernelspecs (format "^%s" name) specs refresh))
(error "No valid kernelspec for kernel name (%s)" name)))
(defun jupyter-completing-read-kernelspec (&optional specs refresh)
"Use `completing-read' to select a kernel and return its kernelspec.
SPECS is a list of kernelspecs that will be used for completion,
if it is nil the kernelspecs associated with the
`default-directory' will be used.
Optional argument REFRESH has the same meaning as in
`jupyter-kernelspecs'."
(let* ((specs (or specs (jupyter-kernelspecs default-directory refresh)))
(display-names (if (null specs) (error "No kernelspecs available")
(mapcar (lambda (k)
(plist-get
(jupyter-kernelspec-plist k)
:display_name))
specs)))
(name (completing-read "kernel: " display-names nil t)))
(when (equal name "")
(error "No kernelspec selected"))
(nth (- (length display-names)
(length (member name display-names)))
specs)))
(defun jupyter-expand-environment-variables (var)
"Return VAR with all environment variables expanded.
VAR is a string, if VAR contains a sequence of characters like
${ENV_VAR}, substitute it with the value of ENV_VAR in
`process-environment'."
(let ((expanded "")
(start 0))
(while (string-match "\\${\\([^}]+\\)}" var start)
(cl-callf concat expanded
(substring var start (match-beginning 0))
(getenv (match-string 1 var)))
(setq start (match-end 0)))
(cl-callf concat expanded (substring var start))))
(defun jupyter-process-environment (kernelspec)
"Return a list of environment variables contained in KERNELSPEC.
The list of environment variables have the same form as the
entries in `process-environment'.
The environment variables returned are constructed from those in
the :env key of KERNELSPEC's property list."
(cl-loop
with env = (plist-get (jupyter-kernelspec-plist kernelspec) :env)
for (k v) on env by #'cddr
collect (format "%s=%s" (cl-subseq (symbol-name k) 1)
(jupyter-expand-environment-variables v))))
(defun jupyter-kernel-argv (kernelspec conn-file)
"Return a list of process arguments contained in KERNELSPEC.
The process arguments are the ones that should be passed to
kernel processes launched using KERNELSPEC.
CONN-FILE is the file name of a connection file, containing the
IP address and ports (among other things), a
launched kernel should connect to."
(cl-loop
with argv = (plist-get (jupyter-kernelspec-plist kernelspec) :argv)
for arg in (append argv nil)
if (equal arg "{connection_file}")
collect (file-local-name conn-file)
else if (equal arg "{resource_dir}")
collect (file-local-name
(jupyter-kernelspec-resource-directory
kernelspec))
else collect arg))
(provide 'jupyter-kernelspec)
;;; jupyter-kernelspec.el ends here