2

I am trying to Yield From a function from within an asynchronous function. Having spent hours trying to figure this out and traversing Stack Overflow to find similar questions previously answered but unable to help me find a solution to my own problem, I find myself here.

Quite simply, I want to query the Asterisk Management Interface via Panoramisk, using a web browser and Websockets. When a user connects to the websocket server, it runs the ws_handle method

async def ws_handle(websocket, path):
    await register(websocket)
    try:
        async for message in websocket:
            data = json.loads(message)
            ...

I then want to retrieve some data, then deliver to the client. The problem which I am having, is that I find I am unable to just say

exts = yield from ExtensionStateList.get(AmiManager)

Where the ExtensionStateList.get function is (roughly) as below:

def get(AmiManager):
    queues_details = yield from AmiManager.send_action(
        {'Action': 'ExtensionStateList'})

    ...

    val = {
        'extensions': EXTENSIONS,
        'parks': PARKS,
        'paging': PAGING,
        'confrences': CONFRENCES,
        'apps': APPS,
        'misc': MISC
    }

    return val

I have used this same file, ExtensionStateList.py in another test file separate from my websockets server file, in a non-async method, calling it as shown before

exts = yield from ExtensionStateList.get(AmiManager)

without a problem, and it populates exts with the value returned from the function.

My research leads me to iterate through it like so:

async for a in ExtensionStateList.get(AmiManager):
    yield a

but I do not know how I can use that to populate the variable I wish to populate. I have tried like this:

exts = ''
async for a in ExtensionStatList.get(AmiManager):
    exts = exts+a

only to be told that it cannot join an AsyncIO.Future to a string. I have also tried swapping out the return val for a yield val, again with no luck.

Evidently, to me, this is a shortcoming in my lacking knowledge of Python. What can I do? I was thinking that maybe I could change ExtensionStateList.get to async, but that would throw me back in the same boat I am in now?

ADDITIONALLY

I have continued scouring through StackOverflow, and found the following question:

What is the difference between @types.coroutine and @asyncio.coroutine decorators?

It seems to me that maybe if I add @asyncio.coroutine on the line above ws_handle, like so:

@asyncio.coroutine
async def ws_handle(websocket, path):

that I would then be able to:

exts = yield from ExtensionStateList.get(AmiManager)

However, I find that this does not work, and it tells me that I can not yield from inside an async function. Am I misunderstanding what I am reading here? Or am I maybe not implementing it correctly? Am I on the right track with this?

As per the answer given here:

'yield from' inside async function Python 3.6.5 aiohttp

I have also tried awaiting the function like so:

exts = await ExtensionStateList.get(AmiManager)

However, Python tells me that the object generator cannot be used in await expression.

FURTHERMORE

For those who may be interested, this is how I am calling my ws_handle function. It is called upon creation of the websocket server, and the websocket server is responsible for dispatching/calling? the ws_handle function.

It seems to me that it calls this function once for each client who connects and this function runs until the user disconnects.

WebsocketServer = websockets.serve(ws_handle, host, port)
asyncio.get_event_loop().run_until_complete(WebsocketServer)
asyncio.get_event_loop().run_forever()

ADDENDUM

Yes, again I add even more. I have modified my ExtensionStateList.py so that when calling the get method, it performs as per below:

async def get(AmiManager):
    val = await getInternal(AmiManager)
    return val

@asyncio.coroutine
def getInternal(AmiManager):

I can now use the yield from internally in the getInternal function, which was previously my get function, and I can call this and receive date as per below:

exts = await ExtensionStateList.get(AmiManager)

I think I am getting a grasp of this, and I see how they are two different ways of doing almost the same thing.

Thanks for pointing me in the right direction guys!

Michael Thompson
  • 541
  • 4
  • 21
  • Is anything in `AmiManager` asynchronous? If not, just use it as a regular (non-asynchronous) generator. If it's doing any I/O, though, you may want to look into something like `run_in_executor` to prevent it from blocking other I/O. – dirn Dec 27 '18 at 13:36
  • Yes, AmiManager refers to Panoramisk (if you're farmiliar). It does run asynchronously, and I have to use yield from to retrieve results. For reference, I will add to my question a little more detail on how I am calling my loops. – Michael Thompson Dec 28 '18 at 02:23

2 Answers2

1

You are confused a little by the change in asyncio syntax. In 3.5 we moved to async def and await. See this answer for details and note the example below:

You want an async generator. See Pep 525 and the example code:

async def ticker(delay, to):
    """Yield numbers from 0 to `to` every `delay` seconds."""
    for i in range(to):
        yield i
        await asyncio.sleep(delay)
MarkReedZ
  • 1,421
  • 4
  • 10
  • Ok, so if I were to make my `ExtensionStateList.get` an async function, and in my ws_handle function I were to have `exts = ExtensionStateList.get...` and `await exts`, I understand that this would be the correct way to do it then? If that is the case, unfortunately, the library which I am using (Panoramisk) explicitly requires a `yield from` in order to send commands to the AMI interface and retrieve data, which throws me back in the same boat that I was in before. But would that be the modern syntax expected and should it work otherwise? Thanks. – Michael Thompson Dec 28 '18 at 01:22
  • https://github.com/gawel/panoramisk/issues/67 - It looks like the library was updated for python 3.7 support. Write an issue if there is still a problem with the await syntax? – MarkReedZ Jan 01 '19 at 00:24
  • Hmmmm :S I will try it, I was just following their documentation but wasn't aware it would support the new syntax. I'll give it a go and share how I go. – Michael Thompson Jan 01 '19 at 10:26
0

Hold the thought - I didn't figure it out. As per my comment below, I must have been tired, and mistook an error for the program working. Oops.

EUREKA!

After hours of hair tearing, documentation reading, and almost tears, I found that @async.coroutine needs to be applied to the ExtensionStateList.get method like so:

#In ExtensionStateList.py
@asyncio.coroutine
def get(AmiManager):

and that I must await when calling that command like so:

exts = ExtensionStateList.get(AmiManager)
await exts

I found that if I only await exts without making it an asyncio.coroutine, it will return the error stating that you cannot await a generator.

Michael Thompson
  • 541
  • 4
  • 21
  • That isn't giving you a generator. `yield from` was how you awaited a coroutine prior to the addition of `async` and `await` in 3.5. You've found a syntax that works, but it isn't what you were asking about. – dirn Dec 27 '18 at 13:37
  • Ok, well this gives me the results I expected, but is there a better way to do this in which the syntax is more modern? For reference, I am intending to run this on FreePBX 14, using the pre-installed Python 3.6. I have no interest in targeting older versions of Python than that. – Michael Thompson Dec 28 '18 at 01:14
  • Also, it turns out that it actually isn't working, and is presenting an error. It was my fault in thinking it was working - I blame fatigue. It was throwing an error and printing the data I was trying to retrieve to the console, rather than sending it to the client, and I must have thought that I intended to print it to the screen and I wasn't reading properly. How embarrassing. – Michael Thompson Dec 28 '18 at 03:34