Skip to content

Commit e2801bf

Browse files
authored
Merge pull request #256 from WilliamVenner/feat/other-choice
Add "Other..." option for checkbox & list questions
2 parents 502c1f3 + ae4daf7 commit e2801bf

File tree

9 files changed

+190
-7
lines changed

9 files changed

+190
-7
lines changed

examples/checkbox_other.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import os
2+
import sys
3+
from pprint import pprint
4+
5+
6+
sys.path.append(os.path.realpath("."))
7+
import inquirer # noqa
8+
9+
10+
questions = [
11+
inquirer.Checkbox(
12+
"interests",
13+
message="What are you interested in?",
14+
choices=["Computers", "Books", "Science", "Nature", "Fantasy", "History"],
15+
default=["Computers", "Books"],
16+
other=True,
17+
carousel=True,
18+
),
19+
]
20+
21+
answers = inquirer.prompt(questions)
22+
23+
pprint(answers)

examples/list_other.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import os
2+
import sys
3+
from pprint import pprint
4+
5+
6+
sys.path.append(os.path.realpath("."))
7+
import inquirer # noqa
8+
9+
10+
questions = [
11+
inquirer.List(
12+
"size", message="What size do you need?", choices=["Jumbo", "Large", "Standard"], carousel=True, other=True
13+
),
14+
]
15+
16+
answers = inquirer.prompt(questions)
17+
18+
pprint(answers)

src/inquirer/questions.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88

99
import inquirer.errors as errors
10+
from inquirer.render.console._other import GLOBAL_OTHER_CHOICE
1011

1112

1213
class TaggedValue:
@@ -32,7 +33,17 @@ def __ne__(self, other):
3233
class Question:
3334
kind = "base question"
3435

35-
def __init__(self, name, message="", choices=None, default=None, ignore=False, validate=True, show_default=False):
36+
def __init__(
37+
self,
38+
name,
39+
message="",
40+
choices=None,
41+
default=None,
42+
ignore=False,
43+
validate=True,
44+
show_default=False,
45+
other=False,
46+
):
3647
self.name = name
3748
self._message = message
3849
self._choices = choices or []
@@ -41,6 +52,22 @@ def __init__(self, name, message="", choices=None, default=None, ignore=False, v
4152
self._validate = validate
4253
self.answers = {}
4354
self.show_default = show_default
55+
self._other = other
56+
57+
if self._other:
58+
self._choices.append(GLOBAL_OTHER_CHOICE)
59+
60+
def add_choice(self, choice):
61+
try:
62+
index = self._choices.index(choice)
63+
return index
64+
except ValueError:
65+
if self._other:
66+
self._choices.insert(-1, choice)
67+
return len(self._choices) - 2
68+
69+
self._choices.append(choice)
70+
return len(self._choices) - 1
4471

4572
@property
4673
def ignore(self):
@@ -112,18 +139,22 @@ def __init__(self, name, default=False, **kwargs):
112139
class List(Question):
113140
kind = "list"
114141

115-
def __init__(self, name, message="", choices=None, default=None, ignore=False, validate=True, carousel=False):
142+
def __init__(
143+
self, name, message="", choices=None, default=None, ignore=False, validate=True, carousel=False, other=False
144+
):
116145

117-
super().__init__(name, message, choices, default, ignore, validate)
146+
super().__init__(name, message, choices, default, ignore, validate, other=other)
118147
self.carousel = carousel
119148

120149

121150
class Checkbox(Question):
122151
kind = "checkbox"
123152

124-
def __init__(self, name, message="", choices=None, default=None, ignore=False, validate=True, carousel=False):
153+
def __init__(
154+
self, name, message="", choices=None, default=None, ignore=False, validate=True, carousel=False, other=False
155+
):
125156

126-
super().__init__(name, message, choices, default, ignore, validate)
157+
super().__init__(name, message, choices, default, ignore, validate, other=other)
127158
self.carousel = carousel
128159

129160

src/inquirer/render/console/_checkbox.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from readchar import key
22

33
from inquirer import errors
4+
from inquirer.render.console._other import GLOBAL_OTHER_CHOICE
45
from inquirer.render.console.base import MAX_OPTIONS_DISPLAYED_AT_ONCE
56
from inquirer.render.console.base import BaseConsoleRender
67
from inquirer.render.console.base import half_options
@@ -62,6 +63,10 @@ def get_options(self):
6263

6364
selector = self.theme.Checkbox.selection_icon
6465
color = self.theme.Checkbox.selection_color
66+
67+
if choice == GLOBAL_OTHER_CHOICE:
68+
symbol = "+"
69+
6570
yield choice, selector + " " + symbol, color
6671

6772
def process_input(self, pressed):
@@ -79,7 +84,9 @@ def process_input(self, pressed):
7984
self.current = min(len(self.question.choices) - 1, self.current + 1)
8085
return
8186
elif pressed == key.SPACE:
82-
if self.current in self.selection:
87+
if self.question.choices[self.current] == GLOBAL_OTHER_CHOICE:
88+
self.other_input()
89+
elif self.current in self.selection:
8390
self.selection.remove(self.current)
8491
else:
8592
self.selection.append(self.current)
@@ -97,3 +104,16 @@ def process_input(self, pressed):
97104
raise errors.EndOfInput(result)
98105
elif pressed == key.CTRL_C:
99106
raise KeyboardInterrupt()
107+
108+
def other_input(self):
109+
other = super().other_input()
110+
111+
# Clear the print that inquirer.text made
112+
print(self.terminal.move_up + self.terminal.clear_eol, end="")
113+
114+
if not other:
115+
return
116+
117+
index = self.question.add_choice(other)
118+
if index not in self.selection:
119+
self.selection.append(index)

src/inquirer/render/console/_list.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from readchar import key
22

33
from inquirer import errors
4+
from inquirer.render.console._other import GLOBAL_OTHER_CHOICE
45
from inquirer.render.console.base import MAX_OPTIONS_DISPLAYED_AT_ONCE
56
from inquirer.render.console.base import BaseConsoleRender
67
from inquirer.render.console.base import half_options
@@ -47,7 +48,7 @@ def get_options(self):
4748
):
4849

