emacs.d/elpa/ein-20200307.2048/ein-core.el

363 lines
14 KiB
EmacsLisp
Raw Normal View History

2020-02-03 19:45:34 +01:00
;;; ein-core.el --- EIN core
2019-11-30 08:46:49 +01:00
;; Copyright (C) 2012 Takafumi Arakaki
;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
;; This file is NOT part of GNU Emacs.
;; ein-core.el 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 of the License, or
;; (at your option) any later version.
;; ein-core.el 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 ein-core.el.
;; If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
;; Optional dependency on tramp:
(declare-function tramp-make-tramp-file-name "tramp")
(declare-function tramp-file-name-localname "tramp")
(declare-function tramp-dissect-file-name "tramp")
(require 'ein) ; get autoloaded functions into namespace
(require 'ein-utils)
2020-02-03 19:45:34 +01:00
(require 'anaphora)
2019-11-30 08:46:49 +01:00
(defgroup ein nil
"IPython notebook client in Emacs"
:group 'applications
:prefix "ein:")
;;; Configuration
2020-02-03 19:45:34 +01:00
(defcustom ein:urls nil
2019-11-30 08:46:49 +01:00
"List of default url-or-port values.
This will be used for completion. So put your IPython servers.
You can connect to servers not in this list \(but you will need
to type every time)."
2020-02-03 19:45:34 +01:00
:type '(repeat (choice (integer :tag "Port number" 8888)))
2019-11-30 08:46:49 +01:00
:group 'ein)
2020-02-03 19:45:34 +01:00
(make-obsolete-variable 'ein:default-url-or-port nil "0.17.0")
(define-obsolete-variable-alias 'ein:url-or-port 'ein:urls "0.17.0")
2019-11-30 08:46:49 +01:00
(defcustom ein:filename-translations nil
"Convert file paths between Emacs and Python process.
This value can take these form:
alist
Its key specifies URL-OR-PORT and value must be a list of two
functions: (TO-PYTHON FROM-PYTHON). Key (URL-OR-PORT) can be
string (URL), integer (port), or `default' (symbol). The
value of `default' is used when other key does not much.
function
Called with an argument URL-OR-PORT (integer or string).
This function must return a list of two functions:
(TO-PYTHON FROM-PYTHON).
Here, the functions TO-PYTHON and FROM-PYTHON are defined as:
TO-PYTHON
A function which converts a file name (returned by
`buffer-file-name') to the one Python understands.
FROM-PYTHON
A function which converts a file path returned by
Python process to the one Emacs understands.
Use `ein:tramp-create-filename-translator' to easily generate the
pair of TO-PYTHON and FROM-PYTHON."
;; I've got the idea from `slime-filename-translations'.
:type '(choice
(alist :tag "Translations mapping"
:key-type (choice :tag "URL or PORT"
(string :tag "URL" "http://127.0.0.1:8888")
(integer :tag "PORT" 8888)
(const default))
:value-type (list (function :tag "TO-PYTHON")
(function :tag "FROM-PYTHON")))
(function :tag "Translations getter"))
:group 'ein)
(defconst ein:source-dir (file-name-directory load-file-name)
"Directory in which `ein*.el` files are located.")
(defun ein:version (&optional interactively copy-to-kill)
"Return a longer version string.
With prefix argument, copy the string to kill ring.
The result contains `ein:version' and either git revision (if
the source is in git repository) or elpa version."
(interactive (list t current-prefix-arg))
(let* ((version
(or (and (ein:git-root-p
(concat (file-name-as-directory ein:source-dir) ".."))
(let ((default-directory ein:source-dir))
(ein:git-revision-dirty)))
(and (string-match "/ein-\\([0-9\\.]*\\)/$" ein:source-dir)
(match-string 1 ein:source-dir)))))
(when interactively
(message "EIN version is %s" version))
(when copy-to-kill
(kill-new version))
version))
;;; Server attribute getters. These should be moved to ein-open.el
(defvar *ein:notebook-version* (make-hash-table :test #'equal)
"url-or-port to major notebook version")
(defvar *ein:kernelspecs* (make-hash-table :test #'equal)
"url-or-port to kernelspecs")
2020-02-03 19:45:34 +01:00
(defun ein:get-kernelspec (url-or-port name &optional lang)
2019-11-30 08:46:49 +01:00
(let* ((kernelspecs (ein:need-kernelspecs url-or-port))
(name (if (stringp name)
(intern (format ":%s" name))
name))
2020-02-03 19:45:34 +01:00
(ks (or (plist-get kernelspecs name)
(cl-loop for (key spec) on (ein:plist-exclude kernelspecs '(:default)) by 'cddr
if (string= (ein:$kernelspec-language spec) lang)
return spec
end))))
(cond ((stringp ks)
(ein:get-kernelspec url-or-port ks))
(t ks))))
2019-11-30 08:46:49 +01:00
(defun ein:need-kernelspecs (url-or-port)
"Callers assume ein:query-kernelspecs succeeded. If not, nil."
2020-02-03 19:45:34 +01:00
(aif (gethash url-or-port *ein:kernelspecs*) it
2019-11-30 08:46:49 +01:00
(ein:log 'warn "No recorded kernelspecs for %s" url-or-port)
nil))
(defun ein:query-kernelspecs (url-or-port callback &optional iteration)
"Send for kernelspecs of URL-OR-PORT with CALLBACK arity 0 (just a semaphore)"
(unless iteration
(setq iteration 0))
(ein:query-singleton-ajax
(ein:url url-or-port "api/kernelspecs")
:type "GET"
:timeout ein:content-query-timeout
:parser 'ein:json-read
:complete (apply-partially #'ein:query-kernelspecs--complete url-or-port)
:success (apply-partially #'ein:query-kernelspecs--success url-or-port callback)
:error (apply-partially #'ein:query-kernelspecs--error url-or-port callback iteration)))
(defun ein:normalize-kernelspec-language (name)
"Normalize the kernelspec language string"
(if (stringp name)
(replace-regexp-in-string "[ ]" "-" name)
name))
(cl-defun ein:query-kernelspecs--success (url-or-port callback
2020-02-03 19:45:34 +01:00
&key data symbol-status response
&allow-other-keys)
2019-11-30 08:46:49 +01:00
(let ((ks (list :default (plist-get data :default)))
(specs (ein:plist-iter (plist-get data :kernelspecs))))
(setf (gethash url-or-port *ein:kernelspecs*)
(ein:flatten (dolist (spec specs ks)
(let ((name (car spec))
(info (cdr spec)))
(push (list name (make-ein:$kernelspec :name (plist-get info :name)
:display-name (plist-get (plist-get info :spec)
:display_name)
:resources (plist-get info :resources)
:language (ein:normalize-kernelspec-language
(plist-get (plist-get info :spec)
:language))
:spec (plist-get info :spec)))
ks))))))
(when callback (funcall callback)))
(cl-defun ein:query-kernelspecs--error (url-or-port callback iteration
2020-02-03 19:45:34 +01:00
&key response error-thrown
&allow-other-keys)
2019-11-30 08:46:49 +01:00
(if (< iteration 3)
(progn
(ein:log 'verbose "Retry kernelspecs #%s in response to %s" iteration (request-response-status-code response))
(ein:query-kernelspecs url-or-port callback (1+ iteration)))
(ein:log 'error
"ein:query-kernelspecs--error %s: ERROR %s DATA %s" url-or-port (car error-thrown) (cdr error-thrown))
(when callback (funcall callback))))
2020-02-03 19:45:34 +01:00
(cl-defun ein:query-kernelspecs--complete (url-or-port &key data response
&allow-other-keys
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
2019-11-30 08:46:49 +01:00
(ein:log 'debug "ein:query-kernelspecs--complete %s" resp-string))
(defsubst ein:notebook-version-numeric (url-or-port)
(truncate (string-to-number (ein:need-notebook-version url-or-port))))
(defun ein:need-notebook-version (url-or-port)
"Callers assume ein:query-notebook-version succeeded. If not, we hardcode a guess."
2020-02-03 19:45:34 +01:00
(aif (gethash url-or-port *ein:notebook-version*) it
2019-11-30 08:46:49 +01:00
(ein:log 'warn "No recorded notebook version for %s" url-or-port)
"5.7.0"))
(defun ein:query-notebook-version (url-or-port callback)
"Send for notebook version of URL-OR-PORT with CALLBACK arity 0 (just a semaphore)"
(ein:query-singleton-ajax
(ein:url url-or-port "api")
:parser #'ein:json-read
:complete (apply-partially #'ein:query-notebook-version--complete url-or-port callback)))
(cl-defun ein:query-notebook-version--complete (url-or-port callback
2020-02-03 19:45:34 +01:00
&key data response
&allow-other-keys
2019-11-30 08:46:49 +01:00
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
(ein:log 'debug "ein:query-notebook-version--complete %s" resp-string)
2020-02-03 19:45:34 +01:00
(aif (plist-get data :version)
2019-11-30 08:46:49 +01:00
(setf (gethash url-or-port *ein:notebook-version*) it)
2020-02-03 19:45:34 +01:00
(case (request-response-status-code response)
2019-11-30 08:46:49 +01:00
(404 (ein:log 'warn "notebook version api not implemented")
(setf (gethash url-or-port *ein:notebook-version*) "2.0.0"))
(t (ein:log 'warn "notebook version currently unknowable"))))
(when callback (funcall callback)))
;;; File name translation (tramp support)
;; Probably it's better to define `ein:filename-translations-get' as
;; an EIEIO method so that I don't have to re-define functions such as
;; `ein:kernel-filename-to-python' and `ein:kernel-filename-from-python'.
(defun ein:filename-translations-get (url-or-port)
(ein:choose-setting 'ein:filename-translations url-or-port))
(defun ein:filename-to-python (url-or-port filename)
2020-02-03 19:45:34 +01:00
(aif (car (ein:filename-translations-get url-or-port))
2019-11-30 08:46:49 +01:00
(funcall it filename)
filename))
(defun ein:filename-from-python (url-or-port filename)
2020-02-03 19:45:34 +01:00
(aif (cadr (ein:filename-translations-get url-or-port))
2019-11-30 08:46:49 +01:00
(funcall it filename)
filename))
(defun ein:make-tramp-file-name (username remote-host python-filename)
2020-02-03 19:45:34 +01:00
(if (>= emacs-major-version 26)
(tramp-make-tramp-file-name "ssh"
2019-11-30 08:46:49 +01:00
username
2020-02-03 19:45:34 +01:00
nil
2019-11-30 08:46:49 +01:00
remote-host
2020-02-03 19:45:34 +01:00
nil
2019-11-30 08:46:49 +01:00
python-filename)
2020-02-03 19:45:34 +01:00
(tramp-make-tramp-file-name "ssh"
2019-11-30 08:46:49 +01:00
username
remote-host
python-filename)))
(defun ein:tramp-create-filename-translator (remote-host &optional username)
"Generate a pair of TO-PYTHON and FROM-PYTHON for
`ein:filename-translations'.
Usage::
(setq ein:filename-translations
`((8888
. ,(ein:tramp-create-filename-translator \"MY-HOSTNAME\"))))
;; Equivalently:
(setq ein:filename-translations
(lambda (url-or-port)
(when (equal url-or-port 8888)
(ein:tramp-create-filename-translator \"MY-HOSTNAME\"))))
This setting assumes that the IPython server which can be
connected using the port 8888 in localhost is actually running in
the host named MY-HOSTNAME.
Adapted from `slime-create-filename-translator'."
(require 'tramp)
2020-02-03 19:45:34 +01:00
(lexical-let ((remote-host remote-host)
(username (or username (user-login-name))))
2019-11-30 08:46:49 +01:00
(list (lambda (emacs-filename)
(tramp-file-name-localname
(tramp-dissect-file-name emacs-filename)))
(lambda (python-filename)
2020-02-03 19:45:34 +01:00
(ein:make-tramp-file-name username remote-host python-filename)))))
2019-11-30 08:46:49 +01:00
;;; Generic getter
(defun ein:generic-getter (func-list)
"Internal function for generic getter functions (`ein:get-*').
FUNC-LIST is a list of function which takes no argument and
return what is desired or nil. Each function in FUNC-LIST is
called one by one and the first non-nil result will be used. The
function is not called when it is not bound. So, it is safe to
give functions defined in lazy-loaded sub-modules.
This is something similar to dispatching in generic function such
as `defgeneric' in EIEIO, but it takes no argument. Actual
implementation is chosen based on context (buffer, point, etc.).
This helps writing generic commands which requires same object
but can operate in different contexts."
(cl-loop for func in func-list
2020-02-03 19:45:34 +01:00
if (and (functionp func) (funcall func))
return it))
2019-11-30 08:46:49 +01:00
(defun ein:get-url-or-port ()
(ein:generic-getter '(ein:get-url-or-port--notebooklist
ein:get-url-or-port--notebook
ein:get-url-or-port--worksheet
2020-02-03 19:45:34 +01:00
ein:get-url-or-port--shared-output)))
2019-11-30 08:46:49 +01:00
(defun ein:get-kernel ()
(ein:generic-getter '(ein:get-kernel--notebook
ein:get-kernel--worksheet
ein:get-kernel--shared-output
2020-02-03 19:45:34 +01:00
ein:get-kernel--connect)))
2019-11-30 08:46:49 +01:00
(defun ein:get-kernel-or-error ()
(or (ein:get-kernel)
(error "No kernel related to the current buffer.")))
(defun ein:get-cell-at-point ()
(ein:generic-getter '(ein:get-cell-at-point--worksheet
ein:get-cell-at-point--shared-output)))
(defun ein:get-traceback-data ()
(ein:generic-getter '(ein:get-traceback-data--worksheet
ein:get-traceback-data--shared-output
ein:get-traceback-data--connect)))
;;; Emacs utilities
(defun ein:clean-compiled-files ()
(let* ((files (directory-files ein:source-dir 'full "^ein-.*\\.elc$")))
(mapc #'delete-file files)
(message "Removed %s byte-compiled files." (length files))))
(defun ein:byte-compile-ein ()
"Byte compile EIN files."
(interactive)
(ein:clean-compiled-files)
(let* ((files (directory-files ein:source-dir 'full "^ein-.*\\.el$"))
2020-02-03 19:45:34 +01:00
(errors (cl-mapcan (lambda (f) (unless (byte-compile-file f) (list f)))
files)))
(aif errors
2019-11-30 08:46:49 +01:00
(error "Got %s errors while compiling these files: %s"
(length errors)
(ein:join-str " " (mapcar #'file-name-nondirectory it))))
(message "Compiled %s files" (length files))))
(provide 'ein-core)
;;; ein-core.el ends here