From 033ebab15ecd498f34cea2d18d528f843607556a Mon Sep 17 00:00:00 2001 From: Matyas Novak Date: Fri, 11 Mar 2022 16:08:52 +0100 Subject: [PATCH 1/3] Recognize a documented attribute of a module as non-imported. --- sphinx/ext/autosummary/generate.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index a08e45fa6e3..34e56eedbab 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -29,6 +29,7 @@ from gettext import NullTranslations from os import path from typing import Any, Dict, List, NamedTuple, Sequence, Set, Tuple, Type, Union +from collections.abc import Mapping from jinja2 import TemplateNotFound from jinja2.sandbox import SandboxedEnvironment @@ -192,7 +193,7 @@ def is_skipped(self, name: str, value: Any, objtype: str) -> bool: name, exc, type='autosummary') return False - def scan(self, imported_members: bool) -> List[str]: + def scan(self, imported_members: bool, attr_docs: Mapping[str,str] = {}) -> List[str]: members = [] for name in members_of(self.object, self.app.config): try: @@ -208,7 +209,7 @@ def scan(self, imported_members: bool) -> List[str]: if inspect.ismodule(value): imported = True elif safe_getattr(value, '__module__') != self.object.__name__: - imported = True + imported = objtype != 'data' or ('', name) not in attr_docs else: imported = False except AttributeError: @@ -305,8 +306,6 @@ 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) @@ -335,8 +334,10 @@ def get_modules(obj: Any) -> Tuple[List[str], List[str]]: ns.update(context) if doc.objtype == 'module': + analyzer = ModuleAnalyzer.for_module(name) + attr_docs = analyzer.find_attr_docs() scanner = ModuleScanner(app, obj) - ns['members'] = scanner.scan(imported_members) + ns['members'] = scanner.scan(imported_members, attr_docs) ns['functions'], ns['all_functions'] = \ get_members(obj, {'function'}, imported=imported_members) ns['classes'], ns['all_classes'] = \ From d955098ef9bae6e41924011ec278f6cd47c63b10 Mon Sep 17 00:00:00 2001 From: Matyas Novak Date: Mon, 14 Mar 2022 23:29:51 +0100 Subject: [PATCH 2/3] Do not pass attr_docs as an argument into ModuleScanner.scan --- sphinx/ext/autosummary/generate.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 34e56eedbab..603ad9eb77b 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -193,8 +193,10 @@ def is_skipped(self, name: str, value: Any, objtype: str) -> bool: name, exc, type='autosummary') return False - def scan(self, imported_members: bool, attr_docs: Mapping[str,str] = {}) -> List[str]: + def scan(self, imported_members: bool) -> List[str]: members = [] + analyzer = ModuleAnalyzer.for_module(self.object.__name__) + attr_docs = analyzer.find_attr_docs() for name in members_of(self.object, self.app.config): try: value = safe_getattr(self.object, name) @@ -306,6 +308,8 @@ 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) @@ -334,10 +338,8 @@ def get_modules(obj: Any) -> Tuple[List[str], List[str]]: ns.update(context) if doc.objtype == 'module': - analyzer = ModuleAnalyzer.for_module(name) - attr_docs = analyzer.find_attr_docs() scanner = ModuleScanner(app, obj) - ns['members'] = scanner.scan(imported_members, attr_docs) + ns['members'] = scanner.scan(imported_members) ns['functions'], ns['all_functions'] = \ get_members(obj, {'function'}, imported=imported_members) ns['classes'], ns['all_classes'] = \ From 07b2a050c75a6f46975407add941dc9be5a5a983 Mon Sep 17 00:00:00 2001 From: Matyas Novak Date: Mon, 14 Mar 2022 23:31:33 +0100 Subject: [PATCH 3/3] Test recognition of a documented instance of a class as a non-imported member of the module --- .../autosummary_class_module.py | 2 ++ .../autosummary_dummy_module.py | 5 ++++ tests/test_ext_autosummary.py | 28 +++++++++++-------- 3 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 tests/roots/test-ext-autosummary/autosummary_class_module.py diff --git a/tests/roots/test-ext-autosummary/autosummary_class_module.py b/tests/roots/test-ext-autosummary/autosummary_class_module.py new file mode 100644 index 00000000000..f13de17032f --- /dev/null +++ b/tests/roots/test-ext-autosummary/autosummary_class_module.py @@ -0,0 +1,2 @@ +class Class(): + pass diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py index 38e96199008..b0419e1ba2f 100644 --- a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py +++ b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py @@ -1,5 +1,6 @@ from os import path # NOQA from typing import Union +from autosummary_class_module import Class __all__ = [ "CONSTANT1", @@ -60,3 +61,7 @@ class _Exc(Exception): qux = 2 #: a module-level attribute that has been excluded from __all__ quuz = 2 + +considered_as_imported = Class() +non_imported_member = Class() +""" This attribute has a docstring, so it is recognized as a not-imported member """ diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index 13da15e503f..11debec1928 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -218,15 +218,15 @@ def test_autosummary_generate_content_for_module(app): assert context['members'] == ['CONSTANT1', 'CONSTANT2', 'Exc', 'Foo', '_Baz', '_Exc', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__name__', '__package__', '_quux', 'bar', - 'quuz', 'qux'] + 'non_imported_member', '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['attributes'] == ['CONSTANT1', 'qux', 'quuz', 'non_imported_member'] + assert context['all_attributes'] == ['CONSTANT1', 'qux', 'quuz', 'non_imported_member'] assert context['fullname'] == 'autosummary_dummy_module' assert context['module'] == 'autosummary_dummy_module' assert context['objname'] == '' @@ -276,7 +276,8 @@ def skip_member(app, what, name, obj, skip, options): context = template.render.call_args[0][1] assert context['members'] == ['CONSTANT1', 'CONSTANT2', '_Baz', '_Exc', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', - '__name__', '__package__', '_quux', 'quuz', 'qux'] + '__name__', '__package__', '_quux', 'non_imported_member', + 'quuz', 'qux'] assert context['functions'] == [] assert context['classes'] == [] assert context['exceptions'] == [] @@ -292,18 +293,20 @@ def test_autosummary_generate_content_for_module_imported_members(app): assert template.render.call_args[0][0] == 'module' context = template.render.call_args[0][1] - assert context['members'] == ['CONSTANT1', 'CONSTANT2', 'Exc', 'Foo', 'Union', '_Baz', - '_Exc', '__all__', '__builtins__', '__cached__', '__doc__', - '__file__', '__loader__', '__name__', '__package__', - '__spec__', '_quux', 'bar', 'path', 'quuz', 'qux'] + assert context['members'] == ['CONSTANT1', 'CONSTANT2', 'Class', 'Exc', 'Foo', 'Union', + '_Baz', '_Exc', '__all__', '__builtins__', '__cached__', + '__doc__', '__file__', '__loader__', '__name__', + '__package__', '__spec__', '_quux', 'bar', + 'considered_as_imported', 'non_imported_member', '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['classes'] == ['Class', 'Foo'] + assert context['all_classes'] == ['Class', '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['attributes'] == ['CONSTANT1', 'qux', 'quuz', 'non_imported_member'] + assert context['all_attributes'] == ['CONSTANT1', 'qux', 'quuz', 'non_imported_member'] assert context['fullname'] == 'autosummary_dummy_module' assert context['module'] == 'autosummary_dummy_module' assert context['objname'] == '' @@ -351,6 +354,7 @@ def test_autosummary_generate(app, status, warning): ' CONSTANT1\n' ' qux\n' ' quuz\n' + ' non_imported_member\n' ' \n' in module) Foo = (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.rst').read_text()