diff --git a/Changelog.rst b/Changelog.rst index 02cd37e..df09155 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -4,6 +4,16 @@ Change history ================ +.. _version-unreleased: + +Unreleased +========== + +- Do not offer true/false value completions for boolean flags (``is_flag=True``, + including ``--foo/--no-foo`` style options), which previously inserted a value + into the positional-argument slot (#116). + + .. _version-0.3.0: 0.3.0 diff --git a/click_repl/_completer.py b/click_repl/_completer.py index 73a769c..5556607 100644 --- a/click_repl/_completer.py +++ b/click_repl/_completer.py @@ -173,7 +173,13 @@ def _get_completion_from_params( choices: list[Completion] = [] param_type = param.type - # shell_complete method for click.Choice is intorduced in click-v8 + # Flags (is_flag=True, incl. secondary opts like --foo/--no-foo) do not + # consume a value, so we must not offer any value completions for them. + # See https://github.com/click-contrib/click-repl/issues/116 + if getattr(param, "is_flag", False): + return choices + + # shell_complete method for click.Choice was introduced in Click v8 if not HAS_CLICK_V8 and isinstance(param_type, click.Choice): choices.extend( self._get_completion_from_choices_click_le_7(param, incomplete) 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 b4b2659..ff8b89b 100644 --- a/tests/test_completion/test_common_tests/test_option_completion.py +++ b/tests/test_completion/test_common_tests/test_option_completion.py @@ -38,6 +38,38 @@ def bool_option(foo): assert {x.text for x in completions} == {"true"} +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. + # See https://github.com/click-contrib/click-repl/issues/116 + @root_command.command() + @click.argument("arg1", type=click.STRING) + @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 = list(c.get_completions(Document("flag-option --some-option "))) + assert {x.text for x in completions} == set() + + +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. + @root_command.command() + @click.option("--shout/--no-shout", default=False) + def toggle_flag(shout): + pass + + completions = list(c.get_completions(Document("toggle-flag --shout "))) + assert {x.text for x in completions} == set() + + completions = list(c.get_completions(Document("toggle-flag --no-shout "))) + assert {x.text for x in completions} == set() + + def test_only_unused_with_unique_option(): @root_command.command() @click.option("-u", type=click.BOOL)