diff --git a/docs/providers/configuration.rst b/docs/providers/configuration.rst index 66c299f3..651bf772 100644 --- a/docs/providers/configuration.rst +++ b/docs/providers/configuration.rst @@ -212,6 +212,19 @@ the container will call ``config.from_pydantic()`` automatically: if __name__ == "__main__": container = Container() # Config is loaded from Settings() +In addition, if you need the pydantic instance to be initialized on use, you can provide ``pydantic_settings.BaseSettings`` type instead. +The container will initialize a pydantic instance on load without kwargs. + +.. 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 instance that is initialized .. note:: diff --git a/src/dependency_injector/providers.pyi b/src/dependency_injector/providers.pyi index e4211e00..e8542eeb 100644 --- a/src/dependency_injector/providers.pyi +++ b/src/dependency_injector/providers.pyi @@ -30,9 +30,12 @@ except ImportError: yaml = None try: - import pydantic + from pydantic_settings import BaseSettings as PydanticSettings except ImportError: - pydantic = None + try: + from pydantic import BaseSettings as PydanticSettings + except ImportError: + PydanticSettings = Any from . import resources @@ -272,7 +275,7 @@ class Configuration(Object[Any]): ini_files: Optional[_Iterable[Union[Path, str]]] = None, yaml_files: Optional[_Iterable[Union[Path, str]]] = None, json_files: Optional[_Iterable[Union[Path, str]]] = None, - pydantic_settings: Optional[_Iterable[PydanticSettings]] = None, + pydantic_settings: Optional[_Iterable[Union[PydanticSettings, Type[PydanticSettings]]]] = None, ) -> None: ... def __enter__(self) -> _Self: ... def __exit__(self, *exc_info: Any) -> None: ... @@ -292,8 +295,8 @@ class Configuration(Object[Any]): def set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> _Self: ... def get_json_files(self) -> _List[Union[Path, str]]: ... def set_json_files(self, files: _Iterable[Union[Path, str]]) -> _Self: ... - def get_pydantic_settings(self) -> _List[PydanticSettings]: ... - def set_pydantic_settings(self, settings: _Iterable[PydanticSettings]) -> _Self: ... + def get_pydantic_settings(self) -> _List[Union[PydanticSettings, Type[PydanticSettings]]]: ... + def set_pydantic_settings(self, settings: _Iterable[Union[PydanticSettings, Type[PydanticSettings]]]) -> _Self: ... def load(self, required: bool = False, envs_required: bool = False) -> None: ... def get(self, selector: str) -> Any: ... def set(self, selector: str, value: Any) -> OverridingContext[P]: ... @@ -319,7 +322,7 @@ class Configuration(Object[Any]): envs_required: bool = False, ) -> None: ... def from_pydantic( - self, settings: PydanticSettings, required: bool = False, **kwargs: Any + self, settings: Union[PydanticSettings, Type[PydanticSettings]], required: bool = False, **kwargs: Any ) -> None: ... def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ... def from_env( @@ -630,8 +633,3 @@ if yaml: else: class YamlLoader: ... - -if pydantic: - PydanticSettings = pydantic.BaseSettings -else: - PydanticSettings = Any diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index 4d4d5c39..edde7b1b 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -157,10 +157,7 @@ cdef dict pydantic_settings_to_dict(settings, dict kwargs): ) if isinstance(settings, type) and issubclass(settings, PydanticSettings): - raise Error( - "Got settings class, but expect instance: " - "instead \"{0}\" use \"{0}()\"".format(settings.__name__) - ) + settings = settings() if not isinstance(settings, PydanticSettings): raise Error( diff --git a/tests/typing/configuration.py b/tests/typing/configuration.py index 9c1dfea0..cea97f76 100644 --- a/tests/typing/configuration.py +++ b/tests/typing/configuration.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Type from typing_extensions import assert_type from pydantic_settings import BaseSettings as PydanticSettings @@ -33,6 +33,8 @@ config2.from_pydantic(PydanticSettings()) +config2.from_pydantic(PydanticSettings) + # Test 3: to check as_*() methods config3 = providers.Configuration() int3 = config3.option.as_int() @@ -80,10 +82,9 @@ ) config5_pydantic.set_pydantic_settings([PydanticSettings()]) -# NOTE: Using assignment since PydanticSettings is context-sensitive: conditional on whether pydantic is installed -config5_pydantic_settings: list[PydanticSettings] = ( - config5_pydantic.get_pydantic_settings() -) +config5_pydantic_settings = config5_pydantic.get_pydantic_settings() + +assert_type(config5_pydantic_settings, list[PydanticSettings | Type[PydanticSettings]]) # Test 6: to check init arguments config6 = providers.Configuration( @@ -91,3 +92,8 @@ strict=True, default={}, ) + +# Test 7: pydantic class +config7_pydantic_class = providers.Configuration( + pydantic_settings=[PydanticSettings] +) diff --git a/tests/unit/providers/configuration/test_from_pydantic_py36.py b/tests/unit/providers/configuration/test_from_pydantic_py36.py index ee2a1176..f47e458c 100644 --- a/tests/unit/providers/configuration/test_from_pydantic_py36.py +++ b/tests/unit/providers/configuration/test_from_pydantic_py36.py @@ -168,21 +168,23 @@ def test_option_not_instance_of_settings(config): def test_subclass_instead_of_instance(config): - with raises(errors.Error) as error: - config.from_pydantic(Settings1) - assert error.value.args[0] == ( - "Got settings class, but expect instance: " - "instead \"Settings1\" use \"Settings1()\"" - ) + config.from_pydantic(Settings1) + + assert config() == {"section1": {"value1": 1}, "section2": {"value2": 2}} + assert config.section1() == {"value1": 1} + assert config.section1.value1() == 1 + assert config.section2() == {"value2": 2} + assert config.section2.value2() == 2 def test_option_subclass_instead_of_instance(config): - with raises(errors.Error) as error: - config.option.from_pydantic(Settings1) - assert error.value.args[0] == ( - "Got settings class, but expect instance: " - "instead \"Settings1\" use \"Settings1()\"" - ) + config.option.from_pydantic(Settings1) + + assert config.option() == {"section1": {"value1": 1}, "section2": {"value2": 2}} + assert config.option.section1() == {"value1": 1} + assert config.option.section1.value1() == 1 + assert config.option.section2() == {"value2": 2} + assert config.option.section2.value2() == 2 @mark.usefixtures("no_pydantic_module_installed")