Skip to content

Commit

Permalink
Merge pull request #433 from rizkyarlin/checkbox-hints
Browse files Browse the repository at this point in the history
Add hints support for checkboxes
  • Loading branch information
Cube707 authored Jan 8, 2024
2 parents 0bd5fc8 + d0155e5 commit 35e9198
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 22 deletions.
16 changes: 16 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 21 additions & 0 deletions examples/checkbox_hints.py
Original file line number Diff line number Diff line change
@@ -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)
17 changes: 17 additions & 0 deletions examples/list_hints.py
Original file line number Diff line number Diff line change
@@ -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)
28 changes: 19 additions & 9 deletions src/inquirer/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -42,6 +48,7 @@ def __init__(
ignore=False,
validate=True,
show_default=False,
hints=None,
other=False,
):
self.name = name
Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -143,14 +151,15 @@ def __init__(
name,
message="",
choices=None,
hints=None,
default=None,
ignore=False,
validate=True,
carousel=False,
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

Expand All @@ -163,6 +172,7 @@ def __init__(
name,
message="",
choices=None,
hints=None,
locked=None,
default=None,
ignore=False,
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/inquirer/render/console/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 7 additions & 0 deletions src/inquirer/render/console/_checkbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 11 additions & 6 deletions src/inquirer/render/console/_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 ""
3 changes: 3 additions & 0 deletions src/inquirer/render/console/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""

Expand Down
34 changes: 34 additions & 0 deletions tests/integration/console_render/test_checkbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
34 changes: 34 additions & 0 deletions tests/integration/console_render/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
21 changes: 14 additions & 7 deletions tests/unit/test_question.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 35e9198

Please sign in to comment.