Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix nested model problem when case_sensitive=False #34

Merged
merged 4 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion pydantic_settings/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,61 @@ 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:

class SubSubSub(BaseModel):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one comment (doesn't matter much here since I guess we won't add this to the documentation) - if you want code in docsting, please use markdown with code fences so they examples render properly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one. Done!

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

if not field.annotation:
values[name] = value
continue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure this is necessary? It looks like where this is called field.annotations is always a BaseModel - also the next line assumes field.annotation.model_fields always exists.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's here only to make mypy happy

Item "None" of "Optional[Type[Any]]" has no attribute "model_fields"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, please add a comment saying that, and also support the case where field.annotations is not None but is not a BaseModel - e.g. has no .model_fields attribute.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


# Find field in sub model by looking in fields case insensitively
for sub_model_field_name, f in field.annotation.model_fields.items():
if 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 +218,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
24 changes: 24 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1486,3 +1486,27 @@ 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

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"}}}')
s = Settings()
assert s.nested.VAL1 == 'v1'
assert s.nested.SUB_sub.Val2 == 'v2'
assert s.nested.SUB_sub.SUB_sub_SuB.VaL3 == 'v3'