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 copy
import errno import errno
import functools import functools
import inspect
import importlib import importlib
import inspect
import json
import os import os
import re import re
import sys import sys
import types
import threading import threading
import types
import warnings import warnings
try: try:
@ -1741,6 +1742,44 @@ cdef class ConfigurationOption(Provider):
current_config = {} current_config = {}
self.override(merge_dicts(current_config, 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): def from_pydantic(self, settings, required=UNDEFINED, **kwargs):
"""Load configuration from pydantic settings. """Load configuration from pydantic settings.
@ -2254,6 +2293,44 @@ cdef class Configuration(Object):
current_config = {} current_config = {}
self.override(merge_dicts(current_config, 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): def from_pydantic(self, settings, required=UNDEFINED, **kwargs):
"""Load configuration from pydantic settings. """Load configuration from pydantic settings.

View File

@ -1,5 +1,6 @@
"""Fixtures module.""" """Fixtures module."""
import json
import os import os
from dependency_injector import providers from dependency_injector import providers
@ -101,6 +102,62 @@ def yaml_config_file_3(tmp_path):
return yaml_config_file_3 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) @fixture(autouse=True)
def environment_variables(): def environment_variables():
os.environ["CONFIG_TEST_ENV"] = "test-value" 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"