diff --git a/CHANGES b/CHANGES index ee7e33cb1b0..4d063a6e3da 100644 --- a/CHANGES +++ b/CHANGES @@ -66,6 +66,8 @@ Features added * #8924: autodoc: Support ``bound`` argument for TypeVar * #7383: autodoc: Support typehints for properties +* #5603: autodoc: Allow to refer to a python object using its canonical name + when the object has two different names; a canonical name and an alias name * #7549: autosummary: Enable :confval:`autosummary_generate` by default * #4826: py domain: Add ``:canonical:`` option to python directives to describe the location where the object is defined diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index a01402090bc..3d33b6a8e83 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1590,6 +1590,20 @@ def get_overloaded_signatures(self) -> List[Signature]: return [] + def get_canonical_fullname(self) -> Optional[str]: + __modname__ = safe_getattr(self.object, '__module__', self.modname) + __qualname__ = safe_getattr(self.object, '__qualname__', None) + if __qualname__ is None: + __qualname__ = safe_getattr(self.object, '__name__', None) + if __qualname__ and '' in __qualname__: + # No valid qualname found if the object is defined as locals + __qualname__ = None + + if __modname__ and __qualname__: + return '.'.join([__modname__, __qualname__]) + else: + return None + def add_directive_header(self, sig: str) -> None: sourcename = self.get_sourcename() @@ -1600,6 +1614,10 @@ def add_directive_header(self, sig: str) -> None: if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals: self.add_line(' :final:', sourcename) + canonical_fullname = self.get_canonical_fullname() + if not self.doc_as_attr and canonical_fullname and self.fullname != canonical_fullname: + self.add_line(' :canonical: %s' % canonical_fullname, sourcename) + # add inheritance info, if wanted if not self.doc_as_attr and self.options.show_inheritance: sourcename = self.get_sourcename() diff --git a/tests/roots/test-ext-autodoc/target/canonical/__init__.py b/tests/roots/test-ext-autodoc/target/canonical/__init__.py new file mode 100644 index 00000000000..4ca2b339c8c --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/canonical/__init__.py @@ -0,0 +1 @@ +from target.canonical.original import Bar, Foo diff --git a/tests/roots/test-ext-autodoc/target/canonical/original.py b/tests/roots/test-ext-autodoc/target/canonical/original.py new file mode 100644 index 00000000000..42049b2165e --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/canonical/original.py @@ -0,0 +1,15 @@ +class Foo: + """docstring""" + + def meth(self): + """docstring""" + + +def bar(): + class Bar: + """docstring""" + + return Bar + + +Bar = bar() diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 8ea799f4973..824ee77c269 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -2474,3 +2474,34 @@ def test_hide_value(app): ' :meta hide-value:', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_canonical(app): + options = {'members': None, + 'imported-members': None} + actual = do_autodoc(app, 'module', 'target.canonical', options) + assert list(actual) == [ + '', + '.. py:module:: target.canonical', + '', + '', + '.. py:class:: Bar()', + ' :module: target.canonical', + '', + ' docstring', + '', + '', + '.. py:class:: Foo()', + ' :module: target.canonical', + ' :canonical: target.canonical.original.Foo', + '', + ' docstring', + '', + '', + ' .. py:method:: Foo.meth()', + ' :module: target.canonical', + '', + ' docstring', + '', + ]