This commit is contained in:
Philipp A. 2025-02-15 22:46:57 +00:00 committed by GitHub
commit 7e4e9d6d28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 190 additions and 25 deletions

View File

@ -1310,7 +1310,43 @@ class ModuleDocumenter(Documenter):
return super().sort_members(documenters, order)
class ModuleLevelDocumenter(Documenter):
class PyObjectDocumenter(Documenter):
"""Documenter for everything except modules"""
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
self.add_canonical_option()
def add_canonical_option(self) -> None:
canonical_fullname = self.get_canonical_fullname()
if (
not isinstance(self.object, NewType)
and canonical_fullname
and self.fullname != canonical_fullname
):
source_name = self.get_sourcename()
self.add_line(f' :canonical: {canonical_fullname}', source_name)
def get_canonical_fullname(self) -> str | None:
modname = safe_getattr(self.object, '__module__', self.modname)
if not modname:
return None
name = safe_getattr(self.object, '__qualname__', None)
if name is None:
name = safe_getattr(self.object, '__name__', None)
if name is None:
return None
if all(map(str.isidentifier, name.split('.'))):
return f'{modname}.{name}'
# qualname doesn't exist or is not valid
# (e.g. object is defined as <locals>)
return None
class ModuleLevelDocumenter(PyObjectDocumenter):
"""Specialized Documenter subclass for objects on module level (functions,
classes, data/constants).
"""
@ -1322,6 +1358,9 @@ class ModuleLevelDocumenter(Documenter):
return modname, [*parents, base]
if path:
modname = path.rstrip('.')
if modname.startswith('builtins.'):
modname, name = modname.split('.', 1)
parents = [*parents, name]
return modname, [*parents, base]
# if documenting a toplevel object without explicit module,
@ -1334,7 +1373,7 @@ class ModuleLevelDocumenter(Documenter):
return modname, [*parents, base]
class ClassLevelDocumenter(Documenter):
class ClassLevelDocumenter(PyObjectDocumenter):
"""Specialized Documenter subclass for objects on class level (methods,
attributes).
"""
@ -1905,19 +1944,9 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
return []
def get_canonical_fullname(self) -> str | None:
__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 '<locals>' in __qualname__:
# No valid qualname found if the object is defined as locals
__qualname__ = None
if __modname__ and __qualname__:
return f'{__modname__}.{__qualname__}'
else:
return None
def add_canonical_option(self) -> None:
if not self.doc_as_attr:
super().add_canonical_option()
def add_directive_header(self, sig: str) -> None:
sourcename = self.get_sourcename()

View File

@ -1 +1 @@
from target.canonical.original import Bar, Foo
from target.canonical.original import Bar, Foo, bar

View File

@ -6,6 +6,8 @@ class Foo:
def bar():
"""docstring"""
class Bar:
"""docstring"""

View File

