Repository: CS-SI/eodag
Pull request #288 (2021-06-01 17:28:05)

fix: conf updatable from non-queryable params to queryable
Fixes GH-278

`provider.search` configuration can now be dynamically updated from queryable parameters (`list`) to non-queryable (`str`) and vice-versa


Patch:
diff --git a/eodag/utils/__init__.py b/eodag/utils/__init__.py
index 08bb05cd..731a2eb9 100644
--- a/eodag/utils/__init__.py
+++ b/eodag/utils/__init__.py
@@ -286,15 +286,13 @@ def merge_mappings(mapping1, mapping2):
     """Merge two mappings with string keys, values from `mapping2` overriding values
     from `mapping1`.
 
-    Do its best to detect the key in `mapping1` to override. For example, let's say
-    we have::
+    Do its best to detect the key in `mapping1` to override. For example::
 
-        mapping2 = {"keya": "new"}
-        mapping1 = {"keyA": "obsolete"}
-
-    Then::
-
-        merge_mappings(mapping1, mapping2) ==> {"keyA": "new"}
+    >>> mapping2 = {"keya": "new"}
+    >>> mapping1 = {"keyA": "obsolete"}
+    >>> merge_mappings(mapping1, mapping2)
+    >>> mapping1
+    {'keyA': 'new'}
 
     If mapping2 has a key that cannot be detected in mapping1, this new key is added
     to mapping1 as is.
@@ -326,35 +324,48 @@ def merge_mappings(mapping1, mapping2):
             current_value = mapping1.get(m1_keys_lowercase.get(key, key), None)
             if current_value is not None:
                 current_value_type = type(current_value)
-                if isinstance(value, str):
-                    # Bool is a type with special meaning in Python, thus the special
-                    # case
-                    if current_value_type is bool:
-                        if value.capitalize() not in ("True", "False"):
-                            raise ValueError(
-                                "Only true or false strings (case insensitive) are "
-                                "allowed for booleans"
+                new_value_type = type(value)
+                try:
+                    # If current or new value is a list (search queryable parameter), simply replace current with new
+                    if (
+                        new_value_type == list
+                        and current_value_type != list
+                        or new_value_type != list
+                        and current_value_type == list
+                    ):
+                        mapping1[m1_keys_lowercase.get(key, key)] = value
+                    elif isinstance(value, str):
+                        # Bool is a type with special meaning in Python, thus the special
+                        # case
+                        if current_value_type is bool:
+                            if value.capitalize() not in ("True", "False"):
+                                raise ValueError(
+                                    "Only true or false strings (case insensitive) are "
+                                    "allowed for booleans"
+                                )
+                            # Get the real Python value of the boolean. e.g: value='tRuE'
+                            # => eval(value.capitalize())=True.
+                            # str.capitalize() transforms the first character of the string
+                            # to a capital letter
+                            mapping1[m1_keys_lowercase.get(key, key)] = eval(
+                                value.capitalize()
                             )
-                        # Get the real Python value of the boolean. e.g: value='tRuE'
-                        # => eval(value.capitalize())=True.
-                        # str.capitalize() transforms the first character of the string
-                        # to a capital letter
-                        mapping1[m1_keys_lowercase.get(key, key)] = eval(
-                            value.capitalize()
-                        )
+                        else:
+                            mapping1[
+                                m1_keys_lowercase.get(key, key)
+                            ] = current_value_type(value)
                     else:
                         mapping1[m1_keys_lowercase.get(key, key)] = current_value_type(
                             value
                         )
-                else:
-                    try:
-                        mapping1[m1_keys_lowercase.get(key, key)] = current_value_type(
-                            value
-                        )
-                    except TypeError:
-                        # Ignore any override value that does not have the same type
-                        # as the default value
-                        pass
+                except (TypeError, ValueError):
+                    # Ignore any override value that does not have the same type
+                    # as the default value
+                    logger.debug(
+                        f"Ignored '{key}' setting override from '{current_value}' to '{value}', "
+                        f"(could not cast {new_value_type} to {current_value_type})"
+                    )
+                    pass
             else:
                 mapping1[key] = value
 


Test patch:
diff --git a/tests/context.py b/tests/context.py
index 4dcf50b7..96aca67b 100644
--- a/tests/context.py
+++ b/tests/context.py
@@ -49,6 +49,7 @@ from eodag.utils import (
     get_geometry_from_various,
     get_timestamp,
     makedirs,
+    merge_mappings,
     path_to_uri,
     ProgressCallback,
     uri_to_path,
diff --git a/tests/units/test_utils.py b/tests/units/test_utils.py
index 6cc092c9..62a5b713 100644
--- a/tests/units/test_utils.py
+++ b/tests/units/test_utils.py
@@ -25,6 +25,7 @@ from io import StringIO
 from tests.context import (
     ProgressCallback,
     get_timestamp,
+    merge_mappings,
     path_to_uri,
     setup_logging,
     uri_to_path,
@@ -118,3 +119,36 @@ class TestUtils(unittest.TestCase):
             with ProgressCallback(total=2, file=tqdm_out, disable=True) as bar:
                 bar(1)
             self.assertEqual(tqdm_out.getvalue(), "")
+
+    def test_merge_mappings(self):
+        """Configuration mappings must be merged properly."""
+
+        # nested dict
+        mapping = {"foo": {"keyA": "obsolete"}}
+        merge_mappings(mapping, {"foo": {"keya": "new"}})
+        self.assertEqual(mapping, {"foo": {"keyA": "new"}})
+
+        # list replaced by string
+        mapping = {"keyA": ["obsolete1", "obsolete2"]}
+        merge_mappings(mapping, {"keya": "new"})
+        self.assertEqual(mapping, {"keyA": "new"})
+
+        # string replaced by list
+        mapping = {"keyA": "obsolete"}
+        merge_mappings(mapping, {"keya": ["new1", "new2"]})
+        self.assertEqual(mapping, {"keyA": ["new1", "new2"]})
+
+        # bool replaced by str
+        mapping = {"keyA": True}
+        merge_mappings(mapping, {"keya": "fAlSe"})
+        self.assertEqual(mapping, {"keyA": False})
+
+        # int replaced by str
+        mapping = {"keyA": 1}
+        merge_mappings(mapping, {"keya": "2"})
+        self.assertEqual(mapping, {"keyA": 2})
+
+        # override ignored if cast cannot be perfomed to origin type
+        mapping = {"keyA": True}
+        merge_mappings(mapping, {"keya": "bar"})
+        self.assertEqual(mapping, {"keyA": True})