1818from __future__ import annotations
1919
2020import argparse
21+ import dataclasses
2122import difflib
22- from typing import Any , Literal , NamedTuple , NoReturn , Optional , Sequence
23+ from typing import Any , Callable , Literal , NamedTuple , NoReturn , Optional , Sequence
2324
24- from craft_cli import EmitterMode , emit
25+ from craft_cli import EmitterMode , emit , utils
2526from craft_cli .errors import ArgumentParsingError , ProvideHelpException
2627from craft_cli .helptexts import HelpBuilder , OutputFormat
2728
@@ -43,7 +44,8 @@ class CommandGroup(NamedTuple):
4344 """Whether the commands in this group are already in the correct order (defaults to False)."""
4445
4546
46- class GlobalArgument (NamedTuple ):
47+ @dataclasses .dataclass
48+ class GlobalArgument :
4749 """Definition of a global argument to be handled by the Dispatcher."""
4850
4951 name : str
@@ -64,6 +66,27 @@ class GlobalArgument(NamedTuple):
6466 help_message : str
6567 """the one-line text that describes the argument, for building the help texts."""
6668
69+ choices : Sequence [str ] | None = dataclasses .field (default = None )
70+ """Valid choices for this option."""
71+
72+ validator : Callable [[str ], Any ] | None = dataclasses .field (default = None )
73+ """A validator callable that converts the option input to the correct value.
74+
75+ The validator is called when parsing the argument. If it raises an exception, the
76+ exception message will be used as part of the usage output. Otherwise, the return
77+ value will be used as the content of this option.
78+ """
79+
80+ case_sensitive : bool = True
81+ """Whether the choices are case sensitive. Only used if choices are set."""
82+
83+ def __post_init__ (self ) -> None :
84+ if self .type == "flag" :
85+ if self .choices is not None or self .validator is not None :
86+ raise TypeError ("A flag argument cannot have choices or a validator." )
87+ elif self .choices and not self .case_sensitive :
88+ self .choices = [choice .lower () for choice in self .choices ]
89+
6790
6891_DEFAULT_GLOBAL_ARGS = [
6992 GlobalArgument (
@@ -93,6 +116,9 @@ class GlobalArgument(NamedTuple):
93116 None ,
94117 "--verbosity" ,
95118 "Set the verbosity level to 'quiet', 'brief', 'verbose', 'debug' or 'trace'" ,
119+ choices = [mode .name .lower () for mode in EmitterMode ],
120+ validator = lambda mode : EmitterMode [mode .upper ()],
121+ case_sensitive = False ,
96122 ),
97123]
98124
@@ -397,20 +423,32 @@ def _parse_options( # noqa: PLR0912 (too many branches)
397423 arg = arg_per_option [sysarg ]
398424 if arg .type == "flag" :
399425 global_args [arg .name ] = True
400- else :
401- try :
402- global_args [arg .name ] = next (sysargs_it )
403- except StopIteration :
404- msg = f"The { arg .name !r} option expects one argument."
405- raise self ._build_usage_exc (msg ) # noqa: TRY200 (use 'raise from')
426+ continue
427+ option = sysarg
428+ try :
429+ value = next (sysargs_it )
430+ except StopIteration :
431+ msg = f"The { arg .name !r} option expects one argument."
432+ raise self ._build_usage_exc (msg ) # noqa: TRY200 (use 'raise from')
406433 elif sysarg .startswith (tuple (options_with_equal )):
407434 option , value = sysarg .split ("=" , 1 )
408- arg = arg_per_option [option ]
409- if not value :
410- raise self ._build_usage_exc (f"The { arg .name !r} option expects one argument." )
411- global_args [arg .name ] = value
412435 else :
413436 filtered_sysargs .append (sysarg )
437+ continue
438+ arg = arg_per_option [option ]
439+ if not value :
440+ raise self ._build_usage_exc (f"The { arg .name !r} option expects one argument." )
441+ if arg .choices is not None :
442+ if not arg .case_sensitive :
443+ value = value .lower ()
444+ if value not in arg .choices :
445+ choices = utils .humanise_list ([f"'{ choice } '" for choice in arg .choices ])
446+ raise self ._build_usage_exc (
447+ f"Bad { arg .name } { value !r} ; valid values are { choices } ."
448+ )
449+
450+ validator = arg .validator or str
451+ global_args [arg .name ] = validator (value )
414452 return global_args , filtered_sysargs
415453
416454 def pre_parse_args (self , sysargs : list [str ]) -> dict [str , Any ]:
@@ -436,14 +474,7 @@ def pre_parse_args(self, sysargs: list[str]) -> dict[str, Any]:
436474 elif global_args ["verbose" ]:
437475 emit .set_mode (EmitterMode .VERBOSE )
438476 elif global_args ["verbosity" ]:
439- try :
440- verbosity_level = EmitterMode [global_args ["verbosity" ].upper ()]
441- except KeyError :
442- raise self ._build_usage_exc ( # noqa: TRY200 (use 'raise from')
443- "Bad verbosity level; valid values are "
444- "'quiet', 'brief', 'verbose', 'debug' and 'trace'."
445- )
446- emit .set_mode (verbosity_level )
477+ emit .set_mode (global_args ["verbosity" ])
447478 emit .trace (f"Raw pre-parsed sysargs: args={ global_args } filtered={ filtered_sysargs } " )
448479
449480 # handle requested help through -h/--help options
0 commit comments