Skip to content

Commit c1c66f3

Browse files
committed
Tests.
1 parent a90398b commit c1c66f3

File tree

2 files changed

+92
-40
lines changed

2 files changed

+92
-40
lines changed

pydantic_settings/sources/providers/cli.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,10 @@ def _verify_cli_flag_annotations(self, model: type[BaseModel], field_name: str,
731731
cli_flag_name = 'CliExplicitFlag'
732732
elif _CliToggleFlag in field_info.metadata:
733733
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+
)
734738
elif _CliDualFlag in field_info.metadata:
735739
cli_flag_name = 'CliDualFlag'
736740
else:
@@ -1049,8 +1053,6 @@ def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, mode
10491053
del kwargs['metavar']
10501054
kwargs['action'] = BooleanOptionalAction
10511055
elif bool_flag is _CliToggleFlag:
1052-
if not isinstance(field_info.default, bool):
1053-
raise SettingsError('CliToggleFlag must have a default value')
10541056
del kwargs['metavar']
10551057
kwargs['action'] = 'store_false' if field_info.default else 'store_true'
10561058

@@ -1372,17 +1374,13 @@ def _serialized_args(self, model: PydanticModel, _is_submodel: bool = False) ->
13721374
continue
13731375

13741376
# Note: prepend 'no-' for boolean optional action flag if model_default value is False and flag is not a short option
1375-
if (
1376-
arg.kwargs.get('action') in (BooleanOptionalAction, 'store_false')
1377-
and model_default is False
1378-
and flag_chars == '--'
1379-
):
1377+
if arg.kwargs.get('action') == BooleanOptionalAction and model_default is False and flag_chars == '--':
13801378
flag_chars += 'no-'
13811379

13821380
optional_args.append(f'{flag_chars}{arg_name}')
13831381

13841382
# If implicit bool flag, do not add a value
1385-
if arg.kwargs.get('action') != BooleanOptionalAction:
1383+
if arg.kwargs.get('action') not in (BooleanOptionalAction, 'store_true', 'store_false'):
13861384
optional_args.append(value)
13871385

13881386
serialized_args: list[str] = []

tests/test_source_cli.py

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@
4141
)
4242
from pydantic_settings.sources import (
4343
CLI_SUPPRESS,
44+
CliDualFlag,
4445
CliExplicitFlag,
4546
CliImplicitFlag,
4647
CliMutuallyExclusiveGroup,
4748
CliPositionalArg,
4849
CliSettingsSource,
4950
CliSubCommand,
5051
CliSuppress,
52+
CliToggleFlag,
5153
CliUnknownArgs,
5254
get_subcommand,
5355
)
@@ -1600,18 +1602,26 @@ class CliFlagNotBool(BaseSettings, cli_parse_args=True):
16001602

16011603
CliFlagNotBool()
16021604

1605+
with pytest.raises(
1606+
SettingsError, match='CliToggleFlag argument CliToggleNoDefault.flag must have a default bool value'
1607+
):
1608+
1609+
class CliToggleNoDefault(BaseSettings, cli_parse_args=True):
1610+
flag: CliToggleFlag[bool]
1611+
1612+
CliToggleNoDefault()
1613+
16031614

16041615
@pytest.mark.parametrize('enforce_required', [True, False])
1605-
def test_cli_bool_flags(monkeypatch, enforce_required):
1606-
class ExplicitSettings(BaseSettings, cli_enforce_required=enforce_required):
1607-
explicit_req: bool
1608-
explicit_opt: bool = False
1609-
implicit_req: CliImplicitFlag[bool]
1610-
implicit_opt: CliImplicitFlag[bool] = False
1611-
1612-
class ImplicitSettings(BaseSettings, cli_implicit_flags=True, cli_enforce_required=enforce_required):
1616+
@pytest.mark.parametrize('implicit_flags_mode', [None, True, 'dual', 'toggle'])
1617+
def test_cli_bool_flags(monkeypatch, enforce_required, implicit_flags_mode):
1618+
class FlagSettings(BaseSettings, cli_implicit_flags=implicit_flags_mode, cli_enforce_required=enforce_required):
16131619
explicit_req: CliExplicitFlag[bool]
16141620
explicit_opt: CliExplicitFlag[bool] = False
1621+
dual_req: CliDualFlag[bool]
1622+
dual_opt: CliDualFlag[bool] = False
1623+
toggle_def_t: CliToggleFlag[bool] = True
1624+
toggle_def_f: CliToggleFlag[bool] = False
16151625
implicit_req: bool
16161626
implicit_opt: bool = False
16171627

@@ -1620,38 +1630,69 @@ class ImplicitSettings(BaseSettings, cli_implicit_flags=True, cli_enforce_requir
16201630
'explicit_opt': False,
16211631
'implicit_req': True,
16221632
'implicit_opt': False,
1633+
'dual_req': True,
1634+
'dual_opt': False,
1635+
'toggle_def_t': True,
1636+
'toggle_def_f': False,
16231637
}
1624-
1625-
explicit_settings = CliApp.run(ExplicitSettings, cli_args=['--explicit_req=True', '--implicit_req'])
1626-
assert explicit_settings.model_dump() == expected
1627-
serialized_args = CliApp.serialize(explicit_settings)
1628-
assert serialized_args == ['--explicit_req', 'True', '--implicit_req']
1629-
assert CliApp.run(ExplicitSettings, cli_args=serialized_args).model_dump() == expected
1630-
1631-
implicit_settings = CliApp.run(ImplicitSettings, cli_args=['--explicit_req=True', '--implicit_req'])
1638+
flag_args = [
1639+
'--explicit_req',
1640+
'True',
1641+
'--dual_req',
1642+
]
1643+
flag_args += ['--implicit_req'] if implicit_flags_mode is not None else ['--implicit_req', 'True']
1644+
implicit_settings = CliApp.run(FlagSettings, cli_args=flag_args)
16321645
assert implicit_settings.model_dump() == expected
16331646
serialized_args = CliApp.serialize(implicit_settings)
1634-
assert serialized_args == ['--explicit_req', 'True', '--implicit_req']
1635-
assert CliApp.run(ImplicitSettings, cli_args=serialized_args).model_dump() == expected
1647+
assert serialized_args == flag_args
16361648

