diff --git a/CHANGES b/CHANGES index 701eff9d5fe..bef6587cad2 100644 --- a/CHANGES +++ b/CHANGES @@ -31,6 +31,8 @@ Bugs fixed :confval:`autodoc_typehints_description_target` is set to "documented" when its info-field-list contains ``:returns:`` field * #9657: autodoc: The base class for a subclass of mocked object is incorrect +* #9607: autodoc: Incorrect base class detection for the subclasses of the + generic class * #9630: autosummary: Failed to build summary table if :confval:`primary_domain` is not 'py' * #9670: html: Fix download file with special characters diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index a255c069144..db68dd6b96e 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1651,7 +1651,7 @@ def add_directive_header(self, sig: str) -> None: # add inheritance info, if wanted if not self.doc_as_attr and self.options.show_inheritance: - if hasattr(self.object, '__orig_bases__') and len(self.object.__orig_bases__): + if inspect.getorigbases(self.object): # A subclass of generic types # refs: PEP-560 bases = list(self.object.__orig_bases__) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index b767b8adcad..4482f20872f 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -187,6 +187,21 @@ def getmro(obj: Any) -> Tuple[Type, ...]: return tuple() +def getorigbases(obj: Any) -> Optional[Tuple[Any, ...]]: + """Get __orig_bases__ from *obj* safely.""" + if not inspect.isclass(obj): + return None + + # Get __orig_bases__ from obj.__dict__ to avoid accessing the parent's __orig_bases__. + # refs: https://github.com/sphinx-doc/sphinx/issues/9607 + __dict__ = safe_getattr(obj, '__dict__', {}) + __orig_bases__ = __dict__.get('__orig_bases__') + if isinstance(__orig_bases__, tuple) and len(__orig_bases__) > 0: + return __orig_bases__ + else: + return None + + def getslots(obj: Any) -> Optional[Dict]: """Get __slots__ attribute of the class as dict. diff --git a/tests/roots/test-ext-autodoc/target/classes.py b/tests/roots/test-ext-autodoc/target/classes.py index 7526e65bc74..d18128584af 100644 --- a/tests/roots/test-ext-autodoc/target/classes.py +++ b/tests/roots/test-ext-autodoc/target/classes.py @@ -29,6 +29,10 @@ class Quux(List[Union[int, float]]): pass +class Corge(Quux): + pass + + Alias = Foo #: docstring diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index 402a1b0c6f2..50cdff6b3a1 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -273,6 +273,21 @@ def test_show_inheritance_for_subclass_of_generic_type(app): ] +@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_show_inheritance_for_decendants_of_generic_type(app): + options = {'show-inheritance': None} + actual = do_autodoc(app, 'class', 'target.classes.Corge', options) + assert list(actual) == [ + '', + '.. py:class:: Corge(iterable=(), /)', + ' :module: target.classes', + '', + ' Bases: :py:class:`target.classes.Quux`', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_process_bases(app): def autodoc_process_bases(app, name, obj, options, bases):