Repository: intake/intake
Pull request #603 (2021-07-12 17:48:30)

Allow mlist parameters type
@dr-leo , I think this does what you want, by introducing a "mlist" type for user parameters. However, you may well still want to subclass, to enable custom validation or formatting of the output. 


Patch:
diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst
index 52b9184..cb73968 100644
--- a/docs/source/glossary.rst
+++ b/docs/source/glossary.rst
@@ -116,7 +116,7 @@ Glossary
         specific circumstances.
 
     TTL
-        Time-to-live, how long before the given entity is considered to have expired. Usually in seconds.
+        Time-to-live, how long before the give entity is considered to have expired. Usually in seconds.
 
     User Parameter
         A data source definition can contain a "parameters" section, which can act as explicit decision indicators
diff --git a/intake/catalog/local.py b/intake/catalog/local.py
index af6cc78..d983565 100644
--- a/intake/catalog/local.py
+++ b/intake/catalog/local.py
@@ -74,7 +74,7 @@ class UserParameter(DictSerialiseMixin):
         if self.max:
             self.max = coerce(self.type, self.max)
 
-        if self.allowed:
+        if self.allowed and type != "mlist":
             self.allowed = [coerce(self.type, item)
                             for item in self.allowed]
 
@@ -111,6 +111,12 @@ class UserParameter(DictSerialiseMixin):
         if self.type is not None:
             value = coerce(self.type, value)
 
+        if self.type == "mlist":
+            for v in value:
+                if v not in self.allowed:
+                    raise ValueError("Item %s not in allowed list", v)
+            return value
+
         if self.min is not None and value < self.min:
             raise ValueError('%s=%s is less than %s' % (self.name, value,
                                                         self.min))
diff --git a/intake/catalog/utils.py b/intake/catalog/utils.py
index e859f66..9187b8f 100644
--- a/intake/catalog/utils.py
+++ b/intake/catalog/utils.py
@@ -278,11 +278,15 @@ def coerce_datetime(v=None):
 COERCION_RULES = {
     'bool': bool,
     'datetime': coerce_datetime,
+    'dict': dict,
     'float': float,
+    'tuple': tuple,
+    'mlist': list,
     'int': int,
     'list': list,
     'str': str,
-    'unicode': str
+    'unicode': str,
+    'other': lambda x: x
 }
 
 
@@ -299,6 +303,12 @@ def coerce(dtype, value):
         return value
     if type(value).__name__ == dtype:
         return value
+    if dtype == "mlist":
+        if isinstance(value, (tuple, set, dict)):
+            return list(value)
+        if not isinstance(value, list):
+            raise TypeError("Will not coerce value %s to list", value)
+        return value
     op = COERCION_RULES[dtype]
     return op() if value is None else op(value)
 


Test patch:
diff --git a/intake/catalog/tests/catalog.yml b/intake/catalog/tests/catalog.yml
index 9fa69ac..1ff99a0 100644
--- a/intake/catalog/tests/catalog.yml
+++ b/intake/catalog/tests/catalog.yml
@@ -5,4 +5,29 @@ sources:
   use_example1:
     description: example1 source plugin
     driver: example1
-    args: {}
\ No newline at end of file
+    args: {}
+  allowed_list_absolute:
+    description: pick one list out of a few
+    driver: example1
+    args:
+      arg1: "{{up}}"
+    parameters:
+      mylist:
+        description: ""
+        type: list
+        default: []
+        allowed:
+          - []
+          - ["one"]
+          - ["one", "two"]
+  allowed_list_multi:
+    description: pick multiple values from an allowed list
+    driver: example1
+    args:
+      args1: "{{up}}"
+    parameters:
+      mylist:
+        description: ""
+        type: list
+        default: []
+        allowed: ["one", "two", "three"]
diff --git a/intake/catalog/tests/example1_source.py b/intake/catalog/tests/example1_source.py
index a6444d5..928c81a 100644
--- a/intake/catalog/tests/example1_source.py
+++ b/intake/catalog/tests/example1_source.py
@@ -15,4 +15,5 @@ class ExampleSource(DataSource):
     partition_access = True
 
     def __init__(self, **kwargs):
+        self.kwargs = kwargs
         super(ExampleSource, self).__init__()
diff --git a/intake/catalog/tests/test_local.py b/intake/catalog/tests/test_local.py
index 1caebd2..3d978c9 100644
--- a/intake/catalog/tests/test_local.py
+++ b/intake/catalog/tests/test_local.py
@@ -248,6 +248,42 @@ def test_user_parameter_validation_allowed():
     assert 'allowed' in str(except_info.value)
 
 
+def test_user_pars_list():
+    # first case: allowed are all lists, must choose exactly one of them
+    # NB: order must match
+    p = local.UserParameter("", "", "list",
+                            allowed=[[], ["one"], ["one", "two"]])
+    with pytest.raises(TypeError):
+        p.validate(0)
+    with pytest.raises((TypeError, ValueError)):
+        # unfortunately, a string does coerce to a list
+        p.validate("one")
+    with pytest.raises(ValueError, match="allowed"):
+        p.validate(["two"])
+    with pytest.raises(ValueError, match="allowed"):
+        p.validate(["two", "one"])
+    p.validate(["one"])
+    p.validate(["one", "two"])
+
+
+def test_user_pars_mlist():
+    # second case: allowed are not lists, can choose any number of them
+    # NB: repeats are allowed
+    p = local.UserParameter("", "", "mlist",
+                            allowed=["one", "two", "three"])
+    with pytest.raises(TypeError):
+        p.validate(0)
+    with pytest.raises((TypeError, ValueError)):
+        # unfortunately, a string does coerce to a list
+        p.validate("one")
+    with pytest.raises(ValueError, match="allowed"):
+        p.validate(["two" ,"other"])
+    p.validate(["two"])
+    p.validate(["two", "two"])
+    p.validate(["two", "one"])
+    p.validate([])
+
+
 @pytest.mark.parametrize("filename", [
     "catalog_non_dict",
     "data_source_missing",