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
7 changes: 6 additions & 1 deletion pydantic_settings/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,8 @@ def _settings_build_values(
dotenv_settings=dotenv_settings,
file_secret_settings=file_secret_settings,
) + (default_settings,)
if not any([source for source in sources if isinstance(source, CliSettingsSource)]):
custom_cli_sources = [source for source in sources if isinstance(source, CliSettingsSource)]
if not any(custom_cli_sources):
if isinstance(cli_settings_source, CliSettingsSource):
sources = (cli_settings_source,) + sources
elif cli_parse_args is not None:
Expand All @@ -414,6 +415,10 @@ def _settings_build_values(
case_sensitive=case_sensitive,
)
sources = (cli_settings,) + sources
# We ensure that if command line arguments haven't been parsed yet, we do so.
elif cli_parse_args and not custom_cli_sources[0].env_vars:
custom_cli_sources[0](args=cli_parse_args)

if sources:
state: dict[str, Any] = {}
states: dict[str, dict[str, Any]] = {}
Expand Down
49 changes: 49 additions & 0 deletions tests/test_source_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2531,6 +2531,33 @@ class Options(BaseSettings):
assert CliApp.run(Options, cli_args=['--nested.foo=5']).model_dump() == {'nested': {'foo': 5, 'bar': 2}}


def test_cli_parse_args_from_model_config_is_respected_with_settings_customise_sources(
monkeypatch: pytest.MonkeyPatch,
):
class MySettings(BaseSettings):
model_config = SettingsConfigDict(cli_parse_args=True)

foo: str

@classmethod
def settings_customise_sources(
cls,
settings_cls: type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
return (CliSettingsSource(settings_cls),)

with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['example.py', '--foo', 'bar'])

cfg = CliApp.run(MySettings)

assert cfg.model_dump() == {'foo': 'bar'}


def test_cli_shortcuts_on_flat_object():
class Settings(BaseSettings):
option: str = Field(default='foo')
Expand Down Expand Up @@ -2612,3 +2639,25 @@ class Cfg(BaseSettings):
serialized_cli_args = CliApp.serialize(cfg)
assert serialized_cli_args == ['0', '1', '2', '3', '4', '5']
assert CliApp.run(Cfg, cli_args=serialized_cli_args).model_dump() == cfg.model_dump()


def test_cli_app_with_separate_parser(monkeypatch):
class Cfg(BaseSettings):
model_config = SettingsConfigDict(cli_parse_args=True)
pet: Literal['dog', 'cat', 'bird']

parser = argparse.ArgumentParser()

# The actual parsing of command line argument should not happen here.
cli_settings = CliSettingsSource(Cfg, root_parser=parser)

parser.add_argument('-e', '--extra', dest='extra', default=0, action='count')

with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['example.py', '--pet', 'dog', '-eeee'])

parsed_args = parser.parse_args()

assert parsed_args.extra == 4
# With parsed arguments passed to CliApp.run, the parser should not need to be called again.
assert CliApp.run(Cfg, cli_args=parsed_args, cli_settings_source=cli_settings).model_dump() == {'pet': 'dog'}
Loading