16371649
expected = {
16381650
'explicit_req': False,
16391651
'explicit_opt': False,
16401652
'implicit_req': False,
16411653
'implicit_opt': False,
1654+
'dual_req': False,
1655+
'dual_opt': False,
1656+
'toggle_def_t': False,
1657+
'toggle_def_f': False,
16421658
}
1659+
flag_args = [
1660+
'--explicit_req',
1661+
'False',
1662+
'--no-dual_req',
1663+
'--no-toggle_def_t',
1664+
]
1665+
flag_args += ['--no-implicit_req'] if implicit_flags_mode is not None else ['--implicit_req', 'False']
1666+
implicit_settings = CliApp.run(FlagSettings, cli_args=flag_args)
1667+
assert implicit_settings.model_dump() == expected
1668+
serialized_args = CliApp.serialize(implicit_settings)
1669+
assert serialized_args == flag_args
16431670

1644-
explicit_settings = CliApp.run(ExplicitSettings, cli_args=['--explicit_req=False', '--no-implicit_req'])
1645-
assert explicit_settings.model_dump() == expected
1646-
serialized_args = CliApp.serialize(explicit_settings)
1647-
assert serialized_args == ['--explicit_req', 'False', '--no-implicit_req']
1648-
assert CliApp.run(ExplicitSettings, cli_args=serialized_args).model_dump() == expected
1649-
1650-
implicit_settings = CliApp.run(ImplicitSettings, cli_args=['--explicit_req=False', '--no-implicit_req'])
1671+
expected = {
1672+
'explicit_req': True,
1673+
'explicit_opt': True,
1674+
'implicit_req': True,
1675+
'implicit_opt': True,
1676+
'dual_req': True,
1677+
'dual_opt': True,
1678+
'toggle_def_t': True,
1679+
'toggle_def_f': True,
1680+
}
1681+
flag_args = [
1682+
'--explicit_req',
1683+
'True',
1684+
'--explicit_opt',
1685+
'True',
1686+
'--dual_req',
1687+
'--dual_opt',
1688+
'--toggle_def_f',
1689+
]
1690+
flag_args += ['--implicit_req'] if implicit_flags_mode is not None else ['--implicit_req', 'True']
1691+
flag_args += ['--implicit_opt'] if implicit_flags_mode is not None else ['--implicit_opt', 'True']
1692+
implicit_settings = CliApp.run(FlagSettings, cli_args=flag_args)
16511693
assert implicit_settings.model_dump() == expected
16521694
serialized_args = CliApp.serialize(implicit_settings)
1653-
assert serialized_args == ['--explicit_req', 'False', '--no-implicit_req']
1654-
assert CliApp.run(ImplicitSettings, cli_args=serialized_args).model_dump() == expected
1695+
assert serialized_args == flag_args
16551696

16561697

16571698
def test_cli_avoid_json(capsys, monkeypatch):
@@ -2706,17 +2747,30 @@ class SettingsAll(BaseSettings):
27062747
def test_cli_kebab_case_all_with_implicit_flag():
27072748
class Settings(BaseSettings):
27082749
model_config = SettingsConfigDict(cli_kebab_case='all')
2709-
test_bool_flag: CliImplicitFlag[bool]
2750+
test_bool_flag_a: CliImplicitFlag[bool]
2751+
test_bool_flag_b: CliToggleFlag[bool] = True
2752+
test_bool_flag_c: CliToggleFlag[bool] = False
2753+
test_bool_flag_d: CliDualFlag[bool] = False
27102754

27112755
assert CliApp.run(
27122756
Settings,
2713-
cli_args=['--test-bool-flag'],
2714-
).model_dump() == {'test_bool_flag': True}
2757+
cli_args=['--test-bool-flag-a', '--test-bool-flag-c', '--test-bool-flag-d'],
2758+
).model_dump() == {
2759+
'test_bool_flag_a': True,
2760+
'test_bool_flag_b': True,
2761+
'test_bool_flag_c': True,
2762+
'test_bool_flag_d': True,
2763+
}
27152764

27162765
assert CliApp.run(
27172766
Settings,
2718-
cli_args=['--no-test-bool-flag'],
2719-
).model_dump() == {'test_bool_flag': False}
2767+
cli_args=['--no-test-bool-flag-a', '--no-test-bool-flag-b', '--no-test-bool-flag-d'],
2768+
).model_dump() == {
2769+
'test_bool_flag_a': False,
2770+
'test_bool_flag_b': False,
2771+
'test_bool_flag_c': False,
2772+
'test_bool_flag_d': False,
2773+
}
27202774

27212775

27222776
def test_cli_with_unbalanced_brackets_in_json_string():

0 commit comments

Comments
 (0)