Marco Ricci commited on 2025-05-29 23:02:50
Zeige 2 geänderte Dateien mit 95 Einfügungen und 4 Löschungen.
A discrepancy exists between the documentation of `click.prompt` and the actual behavior of `click.prompt` when mocked with `click.testing.CliRunner` on click 8.2.0 and below ([`pallets/click#2934`][BUG2934]): when prompting for input and if at end-of-file, the `CliRunner` may return an empty string instead of raising `click.Abort`. This usually translates to extra line breaks in the "mixed" and "echoed" runner output at the last prompt(s). We mitigate this discrepancy from both sides. On the code side, we wrap each call to `click.prompt` to treat aborts and empty responses the same, which is appropriate behavior for the types of prompts we issue. On the test side, we amend our existing tests to use empty-line input instead of no input, and explicitly test the "no input" scenario separately, accepting both the 8.2.0-or-lower or the 8.2.1-or-higher output. Because the behavior depends on the `click` version, which is beyond our control, we also adjust coverage measurement. [BUG2934]: https://github.com/pallets/click/issues/2934
... | ... |
@@ -657,6 +657,7 @@ def prompt_for_selection( |
657 | 657 |
click.echo(x, err=True, color=color) |
658 | 658 |
if n > 1: |
659 | 659 |
choices = click.Choice([''] + [str(i) for i in range(1, n + 1)]) |
660 |
+ try: |
|
660 | 661 |
choice = click.prompt( |
661 | 662 |
f'Your selection? (1-{n}, leave empty to abort)', |
662 | 663 |
err=True, |
... | ... |
@@ -665,6 +666,15 @@ def prompt_for_selection( |
665 | 666 |
show_default=False, |
666 | 667 |
default='', |
667 | 668 |
) |
669 |
+ except click.Abort: # pragma: no cover |
|
670 |
+ # This branch will not be triggered during testing on |
|
671 |
+ # `click` versions < 8.2.1, due to (non-monkeypatch-able) |
|
672 |
+ # deficiencies in `click.testing.CliRunner`. Therefore, as |
|
673 |
+ # an external source of nondeterminism, exclude it from |
|
674 |
+ # coverage. |
|
675 |
+ # |
|
676 |
+ # https://github.com/pallets/click/issues/2934 |
|
677 |
+ choice = '' |
|
668 | 678 |
if not choice: |
669 | 679 |
raise IndexError(EMPTY_SELECTION) |
670 | 680 |
return int(choice) - 1 |
... | ... |
@@ -763,6 +773,7 @@ def prompt_for_passphrase() -> str: |
763 | 773 |
The user input. |
764 | 774 |
|
765 | 775 |
""" |
776 |
+ try: |
|
766 | 777 |
return cast( |
767 | 778 |
'str', |
768 | 779 |
click.prompt( |
... | ... |
@@ -773,6 +784,14 @@ def prompt_for_passphrase() -> str: |
773 | 784 |
err=True, |
774 | 785 |
), |
775 | 786 |
) |
787 |
+ except click.Abort: # pragma: no cover |
|
788 |
+ # This branch will not be triggered during testing on `click` |
|
789 |
+ # versions < 8.2.1, due to (non-monkeypatch-able) deficiencies |
|
790 |
+ # in `click.testing.CliRunner`. Therefore, as an external source |
|
791 |
+ # of nondeterminism, exclude it from coverage. |
|
792 |
+ # |
|
793 |
+ # https://github.com/pallets/click/issues/2934 |
|
794 |
+ return '' |
|
776 | 795 |
|
777 | 796 |
|
778 | 797 |
def toml_key(*parts: str) -> str: |
... | ... |
@@ -666,16 +666,28 @@ class Parametrize(types.SimpleNamespace): |
666 | 666 |
), |
667 | 667 |
pytest.param( |
668 | 668 |
['--phrase', '--', 'sv'], |
669 |
- '', |
|
669 |
+ '\n', |
|
670 | 670 |
'No passphrase was given', |
671 | 671 |
id='phrase-sv', |
672 | 672 |
), |
673 | 673 |
pytest.param( |
674 |
- ['--key'], |
|
674 |
+ ['--phrase', '--', 'sv'], |
|
675 | 675 |
'', |
676 |
+ 'No passphrase was given', |
|
677 |
+ id='phrase-sv-eof', |
|
678 |
+ ), |
|
679 |
+ pytest.param( |
|
680 |
+ ['--key'], |
|
681 |
+ '\n', |
|
676 | 682 |
'No SSH key was selected', |
677 | 683 |
id='key-sv', |
678 | 684 |
), |
685 |
+ pytest.param( |
|
686 |
+ ['--key'], |
|
687 |
+ '', |
|
688 |
+ 'No SSH key was selected', |
|
689 |
+ id='key-sv-eof', |
|
690 |
+ ), |
|
679 | 691 |
], |
680 | 692 |
) |
681 | 693 |
CONFIG_EDITING_VIA_CONFIG_FLAG = pytest.mark.parametrize( |
... | ... |
@@ -4406,7 +4418,7 @@ A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam |
4406 | 4418 |
""" |
4407 | 4419 |
), 'expected clean exit' |
4408 | 4420 |
result = runner.invoke( |
4409 |
- driver, ['--heading='], input='', catch_exceptions=True |
|
4421 |
+ driver, ['--heading='], input='\n', catch_exceptions=True |
|
4410 | 4422 |
) |
4411 | 4423 |
assert result.error_exit(error=IndexError), ( |
4412 | 4424 |
'expected error exit and known error type' |
... | ... |
@@ -4427,6 +4439,43 @@ A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam |
4427 | 4439 |
Your selection? (1-10, leave empty to abort):\x20 |
4428 | 4440 |
""" |
4429 | 4441 |
), 'expected known output' |
4442 |
+ # click.testing.CliRunner on click < 8.2.1 incorrectly mocks the |
|
4443 |
+ # click prompting machinery, meaning that the mixed output will |
|
4444 |
+ # incorrectly contain a line break, contrary to what the |
|
4445 |
+ # documentation for click.prompt prescribes. |
|
4446 |
+ result = runner.invoke( |
|
4447 |
+ driver, ['--heading='], input='', catch_exceptions=True |
|
4448 |
+ ) |
|
4449 |
+ assert result.error_exit(error=IndexError), ( |
|
4450 |
+ 'expected error exit and known error type' |
|
4451 |
+ ) |
|
4452 |
+ assert result.stdout in { |
|
4453 |
+ """\ |
|
4454 |
+[1] Egg and bacon |
|
4455 |
+[2] Egg, sausage and bacon |
|
4456 |
+[3] Egg and spam |
|
4457 |
+[4] Egg, bacon and spam |
|
4458 |
+[5] Egg, bacon, sausage and spam |
|
4459 |
+[6] Spam, bacon, sausage and spam |
|
4460 |
+[7] Spam, egg, spam, spam, bacon and spam |
|
4461 |
+[8] Spam, spam, spam, egg and spam |
|
4462 |
+[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
|
4463 |
+[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam |
|
4464 |
+Your selection? (1-10, leave empty to abort):\x20 |
|
4465 |
+""", |
|
4466 |
+ """\ |
|
4467 |
+[1] Egg and bacon |
|
4468 |
+[2] Egg, sausage and bacon |
|
4469 |
+[3] Egg and spam |
|
4470 |
+[4] Egg, bacon and spam |
|
4471 |
+[5] Egg, bacon, sausage and spam |
|
4472 |
+[6] Spam, bacon, sausage and spam |
|
4473 |
+[7] Spam, egg, spam, spam, bacon and spam |
|
4474 |
+[8] Spam, spam, spam, egg and spam |
|
4475 |
+[9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam |
|
4476 |
+[10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam |
|
4477 |
+Your selection? (1-10, leave empty to abort): """, |
|
4478 |
+ }, 'expected known output' |
|
4430 | 4479 |
|
4431 | 4480 |
def test_112_prompt_for_selection_single(self) -> None: |
4432 | 4481 |
"""[`cli_helpers.prompt_for_selection`][] works in the "single" case.""" |
... | ... |
@@ -4459,7 +4508,7 @@ Great! |
4459 | 4508 |
result = runner.invoke( |
4460 | 4509 |
driver, |
4461 | 4510 |
['Will replace with spam, okay? (Please say "y" or "n".)'], |
4462 |
- input='', |
|
4511 |
+ input='\n', |
|
4463 | 4512 |
) |
4464 | 4513 |
assert result.error_exit(error=IndexError), ( |
4465 | 4514 |
'expected error exit and known error type' |
... | ... |
@@ -4472,6 +4521,29 @@ Will replace with spam, okay? (Please say "y" or "n".):\x20 |
4472 | 4521 |
Boo. |
4473 | 4522 |
""" |
4474 | 4523 |
), 'expected known output' |
4524 |
+ # click.testing.CliRunner on click < 8.2.1 incorrectly mocks the |
|
4525 |
+ # click prompting machinery, meaning that the mixed output will |
|
4526 |
+ # incorrectly contain a line break, contrary to what the |
|
4527 |
+ # documentation for click.prompt prescribes. |
|
4528 |
+ result = runner.invoke( |
|
4529 |
+ driver, |
|
4530 |
+ ['Will replace with spam, okay? (Please say "y" or "n".)'], |
|
4531 |
+ input='', |
|
4532 |
+ ) |
|
4533 |
+ assert result.error_exit(error=IndexError), ( |
|
4534 |
+ 'expected error exit and known error type' |
|
4535 |
+ ) |
|
4536 |
+ assert result.stdout in { |
|
4537 |
+ """\ |
|
4538 |
+[1] baked beans |
|
4539 |
+Will replace with spam, okay? (Please say "y" or "n".):\x20 |
|
4540 |
+Boo. |
|
4541 |
+""", |
|
4542 |
+ """\ |
|
4543 |
+[1] baked beans |
|
4544 |
+Will replace with spam, okay? (Please say "y" or "n".): Boo. |
|
4545 |
+""", |
|
4546 |
+ }, 'expected known output' |
|
4475 | 4547 |
|
4476 | 4548 |
def test_113_prompt_for_passphrase( |
4477 | 4549 |
self, |
4478 | 4550 |