Skip to content

Commit 4750b36

Browse files
committed
Add LazyChoice class for choices where obtaining list of options is expensive (and unnecessary in some cases).
1 parent a322f73 commit 4750b36

File tree

4 files changed

+94
-0
lines changed

4 files changed

+94
-0
lines changed

consolekit/options.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"no_pager_option",
8080
"PromptOption",
8181
"ChoiceOption",
82+
"LazyChoice",
8283
"MultiValueOption",
8384
"flag_option",
8485
"auto_default_option",
@@ -590,3 +591,40 @@ def __init__(self, *args, description: Optional[str] = None, **kwargs):
590591
super().__init__(*args, **kwargs)
591592

592593
self.description: Optional[str] = description
594+
595+
596+
# class LazyChoice(click.Choice[str]):
597+
class LazyChoice(click.Choice):
598+
"""
599+
Modified choice type that lazily loads the data.
600+
601+
Useful for expensive operations that need not happen if the option is not provided or the help text is not being displayed.
602+
603+
:param getter: Function that returns the actual choices.
604+
:param case_sensitive: Set to :py:obj:`False` to make choices case insensitive.
605+
606+
.. versionadded:: 1.11.0
607+
"""
608+
609+
_choices: Optional[Sequence[str]] = None
610+
611+
def __init__(
612+
self,
613+
getter: Callable[[], Iterable[str]],
614+
case_sensitive: bool = True,
615+
) -> None:
616+
self._getter = getter
617+
618+
self.case_sensitive = case_sensitive
619+
620+
@property
621+
def choices(self) -> Sequence[str]: # type: ignore[override]
622+
"""
623+
The choices, obtained from the getter function and cached.
624+
"""
625+
626+
if self._choices is None:
627+
choices = tuple(self._getter())
628+
self._choices = choices
629+
630+
return self._choices

tests/test_options.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from consolekit.options import (
1414
ChoiceOption,
1515
DescribedArgument,
16+
LazyChoice,
1617
MultiValueOption,
1718
PromptOption,
1819
auto_default_argument,
@@ -342,6 +343,41 @@ def main(station: str) -> None:
342343
assert result.stdout.rstrip() == "Tuning to station: Radio 1"
343344

344345

346+
@pytest.mark.parametrize(
347+
"click_version",
348+
[pytest.param('7', marks=click_8_only), pytest.param('8', marks=not_click_8)],
349+
)
350+
def test_lazy_choice(
351+
advanced_file_regression: AdvancedFileRegressionFixture,
352+
click_version: str,
353+
cli_runner: CliRunner,
354+
):
355+
356+
def expensive_operation():
357+
print("Performing expensive operation to get choices.")
358+
return ["Radio 1", "Radio 2", "Radio 3"]
359+
360+
@click.option(
361+
"-s",
362+
"--station",
363+
help="The station to play.",
364+
type=LazyChoice(expensive_operation, case_sensitive=False),
365+
cls=ChoiceOption,
366+
prompt="Select a station",
367+
)
368+
@click_command()
369+
def main(station: str) -> None:
370+
print(f"Tuning to station: {station}")
371+
372+
result = cli_runner.invoke(main, input="5\n0\n2\n")
373+
assert result.exit_code == 0
374+
advanced_file_regression.check(result.stdout.rstrip())
375+
376+
result = cli_runner.invoke(main, args=["--station", "Radio 1"])
377+
assert result.exit_code == 0
378+
assert result.stdout.rstrip() == "Tuning to station: Radio 1"
379+
380+
345381
not_click_8_or_above_82 = pytest.mark.skipif(
346382
_click_major != 8 or (_click_major == 8 and _click_version[1] >= 2), reason="Output differs on click 8"
347383
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Performing expensive operation to get choices.
2+
[1] Radio 1
3+
[2] Radio 2
4+
[3] Radio 3
5+
Select a station: 5
6+
Error: 5 is not in the valid range of 1 to 3.
7+
Select a station: 0
8+
Error: 0 is not in the valid range of 1 to 3.
9+
Select a station: 2
10+
Tuning to station: Radio 2
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Performing expensive operation to get choices.
2+
[1] Radio 1
3+
[2] Radio 2
4+
[3] Radio 3
5+
Select a station: 5
6+
Error: 5 is not in the range 1<=x<=3.
7+
Select a station: 0
8+
Error: 0 is not in the range 1<=x<=3.
9+
Select a station: 2
10+
Tuning to station: Radio 2

0 commit comments

Comments
 (0)