diff --git a/Lib/argparse.py b/Lib/argparse.py index 29e6ebb9634261..9732648aa24498 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -548,6 +548,7 @@ def _get_actions_usage_parts(self, actions, groups): return parts, pos_start def _format_text(self, text): + text = str(text) if '%(prog)' in text: text = text % dict(prog=self._prog) text_width = max(self._width - self._current_indent, 11) @@ -564,6 +565,7 @@ def _apply_text_markup(self, text): When colors are disabled, backticks are preserved as-is. """ + text = str(text) t = self._theme if not t.reset: return text @@ -610,9 +612,9 @@ def _format_action(self, action): parts = [action_header] # if there was help for the action, add lines of help text - if action.help and action.help.strip(): + if action.help: help_text = self._expand_help(action) - if help_text: + if help_text.strip(): help_lines = self._split_lines(help_text, help_width) parts.append('%*s%s\n' % (indent_first, '', help_lines[0])) for line in help_lines[1:]: @@ -711,7 +713,7 @@ def _format_args(self, action, default_metavar): return result def _expand_help(self, action): - help_string = self._get_help_string(action) + help_string = str(self._get_help_string(action)) if '%' not in help_string: return self._apply_text_markup(help_string) params = dict(vars(action), prog=self._prog) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 1dc3f538f4ad8b..cf7b5f3b57dac5 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7398,6 +7398,19 @@ def setUp(self): lambda *args, **kwargs: True)) self.theme = _colorize.get_theme(force_color=True).argparse + class _LazyStr: + def __init__(self, message): + self._message = message + + def __str__(self): + return self._message + + def __contains__(self, item): + return item in self._message + + def __mod__(self, other): + return self._message % other + def test_argparse_color(self): # Arrange: create a parser with a bit of everything parser = argparse.ArgumentParser( @@ -7736,6 +7749,38 @@ def test_backtick_markup_in_description(self): ) self.assertNotIn("`", help_text) + def test_backtick_markup_in_description_accepts_string_like_proxy(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + description=self._LazyStr( + 'Run `python myapp` or ``python -m myapp`` to start.' + ), + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn( + f'Run {prog_extra}python myapp{reset} or ' + f'{prog_extra}python -m myapp{reset} to start.', + help_text, + ) + + def test_backtick_markup_in_epilog_accepts_string_like_proxy(self): + parser = argparse.ArgumentParser( + prog='myapp', + color=True, + epilog=self._LazyStr('Run `%(prog)s --help` for more info.'), + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}myapp --help{reset}', help_text) + def test_backtick_markup_multiple(self): parser = argparse.ArgumentParser( prog='PROG', @@ -7837,6 +7882,34 @@ def test_backtick_markup_in_argument_help_color_disabled(self): self.assertIn("set the `foo` value", help_text) self.assertNotIn("\x1b[", help_text) + def test_backtick_markup_in_argument_help_accepts_string_like_proxy(self): + parser = argparse.ArgumentParser(prog="PROG", color=True) + parser.add_argument( + "--foo", help=self._LazyStr("set the `foo` value") + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f"set the {prog_extra}foo{reset} value", help_text) + + def test_argument_help_interpolation_accepts_string_like_proxy(self): + parser = argparse.ArgumentParser(prog="PROG", color=True) + parser.add_argument( + "--foo", + default="bar", + help=self._LazyStr("set `foo` (default: %(default)s)"), + ) + + prog_extra = self.theme.prog_extra + interp = self.theme.interpolated_value + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f"set {prog_extra}foo{reset}", help_text) + self.assertIn(f"default: {interp}bar{reset}", help_text) + def test_help_with_format_specifiers(self): # GH-142950: format specifiers like %x should work with color=True parser = argparse.ArgumentParser(prog='PROG', color=True) diff --git a/Misc/NEWS.d/next/Library/2026-05-22-17-53-01.gh-issue-148468.o5fZ3K.rst b/Misc/NEWS.d/next/Library/2026-05-22-17-53-01.gh-issue-148468.o5fZ3K.rst new file mode 100644 index 00000000000000..bdd13ae243ef45 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-22-17-53-01.gh-issue-148468.o5fZ3K.rst @@ -0,0 +1,3 @@ +Fix a regression in :mod:`argparse` so colorized help formatting once again +accepts string-like proxy objects for descriptions, epilogs, and argument +help text.