Skip to content

Commit 4840d69

Browse files
kschwabhramezani
andauthoredJul 19, 2024
Add cli_exit_on_error config option (#340)
Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
1 parent bcbdd2a commit 4840d69

File tree

5 files changed

+131
-65
lines changed

5 files changed

+131
-65
lines changed
 

‎docs/index.md

+31-8
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,28 @@ options:
868868
"""
869869
```
870870

871+
#### Change Whether CLI Should Exit on Error
872+
873+
Change whether the CLI internal parser will exit on error or raise a `SettingsError` exception by using
874+
`cli_exit_on_error`. By default, the CLI internal parser will exit on error.
875+
876+
```py
877+
import sys
878+
879+
from pydantic_settings import BaseSettings, SettingsError
880+
881+
882+
class Settings(BaseSettings, cli_parse_args=True, cli_exit_on_error=False): ...
883+
884+
885+
try:
886+
sys.argv = ['example.py', '--bad-arg']
887+
Settings()
888+
except SettingsError as e:
889+
print(e)
890+
#> error parsing CLI: unrecognized arguments: --bad-arg
891+
```
892+
871893
#### Enforce Required Arguments at CLI
872894

873895
Pydantic settings is designed to pull values in from various sources when instantating a model. This means a field that
@@ -884,10 +906,15 @@ import sys
884906

885907
from pydantic import Field
886908

887-
from pydantic_settings import BaseSettings
909+
from pydantic_settings import BaseSettings, SettingsError
888910

889911

890-
class Settings(BaseSettings, cli_parse_args=True, cli_enforce_required=True):
912+
class Settings(
913+
BaseSettings,
914+
cli_parse_args=True,
915+
cli_enforce_required=True,
916+
cli_exit_on_error=False,
917+
):
891918
my_required_field: str = Field(description='a top level required field')
892919

893920

@@ -896,13 +923,9 @@ os.environ['MY_REQUIRED_FIELD'] = 'hello from environment'
896923
try:
897924
sys.argv = ['example.py']
898925
Settings()
899-
except SystemExit as e:
926+
except SettingsError as e:
900927
print(e)
901-
#> 2
902-
"""
903-
usage: example.py [-h] --my_required_field str
904-
example.py: error: the following arguments are required: --my_required_field
905-
"""
928+
#> error parsing CLI: the following arguments are required: --my_required_field
906929
```
907930

908931
#### Change the None Type Parse String

‎pydantic_settings/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
PydanticBaseSettingsSource,
1212
PyprojectTomlConfigSettingsSource,
1313
SecretsSettingsSource,
14+
SettingsError,
1415
TomlConfigSettingsSource,
1516
YamlConfigSettingsSource,
1617
)
@@ -29,6 +30,7 @@
2930
'PydanticBaseSettingsSource',
3031
'SecretsSettingsSource',
3132
'SettingsConfigDict',
33+
'SettingsError',
3234
'TomlConfigSettingsSource',
3335
'YamlConfigSettingsSource',
3436
'AzureKeyVaultSettingsSource',

‎pydantic_settings/main.py

+11
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class SettingsConfigDict(ConfigDict, total=False):
3838
cli_avoid_json: bool
3939
cli_enforce_required: bool
4040
cli_use_class_docs_for_groups: bool
41+
cli_exit_on_error: bool
4142
cli_prefix: str
4243
secrets_dir: str | Path | None
4344
json_file: PathType | None
@@ -110,6 +111,8 @@ class BaseSettings(BaseModel):
110111
_cli_enforce_required: Enforce required fields at the CLI. Defaults to `False`.
111112
_cli_use_class_docs_for_groups: Use class docstrings in CLI group help text instead of field descriptions.
112113
Defaults to `False`.
114+
_cli_exit_on_error: Determines whether or not the internal parser exits with error info when an error occurs.
115+
Defaults to `True`.
113116
_cli_prefix: The root parser command line arguments prefix. Defaults to "".
114117
_secrets_dir: The secret files directory. Defaults to `None`.
115118
"""
@@ -132,6 +135,7 @@ def __init__(
132135
_cli_avoid_json: bool | None = None,
133136
_cli_enforce_required: bool | None = None,
134137
_cli_use_class_docs_for_groups: bool | None = None,
138+
_cli_exit_on_error: bool | None = None,
135139
_cli_prefix: str | None = None,
136140
_secrets_dir: str | Path | None = None,
137141
**values: Any,
@@ -156,6 +160,7 @@ def __init__(
156160
_cli_avoid_json=_cli_avoid_json,
157161
_cli_enforce_required=_cli_enforce_required,
158162
_cli_use_class_docs_for_groups=_cli_use_class_docs_for_groups,
163+
_cli_exit_on_error=_cli_exit_on_error,
159164
_cli_prefix=_cli_prefix,
160165
_secrets_dir=_secrets_dir,
161166
)
@@ -204,6 +209,7 @@ def _settings_build_values(
204209
_cli_avoid_json: bool | None = None,
205210
_cli_enforce_required: bool | None = None,
206211
_cli_use_class_docs_for_groups: bool | None = None,
212+
_cli_exit_on_error: bool | None = None,
207213
_cli_prefix: str | None = None,
208214
_secrets_dir: str | Path | None = None,
209215
) -> dict[str, Any]:
@@ -250,6 +256,9 @@ def _settings_build_values(
250256
if _cli_use_class_docs_for_groups is not None
251257
else self.model_config.get('cli_use_class_docs_for_groups')
252258
)
259+
cli_exit_on_error = (
260+
_cli_exit_on_error if _cli_exit_on_error is not None else self.model_config.get('cli_exit_on_error')
261+
)
253262
cli_prefix = _cli_prefix if _cli_prefix is not None else self.model_config.get('cli_prefix')
254263

255264
secrets_dir = _secrets_dir if _secrets_dir is not None else self.model_config.get('secrets_dir')
@@ -300,6 +309,7 @@ def _settings_build_values(
300309
cli_avoid_json=cli_avoid_json,
301310
cli_enforce_required=cli_enforce_required,
302311
cli_use_class_docs_for_groups=cli_use_class_docs_for_groups,
312+
cli_exit_on_error=cli_exit_on_error,
303313
cli_prefix=cli_prefix,
304314
case_sensitive=case_sensitive,
305315
)
@@ -346,6 +356,7 @@ def _settings_build_values(
346356
cli_avoid_json=False,
347357
cli_enforce_required=False,
348358
cli_use_class_docs_for_groups=False,
359+
cli_exit_on_error=True,
349360
cli_prefix='',
350361
json_file=None,
351362
json_file_encoding=None,

‎pydantic_settings/sources.py

+24-6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
Iterator,
2323
List,
2424
Mapping,
25+
NoReturn,
2526
Optional,
2627
Sequence,
2728
Tuple,
@@ -110,6 +111,10 @@ def import_azure_key_vault() -> None:
110111
ENV_FILE_SENTINEL: DotenvType = Path('')
111112

112113

114+
class SettingsError(ValueError):
115+
pass
116+
117+
113118
class _CliSubCommand:
114119
pass
115120

@@ -119,7 +124,14 @@ class _CliPositionalArg:
119124

120125

121126
class _CliInternalArgParser(ArgumentParser):
122-
pass
127+
def __init__(self, cli_exit_on_error: bool = True, **kwargs: Any) -> None:
128+
super().__init__(**kwargs)
129+
self._cli_exit_on_error = cli_exit_on_error
130+
131+
def error(self, message: str) -> NoReturn:
132+
if not self._cli_exit_on_error:
133+
raise SettingsError(f'error parsing CLI: {message}')
134+
super().error(message)
123135

124136

125137
T = TypeVar('T')
@@ -131,10 +143,6 @@ class EnvNoneType(str):
131143
pass
132144

133145

134-
class SettingsError(ValueError):
135-
pass
136-
137-
138146
class PydanticBaseSettingsSource(ABC):
139147
"""
140148
Abstract base class for settings sources, every settings source classes should inherit from it.
@@ -893,6 +901,8 @@ class CliSettingsSource(EnvSettingsSource, Generic[T]):
893901
cli_enforce_required: Enforce required fields at the CLI. Defaults to `False`.
894902
cli_use_class_docs_for_groups: Use class docstrings in CLI group help text instead of field descriptions.
895903
Defaults to `False`.
904+
cli_exit_on_error: Determines whether or not the internal parser exits with error info when an error occurs.
905+
Defaults to `True`.
896906
cli_prefix: Prefix for command line arguments added under the root parser. Defaults to "".
897907
case_sensitive: Whether CLI "--arg" names should be read with case-sensitivity. Defaults to `True`.
898908
Note: Case-insensitive matching is only supported on the internal root parser and does not apply to CLI
@@ -919,6 +929,7 @@ def __init__(
919929
cli_avoid_json: bool | None = None,
920930
cli_enforce_required: bool | None = None,
921931
cli_use_class_docs_for_groups: bool | None = None,
932+
cli_exit_on_error: bool | None = None,
922933
cli_prefix: str | None = None,
923934
case_sensitive: bool | None = True,
924935
root_parser: Any = None,
@@ -953,6 +964,11 @@ def __init__(
953964
if cli_use_class_docs_for_groups is not None
954965
else settings_cls.model_config.get('cli_use_class_docs_for_groups', False)
955966
)
967+
self.cli_exit_on_error = (
968+
cli_exit_on_error
969+
if cli_exit_on_error is not None
970+
else settings_cls.model_config.get('cli_exit_on_error', True)
971+
)
956972
self.cli_prefix = cli_prefix if cli_prefix is not None else settings_cls.model_config.get('cli_prefix', '')
957973
if self.cli_prefix:
958974
if cli_prefix.startswith('.') or cli_prefix.endswith('.') or not cli_prefix.replace('.', '').isidentifier(): # type: ignore
@@ -973,7 +989,9 @@ def __init__(
973989
)
974990

975991
root_parser = (
976-
_CliInternalArgParser(prog=self.cli_prog_name, description=settings_cls.__doc__)
992+
_CliInternalArgParser(
993+
cli_exit_on_error=self.cli_exit_on_error, prog=self.cli_prog_name, description=settings_cls.__doc__
994+
)
977995
if root_parser is None
978996
else root_parser
979997
)

0 commit comments

Comments
 (0)