Skip to content

Commit f194178

Browse files
authored
Use a new _best_object_type_for_member() function (#13961)
1 parent a8f2b96 commit f194178

File tree

4 files changed

+99
-142
lines changed

4 files changed

+99
-142
lines changed

doc/development/tutorials/examples/autodoc_intenum.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,20 @@
1111
from docutils.statemachine import StringList
1212

1313
from sphinx.application import Sphinx
14+
from sphinx.ext.autodoc import Documenter
1415
from sphinx.util.typing import ExtensionMetadata
1516

1617

1718
class IntEnumDocumenter(ClassDocumenter):
1819
objtype = 'intenum'
1920
directivetype = ClassDocumenter.objtype
20-
priority = 10 + ClassDocumenter.priority
21+
priority = 25
2122
option_spec = dict(ClassDocumenter.option_spec)
2223
option_spec['hex'] = bool_option
2324

2425
@classmethod
2526
def can_document_member(
26-
cls, member: Any, membername: str, isattr: bool, parent: Any
27+
cls, member: Any, membername: str, isattr: bool, parent: Documenter
2728
) -> bool:
2829
try:
2930
return issubclass(member, IntEnum)

sphinx/ext/autodoc/_documenters.py

Lines changed: 88 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import operator
34
from typing import TYPE_CHECKING, NewType, TypeVar
45

56
from docutils.statemachine import StringList
@@ -80,8 +81,6 @@ class Documenter:
8081
objtype: ClassVar = 'object'
8182
#: indentation by which to indent the directive content
8283
content_indent: ClassVar = ' '
83-
#: priority if multiple documenters return True from can_document_member
84-
priority: ClassVar = 0
8584
#: order if autodoc_member_order is set to 'groupwise'
8685
member_order: ClassVar = 0
8786
#: true if the generated content may contain titles
@@ -100,14 +99,6 @@ def get_attr(self, obj: Any, name: str, *defargs: Any) -> Any:
10099
"""getattr() override for types such as Zope interfaces."""
101100
return autodoc_attrgetter(obj, name, *defargs, registry=self.env._registry)
102101

103-
@classmethod
104-
def can_document_member(
105-
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
106-
) -> bool:
107-
"""Called to see if a member can be documented by this Documenter."""
108-
msg = 'must be implemented in subclasses'
109-
raise NotImplementedError(msg)
110-
111102
def __init__(
112103
self, directive: DocumenterBridge, name: str, indent: str = ''
113104
) -> None:
@@ -728,18 +719,16 @@ def _gather_members(
728719
member_documenters: list[tuple[Documenter, bool]] = []
729720
for member_name, member, is_attr in filtered_members:
730721
# prefer the documenter with the highest priority
731-
doccls = max(
732-
(
733-
cls
734-
for cls in registry.documenters.values()
735-
if cls.can_document_member(member, member_name, is_attr, self)
736-
),
737-
key=lambda cls: cls.priority,
738-
default=None,
722+
obj_type = _best_object_type_for_member(
723+
member=member,
724+
member_name=member_name,
725+
is_attr=is_attr,
726+
parent_documenter=self,
739727
)
740-
if doccls is None:
728+
if not obj_type:
741729
# don't know how to document this member
742730
continue
731+
doccls = registry.documenters[obj_type]
743732
# give explicitly separated module name, so that members
744733
# of inner classes can be documented
745734
module_prefix = f'{props.module_name}::'
@@ -793,13 +782,6 @@ class ModuleDocumenter(Documenter):
793782
'noindex': bool_option,
794783
}
795784

796-
@classmethod
797-
def can_document_member(
798-
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
799-
) -> bool:
800-
# don't document submodules automatically
801-
return False
802-
803785

804786
class FunctionDocumenter(Documenter):
805787
"""Specialized Documenter subclass for functions."""
@@ -809,17 +791,6 @@ class FunctionDocumenter(Documenter):
809791
objtype = 'function'
810792
member_order = 30
811793

812-
@classmethod
813-
def can_document_member(
814-
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
815-
) -> bool:
816-
# supports functions, builtins and bound methods exported at the module level
817-
return (
818-
inspect.isfunction(member)
819-
or inspect.isbuiltin(member)
820-
or (inspect.isroutine(member) and isinstance(parent, ModuleDocumenter))
821-
)
822-
823794

824795
class DecoratorDocumenter(FunctionDocumenter):
825796
"""Specialized Documenter subclass for decorator functions."""
@@ -828,9 +799,6 @@ class DecoratorDocumenter(FunctionDocumenter):
828799

829800
objtype = 'decorator'
830801

831-
# must be lower than FunctionDocumenter
832-
priority = FunctionDocumenter.priority - 1
833-
834802

835803
class ClassDocumenter(Documenter):
836804
"""Specialized Documenter subclass for classes."""
@@ -854,19 +822,6 @@ class ClassDocumenter(Documenter):
854822
'noindex': bool_option,
855823
}
856824

857-
# Must be higher than FunctionDocumenter, ClassDocumenter, and
858-
# AttributeDocumenter as NewType can be an attribute and is a class
859-
# after Python 3.10.
860-
priority = 15
861-
862-
@classmethod
863-
def can_document_member(
864-
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
865-
) -> bool:
866-
return isinstance(member, type) or (
867-
isattr and isinstance(member, (NewType, TypeVar))
868-
)
869-
870825
def get_canonical_fullname(self) -> str | None:
871826
__modname__ = safe_getattr(
872827
self.props._obj, '__module__', self.props.module_name
@@ -892,25 +847,6 @@ class ExceptionDocumenter(ClassDocumenter):
892847
objtype = 'exception'
893848
member_order = 10
894849

895-
# needs a higher priority than ClassDocumenter
896-
priority = ClassDocumenter.priority + 5
897-
898-
@classmethod
899-
def can_document_member(
900-
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
901-
) -> bool:
902-
try:
903-
return isinstance(member, type) and issubclass(member, BaseException)
904-
except TypeError as exc:
905-
# It's possible for a member to be considered a type, but fail
906-
# issubclass checks due to not being a class. For example:
907-
# https://github.com/sphinx-doc/sphinx/issues/11654#issuecomment-1696790436
908-
msg = (
909-
f'{cls.__name__} failed to discern if member {member} with'
910-
f' membername {membername} is a BaseException subclass.'
911-
)
912-
raise ValueError(msg) from exc
913-
914850

915851
class DataDocumenter(Documenter):
916852
"""Specialized Documenter subclass for data items."""
@@ -921,17 +857,10 @@ class DataDocumenter(Documenter):
921857

922858
objtype = 'data'
923859
member_order = 40
924-
priority = -10
925860
option_spec: ClassVar[OptionSpec] = dict(Documenter.option_spec)
926861
option_spec['annotation'] = annotation_option
927862
option_spec['no-value'] = bool_option
928863

929-
@classmethod
930-
def can_document_member(
931-
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
932-
) -> bool:
933-
return isinstance(parent, ModuleDocumenter) and isattr
934-
935864

936865
class MethodDocumenter(Documenter):
937866
"""Specialized Documenter subclass for methods (normal, static and class)."""
@@ -941,13 +870,6 @@ class MethodDocumenter(Documenter):
941870
objtype = 'method'
942871
directivetype = 'method'
943872
member_order = 50
944-
priority = 1 # must be more than FunctionDocumenter
945-
946-
@classmethod
947-
def can_document_member(
948-
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
949-
) -> bool:
950-
return inspect.isroutine(member) and not isinstance(parent, ModuleDocumenter)
951873

952874

953875
class AttributeDocumenter(Documenter):
@@ -961,26 +883,12 @@ class AttributeDocumenter(Documenter):
961883
option_spec['annotation'] = annotation_option
962884
option_spec['no-value'] = bool_option
963885

964-
# must be higher than the MethodDocumenter, else it will recognize
965-
# some non-data descriptors as methods
966-
priority = 10
967-
968886
@staticmethod
969887
def is_function_or_method(obj: Any) -> bool:
970888
return (
971889
inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj)
972890
)
973891

974-
@classmethod
975-
def can_document_member(
976-
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
977-
) -> bool:
978-
if isinstance(parent, ModuleDocumenter):
979-
return False
980-
if inspect.isattributedescriptor(member):
981-
return True
982-
return not inspect.isroutine(member) and not isinstance(member, type)
983-
984892

985893
class PropertyDocumenter(Documenter):
986894
"""Specialized Documenter subclass for properties."""
@@ -990,26 +898,6 @@ class PropertyDocumenter(Documenter):
990898
objtype = 'property'
991899
member_order = 60
992900

993-
# before AttributeDocumenter
994-
priority = AttributeDocumenter.priority + 1
995-
996-
@classmethod
997-
def can_document_member(
998-
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
999-
) -> bool:
1000-
if isinstance(parent, ClassDocumenter):
1001-
if inspect.isproperty(member):
1002-
return True
1003-
else:
1004-
# See FakeDirective &c in autosummary, parent might not be a
1005-
# 'proper' Documenter.
1006-
obj = parent.props._obj if hasattr(parent, 'props') else None
1007-
__dict__ = safe_getattr(obj, '__dict__', {})
1008-
obj = __dict__.get(membername)
1009-
return isinstance(obj, classmethod) and inspect.isproperty(obj.__func__)
1010-
else:
1011-
return False
1012-
1013901

1014902
class TypeAliasDocumenter(Documenter):
1015903
"""Specialized Documenter subclass for type aliases."""
@@ -1025,12 +913,6 @@ class TypeAliasDocumenter(Documenter):
1025913
'no-value': bool_option,
1026914
}
1027915

