-
-
Notifications
You must be signed in to change notification settings - Fork 65
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
How to Override Deeply Nested Settings using Environment Variables? #203
Comments
Mirroring the instantiated fields in the environment works: import os
import json
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict, EnvSettingsSource
class DeepSubModel(BaseModel):
v4: str
class SubModel(BaseModel):
v1: str
v2: str
v3: int
deep: DeepSubModel
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_nested_delimiter='__')
v0: str
sub_model: SubModel
@classmethod
def settings_customise_sources(
cls,
settings_cls,
init_settings,
env_settings,
dotenv_settings,
file_secret_settings):
# Mirror the instantiated fields in the environment
for key, value in cls.model_construct(**init_settings.init_kwargs).model_dump().items():
os.environ[f'{env_settings.env_prefix}{key}'] = json.dumps(value) if isinstance(value, dict) else value
# Re-instantiate EnvSettingsSource so it uses the latest environment
return EnvSettingsSource(settings_cls,
env_settings.case_sensitive,
env_settings.env_prefix,
env_settings.env_nested_delimiter), init_settings, file_secret_settings
os.environ['SUB_MODEL__DEEP__V4'] = 'override-v4'
print(Settings(v0='0', sub_model=SubModel(v1='init-v1', v2=b'init-v2', v3=3,
deep=DeepSubModel(v4='init-v4'))).model_dump())
"""
{'v0': '0', 'sub_model': {'v1': 'init-v1', 'v2': 'init-v2', 'v3': 3, 'deep': {'v4': 'override-v4'}}}
""" If the above were formalized under The changes are pretty straightforward, I can throw up a PR if desired. |
Thanks @kschwab for reporting this.
Here are the results of the sources in your example:
As you can see in If you initialize your print(Settings(v0='0', sub_model={'v1': 'init-v1', 'v2': b'init-v2', 'v3': 3, 'deep':{'v4':'init-v4'}}).model_dump()) |
Got it, thanks @hramezani for the response. If I understand correctly, class InitSettingsSource(PydanticBaseSettingsSource):
"""
Source class for loading values provided during settings class initialization.
"""
def __call__(self) -> dict[str, Any]:
InitSettings = create_model('InitSettings', **{key: (Any, val) for key,val in self.init_kwargs.items()})
return InitSettings().model_dump() This would then allow for the below approaches to be equivalent: # 1. Expressing "sub_model" and "deep" as dicts
print(Settings(v0='0', sub_model={'v1': 'init-v1', 'v2': b'init-v2', 'v3': 3,
'deep':{'v4':'init-v4'}}).model_dump())
# 2. Expressing "sub_model" and "deep" as objects
print(Settings(v0='0', sub_model=SubModel(v1='init-v1', v2=b'init-v2', v3=3,
deep=DeepSubModel(v4='init-v4'))).model_dump())
# 3. Expressing "sub_model" as object and "deep" as dict
print(Settings(v0='0', sub_model=SubModel(v1='init-v1', v2=b'init-v2', v3=3,
deep={'v4':'init-v4'})).model_dump())
# 4. Expressing "sub_model" as dict and "deep" as object
print(Settings(v0='0', sub_model={'v1': 'init-v1', 'v2': b'init-v2', 'v3': 3,
'deep':DeepSubModel(v4='init-v4')}).model_dump()) As an aside, our application is in the simulation and modelling domain. Having the ability to express model variants in object form improves readability and reduces our configuration maintenance. Pydantic settings is perfect for this, just need a slight tweak to enable this generally. |
Good idea @kschwab You can use TypeAdapter as well:
Would you like to open a PR? Otherwise I will do it. |
Done. Opened a PR. Thanks! |
@kschwab I am going to revert the fix for this issue because this introduced a breaking change I think it would be easy for you to implement your custom class CustomInitSettingsSource(InitSettingsSource):
def __call__(self) -> dict[str, Any]:
return TypeAdapter(Dict[str, Any]).dump_python(self.init_kwargs, by_alias=True) |
@hramezani, yes, reverting the original commit makes sense. Actually #241 and @moonrail raise a couple of interesting points. The above suggestion would not work for cases such as #241 where the type information must be preserved. i.e., the fix needs to handle sources that result in dict with objects. Currently only A better solution is to modify def deep_update(
mapping: dict[KeyType, Any], *updating_mappings: dict[KeyType, Any]
) -> dict[KeyType, Any]:
updated_mapping = mapping.copy()
for updating_mapping in updating_mappings:
for key, new_val in updating_mapping.items():
if key in updated_mapping:
old_val = updated_mapping[key]
old_val_type = type(old_val)
if is_model_class(old_val_type) and isinstance(new_val, dict):
old_val = old_val.model_dump()
updated_mapping[key] = (
TypeAdapter(old_val_type).validate_python(deep_update(old_val, new_val))
if isinstance(old_val, dict) and isinstance(new_val, dict)
else new_val
)
else:
updated_mapping[key] = new_val
return updated_mapping Where the resulting object type would be:
The above will also resolve the second point raised in #241, "copy-by-reference to copy-by-value", which really only applies to I'll open a new PR with the changes and additional tests to cover #241 once ready. |
@hramezani opened PR #244 with updated fixes 👍🏾 |
Thanks @kschwab for creating the new PR. I am afraid this change breaks BTW, we can keep the PR open and merge it in V3 |
@hramezani no concerns there, I agree on the risk. V3 sounds good. |
Hello,
Is it possible to override a deeply nested setting without having to redefine the entirety of the model?
Below is a modified example based off of Parsing environment variable values:
The difference here is
Settings
is defined through instantiation instead of environment variables. Ideally, the below concept would still apply toSettings
with respect to nested precedence, allowing for point modifications of nested variables:The text was updated successfully, but these errors were encountered: