Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## Unreleased: pdoc next

- Fix overridden methods being shown as inherited when a class mixes a `Generic[...]` base with a plain base class.
([#887](https://github.com/mitmproxy/pdoc/pull/887), @SAY-5)
- Fix incorrect ordering of `start-after`/`end-before` in RST include directives.
([#876](https://github.com/mitmproxy/pdoc/pull/876), @JohanKarlbergg)
- Support Pydantic [`computed_field`](https://docs.pydantic.dev/2.0/usage/computed_fields/) descriptions ([#855](https://github.com/mitmproxy/pdoc/pull/855), @avhz)
Expand Down
10 changes: 8 additions & 2 deletions pdoc/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,9 +683,15 @@ def _bases(self) -> tuple[type, ...]:

# __mro__ and __orig_bases__ differ between Python versions and special cases like TypedDict/NamedTuple.
# This here is a pragmatic approximation of what we want.
# Only prepend __orig_bases__ entries absent from __mro__, so a plain base cannot shadow the class itself.
mro = self.obj.__mro__
return (
*(base for base in orig_bases if isinstance(base, type)),
*self.obj.__mro__,
*(
base
for base in orig_bases
if isinstance(base, type) and base not in mro
),
*mro,
)

@cached_property
Expand Down
16 changes: 16 additions & 0 deletions test/test_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,19 @@ def test_source_file_method():
assert m.members["Foo"].members["a_cached_function"].source_file == (
here / "testdata" / "demo_long.py"
)


def test_overridden_method_with_generic_base():
mod = extract.load_module(
extract.parse_spec(here / "testdata" / "generic_mixin.py")
)

m = Module(mod)
cls = m.members["Mixed"]
assert isinstance(cls, Class)

for name in ("generic_method", "plain_method"):
member = cls.members[name]
assert member.taken_from == ("generic_mixin", f"Mixed.{name}")
assert member.docstring == f"{name} implementation docstring"
assert member in cls.own_members
23 changes: 23 additions & 0 deletions test/testdata/generic_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from abc import ABC
from typing import Generic
from typing import TypeVar

T = TypeVar("T")


class GenericBase(ABC, Generic[T]):
def generic_method(self):
pass


class PlainBase(ABC):
def plain_method(self):
pass


class Mixed(GenericBase[T], PlainBase):
def generic_method(self):
"""generic_method implementation docstring"""

def plain_method(self):
"""plain_method implementation docstring"""
Loading