Add implementation and tests

This commit is contained in:
Roman Mogylatov 2022-07-06 23:18:20 -04:00
parent 14b5ddae4f
commit df8f1f10a9
5 changed files with 10132 additions and 8054 deletions

File diff suppressed because it is too large Load Diff

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.
@ -2254,6 +2293,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,6 @@
"""Fixtures module."""
import json
import os
from dependency_injector import providers
@ -101,6 +102,62 @@ def yaml_config_file_3(tmp_path):
return yaml_config_file_3
@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):
yaml_config_file_3 = str(tmp_path / "config_3.json")
with open(yaml_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section1": {
"value1": "${CONFIG_TEST_ENV}",
"value2": "${CONFIG_TEST_PATH}/path",
},
},
),
)
return yaml_config_file_3
@fixture(autouse=True)
def environment_variables():
os.environ["CONFIG_TEST_ENV"] = "test-value"

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"