Skip to content

Commit 8eb2c55

Browse files
AA-Turnerstephenfin
authored andcommitted
coverage: Extract _determine_py_coverage_modules into a function
1 parent 3c52a27 commit 8eb2c55

File tree

1 file changed

+68
-57
lines changed

1 file changed

+68
-57
lines changed

sphinx/ext/coverage.py

Lines changed: 68 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from sphinx.util.inspect import safe_getattr
2525

2626
if TYPE_CHECKING:
27-
from collections.abc import Iterator
27+
from collections.abc import Iterator, Sequence
2828

2929
from sphinx.application import Sphinx
3030
from sphinx.util.typing import ExtensionMetadata
@@ -77,34 +77,76 @@ def _load_modules(mod_name: str, ignored_module_exps: list[re.Pattern[str]]) ->
7777
:raises ImportError: If the module indicated by ``mod_name`` could not be
7878
loaded.
7979
"""
80-
modules: set[str] = set()
80+
if any(exp.match(mod_name) for exp in ignored_module_exps):
81+
return set()
8182

82-
for exp in ignored_module_exps:
83-
if exp.match(mod_name):
84-
return modules
85-
86-
# this can raise an exception but it's the responsibility of the caller to
87-
# handle this
83+
# This can raise an exception, which must be handled by the caller.
8884
mod = import_module(mod_name)
85+
modules = {mod_name}
86+
if mod.__spec__ is None:
87+
return modules
8988

90-
modules.add(mod_name)
91-
92-
for sub_mod_info in pkgutil.iter_modules(mod.__path__):
93-
if sub_mod_info.name == '__main__':
89+
search_locations = mod.__spec__.submodule_search_locations
90+
for (_, sub_mod_name, sub_mod_ispkg) in pkgutil.iter_modules(search_locations):
91+
if sub_mod_name == '__main__':
9492
continue
9593

96-
if sub_mod_info.ispkg:
97-
modules |= _load_modules(f'{mod_name}.{sub_mod_info.name}', ignored_module_exps)
94+
if sub_mod_ispkg:
95+
modules |= _load_modules(f'{mod_name}.{sub_mod_name}', ignored_module_exps)
9896
else:
99-
for exp in ignored_module_exps:
100-
if exp.match(sub_mod_info.name):
101-
continue
102-
103-
modules.add(f'{mod_name}.{sub_mod_info.name}')
97+
if any(exp.match(sub_mod_name) for exp in ignored_module_exps):
98+
continue
99+
modules.add(f'{mod_name}.{sub_mod_name}')
104100

105101
return modules
106102

107103

104+
def _determine_py_coverage_modules(
105+
coverage_modules: Sequence[str],
106+
seen_modules: set[str],
107+
ignored_module_exps: list[re.Pattern[str]],
108+
py_undoc: dict[str, dict[str, Any]],
109+
) -> list[str]:
110+
"""Return a sorted list of modules to check for coverage.
111+
112+
Figure out which of the two operating modes to use:
113+
114+
- If 'coverage_modules' is not specified, we check coverage for all modules
115+
seen in the documentation tree. Any objects found in these modules that are
116+
not documented will be noted. This will therefore only identify missing
117+
objects, but it requires no additional configuration.
118+
119+
- If 'coverage_modules' is specified, we check coverage for all modules
120+
specified in this configuration value. Any objects found in these modules
121+
that are not documented will be noted. In addition, any objects from other
122+
modules that are documented will be noted. This will therefore identify both
123+
missing modules and missing objects, but it requires manual configuration.
124+
"""
125+
126+
if not coverage_modules:
127+
return sorted(seen_modules)
128+
129+
modules = set()
130+
for mod_name in coverage_modules:
131+
try:
132+
modules |= _load_modules(mod_name, ignored_module_exps)
133+
except ImportError as err:
134+
# TODO(stephenfin): Define a subtype for all logs in this module
135+
logger.warning(__('module %s could not be imported: %s'), mod_name, err)
136+
py_undoc[mod_name] = {'error': err}
137+
continue
138+
139+
# if there are additional modules then we warn (but still scan)
140+
additional_modules = set(seen_modules) - modules
141+
if additional_modules:
142+
logger.warning(
143+
__('the following modules are documented but were not specified '
144+
'in coverage_modules: %s'),
145+
', '.join(additional_modules),
146+
)
147+
return sorted(modules)
148+
149+
108150
class CoverageBuilder(Builder):
109151
"""
110152
Evaluates coverage of code in the documentation.
@@ -131,7 +173,6 @@ def init(self) -> None:
131173
for (name, exps) in self.config.coverage_ignore_c_items.items():
132174
self.c_ignorexps[name] = compile_regex_list('coverage_ignore_c_items',
133175
exps)
134-
self.module_names = self.config.coverage_modules
135176
self.mod_ignorexps = compile_regex_list('coverage_ignore_modules',
136177
self.config.coverage_ignore_modules)
137178
self.cls_ignorexps = compile_regex_list('coverage_ignore_classes',
@@ -209,45 +250,15 @@ def ignore_pyobj(self, full_name: str) -> bool:
209250
)
210251

