How It Works: Sync → Async
Scenario: Calling async code from sync context (no event loop running).
Use case: CLI tools, scripts, legacy applications using modern async libraries.
Overview
When you call an async method from sync context, SmartAsync:
Detects no event loop is running
Creates a coroutine from the async method
Executes it with
asyncio.run()Returns the result directly
Execution Flow
flowchart TD
A[User calls method] --> B{Cached async context?}
B -->|No| C[Try get_running_loop]
C -->|RuntimeError| D[Sync context detected]
D --> E[Create coroutine]
E --> F[asyncio.run coroutine]
F --> G[Return result]
style D fill:#f9f,stroke:#333
style F fill:#bbf,stroke:#333
Step-by-Step
1. User calls method (no await)
@smartasync
async def fetch(url: str):
async with httpx.AsyncClient() as client:
return await client.get(url).json()
# Sync context
data = fetch("https://api.example.com") # No await
2. Decorator detects context
try:
asyncio.get_running_loop() # Raises RuntimeError
except RuntimeError:
# We're in sync context
pass
3. Pattern match: (False, True)
match (async_context, async_method):
case (False, True): # Sync context + Async method
coro = method(self, *args, **kwargs)
return asyncio.run(coro) # Execute and return result
4. Execute with asyncio.run()
# asyncio.run() internally:
# 1. Creates new event loop
# 2. Runs coroutine to completion
# 3. Closes loop
# 4. Returns result
5. User receives result
data = fetch(url) # Receives dict, not coroutine
print(data["key"]) # Direct access
Complete Example
from smartasync import smartasync
import httpx
class GitHubClient:
@smartasync
async def get_repo(self, owner: str, repo: str):
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://api.github.com/repos/{owner}/{repo}"
)
return response.json()
# Sync usage (CLI tool)
def main():
client = GitHubClient()
# No await needed - SmartAsync handles it
data = client.get_repo("python", "cpython")
print(f"Stars: {data['stargazers_count']}")
print(f"Forks: {data['forks_count']}")
if __name__ == "__main__":
main()
What Happens Internally
sequenceDiagram
participant User
participant Decorator
participant EventLoop
participant AsyncMethod
User->>Decorator: fetch(url)
Decorator->>Decorator: Check cache (False)
Decorator->>Decorator: get_running_loop() raises
Decorator->>AsyncMethod: Create coroutine
AsyncMethod-->>Decorator: coro object
Decorator->>EventLoop: asyncio.run(coro)
EventLoop->>AsyncMethod: Execute coroutine
AsyncMethod-->>EventLoop: result
EventLoop-->>Decorator: result
Decorator-->>User: result
Performance
Operation |
Time |
Notes |
|---|---|---|
Context detection |
~2μs |
Check for event loop |
asyncio.run() overhead |
~100μs |
Create/destroy loop |
Network request |
~10-200ms |
Actual work |
Total overhead |
~0.05-1% |
Negligible for I/O |
Conclusion: The ~100μs overhead is acceptable because:
Only paid once per CLI invocation
Dominated by network/disk I/O (ms scale)
Alternative (persistent loop) adds complexity
Edge Cases
Nested asyncio.run() (forbidden)
async def outer():
# Inside async context - event loop running
data = fetch(url) # ❌ Would try to nest event loops!
SmartAsync behavior: Detects async context, returns coroutine instead.
Exception propagation
@smartasync
async def buggy():
raise ValueError("Error!")
# Sync context
try:
result = buggy() # Exception raised during asyncio.run()
except ValueError:
print("Caught!") # Works normally
Key Points
✅ Automatic: No asyncio.run() boilerplate needed
✅ Transparent: Exceptions propagate normally
✅ Simple: User doesn’t need to know about event loops
⚠️ Overhead: ~100μs per call (acceptable for I/O operations)