Skip to content

Commit

Permalink
winrt/client: add FutureLike wrapper for IAsyncOperation
Browse files Browse the repository at this point in the history
The fix from #1101 causes the following when run with `python3.10 -X dev`.

    ERROR:asyncio:Exception in callback Future.set_result(<Future cance...events.py:429>, <_bleak_winrt...001C33C60B100>)
    handle: <Handle Future.set_result(<Future cance...events.py:429>, <_bleak_winrt...001C33C60B100>)>
    Traceback (most recent call last):
    File "C:\Users\david\AppData\Local\Programs\Python\Python310\lib\asyncio\events.py", line 80, in _run
        self._context.run(self._callback, *self._args)
    asyncio.exceptions.InvalidStateError: invalid state
    ERROR:asyncio:Exception in callback Future.set_result(<Future cance...events.py:429>, <_bleak_winrt...001C33C60BFC0>)
    handle: <Handle Future.set_result(<Future cance...events.py:429>, <_bleak_winrt...001C33C60BFC0>)>
    Traceback (most recent call last):
    File "C:\Users\david\AppData\Local\Programs\Python\Python310\lib\asyncio\events.py", line 80, in _run
        self._context.run(self._callback, *self._args)
    asyncio.exceptions.InvalidStateError: invalid state

This adds a new FutureLike wrapper to make the IAsyncOperation object
look like an asyncio.Future instead of wrapping it in a task.
  • Loading branch information
dlech committed Nov 1, 2022
1 parent 50057cc commit ccf7607
Showing 1 changed file with 72 additions and 6 deletions.
78 changes: 72 additions & 6 deletions bleak/backends/winrt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
"""

import asyncio
import functools
import logging
import sys
import uuid
import warnings
from ctypes import pythonapi
from typing import Any, Dict, List, Optional, Sequence, Union, cast

import async_timeout
Expand Down Expand Up @@ -44,11 +46,15 @@
DevicePairingResultStatus,
DeviceUnpairingResultStatus,
)
from bleak_winrt.windows.foundation import EventRegistrationToken
from bleak_winrt.windows.foundation import (
AsyncStatus,
EventRegistrationToken,
IAsyncOperation,
)
from bleak_winrt.windows.storage.streams import Buffer

from ... import BleakScanner
from ...exc import PROTOCOL_ERROR_CODES, BleakError, BleakDeviceNotFoundError
from ...exc import PROTOCOL_ERROR_CODES, BleakDeviceNotFoundError, BleakError
from ..characteristic import BleakGATTCharacteristic
from ..client import BaseBleakClient, NotifyCallback
from ..device import BLEDevice
Expand Down Expand Up @@ -569,10 +575,9 @@ async def get_services(self, **kwargs) -> BleakGATTServiceCollection:
)
self._services_changed_events.append(services_changed_event)

async def get_services():
return await self._requester.get_gatt_services_async(*args)

get_services_task = asyncio.create_task(get_services())
get_services_task = FutureLike(
self._requester.get_gatt_services_async(*args)
)

try:
await asyncio.wait(
Expand Down Expand Up @@ -889,3 +894,64 @@ async def stop_notify(

event_handler_token = self._notification_callbacks.pop(characteristic.handle)
characteristic.obj.remove_value_changed(event_handler_token)


class FutureLike:
"""
Wraps a WinRT IAsyncOperation in a "future-like" object so that it can
be passed to Python APIs.
Needed until https://github.com/pywinrt/pywinrt/issues/14
"""

_asyncio_future_blocking = True

def __init__(self, async_result: IAsyncOperation) -> None:
self._async_result = async_result
self._callbacks = []
self._loop = asyncio.get_running_loop()

def call_callbacks(op: IAsyncOperation, status: AsyncStatus):
for c in self._callbacks:
c(self)

async_result.completed = functools.partial(
self._loop.call_soon_threadsafe, call_callbacks
)

def result(self) -> Any:
return self._async_result.get_results()

def done(self) -> bool:
return self._async_result.status != AsyncStatus.STARTED

def cancelled(self) -> bool:
return self._async_result.status == AsyncStatus.CANCELED

def add_done_callback(self, callback, *, context=None) -> None:
self._callbacks.append(callback)

def remove_done_callback(self, callback) -> None:
self._callbacks.remove(callback)

def cancel(self, msg=None) -> bool:
if self._async_result.status != AsyncStatus.STARTED:
return False
self._async_result.cancel()
return True

def exception(self) -> Optional[Exception]:
if self._async_result.status == AsyncStatus.STARTED:
raise asyncio.InvalidStateError
if self._async_result.status == AsyncStatus.COMPLETED:
return None
if self._async_result.status == AsyncStatus.CANCELED:
raise asyncio.CancelledError
if self._async_result.status == AsyncStatus.ERROR:
try:
pythonapi.PyErr_SetFromWindowsErr(self._async_result.error_code)
except OSError as e:
return e

def get_loop(self) -> asyncio.AbstractEventLoop:
return self._loop

0 comments on commit ccf7607

Please sign in to comment.