Skip to content

Commit b2e84c2

Browse files
authored
Fix issue with nested model uppercase field name in case insensitive mode (#309)
1 parent 0a9faca commit b2e84c2

File tree

2 files changed

+31
-6
lines changed

2 files changed

+31
-6
lines changed

pydantic_settings/sources.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -597,8 +597,11 @@ def _field_is_complex(self, field: FieldInfo) -> tuple[bool, bool]:
597597

598598
return True, allow_parse_failure
599599

600+
# Default value of `case_sensitive` is `None`, because we don't want to break existing behavior.
601+
# We have to change the method to a non-static method and use
602+
# `self.case_sensitive` instead in V3.
600603
@staticmethod
601-
def next_field(field: FieldInfo | Any | None, key: str) -> FieldInfo | None:
604+
def next_field(field: FieldInfo | Any | None, key: str, case_sensitive: bool | None = None) -> FieldInfo | None:
602605
"""
603606
Find the field in a sub model by key(env name)
604607
@@ -623,6 +626,7 @@ class Cfg(BaseSettings):
623626
Args:
624627
field: The field.
625628
key: The key (env name).
629+
case_sensitive: Whether to search for key case sensitively.
626630
627631
Returns:
628632
Field if it finds the next field otherwise `None`.
@@ -633,11 +637,18 @@ class Cfg(BaseSettings):
633637
annotation = field.annotation if isinstance(field, FieldInfo) else field
634638
if origin_is_union(get_origin(annotation)) or isinstance(annotation, WithArgsTypes):
635639
for type_ in get_args(annotation):
636-
type_has_key = EnvSettingsSource.next_field(type_, key)
640+
type_has_key = EnvSettingsSource.next_field(type_, key, case_sensitive)
637641
if type_has_key:
638642
return type_has_key
639-
elif is_model_class(annotation) and annotation.model_fields.get(key):
640-
return annotation.model_fields[key]
643+
elif is_model_class(annotation):
644+
# `case_sensitive is None` is here to be compatible with the old behavior.
645+
# Has to be removed in V3.
646+
if (case_sensitive is None or case_sensitive) and annotation.model_fields.get(key):
647+
return annotation.model_fields[key]
648+
elif not case_sensitive:
649+
for field_name, f in annotation.model_fields.items():
650+
if field_name.lower() == key.lower():
651+
return f
641652

642653
return None
643654

@@ -670,12 +681,12 @@ def explode_env_vars(self, field_name: str, field: FieldInfo, env_vars: Mapping[
670681
env_var = result
671682
target_field: FieldInfo | None = field
672683
for key in keys:
673-
target_field = self.next_field(target_field, key)
684+
target_field = self.next_field(target_field, key, self.case_sensitive)
674685
if isinstance(env_var, dict):
675686
env_var = env_var.setdefault(key, {})
676687

677688
# get proper field with last_key
678-
target_field = self.next_field(target_field, last_key)
689+
target_field = self.next_field(target_field, last_key, self.case_sensitive)
679690

680691
# check if env_val maps to a complex field and if so, parse the env_val
681692
if (target_field or is_dict) and env_val:

tests/test_settings.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3859,3 +3859,17 @@ class Settings(BaseSettings):
38593859
env.set('nested__bar', '123')
38603860
s = Settings()
38613861
assert s.model_dump() == {'nested': {'BaR': 123, 'FOO': 'string'}}
3862+
3863+
3864+
def test_case_insensitive_nested_list(env):
3865+
class NestedSettings(BaseModel):
3866+
FOO: List[str]
3867+
3868+
class Settings(BaseSettings):
3869+
model_config = SettingsConfigDict(env_nested_delimiter='__', case_sensitive=False)
3870+
3871+
nested: Optional[NestedSettings]
3872+
3873+
env.set('nested__FOO', '["string1", "string2"]')
3874+
s = Settings()
3875+
assert s.model_dump() == {'nested': {'FOO': ['string1', 'string2']}}

0 commit comments

Comments
 (0)