|
| 1 | +// Bindings for Python asynchronous context managers (the `async with` protocol: |
| 2 | +// __aenter__ / __aexit__). |
| 3 | +// |
| 4 | +// F# `use`/`use!` only supports IDisposable (synchronous Dispose), so there is |
| 5 | +// no built-in way to consume a Python async context manager, and `async with` |
| 6 | +// cannot be expressed directly because F# `task`/`async` cannot `await` inside a |
| 7 | +// `finally`. Cast a value returning such an object to IAsyncContextManager and |
| 8 | +// drive the protocol from a `task { }` by awaiting __aexit__ on the success and |
| 9 | +// error paths (never in a finalizer) — see AsyncContextManager.using. |
| 10 | +namespace Fable.Python.AsyncIO |
| 11 | + |
| 12 | +open System.Threading.Tasks |
| 13 | +open Fable.Core |
| 14 | + |
| 15 | +/// A Python asynchronous context manager: an object implementing `__aenter__` |
| 16 | +/// and `__aexit__`. Bind (or unbox) library values returning such objects to |
| 17 | +/// this interface to drive the `async with` protocol from F#. |
| 18 | +type IAsyncContextManager<'T> = |
| 19 | + /// `__aenter__()` — acquire the resource. Await the result in a `task`. |
| 20 | + [<Emit("$0.__aenter__()")>] |
| 21 | + abstract member AEnter: unit -> Task<'T> |
| 22 | + |
| 23 | + /// `__aexit__(None, None, None)` — release after the body succeeded. |
| 24 | + [<Emit("$0.__aexit__(None, None, None)")>] |
| 25 | + abstract member AExit: unit -> Task<bool> |
| 26 | + |
| 27 | + /// `__aexit__(type(e), e, e.__traceback__)` — release after the body raised. |
| 28 | + /// A truthy result means the exception was handled and should be suppressed. |
| 29 | + [<Emit("$0.__aexit__(type($1), $1, getattr($1, '__traceback__', None))")>] |
| 30 | + abstract member AExit: error: exn -> Task<bool> |
| 31 | + |
| 32 | +[<RequireQualifiedAccess>] |
| 33 | +module AsyncContextManager = |
| 34 | + |
| 35 | + /// Run the body within a Python asynchronous context manager, mirroring |
| 36 | + /// Python's `async with manager as resource: ...`. |
| 37 | + /// |
| 38 | + /// `__aenter__()` is awaited to acquire the resource, `body` is run with it, |
| 39 | + /// and `__aexit__(...)` is awaited afterwards on both the success and error |
| 40 | + /// paths. If the body raises and `__aexit__` returns a truthy value the |
| 41 | + /// exception is suppressed (as `async with` does); otherwise it is re-raised. |
| 42 | + /// |
| 43 | + /// ```fsharp |
| 44 | + /// task { |
| 45 | + /// let! rows = |
| 46 | + /// AsyncContextManager.using (pool.acquire ()) (fun conn -> |
| 47 | + /// task { return! conn.fetch "SELECT 1" }) |
| 48 | + /// return rows |
| 49 | + /// } |
| 50 | + /// ``` |
| 51 | + let using (manager: IAsyncContextManager<'T>) (body: 'T -> Task<'U>) : Task<'U> = |
| 52 | + task { |
| 53 | + let! resource = manager.AEnter() |
| 54 | +
|
| 55 | + try |
| 56 | + let! result = body resource |
| 57 | + let! _ = manager.AExit() |
| 58 | + return result |
| 59 | + with error -> |
| 60 | + let! suppress = manager.AExit(error) |
| 61 | +
|
| 62 | + if suppress then |
| 63 | + return Unchecked.defaultof<'U> |
| 64 | + else |
| 65 | + return raise error |
| 66 | + } |
0 commit comments