diff --git a/CHANGES b/CHANGES index a548dd5ca..e301f1165 100644 --- a/CHANGES +++ b/CHANGES @@ -101,6 +101,8 @@ Bugs fixed * #7551: autosummary: a nested class is indexed as non-nested class * #7661: autosummary: autosummary directive emits warnings twices if failed to import the target module +* #7685: autosummary: The template variable "members" contains imported members + even if :confval:`autossummary_imported_members` is False * #7535: sphinx-autogen: crashes when custom template uses inheritance * #7536: sphinx-autogen: crashes when template uses i18n feature * #7653: sphinx-quickstart: Fix multiple directory creation for nested relpath diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index a57c73fb7..473ac3db6 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -18,6 +18,7 @@ """ import argparse +import inspect import locale import os import pkgutil @@ -176,6 +177,56 @@ class AutosummaryRenderer: # -- Generating output --------------------------------------------------------- +class ModuleScanner: + def __init__(self, app: Any, obj: Any) -> None: + self.app = app + self.object = obj + + def get_object_type(self, name: str, value: Any) -> str: + return get_documenter(self.app, value, self.object).objtype + + def is_skipped(self, name: str, value: Any, objtype: str) -> bool: + try: + return self.app.emit_firstresult('autodoc-skip-member', objtype, + name, value, False, {}) + except Exception as exc: + logger.warning(__('autosummary: failed to determine %r to be documented, ' + 'the following exception was raised:\n%s'), + name, exc, type='autosummary') + return False + + def scan(self, imported_members: bool) -> List[str]: + members = [] + for name in dir(self.object): + try: + value = safe_getattr(self.object, name) + except AttributeError: + value = None + + objtype = self.get_object_type(name, value) + if self.is_skipped(name, value, objtype): + continue + + try: + if inspect.ismodule(value): + imported = True + elif safe_getattr(value, '__module__') != self.object.__name__: + imported = True + else: + imported = False + except AttributeError: + imported = False + + if imported_members: + # list all members up + members.append(name) + elif imported is False: + # list not-imported members up + members.append(name) + + return members + + def generate_autosummary_content(name: str, obj: Any, parent: Any, template: AutosummaryRenderer, template_name: str, imported_members: bool, app: Any, @@ -246,7 +297,8 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any, ns.update(context) if doc.objtype == 'module': - ns['members'] = dir(obj) + scanner = ModuleScanner(app, obj) + ns['members'] = scanner.scan(imported_members) ns['functions'], ns['all_functions'] = \ get_members(obj, {'function'}, imported=imported_members) ns['classes'], ns['all_classes'] = \ diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py index 0c54a1477..85981a0f8 100644 --- a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py +++ b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py @@ -1,4 +1,4 @@ -from os import * # NOQA +from os import path # NOQA from typing import Union @@ -17,7 +17,23 @@ class Foo: pass -def bar(x: Union[int, str], y: int = 1): +class _Baz: + pass + + +def bar(x: Union[int, str], y: int = 1) -> None: + pass + + +def _quux(): + pass + + +class Exc(Exception): + pass + + +class _Exc(Exception): pass diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index 166029ccb..281ba141e 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -19,7 +19,10 @@ from sphinx import addnodes from sphinx.ext.autosummary import ( autosummary_table, autosummary_toc, mangle_signature, import_by_name, extract_summary ) -from sphinx.ext.autosummary.generate import AutosummaryEntry, generate_autosummary_docs, main as autogen_main +from sphinx.ext.autosummary.generate import ( + AutosummaryEntry, generate_autosummary_content, generate_autosummary_docs, + main as autogen_main +) from sphinx.testing.util import assert_node, etree_parse from sphinx.util.docutils import new_document from sphinx.util.osutil import cd @@ -189,6 +192,83 @@ def test_escaping(app, status, warning): assert str_content(title) == 'underscore_module_' +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module(app): + import autosummary_dummy_module + template = Mock() + + generate_autosummary_content('autosummary_dummy_module', autosummary_dummy_module, None, + template, None, False, app, False, {}) + assert template.render.call_args[0][0] == 'module' + + context = template.render.call_args[0][1] + assert context['members'] == ['Exc', 'Foo', '_Baz', '_Exc', '__builtins__', + '__cached__', '__doc__', '__file__', '__name__', + '__package__', '_quux', 'bar', 'qux'] + assert context['functions'] == ['bar'] + assert context['all_functions'] == ['_quux', 'bar'] + assert context['classes'] == ['Foo'] + assert context['all_classes'] == ['Foo', '_Baz'] + assert context['exceptions'] == ['Exc'] + assert context['all_exceptions'] == ['Exc', '_Exc'] + assert context['attributes'] == ['qux'] + assert context['all_attributes'] == ['qux'] + assert context['fullname'] == 'autosummary_dummy_module' + assert context['module'] == 'autosummary_dummy_module' + assert context['objname'] == '' + assert context['name'] == '' + assert context['objtype'] == 'module' + + +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module_skipped(app): + import autosummary_dummy_module + template = Mock() + + def skip_member(app, what, name, obj, skip, options): + if name in ('Foo', 'bar', 'Exc'): + return True + + app.connect('autodoc-skip-member', skip_member) + generate_autosummary_content('autosummary_dummy_module', autosummary_dummy_module, None, + template, None, False, app, False, {}) + context = template.render.call_args[0][1] + assert context['members'] == ['_Baz', '_Exc', '__builtins__', '__cached__', '__doc__', + '__file__', '__name__', '__package__', '_quux', 'qux'] + assert context['functions'] == [] + assert context['classes'] == [] + assert context['exceptions'] == [] + + +@pytest.mark.sphinx(testroot='ext-autosummary') +def test_autosummary_generate_content_for_module_imported_members(app): + import autosummary_dummy_module + template = Mock() + + generate_autosummary_content('autosummary_dummy_module', autosummary_dummy_module, None, + template, None, True, app, False, {}) + assert template.render.call_args[0][0] == 'module' + + context = template.render.call_args[0][1] + assert context['members'] == ['Exc', 'Foo', 'Union', '_Baz', '_Exc', '__builtins__', + '__cached__', '__doc__', '__file__', '__loader__', + '__name__', '__package__', '__spec__', '_quux', + 'bar', 'path', 'qux'] + assert context['functions'] == ['bar'] + assert context['all_functions'] == ['_quux', 'bar'] + assert context['classes'] == ['Foo'] + assert context['all_classes'] == ['Foo', '_Baz'] + assert context['exceptions'] == ['Exc'] + assert context['all_exceptions'] == ['Exc', '_Exc'] + assert context['attributes'] == ['qux'] + assert context['all_attributes'] == ['qux'] + assert context['fullname'] == 'autosummary_dummy_module' + assert context['module'] == 'autosummary_dummy_module' + assert context['objname'] == '' + assert context['name'] == '' + assert context['objtype'] == 'module' + + @pytest.mark.sphinx('dummy', testroot='ext-autosummary') def test_autosummary_generate(app, status, warning): app.builder.build_all()