mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2024-11-28 04:23:59 +03:00
Pydantic settings support (#388)
* Add implementation and basic test * Add full test coverage + bugfix * Add test coverage for .from_yaml() method * Update setup.py, tox and dev requirements * Stop running pydantic tests on Python 3.5 and below * Remove pydantic from tox Python < 3.6 * Add example and docs * Update features block * Add extra test * Update changelog
This commit is contained in:
parent
1fabbf314b
commit
15fa6c301e
|
@ -59,8 +59,8 @@ Key features of the ``Dependency Injector``:
|
||||||
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing
|
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing
|
||||||
and configuring dev / stage environment to replace API clients with stubs etc. See
|
and configuring dev / stage environment to replace API clients with stubs etc. See
|
||||||
`Provider overriding <https://python-dependency-injector.ets-labs.org/providers/overriding.html>`_.
|
`Provider overriding <https://python-dependency-injector.ets-labs.org/providers/overriding.html>`_.
|
||||||
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, environment variables
|
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, ``pydantic`` settings,
|
||||||
and dictionaries.
|
environment variables, and dictionaries.
|
||||||
See `Configuration provider <https://python-dependency-injector.ets-labs.org/providers/configuration.html>`_.
|
See `Configuration provider <https://python-dependency-injector.ets-labs.org/providers/configuration.html>`_.
|
||||||
- **Containers**. Provides declarative and dynamic containers.
|
- **Containers**. Provides declarative and dynamic containers.
|
||||||
See `Containers <https://python-dependency-injector.ets-labs.org/containers/index.html>`_.
|
See `Containers <https://python-dependency-injector.ets-labs.org/containers/index.html>`_.
|
||||||
|
|
|
@ -70,8 +70,8 @@ Key features of the ``Dependency Injector``:
|
||||||
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing
|
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing
|
||||||
and configuring dev / stage environment to replace API clients with stubs etc. See
|
and configuring dev / stage environment to replace API clients with stubs etc. See
|
||||||
:ref:`provider-overriding`.
|
:ref:`provider-overriding`.
|
||||||
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, environment variables
|
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, ``pydantic`` settings,
|
||||||
and dictionaries. See :ref:`configuration-provider`.
|
environment variables, and dictionaries. See :ref:`configuration-provider`.
|
||||||
- **Resources**. Helps with initialization and configuring of logging, event loop, thread
|
- **Resources**. Helps with initialization and configuring of logging, event loop, thread
|
||||||
or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
|
or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
|
||||||
See :ref:`resource-provider`.
|
See :ref:`resource-provider`.
|
||||||
|
|
|
@ -16,8 +16,8 @@ Key features of the ``Dependency Injector``:
|
||||||
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing
|
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing
|
||||||
and configuring dev / stage environment to replace API clients with stubs etc. See
|
and configuring dev / stage environment to replace API clients with stubs etc. See
|
||||||
:ref:`provider-overriding`.
|
:ref:`provider-overriding`.
|
||||||
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, environment variables
|
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, ``pydantic`` settings,
|
||||||
and dictionaries. See :ref:`configuration-provider`.
|
environment variables, and dictionaries. See :ref:`configuration-provider`.
|
||||||
- **Resources**. Helps with initialization and configuring of logging, event loop, thread
|
- **Resources**. Helps with initialization and configuring of logging, event loop, thread
|
||||||
or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
|
or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
|
||||||
See :ref:`resource-provider`.
|
See :ref:`resource-provider`.
|
||||||
|
|
|
@ -7,6 +7,10 @@ that were made in every particular version.
|
||||||
From version 0.7.6 *Dependency Injector* framework strictly
|
From version 0.7.6 *Dependency Injector* framework strictly
|
||||||
follows `Semantic versioning`_
|
follows `Semantic versioning`_
|
||||||
|
|
||||||
|
Development version
|
||||||
|
-------------------
|
||||||
|
- Add ``Configuration.from_pydantic()`` method to load configuration from a ``pydantic`` settings.
|
||||||
|
|
||||||
4.14.0
|
4.14.0
|
||||||
------
|
------
|
||||||
- Add container providers traversal.
|
- Add container providers traversal.
|
||||||
|
|
|
@ -5,10 +5,11 @@ Configuration provider
|
||||||
|
|
||||||
.. meta::
|
.. meta::
|
||||||
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Configuration,Injection,
|
:keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Configuration,Injection,
|
||||||
Option,Ini,Json,Yaml,Dict,Environment Variable,Load,Read,Get
|
Option,Ini,Json,Yaml,Pydantic,Dict,Environment Variable,Load,Read,Get
|
||||||
:description: Configuration provides configuration options to the other providers. This page
|
:description: Configuration provides configuration options to the other providers. This page
|
||||||
demonstrates how to use Configuration provider to inject the dependencies, load
|
demonstrates how to use Configuration provider to inject the dependencies, load
|
||||||
a configuration from an ini or yaml file, dictionary or an environment variable.
|
a configuration from an ini or yaml file, a dictionary, an environment variable,
|
||||||
|
or a pydantic settings object.
|
||||||
|
|
||||||
.. currentmodule:: dependency_injector.providers
|
.. currentmodule:: dependency_injector.providers
|
||||||
|
|
||||||
|
@ -89,6 +90,38 @@ You can also specify a YAML loader as an argument:
|
||||||
|
|
||||||
*Don't forget to mirror the changes in the requirements file.*
|
*Don't forget to mirror the changes in the requirements file.*
|
||||||
|
|
||||||
|
Loading from a Pydantic settings
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
``Configuration`` provider can load configuration from a ``pydantic`` settings object using the
|
||||||
|
:py:meth:`Configuration.from_pydantic` method:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/providers/configuration/configuration_pydantic.py
|
||||||
|
:language: python
|
||||||
|
:lines: 3-
|
||||||
|
:emphasize-lines: 31
|
||||||
|
|
||||||
|
To get the data from pydantic settings ``Configuration`` provider calls ``Settings.dict()`` method.
|
||||||
|
If you need to pass an argument to this call, use ``.from_pydantic()`` keyword arguments.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
container.config.from_pydantic(Settings(), exclude={'optional'})
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
``Dependency Injector`` doesn't install ``pydantic`` by default.
|
||||||
|
|
||||||
|
You can install the ``Dependency Injector`` with an extra dependency::
|
||||||
|
|
||||||
|
pip install dependency-injector[pydantic]
|
||||||
|
|
||||||
|
or install ``pydantic`` directly::
|
||||||
|
|
||||||
|
pip install pydantic
|
||||||
|
|
||||||
|
*Don't forget to mirror the changes in the requirements file.*
|
||||||
|
|
||||||
Loading from a dictionary
|
Loading from a dictionary
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
@ -211,7 +244,7 @@ Methods ``.from_*()`` in strict mode raise an exception if configuration file do
|
||||||
configuration data is undefined:
|
configuration data is undefined:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 10,15,20,25
|
:emphasize-lines: 10,15,20,25,30
|
||||||
|
|
||||||
class Container(containers.DeclarativeContainer):
|
class Container(containers.DeclarativeContainer):
|
||||||
|
|
||||||
|
@ -231,6 +264,11 @@ configuration data is undefined:
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
try:
|
||||||
|
container.config.from_pydantic(EmptySettings()) # raise exception
|
||||||
|
except ValueError:
|
||||||
|
...
|
||||||
|
|
||||||
try:
|
try:
|
||||||
container.config.from_env('UNDEFINED_ENV_VAR') # raise exception
|
container.config.from_env('UNDEFINED_ENV_VAR') # raise exception
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|
37
examples/providers/configuration/configuration_pydantic.py
Normal file
37
examples/providers/configuration/configuration_pydantic.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
"""`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()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
container = Container()
|
||||||
|
|
||||||
|
container.config.from_pydantic(Settings())
|
||||||
|
|
||||||
|
assert container.config.aws.access_key_id() == 'KEY'
|
||||||
|
assert container.config.aws.secret_access_key() == 'SECRET'
|
||||||
|
assert container.config.optional() == 'default_value'
|
|
@ -10,5 +10,6 @@ mypy
|
||||||
pyyaml
|
pyyaml
|
||||||
httpx
|
httpx
|
||||||
fastapi
|
fastapi
|
||||||
|
pydantic
|
||||||
|
|
||||||
-r requirements-ext.txt
|
-r requirements-ext.txt
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -64,6 +64,9 @@ setup(name='dependency-injector',
|
||||||
'yaml': [
|
'yaml': [
|
||||||
'pyyaml',
|
'pyyaml',
|
||||||
],
|
],
|
||||||
|
'pydantic': [
|
||||||
|
'pydantic',
|
||||||
|
],
|
||||||
'flask': [
|
'flask': [
|
||||||
'flask',
|
'flask',
|
||||||
],
|
],
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -26,6 +26,11 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
yaml = None
|
yaml = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pydantic
|
||||||
|
except ImportError:
|
||||||
|
pydantic = None
|
||||||
|
|
||||||
from . import resources
|
from . import resources
|
||||||
|
|
||||||
|
|
||||||
|
@ -162,6 +167,7 @@ class ConfigurationOption(Provider[Any]):
|
||||||
def update(self, value: Any) -> None: ...
|
def update(self, value: Any) -> None: ...
|
||||||
def from_ini(self, filepath: Union[Path, str], required: bool = False) -> None: ...
|
def from_ini(self, filepath: Union[Path, str], required: bool = False) -> None: ...
|
||||||
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any]=None) -> None: ...
|
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any]=None) -> None: ...
|
||||||
|
def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ...
|
||||||
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
|
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
|
||||||
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ...
|
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ...
|
||||||
|
|
||||||
|
@ -183,6 +189,7 @@ class Configuration(Object[Any]):
|
||||||
def update(self, value: Any) -> None: ...
|
def update(self, value: Any) -> None: ...
|
||||||
def from_ini(self, filepath: Union[Path, str], required: bool = False) -> None: ...
|
def from_ini(self, filepath: Union[Path, str], required: bool = False) -> None: ...
|
||||||
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any]=None) -> None: ...
|
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any]=None) -> None: ...
|
||||||
|
def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ...
|
||||||
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
|
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
|
||||||
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ...
|
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False) -> None: ...
|
||||||
|
|
||||||
|
@ -397,3 +404,8 @@ if yaml:
|
||||||
class YamlLoader(yaml.SafeLoader): ...
|
class YamlLoader(yaml.SafeLoader): ...
|
||||||
else:
|
else:
|
||||||
class YamlLoader: ...
|
class YamlLoader: ...
|
||||||
|
|
||||||
|
if pydantic:
|
||||||
|
PydanticSettings = pydantic.BaseSettings
|
||||||
|
else:
|
||||||
|
PydanticSettings = Any
|
||||||
|
|
|
@ -35,6 +35,11 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
yaml = None
|
yaml = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pydantic
|
||||||
|
except ImportError:
|
||||||
|
pydantic = None
|
||||||
|
|
||||||
from .errors import (
|
from .errors import (
|
||||||
Error,
|
Error,
|
||||||
NoSuchProviderError,
|
NoSuchProviderError,
|
||||||
|
@ -1413,7 +1418,6 @@ cdef class ConfigurationOption(Provider):
|
||||||
'"pip install dependency-injector[yaml]"'
|
'"pip install dependency-injector[yaml]"'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if loader is None:
|
if loader is None:
|
||||||
loader = YamlLoader
|
loader = YamlLoader
|
||||||
|
|
||||||
|
@ -1433,6 +1437,43 @@ cdef class ConfigurationOption(Provider):
|
||||||
current_config = {}
|
current_config = {}
|
||||||
self.override(merge_dicts(current_config, config))
|
self.override(merge_dicts(current_config, config))
|
||||||
|
|
||||||
|
def from_pydantic(self, settings, required=UNDEFINED, **kwargs):
|
||||||
|
"""Load configuration from pydantic settings.
|
||||||
|
|
||||||
|
Loaded configuration is merged recursively over existing configuration.
|
||||||
|
|
||||||
|
:param settings: Pydantic settings instances.
|
||||||
|
:type settings: :py:class:`pydantic.BaseSettings`
|
||||||
|
|
||||||
|
:param required: When required is True, raise an exception if settings dict is empty.
|
||||||
|
:type required: bool
|
||||||
|
|
||||||
|
:param kwargs: Keyword arguments forwarded to ``pydantic.BaseSettings.dict()`` call.
|
||||||
|
:type kwargs: Dict[Any, Any]
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
if pydantic is None:
|
||||||
|
raise Error(
|
||||||
|
'Unable to load pydantic configuration - pydantic is not installed. '
|
||||||
|
'Install pydantic or install Dependency Injector with pydantic extras: '
|
||||||
|
'"pip install dependency-injector[pydantic]"'
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic.BaseSettings):
|
||||||
|
raise Error(
|
||||||
|
'Got settings class, but expect instance: '
|
||||||
|
'instead "{0}" use "{0}()"'.format(settings.__name__)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(settings, pydantic.BaseSettings):
|
||||||
|
raise Error(
|
||||||
|
'Unable to recognize settings instance, expect "pydantic.BaseSettings", '
|
||||||
|
'got {0} instead'.format(settings)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.from_dict(settings.dict(**kwargs), 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.
|
||||||
|
|
||||||
|
@ -1766,6 +1807,43 @@ cdef class Configuration(Object):
|
||||||
current_config = {}
|
current_config = {}
|
||||||
self.override(merge_dicts(current_config, config))
|
self.override(merge_dicts(current_config, config))
|
||||||
|
|
||||||
|
def from_pydantic(self, settings, required=UNDEFINED, **kwargs):
|
||||||
|
"""Load configuration from pydantic settings.
|
||||||
|
|
||||||
|
Loaded configuration is merged recursively over existing configuration.
|
||||||
|
|
||||||
|
:param settings: Pydantic settings instances.
|
||||||
|
:type settings: :py:class:`pydantic.BaseSettings`
|
||||||
|
|
||||||
|
:param required: When required is True, raise an exception if settings dict is empty.
|
||||||
|
:type required: bool
|
||||||
|
|
||||||
|
:param kwargs: Keyword arguments forwarded to ``pydantic.BaseSettings.dict()`` call.
|
||||||
|
:type kwargs: Dict[Any, Any]
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
if pydantic is None:
|
||||||
|
raise Error(
|
||||||
|
'Unable to load pydantic configuration - pydantic is not installed. '
|
||||||
|
'Install pydantic or install Dependency Injector with pydantic extras: '
|
||||||
|
'"pip install dependency-injector[pydantic]"'
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic.BaseSettings):
|
||||||
|
raise Error(
|
||||||
|
'Got settings class, but expect instance: '
|
||||||
|
'instead "{0}" use "{0}()"'.format(settings.__name__)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(settings, pydantic.BaseSettings):
|
||||||
|
raise Error(
|
||||||
|
'Unable to recognize settings instance, expect "pydantic.BaseSettings", '
|
||||||
|
'got {0} instead'.format(settings)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.from_dict(settings.dict(**kwargs), 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.
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,11 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
yaml = None
|
yaml = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pydantic
|
||||||
|
except ImportError:
|
||||||
|
pydantic = None
|
||||||
|
|
||||||
|
|
||||||
class ConfigTests(unittest.TestCase):
|
class ConfigTests(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -642,6 +647,27 @@ class ConfigFromYamlTests(unittest.TestCase):
|
||||||
'"pip install dependency-injector[yaml]"',
|
'"pip install dependency-injector[yaml]"',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_option_no_yaml_installed(self):
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def no_yaml_module():
|
||||||
|
yaml = providers.yaml
|
||||||
|
providers.yaml = None
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
providers.yaml = yaml
|
||||||
|
|
||||||
|
with no_yaml_module():
|
||||||
|
with self.assertRaises(errors.Error) as error:
|
||||||
|
self.config.option.from_yaml(self.config_file_1)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
error.exception.args[0],
|
||||||
|
'Unable to load yaml configuration - PyYAML is not installed. '
|
||||||
|
'Install PyYAML or install Dependency Injector with yaml extras: '
|
||||||
|
'"pip install dependency-injector[yaml]"',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
|
class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -723,6 +749,216 @@ class ConfigFromYamlWithEnvInterpolationTests(unittest.TestCase):
|
||||||
self.assertEqual(self.config.option.section1.value1(), 'test-value')
|
self.assertEqual(self.config.option.section1.value1(), 'test-value')
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFromPydanticTests(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.config = providers.Configuration(name='config')
|
||||||
|
|
||||||
|
class Section11(pydantic.BaseModel):
|
||||||
|
value1 = 1
|
||||||
|
|
||||||
|
class Section12(pydantic.BaseModel):
|
||||||
|
value2 = 2
|
||||||
|
|
||||||
|
class Settings1(pydantic.BaseSettings):
|
||||||
|
section1 = Section11()
|
||||||
|
section2 = Section12()
|
||||||
|
|
||||||
|
self.Settings1 = Settings1
|
||||||
|
|
||||||
|
class Section21(pydantic.BaseModel):
|
||||||
|
value1 = 11
|
||||||
|
value11 = 11
|
||||||
|
|
||||||
|
class Section3(pydantic.BaseModel):
|
||||||
|
value3 = 3
|
||||||
|
|
||||||
|
class Settings2(pydantic.BaseSettings):
|
||||||
|
section1 = Section21()
|
||||||
|
section3 = Section3()
|
||||||
|
|
||||||
|
self.Settings2 = Settings2
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test(self):
|
||||||
|
self.config.from_pydantic(self.Settings1())
|
||||||
|
|
||||||
|
self.assertEqual(self.config(), {'section1': {'value1': 1}, 'section2': {'value2': 2}})
|
||||||
|
self.assertEqual(self.config.section1(), {'value1': 1})
|
||||||
|
self.assertEqual(self.config.section1.value1(), 1)
|
||||||
|
self.assertEqual(self.config.section2(), {'value2': 2})
|
||||||
|
self.assertEqual(self.config.section2.value2(), 2)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_kwarg(self):
|
||||||
|
self.config.from_pydantic(self.Settings1(), exclude={'section2'})
|
||||||
|
|
||||||
|
self.assertEqual(self.config(), {'section1': {'value1': 1}})
|
||||||
|
self.assertEqual(self.config.section1(), {'value1': 1})
|
||||||
|
self.assertEqual(self.config.section1.value1(), 1)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_merge(self):
|
||||||
|
self.config.from_pydantic(self.Settings1())
|
||||||
|
self.config.from_pydantic(self.Settings2())
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.config(),
|
||||||
|
{
|
||||||
|
'section1': {
|
||||||
|
'value1': 11,
|
||||||
|
'value11': 11,
|
||||||
|
},
|
||||||
|
'section2': {
|
||||||
|
'value2': 2,
|
||||||
|
},
|
||||||
|
'section3': {
|
||||||
|
'value3': 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(self.config.section1(), {'value1': 11, 'value11': 11})
|
||||||
|
self.assertEqual(self.config.section1.value1(), 11)
|
||||||
|
self.assertEqual(self.config.section1.value11(), 11)
|
||||||
|
self.assertEqual(self.config.section2(), {'value2': 2})
|
||||||
|
self.assertEqual(self.config.section2.value2(), 2)
|
||||||
|
self.assertEqual(self.config.section3(), {'value3': 3})
|
||||||
|
self.assertEqual(self.config.section3.value3(), 3)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_empty_settings(self):
|
||||||
|
self.config.from_pydantic(pydantic.BaseSettings())
|
||||||
|
self.assertEqual(self.config(), {})
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_empty_settings_strict_mode(self):
|
||||||
|
self.config = providers.Configuration(strict=True)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.config.from_pydantic(pydantic.BaseSettings())
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_option_empty_settings(self):
|
||||||
|
self.config.option.from_pydantic(pydantic.BaseSettings())
|
||||||
|
self.assertEqual(self.config.option(), {})
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_option_empty_settings_strict_mode(self):
|
||||||
|
self.config = providers.Configuration(strict=True)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.config.option.from_pydantic(pydantic.BaseSettings())
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_required_empty_settings(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.config.from_pydantic(pydantic.BaseSettings(), required=True)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_required_option_empty_settings(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.config.option.from_pydantic(pydantic.BaseSettings(), required=True)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_not_required_empty_settings_strict_mode(self):
|
||||||
|
self.config = providers.Configuration(strict=True)
|
||||||
|
self.config.from_pydantic(pydantic.BaseSettings(), required=False)
|
||||||
|
self.assertEqual(self.config(), {})
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_not_required_option_empty_settings_strict_mode(self):
|
||||||
|
self.config = providers.Configuration(strict=True)
|
||||||
|
self.config.option.from_pydantic(pydantic.BaseSettings(), required=False)
|
||||||
|
self.assertEqual(self.config.option(), {})
|
||||||
|
self.assertEqual(self.config(), {'option': {}})
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_not_instance_of_settings(self):
|
||||||
|
with self.assertRaises(errors.Error) as error:
|
||||||
|
self.config.from_pydantic({})
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
error.exception.args[0],
|
||||||
|
'Unable to recognize settings instance, expect "pydantic.BaseSettings", '
|
||||||
|
'got {0} instead'.format({})
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_option_not_instance_of_settings(self):
|
||||||
|
with self.assertRaises(errors.Error) as error:
|
||||||
|
self.config.option.from_pydantic({})
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
error.exception.args[0],
|
||||||
|
'Unable to recognize settings instance, expect "pydantic.BaseSettings", '
|
||||||
|
'got {0} instead'.format({})
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_subclass_instead_of_instance(self):
|
||||||
|
with self.assertRaises(errors.Error) as error:
|
||||||
|
self.config.from_pydantic(self.Settings1)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
error.exception.args[0],
|
||||||
|
'Got settings class, but expect instance: '
|
||||||
|
'instead "Settings1" use "Settings1()"'
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_option_subclass_instead_of_instance(self):
|
||||||
|
with self.assertRaises(errors.Error) as error:
|
||||||
|
self.config.option.from_pydantic(self.Settings1)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
error.exception.args[0],
|
||||||
|
'Got settings class, but expect instance: '
|
||||||
|
'instead "Settings1" use "Settings1()"'
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_no_pydantic_installed(self):
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def no_pydantic_module():
|
||||||
|
pydantic = providers.pydantic
|
||||||
|
providers.pydantic = None
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
providers.pydantic = pydantic
|
||||||
|
|
||||||
|
with no_pydantic_module():
|
||||||
|
with self.assertRaises(errors.Error) as error:
|
||||||
|
self.config.from_pydantic(self.Settings1())
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
error.exception.args[0],
|
||||||
|
'Unable to load pydantic configuration - pydantic is not installed. '
|
||||||
|
'Install pydantic or install Dependency Injector with pydantic extras: '
|
||||||
|
'"pip install dependency-injector[pydantic]"',
|
||||||
|
)
|
||||||
|
|
||||||
|
@unittest.skipIf(sys.version_info[:2] < (3, 6), 'Pydantic supports Python 3.6+')
|
||||||
|
def test_option_no_pydantic_installed(self):
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def no_pydantic_module():
|
||||||
|
pydantic = providers.pydantic
|
||||||
|
providers.pydantic = None
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
providers.pydantic = pydantic
|
||||||
|
|
||||||
|
with no_pydantic_module():
|
||||||
|
with self.assertRaises(errors.Error) as error:
|
||||||
|
self.config.option.from_pydantic(self.Settings1())
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
error.exception.args[0],
|
||||||
|
'Unable to load pydantic configuration - pydantic is not installed. '
|
||||||
|
'Install pydantic or install Dependency Injector with pydantic extras: '
|
||||||
|
'"pip install dependency-injector[pydantic]"',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigFromDict(unittest.TestCase):
|
class ConfigFromDict(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user