Skip to content

Commit 88cfb0b

Browse files
authored
Support async for and async with constructs using macros (#1226)
Fixes #1179 Fixes #1181
1 parent 7108212 commit 88cfb0b

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
* Added `afor` and `awith` macros to support async Python interop (#1179, #1181)
10+
811
### Changed
912
* Single arity functions can be tagged with `^:allow-unsafe-names` to preserve their parameter names (#1212)
1013

src/basilisp/core.lpy

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4085,6 +4085,63 @@
40854085
(finally
40864086
(println (* 1000 (- (perf-counter) start#)) "msecs")))))
40874087

4088+
;;;;;;;;;;;;;;;;;;
4089+
;; Async Macros ;;
4090+
;;;;;;;;;;;;;;;;;;
4091+
4092+
(defmacro afor
4093+
"Repeatedly execute ``body`` while the binding name is repeatedly rebound to
4094+
successive values from the asynchronous iterable.
4095+
4096+
.. warning::
4097+
4098+
The ``afor`` macro may only be used in an asynchronous function context."
4099+
[binding & body]
4100+
(if (operator/ne 2 (count binding))
4101+
(throw
4102+
(ex-info "bindings take the form [name iter]"
4103+
{:bindings binding}))
4104+
nil)
4105+
(let [bound-name (first binding)
4106+
iter (second binding)]
4107+
`(let [it# (. ~iter ~'__aiter__)]
4108+
(loop [val# nil]
4109+
(try
4110+
(let [~bound-name (await (. it# ~'__anext__))
4111+
result# (do ~@body)]
4112+
(recur result#))
4113+
(catch python/StopAsyncIteration _
4114+
val#))))))
4115+
4116+
(defmacro awith
4117+
"Evaluate ``body`` within a ``try`` / ``except`` expression, binding the named
4118+
expressions as per Python's async context manager protocol spec (Python's
4119+
``async with`` blocks).
4120+
4121+
.. warning::
4122+
4123+
The ``awith`` macro may only be used in an asynchronous function context."
4124+
[bindings & body]
4125+
(let [binding (first bindings)
4126+
expr (second bindings)]
4127+
`(let [obj# ~expr
4128+
~binding (await (. obj# ~'__aenter__))
4129+
hit-except# (volatile! false)]
4130+
(try
4131+
(let [res# ~@(if (nthnext bindings 2)
4132+
[(concat
4133+
(list 'awith (vec (nthrest bindings 2)))
4134+
body)]
4135+
(list (concat '(do) body)))]
4136+
res#)
4137+
(catch python/Exception e#
4138+
(vreset! hit-except# true)
4139+
(when-not (await (. obj# (~'__aexit__ (python/type e#) e# (.- e# ~'__traceback__))))
4140+
(throw e#)))
4141+
(finally
4142+
(when-not @hit-except#
4143+
(await (. obj# (~'__aexit__ nil nil nil)))))))))
4144+
40884145
;;;;;;;;;;;;;;;;;;;;;;
40894146
;; Threading Macros ;;
40904147
;;;;;;;;;;;;;;;;;;;;;;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
(ns tests.basilisp.test-core-async-macros
2+
(:import asyncio contextlib)
3+
(:require
4+
[basilisp.test :refer [deftest is are testing]]))
5+
6+
(defn async-to-sync
7+
[f & args]
8+
(let [loop (asyncio/new-event-loop)]
9+
(asyncio/set-event-loop loop)
10+
(.run-until-complete loop (apply f args))))
11+
12+
(deftest awith-test
13+
(testing "base case"
14+
(let [get-val (contextlib/asynccontextmanager
15+
(fn ^:async get-val
16+
[]
17+
(yield :async-val)))
18+
val-ctxmgr (fn ^:async yield-val
19+
[]
20+
(awith [v (get-val)]
21+
v))]
22+
(is (= :async-val (async-to-sync val-ctxmgr))))))
23+
24+
(deftest afor-test
25+
(testing "base case"
26+
(let [get-vals (fn ^:async get-vals
27+
[]
28+
(dotimes [n 5]
29+
(yield n)))
30+
val-loop (fn ^:async val-loop
31+
[]
32+
(let [a (atom [])
33+
res (afor [v (get-vals)]
34+
(swap! a conj v)
35+
v)]
36+
[@a res]))]
37+
(is (= [[0 1 2 3 4] 4] (async-to-sync val-loop))))))

0 commit comments

Comments
 (0)