Added support for pydantic-settings in Configuration.from_pydantic() method

This commit is contained in:
Aleksandr Tsariapkin 2023-11-26 14:18:05 +04:00
parent cc2304e46e
commit 18143a4e44
8 changed files with 8483 additions and 8304 deletions

View File

@ -12,6 +12,7 @@ pyyaml
httpx httpx
fastapi fastapi
pydantic pydantic
pydantic-settings
numpy numpy
scipy scipy
boto3 boto3

View File

@ -1,2 +1,3 @@
flask flask~=2.2.2
Werkzeug~=2.2.2
aiohttp aiohttp

File diff suppressed because it is too large Load Diff

View File

@ -27,9 +27,12 @@ except ImportError:
yaml = None yaml = None
try: try:
import pydantic import pydantic_settings
except ImportError: except ImportError:
pydantic = None try:
import pydantic as pydantic_settings
except ImportError:
pydantic_settings = None
from . import resources from . import resources
@ -544,7 +547,7 @@ if yaml:
else: else:
class YamlLoader: ... class YamlLoader: ...
if pydantic: if pydantic_settings:
PydanticSettings = pydantic.BaseSettings PydanticSettings = pydantic_settings.BaseSettings
else: else:
PydanticSettings = Any PydanticSettings = Any

View File

@ -49,9 +49,12 @@ except ImportError:
yaml = None yaml = None
try: try:
import pydantic import pydantic_settings
except ImportError: except ImportError:
pydantic = None try:
import pydantic as pydantic_settings
except ImportError:
pydantic_settings = None
from .errors import ( from .errors import (
Error, Error,
@ -1786,36 +1789,38 @@ cdef class ConfigurationOption(Provider):
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
:param settings: Pydantic settings instances. :param settings: Pydantic settings instances.
:type settings: :py:class:`pydantic.BaseSettings` :type settings: :py:class:`pydantic.BaseSettings` or :py:class:`pydantic_settings.BaseSettings`
:param required: When required is True, raise an exception if settings dict is empty. :param required: When required is True, raise an exception if settings dict is empty.
:type required: bool :type required: bool
:param kwargs: Keyword arguments forwarded to ``pydantic.BaseSettings.dict()`` call. :param kwargs: Keyword arguments forwarded to ``pydantic.BaseSettings.dict()``
or ``pydantic_settings.BaseSettings.model_dump()`` call, depending on the used package.
:type kwargs: Dict[Any, Any] :type kwargs: Dict[Any, Any]
:rtype: None :rtype: None
""" """
if pydantic is None: if pydantic_settings is None:
raise Error( raise Error(
"Unable to load pydantic configuration - pydantic is not installed. " "Unable to load pydantic configuration - neither pydantic_settings nor pydantic v1 is not installed. "
"Install pydantic or install Dependency Injector with pydantic extras: " "Install pydantic_settings or pydantic v1 or install Dependency Injector with pydantic extras: "
"\"pip install dependency-injector[pydantic]\"" "\"pip install dependency-injector[pydantic]\""
) )
if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic.BaseSettings): if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic_settings.BaseSettings):
raise Error( raise Error(
"Got settings class, but expect instance: " "Got settings class, but expect instance: "
"instead \"{0}\" use \"{0}()\"".format(settings.__name__) "instead \"{0}\" use \"{0}()\"".format(settings.__name__)
) )
if not isinstance(settings, pydantic.BaseSettings): if not isinstance(settings, pydantic_settings.BaseSettings):
raise Error( raise Error(
"Unable to recognize settings instance, expect \"pydantic.BaseSettings\", " "Unable to recognize settings instance, expect \"pydantic.BaseSettings\", "
"got {0} instead".format(settings) "got {0} instead".format(settings)
) )
self.from_dict(settings.dict(**kwargs), required=required) settings_dict = settings.model_dump(**kwargs) if hasattr(settings, "model_dump") else settings.dict(**kwargs)
self.from_dict(settings_dict, required=required)
def from_dict(self, options, required=UNDEFINED): def from_dict(self, options, required=UNDEFINED):
"""Load configuration from the dictionary. """Load configuration from the dictionary.
@ -2355,36 +2360,38 @@ cdef class Configuration(Object):
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
:param settings: Pydantic settings instances. :param settings: Pydantic settings instances.
:type settings: :py:class:`pydantic.BaseSettings` :type settings: :py:class:`pydantic.BaseSettings` or :py:class:`pydantic_settings.BaseSettings`
:param required: When required is True, raise an exception if settings dict is empty. :param required: When required is True, raise an exception if settings dict is empty.
:type required: bool :type required: bool
:param kwargs: Keyword arguments forwarded to ``pydantic.BaseSettings.dict()`` call. :param kwargs: Keyword arguments forwarded to ``pydantic.BaseSettings.dict()``
or ``pydantic.BaseSettings.model_dump call()`` call, depending on the used package.
:type kwargs: Dict[Any, Any] :type kwargs: Dict[Any, Any]
:rtype: None :rtype: None
""" """
if pydantic is None: if pydantic_settings is None:
raise Error( raise Error(
"Unable to load pydantic configuration - pydantic is not installed. " "Unable to load pydantic configuration - neither pydantic_settings nor pydantic v1 is not installed. "
"Install pydantic or install Dependency Injector with pydantic extras: " "Install pydantic_settings or pydantic v1 or install Dependency Injector with pydantic extras: "
"\"pip install dependency-injector[pydantic]\"" "\"pip install dependency-injector[pydantic]\""
) )
if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic.BaseSettings): if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic_settings.BaseSettings):
raise Error( raise Error(
"Got settings class, but expect instance: " "Got settings class, but expect instance: "
"instead \"{0}\" use \"{0}()\"".format(settings.__name__) "instead \"{0}\" use \"{0}()\"".format(settings.__name__)
) )
if not isinstance(settings, pydantic.BaseSettings): if not isinstance(settings, pydantic_settings.BaseSettings):
raise Error( raise Error(
"Unable to recognize settings instance, expect \"pydantic.BaseSettings\", " "Unable to recognize settings instance, expect \"pydantic.BaseSettings\", "
"got {0} instead".format(settings) "got {0} instead".format(settings)
) )
self.from_dict(settings.dict(**kwargs), required=required) settings_dict = settings.model_dump(**kwargs) if hasattr(settings, "model_dump") else settings.dict(**kwargs)
self.from_dict(settings_dict, required=required)
def from_dict(self, options, required=UNDEFINED): def from_dict(self, options, required=UNDEFINED):
"""Load configuration from the dictionary. """Load configuration from the dictionary.

