pythonpython-3.xintrospection

How to reimplement Python's __qualname__ in Python 3.7? (with some minor adjustments)


The __qualname__ attribute is useful to me because it contextualizes functions; however, it's difficult for me to use for my use case because:

  1. __qualname__ returns a string. For my usecase, I need references to the parent object(s).

  2. __qualname__ sometimes returns the super class instead of the referenced class. For example:

    class Parent():
    
        def __init__(self):
            pass
    
    
    class Child(Parent):
        pass
    
    
    print(Child.__init__.__qualname__)  # Prints: "Parent.__init__"
    
  3. The package I am developing needs to be robust, and the edge cases for __qualname__ are not documented as far as I can tell.

Outside of parsing the Python file with ast, can __qualname__ be reimplemented in Python3 with inspection? How does Python implement __qualname__? In reimplementing the core functionality, I think I'll be able to adapt it for my use case.


Prior Research:

I was unable to find the qualname implementation in the Python source code.


Solution

  • If you can accept an unconventional (or cumbersome) class creation using subclasses from a BaseNode class for every attribute

    class A(BaseNode):
        class a(BaseNode):
            pass
        a = a()
    
    
    class B(BaseNode):
        class a1(A):
            pass
        a1 = a1()
    
        class a2(A):
            pass
        a2 = a2()
    
    
    class C(B):
        class c(BaseNode):
            pass
        c = c()
    

    then every attribute knows his path name in the class hierarchy. (The class creation can be simplified by using __init_subclass__() or a decorator.)

    c = C()
    assert c._pathname      == 'C'
    assert c.c._pathname    == 'C.c'
    assert c.a1._pathname   == 'C.a1'
    assert c.a1.a._pathname == 'C.a1.a'
    assert c.a2._pathname   == 'C.a2'
    assert c.a2.a._pathname == 'C.a2.a'
    

    Every attribute instance is created by demand which you can see on the print outputs

    *** <__main__.C object at 0x7fca81558190>  creates 'c' from <class '__main__.C.c'>
    *** <__main__.C object at 0x7fca81558190>  creates 'a1' from <class '__main__.B.a1'>
    *** <__main__.B.a1 object at 0x7fca8155b760>  creates 'a' from <class '__main__.A.a'>
    *** <__main__.C object at 0x7fca81558190>  creates 'a2' from <class '__main__.B.a2'>
    *** <__main__.B.a2 object at 0x7fca8155b490>  creates 'a' from <class '__main__.A.a'>
    

    The BaseNode class uses self._parent for tracking the parent object (= instance of the parent class).

        class BaseNode:
        def __init__(self, parent=None):
            self._parent = parent
    
        @property
        def _path(self):
            path = []
            obj = self
            while obj:
                path.append(obj)
                obj = obj._parent
            return path[::-1]
    
        @property
        def _pathname(self):
            return '.'.join(node.__class__.__name__ for node in self._path)
    
        def __get__(self, obj, cls=None):
            if obj is None:
                return self
            name = self.__class__.__name__
            try:
                inst = obj.__dict__[name]
            except KeyError:
                print(f'*** {obj}  creates {name!r} from {self.__class__}')
                inst = self.__class__(obj)
                obj.__dict__[name] = inst
            return inst
    

    The descriptor method __get__() dynamically creates the attributes (= BaseNode instances) on demand.

    BTW: Any comments or improvements for this approach are welcome :-)