diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 7b7a0a1d4..95a4b6267 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -419,6 +419,20 @@ General configuration .. versionadded:: 1.1 +.. confval:: nitpick_ignore_regex + + An extended version of :confval:`nitpick_ignore`, which instead interprets + the ``type`` and ``target`` strings as regular expressions. Note, that the + regular expression must match the whole string (as if the ``^`` and ``$`` + markers were inserted). + + For example, ``(r'py:.*', r'foo.*bar\.B.*')`` will ignore nitpicky warnings + for all python entities that start with ``'foo'`` and have ``'bar.B'`` in + them, such as ``('py:const', 'foo_package.bar.BAZ_VALUE')`` or + ``('py:class', 'food.bar.Barman')``. + + .. versionadded:: 4.1 + .. confval:: numfig If true, figures, tables and code-blocks are automatically numbered if they diff --git a/sphinx/config.py b/sphinx/config.py index 5e265f576..1ba98d007 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -131,6 +131,7 @@ class Config: 'manpages_url': (None, 'env', []), 'nitpicky': (False, None, []), 'nitpick_ignore': ([], None, []), + 'nitpick_ignore_regex': ([], None, []), 'numfig': (False, 'env', []), 'numfig_secnum_depth': (1, 'env', []), 'numfig_format': ({}, 'env', []), # will be initialized in init_numfig_format() diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index e2899d994..281407983 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -8,6 +8,7 @@ :license: BSD, see LICENSE for details. """ +import re from typing import Any, Dict, List, Optional, Tuple, Type, cast from docutils import nodes @@ -171,14 +172,27 @@ class ReferencesResolver(SphinxPostTransform): warn = node.get('refwarn') if self.config.nitpicky: warn = True + dtype = '%s:%s' % (domain.name, typ) if domain else typ if self.config.nitpick_ignore: - dtype = '%s:%s' % (domain.name, typ) if domain else typ if (dtype, target) in self.config.nitpick_ignore: warn = False # for "std" types also try without domain name if (not domain or domain.name == 'std') and \ (typ, target) in self.config.nitpick_ignore: warn = False + if self.config.nitpick_ignore_regex: + def matches_ignore(entry_type: str, entry_target: str) -> bool: + for ignore_type, ignore_target in self.config.nitpick_ignore_regex: + if re.fullmatch(ignore_type, entry_type) and \ + re.fullmatch(ignore_target, entry_target): + return True + return False + if matches_ignore(dtype, target): + warn = False + # for "std" types also try without domain name + if (not domain or domain.name == 'std') and \ + matches_ignore(typ, target): + warn = False if not warn: return diff --git a/tests/roots/test-nitpicky-warnings/conf.py b/tests/roots/test-nitpicky-warnings/conf.py new file mode 100644 index 000000000..2db221cc6 --- /dev/null +++ b/tests/roots/test-nitpicky-warnings/conf.py @@ -0,0 +1 @@ +nitpicky = True diff --git a/tests/roots/test-nitpicky-warnings/index.rst b/tests/roots/test-nitpicky-warnings/index.rst new file mode 100644 index 000000000..e73840d4f --- /dev/null +++ b/tests/roots/test-nitpicky-warnings/index.rst @@ -0,0 +1,7 @@ +test-nitpicky-warnings +====================== + +:py:const:`prefix.anything.postfix` +:py:class:`prefix.anything` +:py:class:`anything.postfix` +:js:class:`prefix.anything.postfix` diff --git a/tests/test_config.py b/tests/test_config.py index a48e7ce30..b9b4f612a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -311,3 +311,77 @@ def test_check_enum_for_list_failed(logger): config.init_values() check_confval_types(None, config) assert logger.warning.called + + +nitpick_warnings = [ + "WARNING: py:const reference target not found: prefix.anything.postfix", + "WARNING: py:class reference target not found: prefix.anything", + "WARNING: py:class reference target not found: anything.postfix", + "WARNING: js:class reference target not found: prefix.anything.postfix", +] + + +@pytest.mark.sphinx(testroot='nitpicky-warnings') +def test_nitpick_base(app, status, warning): + app.builder.build_all() + + warning = warning.getvalue().strip().split('\n') + assert len(warning) == len(nitpick_warnings) + for actual, expected in zip(warning, nitpick_warnings): + assert expected in actual + + +@pytest.mark.sphinx(testroot='nitpicky-warnings', confoverrides={ + 'nitpick_ignore': [ + ('py:const', 'prefix.anything.postfix'), + ('py:class', 'prefix.anything'), + ('py:class', 'anything.postfix'), + ('js:class', 'prefix.anything.postfix'), + ], +}) +def test_nitpick_ignore(app, status, warning): + app.builder.build_all() + assert not len(warning.getvalue().strip()) + + +@pytest.mark.sphinx(testroot='nitpicky-warnings', confoverrides={ + 'nitpick_ignore_regex': [ + (r'py:.*', r'.*postfix'), + (r'.*:class', r'prefix.*'), + ] +}) +def test_nitpick_ignore_regex1(app, status, warning): + app.builder.build_all() + assert not len(warning.getvalue().strip()) + + +@pytest.mark.sphinx(testroot='nitpicky-warnings', confoverrides={ + 'nitpick_ignore_regex': [ + (r'py:.*', r'prefix.*'), + (r'.*:class', r'.*postfix'), + ] +}) +def test_nitpick_ignore_regex2(app, status, warning): + app.builder.build_all() + assert not len(warning.getvalue().strip()) + + +@pytest.mark.sphinx(testroot='nitpicky-warnings', confoverrides={ + 'nitpick_ignore_regex': [ + # None of these should match + (r'py:', r'.*'), + (r':class', r'.*'), + (r'', r'.*'), + (r'.*', r'anything'), + (r'.*', r'prefix'), + (r'.*', r'postfix'), + (r'.*', r''), + ] +}) +def test_nitpick_ignore_regex_fullmatch(app, status, warning): + app.builder.build_all() + + warning = warning.getvalue().strip().split('\n') + assert len(warning) == len(nitpick_warnings) + for actual, expected in zip(warning, nitpick_warnings): + assert expected in actual