Configuration(pydantic_settings=[...]) (#525)

* Add implementation

* Update changelog

* Fix deepcopy()

* Add example

* Add tests

* Add docs
This commit is contained in:
Roman Mogylatov 2021-10-26 21:08:47 -04:00 committed by GitHub
parent 34902db86e
commit 6030950596
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 6886 additions and 6247 deletions

View File

@ -16,6 +16,7 @@ Develop
- Add support of ``with`` statement for ``container.override_providers()`` method. - Add support of ``with`` statement for ``container.override_providers()`` method.
- Add ``Configuration(yaml_files=[...])`` argument. - Add ``Configuration(yaml_files=[...])`` argument.
- Add ``Configuration(ini_files=[...])`` argument. - Add ``Configuration(ini_files=[...])`` argument.
- Add ``Configuration(pydantic_settings=[...])`` argument.
- Drop support of Python 3.4. There are no immediate breaking changes, but Dependency Injector - Drop support of Python 3.4. There are no immediate breaking changes, but Dependency Injector
will no longer be tested on Python 3.4 and any bugs will not be fixed. will no longer be tested on Python 3.4 and any bugs will not be fixed.
- Fix ``Dependency.is_defined`` attribute to always return boolean value. - Fix ``Dependency.is_defined`` attribute to always return boolean value.

View File

@ -154,6 +154,21 @@ If you need to pass an argument to this call, use ``.from_pydantic()`` keyword a
container.config.from_pydantic(Settings(), exclude={"optional"}) container.config.from_pydantic(Settings(), exclude={"optional"})
Alternatively, you can provide a ``pydantic`` settings object over the configuration provider argument. In that case,
the container will call ``config.from_pydantic()`` automatically:
.. code-block:: python
:emphasize-lines: 3
class Container(containers.DeclarativeContainer):
config = providers.Configuration(pydantic_settings=[Settings()])
if __name__ == "__main__":
container = Container() # Config is loaded from Settings()
.. note:: .. note::
``Dependency Injector`` doesn't install ``pydantic`` by default. ``Dependency Injector`` doesn't install ``pydantic`` by default.

View File

@ -0,0 +1,35 @@
"""`Configuration` provider values loading example."""
import os
from dependency_injector import containers, providers
from pydantic import BaseSettings, Field
# Emulate environment variables
os.environ["AWS_ACCESS_KEY_ID"] = "KEY"
os.environ["AWS_SECRET_ACCESS_KEY"] = "SECRET"
class AwsSettings(BaseSettings):
access_key_id: str = Field(env="aws_access_key_id")
secret_access_key: str = Field(env="aws_secret_access_key")
class Settings(BaseSettings):
aws: AwsSettings = AwsSettings()
optional: str = Field(default="default_value")
class Container(containers.DeclarativeContainer):
config = providers.Configuration(pydantic_settings=[Settings()])
if __name__ == "__main__":
container = Container()
assert container.config.aws.access_key_id() == "KEY"
assert container.config.aws.secret_access_key() == "SECRET"
assert container.config.optional() == "default_value"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -116,6 +116,7 @@ cdef class Configuration(Object):
cdef dict __children cdef dict __children
cdef list __yaml_files cdef list __yaml_files
cdef list __ini_files cdef list __ini_files
cdef list __pydantic_settings
cdef object __weakref__ cdef object __weakref__

View File

@ -224,6 +224,7 @@ class Configuration(Object[Any]):
strict: bool = False, strict: bool = False,
yaml_files: Optional[_Iterable[Union[Path, str]]] = None, yaml_files: Optional[_Iterable[Union[Path, str]]] = None,
ini_files: Optional[_Iterable[Union[Path, str]]] = None, ini_files: Optional[_Iterable[Union[Path, str]]] = None,
pydantic_settings: Optional[_Iterable[PydanticSettings]] = None,
) -> None: ... ) -> None: ...
def __enter__(self) -> Configuration : ... def __enter__(self) -> Configuration : ...
def __exit__(self, *exc_info: Any) -> None: ... def __exit__(self, *exc_info: Any) -> None: ...
@ -248,6 +249,9 @@ class Configuration(Object[Any]):
def get_ini_files(self) -> _List[Union[Path, str]]: ... def get_ini_files(self) -> _List[Union[Path, str]]: ...
def set_ini_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ... def set_ini_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def get_pydantic_settings(self) -> _List[PydanticSettings]: ...
def set_pydantic_settings(self, settings: _Iterable[PydanticSettings]) -> Configuration: ...
def load(self, required: bool = False, envs_required: bool = False) -> None: ... def load(self, required: bool = False, envs_required: bool = False) -> None: ...
def get(self, selector: str) -> Any: ... def get(self, selector: str) -> Any: ...

View File

