diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index ff5a229..002b3f2 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -36,6 +36,10 @@ _optparse_callable_map: Dict[str, Union[Type[Any], _ARG]] = { } +_optparse_default: str = "%default" +_argparse_default: str = "%(default)s" + + class _CallbackAction(argparse.Action): """Shim for optparse-style callback actions.""" @@ -189,13 +193,18 @@ class Option: short_option_name, long_option_name = _ARG.NO, short_option_name # optparse -> argparse `%default` => `%(default)s` - if help is not _ARG.NO and "%default" in help: + if help is not _ARG.NO and _optparse_default in help: LOG.warning( - "option %s: please update `help=` text to use %%(default)s " - "instead of %%default -- this will be an error in the future", + "option %s: please update `help=` text to use %s " + "instead of %s -- this will be an error in the future", long_option_name, + _argparse_default, + _optparse_default, ) - help = help.replace("%default", "%(default)s") + help = help.replace(_optparse_default, _argparse_default) + + # ensure help text doesn't get clobbered by later updates to defaults + help = self._bind_defaults_to_help_text(help, default) # optparse -> argparse for `callback` if action == "callback": @@ -294,6 +303,26 @@ class Option: parts.append(f"{k}={v!r}") return f"Option({', '.join(parts)})" + @staticmethod + def _bind_defaults_to_help_text( + help: Union[str, _ARG] = _ARG.NO, + default: Union[Any, _ARG] = _ARG.NO, + long_option_name: Union[str, _ARG] = _ARG.NO, + ) -> Union[str, _ARG]: + """Ensure help text doesn't get clobbered by updates to defaults.""" + if help is not _ARG.NO and _argparse_default in help: + if default is _ARG.NO: + LOG.warning( + "option %s: %s detected in `help=` text, " + "but no default value was provided", + long_option_name, + _argparse_default, + ) + else: + help = help.replace(_argparse_default, "%s") % default + + return help + def normalize(self, value: Any, *normalize_args: str) -> Any: """Normalize the value based on the option configuration.""" if self.comma_separated_list and isinstance(value, str): diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index fe254b7..830b5c6 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -1,12 +1,14 @@ """Integration tests for the main entrypoint of flake8.""" import json import os +import re import sys from unittest import mock import pytest from flake8 import utils +from flake8.defaults import EXCLUDE from flake8.main import cli from flake8.options import config @@ -149,6 +151,33 @@ def test_extend_exclude(tmpdir, capsys): assert err == "" +def test_config_value_does_not_clobber_default_help_text(tmpdir, capsys): + """Test a config value does not clobber the default help text output.""" + setup_cfg = """\ +[flake8] +exclude = 1,2 +""" + expected = ",".join(EXCLUDE) + + default_pattern = re.compile(r"\(Default:(.*?)\)") + whitespace_pattern = re.compile(r"\s+") + + with tmpdir.as_cwd(): + tmpdir.join("setup.cfg").write(setup_cfg) + with pytest.raises(SystemExit): + cli.main(["-h"]) + + out, err = capsys.readouterr() + + for section in out.split("--"): + if section.startswith("exclude patterns"): + section = re.sub(whitespace_pattern, "", section) + default_groups = re.search(default_pattern, section) + assert default_groups is not None + default = default_groups.group(1) + assert default == expected + + def test_malformed_per_file_ignores_error(tmpdir, capsys): """Test the error message for malformed `per-file-ignores`.""" setup_cfg = """\