Skip to content

Commit

Permalink
Add cli_ignore_unknown_args config option. (#405)
Browse files Browse the repository at this point in the history
Co-authored-by: hyperlint-ai[bot] <154288675+hyperlint-ai[bot]@users.noreply.github.com>
  • Loading branch information
kschwab and hyperlint-ai[bot] authored Sep 17, 2024
1 parent b680531 commit 7e7ccdb
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 2 deletions.
20 changes: 20 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,26 @@ class ImplicitSettings(BaseSettings, cli_parse_args=True, cli_implicit_flags=Tru
"""
```

#### Ignore Unknown Arguments

Change whether to ignore unknown CLI arguments and only parse known ones using `cli_ignore_unknown_args`. By default, the CLI
does not ignore any args.

```py
import sys

from pydantic_settings import BaseSettings


class Settings(BaseSettings, cli_parse_args=True, cli_ignore_unknown_args=True):
good_arg: str


sys.argv = ['example.py', '--bad-arg=bad', 'ANOTHER_BAD_ARG', '--good_arg=hello world']
print(Settings().model_dump())
#> {'good_arg': 'hello world'}
```

#### Change Whether CLI Should Exit on Error

Change whether the CLI internal parser will exit on error or raise a `SettingsError` exception by using
Expand Down
12 changes: 12 additions & 0 deletions pydantic_settings/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class SettingsConfigDict(ConfigDict, total=False):
cli_exit_on_error: bool
cli_prefix: str
cli_implicit_flags: bool | None
cli_ignore_unknown_args: bool | None
secrets_dir: PathType | None
json_file: PathType | None
json_file_encoding: str | None
Expand Down Expand Up @@ -120,6 +121,7 @@ class BaseSettings(BaseModel):
_cli_prefix: The root parser command line arguments prefix. Defaults to "".
_cli_implicit_flags: Whether `bool` fields should be implicitly converted into CLI boolean flags.
(e.g. --flag, --no-flag). Defaults to `False`.
_cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`.
_secrets_dir: The secret files directory or a sequence of directories. Defaults to `None`.
"""

Expand All @@ -145,6 +147,7 @@ def __init__(
_cli_exit_on_error: bool | None = None,
_cli_prefix: str | None = None,
_cli_implicit_flags: bool | None = None,
_cli_ignore_unknown_args: bool | None = None,
_secrets_dir: PathType | None = None,
**values: Any,
) -> None:
Expand Down Expand Up @@ -172,6 +175,7 @@ def __init__(
_cli_exit_on_error=_cli_exit_on_error,
_cli_prefix=_cli_prefix,
_cli_implicit_flags=_cli_implicit_flags,
_cli_ignore_unknown_args=_cli_ignore_unknown_args,
_secrets_dir=_secrets_dir,
)
)
Expand Down Expand Up @@ -223,6 +227,7 @@ def _settings_build_values(
_cli_exit_on_error: bool | None = None,
_cli_prefix: str | None = None,
_cli_implicit_flags: bool | None = None,
_cli_ignore_unknown_args: bool | None = None,
_secrets_dir: PathType | None = None,
) -> dict[str, Any]:
# Determine settings config values
Expand Down Expand Up @@ -280,6 +285,11 @@ def _settings_build_values(
cli_implicit_flags = (
_cli_implicit_flags if _cli_implicit_flags is not None else self.model_config.get('cli_implicit_flags')
)
cli_ignore_unknown_args = (
_cli_ignore_unknown_args
if _cli_ignore_unknown_args is not None
else self.model_config.get('cli_ignore_unknown_args')
)

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

Expand Down Expand Up @@ -339,6 +349,7 @@ def _settings_build_values(
cli_exit_on_error=cli_exit_on_error,
cli_prefix=cli_prefix,
cli_implicit_flags=cli_implicit_flags,
cli_ignore_unknown_args=cli_ignore_unknown_args,
case_sensitive=case_sensitive,
)
if cli_settings_source is None
Expand Down Expand Up @@ -388,6 +399,7 @@ def _settings_build_values(
cli_exit_on_error=True,
cli_prefix='',
cli_implicit_flags=False,
cli_ignore_unknown_args=False,
json_file=None,
json_file_encoding=None,
yaml_file=None,
Expand Down
16 changes: 14 additions & 2 deletions pydantic_settings/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,7 @@ class CliSettingsSource(EnvSettingsSource, Generic[T]):
cli_prefix: Prefix for command line arguments added under the root parser. Defaults to "".
cli_implicit_flags: Whether `bool` fields should be implicitly converted into CLI boolean flags.
(e.g. --flag, --no-flag). Defaults to `False`.
cli_ignore_unknown_args: Whether to ignore unknown CLI args and parse only known ones. Defaults to `False`.
case_sensitive: Whether CLI "--arg" names should be read with case-sensitivity. Defaults to `True`.
Note: Case-insensitive matching is only supported on the internal root parser and does not apply to CLI
subcommands.
Expand Down Expand Up @@ -1056,9 +1057,10 @@ def __init__(
cli_exit_on_error: bool | None = None,
cli_prefix: str | None = None,
cli_implicit_flags: bool | None = None,
cli_ignore_unknown_args: bool | None = None,
case_sensitive: bool | None = True,
root_parser: Any = None,
parse_args_method: Callable[..., Any] | None = ArgumentParser.parse_args,
parse_args_method: Callable[..., Any] | None = None,
add_argument_method: Callable[..., Any] | None = ArgumentParser.add_argument,
add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group,
add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser,
Expand Down Expand Up @@ -1104,6 +1106,11 @@ def __init__(
if cli_implicit_flags is not None
else settings_cls.model_config.get('cli_implicit_flags', False)
)
self.cli_ignore_unknown_args = (
cli_ignore_unknown_args
if cli_ignore_unknown_args is not None
else settings_cls.model_config.get('cli_ignore_unknown_args', False)
)

case_sensitive = case_sensitive if case_sensitive is not None else True
if not case_sensitive and root_parser is not None:
Expand Down Expand Up @@ -1519,14 +1526,19 @@ def none_parser_method(*args: Any, **kwargs: Any) -> Any:
def _connect_root_parser(
self,
root_parser: T,
parse_args_method: Callable[..., Any] | None = ArgumentParser.parse_args,
parse_args_method: Callable[..., Any] | None,
add_argument_method: Callable[..., Any] | None = ArgumentParser.add_argument,
add_argument_group_method: Callable[..., Any] | None = ArgumentParser.add_argument_group,
add_parser_method: Callable[..., Any] | None = _SubParsersAction.add_parser,
add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers,
formatter_class: Any = RawDescriptionHelpFormatter,
) -> None:
def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace:
return ArgumentParser.parse_known_args(*args, **kwargs)[0]

self._root_parser = root_parser
if parse_args_method is None:
parse_args_method = _parse_known_args if self.cli_ignore_unknown_args else ArgumentParser.parse_args
self._parse_args = self._connect_parser_method(parse_args_method, 'parsed_args_method')
self._add_argument = self._connect_parser_method(add_argument_method, 'add_argument_method')
self._add_argument_group = self._connect_parser_method(add_argument_group_method, 'add_argument_group_method')
Expand Down
12 changes: 12 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3981,6 +3981,18 @@ class Settings(BaseSettings, cli_parse_args=True): ...
Settings(_cli_exit_on_error=False)


def test_cli_ignore_unknown_args():
class Cfg(BaseSettings, cli_ignore_unknown_args=True):
this: str = 'hello'
that: int = 123

cfg = Cfg(_cli_parse_args=['not_my_positional_arg', '--not-my-optional-arg=456'])
assert cfg.model_dump() == {'this': 'hello', 'that': 123}

cfg = Cfg(_cli_parse_args=['not_my_positional_arg', '--not-my-optional-arg=456', '--this=goodbye', '--that=789'])
assert cfg.model_dump() == {'this': 'goodbye', 'that': 789}


@pytest.mark.parametrize('parser_type', [pytest.Parser, argparse.ArgumentParser, CliDummyParser])
@pytest.mark.parametrize('prefix', ['', 'cfg'])
def test_cli_user_settings_source(parser_type, prefix):
Expand Down

0 comments on commit 7e7ccdb

Please sign in to comment.