I have a python function that I am documenting with a standard docstring:
"""
Function description
Parameters
----------
p0 : numpy.ndarray, shape (...,4)
p0 description 1
p0 description 2
p1 : int, optional
p1 description
Returns
-------
Return value : my.Returnclass
"""
When I inherit this function from another class and override it, I would like to still make use of the same docstring but modify the return value accordingly. I created a decorator function for this purpose that looks as following and is being used like this:
from typing import Callable, get_type_hints
def modified_docstring(f: Callable) -> Callable:
"""
Decorator: Combine another function's docstring with a given docstring.
Parameters
----------
f : function
Function of which the docstring is taken.
"""
def replace_return_value(original_func, decorated_func):
original_docstring = original_func.__doc__
return_class_original = get_type_hints(original_func).get("return",None)
return_class_decorated = get_type_hints(decorated_func).get("return",None)
if return_class_decorated and return_class_original!=return_class_decorated:
class_name = return_class_decorated.__name__
class_module = return_class_decorated.__module__.split(".")[0]
return _re.sub(r'(Returns([ ]*\n[ ]*)-------([ ]*\n[ ]*)[A-Za-z0-9 ]* : )([A-Za-z0-9\.]*)\n',
fr'\1{class_module}.{class_name}\2',original_docstring)
else:
return original_docstring
def _decorator(func):
func.__doc__ = replace_return_value(f,func)
return func
return _decorator
class Superclass:
def my_super_function() -> 'Superclass':
"""
Function description
Returns
-------
Return value : test.Superclass
"""
return Superclass()
class DerivedClass(Superclass):
@modified_docstring(Superclass.my_super_function)
def my_derived_function() -> 'DerivedClass':
return DerivedClass()
However, because the typehints are not properly initiated in DerivedClass
when the decorator function is being called upon module initialization, this returns the following error:
File "test.py", line 48, in <module>
class DerivedClass(Superclass):
File "test.py", line 50, in DerivedClass
def my_derived_function() -> 'DerivedClass':
File "test.py", line 24, in _decorator
func.__doc__ = replace_return_value(f.__doc__,f,func)
File "test.py", line 14, in replace_return_value
return_class_decorated = get_type_hints(decorated_func).get("return",None)
File "/usr/lib/python3.8/typing.py", line 1264, in get_type_hints
value = _eval_type(value, globalns, localns)
File "/usr/lib/python3.8/typing.py", line 270, in _eval_type
return t._evaluate(globalns, localns)
File "/usr/lib/python3.8/typing.py", line 518, in _evaluate
eval(self.__forward_code__, globalns, localns),
File "<string>", line 1, in <module>
NameError: name 'DerivedClass' is not defined
Is there a clean way to force the typehints to be initialized when the decorator function is being called, or to delay the initialization of the decorator functions until the typehints are properly initialized?
The problem is that 'DerivedClass' doesn't exist in the module globals yet when 'get_type_hints' is called.
Edit: Easiest solution:
wrap the call to 'get_type_hints' in a try except and when you get a NameError and func.__annotations__.get("return", None)
returns a string just assume the function is a member function of the return values class and use the module of the function as the module.
Warning: Both easy-solutions will have a tough time with generics and may result in incorrect type hints when e.g. "List[MyDerivedCls]"
is used it will most likely result in a type documentation of my_module.List[MyDerivedCls]
You can either avoid calling 'get_type_hints' or apply the actual docstring change in a separate call.
Easy solution - don't evaluate type hint and do what 'get_type_hints' does by hand:
Access the return value directly without evaluating the forward reference by using func.__annotations__.get("return", None)
. That way you'll get DerivedClass
as a string. If you need the module where the class is defined you can look int the module of the function and search there and if the value doesn't exist assume that it's the class the function is defined in.
func_return = func.__annotations__.get("return", None)
if isinstance(decorated_func_return, str):
func_return_cls_name = func_return
func_return = sys.modules.get(func.__module__, {}).get(func_return, None)
if func_return is None:
# we haven't found the class in the module of the function - assume it's returning an instance of the class this function is a member of
func_return_module = func.__module__
if func_return is not None:
func_return_cls_name = func_return.__name__
func_return_module = func_return.__module__
You can find the fields defined on a function in the 'callable types' section on this page in the docs
More tedious solution: Delay the docstring modifications
Make it such that your modified_docstring
decorator only marks the function as 'this function should have its docstring modified' and then make a call to a function that applies these modifications.
You cannot use a decorator there as 'DerivedClass' won't exist in the module yet when it's called.
That would look something like this.
def modified_docstring(original_func):
def _wrapper(decorated_func):
decorated_func.__my_docstring_marker__ = original_func
return decorated_func
return _wrapper
def apply_docstring_modifications(cls): # looks like a decorator but doesn't work if used as such
for func_name, func in inspect.getmembers(cls, inspect.isfunction):
if not hasattr(func, "__my_docstring_marker"):
continue
func = function_that_does_docstring_modification(func, func.__my_docstring_marker__)
setattr(cls, func_name, func)
class MyDerivedCls(...):
...
apply_docstring_modifications(MyDerivedCls)
Hope this helped.