Skip to content

Commit a08d093

Browse files
committed
Extract _determine_py_coverage_modules into a function
1 parent 732de74 commit a08d093

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

@@ -76,34 +76,76 @@ def _load_modules(mod_name: str, ignored_module_exps: list[re.Pattern[str]]) ->
7676
:raises ImportError: If the module indicated by ``mod_name`` could not be
7777
loaded.
7878
"""
79-
modules: set[str] = set()
79+
if any(exp.match(mod_name) for exp in ignored_module_exps):
80+
return set()
8081

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

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

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

104100
return modules
105101

106102

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

209250
def build_py_coverage(self) -> None:
210-
seen_objects = self.env.domaindata['py']['objects']
211-
seen_modules = self.env.domaindata['py']['modules']
251+
seen_objects = set(self.env.domaindata['py']['objects'])
252+
seen_modules = set(self.env.domaindata['py']['modules'])
212253

213254
skip_undoc = self.config.coverage_skip_undoc_in_source
214255

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

462473
def setup(app: Sphinx) -> dict[str, Any]:
463474
app.add_builder(CoverageBuilder)
464-
app.add_config_value('coverage_modules', [], False)
475+
app.add_config_value('coverage_modules', (), False, [tuple, list, set])
465476
app.add_config_value('coverage_ignore_modules', [], False)
466477
app.add_config_value('coverage_ignore_functions', [], False)
467478
app.add_config_value('coverage_ignore_classes', [], False)

0 commit comments

Comments
 (0)