Skip to content

Commit e35fde1

Browse files
committed
fix: regain feature-parity with trogon/main
Allow `ArgumentSchema` to contain one or more types. Define `ChoiceSchema` to allow choice-types in a sequence of types; used in place of `click.Choice`.
1 parent 1ef2c21 commit e35fde1

File tree

5 files changed

+61
-29
lines changed

5 files changed

+61
-29
lines changed

examples/demo_click.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def cli(ctx, verbose, hidden_arg):
2828
"--extra",
2929
"-e",
3030
nargs=2,
31-
type=(str, int),
31+
type=(str, click.Choice(["1", "2", "3"])),
3232
multiple=True,
3333
default=[("one", 1), ("two", 2)],
3434
help="Add extra data as key-value pairs (repeatable)",

examples/demo_click_nogroup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"--extra",
1515
"-e",
1616
nargs=2,
17-
type=(str, int),
17+
type=(str, click.Choice(["1", "2", "3"])),
1818
multiple=True,
1919
default=[("one", 1), ("two", 2)],
2020
help="Add extra data as key-value pairs (repeatable)",

trogon/click.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from __future__ import annotations
2-
from typing import Sequence
2+
from typing import Type
33

44

55
from trogon import Trogon
@@ -19,6 +19,7 @@
1919
CommandName,
2020
CommandSchema,
2121
OptionSchema,
22+
ChoiceSchema,
2223
)
2324
from typing import Type, Any
2425
from uuid import UUID
@@ -37,6 +38,13 @@
3738
click.Path: Path,
3839
}
3940

41+
def _convert_click_to_py_type(click_type: Type[Any]) -> Type[Any]:
42+
if isinstance(click_type, click.Choice):
43+
return ChoiceSchema(choices=click_type.choices)
44+
45+
return CLICK_TO_PY_TYPES.get(
46+
click_type, CLICK_TO_PY_TYPES.get(type(click_type), str)
47+
)
4048

4149
def introspect_click_app(
4250
app: BaseCommand, cmd_ignorelist: list[str] | None = None
@@ -73,13 +81,12 @@ def process_command(
7381
subcommands={},
7482
parent=parent,
7583
)
84+
7685
for param in cmd_obj.params:
77-
param_type: Type[Any] = CLICK_TO_PY_TYPES.get(
78-
param.type, CLICK_TO_PY_TYPES.get(type(param.type), str)
79-
)
80-
param_choices: Sequence[str] | None = None
81-
if isinstance(param.type, click.Choice):
82-
param_choices = param.type.choices
86+
87+
click_types: list[Type[Any]] = param.type.types if isinstance(param.type, click.Tuple) else [param.type]
88+
89+
param_types: list[Type[Any]] = [_convert_click_to_py_type(x) for x in click_types]
8390

8491
if isinstance(param, (click.Option, click.core.Group)):
8592
if param.hidden:
@@ -89,14 +96,14 @@ def process_command(
8996

9097
option_data = OptionSchema(
9198
name=param.opts,
92-
type=param_type,
99+
type=param_types,
93100
is_flag=param.is_flag,
94101
counting=param.count,
95102
secondary_opts=param.secondary_opts,
96103
required=param.required,
97104
default=param.default,
98105
help=param.help,
99-
choices=param_choices,
106+
choices=None,
100107
multiple=param.multiple,
101108
nargs=param.nargs,
102109
sensitive=param.hide_input,
@@ -108,9 +115,9 @@ def process_command(
108115
elif isinstance(param, click.Argument):
109116
argument_data = ArgumentSchema(
110117
name=param.name,
111-
type=param_type,
118+
type=param_types,
112119
required=param.required,
113-
choices=param_choices,
120+
choices=None,
114121
multiple=param.multiple,
115122
default=param.default,
116123
nargs=param.nargs,

trogon/schemas.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,19 @@ def process_cli_option(value) -> "MultiValueParamData":
3131
return value
3232

3333

34+
@dataclass
35+
class ChoiceSchema:
36+
# this is used in place of click.Choice
37+
choices: Sequence[str]
38+
39+
def __post_init__(self):
40+
self.__name__ = 'choice'
41+
42+
3443
@dataclass
3544
class ArgumentSchema:
3645
name: str | list[str]
37-
type: Type[Any] | None = None
46+
type: Type[Any] | Sequence[Type[Any]] | None = None
3847
required: bool = False
3948
help: str | None = None
4049
key: str | tuple[str] = field(default_factory=generate_unique_id)
@@ -52,14 +61,20 @@ def __post_init__(self):
5261
self.default = MultiValueParamData.process_cli_option(self.default)
5362

5463
if not self.type:
55-
self.type = str
56-
57-
if self.multi_value:
58-
self.multiple = True
64+
self.type = [str]
65+
elif isinstance(self.type, Type):
66+
self.type = [self.type]
67+
elif len(self.type) == 1 and isinstance(self.type[0], ChoiceSchema):
68+
# if there is only one type is it is a 'ChoiceSchema':
69+
self.choices = self.type[0].choices
70+
self.type = [str]
5971

6072
if self.choices:
6173
self.choices = [str(x) for x in self.choices]
6274

75+
if self.multi_value:
76+
self.multiple = True
77+
6378

6479
@dataclass
6580
class OptionSchema(ArgumentSchema):

trogon/widgets/parameter_controls.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
)
2222

2323

24-
from trogon.schemas import ArgumentSchema, MultiValueParamData, OptionSchema
24+
from trogon.schemas import ArgumentSchema, MultiValueParamData, OptionSchema, ChoiceSchema
2525
from trogon.widgets.multiple_choice import MultipleChoice
2626

2727
ControlWidgetType: TypeVar = Union[Input, Checkbox, MultipleChoice, Select]
@@ -135,7 +135,9 @@ def compose(self) -> ComposeResult:
135135
# There's a special case where we have a Choice with multiple=True,
136136
# in this case, we can just render a single MultipleChoice widget
137137
# instead of multiple radio-sets.
138-
control_method = self.get_control_method(schema=schema)
138+
control_method = self.get_control_method(
139+
param_type=ChoiceSchema(choices=schema.choices)
140+
)
139141
multiple_choice_widget = control_method(
140142
default=default,
141143
label=label,
@@ -187,7 +189,7 @@ def compose(self) -> ComposeResult:
187189
# If it's a multiple, and it's a Choice parameter, then we display
188190
# our special case MultiChoice widget, and so there's no need for this
189191
# button.
190-
if multiple or nargs == -1 and not schema.choices:
192+
if (multiple or nargs == -1) and not schema.choices:
191193
with Horizontal(classes="add-another-button-container"):
192194
yield Button("+ value", variant="success", classes="add-another-button")
193195

@@ -209,12 +211,20 @@ def make_widget_group(self) -> Iterable[Widget]:
209211
)
210212

211213
# Get the types of the parameter. We can map these types on to widgets that will be rendered.
212-
parameter_types = [parameter_type] * schema.nargs if schema.nargs > 1 else [parameter_type]
214+
parameter_types = [
215+
parameter_type[i] if i < len(parameter_type) else parameter_type[-1]
216+
for i in range(schema.nargs if schema.nargs > 1 else 1)
217+
]
218+
# The above ensures that len(parameter_types) == nargs.
219+
# if there are more parameter_types than args, parameter_types is truncated.
220+
# if there are fewer parameter_types than args, the *last* parameter type is repeated as much as necessary.
213221

214222
# For each of the these parameters, render the corresponding widget for it.
215223
# At this point we don't care about filling in the default values.
216224
for _type in parameter_types:
217-
control_method = self.get_control_method(schema=schema)
225+
if schema.choices:
226+
_type = ChoiceSchema(choices=schema.choices)
227+
control_method = self.get_control_method(param_type=_type)
218228
control_widgets = control_method(
219229
default, label, multiple, schema, schema.key
220230
)
@@ -301,12 +311,12 @@ def list_to_tuples(
301311
return MultiValueParamData.process_cli_option(collected_values)
302312

303313
def get_control_method(
304-
self, schema: ArgumentSchema
314+
self, param_type: Type[Any],
305315
) -> Callable[[Any, Text, bool, OptionSchema | ArgumentSchema, str], Widget]:
306-
if schema.choices:
307-
return partial(self.make_choice_control, choices=schema.choices)
316+
if isinstance(param_type, ChoiceSchema):
317+
return partial(self.make_choice_control, choices=param_type.choices)
308318

309-
if schema.type is bool:
319+
if param_type is bool:
310320
return self.make_checkbox_control
311321

312322
return self.make_text_control
@@ -382,7 +392,7 @@ def make_choice_control(
382392
@staticmethod
383393
def _make_command_form_control_label(
384394
name: str | list[str],
385-
type: Type[Any],
395+
types: list[Type[Any]],
386396
is_option: bool,
387397
is_required: bool,
388398
multiple: bool,
@@ -391,7 +401,7 @@ def _make_command_form_control_label(
391401

392402
names = Text(" / ", style="dim").join([Text(n) for n in names])
393403
text = Text.from_markup(
394-
f"{names}[dim]{' multiple' if multiple else ''} <{type.__name__}>[/] {' [b red]*[/]required' if is_required else ''}"
404+
f"{names}[dim]{' multiple' if multiple else ''} <{', '.join(x.__name__ for x in types)}>[/] {' [b red]*[/]required' if is_required else ''}"
395405
)
396406

397407
return text

0 commit comments

Comments
 (0)