0

I have an enumerated descriptor which I want to be able to add to classes dynamically. Here it is

class TypedEnum:
    '''Class to make setting of enum types valid and error checked'''
    def __init__(self, enum, default=None):
        self._enum = enum
        self._enum_by_value = {e.value: e for e in enum}
        self._enum_by_name = {e.name: e for e in enum}
        self._value = next(iter(enum))
        if default is not None:
            self.value = default

    @property
    def value(self):
        return self._value.value

    @value.setter
    def value(self, value):
        if value in self._enum:
            value = getattr(self._enum, value.name)
        elif value in self._enum_by_name:
            value = self._enum_by_name[value]
        elif value in self._enum_by_value:
            value = self._enum_by_value[value]
        else:
            raise ValueError("Value does not exist in enum: {}".format(value))
        self._value = value

    @property
    def name(self):
        return self._value.name

    def __str__(self):
        return repr(self._value.name)

    def __get__(self, obj, type=None):
        return self._value.name

    def __set__(self, obj, value):
        self.value = value

To try and understand this behavior, I wrote up a unit test that goes over it

from enum import Enum
from unittest import TestCase
class abc(Enum):
    a = 0
    b = 1
    c = 2

def test_descriptor():
    '''test mostly the features of __get__ and __set__
    Basically when they are just objects they are fair game,
    but when they become a member of a class they are super
    protected (I can't even figure out a way to get access to
    the base object)'''
    venum = TypedEnum(abc)
    assert isinstance(venum, TypedEnum)
    # set works normally when not a member of an object
    venum = 0
    assert isinstance(venum, int)

    venum = TypedEnum(abc)
    x = venum
    assert isinstance(x, TypedEnum)

    # But when it is a member of a class/object, it is hidden
    class C:
        e = venum

    c = C()
    assert c.e == 'a' and isinstance(c.e, str)
    c.e = 'b'
    assert c.e == 'b' and isinstance(c.e, str)
    TestCase.assertRaises(None, ValueError, setattr, c, 'e', 'z')

    # However, when it is added on in init it doesn't behave the same
    # way
    class C:
        def __init__(self):
            self.e = venum

    c = C()
    # This would not have been true before
    assert c.e != 'a' and isinstance(c.e, TypedEnum)

    # to implement this, it seems we need to overload the getattribute
    class D(C):
        def __getattribute__(self, attr):
            obj = object.__getattribute__(self, attr)
            if hasattr(obj, '__get__'):
                print('getting __get__', obj, attr)
                return obj.__get__(self)
            else:
                return obj

        def __setattr__(self, attr, value):
            if not hasattr(self, attr):
                object.__setattr__(self, attr, value)
                return
            obj = object.__getattribute__(self, attr)
            if hasattr(obj, '__set__'):
                print('setting __set__', obj, attr, value)
                obj.__set__(self, value)
            else:
                setattr(self, attr, value)

    c = D()
    c.e = 'b'
    assert c.e == 'b' and isinstance(c.e, str)
    TestCase.assertRaises(None, ValueError, setattr, c, 'e', 'z')
test_descriptor()

Is this all sane? Why the heck does it work differently whether it is a member of __dict__ or class.__dict__???

Please tell me there is a better way to do this!

vitiral
  • 8,446
  • 8
  • 29
  • 43
  • 1
    Short answer: It has to do with how new-style classes are implemented and the descriptor protocol it uses which works the class level. `obj.x` turns into `descriptor = obj.__class__.x`, which turns into `descriptor.__get__(obj)`. See the **Descriptors** section of [_What's new in Python 2.2_](https://docs.python.org/dev/whatsnew/2.2.html#descriptors). – martineau Feb 13 '15 at 18:33
  • So there really is no other way around it, other than what I implemented here? Is that correct? – vitiral Feb 13 '15 at 21:25
  • 1
    AFAIK overriding `__getattribute__` is the only way, and unfortunately it's inherently slow because it's called on _all_ attribute accesses. Seems like I saw a hack once where every instance was made from a different, dynamically created, class. – martineau Feb 13 '15 at 21:35

0 Answers0