Skip to content

Commit 235dd02

Browse files
kschwabhramezani
andauthored
Parse enum fixes. (#367)
Co-authored-by: Hasan Ramezani <[email protected]>
1 parent 462d261 commit 235dd02

File tree

2 files changed

+71
-9
lines changed

2 files changed

+71
-9
lines changed

pydantic_settings/sources.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -678,9 +678,9 @@ def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, val
678678
ValuesError: When There is an error in deserializing value for complex field.
679679
"""
680680
is_complex, allow_parse_failure = self._field_is_complex(field)
681-
if self.env_parse_enums and lenient_issubclass(field.annotation, Enum):
682-
if value in tuple(val.name for val in field.annotation): # type: ignore
683-
value = field.annotation[value] # type: ignore
681+
if self.env_parse_enums:
682+
enum_val = _annotation_enum_name_to_val(field.annotation, value)
683+
value = value if enum_val is None else enum_val
684684

685685
if is_complex or value_is_complex:
686686
if isinstance(value, EnvNoneType):
@@ -1765,7 +1765,8 @@ def _help_format(self, field_name: str, field_info: FieldInfo, model_default: An
17651765
elif model_default not in (PydanticUndefined, None) and callable(model_default):
17661766
default = f'(default factory: {self._metavar_format(model_default)})'
17671767
elif field_info.default not in (PydanticUndefined, None):
1768-
default = f'(default: {field_info.default})'
1768+
enum_name = _annotation_enum_val_to_name(field_info.annotation, field_info.default)
1769+
default = f'(default: {field_info.default if enum_name is None else enum_name})'
17691770
elif field_info.default_factory is not None:
17701771
default = f'(default: {field_info.default_factory})'
17711772
_help += f' {default}' if _help else default
@@ -2075,3 +2076,19 @@ def _strip_annotated(annotation: Any) -> Any:
20752076
while get_origin(annotation) == Annotated:
20762077
annotation = get_args(annotation)[0]
20772078
return annotation
2079+
2080+
2081+
def _annotation_enum_val_to_name(annotation: type[Any] | None, value: Any) -> Optional[str]:
2082+
for type_ in (annotation, get_origin(annotation), *get_args(annotation)):
2083+
if lenient_issubclass(type_, Enum):
2084+
if value in tuple(val.value for val in type_):
2085+
return type_(value).name
2086+
return None
2087+
2088+
2089+
def _annotation_enum_name_to_val(annotation: type[Any] | None, name: Any) -> Any:
2090+
for type_ in (annotation, get_origin(annotation), *get_args(annotation)):
2091+
if lenient_issubclass(type_, Enum):
2092+
if name in tuple(val.name for val in type_):
2093+
return type_[name]
2094+
return None

tests/test_settings.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,9 +2116,11 @@ class Settings(BaseSettings):
21162116
def test_env_parse_enums(env):
21172117
class Settings(BaseSettings):
21182118
fruit: FruitsEnum
2119+
union_fruit: Optional[Union[int, FruitsEnum]] = None
21192120

21202121
with pytest.raises(ValidationError) as exc_info:
21212122
env.set('FRUIT', 'kiwi')
2123+
env.set('UNION_FRUIT', 'kiwi')
21222124
s = Settings()
21232125
assert exc_info.value.errors(include_url=False) == [
21242126
{
@@ -2127,18 +2129,42 @@ class Settings(BaseSettings):
21272129
'msg': 'Input should be 0, 1 or 2',
21282130
'input': 'kiwi',
21292131
'ctx': {'expected': '0, 1 or 2'},
2130-
}
2132+
},
2133+
{
2134+
'input': 'kiwi',
2135+
'loc': (
2136+
'union_fruit',
2137+
'int',
2138+
),
2139+
'msg': 'Input should be a valid integer, unable to parse string as an integer',
2140+
'type': 'int_parsing',
2141+
},
2142+
{
2143+
'ctx': {
2144+
'expected': '0, 1 or 2',
2145+
},
2146+
'input': 'kiwi',
2147+
'loc': (
2148+
'union_fruit',
2149+
'int-enum[FruitsEnum]',
2150+
),
2151+
'msg': 'Input should be 0, 1 or 2',
2152+
'type': 'enum',
2153+
},
21312154
]
21322155

21332156
env.set('FRUIT', str(FruitsEnum.lime.value))
2157+
env.set('UNION_FRUIT', str(FruitsEnum.lime.value))
21342158
s = Settings()
21352159
assert s.fruit == FruitsEnum.lime
21362160

21372161
env.set('FRUIT', 'kiwi')
2162+
env.set('UNION_FRUIT', 'kiwi')
21382163
s = Settings(_env_parse_enums=True)
21392164
assert s.fruit == FruitsEnum.kiwi
21402165

21412166
env.set('FRUIT', str(FruitsEnum.lime.value))
2167+
env.set('UNION_FRUIT', str(FruitsEnum.lime.value))
21422168
s = Settings(_env_parse_enums=True)
21432169
assert s.fruit == FruitsEnum.lime
21442170

@@ -3129,17 +3155,18 @@ class Cfg(BaseSettings):
31293155
assert cfg.model_dump() == {'child': {'name': 'new name a', 'diff_a': 'new diff a'}}
31303156

31313157

3132-
def test_cli_enums():
3158+
def test_cli_enums(capsys, monkeypatch):
31333159
class Pet(IntEnum):
31343160
dog = 0
31353161
cat = 1
31363162
bird = 2
31373163

31383164
class Cfg(BaseSettings):
3139-
pet: Pet
3165+
pet: Pet = Pet.dog
3166+
union_pet: Union[Pet, int] = 43
31403167

3141-
cfg = Cfg(_cli_parse_args=['--pet', 'cat'])
3142-
assert cfg.model_dump() == {'pet': Pet.cat}
3168+
cfg = Cfg(_cli_parse_args=['--pet', 'cat', '--union_pet', 'dog'])
3169+
assert cfg.model_dump() == {'pet': Pet.cat, 'union_pet': Pet.dog}
31433170

31443171
with pytest.raises(ValidationError) as exc_info:
31453172
Cfg(_cli_parse_args=['--pet', 'rock'])
@@ -3153,6 +3180,24 @@ class Cfg(BaseSettings):
31533180
}
31543181
]
31553182

3183+
with monkeypatch.context() as m:
3184+
m.setattr(sys, 'argv', ['example.py', '--help'])
3185+
3186+
with pytest.raises(SystemExit):
3187+
Cfg(_cli_parse_args=True)
3188+
assert (
3189+
capsys.readouterr().out
3190+
== f"""usage: example.py [-h] [--pet {{dog,cat,bird}}]
3191+
[--union_pet {{{{dog,cat,bird}},int}}]
3192+
3193+
{ARGPARSE_OPTIONS_TEXT}:
3194+
-h, --help show this help message and exit
3195+
--pet {{dog,cat,bird}} (default: dog)
3196+
--union_pet {{{{dog,cat,bird}},int}}
3197+
(default: 43)
3198+
"""
3199+
)
3200+
31563201

31573202
def test_cli_literals():
31583203
class Cfg(BaseSettings):

0 commit comments

Comments
 (0)