Repository: reframe-hpc/reframe

Issue:
Feature request: set multi-level dictionaries from the command line
I'd like to set `extra_resources` from the command line, but it's currently not possible because it's a dictionary of dictionaries, and there's currently no syntax for that.

I should have some time to work on this, but I'd need some directions, in terms of where the code is located (I haven't tried to search it yet) and discuss what the syntax for this would look like.

Discussion:
vkarak: What would be the syntax for setting a nested dict from the command line? Could you give an example?
giordano: Something like
```
-S extra_resources=a:{b:c, d:{e:f}},g:h
```

Patch:
diff --git a/docs/manpage.rst b/docs/manpage.rst
index a59add04..ee2348cd 100644
--- a/docs/manpage.rst
+++ b/docs/manpage.rst
@@ -650,6 +650,9 @@ Options controlling ReFrame execution
    - Sequence types: ``-S seqvar=1,2,3,4``
    - Mapping types: ``-S mapvar=a:1,b:2,c:3``
 
+   Nested mapping types can also be converted using JSON syntax.
+   For example, the :attr:`~reframe.core.pipeline.RegressionTest.extra_resources` complex dictionary could be set with ``-S extra_resources='{"gpu": {"num_gpus_per_node":8}}'``.
+
    Conversions to arbitrary objects are also supported.
    See :class:`~reframe.utility.typecheck.ConvertibleType` for more details.
 
@@ -705,6 +708,9 @@ Options controlling ReFrame execution
 
       Allow setting variables in fixtures.
 
+   .. versionchanged:: 4.4
+
+      Allow setting nested mapping types using JSON syntax.
 
 .. option:: --skip-performance-check
 
diff --git a/reframe/utility/typecheck.py b/reframe/utility/typecheck.py
index 9f27234e..e09bc503 100644
--- a/reframe/utility/typecheck.py
+++ b/reframe/utility/typecheck.py
@@ -95,6 +95,7 @@ of :class:`List[int]`.
 
 import abc
 import datetime
+import json
 import re
 
 
@@ -322,19 +323,23 @@ class _MappingType(_BuiltinType):
         mappping_type = cls._type
         key_type = cls._key_type
         value_type = cls._value_type
-        seq = []
-        for key_datum in s.split(','):
-            try:
-                k, v = key_datum.split(':')
-            except ValueError:
-                # Re-raise as TypeError
-                raise TypeError(
-                    f'cannot convert string {s!r} to {cls.__name__!r}'
-                ) from None
-
-            seq.append((key_type(k), value_type(v)))
-
-        return mappping_type(seq)
+
+        try:
+            items = json.loads(s)
+        except json.JSONDecodeError:
+            items = []
+            for key_datum in s.split(','):
+                try:
+                    k, v = key_datum.split(':')
+                except ValueError:
+                    # Re-raise as TypeError
+                    raise TypeError(
+                        f'cannot convert string {s!r} to {cls.__name__!r}'
+                    ) from None
+
+                items.append((key_type(k), value_type(v)))
+
+        return mappping_type(items)
 
 
 class _StrType(_SequenceType):


Test patch:
diff --git a/unittests/test_typecheck.py b/unittests/test_typecheck.py
index f640a9d7..79342d37 100644
--- a/unittests/test_typecheck.py
+++ b/unittests/test_typecheck.py
@@ -208,6 +208,14 @@ def test_mapping_type():
     # Test conversions
     assert typ.Dict[str, int]('a:1,b:2') == {'a': 1, 'b': 2}
 
+    # Conversion with JSON syntax, for nested dictionaries
+    s = '{"gpu":{"num_gpus_per_node": 8}, "mpi": {"num_slots": 64}}'
+    expected = {
+        'gpu': {'num_gpus_per_node': 8},
+        'mpi': {'num_slots': 64},
+    }
+    assert typ.Dict[str, typ.Dict[str, object]](s) == expected
+
     with pytest.raises(TypeError):
         typ.Dict[str, int]('a:1,b')