Skip to content

Commit

Permalink
Merge pull request #327 from NivEz/main
Browse files Browse the repository at this point in the history
Added checkbox locked choices feature
  • Loading branch information
Cube707 authored Mar 6, 2023
2 parents c0c57f9 + fcb98ba commit 0af3ccb
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 8 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ questions = [
answers = inquirer.prompt(questions)
```

Checkbox questions can take one extra argument `carousel=False`. If set to true, the answers will rotate (back to first when pressing down on last choice, and down to last choice when pressing up on first choice)
Checkbox questions can take extra argument `carousel=False`. If set to true, the answers will rotate (back to first when pressing down on last choice, and down to last choice when pressing up on first choice)

Another argument that can be used is `locked=<List>`. The given choices in the locked argument cannot be removed. This is useful if you want to make clear that a specific option out of the choices must be chosen.

### Path

Expand Down
21 changes: 21 additions & 0 deletions examples/checkbox_locked.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os
import sys
from pprint import pprint


sys.path.append(os.path.realpath("."))
import inquirer # noqa


questions = [
inquirer.Checkbox(
"courses",
message="Which courses would you like to take?",
choices=["Programming fundamentals", "Fullstack development", "Data science", "DevOps"],
locked=["Programming fundamentals"],
),
]

answers = inquirer.prompt(questions)

pprint(answers)
2 changes: 2 additions & 0 deletions src/inquirer/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ def __init__(
name,
message="",
choices=None,
locked=None,
default=None,
ignore=False,
validate=True,
Expand All @@ -173,6 +174,7 @@ def __init__(
):

super().__init__(name, message, choices, default, ignore, validate, other=other)
self.locked = locked
self.carousel = carousel
self.autocomplete = autocomplete

Expand Down
20 changes: 14 additions & 6 deletions src/inquirer/render/console/_checkbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@
class Checkbox(BaseConsoleRender):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.selection = [k for (k, v) in enumerate(self.question.choices) if v in (self.question.default or [])]
self.locked = self.question.locked or []
self.selection = [k for (k, v) in enumerate(self.question.choices) if v in self.default_choices()]
self.current = 0

def default_choices(self):
default = self.question.default or []
return default + self.locked

@property
def is_long(self):
choices = self.question.choices or []
Expand All @@ -39,14 +44,12 @@ def get_options(self):
is_in_beginning = self.current <= half_options
is_in_middle = half_options < self.current < ending_milestone
is_in_end = self.current >= ending_milestone

for index, choice in enumerate(cchoices):
if (
(is_in_middle and self.current - half_options + index in self.selection)
or (is_in_beginning and index in self.selection)
or (is_in_end and index + max(len(choices) - MAX_OPTIONS_DISPLAYED_AT_ONCE, 0) in self.selection)
): # noqa

symbol = self.theme.Checkbox.selected_icon
color = self.theme.Checkbox.selected_color
else:
Expand All @@ -60,17 +63,20 @@ def get_options(self):
or (is_in_beginning and index == self.current)
or (is_in_end and end_index == self.current)
):

selector = self.theme.Checkbox.selection_icon
color = self.theme.Checkbox.selection_color

if choice in self.locked:
color = self.theme.Checkbox.locked_option_color

if choice == GLOBAL_OTHER_CHOICE:
symbol = "+"

yield choice, selector + " " + symbol, color

def process_input(self, pressed):
question = self.question
is_current_choice_locked = question.choices[self.current] in self.locked
if pressed == key.UP:
if question.carousel and self.current == 0:
self.current = len(question.choices) - 1
Expand All @@ -87,12 +93,14 @@ def process_input(self, pressed):
if self.question.choices[self.current] == GLOBAL_OTHER_CHOICE:
self.other_input()
elif self.current in self.selection:
self.selection.remove(self.current)
if not is_current_choice_locked:
self.selection.remove(self.current)
else:
self.selection.append(self.current)
elif pressed == key.LEFT:
if self.current in self.selection:
self.selection.remove(self.current)
if not is_current_choice_locked:
self.selection.remove(self.current)
elif pressed == key.RIGHT:
if self.current not in self.selection:
self.selection.append(self.current)
Expand Down
4 changes: 3 additions & 1 deletion src/inquirer/themes.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ def __init__(self):
self.Editor = collections.namedtuple("editor", "opening_prompt")
self.Checkbox = collections.namedtuple(
"common",
"selection_color selection_icon selected_color unselected_color selected_icon unselected_icon",
"selection_color selection_icon selected_color unselected_color "
"selected_icon unselected_icon locked_option_color",
)
self.List = collections.namedtuple("List", "selection_color selection_cursor unselected_color")

Expand All @@ -93,6 +94,7 @@ def __init__(self):
self.Checkbox.selected_color = term.yellow + term.bold
self.Checkbox.unselected_color = term.normal
self.Checkbox.unselected_icon = "[ ]"
self.Checkbox.locked_option_color = term.gray50
self.List.selection_color = term.blue
self.List.selection_cursor = ">"
self.List.unselected_color = term.normal
Expand Down
28 changes: 28 additions & 0 deletions tests/acceptance/test_checkbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,31 @@ def setUp(self):
def test_default_selection(self):
self.sut.send(key.ENTER)
self.sut.expect("{'interests': ", timeout=1)


@unittest.skipUnless(sys.platform.startswith("lin"), "Linux only")
class CheckLockedTest(unittest.TestCase):
def setUp(self):
self.sut = pexpect.spawn("python examples/checkbox_locked.py")
self.sut.expect("DevOps.*", timeout=1)

def test_default_selection(self):
self.sut.send(key.ENTER)
self.sut.expect(r"{'courses': \['Programming fundamentals'\]}", timeout=1)

def test_locked_option_space(self):
self.sut.send(key.SPACE)
self.sut.send(key.ENTER)
self.sut.expect(r"{'courses': \['Programming fundamentals'\]}", timeout=1)

def test_locked_option_left_key(self):
self.sut.send(key.LEFT)
self.sut.send(key.ENTER)
self.sut.expect(r"{'courses': \['Programming fundamentals'\]}", timeout=1)

def test_locked_with_another_option(self):
self.sut.send(key.DOWN)
self.sut.send(key.DOWN)
self.sut.send(key.SPACE)
self.sut.send(key.ENTER)
self.sut.expect(r"{'courses': \['Programming fundamentals', 'Data science'\]}", timeout=1)
70 changes: 70 additions & 0 deletions tests/integration/console_render/test_checkbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,73 @@ def test_double_invert_all(self):
result = sut.render(question)

assert result == []

def test_unselect_locked_space(self):
stdin_array = [key.SPACE, key.ENTER]
stdin = helper.event_factory(*stdin_array)
message = "Foo message"
variable = "Bar variable"
choices = ["foo", "bar", "bazz"]

question = questions.Checkbox(variable, message, choices=choices, locked=["foo"])

sut = ConsoleRender(event_generator=stdin)
result = sut.render(question)

assert result == ["foo"]

def test_unselect_locked_left(self):
stdin_array = [key.LEFT, key.ENTER]
stdin = helper.event_factory(*stdin_array)
message = "Foo message"
variable = "Bar variable"
choices = ["foo", "bar", "bazz"]

question = questions.Checkbox(variable, message, choices=choices, locked=["foo"])

sut = ConsoleRender(event_generator=stdin)
result = sut.render(question)

assert result == ["foo"]

def test_two_locked_options(self):
stdin_array = [key.ENTER]
stdin = helper.event_factory(*stdin_array)
message = "Foo message"
variable = "Bar variable"
choices = ["foo", "bar", "bazz"]

question = questions.Checkbox(variable, message, choices=choices, locked=["foo", "bazz"])

sut = ConsoleRender(event_generator=stdin)
result = sut.render(question)

assert result == ["foo", "bazz"]

def test_locked_with_typo(self):
stdin_array = [key.ENTER]
stdin = helper.event_factory(*stdin_array)
message = "Foo message"
variable = "Bar variable"
choices = ["foo", "bar", "bazz"]

question = questions.Checkbox(variable, message, choices=choices, locked=["fooo"])

sut = ConsoleRender(event_generator=stdin)
result = sut.render(question)

assert result == []

def test_locked_with_default(self):
stdin_array = [key.DOWN, key.SPACE, key.ENTER]
stdin = helper.event_factory(*stdin_array)
message = "Foo message"
variable = "Bar variable"
choices = ["foo", "bar", "bazz"]

question = questions.Checkbox(variable, message, choices=choices, locked=["bar"], default=["bar"])

sut = ConsoleRender(event_generator=stdin)
result = sut.render(question)

assert result == ["bar"]

0 comments on commit 0af3ccb

Please sign in to comment.