Repository: pantsbuild/pants

Issue:
dict type options in config files
One can also put a dict value into a config file (please add to http://www.pantsbuild.org/options.html):

    [scope]
    config: {
        "domain": "example.com",
      }

This is not well handled though:
```shell
./pants options --options-scope=scope --options-name=config --options-output-format=json
{
  "scope.config": {
    "source": "CONFIG", 
    "value": "{\"domain\": \"example.com\",}"
  }
}
```
expected:
```shell
./pants options --options-scope=scope --options-name=config --options-output-format=json
{
  "scope.config": {
    "source": "CONFIG", 
    "value": {
      "domain": "example.com"
    }
  }
}
```



Discussion:

Resolving patch:
diff --git a/src/python/pants/backend/jvm/tasks/scalafmt.py b/src/python/pants/backend/jvm/tasks/scalafmt.py
--- a/src/python/pants/backend/jvm/tasks/scalafmt.py
+++ b/src/python/pants/backend/jvm/tasks/scalafmt.py
@@ -33,9 +33,9 @@ def register_options(cls, register):
     register('--configuration', advanced=True, type=file_option, fingerprint=False,
               help='Path to scalafmt config file, if not specified default scalafmt config used')
     register('--target-types',
-             default={'scala_library', 'junit_tests', 'java_tests'},
+             default=['scala_library', 'junit_tests', 'java_tests'],
              advanced=True,
-             type=set,
+             type=list,
              help='The target types to apply formatting to.')
     cls.register_jvm_tool(register,
                           'scalafmt',
@@ -47,7 +47,7 @@ def register_options(cls, register):
 
   @memoized_property
   def _formatted_target_types(self):
-    aliases = self.get_options().target_types
+    aliases = set(self.get_options().target_types)
     registered_aliases = self.context.build_file_parser.registered_aliases()
     return tuple({target_type
                   for alias in aliases
diff --git a/src/python/pants/core_tasks/explain_options_task.py b/src/python/pants/core_tasks/explain_options_task.py
--- a/src/python/pants/core_tasks/explain_options_task.py
+++ b/src/python/pants/core_tasks/explain_options_task.py
@@ -80,18 +80,18 @@ def _format_scope(self, scope, option, no_color=False):
     )
 
   def _format_record(self, record):
-    value_color = green if self.get_options().colors else lambda x: x
-    rank_color = self._rank_color(record.rank)
-    simple_value = str(record.value)
-    formatted_value = value_color(simple_value)
     simple_rank = RankedValue.get_rank_name(record.rank)
-    formatted_rank = '(from {rank}{details})'.format(
-      rank=simple_rank,
-      details=rank_color(' {}'.format(record.details)) if record.details else '',
-    )
     if self.is_json():
-      return simple_value.replace('\n', ''), simple_rank
+      return record.value, simple_rank
     elif self.is_text():
+      simple_value = str(record.value)
+      value_color = green if self.get_options().colors else lambda x: x
+      formatted_value = value_color(simple_value)
+      rank_color = self._rank_color(record.rank)
+      formatted_rank = '(from {rank}{details})'.format(
+        rank=simple_rank,
+        details=rank_color(' {}'.format(record.details)) if record.details else '',
+      )
       return '{value} {rank}'.format(
         value=formatted_value,
         rank=formatted_rank,
@@ -148,9 +148,16 @@ def console_output(self, targets):
           if parent_scope is not None and parent_value == history.latest.value:
             continue
         if self.is_json():
-          opt_vals = self._format_record(history.latest)
+          value, rank_name = self._format_record(history.latest)
           scope_key = self._format_scope(scope, option, True)
-          inner_map = dict(value=opt_vals[0], source=opt_vals[1])
+          # We rely on the fact that option values are restricted to a set of types compatible with
+          # json. In particular, we expect dict, list, str, bool, int and float, and so do no
+          # processing here.
+          # TODO(John Sirois): The option parsing system currently lets options of unexpected types
+          # slide by, which can lead to un-overridable values and which would also blow up below in
+          # json encoding, fix options to restrict the allowed `type`s:
+          #   https://github.com/pantsbuild/pants/issues/4695
+          inner_map = dict(value=value, source=rank_name)
           output_map[scope_key] = inner_map
         elif self.is_text():
           yield '{} = {}'.format(self._format_scope(scope, option),


Test patch:
diff --git a/tests/python/pants_test/option/test_options_integration.py b/tests/python/pants_test/option/test_options_integration.py
--- a/tests/python/pants_test/option/test_options_integration.py
+++ b/tests/python/pants_test/option/test_options_integration.py
@@ -46,7 +46,7 @@ def test_valid_json(self):
       output_map = json.loads(pants_run.stdout_data)
       self.assertIn("time", output_map)
       self.assertEquals(output_map["time"]["source"], "HARDCODED")
-      self.assertEquals(output_map["time"]["value"], "False")
+      self.assertEquals(output_map["time"]["value"], False)
     except ValueError:
       self.fail("Invalid JSON output")
 
@@ -58,7 +58,7 @@ def test_valid_json_with_history(self):
       output_map = json.loads(pants_run.stdout_data)
       self.assertIn("time", output_map)
       self.assertEquals(output_map["time"]["source"], "HARDCODED")
-      self.assertEquals(output_map["time"]["value"], "False")
+      self.assertEquals(output_map["time"]["value"], False)
       self.assertEquals(output_map["time"]["history"], [])
       for _, val in output_map.items():
         self.assertIn("history", val)