Skip to content

Commit 085a188

Browse files
committed
Streamline login
Merge the login and open commands (open aliased to login). Add login tests described in #352. Attempt to improve user experience by synchronously executing `ein:jupyter-server-start`. `ein:dev-prefer-deferred` custom variable allows easy switch to compare sychronous versus old asynchronous behavior.
1 parent 81ea6b2 commit 085a188

24 files changed

+753
-465
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ before_script:
3737
- sh tools/install-cask.sh
3838

3939
script:
40-
- make test || ( ( zip -q - log/{testein,testfunc,ecukes}.* 2>/dev/null | uuencode log.zip ) && false )
40+
- make test || ( ( zip -q - log/{testein,testfunc,ecukes}.* 2>/dev/null | uuencode log.zip ) && ( printf "To diagnose, travis logs -i | dos2unix | sed '/^begin 644/,/^end/!d' | uudecode" ) && false )

Makefile

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ IPY_VERSION = 5.8.0
44
SRC=$(shell cask files)
55
ELCFILES = $(SRC:.el=.elc)
66

7-
.PHONY: loaddefs
8-
loaddefs:
7+
.PHONY: autoloads
8+
autoloads:
99
sh tools/update-autoloads.sh
1010

1111
.PHONY: clean
@@ -16,15 +16,21 @@ env-ipy.%:
1616
tools/makeenv.sh env/ipy.$* tools/requirement-ipy.$*.txt
1717

1818
.PHONY: test-compile
19-
test-compile: clean
19+
test-compile: clean autoloads
2020
! ( cask build 2>&1 | awk '{if (/^ /) { gsub(/^ +/, " ", $$0); printf "%s", $$0 } else { printf "\n%s", $$0 }}' | egrep "not known|Error|free variable" )
2121
-cask clean-elc
2222

23+
.PHONY: quick
24+
quick: test-compile test-unit
25+
2326
.PHONY: test-no-build
24-
test-no-build: test-unit test-int
27+
test-no-build: test-unit test-int autoloads
28+
29+
.PHONY: quick
30+
quick: test-compile test-unit
2531

2632
.PHONY: test
27-
test: test-compile test-unit test-int
33+
test: quick test-int
2834

2935
.PHONY: test-int
3036
test-int:

features/login.feature

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@login
2+
Scenario: Logging into nowhere
3+
Given I login to 0
4+
Then I should see message "demoted: (error Connection refused: [error] http://127.0.0.1:0)"

features/notebooklist.feature

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,30 @@ Scenario: Global notebooks
3434
And I switch to log expr "ein:log-all-buffer-name"
3535
Then I should see "Opened notebook"
3636

37-
@foo
38-
Scenario: notebooklist-open works interactively (should be same notebooklist as server-start)
39-
Given I am in buffer "*scratch*"
40-
When I clear log expr "ein:log-all-buffer-name"
37+
@login
38+
Scenario: No token server
39+
Given I start the server configured "c.NotebookApp.token = u''\n"
40+
And I switch to log expr "ein:log-all-buffer-name"
41+
Then I should not see "[warn]"
42+
And I should not see "[error]"
43+
44+
@login
45+
Scenario: With token server
46+
Given I start the server configured "\n"
4147
And I login if necessary
42-
And I open notebooklist
43-
And I wait for the smoke to clear
4448
And I switch to log expr "ein:log-all-buffer-name"
4549
Then I should not see "[warn]"
4650
And I should not see "[error]"
51+
52+
@login
53+
Scenario: With password server
54+
Given I start the server configured "c.NotebookApp.password=u'sha1:712118ed6c09:bc02227d84b76b720cc320b855e1006d0b120f98'\n"
55+
And I login with password "foo"
56+
And I switch to log expr "ein:log-all-buffer-name"
57+
Then I should not see "[warn]"
58+
And I should not see "[error]"
59+
60+
@login
61+
Scenario: Logging into nowhere
62+
Given I login to 0
63+
Then I should see message "demoted: (error Connection refused: [error] http://127.0.0.1:0)"

