Search code examples
pythondecoratorpython-decoratorsdocstring

Programmatically changing docstring of a class object


I have a function and class decorator that changes them to singletons but the docstrings are kind of dropped. Transforming function returns a class object that has its own docstring which should be overwritten. How can I work around this?

def singleton(cls):
    return cls()

def singletonfunction(func):
    return func()

@singletonfunction
def Bit():
    """A 1-bit BitField; must be enclosed in a BitStruct"""
    return BitsInteger(1)

Inspecting Bit yields the BitInteger docstring, not the function's one.


Solution

  • Lets assume the original version is something like this:

    class BitsInteger:
        """BitsInteger docstring"""
        def __init__(self, num):
            pass
    
    def singleton(cls):
        return cls()
    
    def singletonfunction(func):
        return func()
    
    @singletonfunction
    def Bit():
        """A 1-bit BitField; must be enclosed in a BitStruct"""
        return BitsInteger(1)
    
    b = Bit
    print("b.__doc__ :", b.__doc__)
    

    Running this with Python3.5 gives the output:

    b.__doc_ : BitsInteger docstring
    

    This might not be what you expect. When we run with python -i original.py we can have a look around at what is really going on here:

    >>> vars()
    {'__name__': '__main__', '__builtins__': <module 'builtins' (built-in)>, 'b': <__main__.BitsInteger object at 0x7ff05d2ae3c8>, '__spec__': None, 'singletonfunction': <function singletonfunction at 0x7ff05d2b40d0>, 'singleton': <function singleton at 0x7ff05d30cd08>, '__cached__': None, 'BitsInteger': <class '__main__.BitsInteger'>, '__loader__': <_frozen_importlib.SourceFileLoader object at 0x7ff05d2eb4a8>, '__package__': None, 'Bit': <__main__.BitsInteger object at 0x7ff05d2ae3c8>, '__doc__': None}
    >>> 
    

    As you can see b is actually of type BitsInteger

    The reason is because what's really going on is that when you write:

    @singleton_function
    def Bit():
        """Bit docstring"""
        ...
        return BitsInteger(1)
    

    You are really just getting syntactic sugar for this:

    def Bit():
        """Bit docstring"""
        ...
        return BitsInteger(1)
    Bit = singleton_function(Bit)
    

    In this case Bit is the return value of singleton_function which is actually a BitsInteger. I find when you strip back the syntactic sugar it's much clearer what's going on here.

    If you would like to have the convenience of a decorator that doesn't change the docstring I'd recommend using wrapt

    import wrapt
    
    class BitsInteger:
        """BitsInteger docstring"""
        def __init__(self, num):
            pass
    
    def singleton(cls):
        return cls()
    
    @wrapt.decorator
    def singletonfunction(func):
        return func()
    
    @singletonfunction
    def Bit():
        """A 1-bit BitField; must be enclosed in a BitStruct"""
        return BitsInteger(1)
    
    b = Bit
    print("b.__doc_ :", b.__doc__)
    

    This outputs:

    b.__doc_ : A 1-bit BitField; must be enclosed in a BitStruct
    

    Now when you look at Vars() you see that 'b': <FunctionWrapper at 0x7f9a9ac42ba8 for function at 0x7f9a9a9e8c80> which is no longer a BitInteger type. Personally I like using wrapt because I get what I want immediately. Implementing that functionality and covering all the edge cases takes effort and I know that wrapt is well tested and works as intended.