From db4ee9d33a5e545af9fa5c4aee0cf86315a09333 Mon Sep 17 00:00:00 2001 From: Kyle Schwab Date: Tue, 25 Nov 2025 07:39:14 -0700 Subject: [PATCH 1/3] Initial commit. --- pydantic_settings/__init__.py | 4 +++ pydantic_settings/sources/__init__.py | 4 +++ .../sources/providers/__init__.py | 4 +++ pydantic_settings/sources/providers/cli.py | 34 ++++++++++++++++--- pydantic_settings/sources/types.py | 10 ++++++ 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/pydantic_settings/__init__.py b/pydantic_settings/__init__.py index 9df7a632..21124260 100644 --- a/pydantic_settings/__init__.py +++ b/pydantic_settings/__init__.py @@ -4,6 +4,7 @@ CLI_SUPPRESS, AWSSecretsManagerSettingsSource, AzureKeyVaultSettingsSource, + CliDualFlag, CliExplicitFlag, CliImplicitFlag, CliMutuallyExclusiveGroup, @@ -11,6 +12,7 @@ CliSettingsSource, CliSubCommand, CliSuppress, + CliToggleFlag, CliUnknownArgs, DotEnvSettingsSource, EnvSettingsSource, @@ -37,6 +39,8 @@ 'CliApp', 'CliExplicitFlag', 'CliImplicitFlag', + 'CliToggleFlag', + 'CliDualFlag', 'CliMutuallyExclusiveGroup', 'CliPositionalArg', 'CliSettingsSource', diff --git a/pydantic_settings/sources/__init__.py b/pydantic_settings/sources/__init__.py index 44e3bce0..3a557f7c 100644 --- a/pydantic_settings/sources/__init__.py +++ b/pydantic_settings/sources/__init__.py @@ -12,6 +12,7 @@ from .providers.azure import AzureKeyVaultSettingsSource from .providers.cli import ( CLI_SUPPRESS, + CliDualFlag, CliExplicitFlag, CliImplicitFlag, CliMutuallyExclusiveGroup, @@ -19,6 +20,7 @@ CliSettingsSource, CliSubCommand, CliSuppress, + CliToggleFlag, CliUnknownArgs, ) from .providers.dotenv import DotEnvSettingsSource, read_env_file @@ -40,6 +42,8 @@ 'AzureKeyVaultSettingsSource', 'CliExplicitFlag', 'CliImplicitFlag', + 'CliToggleFlag', + 'CliDualFlag', 'CliMutuallyExclusiveGroup', 'CliPositionalArg', 'CliSettingsSource', diff --git a/pydantic_settings/sources/providers/__init__.py b/pydantic_settings/sources/providers/__init__.py index 31759f33..9be7a546 100644 --- a/pydantic_settings/sources/providers/__init__.py +++ b/pydantic_settings/sources/providers/__init__.py @@ -3,6 +3,7 @@ from .aws import AWSSecretsManagerSettingsSource from .azure import AzureKeyVaultSettingsSource from .cli import ( + CliDualFlag, CliExplicitFlag, CliImplicitFlag, CliMutuallyExclusiveGroup, @@ -10,6 +11,7 @@ CliSettingsSource, CliSubCommand, CliSuppress, + CliToggleFlag, ) from .dotenv import DotEnvSettingsSource from .env import EnvSettingsSource @@ -25,6 +27,8 @@ 'AzureKeyVaultSettingsSource', 'CliExplicitFlag', 'CliImplicitFlag', + 'CliToggleFlag', + 'CliDualFlag', 'CliMutuallyExclusiveGroup', 'CliPositionalArg', 'CliSettingsSource', diff --git a/pydantic_settings/sources/providers/cli.py b/pydantic_settings/sources/providers/cli.py index 7f05ce43..166a14ef 100644 --- a/pydantic_settings/sources/providers/cli.py +++ b/pydantic_settings/sources/providers/cli.py @@ -51,10 +51,12 @@ ForceDecode, NoDecode, PydanticModel, + _CliDualFlag, _CliExplicitFlag, _CliImplicitFlag, _CliPositionalArg, _CliSubCommand, + _CliToggleFlag, _CliUnknownArgs, ) from ..utils import ( @@ -239,6 +241,8 @@ def is_no_decode(self) -> bool: _CliBoolFlag = TypeVar('_CliBoolFlag', bound=bool) CliImplicitFlag = Annotated[_CliBoolFlag, _CliImplicitFlag] CliExplicitFlag = Annotated[_CliBoolFlag, _CliExplicitFlag] +CliToggleFlag = Annotated[_CliBoolFlag, _CliToggleFlag] +CliDualFlag = Annotated[_CliBoolFlag, _CliDualFlag] CLI_SUPPRESS = SUPPRESS CliSuppress = Annotated[T, CLI_SUPPRESS] CliUnknownArgs = Annotated[list[str], Field(default=[]), _CliUnknownArgs, NoDecode] @@ -721,6 +725,10 @@ def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str, cli_flag_name = 'CliImplicitFlag' elif _CliExplicitFlag in field_info.metadata: cli_flag_name = 'CliExplicitFlag' + elif _CliToggleFlag in field_info.metadata: + cli_flag_name = 'CliToggleFlag' + elif _CliDualFlag in field_info.metadata: + cli_flag_name = 'CliDualFlag' else: return @@ -1018,11 +1026,27 @@ def _convert_append_action(self, kwargs: dict[str, Any], field_info: FieldInfo, def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, model_default: Any) -> None: if kwargs['metavar'] == 'bool': - if (self.cli_implicit_flags or _CliImplicitFlag in field_info.metadata) and ( - _CliExplicitFlag not in field_info.metadata - ): - del kwargs['metavar'] - kwargs['action'] = BooleanOptionalAction + meta_bool_flags = [ + meta for meta in field_info.metadata if issubclass(meta, _CliImplicitFlag | _CliExplicitFlag) + ] + if not meta_bool_flags and self.cli_implicit_flags: + meta_bool_flags = [_CliImplicitFlag] + if meta_bool_flags: + bool_flag = meta_bool_flags.pop() + if bool_flag is _CliImplicitFlag: + bool_flag = ( + _CliToggleFlag + if self.cli_implicit_flags == 'toggle' and isinstance(field_info.default, bool) + else _CliDualFlag + ) + if bool_flag is _CliDualFlag: + del kwargs['metavar'] + kwargs['action'] = BooleanOptionalAction + elif bool_flag is _CliToggleFlag: + if not isinstance(field_info.default, bool): + raise SettingsError('CliToggleFlag must have a default value') + del kwargs['metavar'] + kwargs['action'] = 'store_false' if field_info.default else 'store_true' def _convert_positional_arg( self, kwargs: dict[str, Any], field_info: FieldInfo, preferred_alias: str, model_default: Any diff --git a/pydantic_settings/sources/types.py b/pydantic_settings/sources/types.py index c4c97a1d..2fe214b0 100644 --- a/pydantic_settings/sources/types.py +++ b/pydantic_settings/sources/types.py @@ -53,6 +53,14 @@ class _CliImplicitFlag: pass +class _CliToggleFlag(_CliImplicitFlag): + pass + + +class _CliDualFlag(_CliImplicitFlag): + pass + + class _CliExplicitFlag: pass @@ -72,6 +80,8 @@ class _CliUnknownArgs: 'PydanticModel', '_CliExplicitFlag', '_CliImplicitFlag', + '_CliToggleFlag', + '_CliDualFlag', '_CliPositionalArg', '_CliSubCommand', '_CliUnknownArgs', From a90398bcd79a1ff6a89c086512456023f38abd16 Mon Sep 17 00:00:00 2001 From: Kyle Schwab Date: Tue, 25 Nov 2025 09:07:08 -0700 Subject: [PATCH 2/3] Docs. --- docs/index.md | 65 ++++++++++++++++++++++ pydantic_settings/main.py | 14 +++-- pydantic_settings/sources/providers/cli.py | 20 +++++-- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/docs/index.md b/docs/index.md index f7073848..f5ddb2ec 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1331,6 +1331,71 @@ class ImplicitSettings(BaseSettings, cli_parse_args=True, cli_implicit_flags=Tru """ ``` +Implicit flag behavior can be further refined using the "toggle" or "dual" mode settings. Similarly, the provided +`CliToggleFlag` and `CliDualFlag` annotations can be used for more granular control. + +For "toggle" flags, if default=`False`, `--flag` will store `True`. Otherwise, if default=`True`, `--no-flag` will store +`False`. + +```py +from pydantic_settings import BaseSettings, CliDualFlag, CliToggleFlag + + +class ImplicitDualSettings( + BaseSettings, cli_parse_args=True, cli_implicit_flags='dual' +): + """With cli_implicit_flags='dual', implicit flags are dual by default.""" + + implicit_req: bool + """ + --implicit_req, --no-implicit_req (required) + """ + + implicit_dual_opt: bool = False + """ + --implicit_dual_opt, --no-implicit_dual_opt (default: False) + """ + + # Implicit flags are dual by default, so must override toggle flags with annotation + flag_a: CliToggleFlag[bool] = False + """ + --flag_a (default: False) + """ + + # Implicit flags are dual by default, so must override toggle flags with annotation + flag_b: CliToggleFlag[bool] = True + """ + --no-flag_b (default: True) + """ + + +class ImplicitToggleSettings( + BaseSettings, cli_parse_args=True, cli_implicit_flags='toggle' +): + """With cli_implicit_flags='toggle', implicit flags are toggle by default.""" + + implicit_req: bool + """ + --implicit_req, --no-implicit_req (required) + """ + + # Implicit flags are toggle by default, so must override dual flags with annotation + implicit_dual_opt: CliDualFlag[bool] = False + """ + --implicit_dual_opt, --no-implicit_dual_opt (default: False) + """ + + flag_a: bool = False + """ + --flag_a (default: False) + """ + + flag_b: bool = True + """ + --no-flag_b (default: True) + """ +``` + #### Ignore and Retrieve Unknown Arguments Change whether to ignore unknown CLI arguments and only parse known ones using `cli_ignore_unknown_args`. By default, the CLI diff --git a/pydantic_settings/main.py b/pydantic_settings/main.py index 29a09997..9f142fbb 100644 --- a/pydantic_settings/main.py +++ b/pydantic_settings/main.py @@ -61,7 +61,7 @@ class SettingsConfigDict(ConfigDict, total=False): cli_exit_on_error: bool cli_prefix: str cli_flag_prefix_char: str - cli_implicit_flags: bool | None + cli_implicit_flags: bool | Literal['dual', 'toggle'] | None cli_ignore_unknown_args: bool | None cli_kebab_case: bool | Literal['all', 'no_enums'] | None cli_shortcuts: Mapping[str, str | list[str]] | None @@ -153,8 +153,12 @@ class BaseSettings(BaseModel): Defaults to `True`. _cli_prefix: The root parser command line arguments prefix. Defaults to "". _cli_flag_prefix_char: The flag prefix character to use for CLI optional arguments. Defaults to '-'. - _cli_implicit_flags: Whether `bool` fields should be implicitly converted into CLI boolean flags. - (e.g. --flag, --no-flag). Defaults to `False`. + _cli_implicit_flags: Controls how `bool` fields are exposed as CLI flags. + + - False (default): no implicit flags are generated; booleans must be set explicitly (e.g. --flag=true). + - True / 'dual': optional boolean fields generate both positive and negative forms (--flag and --no-flag). + - 'toggle': required boolean fields remain in 'dual' mode, while optional boolean fields generate a single + flag aligned with the default value (if default=False, expose --flag; if default=True, expose --no-flag). _cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`. _cli_kebab_case: CLI args use kebab case. Defaults to `False`. _cli_shortcuts: Mapping of target field name to alias names. Defaults to `None`. @@ -184,7 +188,7 @@ def __init__( _cli_exit_on_error: bool | None = None, _cli_prefix: str | None = None, _cli_flag_prefix_char: str | None = None, - _cli_implicit_flags: bool | None = None, + _cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, _cli_ignore_unknown_args: bool | None = None, _cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, _cli_shortcuts: Mapping[str, str | list[str]] | None = None, @@ -271,7 +275,7 @@ def _settings_build_values( _cli_exit_on_error: bool | None = None, _cli_prefix: str | None = None, _cli_flag_prefix_char: str | None = None, - _cli_implicit_flags: bool | None = None, + _cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, _cli_ignore_unknown_args: bool | None = None, _cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, _cli_shortcuts: Mapping[str, str | list[str]] | None = None, diff --git a/pydantic_settings/sources/providers/cli.py b/pydantic_settings/sources/providers/cli.py index d4ece99f..a1cccb99 100644 --- a/pydantic_settings/sources/providers/cli.py +++ b/pydantic_settings/sources/providers/cli.py @@ -274,8 +274,12 @@ class CliSettingsSource(EnvSettingsSource, Generic[T]): Defaults to `True`. cli_prefix: Prefix for command line arguments added under the root parser. Defaults to "". cli_flag_prefix_char: The flag prefix character to use for CLI optional arguments. Defaults to '-'. - cli_implicit_flags: Whether `bool` fields should be implicitly converted into CLI boolean flags. - (e.g. --flag, --no-flag). Defaults to `False`. + cli_implicit_flags: Controls how `bool` fields are exposed as CLI flags. + + - False (default): no implicit flags are generated; booleans must be set explicitly (e.g. --flag=true). + - True / 'dual': optional boolean fields generate both positive and negative forms (--flag and --no-flag). + - 'toggle': required boolean fields remain in 'dual' mode, while optional boolean fields generate a single + flag aligned with the default value (if default=False, expose --flag; if default=True, expose --no-flag). cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`. cli_kebab_case: CLI args use kebab case. Defaults to `False`. cli_shortcuts: Mapping of target field name to alias names. Defaults to `None`. @@ -307,7 +311,7 @@ def __init__( cli_exit_on_error: bool | None = None, cli_prefix: str | None = None, cli_flag_prefix_char: str | None = None, - cli_implicit_flags: bool | None = None, + cli_implicit_flags: bool | Literal['dual', 'toggle'] | None = None, cli_ignore_unknown_args: bool | None = None, cli_kebab_case: bool | Literal['all', 'no_enums'] | None = None, cli_shortcuts: Mapping[str, str | list[str]] | None = None, @@ -1011,7 +1015,9 @@ def _add_parser_args( if isinstance(group, dict): group = self._add_group(parser, **group) context = parser if group is None else group - arg.args = [f'{flag_prefix[: len(name)]}{name}' for name in arg_names] + if arg.kwargs.get('action') == 'store_false': + flag_prefix += 'no-' + arg.args = [f'{flag_prefix[: 1 if len(name) == 1 else None]}{name}' for name in arg_names] self._add_argument(context, *arg.args, **arg.kwargs) added_args += list(arg_names) @@ -1366,7 +1372,11 @@ def _serialized_args(self, model: PydanticModel, _is_submodel: bool = False) -> continue # Note: prepend 'no-' for boolean optional action flag if model_default value is False and flag is not a short option - if arg.kwargs.get('action') == BooleanOptionalAction and model_default is False and flag_chars == '--': + if ( + arg.kwargs.get('action') in (BooleanOptionalAction, 'store_false') + and model_default is False + and flag_chars == '--' + ): flag_chars += 'no-' optional_args.append(f'{flag_chars}{arg_name}') From c1c66f39ef775f21b8f5a2254414fdbadf21c5e7 Mon Sep 17 00:00:00 2001 From: Kyle Schwab Date: Wed, 26 Nov 2025 07:07:00 -0700 Subject: [PATCH 3/3] Tests. --- pydantic_settings/sources/providers/cli.py | 14 ++- tests/test_source_cli.py | 118 +++++++++++++++------ 2 files changed, 92 insertions(+), 40 deletions(-) diff --git a/pydantic_settings/sources/providers/cli.py b/pydantic_settings/sources/providers/cli.py index a1cccb99..bcbcab65 100644 --- a/pydantic_settings/sources/providers/cli.py +++ b/pydantic_settings/sources/providers/cli.py @@ -731,6 +731,10 @@ def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str, cli_flag_name = 'CliExplicitFlag' elif _CliToggleFlag in field_info.metadata: cli_flag_name = 'CliToggleFlag' + if not isinstance(field_info.default, bool): + raise SettingsError( + f'{cli_flag_name} argument {model.__name__}.{field_name} must have a default bool value' + ) elif _CliDualFlag in field_info.metadata: cli_flag_name = 'CliDualFlag' else: @@ -1049,8 +1053,6 @@ def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, mode del kwargs['metavar'] kwargs['action'] = BooleanOptionalAction elif bool_flag is _CliToggleFlag: - if not isinstance(field_info.default, bool): - raise SettingsError('CliToggleFlag must have a default value') del kwargs['metavar'] kwargs['action'] = 'store_false' if field_info.default else 'store_true' @@ -1372,17 +1374,13 @@ def _serialized_args(self, model: PydanticModel, _is_submodel: bool = False) -> continue # Note: prepend 'no-' for boolean optional action flag if model_default value is False and flag is not a short option - if ( - arg.kwargs.get('action') in (BooleanOptionalAction, 'store_false') - and model_default is False - and flag_chars == '--' - ): + if arg.kwargs.get('action') == BooleanOptionalAction and model_default is False and flag_chars == '--': flag_chars += 'no-' optional_args.append(f'{flag_chars}{arg_name}') # If implicit bool flag, do not add a value - if arg.kwargs.get('action') != BooleanOptionalAction: + if arg.kwargs.get('action') not in (BooleanOptionalAction, 'store_true', 'store_false'): optional_args.append(value) serialized_args: list[str] = [] diff --git a/tests/test_source_cli.py b/tests/test_source_cli.py index 42518ec8..5b6a2e34 100644 --- a/tests/test_source_cli.py +++ b/tests/test_source_cli.py @@ -41,6 +41,7 @@ ) from pydantic_settings.sources import ( CLI_SUPPRESS, + CliDualFlag, CliExplicitFlag, CliImplicitFlag, CliMutuallyExclusiveGroup, @@ -48,6 +49,7 @@ CliSettingsSource, CliSubCommand, CliSuppress, + CliToggleFlag, CliUnknownArgs, get_subcommand, ) @@ -1600,18 +1602,26 @@ class CliFlagNotBool(BaseSettings, cli_parse_args=True): CliFlagNotBool() + with pytest.raises( + SettingsError, match='CliToggleFlag argument CliToggleNoDefault.flag must have a default bool value' + ): + + class CliToggleNoDefault(BaseSettings, cli_parse_args=True): + flag: CliToggleFlag[bool] + + CliToggleNoDefault() + @pytest.mark.parametrize('enforce_required', [True, False]) -def test_cli_bool_flags(monkeypatch, enforce_required): - class ExplicitSettings(BaseSettings, cli_enforce_required=enforce_required): - explicit_req: bool - explicit_opt: bool = False - implicit_req: CliImplicitFlag[bool] - implicit_opt: CliImplicitFlag[bool] = False - - class ImplicitSettings(BaseSettings, cli_implicit_flags=True, cli_enforce_required=enforce_required): +@pytest.mark.parametrize('implicit_flags_mode', [None, True, 'dual', 'toggle']) +def test_cli_bool_flags(monkeypatch, enforce_required, implicit_flags_mode): + class FlagSettings(BaseSettings, cli_implicit_flags=implicit_flags_mode, cli_enforce_required=enforce_required): explicit_req: CliExplicitFlag[bool] explicit_opt: CliExplicitFlag[bool] = False + dual_req: CliDualFlag[bool] + dual_opt: CliDualFlag[bool] = False + toggle_def_t: CliToggleFlag[bool] = True + toggle_def_f: CliToggleFlag[bool] = False implicit_req: bool implicit_opt: bool = False @@ -1620,38 +1630,69 @@ class ImplicitSettings(BaseSettings, cli_implicit_flags=True, cli_enforce_requir 'explicit_opt': False, 'implicit_req': True, 'implicit_opt': False, + 'dual_req': True, + 'dual_opt': False, + 'toggle_def_t': True, + 'toggle_def_f': False, } - - explicit_settings = CliApp.run(ExplicitSettings, cli_args=['--explicit_req=True', '--implicit_req']) - assert explicit_settings.model_dump() == expected - serialized_args = CliApp.serialize(explicit_settings) - assert serialized_args == ['--explicit_req', 'True', '--implicit_req'] - assert CliApp.run(ExplicitSettings, cli_args=serialized_args).model_dump() == expected - - implicit_settings = CliApp.run(ImplicitSettings, cli_args=['--explicit_req=True', '--implicit_req']) + flag_args = [ + '--explicit_req', + 'True', + '--dual_req', + ] + flag_args += ['--implicit_req'] if implicit_flags_mode is not None else ['--implicit_req', 'True'] + implicit_settings = CliApp.run(FlagSettings, cli_args=flag_args) assert implicit_settings.model_dump() == expected serialized_args = CliApp.serialize(implicit_settings) - assert serialized_args == ['--explicit_req', 'True', '--implicit_req'] - assert CliApp.run(ImplicitSettings, cli_args=serialized_args).model_dump() == expected + assert serialized_args == flag_args expected = { 'explicit_req': False, 'explicit_opt': False, 'implicit_req': False, 'implicit_opt': False, + 'dual_req': False, + 'dual_opt': False, + 'toggle_def_t': False, + 'toggle_def_f': False, } + flag_args = [ + '--explicit_req', + 'False', + '--no-dual_req', + '--no-toggle_def_t', + ] + flag_args += ['--no-implicit_req'] if implicit_flags_mode is not None else ['--implicit_req', 'False'] + implicit_settings = CliApp.run(FlagSettings, cli_args=flag_args) + assert implicit_settings.model_dump() == expected + serialized_args = CliApp.serialize(implicit_settings) + assert serialized_args == flag_args - explicit_settings = CliApp.run(ExplicitSettings, cli_args=['--explicit_req=False', '--no-implicit_req']) - assert explicit_settings.model_dump() == expected - serialized_args = CliApp.serialize(explicit_settings) - assert serialized_args == ['--explicit_req', 'False', '--no-implicit_req'] - assert CliApp.run(ExplicitSettings, cli_args=serialized_args).model_dump() == expected - - implicit_settings = CliApp.run(ImplicitSettings, cli_args=['--explicit_req=False', '--no-implicit_req']) + expected = { + 'explicit_req': True, + 'explicit_opt': True, + 'implicit_req': True, + 'implicit_opt': True, + 'dual_req': True, + 'dual_opt': True, + 'toggle_def_t': True, + 'toggle_def_f': True, + } + flag_args = [ + '--explicit_req', + 'True', + '--explicit_opt', + 'True', + '--dual_req', + '--dual_opt', + '--toggle_def_f', + ] + flag_args += ['--implicit_req'] if implicit_flags_mode is not None else ['--implicit_req', 'True'] + flag_args += ['--implicit_opt'] if implicit_flags_mode is not None else ['--implicit_opt', 'True'] + implicit_settings = CliApp.run(FlagSettings, cli_args=flag_args) assert implicit_settings.model_dump() == expected serialized_args = CliApp.serialize(implicit_settings) - assert serialized_args == ['--explicit_req', 'False', '--no-implicit_req'] - assert CliApp.run(ImplicitSettings, cli_args=serialized_args).model_dump() == expected + assert serialized_args == flag_args def test_cli_avoid_json(capsys, monkeypatch): @@ -2706,17 +2747,30 @@ class SettingsAll(BaseSettings): def test_cli_kebab_case_all_with_implicit_flag(): class Settings(BaseSettings): model_config = SettingsConfigDict(cli_kebab_case='all') - test_bool_flag: CliImplicitFlag[bool] + test_bool_flag_a: CliImplicitFlag[bool] + test_bool_flag_b: CliToggleFlag[bool] = True + test_bool_flag_c: CliToggleFlag[bool] = False + test_bool_flag_d: CliDualFlag[bool] = False assert CliApp.run( Settings, - cli_args=['--test-bool-flag'], - ).model_dump() == {'test_bool_flag': True} + cli_args=['--test-bool-flag-a', '--test-bool-flag-c', '--test-bool-flag-d'], + ).model_dump() == { + 'test_bool_flag_a': True, + 'test_bool_flag_b': True, + 'test_bool_flag_c': True, + 'test_bool_flag_d': True, + } assert CliApp.run( Settings, - cli_args=['--no-test-bool-flag'], - ).model_dump() == {'test_bool_flag': False} + cli_args=['--no-test-bool-flag-a', '--no-test-bool-flag-b', '--no-test-bool-flag-d'], + ).model_dump() == { + 'test_bool_flag_a': False, + 'test_bool_flag_b': False, + 'test_bool_flag_c': False, + 'test_bool_flag_d': False, + } def test_cli_with_unbalanced_brackets_in_json_string():