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 1
    yield 2
    yield 3


assert list(gen()) == [1, 2, 3]

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 anysync


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


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

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


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

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 and other async libraries like trio via anyio as well as 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 operates as expected where work() is called in a sync context:

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.1.tar.gz (9.2 kB view details)

Uploaded Source

Built Distribution

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

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for anysync-0.3.1.tar.gz
Algorithm Hash digest
SHA256 f878d2fd1c929f600a125520f6662287ead9aaca66f12b1640751b0167500827
MD5 97b6bbb719cabc018aa0433fe4a8be54
BLAKE2b-256 84c5e6f2b218d10fca3dea947c2ce3ab5d08a42dab1a4080851c23123a7f1ee1

See more details on using hashes here.

Provenance

File details

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

File metadata

  • Download URL: anysync-0.3.1-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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 77b33aeceeb4375ed031b2e43564cce00a6c17018f9d7e16610cd778d7e23c0a
MD5 6cf1a8005c812b5f23e98fadd2a7f53d
BLAKE2b-256 4d80cd00f222ccd1d655c45e171c6e4435987ab7798068bd51d569891ad63f54

See more details on using hashes here.

Provenance

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