I can write a descriptor returning a future which could be awaited on.
class AsyncDescriptor:
def __get__(self, obj, cls=None):
# generate some async future here
return future
def __set__(self, obj, value):
# generate some async future here
return future
class Device:
attr=AsyncDescriptor()
device=Device()
Now I can get the value in a coroutine with value=await device.attr
.
How would I set this attribute?
await device.attr=5
-> SyntaxError: can't assign to await expression
await setattr(device, 'attr', 5)
-> TypeError: object NoneType can't be used in 'await' expression
device.attr=5
-> RuntimeWarning: coroutine '__set__' was never awaited
What you are trying to do is not possible (with Python 3.5).
While it may be sensible for __get__
to return a Future, making __set__
async is simply not supported by Python 3.5. The return value of __set__
is ignored by Python since there is no "return value" of assignments. And the call to __set__
is always synchronous. Something like a = (b.c = 5)
actually raises a SyntaxError
as you already noticed.
If async assignments like await device.attr = 5
were allowed, there would probably be a separate protocol for async descriptors, i.e. with coroutines __aget__
and __aset__
as special methods in analogy to async context manager (async with
/ __aenter__
) and async iteration (async for
/ __aiter__
). See PEP 492 for the design decisions behind the async
/ await
support.
Also note that __get__
returning a future does not make __get__
a coroutine.
Without further context, it looks like you want to hide something behind the attribute access abstraction provided by the descriptor protocol which should better be done explicitly, but that's up to you of course.
await setattr(device,'attr',5) construct is also possible and actually more decent than device.attr = 5, but don't overload the genuine setattr of course.
this one is actually usefull for async threadless while keeping easy readability. running code that set value without await will raise a nice "RuntimeWarning: coroutine attr.__set__
was never awaited"
import asyncio, sys
async def main():
obj.attr = "not async value"
print(obj.attr)
print()
print("now give set an async value")
#DO NOT DO THAT use aio.asetattr(obj,'attr',5)
setattr = aio.asetattr
# ============== yes i can, thanks python ! ===========
await setattr(obj,'attr',5)
# ======================================
print(obj.attr)
print("bye")
flush_io()
def flush_io():
sys.stdout.flush()
sys.stderr.flush()
class attr:
def __init__(self, value):
self.value = value
def __get__(self, obj, objtype):
if obj is None:
return self
return self.value
async def __set__(self, obj, value):
if value is not obj.__class__:
print(" async updating", self, end=" ")
for i in range(value):
await asyncio.sleep(1)
print(".", end="")
flush_io()
print()
self.value = value
print("set", obj, value)
return
print("__set__", obj, value)
def __repr__(self):
return "<async attr>"
class aobj:
attr = attr("empty")
def __repr__(self):
return "<aobj>"
class aio:
async def asetattr(self, obj, attr, value):
await asyncio.sleep(1)
a_attr = getattr( type(obj), attr)
await a_attr.__set__(obj, value)
print("done!")
aio = aio()
obj = aobj()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
but python is great so if you want to write device.attr = 5 but awaiting the set
operation, you just can ( i don't say its a good idea, though it may be usefull on threadless python scenario) with the help of a context block:
import asyncio, sys
async def main():
obj.attr = "not async value"
print(obj.attr)
print()
print("now set with an async value")
async with aio(obj):
# ============== yes i can, thanks python ! ===
obj.attr = 5
# =============================================
print(obj.attr)
print("bye")
flush_io()
def flush_io():
sys.stdout.flush()
sys.stderr.flush()
class attr:
def __init__(self, value):
self.value = value
def __get__(self, obj, objtype):
if obj is None:
print("__get__", obj, objtype)
return self
print("get", obj, objtype)
return self.value
def __set__(self, obj, value):
if value is not obj.__class__:
if obj in aio.updating:
aio.setlist.append([self, value])
print(" future set", self, value)
return
self.value = value
print("set", obj, value)
return
print("__set__", obj, value)
async def setter(self, value):
print(" async updating", self, end=" ")
for i in range(value):
await asyncio.sleep(1)
print(".", end="")
flush_io()
print()
self.value = value
def __repr__(self):
return "<async attr>"
class aobj:
attr = attr("empty")
def __repr__(self):
return "<aobj>"
class aio:
updating = []
setlist = []
def __call__(self, obj):
self.updating.append(obj)
return self
async def __aenter__(self):
print("aenter", self.updating)
async def __aexit__(self, *tb):
self.updating.pop()
while len(self.setlist):
obj, value = self.setlist.pop()
await obj.setter(value)
print("aexit")
aio = aio()
obj = aobj()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())