Skip to content

Commit a4a8acc

Browse files
authored
Extract _gather_members() (#13968)
1 parent 3dec862 commit a4a8acc

File tree

3 files changed

+322
-265
lines changed

3 files changed

+322
-265
lines changed

sphinx/ext/autodoc/_documenters.py

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

3-
import operator
43
from typing import TYPE_CHECKING, NewType, TypeVar
54

65
from docutils.statemachine import StringList
@@ -16,16 +15,15 @@
1615
member_order_option,
1716
members_option,
1817
)
19-
from sphinx.ext.autodoc._member_finder import _filter_members, _get_members_to_document
18+
from sphinx.ext.autodoc._member_finder import _document_members
2019
from sphinx.ext.autodoc._renderer import _add_content, _directive_header_lines
21-
from sphinx.ext.autodoc._sentinels import ALL
2220
from sphinx.ext.autodoc.importer import _load_object_by_name
2321
from sphinx.ext.autodoc.mock import ismock
2422
from sphinx.locale import _, __
2523
from sphinx.pycode import ModuleAnalyzer
2624
from sphinx.util import inspect, logging
2725
from sphinx.util.inspect import safe_getattr
28-
from sphinx.util.typing import AnyTypeAliasType, restify, stringify_annotation
26+
from sphinx.util.typing import restify, stringify_annotation
2927

