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
feat: Add the ability to add extra settings sources #2107
Conversation
Codecov Report
@@ Coverage Diff @@
## master pydantic/pydantic#2107 +/- ##
===========================================
- Coverage 100.00% 99.88% -0.12%
===========================================
Files 21 22 +1
Lines 4199 4351 +152
Branches 854 875 +21
===========================================
+ Hits 4199 4346 +147
- Misses 0 5 +5
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could be useful, but I think it needs to be rethought. It will also need tests to pass and lots of docs.
pydantic/env_settings.py
Outdated
) | ||
|
||
def filter_relevant_env_vars(self, env_vars: Mapping[str, Optional[str]]) -> Dict[str, Optional[str]]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this method can be private. This also makes sure that it can't conflict with settings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same with most of the methods below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you move this down to where the code was before? If so hopefully we can reduce the number of changes and keep history easy to track.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to revert this modification as the new implementation will only use load_env_vars_from_source
.
Also, load_env_vars_from_source
will be made private to avoid conflicts as you requested.
pydantic/env_settings.py
Outdated
for field in self.__fields__.values(): | ||
for env_name in field.field_info.extra['env_names']: | ||
value = loader(env_name) | ||
if value != undefined: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
surely we should use None
or KeyError
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to keep the undefined
as it allows the source to support any type including None
.
About the KeyError
check, I think it's too specific to dict based sources: in the case of a REST API based source, a call to loader
can trigger an API call that simply returns a flat value.
What do you think ?
pydantic/env_settings.py
Outdated
@@ -44,19 +45,58 @@ def _build_values( | |||
_env_file_encoding: Optional[str] = None, | |||
_secrets_dir: Union[Path, str, None] = None, | |||
) -> Dict[str, Any]: | |||
extra_settings = [source(self) for source in reversed(self.__config__.extra_settings_sources)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why the reverse
?
Also shouldn't this be
extra_settings = [source(self) for source in reversed(self.__config__.extra_settings_sources)] | |
extra_settings = [self.load_env_vars_from_source(source) for source in reversed(self.__config__.extra_settings_sources)] |
?
I think this would reduce the logic and complexity in your customer loaders/sources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As explained in my latest comment, the settings source sequence is expressed from the highest priority to the lowest one.
We need to reverse it to send the settings dicts to deep_update
from the lowest priority to the highest one.
In the next revision, I'll move to your implementation as all sources can be treated as case 2 sources.
please see pydantic/pydantic-settings#32 and its PR #2154. We need a way to make customising the priority of settings sources easier as well as adding new ones. I would propose something like this: from typing import Tuple, Any, Callable
from pydantic import BaseSettings
SettingsSourceCallable = Callable[[str], Any]
class Settings(BaseSettings):
foo: str
bar: str
class Config:
@classmethod
def customise_sources(
cls,
init_settings: SettingsSourceCallable,
env_settings: SettingsSourceCallable,
file_secret_settings: SettingsSourceCallable,
) -> Tuple[SettingsSourceCallable, ...]:
return init_settings, env_settings, file_secret_settings Here Advantages of this approach:
Disadvantages:
|
First of all, thanks for your feedback 🙏 I agree with your solution involving a SettingsSourceCallable implementationAbout the implementation of
In the first revision of the PR, I let the However, I see now we can simplify the workflow if the custom source handles the "bulk loading" itself (case 1). import json
from functools import cached_property
from pathlib import Path
class JSONConfigSource:
def __init__(self, path: Path = 'config.json'):
self.config_path = path
@cached_property
def json_config(self):
return json.loads(self.config_path.read_text())
def __call__(self, env_name: str, field: ModelField, settings: BaseSettings):
return self.json_config.get(env_name, undefined) In this case, all sources can move to the case 2 and we can simplify the workflow as suggest. We still have to edit slightly the Settings priorityAs you mentioned it in your comments, settings priority seems to be important for some people. Custom source configBuilt-in sources like .env or docker secrets can be configured directly using from pathlib import Path
from pydantic import BaseSettings
from .ext_sources import JSONConfigSource # defined ealier
class Settings(BaseSettings):
foo: str
bar: str
class Config:
@classmethod
def customise_sources(
cls,
init_settings: SettingsSourceCallable,
env_settings: SettingsSourceCallable,
file_secret_settings: SettingsSourceCallable,
) -> Tuple[SettingsSourceCallable, ...]:
json_config_settings = JSONConfigSource(path=Path("config.json"))
# here we disable both env_settings and file_secret_settings
return init_settings, json_config_settings
settings = Settings() That's not ideal but it works so we can start like this. Based on your comments and the related issues, the whole feature appears clearer to me. Let me know if you want to add / change something else. I'll try to do the necessary changes by the end of the week, including docs & tests 🙂 |
3d1be86
to
7765fc0
Compare
By defining a common type for all settings sources ( Init settings
More than simply messing up with unit tests, this would introduce breaking changes, which we might want to avoid.
For now, I'll start my implementation with the solution 4) as this is the most complete and the closest from the actual source code. |
c21de41
to
e8d65f6
Compare
I've modified my implementation to include a way of customising the priority of settings sources. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is looking great, just a few things to fix.
docs/usage/settings.md
Outdated
If the default order of priority doesn't match your needs, it's possible to change it by overriding a config method: | ||
|
||
```py | ||
class Settings(BaseSettings): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you move this code (and the rest below) into python files in examples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you might need to add one or two files instead of ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, it's cleaner this way 🙂
e8d65f6
to
8025750
Compare
8025750
to
75b3faf
Compare
This is a really useful feature. I would love to be able to load configuration via a local |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This customization will be very handy and is very explicit! LGTM! 👍
Samuel may still have some remarks though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You just forgot to test your custom __repr__
for ...SettingsSource
and to add a change file
Thanks 🙏
You're right, I added the missing test and a change file to my latest commit. Tell me if we are missing anything else 🙂 |
Co-authored-by: Eric Jolibois <em.jolibois@gmail.com>
thanks so much. I've extended the docs slightly as the original docs on this were pretty minimal. |
No problem, thanks for taking the time to merge this one 👌 |
As a thank-you to everyone on this thread, I spent yesterday browsing the master branch and saw this hook before I saw this issue. I was just testing why it didn't work now and saw that it was because this feature isn't released yet! It's a really nice implementation and fits my needs exactly - thanks. Looking forward to this hitting a new release. In the meantime, my draft implementation - to pull some secrets from SSM in prod - is at the gist below. https://gist.github.com/DomWeldon/ce7e070283d97368cd9abc5be71b247d |
Change Summary
filter_relevant_env_vars
andload_env_vars_from_source
functions to ease creation of external sources pluginsextra_settings_sources
ModelConfig key for BaseSettingsMore info in the linked issue.
Related issue number
#2106
Checklist
changes/<pull request or issue id>-<github username>.md
file added describing change(see changes/README.md for details)