211252
def build_py_coverage(self) -> None:
212-
seen_objects = self.env.domaindata['py']['objects']
213-
seen_modules = self.env.domaindata['py']['modules']
253+
seen_objects = set(self.env.domaindata['py']['objects'])
254+
seen_modules = set(self.env.domaindata['py']['modules'])
214255

215256
skip_undoc = self.config.coverage_skip_undoc_in_source
216257

217-
# Figure out which of the two operating modes to use:
218-
#
219-
# - If 'coverage_modules' is not specified, we check coverage for all modules
220-
# seen in the documentation tree. Any objects found in these modules that are
221-
# not documented will be noted. This will therefore only identify missing
222-
# objects but it requires no additional configuration.
223-
# - If 'coverage_modules' is specified, we check coverage for all modules
224-
# specified in this configuration value. Any objects found in these modules
225-
# that are not documented will be noted. In addition, any objects from other
226-
# modules that are documented will be noted. This will therefore identify both
227-
# missing modules and missing objects but it requires manual configuration.
228-
if not self.module_names:
229-
modules = set(seen_modules)
230-
else:
231-
modules = set()
232-
for mod_name in self.module_names:
233-
try:
234-
modules |= _load_modules(mod_name, self.mod_ignorexps)
235-
except ImportError as err:
236-
# TODO(stephenfin): Define a subtype for all logs in this module
237-
logger.warning(__('module %s could not be imported: %s'), mod_name, err)
238-
self.py_undoc[mod_name] = {'error': err}
239-
continue
240-
241-
# if there are additional modules then we warn (but still scan)
242-
additional_modules = set(seen_modules) - modules
243-
if additional_modules:
244-
logger.warning(
245-
__('the following modules are documented but were not specified '
246-
'in coverage_modules: %s'),
247-
', '.join(additional_modules),
248-
)
249-
250-
for mod_name in sorted(modules):
258+
modules = _determine_py_coverage_modules(
259+
self.config.coverage_modules, seen_modules, self.mod_ignorexps, self.py_undoc,
260+
)
261+
for mod_name in modules:
251262
ignore = False
252263
for exp in self.mod_ignorexps:
253264
if exp.match(mod_name):
@@ -463,7 +474,7 @@ def finish(self) -> None:
463474

464475
def setup(app: Sphinx) -> ExtensionMetadata:
465476
app.add_builder(CoverageBuilder)
466-
app.add_config_value('coverage_modules', [], '')
477+
app.add_config_value('coverage_modules', (), '', [tuple, list, set])
467478
app.add_config_value('coverage_ignore_modules', [], '')
468479
app.add_config_value('coverage_ignore_functions', [], '')
469480
app.add_config_value('coverage_ignore_classes', [], '')

0 commit comments

Comments
 (0)