Configuration(ini_files=[...]) (#524)

* Update changelog

* Add implementation

* Add tests

* Add more tests and example

* Update changelog

* Update documentation
This commit is contained in:
Roman Mogylatov 2021-10-26 20:27:11 -04:00 committed by GitHub
parent b16b190ff7
commit 34902db86e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 6812 additions and 6215 deletions

View File

@ -15,11 +15,12 @@ Develop
- 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. - Add ``Configuration(yaml_files=[...])`` argument.
- Fix ``envs_required=False`` behavior in ``Configuration.from_*()`` methods - Add ``Configuration(ini_files=[...])`` argument.
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.
- Fix ``envs_required=False`` behavior in ``Configuration.from_*()`` methods
to give a priority to the explicitly provided value.
- Update documentation and fix typos. - Update documentation and fix typos.
- Migrate tests to ``pytest``. - Migrate tests to ``pytest``.

View File

@ -45,6 +45,21 @@ where ``examples/providers/configuration/config.ini`` is:
.. literalinclude:: ../../examples/providers/configuration/config.ini .. literalinclude:: ../../examples/providers/configuration/config.ini
:language: ini :language: ini
Alternatively, you can provide a path to the INI file over the configuration provider argument. In that case,
the container will call ``config.from_ini()`` automatically:
.. code-block:: python
:emphasize-lines: 3
class Container(containers.DeclarativeContainer):
config = providers.Configuration(ini_files=["./config.ini"])
if __name__ == "__main__":
container = Container() # Config is loaded from ./config.ini
:py:meth:`Configuration.from_ini` method supports environment variables interpolation. :py:meth:`Configuration.from_ini` 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(ini_files=["./config.ini"])
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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -216,7 +216,15 @@ 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, yaml_files: Optional[_Iterable[Union[Path, str]]] = None) -> None: ... def __init__(
self,
name: str = DEFAULT_NAME,
default: Optional[Any] = None,
*,
strict: bool = False,
yaml_files: Optional[_Iterable[Union[Path, str]]] = None,
ini_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: ...
@ -237,6 +245,9 @@ class Configuration(Object[Any]):
def get_yaml_files(self) -> _List[Union[Path, str]]: ... def get_yaml_files(self) -> _List[Union[Path, str]]: ...
def set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ... def set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def get_ini_files(self) -> _List[Union[Path, str]]: ...
def set_ini_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def load(self, required: bool = False, envs_required: bool = False) -> None: ... def load(self, required: bool = False, envs_required: bool = False) -> None: ...
def get(self, selector: str) -> Any: ... def get(self, selector: str) -> Any: ...

View File

@ -1755,11 +1755,12 @@ cdef class Configuration(Object):
DEFAULT_NAME = 'config' DEFAULT_NAME = 'config'
def __init__(self, name=DEFAULT_NAME, default=None, strict=False, yaml_files=None): def __init__(self, name=DEFAULT_NAME, default=None, strict=False, yaml_files=None, ini_files=None):
self.__name = name self.__name = name
self.__strict = strict self.__strict = strict
self.__children = {} self.__children = {}
self.__yaml_files = [] self.__yaml_files = []
self.__ini_files = []
super().__init__(provides={}) super().__init__(provides={})
self.set_default(default) self.set_default(default)
@ -1768,6 +1769,10 @@ cdef class Configuration(Object):
yaml_files = [] yaml_files = []
self.set_yaml_files(yaml_files) self.set_yaml_files(yaml_files)
if ini_files is None:
ini_files = []
self.set_ini_files(ini_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:
@ -1779,6 +1784,7 @@ cdef class Configuration(Object):
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()) copied.set_yaml_files(self.get_yaml_files())
copied.set_ini_files(self.get_ini_files())
self._copy_overridings(copied, memo) self._copy_overridings(copied, memo)
return copied return copied
@ -1861,6 +1867,15 @@ cdef class Configuration(Object):
self.__yaml_files = list(files) self.__yaml_files = list(files)
return self return self
def get_ini_files(self):
"""Return list of INI files."""
return self.__ini_files
def set_ini_files(self, files):
"""Set list of INI files."""
self.__ini_files = list(files)
return self
def load(self, required=UNDEFINED, envs_required=UNDEFINED): def load(self, required=UNDEFINED, envs_required=UNDEFINED):
"""Load configuration. """Load configuration.
@ -1881,6 +1896,9 @@ cdef class Configuration(Object):
for file in self.get_yaml_files(): for file in self.get_yaml_files():
self.from_yaml(file, required=required, envs_required=envs_required) self.from_yaml(file, required=required, envs_required=envs_required)
for file in self.get_ini_files():
self.from_ini(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.

View File

@ -6,21 +6,37 @@ from pytest import fixture
@fixture @fixture
def yaml_config_file(tmp_path): def yaml_config_file(tmp_path):
yaml_config_file = str(tmp_path / "config.yml") config_file = str(tmp_path / "config.yml")
with open(yaml_config_file, "w") as file: with open(config_file, "w") as file:
file.write( file.write(
"section1:\n" "section1:\n"
" value1: yaml-loaded\n" " value1: yaml-loaded\n"
) )
return yaml_config_file return config_file
def test_auto_load(yaml_config_file): @fixture
def ini_config_file(tmp_path):
config_file = str(tmp_path / "config.ini")
with open(config_file, "w") as file:
file.write(
"[section2]:\n"
"value2 = ini-loaded\n"
)
return config_file
def test_auto_load(yaml_config_file, ini_config_file):
class ContainerWithConfig(containers.DeclarativeContainer): class ContainerWithConfig(containers.DeclarativeContainer):
config = providers.Configuration(yaml_files=[yaml_config_file]) config = providers.Configuration(
yaml_files=[yaml_config_file],
ini_files=[ini_config_file],
)
container = ContainerWithConfig() container = ContainerWithConfig()
assert container.config.section1.value1() == "yaml-loaded" assert container.config.section1.value1() == "yaml-loaded"
assert container.config.section2.value2() == "ini-loaded"
def test_auto_load_and_overriding(yaml_config_file): def test_auto_load_and_overriding(yaml_config_file):

View File

@ -0,0 +1,97 @@
"""Configuration(ini_files=[...]) tests."""
from dependency_injector import providers
from pytest import fixture, mark, raises
@fixture
def config(config_type, ini_config_file_1, ini_config_file_2):
if config_type == "strict":
return providers.Configuration(strict=True)
elif config_type == "default":
return providers.Configuration(ini_files=[ini_config_file_1, ini_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, ini_config_file_1, ini_config_file_2):
assert config.get_ini_files() == [ini_config_file_1, ini_config_file_2]
def test_set_files(config):
config.set_ini_files(["file1.ini", "file2.ini"])
assert config.get_ini_files() == ["file1.ini", "file2.ini"]
def test_file_does_not_exist(config):
config.set_ini_files(["./does_not_exist.ini"])
config.load()
assert config() == {}
@mark.parametrize("config_type", ["strict"])
def test_file_does_not_exist_strict_mode(config):
config.set_ini_files(["./does_not_exist.ini"])
with raises(IOError):
config.load()
assert config() == {}
def test_required_file_does_not_exist(config):
config.set_ini_files(["./does_not_exist.ini"])
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_ini_files(["./does_not_exist.ini"])
config.load(required=False)
assert config() == {}
def test_missing_envs_required(config, ini_config_file_3):
with open(ini_config_file_3, "w") as file:
file.write(
"[section]\n"
"undefined=${UNDEFINED}\n"
)
config.set_ini_files([ini_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, ini_config_file_3):
with open(ini_config_file_3, "w") as file:
file.write(
"[section]\n"
"undefined=${UNDEFINED}\n"
)
config.set_ini_files([ini_config_file_3])
config.load(envs_required=False)
assert config.section.undefined() == ""