Skip to content

Commit 7e7ccdb

Browse files
Add cli_ignore_unknown_args config option. (#405)
Co-authored-by: hyperlint-ai[bot] <154288675+hyperlint-ai[bot]@users.noreply.github.com>
1 parent b680531 commit 7e7ccdb

File tree

4 files changed

+58
-2
lines changed

4 files changed

+58
-2
lines changed

docs/index.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,26 @@ class ImplicitSettings(BaseSettings, cli_parse_args=True, cli_implicit_flags=Tru
979979
"""
980980
```
981981

982+
#### Ignore Unknown Arguments
983+
984+
Change whether to ignore unknown CLI arguments and only parse known ones using `cli_ignore_unknown_args`. By default, the CLI
985+
does not ignore any args.
986+
987+
```py
988+
import sys
989+
990+
from pydantic_settings import BaseSettings
991+
992+
993+
class Settings(BaseSettings, cli_parse_args=True, cli_ignore_unknown_args=True):
994+
good_arg: str
995+
996+
997+
sys.argv = ['example.py', '--bad-arg=bad', 'ANOTHER_BAD_ARG', '--good_arg=hello world']
998+
print(Settings().model_dump())
999+
#> {'good_arg': 'hello world'}
1000+
```
1001+
9821002
#### Change Whether CLI Should Exit on Error
9831003

9841004
Change whether the CLI internal parser will exit on error or raise a `SettingsError` exception by using

pydantic_settings/main.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class SettingsConfigDict(ConfigDict, total=False):
4242
cli_exit_on_error: bool
4343
cli_prefix: str
4444
cli_implicit_flags: bool | None
45+
cli_ignore_unknown_args: bool | None
4546
secrets_dir: PathType | None
4647
json_file: PathType | None
4748
json_file_encoding: str | None
@@ -120,6 +121,7 @@ class BaseSettings(BaseModel):
120121
_cli_prefix: The root parser command line arguments prefix. Defaults to "".
121122
_cli_implicit_flags: Whether `bool` fields should be implicitly converted into CLI boolean flags.
122123
(e.g. --flag, --no-flag). Defaults to `False`.
124+
_cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`.
123125
_secrets_dir: The secret files directory or a sequence of directories. Defaults to `None`.
124126
"""
125127

@@ -145,6 +147,7 @@ def __init__(
145147
_cli_exit_on_error: bool | None = None,
146148
_cli_prefix: str | None = None,
147149
_cli_implicit_flags: bool | None = None,
150+
_cli_ignore_unknown_args: bool | None = None,
148151
_secrets_dir: PathType | None = None,
149152
**values: Any,
150153
) -> None:
@@ -172,6 +175,7 @@ def __init__(
172175
_cli_exit_on_error=_cli_exit_on_error,
173176
_cli_prefix=_cli_prefix,
174177
_cli_implicit_flags=_cli_implicit_flags,
178+
_cli_ignore_unknown_args=_cli_ignore_unknown_args,
175179
_secrets_dir=_secrets_dir,
176180
)
177181
)
@@ -223,6 +227,7 @@ def _settings_build_values(
223227
_cli_exit_on_error: bool | None = None,
224228
_cli_prefix: str | None = None,
225229
_cli_implicit_flags: bool | None = None,
230+
_cli_ignore_unknown_args: bool | None = None,
226231
_secrets_dir: PathType | None = None,
227232
) -> dict[str, Any]:
228233
# Determine settings config values
@@ -280,6 +285,11 @@ def _settings_build_values(
280285
cli_implicit_flags = (
281286
_cli_implicit_flags if _cli_implicit_flags is not None else self.model_config.get('cli_implicit_flags')
282287
)
288+
cli_ignore_unknown_args = (
289+
_cli_ignore_unknown_args
290+
if _cli_ignore_unknown_args is not None
291+
else self.model_config.get('cli_ignore_unknown_args')
292+
)
283293

284294
secrets_dir = _secrets_dir if _secrets_dir is not None else self.model_config.get('secrets_dir')
285295

@@ -339,6 +349,7 @@ def _settings_build_values(
339349
cli_exit_on_error=cli_exit_on_error,
340350
cli_prefix=cli_prefix,
341351
cli_implicit_flags=cli_implicit_flags,
352+
cli_ignore_unknown_args=cli_ignore_unknown_args,
342353
case_sensitive=case_sensitive,
343354
)
344355
if cli_settings_source is None
@@ -388,6 +399,7 @@ def _settings_build_values(
388399
cli_exit_on_error=True,
389400
cli_prefix='',
390401
cli_implicit_flags=False,
402+
cli_ignore_unknown_args=False,
391403
json_file=None,
392404
json_file_encoding=None,
393405
yaml_file=None,

pydantic_settings/sources.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,7 @@ class CliSettingsSource(EnvSettingsSource, Generic[T]):
10281028
cli_prefix: Prefix for command line arguments added under the root parser. Defaults to "".
10291029
cli_implicit_flags: Whether `bool` fields should be implicitly converted into CLI boolean flags.
10301030
(e.g. --flag, --no-flag). Defaults to `False`.
1031+
cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`.
10311032
case_sensitive: Whether CLI "--arg" names should be read with case-sensitivity. Defaults to `True`.
10321033
Note: Case-insensitive matching is only supported on the internal root parser and does not apply to CLI
10331034
subcommands.
@@ -1056,9 +1057,10 @@ def __init__(
10561057
cli_exit_on_error: bool | None = None,
10571058
cli_prefix: str | None = None,
10581059
cli_implicit_flags: bool | None = None,
1060+
cli_ignore_unknown_args: bool | None = None,
10591061
case_sensitive: bool | None = True,
10601062
root_parser: Any = None,
1061-
parse_args_method: Callable[..., Any] | None = ArgumentParser.parse_args,
1063+
parse_args_method: Callable[..., Any] | None = None,
10621064
add_argument_method: Callable[..., Any] | None = ArgumentParser.add_argument,
10631065
add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group,
10641066
add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser,
@@ -1104,6 +1106,11 @@ def __init__(
11041106
if cli_implicit_flags is not None
11051107
else settings_cls.model_config.get('cli_implicit_flags', False)
11061108
)
1109+
self.cli_ignore_unknown_args = (
1110+
cli_ignore_unknown_args
1111+
if cli_ignore_unknown_args is not None
1112+
else settings_cls.model_config.get('cli_ignore_unknown_args', False)
1113+
)
11071114

11081115
case_sensitive = case_sensitive if case_sensitive is not None else True
11091116
if not case_sensitive and root_parser is not None:
@@ -1519,14 +1526,19 @@ def none_parser_method(*args: Any, **kwargs: Any) -> Any:
15191526
def _connect_root_parser(
15201527
self,
15211528
root_parser: T,
1522-
parse_args_method: Callable[..., Any] | None = ArgumentParser.parse_args,
1529+
parse_args_method: Callable[..., Any] | None,
15231530
add_argument_method: Callable[..., Any] | None = ArgumentParser.add_argument,
15241531
add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group,
15251532
add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser,
15261533
add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers,
15271534
formatter_class: Any = RawDescriptionHelpFormatter,
15281535
) -> None:
1536+
def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace:
1537+
return ArgumentParser.parse_known_args(*args, **kwargs)[0]
1538+
15291539
self._root_parser = root_parser
1540+
if parse_args_method is None:
1541+
parse_args_method = _parse_known_args if self.cli_ignore_unknown_args else ArgumentParser.parse_args
15301542
self._parse_args = self._connect_parser_method(parse_args_method, 'parsed_args_method')
15311543
self._add_argument = self._connect_parser_method(add_argument_method, 'add_argument_method')
15321544
self._add_argument_group = self._connect_parser_method(add_argument_group_method, 'add_argument_group_method')

tests/test_settings.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3981,6 +3981,18 @@ class Settings(BaseSettings, cli_parse_args=True): ...
39813981
Settings(_cli_exit_on_error=False)
39823982

39833983

3984+
def test_cli_ignore_unknown_args():
3985+
class Cfg(BaseSettings, cli_ignore_unknown_args=True):
3986+
this: str = 'hello'
3987+
that: int = 123
3988+
3989+
cfg = Cfg(_cli_parse_args=['not_my_positional_arg', '--not-my-optional-arg=456'])
3990+
assert cfg.model_dump() == {'this': 'hello', 'that': 123}
3991+
3992+
cfg = Cfg(_cli_parse_args=['not_my_positional_arg', '--not-my-optional-arg=456', '--this=goodbye', '--that=789'])
3993+
assert cfg.model_dump() == {'this': 'goodbye', 'that': 789}
3994+
3995+
39843996
@pytest.mark.parametrize('parser_type', [pytest.Parser, argparse.ArgumentParser, CliDummyParser])
39853997
@pytest.mark.parametrize('prefix', ['', 'cfg'])
39863998
def test_cli_user_settings_source(parser_type, prefix):

0 commit comments

Comments
 (0)