Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pydantic_settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
CLI_SUPPRESS,
AWSSecretsManagerSettingsSource,
AzureKeyVaultSettingsSource,
CliDualFlag,
CliExplicitFlag,
CliImplicitFlag,
CliMutuallyExclusiveGroup,
CliPositionalArg,
CliSettingsSource,
CliSubCommand,
CliSuppress,
CliToggleFlag,
CliUnknownArgs,
DotEnvSettingsSource,
EnvSettingsSource,
Expand All @@ -37,6 +39,8 @@
'CliApp',
'CliExplicitFlag',
'CliImplicitFlag',
'CliToggleFlag',
'CliDualFlag',
'CliMutuallyExclusiveGroup',
'CliPositionalArg',
'CliSettingsSource',
Expand Down
14 changes: 9 additions & 5 deletions pydantic_settings/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions pydantic_settings/sources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
from .providers.azure import AzureKeyVaultSettingsSource
from .providers.cli import (
CLI_SUPPRESS,
CliDualFlag,
CliExplicitFlag,
CliImplicitFlag,
CliMutuallyExclusiveGroup,
CliPositionalArg,
CliSettingsSource,
CliSubCommand,
CliSuppress,
CliToggleFlag,
CliUnknownArgs,
)
from .providers.dotenv import DotEnvSettingsSource, read_env_file
Expand All @@ -40,6 +42,8 @@
'AzureKeyVaultSettingsSource',
'CliExplicitFlag',
'CliImplicitFlag',
'CliToggleFlag',
'CliDualFlag',
'CliMutuallyExclusiveGroup',
'CliPositionalArg',
'CliSettingsSource',
Expand Down
4 changes: 4 additions & 0 deletions pydantic_settings/sources/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
from .aws import AWSSecretsManagerSettingsSource
from .azure import AzureKeyVaultSettingsSource
from .cli import (
CliDualFlag,
CliExplicitFlag,
CliImplicitFlag,
CliMutuallyExclusiveGroup,
CliPositionalArg,
CliSettingsSource,
CliSubCommand,
CliSuppress,
CliToggleFlag,
)
from .dotenv import DotEnvSettingsSource
from .env import EnvSettingsSource
Expand All @@ -25,6 +27,8 @@
'AzureKeyVaultSettingsSource',
'CliExplicitFlag',
'CliImplicitFlag',
'CliToggleFlag',
'CliDualFlag',
'CliMutuallyExclusiveGroup',
'CliPositionalArg',
'CliSettingsSource',
Expand Down
52 changes: 42 additions & 10 deletions pydantic_settings/sources/providers/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@
ForceDecode,
NoDecode,
PydanticModel,
_CliDualFlag,
_CliExplicitFlag,
_CliImplicitFlag,
_CliPositionalArg,
_CliSubCommand,
_CliToggleFlag,
_CliUnknownArgs,
)
from ..utils import (
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -270,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`.
Expand Down Expand Up @@ -303,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,
Expand Down Expand Up @@ -721,6 +729,14 @@ 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'
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:
return

Expand Down Expand Up @@ -1003,7 +1019,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)

Expand All @@ -1018,11 +1036,25 @@ 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:
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
Expand Down Expand Up @@ -1348,7 +1380,7 @@ def _serialized_args(self, model: PydanticModel, _is_submodel: bool = False) ->
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] = []
Expand Down
10 changes: 10 additions & 0 deletions pydantic_settings/sources/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ class _CliImplicitFlag:
pass


class _CliToggleFlag(_CliImplicitFlag):
pass


class _CliDualFlag(_CliImplicitFlag):
pass


class _CliExplicitFlag:
pass

Expand All @@ -72,6 +80,8 @@ class _CliUnknownArgs:
'PydanticModel',
'_CliExplicitFlag',
'_CliImplicitFlag',
'_CliToggleFlag',
'_CliDualFlag',
'_CliPositionalArg',
'_CliSubCommand',
'_CliUnknownArgs',
Expand Down
Loading