javex
javex

Reputation: 7544

How to let inheritance take precedence over class properties when using a metaclass?

I have posted a similar question before but I interpreted my problem wrong, so I flagged it for deletion and come forth with the new and correct problem.

My general goal is the following: I want to have a property on a class, so I implement it on a metaclass using a property as suggested on my question Implementing a class property that preserves the docstring. Additionally, I want users to be able to subclass the base class and override this property with static values. The thing here is that if the user does not provide an attribute, I want to calculate a sensible default and since all configuration is done at the class level, I need a property at the class level.

A basic example will show this:

class Meta(type):
    @property
    def test(self):
        return "Meta"


class Test(object):
    __metaclass__ = Meta


class TestSub(Test):
    test = "TestSub"


class TestSubWithout(Test):
    pass

print(TestSub.test, TestSubWithout.test)

Here is what it prints:

('Meta', 'Meta')

And what I want it to print:

('TestSub', 'Meta')

Essentially, on TestSub the user overrides the test attribute himself. Now, TestSub is the correct output, since the user provided it. In the case of TestSubWithout, the user instead did not provide the attribute and so the default should be calculated (of course, the real calculation has more than just a static return, this is just to show it).

I know what happens here: First the attribute test is assigned to TestSub and then the metaclass overrides it. But I don't know how to prevent it in a clean and pythonic way.

Upvotes: 0

Views: 152

Answers (2)

Blckknght
Blckknght

Reputation: 104712

A property is a "data descriptor", which means that it takes precedence in the attribute search path over values stored in a instance dictionary (or for a class, the instance dictionaries of the other classes in its MRO).

Instead, write your own non-data descriptor that works like property:

class NonDataProperty(object):
    def __init__(self, fget):
        self.fget = fget

    def __get__(self, obj, type):
        if obj:
            return self.fget(obj)
        else:
            return self

    # don't define a __set__ method!

Here's a test of it:

class MyMeta(type):
    @NonDataProperty
    def x(self):
        return "foo"

class Foo(metaclass=MyMeta):
    pass # does not override x

class Bar(metaclass=MyMeta):
    x = "bar" # does override x

class Baz:
    x = "baz"

class Baz_with_meta(Baz, metaclass=MyMeta):
    pass # inherited x value will take precedence over non-data descriptor

print(Foo.x) # prints "foo"
print(Bar.x) # prints "bar"
print(Baz_with_meta.x) # prints "baz"

Upvotes: 1

javex
javex

Reputation: 7544

I cleanest way I could come up with is creating a subclass of property that handles this case:

class meta_property(property):
    def __init__(self, fget, fset=None, fdel=None, doc=None):
        self.key = fget.__name__
        super(meta_property, self).__init__(fget, fset, fdel, doc)

    def __get__(self, obj, type_):
        if self.key in obj.__dict__:
            return obj.__dict__[self.key]
        else:
            return super(meta_property, self).__get__(obj, type_)

This handles the case by storing the name of the function and returning the overridden value if it is present. This seems like an okayish solution but I am open to more advanced suggestions.

Upvotes: 1

Related Questions