Repository: janehmueller/python-json-config

Issue:
Revise ENV merging
Instead of merging the current config with the ENV variables by doing:

```python
builder = ConfigBuilder()
# you can also just pass a single prefix (builder.merge_with_env_variables("MYPROJECT")
builder.merge_with_env_variables(["MYPROJECT", "MYPYTHONPROJECTS"])
config = builder.parse_config({"server.host": "0.0.0.0"})
```

It should be possible to do it within the builder patter like this:

```python
builder = ConfigBuilder()
builder.parse_config({"server.host": "0.0.0.0"}).merge_with_env_variables("MYPROJECT").merge_with_env_variables("MYPYTHONPROJECTS")
```

Discussion:

Patch:
diff --git a/README.md b/README.md
index 22f8ea1..8fa7d77 100644
--- a/README.md
+++ b/README.md
@@ -131,3 +131,13 @@ assert config.server.host == "localhost"
 assert config.cache == "redis"
 assert config.user == "user"
 ```
+Alternatively you can also do the merging after creating the config object:
+```
+builder = ConfigBuilder()
+config = builder.parse_config({"server.host": "0.0.0.0"})
+config.merge_with_env_variables(["MYPROJECT", "MYPYTHONPROJECTS"])
+
+assert config.server.host == "localhost"
+assert config.cache == "redis"
+assert config.user == "user"
+```
diff --git a/python_json_config/config_builder.py b/python_json_config/config_builder.py
index 9a9d622..a3fadef 100644
--- a/python_json_config/config_builder.py
+++ b/python_json_config/config_builder.py
@@ -1,5 +1,4 @@
 import json
-import os
 from typing import Dict, Union, List
 
 import jsonschema
@@ -23,8 +22,8 @@ class ConfigBuilder(object):
         self.__strict_access: bool = None
         self.__field_access_settings: Dict[str, bool] = {}
 
-        # environment variables that will be merged into the config (pointing from path joined with dots to the value)
-        self.__environment_variables: Dict[str, str] = {}
+        # environment variable prefixes  that will be merged into the config
+        self.__environment_variable_prefixes: List[str] = []
 
     def validate_field_type(self, field_name: str, field_type: type):
         """
@@ -179,15 +178,7 @@ class ConfigBuilder(object):
         :return: The builder object for chaining of calls.
         """
         prefixes = [prefix] if isinstance(prefix, str) else prefix
-        for key in os.environ:
-            for prefix in prefixes:
-                if key.startswith(prefix):
-                    value = os.environ[key]
-                    cleaned_key = key[len(prefix):]
-                    if cleaned_key[0] == "_":
-                        cleaned_key = cleaned_key[1:]
-                    cleaned_key = cleaned_key.lower().replace("_", ".")
-                    self.__environment_variables[cleaned_key] = value
+        self.__environment_variable_prefixes += prefixes
         return self
 
     def parse_config(self, config: Union[str, dict]) -> Config:
@@ -217,8 +208,7 @@ class ConfigBuilder(object):
                                                 if not status])
 
         # Add/Overwrite values set via environment variables
-        for path, value in self.__environment_variables.items():
-            self.__config.add(path, value)
+        self.__config.merge_with_env_variables(self.__environment_variable_prefixes)
 
         # Apply the custom validation and transformation function
         self.__validate_types()
diff --git a/python_json_config/config_node.py b/python_json_config/config_node.py
index 4cf2627..f84e291 100644
--- a/python_json_config/config_node.py
+++ b/python_json_config/config_node.py
@@ -1,3 +1,4 @@
+import os
 import warnings
 from typing import List, Union, Tuple
 
@@ -47,7 +48,7 @@ class ConfigNode(object):
         self.__node_dict = node_dict
 
     """
-    CRUD methods to access and modify the config contents.
+    Methods to access and modify the config contents.
     """
     def get(self, path: Union[str, List[str]]):
         """
@@ -124,6 +125,25 @@ class ConfigNode(object):
         else:
             self.get(key).update(path=path[1:], value=value, upsert=upsert)
 
+    def merge_with_env_variables(self, prefix: Union[str, List[str]]):
+        """
+        Take all environment variables that start with the specified prefix or one of the specific prefixes and merge
+        them into the config. These values overwrite existing ones.
+        The environment variable names will be split on underscores (_) and changed to lowercase to determine the
+        different keys (e.g., "FOO_BAR_TEST_ME" will result in the keys ["bar", "test", "me"] (with the prefix "FOO").
+        :param prefix: Either a single or a list of prefixes of the environment variables (e.g., "FOO_").
+        """
+        prefixes = [prefix] if isinstance(prefix, str) else prefix
+        for key in os.environ:
+            for prefix in prefixes:
+                if key.startswith(prefix):
+                    value = os.environ[key]
+                    cleaned_key = key[len(prefix):]
+                    if cleaned_key[0] == "_":
+                        cleaned_key = cleaned_key[1:]
+                    cleaned_key = cleaned_key.lower().split("_")
+                    self.update(path=cleaned_key, value=value, upsert=True)
+
     """
     Iteration functions
     """


Test patch:
diff --git a/tests/config_builder_test.py b/tests/config_builder_test.py
index 0997bd8..56fac8a 100644
--- a/tests/config_builder_test.py
+++ b/tests/config_builder_test.py
@@ -151,16 +151,18 @@ class ConfigBuilderTest(TestCase):
         builder.parse_config(self.path)
 
     def test_merge_env_variable(self):
-        builder = ConfigBuilder()
         prefix = "PYTHONJSONCONFIG"
         variables = {f"{prefix}_TESTVALUE1": "bla", f"{prefix}_TESTVALUE2": "1"}
         for key, value in variables.items():
             os.environ[key] = value
-        builder.merge_with_env_variables(prefix)
-        for key, value in variables.items():
-            del os.environ[key]
 
+        builder = ConfigBuilder()
+        builder.merge_with_env_variables(prefix)
         config = builder.parse_config({"testvalue1": "blub", "testvalue3": 5})
+
         self.assertEqual(config.testvalue1, "bla")
         self.assertEqual(config.testvalue2, "1")
         self.assertEqual(config.testvalue3, 5)
+
+        for key, value in variables.items():
+            del os.environ[key]
diff --git a/tests/config_node/test_crud.py b/tests/config_node/test_crud.py
index 7dc8f02..cf61183 100644
--- a/tests/config_node/test_crud.py
+++ b/tests/config_node/test_crud.py
@@ -1,3 +1,4 @@
+import os
 import warnings
 from unittest import TestCase
 
@@ -117,3 +118,19 @@ class ConfigNodeCrudTest(TestCase):
             self.assertIsNone(config.key2.nokey)
         with self.assertRaises(AttributeError):
             self.assertIsNone(config.key2.nokey2)
+
+    def test_merge_env_variable(self):
+        prefix = "PYTHONJSONCONFIG"
+        variables = {f"{prefix}_TESTVALUE1": "bla", f"{prefix}_TESTVALUE2": "1"}
+        for key, value in variables.items():
+            os.environ[key] = value
+
+        config = ConfigNode({"testvalue1": "blub", "testvalue3": 5})
+        config.merge_with_env_variables(prefix)
+
+        self.assertEqual(config.testvalue1, "bla")
+        self.assertEqual(config.testvalue2, "1")
+        self.assertEqual(config.testvalue3, 5)
+
+        for key, value in variables.items():
+            del os.environ[key]