Skip to content

Conversation

tirkarthi
Copy link
Member

@tirkarthi tirkarthi commented May 25, 2019

Return a coroutine while patching async functions with a decorator.

Co-authored-by: Andrew Svetlov [email protected]

https://bugs.python.org/issue36996

@tirkarthi
Copy link
Member Author

I have tried using below pattern but I am not sure modifying the return value data type based on a flag as a good idea. Also using yield from makes it not usable as a function with the return statement in else part since patched is marked as a generator. So I have used a separate decorate_async_callable as suggested and my concern is that future changes to decorate_callable need to be done in decorate_async_callable too due to the code duplication. If there is any better way to handle this it would be great to know.

def decorate_callable(self, func, async_func=False):
    def patched(func):
        if async_func:
            yield from func(*args, **keywargs)
        else:
            return func(*args, **keywargs)
    if async_func:
        patched = types.coroutine(async_func)  

cc: @lisroach @asvetlov

Copy link
Contributor

@asvetlov asvetlov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting challenge.
Let me try to find a way to don't duplicate too much.



def decorate_async_callable(self, func):
'''This is same as decorate_callable except patched is a coroutine'''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace docstring with just a comment.
Docstrings are for public API but this class is entirely an implementation detail.

In the comment please add an explicit text like NB. Keep the method in sync with decorate_callable.
Add the same comment to decorate_callable.

@asvetlov
Copy link
Contributor

This seems to work fine:

    @contextlib.contextmanager
    def decoration_helper(self, patched, args, keywargs):
        extra_args = []
        entered_patchers = []
        patching = None

        exc_info = tuple()
        try:
            for patching in patched.patchings:
                arg = patching.__enter__()
                entered_patchers.append(patching)
                if patching.attribute_name is not None:
                    keywargs.update(arg)
                elif patching.new is DEFAULT:
                    extra_args.append(arg)

            args += tuple(extra_args)
            yield (args, keywargs)
        except:
            if (patching not in entered_patchers and
                _is_started(patching)):
                # the patcher may have been started, but an exception
                # raised whilst entering one of its additional_patchers
                entered_patchers.append(patching)
            # Pass the exception to __exit__
            exc_info = sys.exc_info()
            # re-raise the exception
            raise
        finally:
            for patching in reversed(entered_patchers):
                patching.__exit__(*exc_info)


    def decorate_callable(self, func):
        if hasattr(func, 'patchings'):
            func.patchings.append(self)
            return func

        @wraps(func)
        def patched(*args, **keywargs):
            with self.decoration_helper(patched,
                                        args,
                                        keywargs) as (newargs, newkeywargs):
                return func(*newargs, **newkeywargs)

        patched.patchings = [self]
        return patched


    def decorate_async_callable(self, func):
        '''This is same as decorate_callable except patched is a coroutine'''
        if hasattr(func, 'patchings'):
            func.patchings.append(self)
            return func

        @wraps(func)
        async def patched(*args, **keywargs):
            with self.decoration_helper(patched,
                                        args,
                                        keywargs) as (newargs, newkeywargs):
                return await func(*newargs, **newkeywargs)

        patched.patchings = [self]
        return patched

@tirkarthi
Copy link
Member Author

This looks like a cool pattern to me on the control flow with yield and acts like the code is pasted except the logic can be used for sync and async functions.

Copy link
Contributor

@asvetlov asvetlov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, a few very minor comments

tirkarthi and others added 2 commits May 25, 2019 14:21
Add parens to comment

Co-Authored-By: Andrew Svetlov <[email protected]>
Add parens to comment

Co-Authored-By: Andrew Svetlov <[email protected]>
@miss-islington miss-islington merged commit 436c2b0 into python:master May 28, 2019
@asvetlov
Copy link
Contributor

thanks!

@tirkarthi
Copy link
Member Author

Thanks @asvetlov for the review and merge.

DinoV pushed a commit to DinoV/cpython that referenced this pull request Jan 14, 2020
…tor (pythonGH-13562)

Return a coroutine while patching async functions with a decorator. 

Co-authored-by: Andrew Svetlov <[email protected]>


https://bugs.python.org/issue36996
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants