From 9e2045fd10b5198c5772bcbfe3388b7ae62f0500 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 15 Aug 2020 00:29:07 +0900 Subject: [PATCH] Close #8119: autodoc: Control visibility of module member not in __all__ This allows `autodoc-skip-member` handlers to determine whether a member not included in `__all__` attribute of the module should be documented or not. --- CHANGES | 4 +++ sphinx/ext/autodoc/__init__.py | 51 ++++++++++++++++++++------------ tests/test_ext_autodoc_events.py | 25 ++++++++++++++++ 3 files changed, 61 insertions(+), 19 deletions(-) diff --git a/CHANGES b/CHANGES index d6870c7509c..13a9c77c139 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,10 @@ Deprecated Features added -------------- +* #8119: autodoc: Allow to determine whether a member not included in + ``__all__`` attribute of the module should be documented or not via + :event:`autodoc-skip-member` event + Bugs fixed ---------- diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 1d2d4016767..f28cd163e40 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -269,12 +269,13 @@ class ObjectMember(tuple): interface. """ - def __new__(cls, name: str, obj: Any) -> Any: + def __new__(cls, name: str, obj: Any, **kwargs: Any) -> Any: return super().__new__(cls, (name, obj)) # type: ignore - def __init__(self, name: str, obj: Any) -> None: + def __init__(self, name: str, obj: Any, skipped: bool = False) -> None: self.__name__ = name self.object = obj + self.skipped = skipped ObjectMembers = Union[List[ObjectMember], List[Tuple[str, Any]]] @@ -662,7 +663,8 @@ def is_filtered_inherited_member(name: str) -> bool: attr_docs = {} # process members and determine which to skip - for (membername, member) in members: + for obj in members: + membername, member = obj # if isattr is True, the member is documented as an attribute if member is INSTANCEATTR: isattr = True @@ -739,6 +741,10 @@ def is_filtered_inherited_member(name: str) -> bool: # ignore undocumented members if :undoc-members: is not given keep = has_doc or self.options.undoc_members + if isinstance(obj, ObjectMember) and obj.skipped: + # forcedly skipped member (ex. a module attribute not defined in __all__) + keep = False + # give the user a chance to decide whether this member # should be skipped if self.env.app: @@ -1002,26 +1008,33 @@ def add_directive_header(self, sig: str) -> None: def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]: if want_all: - if self.__all__: - memberlist = self.__all__ - else: + members = get_module_members(self.object) + if not self.__all__: # for implicit module members, check __module__ to avoid # documenting imported objects - return True, get_module_members(self.object) + return True, members + else: + ret = [] + for name, value in members: + if name in self.__all__: + ret.append(ObjectMember(name, value)) + else: + ret.append(ObjectMember(name, value, skipped=True)) + + return False, ret else: memberlist = self.options.members or [] - ret = [] - for mname in memberlist: - try: - ret.append((mname, safe_getattr(self.object, mname))) - except AttributeError: - logger.warning( - __('missing attribute mentioned in :members: or __all__: ' - 'module %s, attribute %s') % - (safe_getattr(self.object, '__name__', '???'), mname), - type='autodoc' - ) - return False, ret + ret = [] + for name in memberlist: + try: + value = safe_getattr(self.object, name) + ret.append(ObjectMember(name, value)) + except AttributeError: + logger.warning(__('missing attribute mentioned in :members: option: ' + 'module %s, attribute %s') % + (safe_getattr(self.object, '__name__', '???'), name), + type='autodoc') + return False, ret def sort_members(self, documenters: List[Tuple["Documenter", bool]], order: str) -> List[Tuple["Documenter", bool]]: diff --git a/tests/test_ext_autodoc_events.py b/tests/test_ext_autodoc_events.py index 4e8348abc46..a643bedeb61 100644 --- a/tests/test_ext_autodoc_events.py +++ b/tests/test_ext_autodoc_events.py @@ -79,3 +79,28 @@ def test_between_exclude(app): ' third line', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_skip_module_member(app): + def autodoc_skip_member(app, what, name, obj, skip, options): + if name == "Class": + return True # Skip "Class" class in __all__ + elif name == "raises": + return False # Show "raises()" function (not in __all__) + + app.connect('autodoc-skip-member', autodoc_skip_member) + + options = {"members": None} + actual = do_autodoc(app, 'module', 'target', options) + assert list(actual) == [ + '', + '.. py:module:: target', + '', + '', + '.. py:function:: raises(exc, func, *args, **kwds)', + ' :module: target', + '', + ' Raise AssertionError if ``func(*args, **kwds)`` does not raise *exc*.', + '', + ]