diff --git a/CHANGES b/CHANGES index b35f126d788..a5725b0d518 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,10 @@ Features added * #9815: html theme: Wrap sidebar components in div to allow customizing their layout via CSS +* #9831: Autosummary now documents only the members specified in a module's + ``__all__`` attribute if :confval:`autosummary_ignore_module_all` is set to + ``False``. The default behaviour is unchanged. Autogen also now supports + this behavior with the ``--respect-module-all`` switch. Bugs fixed ---------- diff --git a/doc/man/sphinx-autogen.rst b/doc/man/sphinx-autogen.rst index 18ae8d1e9d6..4c8f0f20782 100644 --- a/doc/man/sphinx-autogen.rst +++ b/doc/man/sphinx-autogen.rst @@ -39,6 +39,10 @@ Options Document imported members. +.. option:: -a, --respect-module-all + + Document exactly the members in a module's ``__all__`` attribute. + Example ------- diff --git a/doc/usage/extensions/autosummary.rst b/doc/usage/extensions/autosummary.rst index 9533ed109c7..ac7bbd68f7b 100644 --- a/doc/usage/extensions/autosummary.rst +++ b/doc/usage/extensions/autosummary.rst @@ -201,6 +201,25 @@ also use these config values: .. versionadded:: 2.1 + .. versionchanged:: 4.4 + + If ``autosummary_ignore_module_all`` is ``False``, this configuration + value is ignored for members listed in ``__all__``. + +.. confval:: autosummary_ignore_module_all + + If ``False`` and a module has the ``__all__`` attribute set, autosummary + documents every member listed in ``__all__`` and no others. Default is + ``True`` + + Note that if an imported member is listed in ``__all__``, it will be + documented regardless of the value of ``autosummary_imported_members``. To + match the behaviour of ``from module import *``, set + ``autosummary_ignore_module_all`` to False and + ``autosummary_imported_members`` to True. + + .. versionadded:: 4.4 + .. confval:: autosummary_filename_map A dict mapping object names to filenames. This is necessary to avoid diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 478b5c9f33a..298c9013802 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -826,5 +826,6 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('autosummary_mock_imports', lambda config: config.autodoc_mock_imports, 'env') app.add_config_value('autosummary_imported_members', [], False, [bool]) + app.add_config_value('autosummary_ignore_module_all', True, 'env', bool) return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index a5c8ce11fde..87cd0d64eb8 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -28,7 +28,7 @@ import warnings from gettext import NullTranslations from os import path -from typing import Any, Dict, List, NamedTuple, Set, Tuple, Type, Union +from typing import Any, Dict, List, NamedTuple, Sequence, Set, Tuple, Type, Union from jinja2 import TemplateNotFound from jinja2.sandbox import SandboxedEnvironment @@ -46,7 +46,7 @@ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.registry import SphinxComponentRegistry from sphinx.util import logging, rst, split_full_qualified_name -from sphinx.util.inspect import safe_getattr +from sphinx.util.inspect import getall, safe_getattr from sphinx.util.osutil import ensuredir from sphinx.util.template import SphinxTemplateLoader @@ -68,6 +68,7 @@ def __init__(self, translator: NullTranslations) -> None: self.config.add('autosummary_context', {}, True, None) self.config.add('autosummary_filename_map', {}, True, None) + self.config.add('autosummary_ignore_module_all', True, 'env', bool) self.config.init_values() def emit_firstresult(self, *args: Any) -> None: @@ -192,7 +193,7 @@ def is_skipped(self, name: str, value: Any, objtype: str) -> bool: def scan(self, imported_members: bool) -> List[str]: members = [] - for name in dir(self.object): + for name in members_of(self.object, self.app.config): try: value = safe_getattr(self.object, name) except AttributeError: @@ -212,16 +213,31 @@ def scan(self, imported_members: bool) -> List[str]: except AttributeError: imported = False + respect_module_all = not self.app.config.autosummary_ignore_module_all if imported_members: # list all members up members.append(name) elif imported is False: - # list not-imported members up + # list not-imported members + members.append(name) + elif '__all__' in dir(self.object) and respect_module_all: + # list members that have __all__ set members.append(name) return members +def members_of(obj: Any, conf: Config) -> Sequence[str]: + """Get the members of ``obj``, possibly ignoring the ``__all__`` module attribute + + Follows the ``conf.autosummary_ignore_module_all`` setting.""" + + if conf.autosummary_ignore_module_all: + return dir(obj) + else: + return getall(obj) or dir(obj) + + def generate_autosummary_content(name: str, obj: Any, parent: Any, template: AutosummaryRenderer, template_name: str, imported_members: bool, app: Any, @@ -245,7 +261,7 @@ def get_class_members(obj: Any) -> Dict[str, Any]: def get_module_members(obj: Any) -> Dict[str, Any]: members = {} - for name in dir(obj): + for name in members_of(obj, app.config): try: members[name] = safe_getattr(obj, name) except AttributeError: @@ -630,6 +646,10 @@ def get_parser() -> argparse.ArgumentParser: dest='imported_members', default=False, help=__('document imported members (default: ' '%(default)s)')) + parser.add_argument('-a', '--respect-module-all', action='store_true', + dest='respect_module_all', default=False, + help=__('document exactly the members in module __all__ attribute. ' + '(default: %(default)s)')) return parser @@ -646,6 +666,7 @@ def main(argv: List[str] = sys.argv[1:]) -> None: if args.templates: app.config.templates_path.append(path.abspath(args.templates)) + app.config.autosummary_ignore_module_all = not args.respect_module_all # type: ignore generate_autosummary_docs(args.source_file, args.output_dir, '.' + args.suffix, diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py index ca347545989..38e96199008 100644 --- a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py +++ b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py @@ -1,6 +1,16 @@ from os import path # NOQA from typing import Union +__all__ = [ + "CONSTANT1", + "Exc", + "Foo", + "_Baz", + "bar", + "qux", + "path", +] + #: module variable CONSTANT1 = None CONSTANT2 = None @@ -48,3 +58,5 @@ class _Exc(Exception): #: a module-level attribute qux = 2 +#: a module-level attribute that has been excluded from __all__ +quuz = 2 diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index cb595e4546c..2fe72dbe3b3 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -216,14 +216,42 @@ def test_autosummary_generate_content_for_module(app): context = template.render.call_args[0][1] assert context['members'] == ['CONSTANT1', 'CONSTANT2', 'Exc', 'Foo', '_Baz', '_Exc', - '__builtins__', '__cached__', '__doc__', '__file__', - '__name__', '__package__', '_quux', 'bar', 'qux'] + '__all__', '__builtins__', '__cached__', '__doc__', + '__file__', '__name__', '__package__', '_quux', 'bar', + 'quuz', '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'] == ['CONSTANT1', 'qux', 'quuz'] + assert context['all_attributes'] == ['CONSTANT1', 'qux', 'quuz'] + 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___all__(app): + import autosummary_dummy_module + template = Mock() + app.config.autosummary_ignore_module_all = False + + 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'] == ['CONSTANT1', 'Exc', 'Foo', '_Baz', 'bar', 'qux', 'path'] + assert context['functions'] == ['bar'] + assert context['all_functions'] == ['bar'] + assert context['classes'] == ['Foo'] + assert context['all_classes'] == ['Foo', '_Baz'] + assert context['exceptions'] == ['Exc'] + assert context['all_exceptions'] == ['Exc'] assert context['attributes'] == ['CONSTANT1', 'qux'] assert context['all_attributes'] == ['CONSTANT1', 'qux'] assert context['fullname'] == 'autosummary_dummy_module' @@ -246,9 +274,9 @@ def skip_member(app, what, name, obj, skip, options): 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'] == ['CONSTANT1', 'CONSTANT2', '_Baz', '_Exc', '__builtins__', - '__cached__', '__doc__', '__file__', '__name__', - '__package__', '_quux', 'qux'] + assert context['members'] == ['CONSTANT1', 'CONSTANT2', '_Baz', '_Exc', '__all__', + '__builtins__', '__cached__', '__doc__', '__file__', + '__name__', '__package__', '_quux', 'quuz', 'qux'] assert context['functions'] == [] assert context['classes'] == [] assert context['exceptions'] == [] @@ -265,17 +293,17 @@ def test_autosummary_generate_content_for_module_imported_members(app): context = template.render.call_args[0][1] assert context['members'] == ['CONSTANT1', 'CONSTANT2', 'Exc', 'Foo', 'Union', '_Baz', - '_Exc', '__builtins__', '__cached__', '__doc__', + '_Exc', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', - '__spec__', '_quux', 'bar', 'path', 'qux'] + '__spec__', '_quux', 'bar', 'path', 'quuz', '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'] == ['CONSTANT1', 'qux'] - assert context['all_attributes'] == ['CONSTANT1', 'qux'] + assert context['attributes'] == ['CONSTANT1', 'qux', 'quuz'] + assert context['all_attributes'] == ['CONSTANT1', 'qux', 'quuz'] assert context['fullname'] == 'autosummary_dummy_module' assert context['module'] == 'autosummary_dummy_module' assert context['objname'] == '' @@ -313,6 +341,7 @@ def test_autosummary_generate(app, status, warning): assert doctree[3][0][0][2][5].astext() == 'autosummary_dummy_module.qux\n\na module-level attribute' module = (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text() + assert (' .. autosummary::\n' ' \n' ' Foo\n' @@ -321,6 +350,7 @@ def test_autosummary_generate(app, status, warning): ' \n' ' CONSTANT1\n' ' qux\n' + ' quuz\n' ' \n' in module) Foo = (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.rst').read_text()