Skip to content

Commit fe3ad76

Browse files
authored
Strip ANSI from confirm() and prompt() (#3653)
2 parents 6ec99f8 + 8008ab9 commit fe3ad76

3 files changed

Lines changed: 68 additions & 0 deletions

File tree

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ Unreleased
66
Colorama is no longer a dependency and is not used. {issue}`2986` {pr}`3505`
77
- {class}`Argument` accepts a `help` parameter, and help output includes
88
a `Positional arguments` section when argument help is available. {issue}`2983` {pr}`3473`
9+
- `confirm()` and `prompt()` strip ANSI color and style codes from the
10+
prompt when the output stream does not support them, matching `echo()`.
11+
This stripping was lost in `8.4.0` when {pr}`2969` began writing the
12+
prompt with `input()` directly. {issue}`3572` {pr}`3653`
913
- Fix test failures when using pytest >= 9.1. {pr}`3656`
1014

1115
## Version 8.4.2

src/click/termui.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from contextlib import redirect_stdout
1212
from gettext import gettext as _
1313

14+
from . import _compat
1415
from ._compat import isatty
1516
from ._compat import strip_ansi
1617
from .exceptions import Abort
@@ -83,7 +84,20 @@ def hidden_prompt_func(prompt: str) -> str:
8384
def _readline_prompt(func: t.Callable[[str], str], text: str, err: bool) -> str:
8485
"""Call a prompt function, passing the full prompt on non-Windows so
8586
readline can handle line editing and cursor positioning correctly.
87+
88+
The prompt is handed to *func* (such as :func:`input`) rather than
89+
written through :func:`echo`, so it has to strip ANSI color and style
90+
codes itself when the destination stream does not support them. Without
91+
this the prompt would keep codes that :func:`echo` removes from the
92+
rest of the output.
8693
"""
94+
stream = sys.stderr if err else sys.stdout
95+
96+
# Look up ``should_strip_ansi`` on the module so that ``CliRunner``,
97+
# which patches it there during test isolation, is honored.
98+
if _compat.should_strip_ansi(stream, resolve_color_default()):
99+
text = strip_ansi(text)
100+
87101
if err:
88102
with redirect_stdout(sys.stderr):
89103
return func(text)

tests/test_termui.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,56 @@ def cli(password):
10221022
assert "Confirm Password: " in result.output
10231023

10241024

1025+
@pytest.mark.parametrize(
1026+
("call", "user_input", "color", "expect"),
1027+
[
1028+
pytest.param(
1029+
lambda: click.confirm(click.style("Hello World!", fg="green")),
1030+
"y",
1031+
False,
1032+
"Hello World! [y/N]: y\n",
1033+
id="confirm-no-color",
1034+
),
1035+
pytest.param(
1036+
lambda: click.confirm(click.style("Hello World!", fg="green")),
1037+
"y",
1038+
True,
1039+
"\x1b[32mHello World!\x1b[0m [y/N]: y\n",
1040+
id="confirm-color",
1041+
),
1042+
pytest.param(
1043+
lambda: click.prompt(click.style("Name", fg="green")),
1044+
"Bob",
1045+
False,
1046+
"Name: Bob\n",
1047+
id="prompt-no-color",
1048+
),
1049+
pytest.param(
1050+
lambda: click.prompt(click.style("Name", fg="green")),
1051+
"Bob",
1052+
True,
1053+
"\x1b[32mName\x1b[0m: Bob\n",
1054+
id="prompt-color",
1055+
),
1056+
],
1057+
)
1058+
def test_prompt_and_confirm_ansi_respects_color(
1059+
runner, call, user_input, color, expect
1060+
):
1061+
"""``confirm`` and ``prompt`` strip ANSI color and style codes from the
1062+
prompt when color is disabled and keep them when it is enabled, like
1063+
``echo``. The prompt is written by ``input`` rather than ``echo``, so it
1064+
used to keep the codes regardless (issue 3572).
1065+
"""
1066+
1067+
@click.command()
1068+
def cli():
1069+
call()
1070+
1071+
result = runner.invoke(cli, input=user_input, color=color)
1072+
assert result.output == expect
1073+
1074+
10251075
def test_false_show_default_cause_no_default_display_in_prompt(runner):
10261076
@click.command()
10271077
@click.option("--arg1", show_default=False, prompt=True, default="my-default-value")

0 commit comments

Comments
 (0)