Add `Configuration.from_json()` method (#602)

* Add implementation and tests

* Refactor naming in configuration fixtures

* Add typing for .from_json()

* Move get/set_ini_files() methods upper

* Add init implementation and tests

* Refactor typing tests

* Add examples

* Add docs

* Update docs index and readme

* Update changelog
This commit is contained in:
Roman Mogylatov 2022-07-10 21:08:45 -04:00 committed by GitHub
parent 14b5ddae4f
commit 753e863d02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 13271 additions and 10466 deletions

View File

@ -59,7 +59,7 @@ Key features of the ``Dependency Injector``:
- **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
`Provider overriding <https://python-dependency-injector.ets-labs.org/providers/overriding.html>`_.
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, ``pydantic`` settings,
- **Configuration**. Reads configuration from ``yaml``, ``ini``, and ``json`` files, ``pydantic`` settings,
environment variables, and dictionaries.
See `Configuration provider <https://python-dependency-injector.ets-labs.org/providers/configuration.html>`_.
- **Resources**. Helps with initialization and configuring of logging, event loop, thread

View File

@ -70,7 +70,7 @@ Key features of the ``Dependency Injector``:
- **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
:ref:`provider-overriding`.
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, ``pydantic`` settings,
- **Configuration**. Reads configuration from ``yaml``, ``ini``, and ``json`` files, ``pydantic`` settings,
environment variables, and dictionaries. See :ref:`configuration-provider`.
- **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.

View File

@ -16,7 +16,7 @@ Key features of the ``Dependency Injector``:
- **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
:ref:`provider-overriding`.
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, ``pydantic`` settings,
- **Configuration**. Reads configuration from ``yaml``, ``ini``, and ``json`` files, ``pydantic`` settings,
environment variables, and dictionaries. See :ref:`configuration-provider`.
- **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.

View File

@ -10,6 +10,7 @@ follows `Semantic versioning`_
Development
-----------
- Add ``Configuration.from_json()`` method to load configuration from a json file.
- Improve wording on the "Dependency injection and inversion of control in Python" docs page.
- Update typing in the main example and cohesion/coupling correlation definition in
"Dependency injection and inversion of control in Python".

View File

@ -136,6 +136,50 @@ To use another loader use ``loader`` argument:
*Don't forget to mirror the changes in the requirements file.*
Loading from a JSON file
------------------------
``Configuration`` provider can load configuration from a ``json`` file using the
:py:meth:`Configuration.from_json` method:
.. literalinclude:: ../../examples/providers/configuration/configuration_json.py
:language: python
:lines: 3-
:emphasize-lines: 12
where ``examples/providers/configuration/config.json`` is:
.. literalinclude:: ../../examples/providers/configuration/config.json
:language: json
Alternatively, you can provide a path to a json file over the configuration provider argument. In that case,
the container will call ``config.from_json()`` automatically:
.. code-block:: python
:emphasize-lines: 3
class Container(containers.DeclarativeContainer):
config = providers.Configuration(json_files=["./config.json"])
if __name__ == "__main__":
container = Container() # Config is loaded from ./config.json
:py:meth:`Configuration.from_json` method supports environment variables interpolation.
.. code-block:: json
{
"section": {
"option1": "${ENV_VAR}",
"option2": "${ENV_VAR}/path",
"option3": "${ENV_VAR:default}"
}
}
See also: :ref:`configuration-envs-interpolation`.
Loading from a Pydantic settings
--------------------------------

View File

@ -0,0 +1,6 @@
{
"aws": {
"access_key_id": "KEY",
"secret_access_key": "SECRET"
}
}

View File

@ -0,0 +1,27 @@
"""`Configuration` provider values loading example."""
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
if __name__ == "__main__":
container = Container()
container.config.from_json("./config.json")
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"

View File

@ -0,0 +1,25 @@
"""`Configuration` provider values loading example."""
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration(json_files=["./config.json"])
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

File diff suppressed because it is too large Load Diff

View File

@ -132,8 +132,9 @@ cdef class Configuration(Object):
cdef str __name
cdef bint __strict
cdef dict __children
cdef list __yaml_files
cdef list __ini_files
cdef list __yaml_files
cdef list __json_files
cdef list __pydantic_settings
cdef object __weakref__

View File

@ -218,6 +218,7 @@ class ConfigurationOption(Provider[Any]):
def update(self, value: Any) -> None: ...
def from_ini(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ...
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ...
def from_json(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> 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_env(self, name: str, default: Optional[Any] = None, required: bool = False, as_: Optional[_Callable[..., Any]] = None) -> None: ...
@ -237,8 +238,9 @@ class Configuration(Object[Any]):
default: Optional[Any] = None,
*,
strict: bool = False,
yaml_files: Optional[_Iterable[Union[Path, str]]] = None,
ini_files: Optional[_Iterable[Union[Path, str]]] = None,
yaml_files: Optional[_Iterable[Union[Path, str]]] = None,
json_files: Optional[_Iterable[Union[Path, str]]] = None,
pydantic_settings: Optional[_Iterable[PydanticSettings]] = None,
) -> None: ...
def __enter__(self) -> Configuration : ...
@ -258,11 +260,14 @@ class Configuration(Object[Any]):
def get_children(self) -> _Dict[str, ConfigurationOption]: ...
def set_children(self, children: _Dict[str, ConfigurationOption]) -> Configuration: ...
def get_ini_files(self) -> _List[Union[Path, str]]: ...
def set_ini_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def get_yaml_files(self) -> _List[Union[Path, str]]: ...
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 get_json_files(self) -> _List[Union[Path, str]]: ...
def set_json_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def get_pydantic_settings(self) -> _List[PydanticSettings]: ...
def set_pydantic_settings(self, settings: _Iterable[PydanticSettings]) -> Configuration: ...
@ -275,6 +280,7 @@ class Configuration(Object[Any]):
def update(self, value: Any) -> None: ...
def from_ini(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ...
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ...
def from_json(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> 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_env(self, name: str, default: Optional[Any] = None, required: bool = False, as_: Optional[_Callable[..., Any]] = None) -> None: ...

View File

@ -5,13 +5,14 @@ from __future__ import absolute_import
import copy
import errno
import functools
import inspect
import importlib
import inspect
import json
import os
import re
import sys
import types
import threading
import types
import warnings
try:
@ -1741,6 +1742,44 @@ cdef class ConfigurationOption(Provider):
current_config = {}
self.override(merge_dicts(current_config, config))
def from_json(self, filepath, required=UNDEFINED, envs_required=UNDEFINED):
"""Load configuration from a json file.
Loaded configuration is merged recursively over the existing configuration.
:param filepath: Path to a configuration file.
:type filepath: str
:param required: When required is True, raise an exception if file does not exist.
:type required: bool
:param envs_required: When True, raises an exception on undefined environment variable.
:type envs_required: bool
:rtype: None
"""
try:
with open(filepath) as opened_file:
config_content = opened_file.read()
except IOError as exception:
if required is not False \
and (self._is_strict_mode_enabled() or required is True) \
and exception.errno in (errno.ENOENT, errno.EISDIR):
exception.strerror = "Unable to load configuration file {0}".format(exception.strerror)
raise
return
config_content = _resolve_config_env_markers(
config_content,
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = json.loads(config_content)
current_config = self.__call__()
if not current_config:
current_config = {}
self.override(merge_dicts(current_config, config))
def from_pydantic(self, settings, required=UNDEFINED, **kwargs):
"""Load configuration from pydantic settings.
@ -1886,24 +1925,29 @@ cdef class Configuration(Object):
DEFAULT_NAME = "config"
def __init__(self, name=DEFAULT_NAME, default=None, strict=False, yaml_files=None, ini_files=None, pydantic_settings=None):
def __init__(self, name=DEFAULT_NAME, default=None, strict=False, ini_files=None, yaml_files=None, json_files=None, pydantic_settings=None):
self.__name = name
self.__strict = strict
self.__children = {}
self.__yaml_files = []
self.__ini_files = []
self.__yaml_files = []
self.__json_files = []
self.__pydantic_settings = []
super().__init__(provides={})
self.set_default(default)
if ini_files is None:
ini_files = []
self.set_ini_files(ini_files)
if yaml_files is None:
yaml_files = []
self.set_yaml_files(yaml_files)
if ini_files is None:
ini_files = []
self.set_ini_files(ini_files)
if json_files is None:
json_files = []
self.set_json_files(json_files)
if pydantic_settings is None:
pydantic_settings = []
@ -1919,8 +1963,9 @@ cdef class Configuration(Object):
copied.set_default(self.get_default())
copied.set_strict(self.get_strict())
copied.set_children(deepcopy(self.get_children(), memo))
copied.set_yaml_files(self.get_yaml_files())
copied.set_ini_files(self.get_ini_files())
copied.set_yaml_files(self.get_yaml_files())
copied.set_json_files(self.get_json_files())
copied.set_pydantic_settings(self.get_pydantic_settings())
self._copy_overridings(copied, memo)
@ -1995,6 +2040,15 @@ cdef class Configuration(Object):
self.__children = children
return self
def get_ini_files(self):
"""Return list of INI files."""
return list(self.__ini_files)
def set_ini_files(self, files):
"""Set list of INI files."""
self.__ini_files = list(files)
return self
def get_yaml_files(self):
"""Return list of YAML files."""
return list(self.__yaml_files)
@ -2004,13 +2058,13 @@ cdef class Configuration(Object):
self.__yaml_files = list(files)
return self
def get_ini_files(self):
"""Return list of INI files."""
return list(self.__ini_files)
def get_json_files(self):
"""Return list of JSON files."""
return list(self.__json_files)
def set_ini_files(self, files):
"""Set list of INI files."""
self.__ini_files = list(files)
def set_json_files(self, files):
"""Set list of JSON files."""
self.__json_files = list(files)
return self
def get_pydantic_settings(self):
@ -2039,11 +2093,14 @@ cdef class Configuration(Object):
:param envs_required: When True, raises an error on undefined environment variable.
:type envs_required: bool
"""
for file in self.get_ini_files():
self.from_ini(file, required=required, envs_required=envs_required)
for file in self.get_yaml_files():
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)
for file in self.get_json_files():
self.from_json(file, required=required, envs_required=envs_required)
for settings in self.get_pydantic_settings():
self.from_pydantic(settings, required=required)
@ -2254,6 +2311,44 @@ cdef class Configuration(Object):
current_config = {}
self.override(merge_dicts(current_config, config))
def from_json(self, filepath, required=UNDEFINED, envs_required=UNDEFINED):
"""Load configuration from a json file.
Loaded configuration is merged recursively over the existing configuration.
:param filepath: Path to a configuration file.
:type filepath: str
:param required: When required is True, raise an exception if file does not exist.
:type required: bool
:param envs_required: When True, raises an exception on undefined environment variable.
:type envs_required: bool
:rtype: None
"""
try:
with open(filepath) as opened_file:
config_content = opened_file.read()
except IOError as exception:
if required is not False \
and (self._is_strict_mode_enabled() or required is True) \
and exception.errno in (errno.ENOENT, errno.EISDIR):
exception.strerror = "Unable to load configuration file {0}".format(exception.strerror)
raise
return
config_content = _resolve_config_env_markers(
config_content,
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = json.loads(config_content)
current_config = self.__call__()
if not current_config:
current_config = {}
self.override(merge_dicts(current_config, config))
def from_pydantic(self, settings, required=UNDEFINED, **kwargs):
"""Load configuration from pydantic settings.

View File

@ -1,5 +1,7 @@
from pathlib import Path
from dependency_injector import providers
from pydantic import BaseSettings as PydanticSettings
# Test 1: to check the getattr
@ -8,18 +10,26 @@ provider1 = providers.Factory(dict, a=config1.a)
# Test 2: to check the from_*() method
config2 = providers.Configuration()
config2.from_dict({})
config2.from_value({})
config2.from_ini("config.ini")
config2.from_ini(Path("config.ini"))
config2.from_yaml("config.yml")
config2.from_yaml(Path("config.yml"))
config2.from_json("config.json")
config2.from_json(Path("config.json"))
config2.from_env("ENV", "default")
config2.from_env("ENV", as_=int, default=123)
config2.from_env("ENV", as_=float, required=True)
config2.from_env("ENV", as_=lambda env: str(env))
config2.from_pydantic(PydanticSettings())
# Test 3: to check as_*() methods
config3 = providers.Configuration()
int3: providers.Callable[int] = config3.option.as_int()
@ -29,3 +39,39 @@ int3_custom: providers.Callable[int] = config3.option.as_(int)
# Test 4: to check required() method
config4 = providers.Configuration()
option4: providers.ConfigurationOption = config4.option.required()
# Test 5: to check get/set config files' methods and init arguments
# Test 5: ini
config5_ini = providers.Configuration(
ini_files=["config.ini", Path("config.ini")],
)
config5_ini.set_ini_files(["config.ini", Path("config.ini")])
config5_ini_files: list[str | Path] = config5_ini.get_ini_files()
# Test 5: yaml
config5_yaml = providers.Configuration(
yaml_files=["config.yml", Path("config.yml")],
)
config5_yaml.set_yaml_files(["config.yml", Path("config.yml")])
config5_yaml_files: list[str | Path] = config5_yaml.get_yaml_files()
# Test 5: json
config5_json = providers.Configuration(
json_files=["config.json", Path("config.json")],
)
config5_json.set_json_files(["config.json", Path("config.json")])
config5_json_files: list[str | Path] = config5_json.get_json_files()
# Test 5: pydantic
config5_pydantic = providers.Configuration(
pydantic_settings=[PydanticSettings()],
)
config5_pydantic.set_pydantic_settings([PydanticSettings()])
config5_pydantic_settings: list[PydanticSettings] = config5_pydantic.get_pydantic_settings()
# Test 6: to check init arguments
config6 = providers.Configuration(
name="config",
strict=True,
default={},
)

View File

@ -1,5 +1,6 @@
"""Fixtures module."""
import json
import os
from dependency_injector import providers
@ -51,14 +52,14 @@ def ini_config_file_2(tmp_path):
@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:
config_file = str(tmp_path / "config_3.ini")
with open(config_file, "w") as file:
file.write(
"[section1]\n"
"value1=${CONFIG_TEST_ENV}\n"
"value2=${CONFIG_TEST_PATH}/path\n"
)
return ini_config_file_3
return config_file
@fixture
@ -91,14 +92,70 @@ def yaml_config_file_2(tmp_path):
@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:
config_file = str(tmp_path / "config_3.yml")
with open(config_file, "w") as file:
file.write(
"section1:\n"
" value1: ${CONFIG_TEST_ENV}\n"
" value2: ${CONFIG_TEST_PATH}/path\n"
)
return yaml_config_file_3
return config_file
@fixture
def json_config_file_1(tmp_path):
config_file = str(tmp_path / "config_1.json")
with open(config_file, "w") as file:
file.write(
json.dumps(
{
"section1": {
"value1": 1,
},
"section2": {
"value2": 2,
},
},
),
)
return config_file
@fixture
def json_config_file_2(tmp_path):
config_file = str(tmp_path / "config_2.json")
with open(config_file, "w") as file:
file.write(
json.dumps(
{
"section1": {
"value1": 11,
"value11": 11,
},
"section3": {
"value3": 3,
},
},
),
)
return config_file
@fixture
def json_config_file_3(tmp_path):
config_file = str(tmp_path / "config_3.json")
with open(config_file, "w") as file:
file.write(
json.dumps(
{
"section1": {
"value1": "${CONFIG_TEST_ENV}",
"value2": "${CONFIG_TEST_PATH}/path",
},
},
),
)
return config_file
@fixture(autouse=True)

View File

@ -0,0 +1,84 @@
"""Configuration.from_json() tests."""
from dependency_injector import errors
from pytest import mark, raises
def test(config, json_config_file_1):
config.from_json(json_config_file_1)
assert config() == {"section1": {"value1": 1}, "section2": {"value2": 2}}
assert config.section1() == {"value1": 1}
assert config.section1.value1() == 1
assert config.section2() == {"value2": 2}
assert config.section2.value2() == 2
def test_merge(config, json_config_file_1, json_config_file_2):
config.from_json(json_config_file_1)
config.from_json(json_config_file_2)
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_file_does_not_exist(config):
config.from_json("./does_not_exist.json")
assert config() == {}
@mark.parametrize("config_type", ["strict"])
def test_file_does_not_exist_strict_mode(config):
with raises(IOError):
config.from_json("./does_not_exist.json")
def test_option_file_does_not_exist(config):
config.option.from_json("./does_not_exist.json")
assert config.option() is None
@mark.parametrize("config_type", ["strict"])
def test_option_file_does_not_exist_strict_mode(config):
with raises(IOError):
config.option.from_json("./does_not_exist.json")
def test_required_file_does_not_exist(config):
with raises(IOError):
config.from_json("./does_not_exist.json", required=True)
def test_required_option_file_does_not_exist(config):
with raises(IOError):
config.option.from_json("./does_not_exist.json", required=True)
@mark.parametrize("config_type", ["strict"])
def test_not_required_file_does_not_exist_strict_mode(config):
config.from_json("./does_not_exist.json", required=False)
assert config() == {}
@mark.parametrize("config_type", ["strict"])
def test_not_required_option_file_does_not_exist_strict_mode(config):
config.option.from_json("./does_not_exist.json", required=False)
with raises(errors.Error):
config.option()

View File

@ -0,0 +1,198 @@
"""Configuration.from_json() with environment variables interpolation tests."""
import json
import os
from pytest import mark, raises
def test_env_variable_interpolation(config, json_config_file_3):
config.from_json(json_config_file_3)
assert config() == {
"section1": {
"value1": "test-value",
"value2": "test-path/path",
},
}
assert config.section1() == {
"value1": "test-value",
"value2": "test-path/path",
}
assert config.section1.value1() == "test-value"
assert config.section1.value2() == "test-path/path"
def test_missing_envs_not_required(config, json_config_file_3):
del os.environ["CONFIG_TEST_ENV"]
del os.environ["CONFIG_TEST_PATH"]
config.from_json(json_config_file_3)
assert config() == {
"section1": {
"value1": "",
"value2": "/path",
},
}
assert config.section1() == {
"value1": "",
"value2": "/path",
}
assert config.section1.value1() == ""
assert config.section1.value2() == "/path"
def test_missing_envs_required(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.from_json(json_config_file_3, envs_required=True)
@mark.parametrize("config_type", ["strict"])
def test_missing_envs_strict_mode(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.from_json(json_config_file_3)
@mark.parametrize("config_type", ["strict"])
def test_missing_envs_not_required_in_strict_mode(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
config.from_json(json_config_file_3, envs_required=False)
assert config.section.undefined() == ""
def test_option_missing_envs_not_required(config, json_config_file_3):
del os.environ["CONFIG_TEST_ENV"]
del os.environ["CONFIG_TEST_PATH"]
config.option.from_json(json_config_file_3)
assert config.option() == {
"section1": {
"value1": "",
"value2": "/path",
},
}
assert config.option.section1() == {
"value1": "",
"value2": "/path",
}
assert config.option.section1.value1() == ""
assert config.option.section1.value2() == "/path"
def test_option_missing_envs_required(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.option.from_json(json_config_file_3, envs_required=True)
@mark.parametrize("config_type", ["strict"])
def test_option_missing_envs_not_required_in_strict_mode(config, json_config_file_3):
config.override({"option": {}})
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
config.option.from_json(json_config_file_3, envs_required=False)
assert config.option.section.undefined() == ""
@mark.parametrize("config_type", ["strict"])
def test_option_missing_envs_strict_mode(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.option.from_json(json_config_file_3)
def test_default_values(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"defined_with_default": "${DEFINED:default}",
"undefined_with_default": "${UNDEFINED:default}",
"complex": "${DEFINED}/path/${DEFINED:default}/${UNDEFINED}/${UNDEFINED:default}",
},
},
),
)
config.from_json(json_config_file_3)
assert config.section() == {
"defined_with_default": "defined",
"undefined_with_default": "default",
"complex": "defined/path/defined//default",
}
def test_option_env_variable_interpolation(config, json_config_file_3):
config.option.from_json(json_config_file_3)
assert config.option() == {
"section1": {
"value1": "test-value",
"value2": "test-path/path",
},
}
assert config.option.section1() == {
"value1": "test-value",
"value2": "test-path/path",
}
assert config.option.section1.value1() == "test-value"
assert config.option.section1.value2() == "test-path/path"

View File

@ -47,6 +47,11 @@ def test_set_files(config):
assert config.get_ini_files() == ["file1.ini", "file2.ini"]
def test_copy(config, ini_config_file_1, ini_config_file_2):
config_copy = providers.deepcopy(config)
assert config_copy.get_ini_files() == [ini_config_file_1, ini_config_file_2]
def test_file_does_not_exist(config):
config.set_ini_files(["./does_not_exist.ini"])
config.load()

View File

@ -0,0 +1,114 @@
"""Configuration(json_files=[...]) tests."""
import json
from dependency_injector import providers
from pytest import fixture, mark, raises
@fixture
def config(config_type, json_config_file_1, json_config_file_2):
if config_type == "strict":
return providers.Configuration(strict=True)
elif config_type == "default":
return providers.Configuration(json_files=[json_config_file_1, json_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, json_config_file_1, json_config_file_2):
assert config.get_json_files() == [json_config_file_1, json_config_file_2]
def test_set_files(config):
config.set_json_files(["file1.json", "file2.json"])
assert config.get_json_files() == ["file1.json", "file2.json"]
def test_copy(config, json_config_file_1, json_config_file_2):
config_copy = providers.deepcopy(config)
assert config_copy.get_json_files() == [json_config_file_1, json_config_file_2]
def test_file_does_not_exist(config):
config.set_json_files(["./does_not_exist.json"])
config.load()
assert config() == {}
@mark.parametrize("config_type", ["strict"])
def test_file_does_not_exist_strict_mode(config):
config.set_json_files(["./does_not_exist.json"])
with raises(IOError):
config.load()
assert config() == {}
def test_required_file_does_not_exist(config):
config.set_json_files(["./does_not_exist.json"])
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_json_files(["./does_not_exist.json"])
config.load(required=False)
assert config() == {}
def test_missing_envs_required(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
config.set_json_files([json_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, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
config.set_json_files([json_config_file_3])
config.load(envs_required=False)
assert config.section.undefined() == ""

View File

@ -80,6 +80,11 @@ def test_get_pydantic_settings(config, pydantic_settings_1, pydantic_settings_2)
assert config.get_pydantic_settings() == [pydantic_settings_1, pydantic_settings_2]
def test_copy(config, pydantic_settings_1, pydantic_settings_2):
config_copy = providers.deepcopy(config)
assert config_copy.get_pydantic_settings() == [pydantic_settings_1, pydantic_settings_2]
def test_set_pydantic_settings(config):
class Settings3(pydantic.BaseSettings):
...

View File

@ -47,6 +47,11 @@ def test_set_files(config):
assert config.get_yaml_files() == ["file1.yml", "file2.yml"]
def test_copy(config, yaml_config_file_1, yaml_config_file_2):
config_copy = providers.deepcopy(config)
assert config_copy.get_yaml_files() == [yaml_config_file_1, yaml_config_file_2]
def test_file_does_not_exist(config):
config.set_yaml_files(["./does_not_exist.yml"])
config.load()