N Chauhan
N Chauhan

Reputation: 3515

property() setter issues in metaclass

I'm trying to use property() in my metaclass to give a way to access/set an internal attribute. I'm using property() as apposed to @property as it's inside a metaclass and I need to apply the property to a class passed in the __new__ method.

This is a basic example of my code with relevant parts:

def __new__(mcls, name, bases, dct, **kwargs):

    def getabstract(obj):
        return getattr(obj, '_abstract', False)


    def setabstract(obj, value):
        if str(value) in ('True', 'False'):
            return setattr(obj, '_abstract', value)
        else: print(f'invalid abstract assignment {value}')
        return None        

    cls = super().__new__(mcls, name, bases, dct)

    for name, value in cls.__dict__.items():
        if callable(value):
            # only applies to callable (method)
            value._abstract = getattr(value, '_abstract', False)
            # add an _abstract attribute to the method or return its
            # current value if it already has one

    for base in bases:
        base._abstract = getattr(base, '_abstract', False)
        # give each base class an _abstract attribute if not already given

    cls.abstract = property(getabstract(cls),
                            setabstract(cls, getattr(cls, '_abstract', False)))
    # make an abstract property for class
    for name, value in cls.__dict__.items():
        if callable(value):
            # make an abstract property for functions
            value.abstract = property(getabstract(value),
                                      setabstract(value, getattr(value, '_abstract', False)))

When I run this, no errors occur, but when accessing a new class made by this metaclass e.g. Foo.abstract it returns:

    <property object at 0x.....>

Also, the setabstract() function used as the setter only sets the attribute if it's either True or False, but when I do something like Foo.abstract = 'foo', it still sets the value to 'foo'

Is there something I'm doing wrong here or something I've missed?

Upvotes: 0

Views: 1034

Answers (1)

Olivier Melan&#231;on
Olivier Melan&#231;on

Reputation: 22294

A property is not an instance attribute, it is a descriptor attribute of the class which is bound to the object which accessed it. Here is a simplified example:

class A:
    @property
    def x(self):
        return True

print(A.x) # <property object at 0x0000021AE2944548>
print(A().x) # True

When getting A.x, you obtain the unbound property. When getting A().x, the property is bound to an instance and the A.x.__get__ returns a value.

Back to your code

What this means is that a property must be a class attribute, not an instance attribute. Or, in your case, the property must be a metaclass attribute.

class Meta(type):
    @property
    def abstract(cls):
        return getattr(cls, '_abstract', False)

    @abstract.setter
    def abstract(cls, value):
        if value in (True, False):
            setattr(cls, '_abstract', value)
        else:
            raise TypeError(f'invalid abstract assignment {value}')

class Foo(metaclass=Meta):
    pass

print(Foo.abstract) # False
Foo.abstract = 'Not at bool' # TypeError: invalid abstract assignment Not at bool

Although, this only partially solves your issue as you want every method of you classes to have an abstract property. To do so, you will need their class to have that property.

Before we go any deeper, let me recall you one key concept in Python: we're all consenting adults here. Maybe you should just trust your users not to set abstract to anything else than a bool and let it be a non-property attribute. In particular, any user can change the _abstract attribute itself anyway.

If that does not suit you and you want abstract to be a property, then one way to do that is to define a wrapper class for methods that hold that property.

class Abstract:
    @property
    def abstract(cls):
        return getattr(cls, '_abstract', False)

    @abstract.setter
    def abstract(cls, value):
        if value in (True, False):
            setattr(cls, '_abstract', value)
        else:
            raise TypeError(f'invalid abstract assignment {value}')

class AbstractCallable(Abstract):
    def __init__(self, f):
        self.callable = f

    def __call__(self, *args, **kwargs):
        return self.callable(*args, **kwargs)

class Meta(type, Abstract):
    def __new__(mcls, name, bases, dct, **kwargs):
        for name, value in dct.items():
            if callable(value):
                dct[name] = AbstractCallable(value)

        return super().__new__(mcls, name, bases, dct)

class Foo(metaclass=Meta):
    def bar(self):
        pass

print(Foo.abstract) # False
print(Foo.bar.abstract) # False
Foo.bar.abstract = 'baz' # TypeError: invalid abstract assignment baz

Upvotes: 4

Related Questions