1028-
@classmethod
1029-
def can_document_member(
1030-
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any
1031-
) -> bool:
1032-
return isinstance(member, AnyTypeAliasType)
1033-
1034916

1035917
class DocstringSignatureMixin:
1036918
"""Retained for compatibility."""
@@ -1078,3 +960,83 @@ def _document_members(
1078960
real_modname=real_modname,
1079961
check_module=members_check_module and not is_attr,
1080962
)
963+
964+
965+
def _best_object_type_for_member(
966+
member: Any,
967+
member_name: str,
968+
is_attr: bool,
969+
parent_documenter: Documenter,
970+
) -> str | None:
971+
"""Return the best object type that supports documenting *member*."""
972+
filtered = []
973+
974+
# Don't document submodules automatically: 'module' is never returned.
975+
976+
try:
977+
if isinstance(member, type) and issubclass(member, BaseException):
978+
# priority must be higher than 'class'
979+
filtered.append((20, 'exception'))
980+
except TypeError as exc:
981+
# It's possible for a member to be considered a type, but fail
982+
# issubclass checks due to not being a class. For example:
983+
# https://github.com/sphinx-doc/sphinx/issues/11654#issuecomment-1696790436
984+
msg = f'Failed to discern if member {member} is a BaseException subclass.'
985+
raise ValueError(msg) from exc
986+
987+
if isinstance(member, type) or (is_attr and isinstance(member, (NewType, TypeVar))):
988+
# priority must be higher than 'function', 'class', and 'attribute'
989+
# as NewType can be an attribute and is a class after Python 3.10.
990+
filtered.append((15, 'class'))
991+
992+
if isinstance(parent_documenter, ClassDocumenter):
993+
if inspect.isproperty(member):
994+
# priority must be higher than 'attribute'
995+
filtered.append((11, 'property'))
996+
997+
# Support for class properties. Note: these only work on Python 3.9.
998+
elif hasattr(parent_documenter, 'props'):
999+
# See FakeDirective &c in autosummary, parent might not be a
1000+
# 'proper' Documenter.
1001+
__dict__ = safe_getattr(parent_documenter.props._obj, '__dict__', {})
1002+
obj = __dict__.get(member_name)
1003+
if isinstance(obj, classmethod) and inspect.isproperty(obj.__func__):
1004+
# priority must be higher than 'attribute'
1005+
filtered.append((11, 'property'))
1006+
1007+
if not isinstance(parent_documenter, ModuleDocumenter):
1008+
if inspect.isattributedescriptor(member) or not (
1009+
inspect.isroutine(member) or isinstance(member, type)
1010+
):
1011+
# priority must be higher than 'method', else it will recognise
1012+
# some non-data descriptors as methods
1013+
filtered.append((10, 'attribute'))
1014+
1015+
if inspect.isroutine(member) and not isinstance(
1016+
parent_documenter, ModuleDocumenter
1017+
):
1018+
# priority must be higher than 'function'
1019+
filtered.append((1, 'method'))
1020+
1021+
if (
1022+
inspect.isfunction(member)
1023+
or inspect.isbuiltin(member)
1024+
or (
1025+
inspect.isroutine(member)
1026+
and isinstance(parent_documenter, ModuleDocumenter)
1027+
)
1028+
):
1029+
# supports functions, builtins and bound methods exported
1030+
# at the module level
1031+
filtered.extend(((0, 'function'), (-1, 'decorator')))
1032+
1033+
if isinstance(member, AnyTypeAliasType):
1034+
filtered.append((0, 'type'))
1035+
1036+
if isinstance(parent_documenter, ModuleDocumenter) and is_attr:
1037+
filtered.append((-10, 'data'))
1038+
1039+
if filtered:
1040+
# return the highest priority object type
1041+
return max(filtered, key=operator.itemgetter(0))[1]
1042+
return None

