diff --git a/CHANGELOG.md b/CHANGELOG.md index 089fb0d9..d62b3155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + * Added `afor` and `awith` macros to support async Python interop (#1179, #1181) + ### Changed * Single arity functions can be tagged with `^:allow-unsafe-names` to preserve their parameter names (#1212) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 09171512..7b948f81 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -4085,6 +4085,63 @@ (finally (println (* 1000 (- (perf-counter) start#)) "msecs"))))) +;;;;;;;;;;;;;;;;;; +;; Async Macros ;; +;;;;;;;;;;;;;;;;;; + +(defmacro afor + "Repeatedly execute ``body`` while the binding name is repeatedly rebound to + successive values from the asynchronous iterable. + + .. warning:: + + The ``afor`` macro may only be used in an asynchronous function context." + [binding & body] + (if (operator/ne 2 (count binding)) + (throw + (ex-info "bindings take the form [name iter]" + {:bindings binding})) + nil) + (let [bound-name (first binding) + iter (second binding)] + `(let [it# (. ~iter ~'__aiter__)] + (loop [val# nil] + (try + (let [~bound-name (await (. it# ~'__anext__)) + result# (do ~@body)] + (recur result#)) + (catch python/StopAsyncIteration _ + val#)))))) + +(defmacro awith + "Evaluate ``body`` within a ``try`` / ``except`` expression, binding the named + expressions as per Python's async context manager protocol spec (Python's + ``async with`` blocks). + + .. warning:: + + The ``awith`` macro may only be used in an asynchronous function context." + [bindings & body] + (let [binding (first bindings) + expr (second bindings)] + `(let [obj# ~expr + ~binding (await (. obj# ~'__aenter__)) + hit-except# (volatile! false)] + (try + (let [res# ~@(if (nthnext bindings 2) + [(concat + (list 'awith (vec (nthrest bindings 2))) + body)] + (list (concat '(do) body)))] + res#) + (catch python/Exception e# + (vreset! hit-except# true) + (when-not (await (. obj# (~'__aexit__ (python/type e#) e# (.- e# ~'__traceback__)))) + (throw e#))) + (finally + (when-not @hit-except# + (await (. obj# (~'__aexit__ nil nil nil))))))))) + ;;;;;;;;;;;;;;;;;;;;;; ;; Threading Macros ;; ;;;;;;;;;;;;;;;;;;;;;; diff --git a/tests/basilisp/test_core_async_macros.lpy b/tests/basilisp/test_core_async_macros.lpy new file mode 100644 index 00000000..85ee2cff --- /dev/null +++ b/tests/basilisp/test_core_async_macros.lpy @@ -0,0 +1,37 @@ +(ns tests.basilisp.test-core-async-macros + (:import asyncio contextlib) + (:require + [basilisp.test :refer [deftest is are testing]])) + +(defn async-to-sync + [f & args] + (let [loop (asyncio/new-event-loop)] + (asyncio/set-event-loop loop) + (.run-until-complete loop (apply f args)))) + +(deftest awith-test + (testing "base case" + (let [get-val (contextlib/asynccontextmanager + (fn ^:async get-val + [] + (yield :async-val))) + val-ctxmgr (fn ^:async yield-val + [] + (awith [v (get-val)] + v))] + (is (= :async-val (async-to-sync val-ctxmgr)))))) + +(deftest afor-test + (testing "base case" + (let [get-vals (fn ^:async get-vals + [] + (dotimes [n 5] + (yield n))) + val-loop (fn ^:async val-loop + [] + (let [a (atom []) + res (afor [v (get-vals)] + (swap! a conj v) + v)] + [@a res]))] + (is (= [[0 1 2 3 4] 4] (async-to-sync val-loop))))))