Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
57 changes: 57 additions & 0 deletions src/basilisp/core.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -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 ;;
;;;;;;;;;;;;;;;;;;;;;;
Expand Down
37 changes: 37 additions & 0 deletions tests/basilisp/test_core_async_macros.lpy
Original file line number Diff line number Diff line change
@@ -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))))))