|
| 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 | + /// Acquire the resource, run the body, and await `__aexit__` exactly once. |
| 36 | + /// Returns `Ok result` when the body succeeds, or `Error (error, suppress)` |
| 37 | + /// when it raises — where `suppress` is the truthy/falsy value `__aexit__` |
| 38 | + /// returned for that error. |
| 39 | + /// |
| 40 | + /// The body's outcome is captured in an inner `task` so that `__aexit__` is |
| 41 | + /// awaited *outside* the `try`. Awaiting the success-path `__aexit__` inside |
| 42 | + /// the `try` would route an exception it raises into the error branch and |
| 43 | + /// call `__aexit__` a second time — Python's `async with` never does this. |
| 44 | + let private run (manager: IAsyncContextManager<'T>) (body: 'T -> Task<'U>) : Task<Result<'U, exn * bool>> = |
| 45 | + task { |
| 46 | + let! resource = manager.AEnter() |
| 47 | + |
| 48 | + let! outcome = |
| 49 | + task { |
| 50 | + try |
| 51 | + let! result = body resource |
| 52 | + return Ok result |
| 53 | + with error -> |
| 54 | + return Error error |
| 55 | + } |
| 56 | + |
| 57 | + match outcome with |
| 58 | + | Ok result -> |
| 59 | + let! _ = manager.AExit() |
| 60 | + return Ok result |
| 61 | + | Error error -> |
| 62 | + let! suppress = manager.AExit(error) |
| 63 | + return Error(error, suppress) |
| 64 | + } |
| 65 | + |
| 66 | + /// Run the body within a Python asynchronous context manager, mirroring |
| 67 | + /// Python's `async with manager as resource: ...`. |
| 68 | + /// |
| 69 | + /// `__aenter__()` is awaited to acquire the resource, `body` is run with it, |
| 70 | + /// and `__aexit__(...)` is awaited afterwards on both the success and error |
| 71 | + /// paths. If the body raises, the exception is **always re-raised** after |
| 72 | + /// `__aexit__` runs — a truthy `__aexit__` return is ignored so the result |
| 73 | + /// type stays a plain `'U`. Use `tryUsing` if you need to honor suppression. |
| 74 | + /// |
| 75 | + /// ```fsharp |
| 76 | + /// task { |
| 77 | + /// let! rows = |
| 78 | + /// AsyncContextManager.using (pool.acquire ()) (fun conn -> |
| 79 | + /// task { return! conn.fetch "SELECT 1" }) |
| 80 | + /// return rows |
| 81 | + /// } |
| 82 | + /// ``` |
| 83 | + let using (manager: IAsyncContextManager<'T>) (body: 'T -> Task<'U>) : Task<'U> = |
| 84 | + task { |
| 85 | + let! outcome = run manager body |
| 86 | +
|
| 87 | + match outcome with |
| 88 | + | Ok result -> return result |
| 89 | + | Error(error, _) -> return raise error |
| 90 | + } |
| 91 | +
|
| 92 | + /// Like `using`, but honors `__aexit__`'s suppression signal — matching the |
| 93 | + /// full `async with` contract. |
| 94 | + /// |
| 95 | + /// Returns `Some result` when the body succeeds. If the body raises and |
| 96 | + /// `__aexit__` returns a truthy value (the exception is handled), returns |
| 97 | + /// `None`; otherwise the exception is re-raised. |
| 98 | + let tryUsing (manager: IAsyncContextManager<'T>) (body: 'T -> Task<'U>) : Task<'U option> = |
| 99 | + task { |
| 100 | + let! outcome = run manager body |
| 101 | +
|
| 102 | + match outcome with |
| 103 | + | Ok result -> return Some result |
| 104 | + | Error(error, suppress) -> |
| 105 | + if suppress then |
| 106 | + return None |
| 107 | + else |
| 108 | + return raise error |
| 109 | + } |
0 commit comments