From ac39e649cadb7f4d049b44ee37714a15b0d02f7b Mon Sep 17 00:00:00 2001 From: euiaujr Date: Tue, 13 Oct 2026 16:09:23 +0200 Subject: [PATCH] Fix completion stopping after a boolean flag (#116) A boolean flag (is_flag=True) consumes no value, so matching it as the last token must not short-circuit completion of the remaining parameters. Previously the completer returned nothing after a flag; now it continues offering the command's other options and arguments while still not offering true/false for the flag itself. - _completer.py: skip the param_called/break short-circuit for flags - tests: assert completion continues after a flag (options + arguments) - Changelog: add entry --- Changelog.rst | 9 +++ click_repl/_completer.py | 8 ++- .../test_option_completion.py | 56 +++++++++++++++---- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/Changelog.rst b/Changelog.rst index 72b8a9d..91153b0 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -5,6 +5,15 @@ ================ +.. _version-unreleased: + +Unreleased +========== + +- Fix completion stopping after a boolean flag; remaining options and + arguments now complete (#116) + + .. _version-0.4.1: 0.4.1 diff --git a/click_repl/_completer.py b/click_repl/_completer.py index 995622c..6f69404 100644 --- a/click_repl/_completer.py +++ b/click_repl/_completer.py @@ -241,8 +241,12 @@ def _get_completion_for_cmd_args( for option in opts: # We want to make sure if this parameter was called # If we are inside a parameter that was called, we want to show only - # relevant choices - if option in current_args: # noqa: E203 + # relevant choices. A boolean flag consumes no value, so it must + # not short-circuit completion of the parameters that follow it. + # See https://github.com/click-contrib/click-repl/issues/116 + if option in current_args and not getattr( # noqa: E203 + param, "is_flag", False + ): param_called = True break diff --git a/tests/test_completion/test_common_tests/test_option_completion.py b/tests/test_completion/test_common_tests/test_option_completion.py index 4e81418..9f2260f 100644 --- a/tests/test_completion/test_common_tests/test_option_completion.py +++ b/tests/test_completion/test_common_tests/test_option_completion.py @@ -41,33 +41,67 @@ def bool_option(foo): def test_flag_option_offers_no_value(): # A flag (is_flag=True) does not consume a value, so once it has been # typed the completer must not offer true/false as if it needed one. + # It must, however, keep completing the command's other parameters + # instead of stopping. # See https://github.com/click-contrib/click-repl/issues/116 @root_command.command() - @click.argument("arg1", type=click.STRING) + @click.argument("arg1", type=click.Choice(("alpha", "beta"))) @click.option("-b", "--some-option", "some_option", is_flag=True) def flag_option(arg1, some_option): pass - completions = list(c.get_completions(Document("flag-option -b "))) - assert {x.text for x in completions} == set() + completions = {x.text for x in c.get_completions(Document("flag-option -b "))} + # No true/false value completion for the flag itself. + assert "true" not in completions and "false" not in completions + # Remaining argument still completes. + assert {"alpha", "beta"} <= completions - completions = list(c.get_completions(Document("flag-option --some-option "))) - assert {x.text for x in completions} == set() + completions = {x.text for x in c.get_completions(Document("flag-option --some-option "))} + assert "true" not in completions and "false" not in completions + assert {"alpha", "beta"} <= completions def test_boolean_flag_with_secondary_opts_offers_no_value(): # Flags declared with a secondary option (--foo/--no-foo) are also flags - # and must not trigger true/false value completions. + # and must not trigger true/false value completions, while still letting + # the remaining parameters complete. @root_command.command() + @click.argument("arg1", type=click.Choice(("alpha", "beta"))) @click.option("--shout/--no-shout", default=False) - def toggle_flag(shout): + def toggle_flag(arg1, shout): pass - completions = list(c.get_completions(Document("toggle-flag --shout "))) - assert {x.text for x in completions} == set() + completions = {x.text for x in c.get_completions(Document("toggle-flag --shout "))} + assert "true" not in completions and "false" not in completions + assert {"alpha", "beta"} <= completions - completions = list(c.get_completions(Document("toggle-flag --no-shout "))) - assert {x.text for x in completions} == set() + completions = {x.text for x in c.get_completions(Document("toggle-flag --no-shout "))} + assert "true" not in completions and "false" not in completions + assert {"alpha", "beta"} <= completions + + +def test_completion_continues_after_boolean_flag(): + # A flag consumes no value, so after it has been typed the completer must + # still offer the command's remaining options and arguments instead of + # stopping. See https://github.com/click-contrib/click-repl/issues/116 + @root_command.command() + @click.argument("arg1", type=click.Choice(("foo", "bar"))) + @click.option("--verbose", is_flag=True) + @click.option("--mode", type=click.Choice(("fast", "slow"))) + def continue_after_flag(arg1, verbose, mode): + pass + + # Remaining options are still offered after the flag. + completions = list(c.get_completions(Document("continue-after-flag --verbose --"))) + assert {x.text for x in completions} >= {"--verbose", "--mode"} + + # The positional argument still completes after the flag. + completions = list(c.get_completions(Document("continue-after-flag --verbose "))) + assert {"foo", "bar"} <= {x.text for x in completions} + + # Still works after more than one flag. + completions = list(c.get_completions(Document("continue-after-flag --verbose ba"))) + assert {x.text for x in completions} == {"bar"} def test_only_unused_with_unique_option():