Skip to content

Commit

Permalink
Fix nested model problem when case_sensitive=False (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
hramezani authored Apr 27, 2023
1 parent 0ac8e81 commit e10b1c9
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 1 deletion.
66 changes: 65 additions & 1 deletion pydantic_settings/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,65 @@ def _extract_field_info(self, field: FieldInfo, field_name: str) -> List[Tuple[s

return field_info

def _replace_field_names_case_insensitively(self, field: FieldInfo, field_values: Dict[str, Any]) -> Dict[str, Any]:
"""
Replace field names in values dict by looking in models fields insensitively.
By having the following models:
```py
class SubSubSub(BaseModel):
VaL3: str
class SubSub(BaseModel):
Val2: str
SUB_sub_SuB: SubSubSub
class Sub(BaseModel):
VAL1: str
SUB_sub: SubSub
class Settings(BaseSettings):
nested: Sub
model_config = ConfigDict(env_nested_delimiter='__')
```
Then:
_replace_field_names_case_insensitively(
field,
{"val1": "v1", "sub_SUB": {"VAL2": "v2", "sub_SUB_sUb": {"vAl3": "v3"}}}
)
Returns {'VAL1': 'v1', 'SUB_sub': {'Val2': 'v2', 'SUB_sub_SuB': {'VaL3': 'v3'}}}
"""
values: Dict[str, Any] = {}

for name, value in field_values.items():
sub_model_field: Optional[FieldInfo] = None

# This is here to make mypy happy
# Item "None" of "Optional[Type[Any]]" has no attribute "model_fields"
if not field.annotation or not hasattr(field.annotation, 'model_fields'):
values[name] = value
continue

# Find field in sub model by looking in fields case insensitively
for sub_model_field_name, f in field.annotation.model_fields.items():
if not f.validation_alias and sub_model_field_name.lower() == name.lower():
sub_model_field = f
break

if not sub_model_field:
values[name] = value
continue

if lenient_issubclass(sub_model_field.annotation, BaseModel):
values[sub_model_field_name] = self._replace_field_names_case_insensitively(sub_model_field, value)
else:
values[sub_model_field_name] = value

return values

def __call__(self) -> Dict[str, Any]:
d: Dict[str, Any] = {}

Expand All @@ -163,7 +222,10 @@ def __call__(self) -> Dict[str, Any]:
) from e

if field_value is not None:
d[field_key] = field_value
if not self.config.get('case_sensitive', False) and lenient_issubclass(field.annotation, BaseModel):
d[field_key] = self._replace_field_names_case_insensitively(field, field_value)
else:
d[field_key] = field_value

return d

Expand Down Expand Up @@ -300,6 +362,7 @@ def next_field(field: Optional[FieldInfo], key: str) -> Optional[FieldInfo]:
By having the following models:
```py
class SubSubModel(BaseSettings):
dvals: Dict
Expand All @@ -309,6 +372,7 @@ class SubModel(BaseSettings):
class Cfg(BaseSettings):
sub_model: SubModel
```
Then:
next_field(sub_model, 'vals') Returns the `vals` field of `SubModel` class
Expand Down
26 changes: 26 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1486,3 +1486,29 @@ class Cfg(BaseSettings):
env.set('cfg_sub_model__vals', '{"invalid": dict}')
with pytest.raises(ValidationError):
Cfg()


def test_nested_model_case_insensitive(env):
class SubSubSub(BaseModel):
VaL3: str
val4: str = Field(validation_alias='VAL4')

class SubSub(BaseModel):
Val2: str
SUB_sub_SuB: SubSubSub

class Sub(BaseModel):
VAL1: str
SUB_sub: SubSub

class Settings(BaseSettings):
nested: Sub

model_config = ConfigDict(env_nested_delimiter='__')

env.set('nested', '{"val1": "v1", "sub_SUB": {"VAL2": "v2", "sub_SUB_sUb": {"vAl3": "v3", "VAL4": "v4"}}}')
s = Settings()
assert s.nested.VAL1 == 'v1'
assert s.nested.SUB_sub.Val2 == 'v2'
assert s.nested.SUB_sub.SUB_sub_SuB.VaL3 == 'v3'
assert s.nested.SUB_sub.SUB_sub_SuB.val4 == 'v4'

0 comments on commit e10b1c9

Please sign in to comment.