Skip to main content

A lightweight library for allowing async functions to be called in a synchronous manner.

Project description

AnySync

A lightweight library for allowing async functions to be called in a synchronous manner.

import asyncio
from anysync import anysync


@anysync
async def f():
    return 42


assert f().run() == 42


async def main():
    assert await f() == 42


asyncio.run(main())

Just pip install anysync and you're good to go!

Usage

Coroutines

The primary use case for anysync is to allow async functions to be called in a synchronous manner. All you need to do is add the anysync.coroutine decorator to your async function:

import asyncio
import anysync


@anysync.coroutine
async def f():
    return 42


assert f().run() == 42

Generators

You can also use anysync with async generators:

import asyncio
import anysync


@anysync.generator
async def gen():
    yield 42


assert list(gen()) == [42]

Note that in this case you don't need to call run(). The generator will automatically detect how it's being used and run the coroutine accordingly.

Context Managers

You can even use AnySync on your async context managers.

import asyncio
import anysync


@@anysync.contextmanager
async def cm():
    yield 42


def test_sync():
    with cm() as x:
        assert x == 42


async def test_async():
    async with cm() as x:
        assert x == 42


test_sync()
asyncio.run(test_async())

You can alternatively subclass the AnySyncContextManager class:

from anysync import AnySyncContextManager


class CM(AnySyncContextManager):
    async def __aenter__(self):
        return 42

    async def __aexit__(self, exc_type, exc, tb):
        pass


def test_sync():
    with CM() as x:
        assert x == 42


async def test_async():
    async with CM() as x:
        assert x == 42


test_sync()
asyncio.run(test_async())

Comparisons

asyncio.run

Unlike asyncio.run, an AnySync object can be run() even if an event loop is already running.

For example, the following code will raise a RuntimeError:

import asyncio


async def f():
    return 42


async def test_async():
    assert asyncio.run(f()) == 42


asyncio.run(test_async())

However, with AnySync, the following code will work as expected:

import asyncio
from anysync import anysync


@anysync
async def f():
    return 42


async def test_async():
    assert f().run() == 42


asyncio.run(test_async())

unsync

AnySync is similar to unsync in that it allows async functions to be called synchronously when needed. The main differences are that AnySync works with type checkers, is lighter weight, works with other async libraries like trio via anyio, in addition to supporting async generators and context managers.

Automatic Detection

The other approach to dealing with the challenges of mixing synchronous and asynchronous code is to automatically infer whether a function should be run synchronously based on whether it is being run in an async context. This approach is taken by libraries like Prefect's sync_compatible decorator. The main downside is that the behavior of the function changes dynamically depending on the context which can lead to unexpected behavior.

For example, the code below works as expected beca

from prefect.utilities.asyncutils import sync_compatible


@sync_compatible
async def request():
    ...
    return "hello"


def work():
    response = request()
    ...
    return response.upper()


def test_sync():
    assert work() == "HELLO"


test_sync()

However, if we now call work() from an async context, the behavior changes.

import asyncio


async def test_async():
    assert work() == "HELLO"  # AttributeError: 'coroutine' object has no attribute 'upper'


asyncio.run(test_async())

Because work() is now being called from an async context, request() automatically returns a coroutine object which causes work() to fail.

Other Considerations

How it Works

AnySync works by detecting the presence of a running event loop. If one already exists, then AnySync uses a separate thread to run the coroutine. Where possible AnySync tries to reuse a single global background thread that's created only when it's needed. However, in the case that a program repeatedly trys to synchronously run a coroutine while in an async context, AnySync will create a new thread each time.

For example, you can count the number of threads that are used in two different scenarios. The first reuses the same global thread over and over again.

from threading import current_thread

import anysync

threads = set()


@anysync.coroutine
async def f():
    threads.add(current_thread())  # runs in the main thread
    return g().run()


@anysync.coroutine
async def g():
    threads.add(current_thread())  # runs in anysync's global background thread
    return 42


f().run()
f().run()

main_thread = current_thread()
assert len(threads - {main_thread}) == 1

In the second scenario, ends up creating two threads in addition to AnySync's global background thread because g() runs in the global background thread and h() runs in a new thread each time.

from threading import current_thread

import anysync

threads = set()


@anysync.coroutine
async def f():
    threads.add(current_thread())  # runs in the main thread
    return g().run()


@anysync.coroutine
async def g():
    threads.add(current_thread())  # runs in anysync's global background thread
    return h().run()


@anysync.coroutine
async def h():
    threads.add(current_thread())  # runs in a new thread each time
    return 42


f().run()
f().run()

main_thread = current_thread()
assert len(threads - {main_thread}) == 3

Interacting with contextvars

AnySync wrapped coroutines or context managers will not propagate changes to contextvars from async to synchronous contexts. This is because contextvars are not shared between threads or event loops and AnySync must create these in order to run coroutines synchronously. Given this, the following is not supported:

from contextvars import ContextVar
from anysync import anysync


var = ContextVar("var", default=0)


@anysync
async def f():
    var.set(42)


f().run()
assert var.get() == 42  # AssertionError: 0 != 42

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

anysync-0.3.0.tar.gz (8.6 kB view details)

Uploaded Source

Built Distribution

anysync-0.3.0-py3-none-any.whl (7.6 kB view details)

Uploaded Python 3

File details

Details for the file anysync-0.3.0.tar.gz.

File metadata

  • Download URL: anysync-0.3.0.tar.gz
  • Upload date:
  • Size: 8.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-httpx/0.27.0

File hashes

Hashes for anysync-0.3.0.tar.gz
Algorithm Hash digest
SHA256 e20b82094e3c6e5742966f6531a21f792ace8c52203f104d67dc14f17a36ef39
MD5 e5c04f1589ef7a19197f286f7ecf3412
BLAKE2b-256 907259d4026043fcc66c7c2f1095e6f3b6e86baf10b8e0c5543a26edc97a0bb7

See more details on using hashes here.

File details

Details for the file anysync-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: anysync-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 7.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-httpx/0.27.0

File hashes

Hashes for anysync-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8529aba5ea6dfdeb297c46cdb481557c09a7bfa575f09b4abf369d01a051eb28
MD5 ecbc553844dc87012ab2e34630cd3a4e
BLAKE2b-256 695e3e51779c6eac25ae040bc950996d34126cc52fa76379257d21854f863a78

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page