diff --git a/doc/usage/extensions/autosummary.rst b/doc/usage/extensions/autosummary.rst index f3a5aea0e..b5acab65d 100644 --- a/doc/usage/extensions/autosummary.rst +++ b/doc/usage/extensions/autosummary.rst @@ -285,8 +285,12 @@ The following variables available in the templates: .. data:: attributes - List containing names of "public" attributes in the class. Only available - for classes. + List containing names of "public" attributes in the class/module. Only + available for classes and modules. + + .. versionchanged:: 3.1 + + Attributes of modules are supported. .. data:: modules diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 071d75348..a57c73fb7 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -42,6 +42,7 @@ from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warnin from sphinx.ext.autodoc import Documenter from sphinx.ext.autosummary import import_by_name, get_documenter from sphinx.locale import __ +from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.registry import SphinxComponentRegistry from sphinx.util import logging from sphinx.util import rst @@ -218,6 +219,21 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any, public.append(name) return public, items + def get_module_attrs(members: Any) -> Tuple[List[str], List[str]]: + """Find module attributes with docstrings.""" + attrs, public = [], [] + try: + analyzer = ModuleAnalyzer.for_module(name) + attr_docs = analyzer.find_attr_docs() + for namespace, attr_name in attr_docs: + if namespace == '' and attr_name in members: + attrs.append(attr_name) + if not attr_name.startswith('_'): + public.append(attr_name) + except PycodeError: + pass # give up if ModuleAnalyzer fails to parse code + return public, attrs + def get_modules(obj: Any) -> Tuple[List[str], List[str]]: items = [] # type: List[str] for _, modname, ispkg in pkgutil.iter_modules(obj.__path__): @@ -237,6 +253,8 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any, get_members(obj, {'class'}, imported=imported_members) ns['exceptions'], ns['all_exceptions'] = \ get_members(obj, {'exception'}, imported=imported_members) + ns['attributes'], ns['all_attributes'] = \ + get_module_attrs(ns['members']) ispackage = hasattr(obj, '__path__') if ispackage and recursive: ns['modules'], ns['all_modules'] = get_modules(obj) diff --git a/sphinx/ext/autosummary/templates/autosummary/module.rst b/sphinx/ext/autosummary/templates/autosummary/module.rst index 5b70d5c40..3a93e872a 100644 --- a/sphinx/ext/autosummary/templates/autosummary/module.rst +++ b/sphinx/ext/autosummary/templates/autosummary/module.rst @@ -2,6 +2,17 @@ .. automodule:: {{ fullname }} + {% block attributes %} + {% if attributes %} + .. rubric:: Module Attributes + + .. autosummary:: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + {% block functions %} {% if functions %} .. rubric:: {{ _('Functions') }} diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py index ffd381f51..0c54a1477 100644 --- a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py +++ b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py @@ -19,3 +19,7 @@ class Foo: def bar(x: Union[int, str], y: int = 1): pass + + +#: a module-level attribute +qux = 2 diff --git a/tests/roots/test-ext-autosummary/index.rst b/tests/roots/test-ext-autosummary/index.rst index bc3f80234..9f657bb73 100644 --- a/tests/roots/test-ext-autosummary/index.rst +++ b/tests/roots/test-ext-autosummary/index.rst @@ -11,4 +11,5 @@ autosummary_dummy_module.Foo autosummary_dummy_module.Foo.Bar autosummary_dummy_module.bar + autosummary_dummy_module.qux autosummary_importfail diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index aa075a9e6..166029ccb 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -203,16 +203,18 @@ def test_autosummary_generate(app, status, warning): [autosummary_table, nodes.table, nodes.tgroup, (nodes.colspec, nodes.colspec, [nodes.tbody, (nodes.row, + nodes.row, nodes.row, nodes.row, nodes.row)])]) assert_node(doctree[4][0], addnodes.toctree, caption="An autosummary") - assert len(doctree[3][0][0][2]) == 4 + assert len(doctree[3][0][0][2]) == 5 assert doctree[3][0][0][2][0].astext() == 'autosummary_dummy_module\n\n' assert doctree[3][0][0][2][1].astext() == 'autosummary_dummy_module.Foo()\n\n' assert doctree[3][0][0][2][2].astext() == 'autosummary_dummy_module.Foo.Bar\n\n' assert doctree[3][0][0][2][3].astext() == 'autosummary_dummy_module.bar(x[, y])\n\n' + assert doctree[3][0][0][2][4].astext() == 'autosummary_dummy_module.qux\n\na module-level attribute' module = (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').read_text() assert (' .. autosummary::\n' @@ -237,6 +239,11 @@ def test_autosummary_generate(app, status, warning): '\n' '.. autoclass:: Foo.Bar\n' in FooBar) + qux = (app.srcdir / 'generated' / 'autosummary_dummy_module.qux.rst').read_text() + assert ('.. currentmodule:: autosummary_dummy_module\n' + '\n' + '.. autodata:: qux' in qux) + @pytest.mark.sphinx('dummy', testroot='ext-autosummary', confoverrides={'autosummary_generate_overwrite': False})