Python - descriptor on class to *set* attributes

I've been attempting to get __get__ __set__ and __delete__ magic method working with decorators put onto the class's method (not the instance). I am entirely unable to get anything other than __get__ to work / be called. I have tried about every combination / arrangement of code but the most recent looks like this:

def my_decorator5(func: Callable[..., R]) -> R:
    """ Decorate func while preserving the signature. """

    print(f"{'*'*40} my_decorator main")

    class Wrap(object):

        def __init__(self, clsmethod) -> None:
            print(f"{'*'*40} is __init__")
            self.value = None
            self.func = func
            self.clsmethod = clsmethod
            self.func_name = func.__name__

        def __get__(self, obj, cls) -> R:
            print(f"{'*'*40} {cls} is getting")
            owner = cls
            self.value = func(cls)
            return cast(R, self.value)

        def __set__(self, cls, newval) -> None:
            print(f"{'*'*40} {cls} is setting")
            self.value = newval

        def __set_name__(self, cls, newval) -> None:
            print(f"{'*'*40} {self} is __set_name__")
            print(f"{'*'*40} {cls} is __set_name__")
            print(f"{'*'*40} {newval} is __set_name__")

        def __delete__(self, cls) -> None:
            print(f"{'*'*40} {cls} is deleting")
            delattr(self, "value")

        def __delattr__(self, key) -> None:
            print(f"{'*'*40} {self} is __delattr__")

I have test code :

class foo3:

    @my_decorator5
    def foo3_str(self) -> str:
        return "Original foo3 str"



print(f"\n{'*'*80}\n{'*'*35} STARTING {'*'*35}\n{'*'*80}\n")


print(f"\n--{dir(foo3.foo3_str)}--\n")
print(f"\n--{foo3.foo3_str.__class__}--\n")
print(f"\n--{dir(foo3.__dict__['foo3_str'])}--\n")
print(f"\n--\tatrr: {foo3.foo3_str.__class__}\n\t__dict__{foo3.__dict__['foo3_str'].__class__}\n\t{foo3.__dict__['foo3_str'].__set__(1,2)}--\n")

print(">>>> getting  >>>>")
print(f"foo3_str ==> {foo3.foo3_str}")
print(f"<<<< got foo3_str <<<<")
print("\n")

print(f"\n--\tatrr: {foo3.foo3_str.__class__}\n\t__dict__{foo3.__dict__['foo3_str'].__class__}--\n")

print(">>>> settting >>>>")
foo3.foo3_str = "Set foo3 str"
print("<<<< set foo3_str <<<<")

print(f"\n--\tatrr: {foo3.foo3_str.__class__}\n\t__dict__{foo3.__dict__['foo3_str'].__class__}--\n")


print("\n")
print(f"foo3_str ==> {foo3.foo3_str}")
print("\n")

print(f">>>> deleting foo3_str")
del foo3.foo3_str
print(f"<<<< deleted foo3_str <<<<")
print("\n")

print(">>>> getting")
try:
    print(f"foo3_str ==> {foo3.foo3_str}")

    foo3.foo3_str = "Eric"
except AttributeError:
    print("...foo3.foo3_str AttributeError")
print(" <<<< getting foo3_str")

My output with this is:

**************************************** my_decorator main
**************************************** is __init__
**************************************** <__main__.my_decorator5.<locals>.Wrap object at 0x000002D185BA6470> is __set_name__
**************************************** <class '__main__.foo3'> is __set_name__
**************************************** foo3_str is __set_name__

********************************************************************************
*********************************** STARTING ***********************************
********************************************************************************

**************************************** <class '__main__.foo3'> is getting

--['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']--

**************************************** <class '__main__.foo3'> is getting

--<class 'str'>--


--['__class__', '__delattr__', '__delete__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__set__', '__set_name__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'clsmethod', 'func', 'func_name', 'value']--

**************************************** <class '__main__.foo3'> is getting
**************************************** 1 is setting

--      atrr: <class 'str'>
        __dict__<class '__main__.my_decorator5.<locals>.Wrap'>
        None--

>>>> getting  >>>>
**************************************** <class '__main__.foo3'> is getting
foo3_str ==> Original foo3 str
<<<< got foo3_str <<<<


**************************************** <class '__main__.foo3'> is getting

--      atrr: <class 'str'>
        __dict__<class '__main__.my_decorator5.<locals>.Wrap'>--

>>>> settting >>>>
<<<< set foo3_str <<<<

--      atrr: <class 'str'>
        __dict__<class 'str'>--



foo3_str ==> Set foo3 str


>>>> deleting foo3_str
<<<< deleted foo3_str <<<<


>>>> getting
...foo3.foo3_str AttributeError
 >> getting foo3_str
**************************************** my_decorator main

When the line

foo3.foo3_str = "Set foo3 str"

I would have expected to see

**************************************** ... is setting

But instead the __set__ function is entirely ignored and the class's dict is set to an actual str type.

Is it possible to have get , set , delete on a decorator for a class property , or without an instance will only the __get__ and never the __set__ and __detele__ be run?

1 answer

  • answered 2018-07-11 05:55 Blckknght

    A descriptor's __set__ method is only called if you assign to its name in an instance. If you assign to the class directly, you'll overwrite the descriptor (without __set__ being called). In contrast, the __get__ method gets called for lookups on either the class or an instance. Some descriptors (such as property) are programmed to to return themselves when called on the class, so it may not be as evident that their __get__ method did in fact run.

    If you want to use descriptors to control access to a class variable, you need to put your descriptor in a metaclass (the class of the class object). Here's a basic example, using a property to control a class variable named foo:

    class Meta(type):
        _foo = "original foo value"
    
        @property
        def foo(cls):
            print("getting foo")
            return cls._foo
    
        @foo.setter
        def foo(cls, value):
            print("setting foo")
            cls._foo = value
    
    class Klass(metaclass=Meta):
        pass
    
    # this invokes the property methods
    print(Klass.foo)
    Klass.foo = "new foo value"
    print(Klass.foo)
    
    # this won't work, the property is not accessible via an instance of Klass
    obj = Klass()
    obj.foo         # raises an AttributeError