Skip to content

Commit 4eb7dd5

Browse files
committed
feat: Add cli_aliases to CLI settings
`cli_aliases` is a mapping of alias name to field path.
1 parent e9f7994 commit 4eb7dd5

File tree

3 files changed

+60
-0
lines changed

3 files changed

+60
-0
lines changed

pydantic_settings/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import inspect
55
import threading
66
from argparse import Namespace
7+
from collections.abc import Mapping
78
from types import SimpleNamespace
89
from typing import Any, ClassVar, TypeVar
910

@@ -57,6 +58,7 @@ class SettingsConfigDict(ConfigDict, total=False):
5758
cli_implicit_flags: bool | None
5859
cli_ignore_unknown_args: bool | None
5960
cli_kebab_case: bool | None
61+
cli_aliases: Mapping[str, str] | None
6062
secrets_dir: PathType | None
6163
json_file: PathType | None
6264
json_file_encoding: str | None
@@ -149,6 +151,7 @@ class BaseSettings(BaseModel):
149151
(e.g. --flag, --no-flag). Defaults to `False`.
150152
_cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`.
151153
_cli_kebab_case: CLI args use kebab case. Defaults to `False`.
154+
_cli_aliases: Mapping of alias name to field path. Defaults to `None`.
152155
_secrets_dir: The secret files directory or a sequence of directories. Defaults to `None`.
153156
"""
154157

@@ -178,6 +181,7 @@ def __init__(
178181
_cli_implicit_flags: bool | None = None,
179182
_cli_ignore_unknown_args: bool | None = None,
180183
_cli_kebab_case: bool | None = None,
184+
_cli_aliases: Mapping[str, str] | None = None,
181185
_secrets_dir: PathType | None = None,
182186
**values: Any,
183187
) -> None:
@@ -208,6 +212,7 @@ def __init__(
208212
_cli_implicit_flags=_cli_implicit_flags,
209213
_cli_ignore_unknown_args=_cli_ignore_unknown_args,
210214
_cli_kebab_case=_cli_kebab_case,
215+
_cli_aliases=_cli_aliases,
211216
_secrets_dir=_secrets_dir,
212217
)
213218
)
@@ -263,6 +268,7 @@ def _settings_build_values(
263268
_cli_implicit_flags: bool | None = None,
264269
_cli_ignore_unknown_args: bool | None = None,
265270
_cli_kebab_case: bool | None = None,
271+
_cli_aliases: Mapping[str, str] | None = None,
266272
_secrets_dir: PathType | None = None,
267273
) -> dict[str, Any]:
268274
# Determine settings config values
@@ -336,6 +342,7 @@ def _settings_build_values(
336342
else self.model_config.get('cli_ignore_unknown_args')
337343
)
338344
cli_kebab_case = _cli_kebab_case if _cli_kebab_case is not None else self.model_config.get('cli_kebab_case')
345+
cli_aliases = _cli_aliases if _cli_aliases is not None else self.model_config.get('cli_aliases')
339346

340347
secrets_dir = _secrets_dir if _secrets_dir is not None else self.model_config.get('secrets_dir')
341348

@@ -401,6 +408,7 @@ def _settings_build_values(
401408
cli_implicit_flags=cli_implicit_flags,
402409
cli_ignore_unknown_args=cli_ignore_unknown_args,
403410
cli_kebab_case=cli_kebab_case,
411+
cli_aliases=cli_aliases,
404412
case_sensitive=case_sensitive,
405413
)
406414
sources = (cli_settings,) + sources
@@ -450,6 +458,7 @@ def _settings_build_values(
450458
cli_implicit_flags=False,
451459
cli_ignore_unknown_args=False,
452460
cli_kebab_case=False,
461+
cli_aliases=None,
453462
json_file=None,
454463
json_file_encoding=None,
455464
yaml_file=None,

pydantic_settings/sources/providers/cli.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class CliSettingsSource(EnvSettingsSource, Generic[T]):
119119
(e.g. --flag, --no-flag). Defaults to `False`.
120120
cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`.
121121
cli_kebab_case: CLI args use kebab case. Defaults to `False`.
122+
cli_aliases: Mapping of alias name to field path. Defaults to `None`.
122123
case_sensitive: Whether CLI "--arg" names should be read with case-sensitivity. Defaults to `True`.
123124
Note: Case-insensitive matching is only supported on the internal root parser and does not apply to CLI
124125
subcommands.
@@ -150,6 +151,7 @@ def __init__(
150151
cli_implicit_flags: bool | None = None,
151152
cli_ignore_unknown_args: bool | None = None,
152153
cli_kebab_case: bool | None = None,
154+
cli_aliases: Mapping[str, str] | None = None,
153155
case_sensitive: bool | None = True,
154156
root_parser: Any = None,
155157
parse_args_method: Callable[..., Any] | None = None,
@@ -215,6 +217,9 @@ def __init__(
215217
self.cli_kebab_case = (
216218
cli_kebab_case if cli_kebab_case is not None else settings_cls.model_config.get('cli_kebab_case', False)
217219
)
220+
self.cli_aliases = (
221+
cli_aliases if cli_aliases is not None else settings_cls.model_config.get('cli_aliases', None)
222+
)
218223

219224
case_sensitive = case_sensitive if case_sensitive is not None else True
220225
if not case_sensitive and root_parser is not None:
@@ -767,6 +772,11 @@ def _add_parser_args(
767772
else f'{arg_prefix}{preferred_alias}'
768773
)
769774

775+
if self.cli_aliases and kwargs['dest'] in self.cli_aliases:
776+
raise SettingsError(
777+
f'{model.__name__}.{field_name} has multiple aliases: {preferred_alias} and {self.cli_aliases[kwargs["dest"]]}'
778+
)
779+
770780
arg_names = self._get_arg_names(arg_prefix, subcommand_prefix, alias_prefixes, alias_names, added_args)
771781
if not arg_names or (kwargs['dest'] in added_args):
772782
continue
@@ -874,6 +884,12 @@ def _get_arg_names(
874884
)
875885
if arg_name not in added_args:
876886
arg_names.append(arg_name)
887+
888+
if self.cli_aliases:
889+
for alias, name in self.cli_aliases.items():
890+
if name in arg_names and alias not in added_args:
891+
arg_names.append(alias)
892+
877893
return arg_names
878894

879895
def _add_parser_submodels(

tests/test_source_cli.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2525,3 +2525,38 @@ def settings_customise_sources(
25252525
cfg = CliApp.run(MySettings)
25262526

25272527
assert cfg.model_dump() == {'foo': 'bar'}
2528+
2529+
2530+
def test_cli_aliases_on_flat_object():
2531+
class Settings(BaseSettings):
2532+
option: str = Field(default='foo')
2533+
2534+
model_config = SettingsConfigDict(cli_aliases={'option2': 'option'})
2535+
2536+
assert CliApp.run(Settings, cli_args=['--option2', 'bar']).model_dump() == {'option': 'bar'}
2537+
2538+
2539+
def test_cli_aliases_on_nested_object():
2540+
class Nested(BaseModel):
2541+
option: str = Field(default='foo')
2542+
2543+
class Settings(BaseSettings):
2544+
nested: Nested = Nested()
2545+
2546+
model_config = SettingsConfigDict(cli_aliases={'option2': 'nested.option'})
2547+
2548+
assert CliApp.run(Settings, cli_args=['--option2', 'bar']).model_dump() == {'nested': {'option': 'bar'}}
2549+
2550+
2551+
def test_cli_aliases_name_collision():
2552+
class Nested(BaseModel):
2553+
option: str = Field(default='foo')
2554+
2555+
class Settings(BaseSettings):
2556+
nested: Nested = Nested()
2557+
option2: str = Field(default='foo2')
2558+
2559+
model_config = SettingsConfigDict(cli_aliases={'option2': 'nested.option'})
2560+
2561+
with pytest.raises(SettingsError, match='Settings.option2 has multiple aliases: option2 and nested.option'):
2562+
CliApp.run(Settings, cli_args=['--option2', 'bar'])

0 commit comments

Comments
 (0)