View File

@ -1,7 +1,11 @@
from pathlib import Path from pathlib import Path
from dependency_injector import providers from dependency_injector import providers
from pydantic import BaseSettings as PydanticSettings
try:
from pydantic_settings import BaseSettings as PydanticSettings
except ImportError:
from pydantic import BaseSettings as PydanticSettings
# Test 1: to check the getattr # Test 1: to check the getattr

View File

@ -4,38 +4,44 @@ import pydantic
from dependency_injector import providers, errors from dependency_injector import providers, errors
from pytest import fixture, mark, raises from pytest import fixture, mark, raises
try:
from pydantic_settings import BaseSettings
except ImportError:
BaseSettings = pydantic.BaseSettings
class Section11(pydantic.BaseModel): class Section11(pydantic.BaseModel):
value1 = 1 value1: int = 1
class Section12(pydantic.BaseModel): class Section12(pydantic.BaseModel):
value2 = 2 value2: int = 2
class Settings1(pydantic.BaseSettings): class Settings1(BaseSettings):
section1 = Section11() section1: Section11 = Section11()
section2 = Section12() section2: Section12 = Section12()
class Section21(pydantic.BaseModel): class Section21(pydantic.BaseModel):
value1 = 11 value1: int = 11
value11 = 11 value11: int = 11
class Section3(pydantic.BaseModel): class Section3(pydantic.BaseModel):
value3 = 3 value3: int = 3
class Settings2(pydantic.BaseSettings): class Settings2(BaseSettings):
section1 = Section21() section1: Section21 = Section21()
section3 = Section3() section3: Section3 = Section3()
@fixture @fixture
def no_pydantic_module_installed(): def no_pydantic_module_installed():
providers.pydantic = None pydantic_setting = providers.pydantic_settings
providers.pydantic_settings = None
yield yield
providers.pydantic = pydantic providers.pydantic_settings = pydantic_setting
def test(config): def test(config):
@ -82,46 +88,46 @@ def test_merge(config):
def test_empty_settings(config): def test_empty_settings(config):
config.from_pydantic(pydantic.BaseSettings()) config.from_pydantic(BaseSettings())
assert config() == {} assert config() == {}
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_empty_settings_strict_mode(config): def test_empty_settings_strict_mode(config):
with raises(ValueError): with raises(ValueError):
config.from_pydantic(pydantic.BaseSettings()) config.from_pydantic(BaseSettings())
def test_option_empty_settings(config): def test_option_empty_settings(config):
config.option.from_pydantic(pydantic.BaseSettings()) config.option.from_pydantic(BaseSettings())
assert config.option() == {} assert config.option() == {}
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_option_empty_settings_strict_mode(config): def test_option_empty_settings_strict_mode(config):
with raises(ValueError): with raises(ValueError):
config.option.from_pydantic(pydantic.BaseSettings()) config.option.from_pydantic(BaseSettings())
def test_required_empty_settings(config): def test_required_empty_settings(config):
with raises(ValueError): with raises(ValueError):
config.from_pydantic(pydantic.BaseSettings(), required=True) config.from_pydantic(BaseSettings(), required=True)
def test_required_option_empty_settings(config): def test_required_option_empty_settings(config):
with raises(ValueError): with raises(ValueError):
config.option.from_pydantic(pydantic.BaseSettings(), required=True) config.option.from_pydantic(BaseSettings(), required=True)
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_not_required_empty_settings_strict_mode(config): def test_not_required_empty_settings_strict_mode(config):
config.from_pydantic(pydantic.BaseSettings(), required=False) config.from_pydantic(BaseSettings(), required=False)
assert config() == {} assert config() == {}
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_not_required_option_empty_settings_strict_mode(config): def test_not_required_option_empty_settings_strict_mode(config):
config.option.from_pydantic(pydantic.BaseSettings(), required=False) config.option.from_pydantic(BaseSettings(), required=False)
assert config.option() == {} assert config.option() == {}
assert config() == {"option": {}} assert config() == {"option": {}}
@ -167,8 +173,8 @@ def test_no_pydantic_installed(config):
with raises(errors.Error) as error: with raises(errors.Error) as error:
config.from_pydantic(Settings1()) config.from_pydantic(Settings1())
assert error.value.args[0] == ( assert error.value.args[0] == (
"Unable to load pydantic configuration - pydantic is not installed. " "Unable to load pydantic configuration - neither pydantic_settings nor pydantic v1 is not installed. "
"Install pydantic or install Dependency Injector with pydantic extras: " "Install pydantic_settings or pydantic v1 or install Dependency Injector with pydantic extras: "
"\"pip install dependency-injector[pydantic]\"" "\"pip install dependency-injector[pydantic]\""
) )
@ -178,7 +184,7 @@ def test_option_no_pydantic_installed(config):
with raises(errors.Error) as error: with raises(errors.Error) as error:
config.option.from_pydantic(Settings1()) config.option.from_pydantic(Settings1())
assert error.value.args[0] == ( assert error.value.args[0] == (
"Unable to load pydantic configuration - pydantic is not installed. " "Unable to load pydantic configuration - neither pydantic_settings nor pydantic v1 is not installed. "
"Install pydantic or install Dependency Injector with pydantic extras: " "Install pydantic_settings or pydantic v1 or install Dependency Injector with pydantic extras: "
"\"pip install dependency-injector[pydantic]\"" "\"pip install dependency-injector[pydantic]\""
) )

View File

@ -5,31 +5,37 @@ from dependency_injector import providers
from pytest import fixture, mark, raises from pytest import fixture, mark, raises
try:
from pydantic_settings import BaseSettings
except ImportError:
BaseSettings = pydantic.BaseSettings
class Section11(pydantic.BaseModel): class Section11(pydantic.BaseModel):
value1 = 1 value1: int = 1
class Section12(pydantic.BaseModel): class Section12(pydantic.BaseModel):
value2 = 2 value2: int = 2
class Settings1(pydantic.BaseSettings): class Settings1(BaseSettings):
section1 = Section11() section1: Section11 = Section11()
section2 = Section12() section2: Section12 = Section12()
class Section21(pydantic.BaseModel): class Section21(pydantic.BaseModel):
value1 = 11 value1: int = 11
value11 = 11 value11: int = 11
class Section3(pydantic.BaseModel): class Section3(pydantic.BaseModel):
value3 = 3 value3: int = 3
class Settings2(pydantic.BaseSettings): class Settings2(BaseSettings):
section1 = Section21() section1: Section21 = Section21()
section3 = Section3() section3: Section3 = Section3()
@fixture @fixture
@ -86,10 +92,10 @@ def test_copy(config, pydantic_settings_1, pydantic_settings_2):
def test_set_pydantic_settings(config): def test_set_pydantic_settings(config):
class Settings3(pydantic.BaseSettings): class Settings3(BaseSettings):
... ...
class Settings4(pydantic.BaseSettings): class Settings4(BaseSettings):
... ...
settings_3 = Settings3() settings_3 = Settings3()
@ -100,27 +106,27 @@ def test_set_pydantic_settings(config):
def test_file_does_not_exist(config): def test_file_does_not_exist(config):
config.set_pydantic_settings([pydantic.BaseSettings()]) config.set_pydantic_settings([BaseSettings()])
config.load() config.load()
assert config() == {} assert config() == {}
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_file_does_not_exist_strict_mode(config): def test_file_does_not_exist_strict_mode(config):
config.set_pydantic_settings([pydantic.BaseSettings()]) config.set_pydantic_settings([BaseSettings()])
with raises(ValueError): with raises(ValueError):
config.load() config.load()
assert config() == {} assert config() == {}
def test_required_file_does_not_exist(config): def test_required_file_does_not_exist(config):
config.set_pydantic_settings([pydantic.BaseSettings()]) config.set_pydantic_settings([BaseSettings()])
with raises(ValueError): with raises(ValueError):
config.load(required=True) config.load(required=True)
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_not_required_file_does_not_exist_strict_mode(config): def test_not_required_file_does_not_exist_strict_mode(config):
config.set_pydantic_settings([pydantic.BaseSettings()]) config.set_pydantic_settings([BaseSettings()])
config.load(required=False) config.load(required=False)
assert config() == {} assert config() == {}