From dee40fd323fd078837d708f5652a22cdf37af978 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 12 May 2026 14:07:43 -0700 Subject: [PATCH 1/2] fix: do not treat overridden methods as inherited when mixing a generic base with a plain base --- CHANGELOG.md | 1 + pdoc/doc.py | 10 ++++++++-- test/test_doc.py | 16 ++++++++++++++++ test/testdata/generic_mixin.py | 23 +++++++++++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 test/testdata/generic_mixin.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d64bb2a6..2a824aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Unreleased: pdoc next +- Fix overridden methods being shown as inherited when a class mixes a `Generic[...]` base with a plain base class. - 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) diff --git a/pdoc/doc.py b/pdoc/doc.py index 41c0d673..c2c6d1ae 100644 --- a/pdoc/doc.py +++ b/pdoc/doc.py @@ -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 diff --git a/test/test_doc.py b/test/test_doc.py index 3fcf96d4..56dd0fdf 100644 --- a/test/test_doc.py +++ b/test/test_doc.py @@ -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 diff --git a/test/testdata/generic_mixin.py b/test/testdata/generic_mixin.py new file mode 100644 index 00000000..a1566b2c --- /dev/null +++ b/test/testdata/generic_mixin.py @@ -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""" From 72f2749b6fb9f0afbbe35995d694b2ec7a64f640 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 21:08:33 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a824aaf..12ea0ad9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## 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)