5151 ForceDecode ,
5252 NoDecode ,
5353 PydanticModel ,
54+ _CliDualFlag ,
5455 _CliExplicitFlag ,
5556 _CliImplicitFlag ,
5657 _CliPositionalArg ,
5758 _CliSubCommand ,
59+ _CliToggleFlag ,
5860 _CliUnknownArgs ,
5961)
6062from ..utils import (
@@ -239,6 +241,8 @@ def is_no_decode(self) -> bool:
239241_CliBoolFlag = TypeVar ('_CliBoolFlag' , bound = bool )
240242CliImplicitFlag = Annotated [_CliBoolFlag , _CliImplicitFlag ]
241243CliExplicitFlag = Annotated [_CliBoolFlag , _CliExplicitFlag ]
244+ CliToggleFlag = Annotated [_CliBoolFlag , _CliToggleFlag ]
245+ CliDualFlag = Annotated [_CliBoolFlag , _CliDualFlag ]
242246CLI_SUPPRESS = SUPPRESS
243247CliSuppress = Annotated [T , CLI_SUPPRESS ]
244248CliUnknownArgs = Annotated [list [str ], Field (default = []), _CliUnknownArgs , NoDecode ]
@@ -270,8 +274,12 @@ class CliSettingsSource(EnvSettingsSource, Generic[T]):
270274 Defaults to `True`.
271275 cli_prefix: Prefix for command line arguments added under the root parser. Defaults to "".
272276 cli_flag_prefix_char: The flag prefix character to use for CLI optional arguments. Defaults to '-'.
273- cli_implicit_flags: Whether `bool` fields should be implicitly converted into CLI boolean flags.
274- (e.g. --flag, --no-flag). Defaults to `False`.
277+ cli_implicit_flags: Controls how `bool` fields are exposed as CLI flags.
278+
279+ - False (default): no implicit flags are generated; booleans must be set explicitly (e.g. --flag=true).
280+ - True / 'dual': optional boolean fields generate both positive and negative forms (--flag and --no-flag).
281+ - 'toggle': required boolean fields remain in 'dual' mode, while optional boolean fields generate a single
282+ flag aligned with the default value (if default=False, expose --flag; if default=True, expose --no-flag).
275283 cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`.
276284 cli_kebab_case: CLI args use kebab case. Defaults to `False`.
277285 cli_shortcuts: Mapping of target field name to alias names. Defaults to `None`.
@@ -303,7 +311,7 @@ def __init__(
303311 cli_exit_on_error : bool | None = None ,
304312 cli_prefix : str | None = None ,
305313 cli_flag_prefix_char : str | None = None ,
306- cli_implicit_flags : bool | None = None ,
314+ cli_implicit_flags : bool | Literal [ 'dual' , 'toggle' ] | None = None ,
307315 cli_ignore_unknown_args : bool | None = None ,
308316 cli_kebab_case : bool | Literal ['all' , 'no_enums' ] | None = None ,
309317 cli_shortcuts : Mapping [str , str | list [str ]] | None = None ,
@@ -721,6 +729,14 @@ def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str,
721729 cli_flag_name = 'CliImplicitFlag'
722730 elif _CliExplicitFlag in field_info .metadata :
723731 cli_flag_name = 'CliExplicitFlag'
732+ elif _CliToggleFlag in field_info .metadata :
733+ cli_flag_name = 'CliToggleFlag'
734+ if not isinstance (field_info .default , bool ):
735+ raise SettingsError (
736+ f'{ cli_flag_name } argument { model .__name__ } .{ field_name } must have a default bool value'
737+ )
738+ elif _CliDualFlag in field_info .metadata :
739+ cli_flag_name = 'CliDualFlag'
724740 else :
725741 return
726742
@@ -1003,7 +1019,9 @@ def _add_parser_args(
10031019 if isinstance (group , dict ):
10041020 group = self ._add_group (parser , ** group )
10051021 context = parser if group is None else group
1006- arg .args = [f'{ flag_prefix [: len (name )]} { name } ' for name in arg_names ]
1022+ if arg .kwargs .get ('action' ) == 'store_false' :
1023+ flag_prefix += 'no-'
1024+ arg .args = [f'{ flag_prefix [: 1 if len (name ) == 1 else None ]} { name } ' for name in arg_names ]
10071025 self ._add_argument (context , * arg .args , ** arg .kwargs )
10081026 added_args += list (arg_names )
10091027
@@ -1018,11 +1036,25 @@ def _convert_append_action(self, kwargs: dict[str, Any], field_info: FieldInfo,
10181036
10191037 def _convert_bool_flag (self , kwargs : dict [str , Any ], field_info : FieldInfo , model_default : Any ) -> None :
10201038 if kwargs ['metavar' ] == 'bool' :
1021- if (self .cli_implicit_flags or _CliImplicitFlag in field_info .metadata ) and (
1022- _CliExplicitFlag not in field_info .metadata
1023- ):
1024- del kwargs ['metavar' ]
1025- kwargs ['action' ] = BooleanOptionalAction
1039+ meta_bool_flags = [
1040+ meta for meta in field_info .metadata if issubclass (meta , _CliImplicitFlag | _CliExplicitFlag )
1041+ ]
1042+ if not meta_bool_flags and self .cli_implicit_flags :
1043+ meta_bool_flags = [_CliImplicitFlag ]
1044+ if meta_bool_flags :
1045+ bool_flag = meta_bool_flags .pop ()
1046+ if bool_flag is _CliImplicitFlag :
1047+ bool_flag = (
1048+ _CliToggleFlag
1049+ if self .cli_implicit_flags == 'toggle' and isinstance (field_info .default , bool )
1050+ else _CliDualFlag
1051+ )
1052+ if bool_flag is _CliDualFlag :
1053+ del kwargs ['metavar' ]
1054+ kwargs ['action' ] = BooleanOptionalAction
1055+ elif bool_flag is _CliToggleFlag :
1056+ del kwargs ['metavar' ]
1057+ kwargs ['action' ] = 'store_false' if field_info .default else 'store_true'
10261058
10271059 def _convert_positional_arg (
10281060 self , kwargs : dict [str , Any ], field_info : FieldInfo , preferred_alias : str , model_default : Any
@@ -1348,7 +1380,7 @@ def _serialized_args(self, model: PydanticModel, _is_submodel: bool = False) ->
13481380 optional_args .append (f'{ flag_chars } { arg_name } ' )
13491381
13501382 # If implicit bool flag, do not add a value
1351- if arg .kwargs .get ('action' ) != BooleanOptionalAction :
1383+ if arg .kwargs .get ('action' ) not in ( BooleanOptionalAction , 'store_true' , 'store_false' ) :
13521384 optional_args .append (value )
13531385
13541386 serialized_args : list [str ] = []
0 commit comments