Skip to content

Commit dc3bd65

Browse files
Allow Configuration provider to accept pydantic settings classes (#963)
1 parent fcb3ea3 commit dc3bd65

5 files changed

Lines changed: 48 additions & 32 deletions

File tree

docs/providers/configuration.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,19 @@ the container will call ``config.from_pydantic()`` automatically:
212212
if __name__ == "__main__":
213213
container = Container() # Config is loaded from Settings()
214214
215+
In addition, if you need the pydantic instance to be initialized on use, you can provide ``pydantic_settings.BaseSettings`` type instead.
216+
The container will initialize a pydantic instance on load without kwargs.
217+
218+
.. code-block:: python
219+
:emphasize-lines: 3
220+
221+
class Container(containers.DeclarativeContainer):
222+
223+
config = providers.Configuration(pydantic_settings=[Settings])
224+
225+
226+
if __name__ == "__main__":
227+
container = Container() # Config is loaded from Settings instance that is initialized
215228
216229
.. note::
217230

src/dependency_injector/providers.pyi

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ except ImportError:
3030
yaml = None
3131

3232
try:
33-
import pydantic
33+
from pydantic_settings import BaseSettings as PydanticSettings
3434
except ImportError:
35-
pydantic = None
35+
try:
36+
from pydantic import BaseSettings as PydanticSettings
37+
except ImportError:
38+
PydanticSettings = Any
3639

3740
from . import resources
3841

@@ -272,7 +275,7 @@ class Configuration(Object[Any]):
272275
ini_files: Optional[_Iterable[Union[Path, str]]] = None,
273276
yaml_files: Optional[_Iterable[Union[Path, str]]] = None,
274277
json_files: Optional[_Iterable[Union[Path, str]]] = None,
275-
pydantic_settings: Optional[_Iterable[PydanticSettings]] = None,
278+
pydantic_settings: Optional[_Iterable[Union[PydanticSettings, Type[PydanticSettings]]]] = None,
276279
) -> None: ...
277280
def __enter__(self) -> _Self: ...
278281
def __exit__(self, *exc_info: Any) -> None: ...
@@ -292,8 +295,8 @@ class Configuration(Object[Any]):
292295
def set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> _Self: ...
293296
def get_json_files(self) -> _List[Union[Path, str]]: ...
294297
def set_json_files(self, files: _Iterable[Union[Path, str]]) -> _Self: ...
295-
def get_pydantic_settings(self) -> _List[PydanticSettings]: ...
296-
def set_pydantic_settings(self, settings: _Iterable[PydanticSettings]) -> _Self: ...
298+
def get_pydantic_settings(self) -> _List[Union[PydanticSettings, Type[PydanticSettings]]]: ...
299+
def set_pydantic_settings(self, settings: _Iterable[Union[PydanticSettings, Type[PydanticSettings]]]) -> _Self: ...
297300
def load(self, required: bool = False, envs_required: bool = False) -> None: ...
298301
def get(self, selector: str) -> Any: ...
299302
def set(self, selector: str, value: Any) -> OverridingContext[P]: ...
@@ -319,7 +322,7 @@ class Configuration(Object[Any]):
319322
envs_required: bool = False,
320323
) -> None: ...
321324
def from_pydantic(
322-
self, settings: PydanticSettings, required: bool = False, **kwargs: Any
325+
self, settings: Union[PydanticSettings, Type[PydanticSettings]], required: bool = False, **kwargs: Any
323326
) -> None: ...
324327
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
325328
def from_env(
@@ -630,8 +633,3 @@ if yaml:
630633

631634
else:
632635
class YamlLoader: ...
633-
634-
if pydantic:
635-
PydanticSettings = pydantic.BaseSettings
636-
else:
637-
PydanticSettings = Any

src/dependency_injector/providers.pyx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,7 @@ cdef dict pydantic_settings_to_dict(settings, dict kwargs):
157157
)
158158

159159
if isinstance(settings, type) and issubclass(settings, PydanticSettings):
160-
raise Error(
161-
"Got settings class, but expect instance: "
162-
"instead \"{0}\" use \"{0}()\"".format(settings.__name__)
163-
)
160+
settings = settings()
164161

165162
if not isinstance(settings, PydanticSettings):
166163
raise Error(

tests/typing/configuration.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pathlib import Path
2-
from typing import Any, Dict
2+
from typing import Any, Dict, Type
33
from typing_extensions import assert_type
44

55
from pydantic_settings import BaseSettings as PydanticSettings
@@ -33,6 +33,8 @@
3333

3434
config2.from_pydantic(PydanticSettings())
3535

36+
config2.from_pydantic(PydanticSettings)
37+
3638
# Test 3: to check as_*() methods
3739
config3 = providers.Configuration()
3840
int3 = config3.option.as_int()
@@ -80,14 +82,18 @@
8082
)
8183
config5_pydantic.set_pydantic_settings([PydanticSettings()])
8284

83-
# NOTE: Using assignment since PydanticSettings is context-sensitive: conditional on whether pydantic is installed
84-
config5_pydantic_settings: list[PydanticSettings] = (
85-
config5_pydantic.get_pydantic_settings()
86-
)
85+
config5_pydantic_settings = config5_pydantic.get_pydantic_settings()
86+
87+
assert_type(config5_pydantic_settings, list[PydanticSettings | Type[PydanticSettings]])
8788

8889
# Test 6: to check init arguments
8990
config6 = providers.Configuration(
9091
name="config",
9192
strict=True,
9293
default={},
9394
)
95+
96+
# Test 7: pydantic class
97+
config7_pydantic_class = providers.Configuration(
98+
pydantic_settings=[PydanticSettings]
99+
)

tests/unit/providers/configuration/test_from_pydantic_py36.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -168,21 +168,23 @@ def test_option_not_instance_of_settings(config):
168168

169169

170170
def test_subclass_instead_of_instance(config):
171-
with raises(errors.Error) as error:
172-
config.from_pydantic(Settings1)
173-
assert error.value.args[0] == (
174-
"Got settings class, but expect instance: "
175-
"instead \"Settings1\" use \"Settings1()\""
176-
)
171+
config.from_pydantic(Settings1)
172+
173+
assert config() == {"section1": {"value1": 1}, "section2": {"value2": 2}}
174+
assert config.section1() == {"value1": 1}
175+
assert config.section1.value1() == 1
176+
assert config.section2() == {"value2": 2}
177+
assert config.section2.value2() == 2
177178

178179

179180
def test_option_subclass_instead_of_instance(config):
180-
with raises(errors.Error) as error:
181-
config.option.from_pydantic(Settings1)
182-
assert error.value.args[0] == (
183-
"Got settings class, but expect instance: "
184-
"instead \"Settings1\" use \"Settings1()\""
185-
)
181+
config.option.from_pydantic(Settings1)
182+
183+
assert config.option() == {"section1": {"value1": 1}, "section2": {"value2": 2}}
184+
assert config.option.section1() == {"value1": 1}
185+
assert config.option.section1.value1() == 1
186+
assert config.option.section2() == {"value2": 2}
187+
assert config.option.section2.value2() == 2
186188

187189

188190
@mark.usefixtures("no_pydantic_module_installed")

0 commit comments

Comments
 (0)