3

So I'm writing an interface framework that allow people to write a collection of commands.

What I am trying to do here is that, for each subclass of Base, find all of its methods that has a description, then collect all those functions and generate a collective description out of them (plus something else, but its not relevant to this question)

Here is MCVE

import types
class Meta(type):

    def __init__(cls, *args, **kwargs):
        cls._interfaces = {}
        super().__init__(*args, **kwargs)

    def __call__(cls, id, *args, **kwargs):
        if id in cls._interfaces:
            return cls._interfaces[id]
        obj = cls.__new__(cls, *args, **kwargs)
        obj.__init__(*args, **kwargs)
        cls._interfaces[id] = obj
        return obj

class Base(metaclass=Meta):

    @property
    def descriptions(self): # this doesn't work. It gives RecursionError
        res=''
        for name in dir(self):
            attr=getattr(self,name)
            if isinstance(attr, types.FunctionType) and hasattr(attr, 'description'):
                res += f'{name}: {attr.description}\n'
        return res

    @property
    def descriptions(self):
        res=''
        for cls in self.__class__.__mro__:
            for name,attr in cls.__dict__.items():
                if isinstance(attr, types.FunctionType) and hasattr(attr, 'description'):
                    res += f'{name}: {attr.description}\n'
        return res

class A(Base):

    def a(self): pass
    a.description='A.a'

    def b(self): pass
    b.description='A.b'

class B(A):
    def c(self): pass
    c.description='B.c'

    def d(self): pass
    d.description='B.d'

print(B(0).descriptions)

Now the problem with my current method is that it doesn't detect method overrides. Suppose I add this snippet into the definition of class B

def a(self): pass
a.description='B.a'

Now the generated descriptions will be completely wrong. What I want is that when a method got overridden, it's collected description also got overridden. So the description should always point to the method that Python would normally resolve to if people do it with usual ways (e.g. B.a.description)

So the expected output would then change to

a: B.a
b: A.b
c: B.c
d: B.d

I suppose I can make a special case out of word descriptions and fix the first description method.

But is there a more elegant and Pythonic way to achieve that? To be honest, self.__class__.__mro__ looks pretty horrible.

4
  • 1
    What do you want to happen with overrides? Commented Sep 21, 2018 at 4:23
  • If I'm guessing right, then you should go with your "this doesn't work" version, but examine type(self) instead of self. Without you telling us what you want, it's hard to be sure, though. Commented Sep 21, 2018 at 4:26
  • (Or perhaps better, put the property on the metaclass and access it through classes instead of instances.) Commented Sep 21, 2018 at 4:27
  • @user2357112 What I want is that when a method got overridden, it's collected description also got overridden. So the description should always point to the method that Python would normally resolve to if people do it with usual ways (e.g. B.a.description). Commented Sep 21, 2018 at 4:33

2 Answers 2

2

When you iterate over self.__class__.__mro__, you are iterating over the resolution order, which would be <class '__main__.B'> <class '__main__.A'> <class '__main__.Base'> <class 'object'> in your example.

As a result, you are looking at the description properties of each of these classes subsequently. When you declare a.description = 'B.a' inside class B, you are not actually overriding it in the parent class A, you are just overriding it in the child class B.

So, instead, you could do something like this:

import inspect


class Base(metaclass=Meta):
    def descriptions(self):
        res = ''
        for mtd in inspect.getmembers(self, predicate=inspect.ismethod):
            if hasattr(mtd[1], 'description'):
                res += f'{mtd[0]}: {mtd[1].description}\n'

        return res

That way, you are only inspecting the actual child and not its parents. However, Base.descriptions cannot be a property in this case, because properties get evaluated upon declaration, which would make inspect.getmembers keep inspecting itself infinitely long.

Sign up to request clarification or add additional context in comments.

Comments

1

I think you can fix your first implementation of descriptions, which may be easier than messing with the MRO. The reason you are getting a RecursionError is that "descriptions" will show up in the list of attribute names. When you do getattr(self, name) on it, it recursively calls itself, since it's a property. You can easily avoid that by only looking up the name if it is not "descriptions". You also have an issue that when you lookup a method by name on an instance, you'll get a bound method object, rather than a function. Perhaps you should be calling getattr on type(self) instead of on self? That would look something like this:

@property
def descriptions(self):
    res=''
    for name in dir(self):
        if name != "descriptions":                    # avoid recursion!
            attr = getattr(type(self), name, None)    # lookup name on class object
            if isinstance(attr, types.FunctionType) and hasattr(attr, 'description'):
                res += f'{name}: {attr.description}\n'
    return res

Another way to avoid the issue would be to get rid of the property decorator, and leave descriptions as a normal method. That way looking it up won't cause any recursion, it will just get a bound method.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.