Skip to content

Commit

Permalink
Add select_choices
Browse files Browse the repository at this point in the history
  • Loading branch information
Andreas P. Cuny authored and dmerejkowsky committed May 14, 2022
1 parent f12b7f4 commit 63d7865
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 1 deletion.
61 changes: 61 additions & 0 deletions cli_ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import inspect
import io
import os
import re
import sys
import time
import traceback
from typing import IO, Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
from operator import itemgetter

import colorama
import tabulate
Expand Down Expand Up @@ -537,6 +539,65 @@ def ask_choice(

return res

def select_choices(
*prompt: Token,
choices: List[Any],
func_desc: Optional[FuncDesc] = None,
sort: Optional[bool] = True,
) -> Any:
"""Ask the user to select one or multiple from a list of choices. Note: delimit your choices by space, comma or semi colon.
Will loop until:
* the user enters a valid index
* or leaves the prompt empty
In the last case, `None` will be returned
:param prompt: a list of tokens suitable for :func:`info`
:param choices: a list of arbitrary elements
:param func_desc: a callable. It will be used to display and
sort the list of choices (unless ``sort`` is False)
Defaults to the identity function.
:param sort: whether to sort the list of choices.
:return: the selected choice(s).
"""
if func_desc is None:
func_desc = lambda x: str(x)
tokens = get_ask_tokens(prompt)
info(*tokens)
if sort:
choices.sort(key=func_desc)
for i, choice in enumerate(choices, start=1):
choice_desc = func_desc(choice)
info(" ", blue, "%i" % i, reset, choice_desc)
keep_asking = True
res = None
while keep_asking:
answer = read_input()
if not answer:
return None
try:
import re

index = [int(item) for item in re.split(r'; |, |\s |;|,|\s',answer)]
index = [x-1 for x in index] # convert to true index
# index = int(answer)
except ValueError:
info("Please enter a valid number")
continue

try:
res = itemgetter(*index)(choices)
# res = choices[index - 1]
keep_asking = False
except:
info("Please enter valid selection number(s)")
continue

return res


def ask_yes_no(*question: Token, default: bool = False) -> bool:
"""Ask the user to answer by yes or no"""
Expand Down
37 changes: 37 additions & 0 deletions cli_ui/tests/test_cli_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ def func_desc(fruit: Fruit) -> str:
fruits = [Fruit("apple", 42), Fruit("banana", 10), Fruit("orange", 12)]
with mock.patch("builtins.input") as m:
m.side_effect = ["nan", "5", "2"]

actual = cli_ui.ask_choice(
"Select a fruit", choices=fruits, func_desc=operator.attrgetter("name")
)
Expand All @@ -321,6 +322,42 @@ def test_ask_choice_ctrl_c() -> None:
with mock.patch("builtins.input") as m:
m.side_effect = KeyboardInterrupt
cli_ui.ask_choice("Select a animal", choices=["cat", "dog", "cow"])


def test_select_choices() -> None:
class Fruit:
def __init__(self, name: str, price: int):
self.name = name
self.price = price

def func_desc(fruit: Fruit) -> str:
return fruit.name

fruits = [Fruit("apple", 42), Fruit("banana", 10), Fruit("orange", 12)]
with mock.patch("builtins.input") as m:
m.side_effect = ["nan", "5", "1, 2"]
actual = cli_ui.select_choices(
"Select a fruit", choices=fruits, func_desc=operator.attrgetter("name")
)
assert actual[0].name == "apple"
assert actual[0].price == 42
assert actual[1].name == "banana"
assert actual[1].price == 10
assert m.call_count == 3


def test_select_choices_empty_input() -> None:
with mock.patch("builtins.input") as m:
m.side_effect = [""]
res = cli_ui.select_choices("Select a animal", choices=["cat", "dog", "cow"])
assert res is None


def test_select_choices_ctrl_c() -> None:
with pytest.raises(KeyboardInterrupt):
with mock.patch("builtins.input") as m:
m.side_effect = KeyboardInterrupt
cli_ui.select_choices("Select a animal", choices=["cat", "dog", "cow"])


def test_quiet(message_recorder: MessageRecorder) -> None:
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
----------

v0.16.2 (2022-05-10)
++++++++++++++++++++

* Add multi selection method `cli_ui.select_choices`

v0.16.1 (2022-03-12)
++++++++++++++++++++

Expand Down
15 changes: 15 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,21 @@ Asking for user input
.. versionchanged:: 0.7

The :py:exc:`KeyboardInterrupt` exception is no longer caught by this function.

.. autofunction:: select_choices


::

>>> choices = ["apple", "banana", "orange"]
>>> fruit = cli_ui.ask_choice("Select several fruits", choices=choices)
:: Select a fruit
1 apple
2 banana
3 orange
<1,2>
>>> fruit
('apple, 'banana')


.. autofunction:: ask_yes_no
Expand Down
2 changes: 1 addition & 1 deletion tbump.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
github_url = "https://github.com/dmerejkowsky/python-cli-ui"

[version]
current = "0.16.1"
current = "0.16.2"

# Example of a semver regexp.
# Make sure this matches current_version before
Expand Down

0 comments on commit 63d7865

Please sign in to comment.