sphinx/ext/autosummary/__init__.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
from sphinx.environment import BuildEnvironment
7171
from sphinx.errors import PycodeError
7272
from sphinx.ext.autodoc._directive_options import _AutoDocumenterOptions
73+
from sphinx.ext.autodoc._documenters import _best_object_type_for_member
7374
from sphinx.ext.autodoc._sentinels import INSTANCE_ATTR
7475
from sphinx.ext.autodoc.directive import DocumenterBridge
7576
from sphinx.ext.autodoc.importer import _format_signatures, import_module
@@ -206,7 +207,6 @@ def _get_documenter(
206207
from sphinx.ext.autodoc import DataDocumenter, ModuleDocumenter
207208

208209
if inspect.ismodule(obj):
209-
# ModuleDocumenter.can_document_member always returns False
210210
return ModuleDocumenter
211211

212212
# Construct a fake documenter for *parent*
@@ -221,14 +221,11 @@ def _get_documenter(
221221
parent_doc = parent_doc_cls(FakeDirective(), '')
222222

223223
# Get the correct documenter class for *obj*
224-
classes = [
225-
cls
226-
for cls in registry.documenters.values()
227-
if cls.can_document_member(obj, '', False, parent_doc)
228-
]
229-
if classes:
230-
classes.sort(key=lambda cls: cls.priority)
231-
return classes[-1]
224+
obj_type = _best_object_type_for_member(
225+
member=obj, member_name='', is_attr=False, parent_documenter=parent_doc
226+
)
227+
if obj_type:
228+
return registry.documenters[obj_type]
232229
else:
233230
return DataDocumenter
234231

0 commit comments

Comments
 (0)