Skip to content

Commit 2eb1460

Browse files
committed
Initial working aio package
0 parents  commit 2eb1460

File tree

5 files changed

+522
-0
lines changed

5 files changed

+522
-0
lines changed

Makefile

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.POSIX:
2+
EMACS = emacs
3+
4+
compile: aio.elc aio-test.elc
5+
6+
aio.elc: aio.el
7+
aio-test.elc: aio-test.el
8+
9+
clean:
10+
rm -f aio.elc aio-test.elc
11+
12+
check: aio.elc aio-test.elc
13+
emacs -Q -nw -L . -l aio-test.elc -f aio-run-tests
14+
15+
.SUFFIXES: .el .elc
16+
.el.elc:
17+
$(EMACS) -batch -Q -L . -f batch-byte-compile $<
18+
19+

README.md

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# aio: async/await for Emacs Lisp
2+
3+
`aio` is to Emacs Lisp as [`asyncio`][asyncio] is to Python. This
4+
package builds upon Emacs 25 generators to provide functions that
5+
pause while they wait on asynchronous events. They do not block any
6+
thread while paused.
7+
8+
## Usage
9+
10+
An async function is defined using `aio-defun` or `aio-lambda`. The
11+
body of such functions can use `aio-await` to pause the function and
12+
wait on a given promise. The function continues with the promise's
13+
resolved value when it's ready. The package provides a number of
14+
functions that return promises, and every async function returns a
15+
promise representing its future return value.
16+
17+
For example:
18+
19+
```el
20+
(aio-defun foo (url)
21+
(aio-await (aio-sleep 3))
22+
(message "Done sleeping. Now fetching %s" url)
23+
(let* ((buffer (aio-await (aio-url-retrieve url)))
24+
(contents (with-current-buffer buffer
25+
(prog1 (buffer-string)
26+
(kill-buffer)))))
27+
(message "Result: %s" contents)))
28+
```
29+
30+
If an uncaught signal terminates an asynchronous function, that signal
31+
is captured by its return value promise and propagated into any
32+
function that awaits on that function.
33+
34+
```el
35+
(aio-defun arith-error-example ()
36+
(/ 0 0))
37+
38+
(aio-defun consumer-example ()
39+
(condition-case error
40+
(aio-await (arith-error-example))
41+
(arith-error (message "Caught %S" error))))
42+
43+
(consumer-example)
44+
;; => #s(aio-promise nil nil)
45+
;; *Messages*: Caught (arith-error)
46+
```
47+
48+
Converting a callback-based function into a promise-returning,
49+
async-friendly function is simple. Create a new promise object with
50+
`aio-promise`, then `aio-resolve` that promise in your callback. To
51+
chain onto an existing promise, use `aio-listen` to attach a new
52+
callback.
53+
54+
## Promise-returning functions
55+
56+
Here are some useful promise-returning — i.e. awaitable — functions
57+
defined by this package.
58+
59+
```el
60+
(aio-sleep seconds &optional result)
61+
;; Return a promise that is resolved after SECONDS with RESULT.
62+
63+
(aio-url-retrieve url &optional silent inhibit-cookies)
64+
;; Wraps `url-retrieve' in a promise.
65+
66+
(aio-process-sentinel process)
67+
;; Return a promise representing the sentinel of PROCESS.
68+
69+
(aio-process-filter process)
70+
;; Return a promise representing the filter of PROCESS.
71+
72+
(aio-select promises)
73+
;; Return a promise that resolves when any in PROMISES resolves.
74+
75+
(aio-all (promises)
76+
;; Return a promise that resolves when all PROMISES are resolved."
77+
```
78+
79+
80+
[asyncio]: https://docs.python.org/3/library/asyncio.html

UNLICENSE

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
This is free and unencumbered software released into the public domain.
2+
3+
Anyone is free to copy, modify, publish, use, compile, sell, or
4+
distribute this software, either in source code form or as a compiled
5+
binary, for any purpose, commercial or non-commercial, and by any
6+
means.
7+
8+
In jurisdictions that recognize copyright laws, the author or authors
9+
of this software dedicate any and all copyright interest in the
10+
software to the public domain. We make this dedication for the benefit
11+
of the public at large and to the detriment of our heirs and
12+
successors. We intend this dedication to be an overt act of
13+
relinquishment in perpetuity of all present and future rights to this
14+
software under copyright law.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.
23+
24+
For more information, please refer to <http://unlicense.org/>

aio-test.el

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
;;; aio-tests.el --- async unit test suite for aio -*- lexical-binding: t; -*-
2+
3+
;;; Commentary:
4+
5+
;; emacs -Q -nw -L . -l aio-test.elc -f aio-run-tests
6+
7+
;; Because the tests run as async functions, the test suite cannot be
8+
;; run in batch mode. The results will be written into a buffer and
9+
;; Emacs will be left running so you can see the results.
10+
11+
;;; Code:
12+
13+
(require 'aio)
14+
(require 'cl-lib)
15+
16+
(defvar aio-tests ())
17+
18+
(defvar aio-test-total nil)
19+
(defvar aio-test-failures nil)
20+
21+
(defun aio-run-tests ()
22+
(setf aio-test-total 0
23+
aio-test-failures 0)
24+
(let* ((buffer (get-buffer-create "*aio-results*"))
25+
(promises
26+
(cl-loop for (name . test) in (reverse aio-tests)
27+
for promise = (funcall test)
28+
for cb =
29+
(let ((test-name name))
30+
(lambda (value)
31+
(insert (format "%S: %S\n" test-name (funcall value)))))
32+
collect promise
33+
do (aio-listen (aio-catch promise) cb)))
34+
(done (aio-all promises)))
35+
(switch-to-buffer buffer)
36+
(erase-buffer)
37+
(aio-listen done (lambda (_)
38+
(with-current-buffer buffer
39+
(insert "*** aio-run-tests complete ***\n")
40+
(insert (format "%d / %d PASS\n"
41+
(- aio-test-total aio-test-failures)
42+
aio-test-total)))))))
43+
44+
(defun aio--should (result value-a value-b expr-a expr-b)
45+
(cl-incf aio-test-total)
46+
(unless result
47+
(cl-incf aio-test-failures)
48+
(insert (format "FAIL:\n%S\n= %S\n%S\n= %S\n"
49+
expr-a value-a expr-b value-b))))
50+
51+
(defmacro aio-should (cmp expr-a expr-b)
52+
(let ((value-a (make-symbol "a"))
53+
(value-b (make-symbol "b")))
54+
`(let ((,value-a ,expr-a)
55+
(,value-b ,expr-b))
56+
(aio--should (,cmp ,value-a ,value-b)
57+
,value-a ,value-b ',expr-a ',expr-b))))
58+
59+
(defmacro aio-deftest (name _ &rest body)
60+
(declare (indent defun))
61+
`(push (cons ',name (aio-lambda () ,@body)) aio-tests))
62+
63+
;; Tests:
64+
65+
(aio-deftest sleep ()
66+
(let ((start (float-time)))
67+
(dotimes (i 3)
68+
(aio-should eql i (aio-await (aio-sleep 0.5 i))))
69+
(aio-should > (- (float-time) start) 1.4)))
70+
71+
(aio-deftest chain ()
72+
(let ((sub (aio-lambda (result) (aio-await (aio-sleep .1 result)))))
73+
(aio-should eq :a (aio-await (funcall sub :a)))
74+
(aio-should eq :b (aio-await (funcall sub :b)))))
75+
76+
(aio-deftest timeout ()
77+
(let ((sleep (aio-sleep 1 t)))
78+
(prog1 nil
79+
(aio-should equal
80+
'(:error aio-timeout . 0.5)
81+
(aio-await (aio-catch (aio-timeout sleep 0.5)))))))
82+
83+
(defun aio-test--shuffle (values)
84+
"Return a shuffled copy of VALUES."
85+
(let ((v (vconcat values)))
86+
(cl-loop for i from (1- (length v)) downto 1
87+
for j = (cl-random (+ i 1))
88+
do (cl-rotatef (aref v i) (aref v j))
89+
finally return (append v nil))))
90+
91+
(aio-deftest sleep-sort ()
92+
(let* ((values (cl-loop for i from 5 to 60
93+
collect (/ i 20.0) into values
94+
finally return (aio-test--shuffle values)))
95+
(promises (cl-loop for value in values
96+
collect (aio-sleep value value)))
97+
(last 0.0))
98+
(while promises
99+
(let ((promise (aio-await (aio-select promises))))
100+
(setf promises (delq promise promises))
101+
(let ((result (aio-await promise)))
102+
(aio-should > result last)
103+
(setf last result))))))
104+
105+
(aio-deftest process-sentinel ()
106+
(let ((process (start-process-shell-command "test" nil "")))
107+
(aio-should equal
108+
"finished\n"
109+
(aio-await (aio-process-sentinel process)))))
110+
111+
(aio-deftest process-filter ()
112+
(let ((process (start-process-shell-command
113+
"test" nil "echo a b c; sleep 1; echo 1 2 3; sleep 1")))
114+
(aio-should equal
115+
"a b c\n"
116+
(aio-await (aio-process-filter process)))
117+
(aio-should equal
118+
"1 2 3\n"
119+
(aio-await (aio-process-filter process)))
120+
(aio-should equal
121+
nil
122+
(aio-await (aio-process-filter process)))))

0 commit comments

Comments
 (0)