-
-
Notifications
You must be signed in to change notification settings - Fork 143
Description
Line 733 in f8704ec
obj = inspect.unwrap(obj) |
PDoc uses inspect.unwrap()
in multiple places. However, this function is designed to work on functions and is not able to work on classes properly.
3.12.1
Before 3.12.3, inspect.unwrap()
would go on a class as long as __wrapper__
field exists, which is fine for functions, but for classes, it goes up to the parent class if you inherited from a decorated class. This results in PDoc rendering wrong docstrings for classes.

PyDev console: starting.
Python 3.12.10 (tags/v3.12.10:0cc8128, Apr 8 2025, 12:21:36) [MSC v.1943 64 bit (AMD64)] on win32
from pdoc_issues.class_wrapper import AlwaysOne
AlwaysOne().value
1
AlwaysOne.__doc__
" This is an `AbstractValue`'s implementation that always returns 1. "
However, ...
D:\Projects\test\.venv.d\venv-3.12.1\Scripts\python.exe -m pdoc pdoc_issues.class_wrapper --html -o html
html\pdoc_issues\class_wrapper\index.html
html\pdoc_issues\class_wrapper\class_definition.html
html\pdoc_issues\class_wrapper\util.html
Process finished with exit code 0

Investigation/debugging result: obj
is evaluated to be the parent class instead of the actual child, and thus stored in context on a child's name.
3.12.10
Starting from 3.12.3, this was changed. From now on, inspect.unwrap()
just does not unwrap a class. Which is crucial for the PDoc's ability to extract docstrings and other metadata.

https://docs.python.org/release/3.12.3/whatsnew/changelog.html#library
python/cpython#112006
Attempting to generate documentation just fails.
D:\Projects\test\.venv.d\venv-3.12.10\Scripts\python.exe -m pdoc pdoc_issues.class_wrapper --html -o html
Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "D:\Projects\test\.venv.d\venv-3.12.10\Lib\site-packages\pdoc\__main__.py", line 6, in <module>
main()
File "D:\Projects\test\.venv.d\venv-3.12.10\Lib\site-packages\pdoc\cli.py", line 559, in main
pdoc.link_inheritance()
File "D:\Projects\test\.venv.d\venv-3.12.10\Lib\site-packages\pdoc\__init__.py", line 498, in link_inheritance
for cls in _toposort(graph):
^^^^^^^^^^^^^^^^
File "D:\Projects\test\.venv.d\venv-3.12.10\Lib\site-packages\pdoc\__init__.py", line 480, in _toposort
assert not graph, f"A cyclic dependency exists amongst {graph!r}"
^^^^^^^^^
AssertionError: A cyclic dependency exists amongst {<Class 'pdoc_issues.class_wrapper.class_definition.AbstractValue'>: {<Class 'pdoc_issues.class_wrapper.class_definition.AbstractValue'>}, <Class 'pdoc_issues.class_wrapper.class_definition.AlwaysOne'>: {<Class 'pdoc_issues.class_wrapper.class_definition.AlwaysOne'>, <Class 'pdoc_issues.class_wrapper.class_definition.AbstractValue'>}, <Class 'pdoc_issues.class_wrapper.AbstractValue'>: {<Class 'pdoc_issues.class_wrapper.class_definition.AbstractValue'>}, <Class 'pdoc_issues.class_wrapper.AlwaysOne'>: {<Class 'pdoc_issues.class_wrapper.class_definition.AlwaysOne'>, <Class 'pdoc_issues.class_wrapper.class_definition.AbstractValue'>}}
Process finished with exit code 1
Code to reproduce
File pdoc_issues/class_wrapper/class_definition.py
"""
This module exports the following classes:
* `AbstractValue`
* `AlwaysOne`
"""
from .util import decorate_class
from abc import ABC, abstractmethod
@decorate_class
class AbstractValue(ABC):
""" This is `AbstractValue` class. """
@abstractmethod
def __value__(self) -> int:
""" An `AbstractValue`'s value implementation, abstract method. """
raise NotImplemented
@property
def value(self) -> int:
""" This is `AbstractValue`'s property. """
return self.__value__()
@decorate_class
class AlwaysOne(AbstractValue):
""" This is an `AbstractValue`'s implementation that always returns 1. """
def __value__(self) -> int:
return 1
__all__ = \
[
'AbstractValue',
'AlwaysOne',
]
File pdoc_issues/class_wrapper/util.py
import functools
import types
from typing import *
def wrap_first[C](cls: Type[C]) -> Type[C]:
wrapped = types.new_class(cls.__name__, (cls, ), {})
wrapped = functools.update_wrapper(wrapped, cls, updated=())
wrapped = cast(Type[C], wrapped)
return wrapped
def wrap_second[C](cls: Type[C]) -> Type[C]:
wrapped = type(cls.__name__, cls.__mro__, dict(cls.__dict__))
wrapped = functools.update_wrapper(wrapped, cls, updated=())
wrapped = cast(Type[C], wrapped)
return wrapped
def decorate_class[C](cls: Type[C]) -> Type[C]:
""" Creates a two-step class decoration. """
wrapped = wrap_first(cls)
wrapped_again = wrap_second(wrapped)
wrapped_again.__decorated__ = True
return wrapped_again
__all__ = \
[
'decorate_class',
]
Potential Solution
Write your own inspect.unwrap()
, which handles classes specifically:
- For functions, the behaviour is unchanged
- For classes, you should unwrap as long as
__module__
and__name__
are preserved
P.S.
I know generating new classes at runtime during decoration is a bad practice, but it's not my code, I just found that while using a third-party library (dataclassabc
)