Feature request: Default Enum choices from Typer
[!NOTE] I am aware that Trogon is currently geared toward Click, but having seen a bit of past discussion on Typer I wanted to raise this as a rough edge stopping me from using Trogon full bore. Thanks for making a great tool, and if I can provide more clarity please let me know!
Whereas Click recommends using click.Choice(["one", "two", ...]), Typer recommends using something like the following:
from typing_extensions import Annotated
class SomeChoiceType(str, Enum):
ONE = "one"
TWO = "two"
some_app = typer.Typer()
@some_app.command
def some_command(some_option: Annotated[SomeChoiceType] = SomeChoiceType.ONE):
...
This all works great in Typer land, and Trogon will start up okay. But once one navigates to the some_command command in the Trogon TUI, it currently spits out an exception like the following:
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /path/to/python3.11/site-packages/textual/widget.py:3325 │
│ in _on_compose │
│ │
│ 3322 │ │
│ 3323 │ async def _on_compose(self) -> None: │
│ 3324 │ │ try: │
│ ❱ 3325 │ │ │ widgets = [*self._nodes, *compose(self)] │
│ 3326 │ │ except TypeError as error: │
│ 3327 │ │ │ raise TypeError( │
│ 3328 │ │ │ │ f"{self!r} compose() method returned an invalid result; {error}" │
│ │
│ /path/to/python3.11/site-packages/trogon/widgets/parameter_controls.py:163 in compose │
│ │
│ 160 │ │ │ │ │ │ for default_value, control_widget in zip( │
│ 161 │ │ │ │ │ │ │ default_value_tuple, widget_group │
│ 162 │ │ │ │ │ │ ): │
│ ❱ 163 │ │ │ │ │ │ │ self._apply_default_value(control_widget, default_value) │
│ 164 │ │ │ │ │ │ │ yield control_widget │
│ 165 │ │ │ │ │ │ │ # Keep track of the first control we render, for easy focus │
│ 166 │ │ │ │ │ │ │ if first_focus_control is None: │
│ │
│ /path/to/python3.11/site-packages/trogon/widgets/parameter_controls.py:247 |
| in _apply_default_value │
│ │
│ 244 │ │ │ control_widget.value = str(default_value) │
│ 245 │ │ │ control_widget.placeholder = f"{default_value} (default)" │
│ 246 │ │ elif isinstance(control_widget, Select): │
│ ❱ 247 │ │ │ control_widget.value = str(default_value) │
│ 248 │ │ │ control_widget.prompt = f"{default_value} (default)" │
│ 249 │ │
│ 250 │ @staticmethod │
│ │
│ /path/to/python3.11/site-packages/textual/widgets/_select.py:387 in _validate_value |
│ │
│ 384 │ │ │ # so we provide a helpful message to catch this mistake in case people didn' │
│ 385 │ │ │ # realise we use a special value to flag "no selection". │
│ 386 │ │ │ help_text = " Did you mean to use Select.clear()?" if value is None else "" │
│ ❱ 387 │ │ │ raise InvalidSelectValueError( │
│ 388 │ │ │ │ f"Illegal select value {value!r}." + help_text │
│ 389 │ │ │ ) │
│ 390 │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
InvalidSelectValueError: Illegal select value 'SomeChoiceType.ONE'.
This appears to occur specifically when one of the enum choices is used as a default. If the Enum is used to restrict a required argument, or an option where the default is e.g. None, Trogon works as expected.
I understand that one would need to inject a .value there somewhere to get at the actual string that the Enum is referencing, which I imagine is fully within Trogon's control to be doing.
I haven't looked at the parameter conversion code base, so I'm fully willing to hear that knowing about Enums and doing something special with them is against Trogon's design!
Here is a small reproduction if that is helpful.
A workaround for this is to override the Enum class's __str__ method to return the string representation of the enum value:
from typing_extensions import Annotated
class SomeChoiceType(str, Enum):
ONE = "one"
TWO = "two"
def __str__(self):
return str(self.value)
some_app = typer.Typer()
@some_app.command
def some_command(some_option: Annotated[SomeChoiceType] = SomeChoiceType.ONE):
...
This continues allowing Typer to help with finding type safety issues while allowing Trogon to construct a Textual Select appropriately.
I've been using the workaround above which feels pretty natural in practice now; I'm not sure there's a clear ask from the Trogon side of the fence on it, at this point. The only thing I can think of would be that Trogon could call .value if it detects that the object in question is an Enum, but that isn't guaranteed to be a string either.