diff --git a/doc/usage/extensions/coverage.rst b/doc/usage/extensions/coverage.rst index 46d31053c..8c8054d5f 100644 --- a/doc/usage/extensions/coverage.rst +++ b/doc/usage/extensions/coverage.rst @@ -51,4 +51,11 @@ should check: .. versionadded:: 1.1 -.. _Python regular expressions: https://docs.python.org/library/re \ No newline at end of file +.. conval:: coverage_show_missing_items + + Print objects that are missing to standard output also. + ``False`` by default. + + .. versionadded:: 3.1 + +.. _Python regular expressions: https://docs.python.org/library/re diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index e8157848f..536b3b9d2 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -22,6 +22,7 @@ from sphinx.application import Sphinx from sphinx.builders import Builder from sphinx.locale import __ from sphinx.util import logging +from sphinx.util.console import red # type: ignore from sphinx.util.inspect import safe_getattr logger = logging.getLogger(__name__) @@ -121,6 +122,14 @@ class CoverageBuilder(Builder): write_header(op, filename) for typ, name in sorted(undoc): op.write(' * %-50s [%9s]\n' % (name, typ)) + if self.config.coverage_show_missing_items: + if self.app.quiet or self.app.warningiserror: + 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) op.write('\n') def ignore_pyobj(self, full_name: str) -> bool: @@ -239,16 +248,48 @@ class CoverageBuilder(Builder): if undoc['funcs']: op.write('Functions:\n') op.writelines(' * %s\n' % x for x in undoc['funcs']) + if self.config.coverage_show_missing_items: + if self.app.quiet or self.app.warningiserror: + for func in undoc['funcs']: + logger.warning( + __('undocumented python function: %s :: %s'), + name, func) + else: + for func in undoc['funcs']: + logger.info(red('undocumented ') + 'py ' + 'function ' + + '%-30s' % func + red(' - in module ') + name) op.write('\n') if undoc['classes']: op.write('Classes:\n') - for name, methods in sorted( + for class_name, methods in sorted( undoc['classes'].items()): if not methods: - op.write(' * %s\n' % name) + op.write(' * %s\n' % class_name) + if self.config.coverage_show_missing_items: + if self.app.quiet or self.app.warningiserror: + logger.warning( + __('undocumented python class: %s :: %s'), + name, class_name) + else: + logger.info(red('undocumented ') + 'py ' + + 'class ' + '%-30s' % class_name + + red(' - in module ') + name) else: - op.write(' * %s -- missing methods:\n\n' % name) + op.write(' * %s -- missing methods:\n\n' % class_name) op.writelines(' - %s\n' % x for x in methods) + if self.config.coverage_show_missing_items: + if self.app.quiet or self.app.warningiserror: + for meth in methods: + logger.warning( + __('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) op.write('\n') if failed: @@ -273,4 +314,5 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('coverage_ignore_c_items', {}, False) app.add_config_value('coverage_write_headline', True, False) app.add_config_value('coverage_skip_undoc_in_source', False, False) + app.add_config_value('coverage_show_missing_items', False, False) return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/tests/test_ext_coverage.py b/tests/test_ext_coverage.py index 16f53112b..033a1c1b1 100644 --- a/tests/test_ext_coverage.py +++ b/tests/test_ext_coverage.py @@ -28,6 +28,8 @@ def test_build(app, status, warning): assert ' * mod -- No module named mod' # in the "failed import" section + assert "undocumented py" not in status.getvalue() + c_undoc = (app.outdir / 'c.txt').read_text() assert c_undoc.startswith('Undocumented C API elements\n' '===========================\n') @@ -46,6 +48,8 @@ def test_build(app, status, warning): assert 'Class' in undoc_py['autodoc_target']['classes'] assert 'undocmeth' in undoc_py['autodoc_target']['classes']['Class'] + assert "undocumented c" not in status.getvalue() + @pytest.mark.sphinx('coverage', testroot='ext-coverage') def test_coverage_ignore_pyobjects(app, status, warning): @@ -64,3 +68,28 @@ Classes: ''' assert actual == expected + + +@pytest.mark.sphinx('coverage', confoverrides={'coverage_show_missing_items': True}) +def test_show_missing_items(app, status, warning): + app.builder.build_all() + + assert "undocumented" in status.getvalue() + + assert "py function raises" in status.getvalue() + assert "py class Base" in status.getvalue() + assert "py method Class.roger" in status.getvalue() + + assert "c api Py_SphinxTest [ function]" in status.getvalue() + + +@pytest.mark.sphinx('coverage', confoverrides={'coverage_show_missing_items': True}) +def test_show_missing_items_quiet(app, status, warning): + app.quiet = True + app.builder.build_all() + + assert "undocumented python function: autodoc_target :: raises" in warning.getvalue() + assert "undocumented python class: autodoc_target :: Base" in warning.getvalue() + assert "undocumented python method: autodoc_target :: Class :: roger" in warning.getvalue() + + assert "undocumented c api: Py_SphinxTest [function]" in warning.getvalue()