Setting a descriptor in python3.5 asynchronously
Asked Answered
C

3

5

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
Cellophane answered 11/9, 2016 at 16:18 Comment(3)
__set__ shouldn't be returning anything. Did you want value to be resolved asynchronously?Friedcake
Yes, this would be the goalDiagnostics
But your examples are all passing a value, not a future.Friedcake
S
9

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.

Spermophyte answered 11/9, 2016 at 17:36 Comment(1)
Hi, while your answer is the ideal one i would like to hear in a python course and deserves its score, i would not say something "is not possible" in python. Discovering own way of doing/expressing things is that makes it a very lifefull language that can be shaped to handle almost any situation.Schmeltzer
S
0

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())
Schmeltzer answered 2/12, 2018 at 8:1 Comment(2)
Doesn't this get rid of the big selling point of decorators, as a way to change the behaviour of normal looking attribute access?Gustaf
maybe, but i tried to show 1 possible way on many to add "aset" as suggested by code_onkel, and which I think is more explicit than decorators.Schmeltzer
S
-1

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())
Schmeltzer answered 2/12, 2018 at 7:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.