@ -1560,6 +1560,7 @@ class _EnumFormatter:
*,
args: str,
indent: int,
canonical: bool = False,
**options: Any,
) -> list[str]:
prefix = indent * ' '
@ -1573,8 +1574,10 @@ class _EnumFormatter:
'',
f'{prefix}.. py:{role}:: {name}{args}',
f'{prefix}{tab}:module: {self.module}',
*itertools.starmap(rst_option, options.items()),
]
if canonical:
lines.append(f'{prefix}{tab}:canonical: {self.module}.{name}')
lines += itertools.starmap(rst_option, options.items())
if doc:
lines.extend(['', f'{prefix}{tab}{doc}'])
lines.append('')
@ -1588,11 +1591,20 @@ class _EnumFormatter:
role: str,
args: str = '',
indent: int = 3,
canonical: bool = False,
**rst_options: Any,
) -> list[str]:
"""Get the RST lines for a named attribute, method, etc."""
qualname = f'{self.name}.{entry_name}'
return self._node(role, qualname, doc, args=args, indent=indent, **rst_options)
return self._node(
role,
qualname,
doc,
args=args,
indent=indent,
canonical=canonical,
**rst_options,
)
def preamble_lookup(
self, doc: str, *, indent: int = 0, **options: Any
@ -1657,15 +1669,37 @@ class _EnumFormatter:
*flags: str,
args: str = '()',
indent: int = 3,
canonical: bool = False,
) -> list[str]:
rst_options = dict.fromkeys(flags, '')
return self.entry(
name, doc, role='method', args=args, indent=indent, **rst_options
name,
doc,
role='method',
args=args,
indent=indent,
canonical=canonical,
**rst_options,
)
def member(self, name: str, value: Any, doc: str, *, indent: int = 3) -> list[str]:
def member(
self,
name: str,
value: Any,
doc: str,
*,
indent: int = 3,
canonical: bool = False,
) -> list[str]:
rst_options = {'value': repr(value)}
return self.entry(name, doc, role='attribute', indent=indent, **rst_options)
return self.entry(
name,
doc,
role='attribute',
indent=indent,
canonical=canonical,
**rst_options,
)
@pytest.fixture
@ -1729,8 +1763,8 @@ def test_enum_class_with_data_type(app, autodoc_enum_options):
actual = do_autodoc(app, 'class', fmt.target, options)
assert list(actual) == [
*fmt.preamble_lookup('this is enum class'),
*fmt.entry('dtype', 'docstring', role='property'),
*fmt.method('isupper', 'inherited'),
*fmt.entry('dtype', 'docstring', role='property', canonical=True),
*fmt.method('isupper', 'inherited', canonical=True),
*fmt.method('say_goodbye', 'docstring', 'classmethod'),
*fmt.method('say_hello', 'docstring'),
*fmt.member('x', 'x', ''),
@ -2167,12 +2201,74 @@ def test_bound_method(app):
'',
'.. py:function:: bound_method()',
' :module: target.bound_method',
' :canonical: target.bound_method.Cls.method',
'',
' Method docstring',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_coroutine(app):
actual = do_autodoc(app, 'function', 'target.functions.coroutinefunc')
assert list(actual) == [
'',
'.. py:function:: coroutinefunc()',
' :module: target.functions',
' :async:',
'',
]
options = {'members': None}
actual = do_autodoc(app, 'class', 'target.coroutine.AsyncClass', options)
assert list(actual) == [
'',
'.. py:class:: AsyncClass()',
' :module: target.coroutine',
'',
'',
' .. py:method:: AsyncClass.do_asyncgen()',
' :module: target.coroutine',
' :async:',
'',
' A documented async generator',
'',
'',
' .. py:method:: AsyncClass.do_coroutine()',
' :module: target.coroutine',
' :async:',
'',
' A documented coroutine function',
'',
'',
' .. py:method:: AsyncClass.do_coroutine2()',
' :module: target.coroutine',
' :async:',
' :classmethod:',
'',
' A documented coroutine classmethod',
'',
'',
' .. py:method:: AsyncClass.do_coroutine3()',
' :module: target.coroutine',
' :async:',
' :staticmethod:',
'',
' A documented coroutine staticmethod',
'',
]
# force-synchronized wrapper
actual = do_autodoc(app, 'function', 'target.coroutine.sync_func')
assert list(actual) == [
'',
'.. py:function:: sync_func()',
' :module: target.coroutine',
' :canonical: target.coroutine._other_coro_func',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_partialmethod(app):
expected = [
@ -3136,9 +3232,17 @@ def test_canonical(app):
'',
' .. py:method:: Foo.meth()',
' :module: target.canonical',
' :canonical: target.canonical.original.Foo.meth',
'',
' docstring',
'',
'',
'.. py:function:: bar()',
' :module: target.canonical',
' :canonical: target.canonical.original.bar',
'',
' docstring',
'',
]

View File

@ -145,6 +145,7 @@ def test_autoattribute_GenericAlias(app):
'',
'.. py:attribute:: Class.T',
' :module: target.genericalias',
' :canonical: typing.List',
'',
' A list of int',
'',
@ -153,6 +154,21 @@ def test_autoattribute_GenericAlias(app):
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_TypeVar(app):
actual = do_autodoc(app, 'attribute', 'target.typevar.Class.T1')
assert list(actual) == [
'',
'.. py:attribute:: Class.T1',
' :module: target.typevar',
' :canonical: target.typevar.T1',
' :value: ~T1',
'',
' T1',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autoattribute_hide_value(app):
actual = do_autodoc(app, 'attribute', 'target.hide_value.Foo.SENTINEL1')

View File

@ -526,6 +526,7 @@ def test_autoattribute_TypeVar_module_level(app):
'',
'.. py:class:: Class.T1',
' :module: target.typevar',
' :canonical: target.typevar.T1',
'',
' T1',
'',

View File

@ -80,6 +80,7 @@ def test_autodata_GenericAlias(app: SphinxTestApp) -> None:
'',
'.. py:data:: T',
' :module: target.genericalias',
' :canonical: typing.List',
'',
' A list of int',
'',

View File

@ -71,6 +71,7 @@ def test_method(app):
'',
'.. py:function:: method(arg1, arg2)',
' :module: target.callable',
' :canonical: target.callable.Callable.method',
'',
' docstring of Callable.method().',
'',
@ -79,11 +80,14 @@ def test_method(app):
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_builtin_function(app):
import os
actual = do_autodoc(app, 'function', 'os.umask')
assert list(actual) == [
'',
'.. py:function:: umask(mask, /)',
' :module: os',
f' :canonical: {os.name}.umask',
'',
' Set the current numeric umask and return the previous umask.',
'',
@ -95,8 +99,8 @@ def test_methoddescriptor(app):
actual = do_autodoc(app, 'function', 'builtins.int.__add__')
assert list(actual) == [
'',
'.. py:function:: __add__(self, value, /)',
' :module: builtins.int',
'.. py:function:: int.__add__(self, value, /)',
' :module: builtins',
'',
' Return self+value.',
'',
@ -192,6 +196,7 @@ def test_synchronized_coroutine(app):
'',
'.. py:function:: sync_func()',
' :module: target.coroutine',
' :canonical: target.coroutine._other_coro_func',
'',
]

View File

@ -334,6 +334,7 @@ def test_autodoc_inherit_docstrings_for_inherited_members(app):
'',
' .. py:method:: Derived.inheritedclassmeth()',
' :module: target.inheritance',
' :canonical: target.inheritance.Base.inheritedclassmeth',
' :classmethod:',
'',
' Inherited class method.',
@ -347,6 +348,7 @@ def test_autodoc_inherit_docstrings_for_inherited_members(app):
'',
' .. py:method:: Derived.inheritedstaticmeth(cls)',
' :module: target.inheritance',
' :canonical: target.inheritance.Base.inheritedstaticmeth',
' :staticmethod:',
'',
' Inherited static method.',
@ -370,6 +372,7 @@ def test_autodoc_inherit_docstrings_for_inherited_members(app):
'',
' .. py:method:: Derived.inheritedclassmeth()',
' :module: target.inheritance',
' :canonical: target.inheritance.Base.inheritedclassmeth',
' :classmethod:',
'',
' Inherited class method.',
@ -377,6 +380,7 @@ def test_autodoc_inherit_docstrings_for_inherited_members(app):
'',
' .. py:method:: Derived.inheritedstaticmeth(cls)',
' :module: target.inheritance',
' :canonical: target.inheritance.Base.inheritedstaticmeth',
' :staticmethod:',
'',
' Inherited static method.',
@ -665,6 +669,7 @@ def test_mocked_module_imports(app):
'',
'.. py:data:: Alias',
' :module: target.need_mocks',
' :canonical: missing_package2.missing_module2.Class',
'',
' docstring',
'',
@ -677,6 +682,7 @@ def test_mocked_module_imports(app):
'',
' .. py:attribute:: TestAutodoc.Alias',
' :module: target.need_mocks',
' :canonical: missing_package2.missing_module2.Class',
'',
' docstring',
'',
@ -1668,6 +1674,7 @@ def test_autodoc_typehints_format_fully_qualified_for_generic_alias(app):
'',
'.. py:data:: L',
' :module: target.genericalias',
' :canonical: typing.List',
'',
' A list of Class',
'',