features/step-definitions/ein-steps.el

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
(When "^I clear log expr \"\\(.+\\)\"$"
22
(lambda (log-expr)
3-
(with-current-buffer (symbol-value (intern log-expr))
4-
(let ((inhibit-read-only t))
5-
(erase-buffer)))))
3+
(let ((buffer (get-buffer (symbol-value (intern log-expr)))))
4+
(when (buffer-live-p buffer)
5+
(with-current-buffer buffer
6+
(let ((inhibit-read-only t))
7+
(erase-buffer)))))))
68

79
(When "^I switch to log expr \"\\(.+\\)\"$"
810
(lambda (log-expr)
@@ -15,40 +17,76 @@
1517
(sit-for 0.8)
1618
)))
1719

18-
(When "^I wait \\([.0-9]+\\) seconds$"
20+
(When "^I wait \\([.0-9]+\\) seconds?$"
1921
(lambda (seconds)
2022
(sit-for (string-to-number seconds))))
2123

2224
(When "^I am in log buffer$"
2325
(lambda ()
2426
(switch-to-buffer ein:log-all-buffer-name)))
2527

28+
(defun ein:testing-new-notebook (url-or-port ks)
29+
(lexical-let (notebook)
30+
(condition-case err
31+
(progn
32+
(ein:notebooklist-new-notebook url-or-port ks nil
33+
(lambda (nb created &rest ignore)
34+
(setq notebook nb)))
35+
(ein:testing-wait-until (lambda ()
36+
(and notebook
37+
(ein:aand (ein:$notebook-kernel notebook)
38+
(ein:kernel-live-p it))))
39+
nil 10000 2000)
40+
notebook)
41+
(error (message "ein:testing-new-notebook: %s" (error-message-string err))
42+
(when notebook
43+
(ein:notebook-close notebook))
44+
nil))))
45+
2646
(When "^new \\(.+\\) notebook$"
2747
(lambda (kernel)
2848
(multiple-value-bind (url-or-port token) (ein:jupyter-server-conn-info)
29-
(lexical-let ((ks (ein:get-kernelspec url-or-port kernel)) notebook)
30-
(ein:notebooklist-new-notebook url-or-port ks nil
31-
(lambda (nb created &rest ignore)
32-
(setq notebook nb)))
33-
(ein:testing-wait-until (lambda () (and notebook
34-
(ein:aand (ein:$notebook-kernel notebook)
35-
(ein:kernel-live-p it))))
36-
nil 10000 2000)
37-
(let ((buf-name (format ein:notebook-buffer-name-template
38-
(ein:$notebook-url-or-port notebook)
39-
(ein:$notebook-notebook-name notebook))))
40-
(switch-to-buffer buf-name)
41-
(Then "I should be in buffer \"%s\"" buf-name))))))
42-
43-
(When "^I open notebooklist"
44-
(lambda ()
45-
(multiple-value-bind (url-or-port token) (ein:jupyter-server-conn-info)
46-
(cl-letf (((symbol-function 'ein:notebooklist-ask-url-or-port)
47-
(lambda (&rest args) url-or-port)))
48-
(When "I call \"ein:notebooklist-open\"")
49+
(with-current-buffer (ein:notebooklist-get-buffer url-or-port)
50+
(lexical-let ((ks (ein:get-kernelspec url-or-port kernel)) notebook)
51+
(loop repeat 2
52+
until notebook
53+
do (setq notebook (ein:testing-new-notebook url-or-port ks)))
54+
(let ((buf-name (format ein:notebook-buffer-name-template
55+
(ein:$notebook-url-or-port notebook)
56+
(ein:$notebook-notebook-name notebook))))
57+
(switch-to-buffer buf-name)
58+
(Then "I should be in buffer \"%s\"" buf-name)))))))
59+
60+
(When "^I start \\(and login to \\)?the server configured \"\\(.*\\)\"$"
61+
(lambda (login config)
62+
(cl-letf (((symbol-function 'y-or-n-p) #'ignore))
63+
(ein:jupyter-server-stop t))
64+
(loop repeat 10
65+
with buffer = (get-buffer ein:jupyter-server-buffer-name)
66+
until (null (get-buffer-process buffer))
67+
do (sleep-for 1)
68+
finally do (ein:aif (get-buffer-process buffer) (delete-process it)))
69+
(When "I clear log expr \"ein:log-all-buffer-name\"")
70+
(When "I clear log expr \"ein:jupyter-server-buffer-name\"")
71+
(clrhash ein:notebooklist-map)
72+
(with-temp-file ".ecukes-temp-config.py" (insert (s-replace "\\n" "\n" config)))
73+
(setq ein:jupyter-server-args '("--no-browser" "--debug" "--config=.ecukes-temp-config.py"))
74+
(ein:jupyter-server-start (executable-find "jupyter")
75+
ein:testing-jupyter-server-root (not login))
76+
(if login
77+
(ein:testing-wait-until (lambda () (ein:notebooklist-list)) nil 20000 1000))))
78+
79+
(When "^I login to \\([.0-9]+\\)$"
80+
(lambda (port)
81+
(cl-letf (((symbol-function 'ein:notebooklist-ask-url-or-port)
82+
(lambda (&rest args) (ein:url port)))
83+
((symbol-function 'read-passwd)
84+
(lambda (&rest args) "foo")))
85+
(with-demoted-errors "demoted: %s"
86+
(When "I call \"ein:notebooklist-login\"")
4987
(And "I wait for the smoke to clear")))))
5088

51-
(When "^I login if necessary"
89+
(When "^I login if necessary$"
5290
(lambda ()
5391
(multiple-value-bind (url-or-port token) (ein:jupyter-server-conn-info)
5492
(when token
@@ -59,15 +97,15 @@
5997
(When "I call \"ein:notebooklist-login\"")
6098
(And "I wait for the smoke to clear"))))))
6199

62-
(When "^I wait for the smoke to clear"
63-
(lambda ()
64-
(ein:testing-flush-queries)))
65-
66-
(When "^I enter the prevailing port"
67-
(lambda ()
100+
(When "^I login with password \"\\(.+\\)\"$"
101+
(lambda (password)
68102
(multiple-value-bind (url-or-port token) (ein:jupyter-server-conn-info)
69-
(let ((parsed-url (url-generic-parse-url url-or-port)))
70-
(When "I type \"%d\"") (url-port parsed-url)))))
103+
(cl-letf (((symbol-function 'ein:notebooklist-ask-url-or-port)
104+
(lambda (&rest args) url-or-port))
105+
((symbol-function 'read-passwd)
106+
(lambda (&rest args) password)))
107+
(When "I call \"ein:notebooklist-login\"")
108+
(And "I wait for the smoke to clear")))))
71109

72110
(When "^I wait for the smoke to clear"
73111
(lambda ()

features/support/env.el

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@
1414
(require 'ein-testing)
1515

1616
(defvar ein:testing-jupyter-server-root (f-parent (f-dirname load-file-name)))
17-
(ein:deflocal ein:%testing-port% nil)
1817

1918
(defun ein:testing-after-scenario ()
2019
(ein:testing-flush-queries)
21-
(with-current-buffer (ein:notebooklist-get-buffer ein:%testing-url%)
20+
(with-current-buffer (ein:notebooklist-get-buffer (car (ein:jupyter-server-conn-info)))
2221
(let ((urlport (ein:$notebooklist-url-or-port ein:%notebooklist%)))
2322
(loop for notebook in (ein:notebook-opened-notebooks)
2423
for path = (ein:$notebook-notebook-path notebook)
@@ -35,20 +34,15 @@
3534
(setq ein:testing-dump-file-messages (concat default-directory "log/ecukes.messages"))
3635
(setq ein:testing-dump-file-server (concat default-directory "log/ecukes.server"))
3736
(setq ein:testing-dump-file-request (concat default-directory "log/ecukes.request"))
38-
(setq ein:jupyter-server-args '("--no-browser" "--debug"))
39-
(setq ein:%testing-url% nil)
40-
(deferred:sync! (ein:jupyter-server-start (executable-find "jupyter") ein:testing-jupyter-server-root))
41-
(ein:testing-wait-until (lambda () (ein:notebooklist-list)) nil 20000 1000)
42-
(assert (processp %ein:jupyter-server-session%) t "notebook server defunct")
43-
(setq ein:%testing-url% (car (ein:jupyter-server-conn-info))))
37+
(Given "I start and login to the server configured \"\\n\"")
38+
)
4439

4540
(After
4641
(ein:testing-after-scenario))
4742

4843
(Teardown
4944
(cl-letf (((symbol-function 'y-or-n-p) #'ignore))
50-
(ein:jupyter-server-stop t))
51-
(assert (not (processp %ein:jupyter-server-session%)) t "notebook server orphaned"))
45+
(ein:jupyter-server-stop t)))
5246

5347
(Fail
5448
(if noninteractive

lisp/ein-core.el

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ Notebook server."
6666
(const :tag "First value of `ein:url-or-port'" nil))
6767
:group 'ein)
6868

69+
(defcustom ein:jupyter-default-server-command "jupyter"
70+
"The default command to start a jupyter notebook server.
71+
72+
It is used when the `ein:jupyter-server-start' command is
73+
interactively called."
74+
:group 'ein
75+
:type '(file))
76+
6977
(defcustom ein:filename-translations nil
7078
"Convert file paths between Emacs and Python process.
7179
@@ -136,6 +144,44 @@ the source is in git repository."
136144
(defvar *ein:kernelspecs* (make-hash-table :test #'equal)
137145
"url-or-port to kernelspecs")
138146

147+
(defun ein:need-password-required (url-or-port)
148+
"Synchronously test whether URL-OR-PORT requires a password."
149+
(lexical-let* (answer
150+
done-p
151+
error-p
152+
(callback1 (lambda (ans) (setf answer ans) (setf done-p t)))
153+
(callback-err (lambda (status) (setf error-p status))))
154+
(ein:query-singleton-ajax
155+
(list 'need-password-required url-or-port)
156+
(ein:url url-or-port "login")
157+
;; request--safe-apply swallowing errors -- so that's why error-p
158+
:error (apply-partially #'ein:need-password-required--error url-or-port callback-err)
159+
:type "POST"
160+
:parser #'ignore
161+
:success (apply-partially #'ein:need-password-required--success url-or-port callback1)
162+
:complete (apply-partially #'ein:need-password-required--complete url-or-port))
163+
(loop repeat 30
164+
until done-p
165+
if error-p
166+
do (error "Connection refused: [%s] %s" error-p url-or-port)
167+
end
168+
do (sleep-for 0 300))
169+
answer))
170+
171+
(defun* ein:need-password-required--error (url-or-port callback &key symbol-status &allow-other-keys)
172+
(funcall callback symbol-status))
173+
174+
(defun* ein:need-password-required--success (url-or-port callback &key data response &allow-other-keys)
175+
(when callback
176+
(let ((explicitly-dont-need
177+
(ein:aand (request-response-status-code response) (= 405 it))))
178+
(funcall callback (not explicitly-dont-need)))))
179+
180+
(defun* ein:need-password-required--complete (url-or-port &key data response
181+
&allow-other-keys
182+
&aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
183+
(ein:log 'debug "ein:need-password-required--complete %s" resp-string))
184+
139185
(defun ein:need-kernelspecs (url-or-port)
140186
"Callers assume ein:query-kernelspecs succeeded. If not, nil."
141187
(ein:aif (gethash url-or-port *ein:kernelspecs*) it

lisp/ein-dev.el

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
(require 'ein-notebook)
3333
(require 'ein-subpackages)
3434

35+
(defcustom ein:dev-prefer-deferred nil
36+
"Deferred chains have unpredictable and often delayed timings. For some user interactions, it may be preferable to act synchronously."
37+
:group 'ein
38+
:type 'boolean)
39+
3540
;;;###autoload
3641
(defun ein:dev-insert-mode-map (map-string)
3742
"Insert mode-map into rst document. For README.rst."

lisp/ein-ipynb-mode.el

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626

2727
;;; Code:
2828

29-
(require 'ein-notebooklist)
30-
29+
(require 'ein-process)
3130

3231
(defvar ein:ipynb-parent-mode 'js-mode
3332
"A mode (a symbol) to use for parent mode for `ein:ipynb-mode'.
@@ -39,13 +38,15 @@ Note that this variable must be set *before* compiling EIN.")
3938
(define-derived-mode ein:ipynb-mode ein:ipynb-parent-mode "ein:ipynb"
4039
"A simple mode for ipynb file.")
4140

41+
;; TODO replace ignore with ein:process-find-file-callback in the
42+
;; eagerly awaited "without-notebooklist" branch
4243
(let ((map ein:ipynb-mode-map))
43-
(define-key map "\C-c\C-z" 'ein:notebooklist-open-notebook-by-file-name)
44-
(define-key map "\C-c\C-o" 'ein:notebooklist-open-notebook-by-file-name)
44+
(define-key map "\C-c\C-z" 'ignore)
45+
(define-key map "\C-c\C-o" 'ignore)
4546
(easy-menu-define ein:ipynb-menu map "EIN IPyNB Mode Menu"
4647
`("EIN IPyNB File"
4748
,@(ein:generate-menu
48-
'(("Open notebook" ein:notebooklist-open-notebook-by-file-name))))))
49+
'(("Open notebook" ignore))))))
4950

5051
;;;###autoload
5152
(add-to-list 'auto-mode-alist '(".*\\.ipynb\\'" . ein:ipynb-mode))

lisp/ein-junk.el

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,33 @@ format string which can be passed to `format-time-string'."
4242
`ein:junk-notebook-name-template'."
4343
(format-time-string ein:junk-notebook-name-template (current-time)))
4444

45+
;;;###autoload
46+
(defun ein:junk-new (name kernelspec url-or-port)
47+
"Open a notebook to try random thing.
48+
Notebook name is determined based on
49+
`ein:junk-notebook-name-template'.
50+
51+
When prefix argument is given, it asks URL or port to use."
52+
(interactive (let* ((name (ein:junk-notebook-name))
53+
(url-or-port (or (ein:get-url-or-port)
54+
(ein:default-url-or-port)))
55+
(kernelspec (completing-read
56+
"Select kernel [default]: "
57+
(ein:list-available-kernels url-or-port) nil t nil nil "default" nil)))
58+
(setq name (read-string "Open notebook as: " name))
59+
(when current-prefix-arg
60+
(setq url-or-port (ein:notebooklist-ask-url-or-port)))
61+
(list name url-or-port)
62+
(ein:notebooklist-new-notebook-with-name name kernelspec url-or-port))))
63+
64+
;;;###autoload
65+
(defun ein:junk-rename (name)
66+
"Rename the current notebook based on `ein:junk-notebook-name-template'
67+
and save it immediately."
68+
(interactive
69+
(list (read-string "Rename notebook: "
70+
(ein:junk-notebook-name))))
71+
(ein:notebook-rename-command name))
4572

4673
(define-obsolete-function-alias 'ein:notebooklist-new-scratch-notebook
4774
'ein:junk-new)

0 commit comments

Comments
 (0)