Skip to content

Commit 94f3a90

Browse files
authored
CLI ignore external parser list fix (#379)
1 parent 818d56e commit 94f3a90

File tree

2 files changed

+71
-27
lines changed

2 files changed

+71
-27
lines changed

pydantic_settings/sources.py

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from enum import Enum
1919
from pathlib import Path
2020
from textwrap import dedent
21+
from types import SimpleNamespace
2122
from typing import (
2223
TYPE_CHECKING,
2324
Any,
@@ -1155,13 +1156,15 @@ def __call__(self, *, args: list[str] | tuple[str, ...] | bool) -> CliSettingsSo
11551156
...
11561157

11571158
@overload
1158-
def __call__(self, *, parsed_args: Namespace | dict[str, list[str] | str]) -> CliSettingsSource[T]:
1159+
def __call__(
1160+
self, *, parsed_args: Namespace | SimpleNamespace | dict[str, list[str] | str]
1161+
) -> CliSettingsSource[T]:
11591162
"""
11601163
Loads parsed command line arguments into the CLI settings source.
11611164
11621165
Note:
1163-
The parsed args must be in `argparse.Namespace` or vars dictionary (e.g., vars(argparse.Namespace))
1164-
format.
1166+
The parsed args must be in `argparse.Namespace`, `SimpleNamespace`, or vars dictionary
1167+
(e.g., vars(argparse.Namespace)) format.
11651168
11661169
Args:
11671170
parsed_args: The parsed args to load.
@@ -1175,7 +1178,7 @@ def __call__(
11751178
self,
11761179
*,
11771180
args: list[str] | tuple[str, ...] | bool | None = None,
1178-
parsed_args: Namespace | dict[str, list[str] | str] | None = None,
1181+
parsed_args: Namespace | SimpleNamespace | dict[str, list[str] | str] | None = None,
11791182
) -> dict[str, Any] | CliSettingsSource[T]:
11801183
if args is not None and parsed_args is not None:
11811184
raise SettingsError('`args` and `parsed_args` are mutually exclusive')
@@ -1194,13 +1197,15 @@ def __call__(
11941197
def _load_env_vars(self) -> Mapping[str, str | None]: ...
11951198

11961199
@overload
1197-
def _load_env_vars(self, *, parsed_args: Namespace | dict[str, list[str] | str]) -> CliSettingsSource[T]:
1200+
def _load_env_vars(
1201+
self, *, parsed_args: Namespace | SimpleNamespace | dict[str, list[str] | str]
1202+
) -> CliSettingsSource[T]:
11981203
"""
11991204
Loads the parsed command line arguments into the CLI environment settings variables.
12001205
12011206
Note:
1202-
The parsed args must be in `argparse.Namespace` or vars dictionary (e.g., vars(argparse.Namespace))
1203-
format.
1207+
The parsed args must be in `argparse.Namespace`, `SimpleNamespace`, or vars dictionary
1208+
(e.g., vars(argparse.Namespace)) format.
12041209
12051210
Args:
12061211
parsed_args: The parsed args to load.
@@ -1211,12 +1216,12 @@ def _load_env_vars(self, *, parsed_args: Namespace | dict[str, list[str] | str])
12111216
...
12121217

12131218
def _load_env_vars(
1214-
self, *, parsed_args: Namespace | dict[str, list[str] | str] | None = None
1219+
self, *, parsed_args: Namespace | SimpleNamespace | dict[str, list[str] | str] | None = None
12151220
) -> Mapping[str, str | None] | CliSettingsSource[T]:
12161221
if parsed_args is None:
12171222
return {}
12181223

1219-
if isinstance(parsed_args, Namespace):
1224+
if isinstance(parsed_args, (Namespace, SimpleNamespace)):
12201225
parsed_args = vars(parsed_args)
12211226

12221227
selected_subcommands: list[str] = []
@@ -1246,26 +1251,35 @@ def _load_env_vars(
12461251

12471252
return self
12481253

1254+
def _get_merge_parsed_list_types(
1255+
self, parsed_list: list[str], field_name: str
1256+
) -> tuple[Optional[type], Optional[type]]:
1257+
merge_type = self._cli_dict_args.get(field_name, list)
1258+
if (
1259+
merge_type is list
1260+
or not origin_is_union(get_origin(merge_type))
1261+
or not any(
1262+
type_
1263+
for type_ in get_args(merge_type)
1264+
if type_ is not type(None) and get_origin(type_) not in (dict, Mapping)
1265+
)
1266+
):
1267+
inferred_type = merge_type
1268+
else:
1269+
inferred_type = list if parsed_list and (len(parsed_list) > 1 or parsed_list[0].startswith('[')) else str
1270+
1271+
return merge_type, inferred_type
1272+
12491273
def _merge_parsed_list(self, parsed_list: list[str], field_name: str) -> str:
12501274
try:
12511275
merged_list: list[str] = []
12521276
is_last_consumed_a_value = False
1253-
merge_type = self._cli_dict_args.get(field_name, list)
1254-
if (
1255-
merge_type is list
1256-
or not origin_is_union(get_origin(merge_type))
1257-
or not any(
1258-
type_
1259-
for type_ in get_args(merge_type)
1260-
if type_ is not type(None) and get_origin(type_) not in (dict, Mapping)
1261-
)
1262-
):
1263-
inferred_type = merge_type
1264-
else:
1265-
inferred_type = (
1266-
list if parsed_list and (len(parsed_list) > 1 or parsed_list[0].startswith('[')) else str
1267-
)
1277+
merge_type, inferred_type = self._get_merge_parsed_list_types(parsed_list, field_name)
12681278
for val in parsed_list:
1279+
if not isinstance(val, str):
1280+
# If val is not a string, it's from an external parser and we can ignore parsing the rest of the
1281+
# list.
1282+
break
12691283
val = val.strip()
12701284
if val.startswith('[') and val.endswith(']'):
12711285
val = val[1:-1].strip()

tests/test_settings.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3665,21 +3665,51 @@ class Cfg(BaseSettings):
36653665
cli_cfg_settings = CliSettingsSource(Cfg, cli_prefix=prefix, root_parser=parser)
36663666

36673667
add_arg('--fruit', choices=['pear', 'kiwi', 'lime'])
3668+
add_arg('--num-list', action='append', type=int)
3669+
add_arg('--num', type=int)
36683670

3669-
args = ['--fruit', 'pear']
3671+
args = ['--fruit', 'pear', '--num', '0', '--num-list', '1', '--num-list', '2', '--num-list', '3']
36703672
parsed_args = parse_args(args)
36713673
assert Cfg(_cli_settings_source=cli_cfg_settings(parsed_args=parsed_args)).model_dump() == {'pet': 'bird'}
36723674
assert Cfg(_cli_settings_source=cli_cfg_settings(args=args)).model_dump() == {'pet': 'bird'}
36733675
assert Cfg(_cli_settings_source=cli_cfg_settings(args=False)).model_dump() == {'pet': 'bird'}
36743676

36753677
arg_prefix = f'{prefix}.' if prefix else ''
3676-
args = ['--fruit', 'kiwi', f'--{arg_prefix}pet', 'dog']
3678+
args = [
3679+
'--fruit',
3680+
'kiwi',
3681+
'--num',
3682+
'0',
3683+
'--num-list',
3684+
'1',
3685+
'--num-list',
3686+
'2',
3687+
'--num-list',
3688+
'3',
3689+
f'--{arg_prefix}pet',
3690+
'dog',
3691+
]
36773692
parsed_args = parse_args(args)
36783693
assert Cfg(_cli_settings_source=cli_cfg_settings(parsed_args=parsed_args)).model_dump() == {'pet': 'dog'}
36793694
assert Cfg(_cli_settings_source=cli_cfg_settings(args=args)).model_dump() == {'pet': 'dog'}
36803695
assert Cfg(_cli_settings_source=cli_cfg_settings(args=False)).model_dump() == {'pet': 'bird'}
36813696

3682-
parsed_args = parse_args(['--fruit', 'kiwi', f'--{arg_prefix}pet', 'cat'])
3697+
parsed_args = parse_args(
3698+
[
3699+
'--fruit',
3700+
'kiwi',
3701+
'--num',
3702+
'0',
3703+
'--num-list',
3704+
'1',
3705+
'--num-list',
3706+
'2',
3707+
'--num-list',
3708+
'3',
3709+
f'--{arg_prefix}pet',
3710+
'cat',
3711+
]
3712+
)
36833713
assert Cfg(_cli_settings_source=cli_cfg_settings(parsed_args=vars(parsed_args))).model_dump() == {'pet': 'cat'}
36843714
assert Cfg(_cli_settings_source=cli_cfg_settings(args=False)).model_dump() == {'pet': 'bird'}
36853715

0 commit comments

Comments
 (0)