Reputation: 3515
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
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.
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