@ -1755,12 +1755,13 @@ cdef class Configuration(Object):
DEFAULT_NAME = 'config' DEFAULT_NAME = 'config'
def __init__(self, name=DEFAULT_NAME, default=None, strict=False, yaml_files=None, ini_files=None): def __init__(self, name=DEFAULT_NAME, default=None, strict=False, yaml_files=None, ini_files=None, pydantic_settings=None):
self.__name = name self.__name = name
self.__strict = strict self.__strict = strict
self.__children = {} self.__children = {}
self.__yaml_files = [] self.__yaml_files = []
self.__ini_files = [] self.__ini_files = []
self.__pydantic_settings = []
super().__init__(provides={}) super().__init__(provides={})
self.set_default(default) self.set_default(default)
@ -1773,6 +1774,10 @@ cdef class Configuration(Object):
ini_files = [] ini_files = []
self.set_ini_files(ini_files) self.set_ini_files(ini_files)
if pydantic_settings is None:
pydantic_settings = []
self.set_pydantic_settings(pydantic_settings)
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
copied = memo.get(id(self)) copied = memo.get(id(self))
if copied is not None: if copied is not None:
@ -1785,6 +1790,7 @@ cdef class Configuration(Object):
copied.set_children(deepcopy(self.get_children(), memo)) copied.set_children(deepcopy(self.get_children(), memo))
copied.set_yaml_files(self.get_yaml_files()) copied.set_yaml_files(self.get_yaml_files())
copied.set_ini_files(self.get_ini_files()) copied.set_ini_files(self.get_ini_files())
copied.set_pydantic_settings(self.get_pydantic_settings())
self._copy_overridings(copied, memo) self._copy_overridings(copied, memo)
return copied return copied
@ -1860,7 +1866,7 @@ cdef class Configuration(Object):
def get_yaml_files(self): def get_yaml_files(self):
"""Return list of YAML files.""" """Return list of YAML files."""
return self.__yaml_files return list(self.__yaml_files)
def set_yaml_files(self, files): def set_yaml_files(self, files):
"""Set list of YAML files.""" """Set list of YAML files."""
@ -1869,13 +1875,22 @@ cdef class Configuration(Object):
def get_ini_files(self): def get_ini_files(self):
"""Return list of INI files.""" """Return list of INI files."""
return self.__ini_files return list(self.__ini_files)
def set_ini_files(self, files): def set_ini_files(self, files):
"""Set list of INI files.""" """Set list of INI files."""
self.__ini_files = list(files) self.__ini_files = list(files)
return self return self
def get_pydantic_settings(self):
"""Return list of Pydantic settings."""
return list(self.__pydantic_settings)
def set_pydantic_settings(self, settings):
"""Set list of Pydantic settings."""
self.__pydantic_settings = list(settings)
return self
def load(self, required=UNDEFINED, envs_required=UNDEFINED): def load(self, required=UNDEFINED, envs_required=UNDEFINED):
"""Load configuration. """Load configuration.
@ -1899,6 +1914,9 @@ cdef class Configuration(Object):
for file in self.get_ini_files(): for file in self.get_ini_files():
self.from_ini(file, required=required, envs_required=envs_required) self.from_ini(file, required=required, envs_required=envs_required)
for settings in self.get_pydantic_settings():
self.from_pydantic(settings, required=required)
def get(self, selector, required=False): def get(self, selector, required=False):
"""Return configuration option. """Return configuration option.

View File

@ -3,7 +3,7 @@
import os import os
from dependency_injector import providers from dependency_injector import providers
from pytest import fixture, mark from pytest import fixture
@fixture @fixture

View File

@ -0,0 +1,121 @@
"""Configuration.from_pydantic() tests."""
import pydantic
from dependency_injector import providers
from pytest import fixture, mark, raises
class Section11(pydantic.BaseModel):
value1 = 1
class Section12(pydantic.BaseModel):
value2 = 2
class Settings1(pydantic.BaseSettings):
section1 = Section11()
section2 = Section12()
class Section21(pydantic.BaseModel):
value1 = 11
value11 = 11
class Section3(pydantic.BaseModel):
value3 = 3
class Settings2(pydantic.BaseSettings):
section1 = Section21()
section3 = Section3()
@fixture
def config(config_type, pydantic_settings_1, pydantic_settings_2):
if config_type == "strict":
return providers.Configuration(strict=True)
elif config_type == "default":
return providers.Configuration(pydantic_settings=[pydantic_settings_1, pydantic_settings_2])
else:
raise ValueError("Undefined config type \"{0}\"".format(config_type))
@fixture
def pydantic_settings_1():
return Settings1()
@fixture
def pydantic_settings_2():
return Settings2()
def test_load(config):
config.load()
assert config() == {
"section1": {
"value1": 11,
"value11": 11,
},
"section2": {
"value2": 2,
},
"section3": {
"value3": 3,
},
}
assert config.section1() == {"value1": 11, "value11": 11}
assert config.section1.value1() == 11
assert config.section1.value11() == 11
assert config.section2() == {"value2": 2}
assert config.section2.value2() == 2
assert config.section3() == {"value3": 3}
assert config.section3.value3() == 3
def test_get_pydantic_settings(config, pydantic_settings_1, pydantic_settings_2):
assert config.get_pydantic_settings() == [pydantic_settings_1, pydantic_settings_2]
def test_set_pydantic_settings(config):
class Settings3(pydantic.BaseSettings):
...
class Settings4(pydantic.BaseSettings):
...
settings_3 = Settings3()
settings_4 = Settings4()
config.set_pydantic_settings([settings_3, settings_4])
assert config.get_pydantic_settings() == [settings_3, settings_4]
def test_file_does_not_exist(config):
config.set_pydantic_settings([pydantic.BaseSettings()])
config.load()
assert config() == {}
@mark.parametrize("config_type", ["strict"])
def test_file_does_not_exist_strict_mode(config):
config.set_pydantic_settings([pydantic.BaseSettings()])
with raises(ValueError):
config.load()
assert config() == {}
def test_required_file_does_not_exist(config):
config.set_pydantic_settings([pydantic.BaseSettings()])
with raises(ValueError):
config.load(required=True)
@mark.parametrize("config_type", ["strict"])
def test_not_required_file_does_not_exist_strict_mode(config):
config.set_pydantic_settings([pydantic.BaseSettings()])
config.load(required=False)
assert config() == {}