3028
if TYPE_CHECKING:
3129
from collections.abc import Iterator
@@ -373,46 +371,6 @@ def _assemble_more_content(
373371

374372
return more_content
375373

376-
def sort_members(
377-
self, documenters: list[tuple[Documenter, bool]], order: str
378-
) -> list[tuple[Documenter, bool]]:
379-
"""Sort the given member list."""
380-
if order == 'groupwise':
381-
# sort by group; alphabetically within groups
382-
documenters.sort(key=lambda e: (e[0].props._groupwise_order_key, e[0].name))
383-
elif order == 'bysource':
384-
if (
385-
isinstance(self, ModuleDocumenter)
386-
and not self.options.ignore_module_all
387-
and (module_all := self.props.all)
388-
):
389-
# Sort by __all__
390-
module_all_idx = {name: idx for idx, name in enumerate(module_all)}
391-
module_all_len = len(module_all)
392-
393-
def key_func(entry: tuple[Documenter, bool]) -> int:
394-
fullname = entry[0].name.split('::')[1]
395-
return module_all_idx.get(fullname, module_all_len)
396-
397-
documenters.sort(key=key_func)
398-
399-
# By default, member discovery order matches source order,
400-
# as dicts are insertion-ordered from Python 3.7.
401-
elif self.analyzer is not None:
402-
# sort by source order, by virtue of the module analyzer
403-
tagorder = self.analyzer.tagorder
404-
tagorder_len = len(tagorder)
405-
406-
def key_func(entry: tuple[Documenter, bool]) -> int:
407-
fullname = entry[0].name.split('::')[1]
408-
return tagorder.get(fullname, tagorder_len)
409-
410-
documenters.sort(key=key_func)
411-
else: # alphabetical
412-
documenters.sort(key=lambda e: e[0].name)
413-
414-
return documenters
415-
416374
def generate(
417375
self,
418376
more_content: StringList | None = None,
@@ -475,8 +433,8 @@ def _generate(
475433
if self.real_modname != guess_modname:
476434
# Add module to dependency list if target object is defined in other module.
477435
try:
478-
analyzer = ModuleAnalyzer.for_module(guess_modname)
479-
self.directive.record_dependencies.add(analyzer.srcname)
436+
srcname = ModuleAnalyzer.for_module(guess_modname).srcname
437+
self.directive.record_dependencies.add(srcname)
480438
except PycodeError:
481439
pass
482440

@@ -512,121 +470,25 @@ def _generate(
512470
self.add_content(more_content, indent=indent)
513471

514472
# document members, if possible
515-
has_members = isinstance(self, ModuleDocumenter) or (
516-
isinstance(self, ClassDocumenter) and not self.props.doc_as_attr
517-
)
518-
if has_members:
519-
want_all = bool(
520-
all_members
521-
or self.options.inherited_members
522-
or self.options.members is ALL
523-
)
524-
member_documenters = self._gather_members(want_all=want_all, indent=indent)
525-
# for implicit module members, check __module__ to avoid
526-
# documenting imported objects
527-
members_check_module = bool(
528-
isinstance(self, ModuleDocumenter)
529-
and want_all
530-
and (self.options.ignore_module_all or self.props.all is None)
531-
)
532-
_document_members(
533-
member_documenters=member_documenters,
534-
real_modname=self.real_modname,
535-
members_check_module=members_check_module,
536-
)
537-
538-
def _gather_members(
539-
self, *, want_all: bool, indent: str
540-
) -> list[tuple[Documenter, bool]]:
541-
"""Generate reST for member documentation.
542-
543-
If *want_all* is True, document all members, else those given by
544-
*self.options.members*.
545-
"""
546-
if not isinstance(self, (ModuleDocumenter, ClassDocumenter)):
547-
msg = 'must be implemented in subclasses'
548-
raise NotImplementedError(msg)
549-
550-
current_document = self._current_document
551-
events = self._events
552-
registry = self.env._registry
553-
props = self.props
554-
indent += ' ' * (props.obj_type != 'module')
555-
556-
# set current namespace for finding members
557-
current_document.autodoc_module = props.module_name
558-
if props.parts:
559-
current_document.autodoc_class = props.parts[0]
560-
561-
inherited_members = frozenset(self.options.inherited_members or ())
562-
if self.analyzer:
563-
self.analyzer.analyze()
564-
attr_docs = self.analyzer.attr_docs
565-
else:
566-
attr_docs = {}
567-
found_members = _get_members_to_document(
568-
want_all=want_all,
569-
get_attr=self.get_attr,
570-
inherit_docstrings=self.config.autodoc_inherit_docstrings,
571-
props=props,
572-
opt_members=self.options.members or (),
573-
inherited_members=inherited_members,
574-
ignore_module_all=bool(self.options.ignore_module_all),
575-
attr_docs=attr_docs,
576-
)
577-
filtered_members = _filter_members(
578-
found_members,
579-
want_all=want_all,
580-
events=events,
473+
analyzer = self.analyzer
474+
if analyzer is not None:
475+
analyzer.analyze()
476+
_document_members(
477+
all_members=all_members,
478+
analyzer_order=analyzer.tagorder if analyzer is not None else {},
479+
attr_docs=analyzer.attr_docs if analyzer is not None else {},
480+
config=self.config,
481+
current_document=self._current_document,
482+
directive=self.directive,
483+
events=self._events,
581484
get_attr=self.get_attr,
582-
inherit_docstrings=self.config.autodoc_inherit_docstrings,
485+
indent=indent,
486+
name=self.name,
583487
options=self.options,
584-
orig_name=self.name,
585-
props=props,
586-
inherited_members=inherited_members,
587-
exclude_members=self.options.exclude_members,
588-
special_members=self.options.special_members,
589-
private_members=self.options.private_members,
590-
undoc_members=self.options.undoc_members,
591-
attr_docs=attr_docs,
488+
props=self.props,
489+
real_modname=self.real_modname,
490+
registry=self.env._registry,
592491
)
593-
# document non-skipped members
594-
member_documenters: list[tuple[Documenter, bool]] = []
595-
for member_name, member, is_attr in filtered_members:
596-
# prefer the documenter with the highest priority
597-
obj_type = _best_object_type_for_member(
598-
member=member,
599-
member_name=member_name,
600-
is_attr=is_attr,
601-
parent_obj_type=self.objtype,
602-
parent_props=self.props,
603-
)
604-
if not obj_type:
605-
# don't know how to document this member
606-
continue
607-
doccls = registry.documenters[obj_type]
608-
# give explicitly separated module name, so that members
609-
# of inner classes can be documented
610-
module_prefix = f'{props.module_name}::'
611-
full_mname = module_prefix + '.'.join((*props.parts, member_name))
612-
documenter = doccls(self.directive, full_mname, indent)
613-
614-
# We now try to import all objects before ordering them. This is to
615-
# avoid possible circular imports if we were to import objects after
616-
# their associated documenters have been sorted.
617-
if documenter._load_object_by_name() is None:
618-
continue
619-
620-
member_documenters.append((documenter, is_attr))
621-
622-
member_order = self.options.member_order or self.config.autodoc_member_order
623-
member_documenters = self.sort_members(member_documenters, member_order)
624-
625-
# reset current objects
626-
current_document.autodoc_module = ''
627-
current_document.autodoc_class = ''
628-
629-
return member_documenters
630492

631493

632494
class ModuleDocumenter(Documenter):
@@ -765,104 +627,3 @@ def autodoc_attrgetter(
765627
return func(obj, name, *defargs)
766628

767629
return safe_getattr(obj, name, *defargs)
768-
769-
770-
def _document_members(
771-
*,
772-
member_documenters: list[tuple[Documenter, bool]],
773-
real_modname: str,
774-
members_check_module: bool,
775-
) -> None:
776-
"""Generate reST for member documentation.
777-
778-
If *all_members* is True, document all members, else those given by
779-
*self.options.members*.
780-
"""
781-
for documenter, is_attr in member_documenters:
782-
assert documenter.props.module_name
783-
# We can directly call ._generate() since the documenters
784-
# already called ``_load_object_by_name()`` before.
785-
#
786-
# Note that those two methods above do not emit events, so
787-
# whatever objects we deduced should not have changed.
788-
documenter._generate(
789-
all_members=True,
790-
real_modname=real_modname,
791-
check_module=members_check_module and not is_attr,
792-
)
793-
794-
795-
def _best_object_type_for_member(
796-
member: Any,
797-
member_name: str,
798-
is_attr: bool,
799-
*,
800-
parent_obj_type: str,
801-
parent_props: _ItemProperties | None,
802-
) -> str | None:
803-
"""Return the best object type that supports documenting *member*."""
804-
filtered = []
805-
806-
# Don't document submodules automatically: 'module' is never returned.
807-
808-
try:
809-
if isinstance(member, type) and issubclass(member, BaseException):
810-
# priority must be higher than 'class'
811-
filtered.append((20, 'exception'))
812-
except TypeError as exc:
813-
# It's possible for a member to be considered a type, but fail
814-
# issubclass checks due to not being a class. For example:
815-
# https://github.com/sphinx-doc/sphinx/issues/11654#issuecomment-1696790436
816-
msg = f'Failed to discern if member {member} is a BaseException subclass.'
817-
raise ValueError(msg) from exc
818-
819-
if isinstance(member, type) or (is_attr and isinstance(member, (NewType, TypeVar))):
820-
# priority must be higher than 'function', 'class', and 'attribute'
821-
# as NewType can be an attribute and is a class after Python 3.10.
822-
filtered.append((15, 'class'))
823-
824-
if parent_obj_type in {'class', 'exception'}:
825-
if inspect.isproperty(member):
826-
# priority must be higher than 'attribute'
827-
filtered.append((11, 'property'))
828-
829-
# See _get_documenter() in autosummary, parent_props might be None.
830-
elif parent_props is not None:
831-
# Support for class properties. Note: these only work on Python 3.9.
832-
__dict__ = safe_getattr(parent_props._obj, '__dict__', {})
833-
obj = __dict__.get(member_name)
834-
if isinstance(obj, classmethod) and inspect.isproperty(obj.__func__):
835-
# priority must be higher than 'attribute'
836-
filtered.append((11, 'property'))
837-
838-
if parent_obj_type != 'module':
839-
if inspect.isattributedescriptor(member) or not (
840-
inspect.isroutine(member) or isinstance(member, type)
841-
):
842-
# priority must be higher than 'method', else it will recognise
843-
# some non-data descriptors as methods
844-
filtered.append((10, 'attribute'))
845-
846-
if inspect.isroutine(member) and parent_obj_type != 'module':
847-
# priority must be higher than 'function'
848-
filtered.append((1, 'method'))
849-
850-
if (
851-
inspect.isfunction(member)
852-
or inspect.isbuiltin(member)
853-
or (inspect.isroutine(member) and parent_obj_type == 'module')
854-
):
855-
# supports functions, builtins and bound methods exported
856-
# at the module level
857-
filtered.extend(((0, 'function'), (-1, 'decorator')))
858-
859-
if isinstance(member, AnyTypeAliasType):
860-
filtered.append((0, 'type'))
861-
862-
if parent_obj_type == 'module' and is_attr:
863-
filtered.append((-10, 'data'))
864-
865-
if filtered:
866-
# return the highest priority object type
867-
return max(filtered, key=operator.itemgetter(0))[1]
868-
return None

0 commit comments

Comments
 (0)