A code transformation tool that automatically converts asynchronous Python code into synchronous equivalents.
npx @tessl/cli install tessl/pypi-unasync@0.6.0A code transformation tool that automatically converts asynchronous Python code into synchronous equivalents by parsing and rewriting Python source files. Unasync enables developers to maintain a single asynchronous codebase while generating both async and sync versions of their libraries, supporting customizable transformation rules and seamless integration with setuptools build process.
pip install unasynctokenize_rt, setuptoolsimport unasyncSpecific imports:
from unasync import Rule, unasync_files, cmdclass_build_pyimport setuptools
import unasync
setuptools.setup(
name="your_package",
# ... other setup configuration
cmdclass={'build_py': unasync.cmdclass_build_py()},
)This automatically transforms files from _async/ directories to _sync/ directories during package build.
import setuptools
import unasync
# Custom transformation rules
rules = [
# Transform ahip -> hip instead of _async -> _sync
unasync.Rule("/ahip/", "/hip/"),
# More specific rule with additional token replacements
unasync.Rule("/ahip/tests/", "/hip/tests/",
additional_replacements={"ahip": "hip"}),
]
setuptools.setup(
name="your_package",
cmdclass={'build_py': unasync.cmdclass_build_py(rules=rules)},
)import unasync
# Transform specific files
unasync.unasync_files(
["/path/to/async/file1.py", "/path/to/async/file2.py"],
rules=[unasync.Rule("/async/", "/sync/")]
)Creates transformation rules that define how async code should be converted to sync code, including directory mappings and custom token replacements.
class Rule:
def __init__(self, fromdir, todir, additional_replacements=None):
"""
A single set of rules for 'unasync'ing file(s).
Parameters:
- fromdir: Source directory path containing async code (e.g., "/_async/")
- todir: Target directory path for generated sync code (e.g., "/_sync/")
- additional_replacements: Optional dict of custom token replacements
beyond the default async-to-sync mappings
The Rule handles path matching, file transformation, and token replacement
according to the specified directory mapping and token rules.
"""Transforms a list of Python files from async to sync using specified rules, handling tokenization, parsing, and code rewriting.
def unasync_files(fpath_list, rules):
"""
Transform a list of files from async to sync using provided rules.
Parameters:
- fpath_list: List of file paths to transform
- rules: List of Rule instances defining transformation rules
Returns:
None
The function applies the most specific matching rule for each file,
performs tokenization and transformation, and writes sync versions
to the appropriate output directories with proper encoding preservation.
"""Creates a custom build_py class for seamless integration with setuptools, automatically transforming async code during the package build process.
def cmdclass_build_py(rules=(Rule("/_async/", "/_sync/"),)):
"""
Creates a 'build_py' class for use within setuptools 'cmdclass' parameter.
Parameters:
- rules: Tuple of Rule instances (defaults to (Rule("/_async/", "/_sync/"),))
Returns:
Custom build_py class for setuptools integration
The returned class extends setuptools.command.build_py to automatically
transform async files to sync versions during the build process.
Default rule transforms files from '/_async/' to '/_sync/' directories.
"""Unasync applies these default async-to-sync transformations:
__aenter__ → __enter____aexit__ → __exit____aiter__ → __iter____anext__ → __next__asynccontextmanager → contextmanagerAsyncIterable → IterableAsyncIterator → IteratorAsyncGenerator → GeneratorStopAsyncIteration → StopIterationasync keywords are removed from function definitions and context managersawait keywords are removed from expressionsAsync (followed by uppercase letter) become Sync# Example: Replace custom async tokens
custom_rule = unasync.Rule(
"/my_async/",
"/my_sync/",
additional_replacements={
"AsyncClient": "SyncClient",
"async_method": "sync_method",
"ASYNC_CONSTANT": "SYNC_CONSTANT"
}
)src/
├── mypackage/
│ ├── __init__.py
│ ├── _async/ # Async source code
│ │ ├── __init__.py
│ │ ├── client.py
│ │ └── utils.py
│ └── _sync/ # Generated sync code (auto-created)
│ ├── __init__.py
│ ├── client.py
│ └── utils.pysrc/
├── mypackage/
│ ├── __init__.py
│ ├── ahip/ # Custom async directory
│ │ ├── __init__.py
│ │ └── client.py
│ └── hip/ # Custom sync directory (auto-created)
│ ├── __init__.py
│ └── client.pyUnasync handles various scenarios gracefully:
tokenize_rtrules = [
# General transformation
unasync.Rule("/_async/", "/_sync/"),
# Specific transformation with custom replacements
unasync.Rule("/async_tests/", "/sync_tests/",
additional_replacements={"TestAsync": "TestSync"}),
# Most specific rule (takes precedence)
unasync.Rule("/async_tests/integration/", "/sync_tests/integration/",
additional_replacements={"IntegrationAsync": "IntegrationSync"})
]Rules are matched by directory path specificity:
# Standard setup.py integration
from setuptools import setup, find_packages
import unasync
setup(
name="my-async-package",
packages=find_packages("src"),
package_dir={"": "src"},
cmdclass={'build_py': unasync.cmdclass_build_py()},
install_requires=['unasync'],
)# pyproject.toml with custom build backend
[build-system]
requires = ["setuptools", "unasync"]
build-backend = "setuptools.build_meta"
[tool.setuptools.cmdclass]
build_py = "unasync:cmdclass_build_py"