# SmartAsync
**Unified sync/async API decorator with automatic context detection**
[](https://pypi.org/project/smartasync/)
[](https://github.com/genropy/smartasync/actions/workflows/test.yml)
[](https://codecov.io/gh/genropy/smartasync)
[](https://smartasync.readthedocs.io/en/latest/?badge=latest)
[](https://www.python.org/downloads/)
[](https://opensource.org/licenses/MIT)
[](https://github.com/softwell/genro-libs)
[](llm-docs/)
SmartAsync allows you to write async methods once and call them in both sync and async contexts without modification. It automatically detects the execution context and adapts accordingly.
## Features
- ✅ **Automatic context detection**: Detects sync vs async execution context at runtime
- ✅ **Zero configuration**: Just apply the `@smartasync` decorator
- ✅ **Asymmetric caching**: Smart caching strategy for optimal performance
- ✅ **Compatible with `__slots__`**: Works with memory-optimized classes
- ✅ **Pure Python**: No dependencies beyond standard library
## Installation
```bash
pip install smartasync
```
## Quick Start
```python
from smartasync import smartasync
import asyncio
class DataManager:
@smartasync
async def fetch_data(self, url: str):
"""Fetch data - works in both sync and async contexts!"""
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
# Sync context - no await needed
manager = DataManager()
data = manager.fetch_data("https://api.example.com/data")
# Async context - use await
async def main():
manager = DataManager()
data = await manager.fetch_data("https://api.example.com/data")
asyncio.run(main())
```
## How It Works
SmartAsync uses `asyncio.get_running_loop()` to detect the execution context:
- **Sync context** (no event loop): Executes with `asyncio.run()`
- **Async context** (event loop running): Returns coroutine to be awaited
### Asymmetric Caching
SmartAsync uses an intelligent caching strategy:
- ✅ **Async context detected**: Cached forever (can't transition from async to sync)
- ⚠️ **Sync context**: Always rechecked (can transition from sync to async)
This ensures correct behavior while optimizing for the most common case (async contexts in web frameworks).
## Use Cases
### 1. CLI + HTTP API
```python
from smartasync import smartasync
from smpub import PublishedClass, ApiSwitcher
class DataHandler(PublishedClass):
api = ApiSwitcher()
@api
@smartasync
async def process_data(self, input_file: str):
"""Process data file."""
async with aiofiles.open(input_file) as f:
data = await f.read()
return process(data)
# CLI usage (sync)
handler = DataHandler()
result = handler.process_data("data.csv")
# HTTP usage (async via FastAPI)
# Automatically works without modification!
```
### 2. Testing
```python
@smartasync
async def database_query(query: str):
async with database.connect() as conn:
return await conn.execute(query)
# Sync tests
def test_query():
result = database_query("SELECT * FROM users")
assert len(result) > 0
# Async tests
async def test_query_async():
result = await database_query("SELECT * FROM users")
assert len(result) > 0
```
### 3. Mixed Codebases
Perfect for gradually migrating sync code to async without breaking existing callers.
## Performance
- **Decoration time**: ~3-4 microseconds (one-time cost)
- **Sync context**: ~102 microseconds (dominated by `asyncio.run()` overhead)
- **Async context (first call)**: ~2.3 microseconds
- **Async context (cached)**: ~1.3 microseconds
For typical CLI tools and web APIs, this overhead is negligible compared to network latency (10-200ms).
## Advanced Usage
### With `__slots__`
SmartAsync works seamlessly with `__slots__` classes:
```python
from smartasync import smartasync
class OptimizedManager:
__slots__ = ('data',)
def __init__(self):
self.data = []
@smartasync
async def add_item(self, item):
await asyncio.sleep(0.01) # Simulate I/O
self.data.append(item)
```
### Cache Reset for Testing
```python
@smartasync
async def my_method():
pass
# Reset cache between tests
my_method._smartasync_reset_cache()
```
## Limitations
- ⚠️ **Cannot transition from async to sync**: Once in async context, cannot move back to sync (this is correct behavior)
- ⚠️ **Sync overhead**: Always rechecks context in sync mode (~2 microseconds per call)
## Thread Safety
SmartAsync is **safe for all common use cases**:
✅ **Safe scenarios** (covers 99% of real-world usage):
- Single-threaded applications (CLI tools, scripts)
- Async event loops (inherently single-threaded)
- Web servers with request isolation (new instance per request)
- Thread pools with instance-per-thread pattern
⚠️ **Theoretical concern** (anti-pattern, not recommended):
- Sharing a single instance across multiple threads in a thread pool
**Why this isn't a real issue:**
The anti-pattern scenario defeats SmartAsync's purpose. If you're using thread pools with shared instances, you should use async workers instead for better performance and natural concurrency.
**Recommendation:** Create instances per thread/request, or better yet, use async patterns natively.
## Related Projects
SmartAsync is part of the **Genro-Libs toolkit**:
- [smartswitch](https://github.com/genropy/smartswitch) - Rule-based function dispatch
- [smpub](https://github.com/genropy/smpub) - CLI/API framework (uses SmartAsync for async handlers)
- [gtext](https://github.com/genropy/gtext) - Text transformation and templates
## Contributing
Contributions welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
## License
MIT License - see [LICENSE](LICENSE) file for details.
## Credits
**Author**: Giovanni Porcari (Genropy Team)
**Part of**: [Genro-Libs](https://github.com/softwell/genro-libs) developer toolkit
Raw data
{
"_id": null,
"home_page": null,
"name": "smartasync",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": null,
"keywords": "async, sync, decorator, asyncio, context-detection, unified-api",
"author": null,
"author_email": "Giovanni Porcari <softwell@softwell.it>",
"download_url": "https://files.pythonhosted.org/packages/31/4a/887e8795de20a68d47861b95eff964ac62be42f946ae58ae25db1ce86156/smartasync-0.2.0.tar.gz",
"platform": null,
"description": "# SmartAsync\n\n**Unified sync/async API decorator with automatic context detection**\n\n[](https://pypi.org/project/smartasync/)\n[](https://github.com/genropy/smartasync/actions/workflows/test.yml)\n[](https://codecov.io/gh/genropy/smartasync)\n[](https://smartasync.readthedocs.io/en/latest/?badge=latest)\n[](https://www.python.org/downloads/)\n[](https://opensource.org/licenses/MIT)\n[](https://github.com/softwell/genro-libs)\n[](llm-docs/)\n\nSmartAsync allows you to write async methods once and call them in both sync and async contexts without modification. It automatically detects the execution context and adapts accordingly.\n\n## Features\n\n- \u2705 **Automatic context detection**: Detects sync vs async execution context at runtime\n- \u2705 **Zero configuration**: Just apply the `@smartasync` decorator\n- \u2705 **Asymmetric caching**: Smart caching strategy for optimal performance\n- \u2705 **Compatible with `__slots__`**: Works with memory-optimized classes\n- \u2705 **Pure Python**: No dependencies beyond standard library\n\n## Installation\n\n```bash\npip install smartasync\n```\n\n## Quick Start\n\n```python\nfrom smartasync import smartasync\nimport asyncio\n\nclass DataManager:\n @smartasync\n async def fetch_data(self, url: str):\n \"\"\"Fetch data - works in both sync and async contexts!\"\"\"\n async with httpx.AsyncClient() as client:\n response = await client.get(url)\n return response.json()\n\n# Sync context - no await needed\nmanager = DataManager()\ndata = manager.fetch_data(\"https://api.example.com/data\")\n\n# Async context - use await\nasync def main():\n manager = DataManager()\n data = await manager.fetch_data(\"https://api.example.com/data\")\n\nasyncio.run(main())\n```\n\n## How It Works\n\nSmartAsync uses `asyncio.get_running_loop()` to detect the execution context:\n\n- **Sync context** (no event loop): Executes with `asyncio.run()`\n- **Async context** (event loop running): Returns coroutine to be awaited\n\n### Asymmetric Caching\n\nSmartAsync uses an intelligent caching strategy:\n- \u2705 **Async context detected**: Cached forever (can't transition from async to sync)\n- \u26a0\ufe0f **Sync context**: Always rechecked (can transition from sync to async)\n\nThis ensures correct behavior while optimizing for the most common case (async contexts in web frameworks).\n\n## Use Cases\n\n### 1. CLI + HTTP API\n\n```python\nfrom smartasync import smartasync\nfrom smpub import PublishedClass, ApiSwitcher\n\nclass DataHandler(PublishedClass):\n api = ApiSwitcher()\n\n @api\n @smartasync\n async def process_data(self, input_file: str):\n \"\"\"Process data file.\"\"\"\n async with aiofiles.open(input_file) as f:\n data = await f.read()\n return process(data)\n\n# CLI usage (sync)\nhandler = DataHandler()\nresult = handler.process_data(\"data.csv\")\n\n# HTTP usage (async via FastAPI)\n# Automatically works without modification!\n```\n\n### 2. Testing\n\n```python\n@smartasync\nasync def database_query(query: str):\n async with database.connect() as conn:\n return await conn.execute(query)\n\n# Sync tests\ndef test_query():\n result = database_query(\"SELECT * FROM users\")\n assert len(result) > 0\n\n# Async tests\nasync def test_query_async():\n result = await database_query(\"SELECT * FROM users\")\n assert len(result) > 0\n```\n\n### 3. Mixed Codebases\n\nPerfect for gradually migrating sync code to async without breaking existing callers.\n\n## Performance\n\n- **Decoration time**: ~3-4 microseconds (one-time cost)\n- **Sync context**: ~102 microseconds (dominated by `asyncio.run()` overhead)\n- **Async context (first call)**: ~2.3 microseconds\n- **Async context (cached)**: ~1.3 microseconds\n\nFor typical CLI tools and web APIs, this overhead is negligible compared to network latency (10-200ms).\n\n## Advanced Usage\n\n### With `__slots__`\n\nSmartAsync works seamlessly with `__slots__` classes:\n\n```python\nfrom smartasync import smartasync\n\nclass OptimizedManager:\n __slots__ = ('data',)\n\n def __init__(self):\n self.data = []\n\n @smartasync\n async def add_item(self, item):\n await asyncio.sleep(0.01) # Simulate I/O\n self.data.append(item)\n```\n\n### Cache Reset for Testing\n\n```python\n@smartasync\nasync def my_method():\n pass\n\n# Reset cache between tests\nmy_method._smartasync_reset_cache()\n```\n\n## Limitations\n\n- \u26a0\ufe0f **Cannot transition from async to sync**: Once in async context, cannot move back to sync (this is correct behavior)\n- \u26a0\ufe0f **Sync overhead**: Always rechecks context in sync mode (~2 microseconds per call)\n\n## Thread Safety\n\nSmartAsync is **safe for all common use cases**:\n\n\u2705 **Safe scenarios** (covers 99% of real-world usage):\n- Single-threaded applications (CLI tools, scripts)\n- Async event loops (inherently single-threaded)\n- Web servers with request isolation (new instance per request)\n- Thread pools with instance-per-thread pattern\n\n\u26a0\ufe0f **Theoretical concern** (anti-pattern, not recommended):\n- Sharing a single instance across multiple threads in a thread pool\n\n**Why this isn't a real issue:**\nThe anti-pattern scenario defeats SmartAsync's purpose. If you're using thread pools with shared instances, you should use async workers instead for better performance and natural concurrency.\n\n**Recommendation:** Create instances per thread/request, or better yet, use async patterns natively.\n\n## Related Projects\n\nSmartAsync is part of the **Genro-Libs toolkit**:\n\n- [smartswitch](https://github.com/genropy/smartswitch) - Rule-based function dispatch\n- [smpub](https://github.com/genropy/smpub) - CLI/API framework (uses SmartAsync for async handlers)\n- [gtext](https://github.com/genropy/gtext) - Text transformation and templates\n\n## Contributing\n\nContributions welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.\n\n## License\n\nMIT License - see [LICENSE](LICENSE) file for details.\n\n## Credits\n\n**Author**: Giovanni Porcari (Genropy Team)\n**Part of**: [Genro-Libs](https://github.com/softwell/genro-libs) developer toolkit\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Unified sync/async API decorator with automatic context detection",
"version": "0.2.0",
"project_urls": {
"Bug Tracker": "https://github.com/genropy/smartasync/issues",
"Documentation": "https://smartasync.readthedocs.io",
"Homepage": "https://github.com/genropy/smartasync",
"Repository": "https://github.com/genropy/smartasync"
},
"split_keywords": [
"async",
" sync",
" decorator",
" asyncio",
" context-detection",
" unified-api"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "c592042430c5475704f09267ebe2b4c3fedd89f2644be416dcb83442710c2459",
"md5": "726f354c5e9e8de3f70954ae50bec2f4",
"sha256": "13accdf27a27a18aa5fbe96d93ed0a5b98d9184f4b350e08ef6479d642576df4"
},
"downloads": -1,
"filename": "smartasync-0.2.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "726f354c5e9e8de3f70954ae50bec2f4",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 7057,
"upload_time": "2025-11-10T23:26:39",
"upload_time_iso_8601": "2025-11-10T23:26:39.421334Z",
"url": "https://files.pythonhosted.org/packages/c5/92/042430c5475704f09267ebe2b4c3fedd89f2644be416dcb83442710c2459/smartasync-0.2.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "314a887e8795de20a68d47861b95eff964ac62be42f946ae58ae25db1ce86156",
"md5": "36b433485812acf51d51cb25b84c9cf8",
"sha256": "2fc701db97613320c13e9ade6b199c39e9ba56d89fe8875d5d5f274f7ba56818"
},
"downloads": -1,
"filename": "smartasync-0.2.0.tar.gz",
"has_sig": false,
"md5_digest": "36b433485812acf51d51cb25b84c9cf8",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 9833,
"upload_time": "2025-11-10T23:26:40",
"upload_time_iso_8601": "2025-11-10T23:26:40.633133Z",
"url": "https://files.pythonhosted.org/packages/31/4a/887e8795de20a68d47861b95eff964ac62be42f946ae58ae25db1ce86156/smartasync-0.2.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-11-10 23:26:40",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "genropy",
"github_project": "smartasync",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "smartasync"
}