diff --git a/.ruff.toml b/.ruff.toml index 7f4cb0feaaa..fca5a9b64ec 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -415,7 +415,6 @@ exclude = [ "sphinx/domains/python/_object.py", "sphinx/domains/rst.py", "sphinx/domains/std/__init__.py", - "sphinx/ext/coverage.py", "sphinx/ext/doctest.py", "sphinx/ext/duration.py", "sphinx/ext/extlinks.py", diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index d1c4aea457c..0cb795a3498 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -62,12 +62,17 @@ def _add_line(sizes: list[int], separator: str) -> str: return '+' + ''.join((separator * (size + 1)) + '+' for size in sizes) -def _add_row(col_widths: list[int], columns: list[str], separator: str) -> Iterator[str]: - yield ''.join(f'| {column: <{col_widths[i]}}' for i, column in enumerate(columns)) + '|' +def _add_row( + col_widths: list[int], columns: list[str], separator: str +) -> Iterator[str]: + row = ''.join(f'| {column: <{col_widths[i]}}' for i, column in enumerate(columns)) + yield f'{row}|' yield _add_line(col_widths, separator) -def _load_modules(mod_name: str, ignored_module_exps: Iterable[re.Pattern[str]]) -> Set[str]: +def _load_modules( + mod_name: str, ignored_module_exps: Iterable[re.Pattern[str]] +) -> Set[str]: """Recursively load all submodules. :param mod_name: The name of a module to load submodules for. @@ -87,7 +92,7 @@ def _load_modules(mod_name: str, ignored_module_exps: Iterable[re.Pattern[str]]) return modules search_locations = mod.__spec__.submodule_search_locations - for (_, sub_mod_name, sub_mod_ispkg) in pkgutil.iter_modules(search_locations): + for _, sub_mod_name, sub_mod_ispkg in pkgutil.iter_modules(search_locations): if sub_mod_name == '__main__': continue @@ -138,16 +143,20 @@ def _determine_py_coverage_modules( # if there are additional modules then we warn but continue scanning if additional_modules := seen_modules - modules: logger.warning( - __('the following modules are documented but were not specified ' - 'in coverage_modules: %s'), + __( + 'the following modules are documented but were not specified ' + 'in coverage_modules: %s' + ), ', '.join(additional_modules), ) # likewise, if there are missing modules we warn but continue scanning if missing_modules := modules - seen_modules: logger.warning( - __('the following modules are specified in coverage_modules ' - 'but were not documented'), + __( + 'the following modules are specified in coverage_modules ' + 'but were not documented' + ), ', '.join(missing_modules), ) @@ -160,8 +169,10 @@ class CoverageBuilder(Builder): """ name = 'coverage' - epilog = __('Testing of coverage in the sources finished, look at the ' - 'results in %(outdir)s' + os.path.sep + 'python.txt.') + epilog = __( + 'Testing of coverage in the sources finished, look at the ' + 'results in %(outdir)s' + os.path.sep + 'python.txt.' + ) def init(self) -> None: self.c_sourcefiles: list[str] = [] @@ -170,24 +181,28 @@ def init(self) -> None: self.c_sourcefiles.extend(glob.glob(pattern)) self.c_regexes: list[tuple[str, re.Pattern[str]]] = [] - for (name, exp) in self.config.coverage_c_regexes.items(): + for name, exp in self.config.coverage_c_regexes.items(): try: self.c_regexes.append((name, re.compile(exp))) except Exception: logger.warning(__('invalid regex %r in coverage_c_regexes'), exp) - self.c_ignorexps: dict[str, list[re.Pattern[str]]] = {} - for (name, exps) in self.config.coverage_ignore_c_items.items(): - self.c_ignorexps[name] = compile_regex_list('coverage_ignore_c_items', - exps) - self.mod_ignorexps = compile_regex_list('coverage_ignore_modules', - self.config.coverage_ignore_modules) - self.cls_ignorexps = compile_regex_list('coverage_ignore_classes', - self.config.coverage_ignore_classes) - self.fun_ignorexps = compile_regex_list('coverage_ignore_functions', - self.config.coverage_ignore_functions) - self.py_ignorexps = compile_regex_list('coverage_ignore_pyobjects', - self.config.coverage_ignore_pyobjects) + self.c_ignorexps: dict[str, list[re.Pattern[str]]] = { + name: compile_regex_list('coverage_ignore_c_items', exps) + for name, exps in self.config.coverage_ignore_c_items.items() + } + self.mod_ignorexps = compile_regex_list( + 'coverage_ignore_modules', self.config.coverage_ignore_modules + ) + self.cls_ignorexps = compile_regex_list( + 'coverage_ignore_classes', self.config.coverage_ignore_classes + ) + self.fun_ignorexps = compile_regex_list( + 'coverage_ignore_functions', self.config.coverage_ignore_functions + ) + self.py_ignorexps = compile_regex_list( + 'coverage_ignore_pyobjects', self.config.coverage_ignore_pyobjects + ) def get_outdated_docs(self) -> str: return 'coverage overview' @@ -209,7 +224,7 @@ def build_c_coverage(self) -> None: c_objects[obj[2]] = obj[1] for filename in self.c_sourcefiles: undoc: set[tuple[str, str]] = set() - with open(filename, encoding="utf-8") as f: + with open(filename, encoding='utf-8') as f: for line in f: for key, regex in self.c_regexes: match = regex.match(line) @@ -231,7 +246,7 @@ def build_c_coverage(self) -> None: def write_c_coverage(self) -> None: output_file = os.path.join(self.outdir, 'c.txt') - with open(output_file, 'w', encoding="utf-8") as op: + with open(output_file, 'w', encoding='utf-8') as op: if self.config.coverage_write_headline: write_header(op, 'Undocumented C API elements', '=') op.write('\n') @@ -242,19 +257,23 @@ def write_c_coverage(self) -> None: op.write(' * %-50s [%9s]\n' % (name, typ)) if self.config.coverage_show_missing_items: if self.app.quiet: - logger.warning(__('undocumented c api: %s [%s] in file %s'), - name, typ, filename) + logger.warning( + __('undocumented c api: %s [%s] in file %s'), + name, + typ, + filename, + ) else: - logger.info(red('undocumented ') + 'c ' + 'api ' + - '%-30s' % (name + " [%9s]" % typ) + - red(' - in file ') + filename) + logger.info( + red('undocumented ') + + f'c api {f"{name} [{typ:>9}]":<30}' + + red(' - in file ') + + filename + ) op.write('\n') def ignore_pyobj(self, full_name: str) -> bool: - return any( - exp.search(full_name) - for exp in self.py_ignorexps - ) + return any(exp.search(full_name) for exp in self.py_ignorexps) def build_py_coverage(self) -> None: seen_objects = frozenset(self.env.domaindata['py']['objects']) @@ -263,7 +282,10 @@ def build_py_coverage(self) -> None: skip_undoc = self.config.coverage_skip_undoc_in_source modules = _determine_py_coverage_modules( - self.config.coverage_modules, seen_modules, self.mod_ignorexps, self.py_undoc, + self.config.coverage_modules, + seen_modules, + self.mod_ignorexps, + self.py_undoc, ) for mod_name in modules: ignore = False @@ -336,8 +358,7 @@ def build_py_coverage(self) -> None: attr = safe_getattr(obj, attr_name) except AttributeError: continue - if not (inspect.ismethod(attr) or - inspect.isfunction(attr)): + if not (inspect.ismethod(attr) or inspect.isfunction(attr)): continue if attr_name[0] == '_': # starts with an underscore, ignore it @@ -380,7 +401,11 @@ def _write_py_statistics(self, op: TextIO) -> None: else: value = 100.0 - table.append([module, '%.2f%%' % value, '%d' % len(self.py_undocumented[module])]) + table.append([ + module, + f'{value:.2f}%', + str(len(self.py_undocumented[module])), + ]) if all_objects: table.append([ @@ -397,7 +422,7 @@ def _write_py_statistics(self, op: TextIO) -> None: def write_py_coverage(self) -> None: output_file = os.path.join(self.outdir, 'python.txt') failed = [] - with open(output_file, 'w', encoding="utf-8") as op: + with open(output_file, 'w', encoding='utf-8') as op: if self.config.coverage_write_headline: write_header(op, 'Undocumented Python objects', '=') @@ -427,27 +452,37 @@ def write_py_coverage(self) -> None: for func in undoc['funcs']: logger.warning( __('undocumented python function: %s :: %s'), - name, func) + name, + func, + ) else: for func in undoc['funcs']: - logger.info(red('undocumented ') + 'py ' + 'function ' + - '%-30s' % func + red(' - in module ') + name) + logger.info( + red('undocumented ') + + f'py function {func:<30}' + + red(' - in module ') + + name + ) op.write('\n') if undoc['classes']: op.write('Classes:\n') - for class_name, methods in sorted( - undoc['classes'].items()): + for class_name, methods in sorted(undoc['classes'].items()): if not methods: op.write(' * %s\n' % class_name) if self.config.coverage_show_missing_items: if self.app.quiet: logger.warning( __('undocumented python class: %s :: %s'), - name, class_name) + name, + class_name, + ) else: - logger.info(red('undocumented ') + 'py ' + - 'class ' + '%-30s' % class_name + - red(' - in module ') + name) + logger.info( + red('undocumented ') + + f'py class {class_name:<30}' + + red(' - in module ') + + name + ) else: op.write(' * %s -- missing methods:\n\n' % class_name) op.writelines(' - %s\n' % x for x in methods) @@ -455,27 +490,36 @@ def write_py_coverage(self) -> None: if self.app.quiet: for meth in methods: logger.warning( - __('undocumented python method:' - ' %s :: %s :: %s'), - name, class_name, meth) + __( + 'undocumented python method:' + ' %s :: %s :: %s' + ), + name, + class_name, + meth, + ) else: for meth in methods: - logger.info(red('undocumented ') + 'py ' + - 'method ' + '%-30s' % - (class_name + '.' + meth) + - red(' - in module ') + name) + logger.info( + red('undocumented ') + + f'py method {f"{class_name}.{meth}":<30}' + + red(' - in module ') + + name + ) op.write('\n') if failed: write_header(op, 'Modules that failed to import') - op.writelines(' * %s -- %s\n' % x for x in failed) + op.writelines(f' * {name} -- {err}\n' for name, err in failed) def finish(self) -> None: # dump the coverage data to a pickle file too picklepath = os.path.join(self.outdir, 'undoc.pickle') with open(picklepath, 'wb') as dumpfile: - pickle.dump((self.py_undoc, self.c_undoc, - self.py_undocumented, self.py_documented), dumpfile) + pickle.dump( + (self.py_undoc, self.c_undoc, self.py_undocumented, self.py_documented), + dumpfile, + ) def setup(app: Sphinx) -> ExtensionMetadata: