Configuration(yaml_files=[...]) (#522)

* Add provider changes and tests

* Move config test fixtures

* Fix issue with explicit providing of envs_required=False for configuration from_*()

* Implement container API

* Increase priority of overriding from context

* Add docs and example

* Update changelog

* Update changelog
This commit is contained in:
Roman Mogylatov 2021-10-23 21:46:50 -04:00 committed by GitHub
parent b97862cb9f
commit b16b190ff7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 9428 additions and 8300 deletions

View File

@ -14,6 +14,9 @@ Develop
``container.wire(modules=["yourapp.module1"])``. ``container.wire(modules=["yourapp.module1"])``.
- Add container wiring configuration ``wiring_config = containers.WiringConfiguration()``. - Add container wiring configuration ``wiring_config = containers.WiringConfiguration()``.
- 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.
- Fix ``envs_required=False`` behavior in ``Configuration.from_*()`` methods
to give a priority to the explicitly provided value.
- 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

@ -72,6 +72,20 @@ where ``examples/providers/configuration/config.yml`` is:
.. literalinclude:: ../../examples/providers/configuration/config.yml .. literalinclude:: ../../examples/providers/configuration/config.yml
:language: ini :language: ini
Alternatively, you can provide a path to the YAML file over the configuration provider argument. In that case,
the container will call ``config.from_yaml()`` automatically:
.. code-block:: python
:emphasize-lines: 3
class Container(containers.DeclarativeContainer):
config = providers.Configuration(yaml_files=["./config.yml"])
if __name__ == "__main__":
container = Container() # Config is loaded from ./config.yml
:py:meth:`Configuration.from_yaml` method supports environment variables interpolation. :py:meth:`Configuration.from_yaml` method supports environment variables interpolation.
.. code-block:: ini .. code-block:: ini

View File

@ -0,0 +1,25 @@
"""`Configuration` provider values loading example."""
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration(yaml_files=["./config.yml"])
if __name__ == "__main__":
container = Container()
assert container.config() == {
"aws": {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
}
assert container.config.aws() == {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
}
assert container.config.aws.access_key_id() == "KEY"
assert container.config.aws.secret_access_key() == "SECRET"

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,7 @@ class Container:
dependencies: Dict[str, Provider] dependencies: Dict[str, Provider]
overridden: Tuple[Provider] overridden: Tuple[Provider]
wiring_config: WiringConfiguration wiring_config: WiringConfiguration
auto_load_config: bool = True
__self__: Self __self__: Self
def __init__(self) -> None: ... def __init__(self) -> None: ...
def __deepcopy__(self, memo: Optional[Dict[str, Any]]) -> Provider: ... def __deepcopy__(self, memo: Optional[Dict[str, Any]]) -> Provider: ...
@ -58,6 +59,7 @@ class Container:
def unwire(self) -> None: ... def unwire(self) -> None: ...
def init_resources(self) -> Optional[Awaitable]: ... def init_resources(self) -> Optional[Awaitable]: ...
def shutdown_resources(self) -> Optional[Awaitable]: ... def shutdown_resources(self) -> Optional[Awaitable]: ...
def load_config(self) -> None: ...
def apply_container_providers_overridings(self) -> None: ... def apply_container_providers_overridings(self) -> None: ...
def reset_singletons(self) -> SingletonResetContext[C_Base]: ... def reset_singletons(self) -> SingletonResetContext[C_Base]: ...
def check_dependencies(self) -> None: ... def check_dependencies(self) -> None: ...

View File

@ -377,6 +377,12 @@ class DynamicContainer(Container):
else: else:
return _sync_ordered_shutdown(resources) return _sync_ordered_shutdown(resources)
def load_config(self):
"""Load configuration."""
config: providers.Configuration
for config in self.traverse(types=[providers.Configuration]):
config.load()
def apply_container_providers_overridings(self): def apply_container_providers_overridings(self):
"""Apply container providers' overridings.""" """Apply container providers' overridings."""
for provider in self.traverse(types=[providers.Container]): for provider in self.traverse(types=[providers.Container]):
@ -671,6 +677,12 @@ class DeclarativeContainer(Container):
:type: WiringConfiguration :type: WiringConfiguration
""" """
auto_load_config = True
"""Automatically load configuration when the container is created.
:type: bool
"""
cls_providers = dict() cls_providers = dict()
"""Read-only dictionary of current container providers. """Read-only dictionary of current container providers.
@ -717,6 +729,9 @@ class DeclarativeContainer(Container):
for name, provider in copied_providers.items(): for name, provider in copied_providers.items():
container.set_provider(name, provider) container.set_provider(name, provider)
if cls.auto_load_config:
container.load_config()
container.override_providers(**overriding_providers) container.override_providers(**overriding_providers)
container.apply_container_providers_overridings() container.apply_container_providers_overridings()

File diff suppressed because it is too large Load Diff

View File

@ -114,6 +114,7 @@ cdef class Configuration(Object):
cdef str __name cdef str __name
cdef bint __strict cdef bint __strict
cdef dict __children cdef dict __children
cdef list __yaml_files
cdef object __weakref__ cdef object __weakref__

View File

@ -216,7 +216,7 @@ class TypedConfigurationOption(Callable[T]):
class Configuration(Object[Any]): class Configuration(Object[Any]):
DEFAULT_NAME: str = 'config' DEFAULT_NAME: str = 'config'
def __init__(self, name: str = DEFAULT_NAME, default: Optional[Any] = None, *, strict: bool = False) -> None: ... def __init__(self, name: str = DEFAULT_NAME, default: Optional[Any] = None, *, strict: bool = False, yaml_files: Optional[_Iterable[Union[Path, str]]] = None) -> None: ...
def __enter__(self) -> Configuration : ... def __enter__(self) -> Configuration : ...
def __exit__(self, *exc_info: Any) -> None: ... def __exit__(self, *exc_info: Any) -> None: ...
def __getattr__(self, item: str) -> ConfigurationOption: ... def __getattr__(self, item: str) -> ConfigurationOption: ...
@ -234,6 +234,11 @@ class Configuration(Object[Any]):
def get_children(self) -> _Dict[str, ConfigurationOption]: ... def get_children(self) -> _Dict[str, ConfigurationOption]: ...
def set_children(self, children: _Dict[str, ConfigurationOption]) -> Configuration: ... def set_children(self, children: _Dict[str, ConfigurationOption]) -> Configuration: ...
def get_yaml_files(self) -> _List[Union[Path, str]]: ...
def set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def load(self, required: bool = False, envs_required: bool = False) -> None: ...
def get(self, selector: str) -> Any: ... def get(self, selector: str) -> Any: ...
def set(self, selector: str, value: Any) -> OverridingContext[P]: ... def set(self, selector: str, value: Any) -> OverridingContext[P]: ...
def reset_cache(self) -> None: ... def reset_cache(self) -> None: ...

View File

@ -1527,7 +1527,7 @@ cdef class ConfigurationOption(Provider):
""" """
self.override(value) self.override(value)
def from_ini(self, filepath, required=UNDEFINED, envs_required=False): def from_ini(self, filepath, required=UNDEFINED, envs_required=UNDEFINED):
"""Load configuration from the ini file. """Load configuration from the ini file.
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
@ -1546,7 +1546,7 @@ cdef class ConfigurationOption(Provider):
try: try:
parser = _parse_ini_file( parser = _parse_ini_file(
filepath, filepath,
envs_required=envs_required or self._is_strict_mode_enabled(), envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
) )
except IOError as exception: except IOError as exception:
if required is not False \ if required is not False \
@ -1565,7 +1565,7 @@ cdef class ConfigurationOption(Provider):
current_config = {} current_config = {}
self.override(merge_dicts(current_config, config)) self.override(merge_dicts(current_config, config))
def from_yaml(self, filepath, required=UNDEFINED, loader=None, envs_required=False): def from_yaml(self, filepath, required=UNDEFINED, loader=None, envs_required=UNDEFINED):
"""Load configuration from the yaml file. """Load configuration from the yaml file.
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
@ -1607,7 +1607,7 @@ cdef class ConfigurationOption(Provider):
config_content = _resolve_config_env_markers( config_content = _resolve_config_env_markers(
config_content, config_content,
envs_required=envs_required or self._is_strict_mode_enabled(), envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
) )
config = yaml.load(config_content, loader) config = yaml.load(config_content, loader)
@ -1755,14 +1755,19 @@ cdef class Configuration(Object):
DEFAULT_NAME = 'config' DEFAULT_NAME = 'config'
def __init__(self, name=DEFAULT_NAME, default=None, strict=False): def __init__(self, name=DEFAULT_NAME, default=None, strict=False, yaml_files=None):
self.__name = name self.__name = name
self.__strict = strict self.__strict = strict
self.__children = {} self.__children = {}
self.__yaml_files = []
super().__init__(provides={}) super().__init__(provides={})
self.set_default(default) self.set_default(default)
if yaml_files is None:
yaml_files = []
self.set_yaml_files(yaml_files)
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:
@ -1773,6 +1778,7 @@ cdef class Configuration(Object):
copied.set_default(self.get_default()) copied.set_default(self.get_default())
copied.set_strict(self.get_strict()) copied.set_strict(self.get_strict())
copied.set_children(deepcopy(self.get_children(), memo)) copied.set_children(deepcopy(self.get_children(), memo))
copied.set_yaml_files(self.get_yaml_files())
self._copy_overridings(copied, memo) self._copy_overridings(copied, memo)
return copied return copied
@ -1846,6 +1852,35 @@ cdef class Configuration(Object):
self.__children = children self.__children = children
return self return self
def get_yaml_files(self):
"""Return list of YAML files."""
return self.__yaml_files
def set_yaml_files(self, files):
"""Set list of YAML files."""
self.__yaml_files = list(files)
return self
def load(self, required=UNDEFINED, envs_required=UNDEFINED):
"""Load configuration.
This method loads configuration from configuration files or pydantic settings that
were set earlier with set_*() methods or provided to the __init__(), e.g.:
.. code-block:: python
config = providers.Configuration(yaml_files=[file1, file2])
config.load()
:param required: When required is True, raise an exception if file does not exist.
:type required: bool
:param envs_required: When True, raises an error on undefined environment variable.
:type envs_required: bool
"""
for file in self.get_yaml_files():
self.from_yaml(file, required=required, envs_required=envs_required)
def get(self, selector, required=False): def get(self, selector, required=False):
"""Return configuration option. """Return configuration option.
@ -1963,7 +1998,7 @@ cdef class Configuration(Object):
""" """
self.override(value) self.override(value)
def from_ini(self, filepath, required=UNDEFINED, envs_required=False): def from_ini(self, filepath, required=UNDEFINED, envs_required=UNDEFINED):
"""Load configuration from the ini file. """Load configuration from the ini file.
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
@ -1982,7 +2017,7 @@ cdef class Configuration(Object):
try: try:
parser = _parse_ini_file( parser = _parse_ini_file(
filepath, filepath,
envs_required=envs_required or self._is_strict_mode_enabled(), envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
) )
except IOError as exception: except IOError as exception:
if required is not False \ if required is not False \
@ -2001,7 +2036,7 @@ cdef class Configuration(Object):
current_config = {} current_config = {}
self.override(merge_dicts(current_config, config)) self.override(merge_dicts(current_config, config))
def from_yaml(self, filepath, required=UNDEFINED, loader=None, envs_required=False): def from_yaml(self, filepath, required=UNDEFINED, loader=None, envs_required=UNDEFINED):
"""Load configuration from the yaml file. """Load configuration from the yaml file.
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
@ -2043,7 +2078,7 @@ cdef class Configuration(Object):
config_content = _resolve_config_env_markers( config_content = _resolve_config_env_markers(
config_content, config_content,
envs_required=envs_required or self._is_strict_mode_enabled(), envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
) )
config = yaml.load(config_content, loader) config = yaml.load(config_content, loader)

View File

@ -0,0 +1,52 @@
"""Tests for container config loading."""
from dependency_injector import containers, providers
from pytest import fixture
@fixture
def yaml_config_file(tmp_path):
yaml_config_file = str(tmp_path / "config.yml")
with open(yaml_config_file, "w") as file:
file.write(
"section1:\n"
" value1: yaml-loaded\n"
)
return yaml_config_file
def test_auto_load(yaml_config_file):
class ContainerWithConfig(containers.DeclarativeContainer):
config = providers.Configuration(yaml_files=[yaml_config_file])
container = ContainerWithConfig()
assert container.config.section1.value1() == "yaml-loaded"
def test_auto_load_and_overriding(yaml_config_file):
class ContainerWithConfig(containers.DeclarativeContainer):
config = providers.Configuration(yaml_files=[yaml_config_file])
container = ContainerWithConfig(config={"section1": {"value1": "overridden"}})
assert container.config.section1.value1() == "overridden"
def test_manual_load(yaml_config_file):
class ContainerWithConfig(containers.DeclarativeContainer):
auto_load_config = False
config = providers.Configuration(yaml_files=[yaml_config_file])
container = ContainerWithConfig()
assert container.config.section1.value1() is None
container.load_config()
assert container.config.section1.value1() == "yaml-loaded"
def test_load_config_does_not_affect_class(yaml_config_file):
class ContainerWithConfig(containers.DeclarativeContainer):
config = providers.Configuration(yaml_files=[yaml_config_file])
assert ContainerWithConfig.config.section1.value1() is None
_ = ContainerWithConfig()
assert ContainerWithConfig.config.section1.value1() is None

View File

@ -1,4 +1,4 @@
"""Tests for container self provier.""" """Tests for container self provider."""
from dependency_injector import containers, providers, errors from dependency_injector import containers, providers, errors
from pytest import raises from pytest import raises

View File

@ -1,7 +1,9 @@
"""Fixtures module.""" """Fixtures module."""
import os
from dependency_injector import providers from dependency_injector import providers
from pytest import fixture from pytest import fixture, mark
@fixture @fixture
@ -17,3 +19,94 @@ def config(config_type):
return providers.Configuration() return providers.Configuration()
else: else:
raise ValueError("Undefined config type \"{0}\"".format(config_type)) raise ValueError("Undefined config type \"{0}\"".format(config_type))
@fixture
def ini_config_file_1(tmp_path):
config_file = str(tmp_path / "config_1.ini")
with open(config_file, "w") as file:
file.write(
"[section1]\n"
"value1=1\n"
"\n"
"[section2]\n"
"value2=2\n"
)
return config_file
@fixture
def ini_config_file_2(tmp_path):
config_file = str(tmp_path / "config_2.ini")
with open(config_file, "w") as file:
file.write(
"[section1]\n"
"value1=11\n"
"value11=11\n"
"[section3]\n"
"value3=3\n"
)
return config_file
@fixture
def ini_config_file_3(tmp_path):
ini_config_file_3 = str(tmp_path / "config_3.ini")
with open(ini_config_file_3, "w") as file:
file.write(
"[section1]\n"
"value1=${CONFIG_TEST_ENV}\n"
"value2=${CONFIG_TEST_PATH}/path\n"
)
return ini_config_file_3
@fixture
def yaml_config_file_1(tmp_path):
config_file = str(tmp_path / "config_1.yml")
with open(config_file, "w") as file:
file.write(
"section1:\n"
" value1: 1\n"
"\n"
"section2:\n"
" value2: 2\n"
)
return config_file
@fixture
def yaml_config_file_2(tmp_path):
config_file = str(tmp_path / "config_2.yml")
with open(config_file, "w") as file:
file.write(
"section1:\n"
" value1: 11\n"
" value11: 11\n"
"section3:\n"
" value3: 3\n"
)
return config_file
@fixture
def yaml_config_file_3(tmp_path):
yaml_config_file_3 = str(tmp_path / "config_3.yml")
with open(yaml_config_file_3, "w") as file:
file.write(
"section1:\n"
" value1: ${CONFIG_TEST_ENV}\n"
" value2: ${CONFIG_TEST_PATH}/path\n"
)
return yaml_config_file_3
@fixture(autouse=True)
def environment_variables():
os.environ["CONFIG_TEST_ENV"] = "test-value"
os.environ["CONFIG_TEST_PATH"] = "test-path"
os.environ["DEFINED"] = "defined"
yield
os.environ.pop("CONFIG_TEST_ENV", None)
os.environ.pop("CONFIG_TEST_PATH", None)
os.environ.pop("DEFINED", None)

View File

@ -1,15 +1,6 @@
"""Configuration.from_env() tests.""" """Configuration.from_env() tests."""
import os from pytest import mark, raises
from pytest import fixture, mark, raises
@fixture(autouse=True)
def environment_variables():
os.environ["CONFIG_TEST_ENV"] = "test-value"
yield
os.environ.pop("CONFIG_TEST_ENV", None)
def test(config): def test(config):

View File

@ -1,39 +1,11 @@
"""Configuration.from_ini() tests.""" """Configuration.from_ini() tests."""
from dependency_injector import errors from dependency_injector import errors
from pytest import fixture, mark, raises from pytest import mark, raises
@fixture def test(config, ini_config_file_1):
def config_file_1(tmp_path): config.from_ini(ini_config_file_1)
config_file = str(tmp_path / "config_1.ini")
with open(config_file, "w") as file:
file.write(
"[section1]\n"
"value1=1\n"
"\n"
"[section2]\n"
"value2=2\n"
)
return config_file
@fixture
def config_file_2(tmp_path):
config_file = str(tmp_path / "config_2.ini")
with open(config_file, "w") as file:
file.write(
"[section1]\n"
"value1=11\n"
"value11=11\n"
"[section3]\n"
"value3=3\n"
)
return config_file
def test(config, config_file_1):
config.from_ini(config_file_1)
assert config() == {"section1": {"value1": "1"}, "section2": {"value2": "2"}} assert config() == {"section1": {"value1": "1"}, "section2": {"value2": "2"}}
assert config.section1() == {"value1": "1"} assert config.section1() == {"value1": "1"}
@ -42,8 +14,8 @@ def test(config, config_file_1):
assert config.section2.value2() == "2" assert config.section2.value2() == "2"
def test_option(config, config_file_1): def test_option(config, ini_config_file_1):
config.option.from_ini(config_file_1) config.option.from_ini(ini_config_file_1)
assert config() == {"option": {"section1": {"value1": "1"}, "section2": {"value2": "2"}}} assert config() == {"option": {"section1": {"value1": "1"}, "section2": {"value2": "2"}}}
assert config.option() == {"section1": {"value1": "1"}, "section2": {"value2": "2"}} assert config.option() == {"section1": {"value1": "1"}, "section2": {"value2": "2"}}
@ -53,9 +25,9 @@ def test_option(config, config_file_1):
assert config.option.section2.value2() == "2" assert config.option.section2.value2() == "2"
def test_merge(config, config_file_1, config_file_2): def test_merge(config, ini_config_file_1, ini_config_file_2):
config.from_ini(config_file_1) config.from_ini(ini_config_file_1)
config.from_ini(config_file_2) config.from_ini(ini_config_file_2)
assert config() == { assert config() == {
"section1": { "section1": {

View File

@ -2,34 +2,11 @@
import os import os
from pytest import fixture, mark, raises from pytest import mark, raises
@fixture def test_env_variable_interpolation(config, ini_config_file_3):
def config_file(tmp_path): config.from_ini(ini_config_file_3)
config_file = str(tmp_path / "config_1.ini")
with open(config_file, "w") as file:
file.write(
"[section1]\n"
"value1=${CONFIG_TEST_ENV}\n"
"value2=${CONFIG_TEST_PATH}/path\n"
)
return config_file
@fixture(autouse=True)
def environment_variables():
os.environ["CONFIG_TEST_ENV"] = "test-value"
os.environ["CONFIG_TEST_PATH"] = "test-path"
os.environ["DEFINED"] = "defined"
yield
os.environ.pop("CONFIG_TEST_ENV", None)
os.environ.pop("CONFIG_TEST_PATH", None)
os.environ.pop("DEFINED", None)
def test_env_variable_interpolation(config, config_file):
config.from_ini(config_file)
assert config() == { assert config() == {
"section1": { "section1": {
@ -45,11 +22,11 @@ def test_env_variable_interpolation(config, config_file):
assert config.section1.value2() == "test-path/path" assert config.section1.value2() == "test-path/path"
def test_missing_envs_not_required(config, config_file): def test_missing_envs_not_required(config, ini_config_file_3):
del os.environ["CONFIG_TEST_ENV"] del os.environ["CONFIG_TEST_ENV"]
del os.environ["CONFIG_TEST_PATH"] del os.environ["CONFIG_TEST_PATH"]
config.from_ini(config_file) config.from_ini(ini_config_file_3)
assert config() == { assert config() == {
"section1": { "section1": {
@ -65,32 +42,43 @@ def test_missing_envs_not_required(config, config_file):
assert config.section1.value2() == "/path" assert config.section1.value2() == "/path"
def test_missing_envs_required(config, config_file): def test_missing_envs_required(config, ini_config_file_3):
with open(config_file, "w") as file: with open(ini_config_file_3, "w") as file:
file.write( file.write(
"[section]\n" "[section]\n"
"undefined=${UNDEFINED}\n" "undefined=${UNDEFINED}\n"
) )
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""): with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.from_ini(config_file, envs_required=True) config.from_ini(ini_config_file_3, envs_required=True)
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_missing_envs_strict_mode(config, config_file): def test_missing_envs_strict_mode(config, ini_config_file_3):
with open(config_file, "w") as file: with open(ini_config_file_3, "w") as file:
file.write( file.write(
"[section]\n" "[section]\n"
"undefined=${UNDEFINED}\n" "undefined=${UNDEFINED}\n"
) )
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""): with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.from_ini(config_file) config.from_ini(ini_config_file_3)
def test_option_missing_envs_not_required(config, config_file): @mark.parametrize("config_type", ["strict"])
def test_missing_envs_not_required_in_strict_mode(config, ini_config_file_3):
with open(ini_config_file_3, "w") as file:
file.write(
"[section]\n"
"undefined=${UNDEFINED}\n"
)
config.from_ini(ini_config_file_3, envs_required=False)
assert config.section.undefined() == ""
def test_option_missing_envs_not_required(config, ini_config_file_3):
del os.environ["CONFIG_TEST_ENV"] del os.environ["CONFIG_TEST_ENV"]
del os.environ["CONFIG_TEST_PATH"] del os.environ["CONFIG_TEST_PATH"]
config.option.from_ini(config_file) config.option.from_ini(ini_config_file_3)
assert config.option() == { assert config.option() == {
"section1": { "section1": {
@ -106,29 +94,41 @@ def test_option_missing_envs_not_required(config, config_file):
assert config.option.section1.value2() == "/path" assert config.option.section1.value2() == "/path"
def test_option_missing_envs_required(config, config_file): def test_option_missing_envs_required(config, ini_config_file_3):
with open(config_file, "w") as file: with open(ini_config_file_3, "w") as file:
file.write( file.write(
"[section]\n" "[section]\n"
"undefined=${UNDEFINED}\n" "undefined=${UNDEFINED}\n"
) )
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""): with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.option.from_ini(config_file, envs_required=True) config.option.from_ini(ini_config_file_3, envs_required=True)
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_option_missing_envs_strict_mode(config, config_file): def test_option_missing_envs_not_required_in_strict_mode(config, ini_config_file_3):
with open(config_file, "w") as file: config.override({"option": {}})
with open(ini_config_file_3, "w") as file:
file.write(
"[section]\n"
"undefined=${UNDEFINED}\n"
)
config.option.from_ini(ini_config_file_3, envs_required=False)
assert config.option.section.undefined() == ""
@mark.parametrize("config_type", ["strict"])
def test_option_missing_envs_strict_mode(config, ini_config_file_3):
with open(ini_config_file_3, "w") as file:
file.write( file.write(
"[section]\n" "[section]\n"
"undefined=${UNDEFINED}\n" "undefined=${UNDEFINED}\n"
) )
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""): with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.option.from_ini(config_file) config.option.from_ini(ini_config_file_3)
def test_default_values(config, config_file): def test_default_values(config, ini_config_file_3):
with open(config_file, "w") as file: with open(ini_config_file_3, "w") as file:
file.write( file.write(
"[section]\n" "[section]\n"
"defined_with_default=${DEFINED:default}\n" "defined_with_default=${DEFINED:default}\n"
@ -136,7 +136,7 @@ def test_default_values(config, config_file):
"complex=${DEFINED}/path/${DEFINED:default}/${UNDEFINED}/${UNDEFINED:default}\n" "complex=${DEFINED}/path/${DEFINED:default}/${UNDEFINED}/${UNDEFINED:default}\n"
) )
config.from_ini(config_file) config.from_ini(ini_config_file_3)
assert config.section() == { assert config.section() == {
"defined_with_default": "defined", "defined_with_default": "defined",

View File

@ -4,34 +4,6 @@ from dependency_injector import providers, errors
from pytest import fixture, mark, raises from pytest import fixture, mark, raises
@fixture
def config_file_1(tmp_path):
config_file = str(tmp_path / "config_1.ini")
with open(config_file, "w") as file:
file.write(
"section1:\n"
" value1: 1\n"
"\n"
"section2:\n"
" value2: 2\n"
)
return config_file
@fixture
def config_file_2(tmp_path):
config_file = str(tmp_path / "config_2.ini")
with open(config_file, "w") as file:
file.write(
"section1:\n"
" value1: 11\n"
" value11: 11\n"
"section3:\n"
" value3: 3\n"
)
return config_file
@fixture @fixture
def no_yaml_module_installed(): def no_yaml_module_installed():
yaml = providers.yaml yaml = providers.yaml
@ -40,8 +12,8 @@ def no_yaml_module_installed():
providers.yaml = yaml providers.yaml = yaml
def test(config, config_file_1): def test(config, yaml_config_file_1):
config.from_yaml(config_file_1) config.from_yaml(yaml_config_file_1)
assert config() == {"section1": {"value1": 1}, "section2": {"value2": 2}} assert config() == {"section1": {"value1": 1}, "section2": {"value2": 2}}
assert config.section1() == {"value1": 1} assert config.section1() == {"value1": 1}
@ -50,9 +22,9 @@ def test(config, config_file_1):
assert config.section2.value2() == 2 assert config.section2.value2() == 2
def test_merge(config, config_file_1, config_file_2): def test_merge(config, yaml_config_file_1, yaml_config_file_2):
config.from_yaml(config_file_1) config.from_yaml(yaml_config_file_1)
config.from_yaml(config_file_2) config.from_yaml(yaml_config_file_2)
assert config() == { assert config() == {
"section1": { "section1": {
@ -121,9 +93,9 @@ def test_not_required_option_file_does_not_exist_strict_mode(config):
@mark.usefixtures("no_yaml_module_installed") @mark.usefixtures("no_yaml_module_installed")
def test_no_yaml_installed(config, config_file_1): def test_no_yaml_installed(config, yaml_config_file_1):
with raises(errors.Error) as error: with raises(errors.Error) as error:
config.from_yaml(config_file_1) config.from_yaml(yaml_config_file_1)
assert error.value.args[0] == ( assert error.value.args[0] == (
"Unable to load yaml configuration - PyYAML is not installed. " "Unable to load yaml configuration - PyYAML is not installed. "
"Install PyYAML or install Dependency Injector with yaml extras: " "Install PyYAML or install Dependency Injector with yaml extras: "
@ -132,9 +104,9 @@ def test_no_yaml_installed(config, config_file_1):
@mark.usefixtures("no_yaml_module_installed") @mark.usefixtures("no_yaml_module_installed")
def test_option_no_yaml_installed(config, config_file_1): def test_option_no_yaml_installed(config, yaml_config_file_1):
with raises(errors.Error) as error: with raises(errors.Error) as error:
config.option.from_yaml(config_file_1) config.option.from_yaml(yaml_config_file_1)
assert error.value.args[0] == ( assert error.value.args[0] == (
"Unable to load yaml configuration - PyYAML is not installed. " "Unable to load yaml configuration - PyYAML is not installed. "
"Install PyYAML or install Dependency Injector with yaml extras: " "Install PyYAML or install Dependency Injector with yaml extras: "

View File

@ -3,34 +3,11 @@
import os import os
import yaml import yaml
from pytest import fixture, mark, raises from pytest import mark, raises
@fixture def test_env_variable_interpolation(config, yaml_config_file_3):
def config_file(tmp_path): config.from_yaml(yaml_config_file_3)
config_file = str(tmp_path / "config_1.ini")
with open(config_file, "w") as file:
file.write(
"section1:\n"
" value1: ${CONFIG_TEST_ENV}\n"
" value2: ${CONFIG_TEST_PATH}/path\n"
)
return config_file
@fixture(autouse=True)
def environment_variables():
os.environ["CONFIG_TEST_ENV"] = "test-value"
os.environ["CONFIG_TEST_PATH"] = "test-path"
os.environ["DEFINED"] = "defined"
yield
os.environ.pop("CONFIG_TEST_ENV", None)
os.environ.pop("CONFIG_TEST_PATH", None)
os.environ.pop("DEFINED", None)
def test_env_variable_interpolation(config, config_file):
config.from_yaml(config_file)
assert config() == { assert config() == {
"section1": { "section1": {
@ -46,11 +23,11 @@ def test_env_variable_interpolation(config, config_file):
assert config.section1.value2() == "test-path/path" assert config.section1.value2() == "test-path/path"
def test_missing_envs_not_required(config, config_file): def test_missing_envs_not_required(config, yaml_config_file_3):
del os.environ["CONFIG_TEST_ENV"] del os.environ["CONFIG_TEST_ENV"]
del os.environ["CONFIG_TEST_PATH"] del os.environ["CONFIG_TEST_PATH"]
config.from_yaml(config_file) config.from_yaml(yaml_config_file_3)
assert config() == { assert config() == {
"section1": { "section1": {
@ -66,32 +43,43 @@ def test_missing_envs_not_required(config, config_file):
assert config.section1.value2() == "/path" assert config.section1.value2() == "/path"
def test_missing_envs_required(config, config_file): def test_missing_envs_required(config, yaml_config_file_3):
with open(config_file, "w") as file: with open(yaml_config_file_3, "w") as file:
file.write( file.write(
"section:\n" "section:\n"
" undefined: ${UNDEFINED}\n" " undefined: ${UNDEFINED}\n"
) )
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""): with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.from_yaml(config_file, envs_required=True) config.from_yaml(yaml_config_file_3, envs_required=True)
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_missing_envs_strict_mode(config, config_file): def test_missing_envs_strict_mode(config, yaml_config_file_3):
with open(config_file, "w") as file: with open(yaml_config_file_3, "w") as file:
file.write( file.write(
"section:\n" "section:\n"
" undefined: ${UNDEFINED}\n" " undefined: ${UNDEFINED}\n"
) )
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""): with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.from_yaml(config_file) config.from_yaml(yaml_config_file_3)
def test_option_missing_envs_not_required(config, config_file): @mark.parametrize("config_type", ["strict"])
def test_missing_envs_not_required_in_strict_mode(config, yaml_config_file_3):
with open(yaml_config_file_3, "w") as file:
file.write(
"section:\n"
" undefined: ${UNDEFINED}\n"
)
config.from_yaml(yaml_config_file_3, envs_required=False)
assert config.section.undefined() is None
def test_option_missing_envs_not_required(config, yaml_config_file_3):
del os.environ["CONFIG_TEST_ENV"] del os.environ["CONFIG_TEST_ENV"]
del os.environ["CONFIG_TEST_PATH"] del os.environ["CONFIG_TEST_PATH"]
config.option.from_yaml(config_file) config.option.from_yaml(yaml_config_file_3)
assert config.option() == { assert config.option() == {
"section1": { "section1": {
@ -107,29 +95,41 @@ def test_option_missing_envs_not_required(config, config_file):
assert config.option.section1.value2() == "/path" assert config.option.section1.value2() == "/path"
def test_option_missing_envs_required(config, config_file): def test_option_missing_envs_required(config, yaml_config_file_3):
with open(config_file, "w") as file: with open(yaml_config_file_3, "w") as file:
file.write( file.write(
"section:\n" "section:\n"
" undefined: ${UNDEFINED}\n" " undefined: ${UNDEFINED}\n"
) )
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""): with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.option.from_yaml(config_file, envs_required=True) config.option.from_yaml(yaml_config_file_3, envs_required=True)
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_option_missing_envs_strict_mode(config, config_file): def test_option_missing_envs_not_required_in_strict_mode(config, yaml_config_file_3):
with open(config_file, "w") as file: config.override({"option": {}})
with open(yaml_config_file_3, "w") as file:
file.write(
"section:\n"
" undefined: ${UNDEFINED}\n"
)
config.option.from_yaml(yaml_config_file_3, envs_required=False)
assert config.option.section.undefined() is None
@mark.parametrize("config_type", ["strict"])
def test_option_missing_envs_strict_mode(config, yaml_config_file_3):
with open(yaml_config_file_3, "w") as file:
file.write( file.write(
"section:\n" "section:\n"
" undefined: ${UNDEFINED}\n" " undefined: ${UNDEFINED}\n"
) )
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""): with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.option.from_yaml(config_file) config.option.from_yaml(yaml_config_file_3)
def test_default_values(config, config_file): def test_default_values(config, yaml_config_file_3):
with open(config_file, "w") as file: with open(yaml_config_file_3, "w") as file:
file.write( file.write(
"section:\n" "section:\n"
" defined_with_default: ${DEFINED:default}\n" " defined_with_default: ${DEFINED:default}\n"
@ -137,7 +137,7 @@ def test_default_values(config, config_file):
" complex: ${DEFINED}/path/${DEFINED:default}/${UNDEFINED}/${UNDEFINED:default}\n" " complex: ${DEFINED}/path/${DEFINED:default}/${UNDEFINED}/${UNDEFINED:default}\n"
) )
config.from_yaml(config_file) config.from_yaml(yaml_config_file_3)
assert config.section() == { assert config.section() == {
"defined_with_default": "defined", "defined_with_default": "defined",
@ -146,8 +146,8 @@ def test_default_values(config, config_file):
} }
def test_option_env_variable_interpolation(config, config_file): def test_option_env_variable_interpolation(config, yaml_config_file_3):
config.option.from_yaml(config_file) config.option.from_yaml(yaml_config_file_3)
assert config.option() == { assert config.option() == {
"section1": { "section1": {
@ -163,8 +163,8 @@ def test_option_env_variable_interpolation(config, config_file):
assert config.option.section1.value2() == "test-path/path" assert config.option.section1.value2() == "test-path/path"
def test_env_variable_interpolation_custom_loader(config, config_file): def test_env_variable_interpolation_custom_loader(config, yaml_config_file_3):
config.from_yaml(config_file, loader=yaml.UnsafeLoader) config.from_yaml(yaml_config_file_3, loader=yaml.UnsafeLoader)
assert config.section1() == { assert config.section1() == {
"value1": "test-value", "value1": "test-value",
@ -174,8 +174,8 @@ def test_env_variable_interpolation_custom_loader(config, config_file):
assert config.section1.value2() == "test-path/path" assert config.section1.value2() == "test-path/path"
def test_option_env_variable_interpolation_custom_loader(config, config_file): def test_option_env_variable_interpolation_custom_loader(config, yaml_config_file_3):
config.option.from_yaml(config_file, loader=yaml.UnsafeLoader) config.option.from_yaml(yaml_config_file_3, loader=yaml.UnsafeLoader)
assert config.option.section1() == { assert config.option.section1() == {
"value1": "test-value", "value1": "test-value",

View File

@ -0,0 +1,97 @@
"""Configuration(yaml_files=[...]) tests."""
from dependency_injector import providers
from pytest import fixture, mark, raises
@fixture
def config(config_type, yaml_config_file_1, yaml_config_file_2):
if config_type == "strict":
return providers.Configuration(strict=True)
elif config_type == "default":
return providers.Configuration(yaml_files=[yaml_config_file_1, yaml_config_file_2])
else:
raise ValueError("Undefined config type \"{0}\"".format(config_type))
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_files(config, yaml_config_file_1, yaml_config_file_2):
assert config.get_yaml_files() == [yaml_config_file_1, yaml_config_file_2]
def test_set_files(config):
config.set_yaml_files(["file1.yml", "file2.yml"])
assert config.get_yaml_files() == ["file1.yml", "file2.yml"]
def test_file_does_not_exist(config):
config.set_yaml_files(["./does_not_exist.yml"])
config.load()
assert config() == {}
@mark.parametrize("config_type", ["strict"])
def test_file_does_not_exist_strict_mode(config):
config.set_yaml_files(["./does_not_exist.yml"])
with raises(IOError):
config.load()
assert config() == {}
def test_required_file_does_not_exist(config):
config.set_yaml_files(["./does_not_exist.yml"])
with raises(IOError):
config.load(required=True)
@mark.parametrize("config_type", ["strict"])
def test_not_required_file_does_not_exist_strict_mode(config):
config.set_yaml_files(["./does_not_exist.yml"])
config.load(required=False)
assert config() == {}
def test_missing_envs_required(config, yaml_config_file_3):
with open(yaml_config_file_3, "w") as file:
file.write(
"section:\n"
" undefined: ${UNDEFINED}\n"
)
config.set_yaml_files([yaml_config_file_3])
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.load(envs_required=True)
@mark.parametrize("config_type", ["strict"])
def test_missing_envs_not_required_in_strict_mode(config, yaml_config_file_3):
with open(yaml_config_file_3, "w") as file:
file.write(
"section:\n"
" undefined: ${UNDEFINED}\n"
)
config.set_yaml_files([yaml_config_file_3])
config.load(envs_required=False)
assert config.section.undefined() is None