diff --git a/docs/usage.md b/docs/usage.md index 98cd3181..f65ed0e5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -117,6 +117,22 @@ If any of the list values is a pair, it should be a tuple like: `(label, value)` As before, the `answers` is a `dict` containing the previous answers. +### hints + +**Optional** for `Checkbox` and `List` questions; the rest of them do not have hints. + +The hint for the selected choice will be shown above the first choice. + +```python +from inquirer import questions +choices = { + "foo": "Foo", + "bar": "Bar", + "bazz": "Bazz", +} +question = questions.Checkbox("foo", "Choose one:", choices=choices.keys(), hints=choices) +``` + ### validate Optional attribute that allows the program to check if the answer is valid or not. It requires a `boolean` value or a `function` with the signature: diff --git a/examples/checkbox_hints.py b/examples/checkbox_hints.py new file mode 100644 index 00000000..879d583a --- /dev/null +++ b/examples/checkbox_hints.py @@ -0,0 +1,21 @@ +from pprint import pprint + +import inquirer # noqa + + +choices_hints = { + ("Computers", "c"): "The really Geeky stuff", + ("Books", "b"): "Its just so cosy", + ("Science", "s"): "I want to know it all", + ("Nature", "n"): "Always outdoors", +} + +questions = [ + inquirer.Checkbox( + "interests", message="What are you interested in?", choices=choices_hints.keys(), hints=choices_hints + ), +] + +answers = inquirer.prompt(questions) + +pprint(answers) diff --git a/examples/list_hints.py b/examples/list_hints.py new file mode 100644 index 00000000..b2877ea8 --- /dev/null +++ b/examples/list_hints.py @@ -0,0 +1,17 @@ +from pprint import pprint + +import inquirer # noqa + +choices_hints = { + "Jumbo": "The biggest one we have", + "Large": "If you need the extra kick", + "Standard": "For your every day use", +} + +questions = [ + inquirer.List("size", message="What size do you need?", choices=choices_hints.keys(), hints=choices_hints), +] + +answers = inquirer.prompt(questions) + +pprint(answers) diff --git a/src/inquirer/questions.py b/src/inquirer/questions.py index 2d93ed67..8dbbffea 100644 --- a/src/inquirer/questions.py +++ b/src/inquirer/questions.py @@ -11,24 +11,30 @@ class TaggedValue: - def __init__(self, label, value): - self.label = label - self.value = value + def __init__(self, choice): + self.label = choice[0] + self.tag = choice[1] + self._hash = hash(choice) def __str__(self): return self.label def __repr__(self): - return self.value + return repr(self.tag) def __eq__(self, other): if isinstance(other, TaggedValue): - return self.value == other.value - return self.value == other + return other.tag == self.tag + if isinstance(other, tuple): + return other == (self.label, self.tag) + return other == self.tag def __ne__(self, other): return not self.__eq__(other) + def __hash__(self) -> int: + return self._hash + class Question: kind = "base question" @@ -42,6 +48,7 @@ def __init__( ignore=False, validate=True, show_default=False, + hints=None, other=False, ): self.name = name @@ -52,6 +59,7 @@ def __init__( self._validate = validate self.answers = {} self.show_default = show_default + self.hints = hints or {} self._other = other if self._other: @@ -84,7 +92,7 @@ def default(self): @property def choices_generator(self): for choice in self._solve(self._choices): - yield (TaggedValue(*choice) if isinstance(choice, tuple) and len(choice) == 2 else choice) + yield (TaggedValue(choice) if isinstance(choice, tuple) and len(choice) == 2 else choice) @property def choices(self): @@ -143,6 +151,7 @@ def __init__( name, message="", choices=None, + hints=None, default=None, ignore=False, validate=True, @@ -150,7 +159,7 @@ def __init__( other=False, autocomplete=None, ): - super().__init__(name, message, choices, default, ignore, validate, other=other) + super().__init__(name, message, choices, default, ignore, validate, hints=hints, other=other) self.carousel = carousel self.autocomplete = autocomplete @@ -163,6 +172,7 @@ def __init__( name, message="", choices=None, + hints=None, locked=None, default=None, ignore=False, @@ -171,7 +181,7 @@ def __init__( other=False, autocomplete=None, ): - super().__init__(name, message, choices, default, ignore, validate, other=other) + super().__init__(name, message, choices, default, ignore, validate, hints=hints, other=other) self.locked = locked self.carousel = carousel self.autocomplete = autocomplete diff --git a/src/inquirer/render/console/__init__.py b/src/inquirer/render/console/__init__.py index c5658023..7e0d84d2 100644 --- a/src/inquirer/render/console/__init__.py +++ b/src/inquirer/render/console/__init__.py @@ -46,6 +46,7 @@ def _event_loop(self, render): self._print_status_bar(render) self._print_header(render) + self._print_hint(render) self._print_options(render) self._process_input(render) @@ -90,6 +91,15 @@ def _print_header(self, render): tq=self._theme.Question, ) + def _print_hint(self, render): + msg_template = "{t.move_up}{t.clear_eol}{color}{msg}" + hint = render.get_hint() + color = self._theme.Question.mark_color + if hint: + self.print_str( + f"\n{msg_template}", msg=hint, color=color, lf=not render.title_inline, tq=self._theme.Question + ) + def _process_input(self, render): try: ev = self._event_gen.next() diff --git a/src/inquirer/render/console/_checkbox.py b/src/inquirer/render/console/_checkbox.py index 93781122..6e40c5bd 100644 --- a/src/inquirer/render/console/_checkbox.py +++ b/src/inquirer/render/console/_checkbox.py @@ -14,6 +14,13 @@ def __init__(self, *args, **kwargs): self.selection = [k for (k, v) in enumerate(self.question.choices) if v in self.default_choices()] self.current = 0 + def get_hint(self): + try: + hint = self.question.hints[self.question.choices[self.current]] + return hint or "" + except KeyError: + return "" + def default_choices(self): default = self.question.default or [] return default + self.locked diff --git a/src/inquirer/render/console/_list.py b/src/inquirer/render/console/_list.py index 51f1dbaf..5cb48af3 100644 --- a/src/inquirer/render/console/_list.py +++ b/src/inquirer/render/console/_list.py @@ -17,6 +17,17 @@ def is_long(self): choices = self.question.choices or [] return len(choices) >= MAX_OPTIONS_DISPLAYED_AT_ONCE + def get_hint(self): + try: + choice = self.question.choices[self.current] + hint = self.question.hints[choice] + if hint: + return f"{choice}: {hint}" + else: + return f"{choice}" + except (KeyError, IndexError): + return "" + def get_options(self): choices = self.question.choices or [] if self.is_long: @@ -87,9 +98,3 @@ def _current_index(self): return self.question.choices.index(self.question.default) except ValueError: return 0 - - def get_current_value(self): - try: - return self.question.choices[self.current] - except IndexError: - return "" diff --git a/src/inquirer/render/console/base.py b/src/inquirer/render/console/base.py index cfe35cfc..950d83ab 100644 --- a/src/inquirer/render/console/base.py +++ b/src/inquirer/render/console/base.py @@ -26,6 +26,9 @@ def other_input(self): def get_header(self): return self.question.message + def get_hint(self): + return "" + def get_current_value(self): return "" diff --git a/tests/integration/console_render/test_checkbox.py b/tests/integration/console_render/test_checkbox.py index 2414e140..f1531971 100644 --- a/tests/integration/console_render/test_checkbox.py +++ b/tests/integration/console_render/test_checkbox.py @@ -394,3 +394,37 @@ def test_locked_with_default(self): result = sut.render(question) assert result == ["bar"] + + def test_first_hint_is_shown(self): + stdin = helper.event_factory(key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = { + "foo": "Foo", + "bar": "Bar", + "bazz": "Bazz", + } + + question = questions.Checkbox(variable, message, choices=choices.keys(), hints=choices) + + sut = ConsoleRender(event_generator=stdin) + sut.render(question) + + self.assertInStdout("Foo") + + def test_second_hint_is_shown(self): + stdin = helper.event_factory(key.DOWN, key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = { + "foo": "Foo", + "bar": "Bar", + "bazz": "Bazz", + } + + question = questions.Checkbox(variable, message, choices=choices.keys(), hints=choices) + + sut = ConsoleRender(event_generator=stdin) + sut.render(question) + + self.assertInStdout("Bar") diff --git a/tests/integration/console_render/test_list.py b/tests/integration/console_render/test_list.py index 5e92e5f6..c4b2e747 100644 --- a/tests/integration/console_render/test_list.py +++ b/tests/integration/console_render/test_list.py @@ -131,3 +131,37 @@ def test_ctrl_c_breaks_execution(self): sut = ConsoleRender(event_generator=stdin) with pytest.raises(KeyboardInterrupt): sut.render(question) + + def test_first_hint_is_shown(self): + stdin = helper.event_factory(key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = { + "foo": "Foo", + "bar": "Bar", + "bazz": "Bazz", + } + + question = questions.List(variable, message, choices=choices.keys(), hints=choices) + + sut = ConsoleRender(event_generator=stdin) + sut.render(question) + + self.assertInStdout("Foo") + + def test_second_hint_is_shown(self): + stdin = helper.event_factory(key.DOWN, key.ENTER) + message = "Foo message" + variable = "Bar variable" + choices = { + "foo": "Foo", + "bar": "Bar", + "bazz": "Bazz", + } + + question = questions.List(variable, message, choices=choices.keys(), hints=choices) + + sut = ConsoleRender(event_generator=stdin) + sut.render(question) + + self.assertInStdout("Bar") diff --git a/tests/unit/test_question.py b/tests/unit/test_question.py index 618d55f5..9c9bdc8d 100644 --- a/tests/unit/test_question.py +++ b/tests/unit/test_question.py @@ -353,10 +353,17 @@ def test_default_value_validation(self): def test_tagged_value(): - tv = questions.TaggedValue("label", "value") - - assert tv.__str__() == "label" - assert tv.__repr__() == "value" - assert tv.__eq__(tv) is True - assert tv.__eq__("") is False - assert tv.__ne__(tv) is False + LABEL = "label" + TAG = "l" + tp = (LABEL, TAG) + tv = questions.TaggedValue(tp) + + assert (str(tv) == str(LABEL)) is True + assert (repr(tv) == repr(TAG)) is True + assert (hash(tv) == hash(tp)) is True + + assert (tv == tv) is True + assert (tv != tv) is False + assert (tv == tp) is True + assert (tv == TAG) is True + assert (tv == "") is False