Skip to content

Commit e10b1c9

Browse files
authored
Fix nested model problem when case_sensitive=False (#34)
1 parent 0ac8e81 commit e10b1c9

File tree

2 files changed

+91
-1
lines changed

2 files changed

+91
-1
lines changed

pydantic_settings/sources.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,65 @@ def _extract_field_info(self, field: FieldInfo, field_name: str) -> List[Tuple[s
144144

145145
return field_info
146146

147+
def _replace_field_names_case_insensitively(self, field: FieldInfo, field_values: Dict[str, Any]) -> Dict[str, Any]:
148+
"""
149+
Replace field names in values dict by looking in models fields insensitively.
150+
151+
By having the following models:
152+
153+
```py
154+
class SubSubSub(BaseModel):
155+
VaL3: str
156+
157+
class SubSub(BaseModel):
158+
Val2: str
159+
SUB_sub_SuB: SubSubSub
160+
161+
class Sub(BaseModel):
162+
VAL1: str
163+
SUB_sub: SubSub
164+
165+
class Settings(BaseSettings):
166+
nested: Sub
167+
168+
model_config = ConfigDict(env_nested_delimiter='__')
169+
```
170+
171+
Then:
172+
_replace_field_names_case_insensitively(
173+
field,
174+
{"val1": "v1", "sub_SUB": {"VAL2": "v2", "sub_SUB_sUb": {"vAl3": "v3"}}}
175+
)
176+
Returns {'VAL1': 'v1', 'SUB_sub': {'Val2': 'v2', 'SUB_sub_SuB': {'VaL3': 'v3'}}}
177+
"""
178+
values: Dict[str, Any] = {}
179+
180+
for name, value in field_values.items():
181+
sub_model_field: Optional[FieldInfo] = None
182+
183+
# This is here to make mypy happy
184+
# Item "None" of "Optional[Type[Any]]" has no attribute "model_fields"
185+
if not field.annotation or not hasattr(field.annotation, 'model_fields'):
186+
values[name] = value
187+
continue
188+
189+
# Find field in sub model by looking in fields case insensitively
190+
for sub_model_field_name, f in field.annotation.model_fields.items():
191+
if not f.validation_alias and sub_model_field_name.lower() == name.lower():
192+
sub_model_field = f
193+
break
194+
195+
if not sub_model_field:
196+
values[name] = value
197+
continue
198+
199+
if lenient_issubclass(sub_model_field.annotation, BaseModel):
200+
values[sub_model_field_name] = self._replace_field_names_case_insensitively(sub_model_field, value)
201+
else:
202+
values[sub_model_field_name] = value
203+
204+
return values
205+
147206
def __call__(self) -> Dict[str, Any]:
148207
d: Dict[str, Any] = {}
149208

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

165224
if field_value is not None:
166-
d[field_key] = field_value
225+
if not self.config.get('case_sensitive', False) and lenient_issubclass(field.annotation, BaseModel):
226+
d[field_key] = self._replace_field_names_case_insensitively(field, field_value)
227+
else:
228+
d[field_key] = field_value
167229

168230
return d
169231

@@ -300,6 +362,7 @@ def next_field(field: Optional[FieldInfo], key: str) -> Optional[FieldInfo]:
300362
301363
By having the following models:
302364
365+
```py
303366
class SubSubModel(BaseSettings):
304367
dvals: Dict
305368
@@ -309,6 +372,7 @@ class SubModel(BaseSettings):
309372
310373
class Cfg(BaseSettings):
311374
sub_model: SubModel
375+
```
312376
313377
Then:
314378
next_field(sub_model, 'vals') Returns the `vals` field of `SubModel` class

tests/test_settings.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,3 +1486,29 @@ class Cfg(BaseSettings):
14861486
env.set('cfg_sub_model__vals', '{"invalid": dict}')
14871487
with pytest.raises(ValidationError):
14881488
Cfg()
1489+
1490+
1491+
def test_nested_model_case_insensitive(env):
1492+
class SubSubSub(BaseModel):
1493+
VaL3: str
1494+
val4: str = Field(validation_alias='VAL4')
1495+
1496+
class SubSub(BaseModel):
1497+
Val2: str
1498+
SUB_sub_SuB: SubSubSub
1499+
1500+
class Sub(BaseModel):
1501+
VAL1: str
1502+
SUB_sub: SubSub
1503+
1504+
class Settings(BaseSettings):
1505+
nested: Sub
1506+
1507+
model_config = ConfigDict(env_nested_delimiter='__')
1508+
1509+
env.set('nested', '{"val1": "v1", "sub_SUB": {"VAL2": "v2", "sub_SUB_sUb": {"vAl3": "v3", "VAL4": "v4"}}}')
1510+
s = Settings()
1511+
assert s.nested.VAL1 == 'v1'
1512+
assert s.nested.SUB_sub.Val2 == 'v2'
1513+
assert s.nested.SUB_sub.SUB_sub_SuB.VaL3 == 'v3'
1514+
assert s.nested.SUB_sub.SUB_sub_SuB.val4 == 'v4'

0 commit comments

Comments
 (0)