4950
color = self.theme.List.selection_color
50-
symbol = self.theme.List.selection_cursor
51+
symbol = "+" if choice == GLOBAL_OTHER_CHOICE else self.theme.List.selection_cursor
5152
else:
5253
color = self.theme.List.unselected_color
5354
symbol = " "
@@ -69,6 +70,14 @@ def process_input(self, pressed):
6970
return
7071
if pressed == key.ENTER:
7172
value = self.question.choices[self.current]
73+
74+
if value == GLOBAL_OTHER_CHOICE:
75+
value = self.other_input()
76+
if not value:
77+
# Clear the print inquirer.text made, since the user didn't enter anything
78+
print(self.terminal.move_up + self.terminal.clear_eol, end="")
79+
return
80+
7281
raise errors.EndOfInput(getattr(value, "value", value))
7382

7483
if pressed == key.CTRL_C:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class OtherChoice:
2+
def __str__(self):
3+
return "Other"
4+
5+
6+
GLOBAL_OTHER_CHOICE = OtherChoice()

src/inquirer/render/console/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from blessed import Terminal
22

3+
import inquirer
4+
35

46
# Should be odd number as there is always one question selected
57
MAX_OPTIONS_DISPLAYED_AT_ONCE = 13
@@ -17,6 +19,10 @@ def __init__(self, question, theme=None, terminal=None, show_default=False, *arg
1719
self.theme = theme
1820
self.show_default = show_default
1921

22+
def other_input(self):
23+
other = inquirer.text(self.question.message)
24+
return other
25+
2026
def get_header(self):
2127
return self.question.message
2228

tests/acceptance/test_checkbox.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,41 @@ def test_out_of_bounds_down(self):
8282
self.sut.expect(r"{'interests': \['Books'\]}.*", timeout=1) # noqa
8383

8484

85+
@unittest.skipUnless(sys.platform.startswith("lin"), "Linux only")
86+
class CheckOtherTest(unittest.TestCase):
87+
def setUp(self):
88+
self.sut = pexpect.spawn("python examples/checkbox_other.py")
89+
self.sut.expect("Computers.*", timeout=1)
90+
91+
def test_other_input(self):
92+
self.sut.send(key.UP)
93+
self.sut.expect(r"\+ Other.*", timeout=1)
94+
self.sut.send(key.SPACE)
95+
self.sut.expect(r": ", timeout=1)
96+
self.sut.send("Hello world")
97+
self.sut.expect(r"Hello world.*", timeout=1)
98+
self.sut.send(key.ENTER)
99+
self.sut.expect(r"> X Hello world[\s\S]*\+ Other.*", timeout=1)
100+
self.sut.send(key.ENTER)
101+
self.sut.expect(r"{'interests': \['Computers', 'Books', 'Hello world'\]}", timeout=1) # noqa
102+
103+
def test_other_blank_input(self):
104+
self.sut.send(key.UP)
105+
self.sut.expect(r"\+ Other.*", timeout=1)
106+
self.sut.send(key.SPACE)
107+
self.sut.expect(r": ", timeout=1)
108+
self.sut.send(key.ENTER) # blank input
109+
self.sut.expect(r"> \+ Other.*", timeout=1)
110+
self.sut.send(key.ENTER)
111+
self.sut.expect(r"{'interests': \['Computers', 'Books'\]}", timeout=1) # noqa
112+
113+
def test_other_select_choice(self):
114+
self.sut.send(key.SPACE)
115+
self.sut.expect(r"[^X] Computers.*", timeout=1)
116+
self.sut.send(key.ENTER)
117+
self.sut.expect(r"{'interests': \['Books'\]}", timeout=1) # noqa
118+
119+
85120
@unittest.skipUnless(sys.platform.startswith("lin"), "Linux only")
86121
class CheckWithTaggedValuesTest(unittest.TestCase):
87122
def setUp(self):

tests/acceptance/test_list.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,41 @@ def test_out_of_bounds_down(self):
5757
self.sut.expect("{'size': 'Jumbo'}.*", timeout=1)
5858

5959

60+
@unittest.skipUnless(sys.platform.startswith("lin"), "Linux only")
61+
class CheckOtherTest(unittest.TestCase):
62+
def setUp(self):
63+
self.sut = pexpect.spawn("python examples/list_other.py")
64+
self.sut.expect("Standard.*", timeout=1)
65+
66+
def test_other_input(self):
67+
self.sut.send(key.UP)
68+
self.sut.expect(r"\+ Other.*", timeout=1)
69+
self.sut.send(key.ENTER)
70+
self.sut.expect(r": ", timeout=1)
71+
self.sut.send("Hello world")
72+
self.sut.expect(r"Hello world", timeout=1)
73+
self.sut.send(key.ENTER)
74+
self.sut.expect("{'size': 'Hello world'}.*", timeout=1)
75+
76+
def test_other_blank_input(self):
77+
self.sut.send(key.UP)
78+
self.sut.expect(r"\+ Other.*", timeout=1)
79+
self.sut.send(key.ENTER)
80+
self.sut.expect(r": ", timeout=1)
81+
self.sut.send(key.ENTER) # blank input
82+
self.sut.expect(r"\+ Other.*", timeout=1)
83+
self.sut.send(key.ENTER)
84+
self.sut.expect(r": ", timeout=1)
85+
self.sut.send("Hello world")
86+
self.sut.expect(r"Hello world", timeout=1)
87+
self.sut.send(key.ENTER)
88+
self.sut.expect("{'size': 'Hello world'}.*", timeout=1)
89+
90+
def test_other_select_choice(self):
91+
self.sut.send(key.ENTER)
92+
self.sut.expect("{'size': 'Jumbo'}.*", timeout=1)
93+
94+
6095
@unittest.skipUnless(sys.platform.startswith("lin"), "Linux only")
6196
class ListTaggedTest(unittest.TestCase):
6297
def setUp(self):

0 commit comments

Comments
 (0)