How to get all declared or just typed parameters of a class

86 views Asked by At

How to write a function that prints out all attributes of a class with types?

class Parent:
    parent_param: str
    parent_default: str | None = None

    def find_params_meta(self):
        ...
        # enter magic code, filter out methods and attributes starting with `"_"`

class Child(Parent):
    child_param: str
    _params_meta: dict[str, Any] | None = None

    def __init__(self, parent_param: str, child_param: str):
        self._params_meta = self.find_params_meta()
        self.child_param = child_param
        self.parent_param = parent_param

assert Child("parent param", "child param")._params_meta == {
    "parent_param": {"types": [str]},
    "parent_default": {"types": [str, None], "default": None},
    "child_param": {"types": [str]},
}

The parent_param and child_param attributes are not instantiated. getattr(self, "parent_param") raises the AttributeError: 'Child' object has no attribute 'parent_param'. type(self).__dict__, dir(self) or inspect.getmembers(self) are not printing them. inspect.get_annotations(type(self)) is closer as it prints {'child_param': <class 'str'>, ...}, but it does not return Parent's attributes.

2

There are 2 answers

1
Bill Huneke On BEST ANSWER

Building on the code you posted:

class Parent:
    ...

    @classmethod
    def find_params_meta(cls):
        return {
            k: v
            for c in reversed(cls.__mro__)
            for k, v in inspect.get_annotations(c).items()
        }

Edit: added reversed since you probably want to give preference to types set on child classes... though it is probably not valid to override the type for a child class (Liskov Substitution Principle)

0
cards On

To get the attributes of a class with annotations use the __annotations__ attribute to get back a the results in a dictionary form

class A:
    parent_param: str
    parent_default: str | None = None


class B(A):
    x:int = 10
    y = 1


def annotation_finder(cls, child2root=True):
    annots = {}

    # fix the order
    ordered_cls = cls.__mro__[:-1] # skip the last class: object
    if not child2root:
        ordered_cls = ordered_cls[::-1] # parent2child

    # update annotations of parents classes
    for c in ordered_cls:
        annots.update(c.__annotations__)

    return annots


print(annotation_finder(B, child2root=True))
#{'x': <class 'int'>, 'parent_param': <class 'str'>, 'parent_default': str | None}