|
| 1 | +# Async Connect Wrapper Implementation |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This document describes the hybrid wrapper pattern used to implement `snowflake.connector.aio.connect()` that preserves metadata from `SnowflakeConnection.__init__` while supporting both simple await and async context manager usage patterns, with full coroutine protocol support. |
| 6 | + |
| 7 | +## Problem Statement |
| 8 | + |
| 9 | +The synchronous `snowflake.connector.connect()` uses: |
| 10 | +```python |
| 11 | +@wraps(SnowflakeConnection.__init__) |
| 12 | +def Connect(**kwargs) -> SnowflakeConnection: |
| 13 | + return SnowflakeConnection(**kwargs) |
| 14 | +``` |
| 15 | + |
| 16 | +The async version cannot be decorated with `@wraps` on a raw async function. We needed a solution that: |
| 17 | +1. Preserves metadata for IDE introspection and tooling |
| 18 | +2. Supports `conn = await aio.connect(...)` |
| 19 | +3. Supports `async with aio.connect(...) as conn:` |
| 20 | +4. Implements the full coroutine protocol (following aiohttp's pattern) |
| 21 | + |
| 22 | +## Implementation |
| 23 | + |
| 24 | +### Architecture |
| 25 | + |
| 26 | +``` |
| 27 | +connect = _AsyncConnectWrapper() |
| 28 | + ↓ (calls __call__) |
| 29 | +_AsyncConnectContextManager (coroutine wrapper) |
| 30 | + ├─ Coroutine Protocol: send(), throw(), close() |
| 31 | + ├─ __await__(), __iter__() |
| 32 | + └─ __aenter__(), __aexit__() (async context manager) |
| 33 | +``` |
| 34 | + |
| 35 | +### Class: _AsyncConnectContextManager |
| 36 | + |
| 37 | +Makes a coroutine both awaitable and an async context manager, while implementing the full coroutine protocol. |
| 38 | + |
| 39 | +```python |
| 40 | +class _AsyncConnectContextManager: |
| 41 | + """Hybrid wrapper that enables both awaiting and async context manager usage. |
| 42 | +
|
| 43 | + Implements the full coroutine protocol for maximum compatibility. |
| 44 | + """ |
| 45 | + |
| 46 | + __slots__ = ("_coro", "_conn") |
| 47 | + |
| 48 | + def __init__(self, coro: Coroutine[Any, Any, SnowflakeConnection]) -> None: |
| 49 | + self._coro = coro |
| 50 | + self._conn: SnowflakeConnection | None = None |
| 51 | + |
| 52 | + def send(self, arg: Any) -> Any: |
| 53 | + """Send a value into the wrapped coroutine.""" |
| 54 | + return self._coro.send(arg) |
| 55 | + |
| 56 | + def throw(self, *args: Any, **kwargs: Any) -> Any: |
| 57 | + """Throw an exception into the wrapped coroutine.""" |
| 58 | + return self._coro.throw(*args, **kwargs) |
| 59 | + |
| 60 | + def close(self) -> None: |
| 61 | + """Close the wrapped coroutine.""" |
| 62 | + return self._coro.close() |
| 63 | + |
| 64 | + def __await__(self) -> Generator[Any, None, SnowflakeConnection]: |
| 65 | + """Enable: conn = await connect(...)""" |
| 66 | + return self._coro.__await__() |
| 67 | + |
| 68 | + def __iter__(self) -> Generator[Any, None, SnowflakeConnection]: |
| 69 | + """Make the wrapper iterable like a coroutine.""" |
| 70 | + return self.__await__() |
| 71 | + |
| 72 | + async def __aenter__(self) -> SnowflakeConnection: |
| 73 | + """Enable: async with connect(...) as conn:""" |
| 74 | + self._conn = await self._coro |
| 75 | + return await self._conn.__aenter__() |
| 76 | + |
| 77 | + async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None: |
| 78 | + """Exit async context manager.""" |
| 79 | + if self._conn is not None: |
| 80 | + return await self._conn.__aexit__(exc_type, exc, tb) |
| 81 | +``` |
| 82 | + |
| 83 | +#### Coroutine Protocol Methods |
| 84 | + |
| 85 | +- **`send(arg)`**: Send a value into the wrapped coroutine (used for manual coroutine driving) |
| 86 | +- **`throw(*args, **kwargs)`**: Throw an exception into the wrapped coroutine |
| 87 | +- **`close()`**: Gracefully close the wrapped coroutine |
| 88 | +- **`__await__()`**: Return a generator to enable `await` syntax |
| 89 | +- **`__iter__()`**: Make the wrapper iterable (some async utilities require this) |
| 90 | + |
| 91 | +### Class: _AsyncConnectWrapper |
| 92 | + |
| 93 | +Callable wrapper that preserves `SnowflakeConnection.__init__` metadata. |
| 94 | + |
| 95 | +```python |
| 96 | +class _AsyncConnectWrapper: |
| 97 | + """Preserves SnowflakeConnection.__init__ metadata for async connect function. |
| 98 | +
|
| 99 | + This wrapper enables introspection tools and IDEs to see the same signature |
| 100 | + as the synchronous snowflake.connector.connect function. |
| 101 | + """ |
| 102 | + |
| 103 | + def __init__(self) -> None: |
| 104 | + self.__wrapped__ = SnowflakeConnection.__init__ |
| 105 | + self.__name__ = "connect" |
| 106 | + self.__doc__ = SnowflakeConnection.__init__.__doc__ |
| 107 | + self.__module__ = __name__ |
| 108 | + self.__qualname__ = "connect" |
| 109 | + self.__annotations__ = getattr(SnowflakeConnection.__init__, "__annotations__", {}) |
| 110 | + |
| 111 | + @wraps(SnowflakeConnection.__init__) |
| 112 | + def __call__(self, **kwargs: Any) -> _AsyncConnectContextManager: |
| 113 | + """Create and connect to a Snowflake connection asynchronously.""" |
| 114 | + async def _connect_coro() -> SnowflakeConnection: |
| 115 | + conn = SnowflakeConnection(**kwargs) |
| 116 | + await conn.connect() |
| 117 | + return conn |
| 118 | + |
| 119 | + return _AsyncConnectContextManager(_connect_coro()) |
| 120 | +``` |
| 121 | + |
| 122 | +## Usage Patterns |
| 123 | + |
| 124 | +### Pattern 1: Simple Await (No Context Manager) |
| 125 | +```python |
| 126 | +conn = await aio.connect( |
| 127 | + account="myaccount", |
| 128 | + user="myuser", |
| 129 | + password="mypassword" |
| 130 | +) |
| 131 | +result = await conn.cursor().execute("SELECT 1") |
| 132 | +await conn.close() |
| 133 | +``` |
| 134 | + |
| 135 | +**Flow:** |
| 136 | +1. `aio.connect(...)` returns `_AsyncConnectContextManager` |
| 137 | +2. `await` calls `__await__()`, returns inner coroutine result |
| 138 | +3. `conn` is a `SnowflakeConnection` object |
| 139 | +4. Wrapper is garbage collected |
| 140 | + |
| 141 | +### Pattern 2: Async Context Manager |
| 142 | +```python |
| 143 | +async with aio.connect( |
| 144 | + account="myaccount", |
| 145 | + user="myuser", |
| 146 | + password="mypassword" |
| 147 | +) as conn: |
| 148 | + result = await conn.cursor().execute("SELECT 1") |
| 149 | + # Auto-closes on exit |
| 150 | +``` |
| 151 | + |
| 152 | +**Flow:** |
| 153 | +1. `aio.connect(...)` returns `_AsyncConnectContextManager` |
| 154 | +2. `async with` calls `__aenter__()`, awaits coroutine, returns connection |
| 155 | +3. Code block executes |
| 156 | +4. `async with` calls `__aexit__()` on exit |
| 157 | + |
| 158 | +### Pattern 3: Manual Coroutine Driving (Advanced) |
| 159 | +```python |
| 160 | +# For advanced use cases with manual iteration |
| 161 | +coro_wrapper = aio.connect(account="myaccount", user="user", password="pass") |
| 162 | +# Can use send(), throw(), close() methods directly if needed |
| 163 | +``` |
| 164 | + |
| 165 | +## Key Design Decisions |
| 166 | + |
| 167 | +### 1. Metadata Copying in `__init__` |
| 168 | +```python |
| 169 | +self.__wrapped__ = SnowflakeConnection.__init__ |
| 170 | +self.__name__ = "connect" |
| 171 | +# ... |
| 172 | +``` |
| 173 | +**Why:** Allows direct attribute access for introspection before calling `__call__` |
| 174 | + |
| 175 | +### 2. `@wraps` Decorator on `__call__` |
| 176 | +```python |
| 177 | +@wraps(SnowflakeConnection.__init__) |
| 178 | +def __call__(self, **kwargs: Any) -> _AsyncConnectContextManager: |
| 179 | +``` |
| 180 | +**Why:** IDEs and inspection tools that examine `__call__` see correct metadata |
| 181 | + |
| 182 | +### 3. Inner Coroutine Function |
| 183 | +```python |
| 184 | +async def _connect_coro() -> SnowflakeConnection: |
| 185 | + conn = SnowflakeConnection(**kwargs) |
| 186 | + await conn.connect() |
| 187 | + return conn |
| 188 | +``` |
| 189 | +**Why:** Defers connection creation and establishment until await time, not at `connect()` call time |
| 190 | + |
| 191 | +### 4. `__slots__` on Context Manager |
| 192 | +```python |
| 193 | +__slots__ = ("_coro", "_conn") |
| 194 | +``` |
| 195 | +**Why:** Memory efficient, especially when many connections are created |
| 196 | + |
| 197 | +### 5. Full Coroutine Protocol |
| 198 | +Following aiohttp's `_RequestContextManager` pattern, we implement `send()`, `throw()`, and `close()` methods. This ensures: |
| 199 | +- Maximum compatibility with async utilities and libraries |
| 200 | +- Support for manual coroutine driving if needed |
| 201 | +- Proper cleanup and exception handling |
| 202 | + |
| 203 | +## Comparison with aiohttp's _RequestContextManager |
| 204 | + |
| 205 | +Our implementation follows the same proven pattern used by aiohttp: |
| 206 | + |
| 207 | +| Feature | aiohttp | Our Implementation | |
| 208 | +|---------|---------|-------------------| |
| 209 | +| `send()` method | ✓ | ✓ | |
| 210 | +| `throw()` method | ✓ | ✓ | |
| 211 | +| `close()` method | ✓ | ✓ | |
| 212 | +| `__await__()` method | ✓ | ✓ | |
| 213 | +| `__iter__()` method | ✓ | ✓ | |
| 214 | +| `__aenter__()` method | ✓ | ✓ | |
| 215 | +| `__aexit__()` method | ✓ | ✓ | |
| 216 | +| Metadata preservation | N/A | ✓ | |
| 217 | + |
| 218 | +## Behavior Comparison |
| 219 | + |
| 220 | +| Aspect | Pattern 1 | Pattern 2 | Pattern 3 | |
| 221 | +|--------|-----------|-----------|-----------| |
| 222 | +| Syntax | `conn = await aio.connect(...)` | `async with aio.connect(...) as conn:` | Manual iteration | |
| 223 | +| Connection Object | In local scope | In context scope | Via wrapper | |
| 224 | +| Cleanup | Manual (`await conn.close()`) | Automatic (`__aexit__`) | Manual | |
| 225 | +| Metadata Available | Yes | Yes | Yes | |
| 226 | +| Error Handling | Manual try/except | Automatic via context manager | Manual | |
| 227 | +| Use Case | Simple connections | Resource management | Advanced/testing | |
| 228 | + |
| 229 | +## Verification |
| 230 | + |
| 231 | +```python |
| 232 | +# Metadata preservation |
| 233 | +assert connect.__name__ == "connect" |
| 234 | +assert hasattr(connect, "__wrapped__") |
| 235 | +assert callable(connect) |
| 236 | + |
| 237 | +# Return type validation |
| 238 | +result = connect(account="test") |
| 239 | +assert hasattr(result, "__await__") # Awaitable |
| 240 | +assert hasattr(result, "__aenter__") # Async context manager |
| 241 | +assert hasattr(result, "__aexit__") # Async context manager |
| 242 | + |
| 243 | +# Full coroutine protocol |
| 244 | +assert hasattr(result, "send") # Coroutine protocol |
| 245 | +assert hasattr(result, "throw") # Exception injection |
| 246 | +assert hasattr(result, "close") # Cleanup |
| 247 | +assert hasattr(result, "__iter__") # Iteration support |
| 248 | +``` |
| 249 | + |
| 250 | +## File Location |
| 251 | + |
| 252 | +`src/snowflake/connector/aio/__init__.py` |
| 253 | + |
| 254 | +## Backwards Compatibility |
| 255 | + |
| 256 | +- ✅ Existing code using `await aio.connect(...)` works unchanged |
| 257 | +- ✅ New code can use `async with aio.connect(...) as conn:` |
| 258 | +- ✅ Metadata available for IDE tooltips and introspection tools |
| 259 | +- ✅ Signature matches synchronous version in tooling |
| 260 | +- ✅ Full coroutine protocol support for advanced use cases |
| 261 | + |
| 262 | +## Benefits |
| 263 | + |
| 264 | +✅ **Full Coroutine Protocol** - Compatible with all async utilities and libraries |
| 265 | +✅ **Flexible Usage** - Simple await or async context manager patterns |
| 266 | +✅ **Metadata Preservation** - IDE tooltips and introspection support |
| 267 | +✅ **Transparent & Efficient** - Minimal overhead, garbage collected after use |
| 268 | +✅ **Backwards Compatible** - No breaking changes to existing code |
| 269 | +✅ **Battle-tested Pattern** - Follows aiohttp's proven design |
0 commit comments