Skip to content

Commit 43f3beb

Browse files
committed
Initial commit.
1 parent d54d146 commit 43f3beb

File tree

5 files changed

+34
-11
lines changed

5 files changed

+34
-11
lines changed

docs/index.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,24 +1309,25 @@ class ImplicitSettings(BaseSettings, cli_parse_args=True, cli_implicit_flags=Tru
13091309
"""
13101310
```
13111311

1312-
#### Ignore Unknown Arguments
1312+
#### Ignore and Retrieve Unknown Arguments
13131313

13141314
Change whether to ignore unknown CLI arguments and only parse known ones using `cli_ignore_unknown_args`. By default, the CLI
1315-
does not ignore any args.
1315+
does not ignore any args. Ignored arguments can then be retrieved using the `CliUnknownArgs` annotation.
13161316

13171317
```py
13181318
import sys
13191319

1320-
from pydantic_settings import BaseSettings
1320+
from pydantic_settings import BaseSettings, CliUnknownArgs
13211321

13221322

13231323
class Settings(BaseSettings, cli_parse_args=True, cli_ignore_unknown_args=True):
13241324
good_arg: str
1325+
ignored_args: CliUnknownArgs
13251326

13261327

13271328
sys.argv = ['example.py', '--bad-arg=bad', 'ANOTHER_BAD_ARG', '--good_arg=hello world']
13281329
print(Settings().model_dump())
1329-
#> {'good_arg': 'hello world'}
1330+
#> {'good_arg': 'hello world', 'ignored_args': ['--bad-arg=bad', 'ANOTHER_BAD_ARG']}
13301331
```
13311332

13321333
#### CLI Kebab Case for Arguments

pydantic_settings/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
CliSettingsSource,
1212
CliSubCommand,
1313
CliSuppress,
14+
CliUnknownArgs,
1415
DotEnvSettingsSource,
1516
EnvSettingsSource,
1617
ForceDecode,
@@ -39,6 +40,7 @@
3940
'CliSettingsSource',
4041
'CliSubCommand',
4142
'CliSuppress',
43+
'CliUnknownArgs',
4244
'DotEnvSettingsSource',
4345
'EnvSettingsSource',
4446
'ForceDecode',

pydantic_settings/sources/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
CliSettingsSource,
1919
CliSubCommand,
2020
CliSuppress,
21+
CliUnknownArgs,
2122
)
2223
from .providers.dotenv import DotEnvSettingsSource, read_env_file
2324
from .providers.env import EnvSettingsSource
@@ -40,6 +41,7 @@
4041
'CliSettingsSource',
4142
'CliSubCommand',
4243
'CliSuppress',
44+
'CliUnknownArgs',
4345
'DefaultSettingsSource',
4446
'DotEnvSettingsSource',
4547
'DotenvType',

pydantic_settings/sources/providers/cli.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
)
3636

3737
import typing_extensions
38-
from pydantic import BaseModel
38+
from pydantic import BaseModel, Field
3939
from pydantic._internal._repr import Representation
4040
from pydantic._internal._utils import is_model_class
4141
from pydantic.dataclasses import is_pydantic_dataclass
@@ -47,7 +47,7 @@
4747

4848
from ...exceptions import SettingsError
4949
from ...utils import _lenient_issubclass, _WithArgsTypes
50-
from ..types import _CliExplicitFlag, _CliImplicitFlag, _CliPositionalArg, _CliSubCommand
50+
from ..types import NoDecode, _CliExplicitFlag, _CliImplicitFlag, _CliPositionalArg, _CliSubCommand, _CliUnknownArgs
5151
from ..utils import (
5252
_annotation_contains_types,
5353
_annotation_enum_val_to_name,
@@ -86,6 +86,7 @@ class CliMutuallyExclusiveGroup(BaseModel):
8686
CliExplicitFlag = Annotated[_CliBoolFlag, _CliExplicitFlag]
8787
CLI_SUPPRESS = SUPPRESS
8888
CliSuppress = Annotated[T, CLI_SUPPRESS]
89+
CliUnknownArgs = Annotated[list[str], Field(default=[]), _CliUnknownArgs, NoDecode]
8990

9091

9192
class CliSettingsSource(EnvSettingsSource, Generic[T]):
@@ -364,6 +365,8 @@ def _load_env_vars(
364365
if not any(field_name for field_name in parsed_args.keys() if f'{last_selected_subcommand}.' in field_name):
365366
parsed_args[last_selected_subcommand] = '{}'
366367

368+
parsed_args.update(self._cli_unknown_args)
369+
367370
self.env_vars = parse_env_vars(
368371
cast(Mapping[str, str], parsed_args),
369372
self.case_sensitive,
@@ -630,8 +633,13 @@ def _connect_root_parser(
630633
add_subparsers_method: Callable[..., Any] | None = ArgumentParser.add_subparsers,
631634
formatter_class: Any = RawDescriptionHelpFormatter,
632635
) -> None:
636+
self._cli_unknown_args: dict[str, list[str]] = {}
637+
633638
def _parse_known_args(*args: Any, **kwargs: Any) -> Namespace:
634-
return ArgumentParser.parse_known_args(*args, **kwargs)[0]
639+
args, unknown_args = ArgumentParser.parse_known_args(*args, **kwargs)
640+
for dest in self._cli_unknown_args:
641+
self._cli_unknown_args[dest] = unknown_args
642+
return cast(Namespace, args)
635643

636644
self._root_parser = root_parser
637645
if parse_args_method is None:
@@ -755,10 +763,7 @@ def _add_parser_args(
755763
if not arg_names or (kwargs['dest'] in added_args):
756764
continue
757765

758-
if is_append_action:
759-
kwargs['action'] = 'append'
760-
if _annotation_contains_types(field_info.annotation, (dict, Mapping), is_strip_annotated=True):
761-
self._cli_dict_args[kwargs['dest']] = field_info.annotation
766+
self._convert_append_action(kwargs, field_info, is_append_action)
762767

763768
if _CliPositionalArg in field_info.metadata:
764769
arg_names, flag_prefix = self._convert_positional_arg(
@@ -783,6 +788,8 @@ def _add_parser_args(
783788
alias_names,
784789
model_default=model_default,
785790
)
791+
elif _CliUnknownArgs in field_info.metadata:
792+
self._cli_unknown_args[kwargs['dest']] = []
786793
elif not is_alias_path_only:
787794
if group is not None:
788795
if isinstance(group, dict):
@@ -805,6 +812,12 @@ def _check_kebab_name(self, name: str) -> str:
805812
return name.replace('_', '-')
806813
return name
807814

815+
def _convert_append_action(self, kwargs: dict[str, Any], field_info: FieldInfo, is_append_action: bool) -> None:
816+
if is_append_action:
817+
kwargs['action'] = 'append'
818+
if _annotation_contains_types(field_info.annotation, (dict, Mapping), is_strip_annotated=True):
819+
self._cli_dict_args[kwargs['dest']] = field_info.annotation
820+
808821
def _convert_bool_flag(self, kwargs: dict[str, Any], field_info: FieldInfo, model_default: Any) -> None:
809822
if kwargs['metavar'] == 'bool':
810823
if (self.cli_implicit_flags or _CliImplicitFlag in field_info.metadata) and (

pydantic_settings/sources/types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ class _CliExplicitFlag:
5757
pass
5858

5959

60+
class _CliUnknownArgs:
61+
pass
62+
63+
6064
__all__ = [
6165
'DEFAULT_PATH',
6266
'ENV_FILE_SENTINEL',
@@ -70,4 +74,5 @@ class _CliExplicitFlag:
7074
'_CliImplicitFlag',
7175
'_CliPositionalArg',
7276
'_CliSubCommand',
77+
'_CliUnknownArgs',
7378
]

0 commit comments

Comments
 (0)