danielgtaylor / python-betterproto

Clean, modern, Python 3.6+ code generator & library for Protobuf 3 and async gRPC
MIT License
1.47k stars 205 forks source link

Option for generating code without async? #20

Open adisunw opened 4 years ago

adisunw commented 4 years ago

I have some infrastructure developed around the previously gen'd grpc code, however I would really like to adopt betterproto for all its extra pythonic features.

The only hurdle i'm facing is that the async implementation is turned on by default. Is there a way to turn this off and not have the async gen code?

nat-n commented 4 years ago

Similar issue: https://github.com/danielgtaylor/python-betterproto/issues/44

githorse commented 4 years ago

Just to clarify beyond all doubt here -- for the moment the answer here is no, correct? We can't turn off async/await in the generated stub?

nat-n commented 4 years ago

@githorse your assessment is correct.

hiagopdutra commented 3 years ago

This library does not work for Cloud Run due to the lack of this functionality. If it worked for sure betterproto would be the best option.

zbentley commented 2 years ago

What about using native Python's async-to-sync functionality? That is, given an async RPC method called via result = await ServiceStub.some_method(), you could do either:

result = asyncio.run(ServiceStub.some_method())

...if you wanted to start and stop the event loop just for that method. If that incurs too much overhead, you could call into an existing event loop via something like:

result = asyncio.run_coroutine_threadsafe(ServiceStub.some_method(), loop=asyncio.get_event_loop()).result()

@nat-n if that satisfies user requirements, would you accept a PR documenting that technique?

whs commented 2 years ago

I tried that as a workaround. The problem was that

whs commented 2 years ago

If I may suggest, protobuf-ts has a swappable transport implementation for RPC where you can swap between gRPC, twirp and grpcweb.

theartofdevel commented 1 year ago

any news? need sync grpc client...

wpdonders commented 1 year ago

I have a similar requirement for a client; and I thought I'd share my current workaround. I didn't want to edit the generated client stubs because I may need to re-generate them frequently. I also wanted to hide the async stuff from the user, and let them call the service methods as if they're synchronous.

To achieve this, I created two decorators.

import asyncio
from functools import wraps

def synced(async_func):
    """Wrap the asynchronous function so that it becomes synchronous"""

    @wraps(async_func)
    def wrapped(*args, **kwargs):
        return asyncio.get_event_loop().run_until_complete(async_func(*args, **kwargs))

    return wrapped

def sync(cls):
    """Decorator for the service stubs to make them synchronized"""
    # Find all service methods (endpoints) by inspecting the passed class
    endpoints = tuple(
        getattr(cls, endpoint)
        for endpoint in dir(cls)
        if callable(getattr(cls, endpoint)) and not endpoint.startswith("_")
    )
   # Replace the methods by their `synced` variants
    for endpoint in endpoints:
        setattr(cls, endpoint.__name__, synced(endpoint))
    return cls

The decorator synced creates a single async event loop each time the method is called and runs the loop until the method is complete. The decorator sync is then used to apply the synced decorator to each service method residing in the service stub class. it assumes that the service methods are callables that don't start with an underscore.

To get a synchronized service from the generated asynchronous ServiceStub, you initiate it as follows:

synchronized_service = sync(ServiceStub(channel=...))  # enter a channel as you normally would

The upside of this method is that your IDE will pick up the service methods. The downside of this method is that tools like mypy get confused because by applying these decorators, we are (dynamically) changing the return types.

zbentley commented 1 year ago

@wpdonders that solution works, but may break in surprising ways in the presence of multiprocessing or other threads. I suggest using the asgiref.sync package (specifically async_to_sync), which provides the same functionality without those drawbacks.