Repository: azavea/raster-vision

Issue:
Plugin configuration values cannot handle list of values
The plugin component of the configuration file is set up to be parsed as a JSON list. However, a multi-item list causes a parse exception when Everett parses the configuration.

E.g.:

```
[PLUGINS]
modules = [ "tests.test_plugin" ]
```

works, while

```
[PLUGINS]
modules = [ "tests.test_plugin", "tests.test_plugin" ]
```

does not, and errors with:

```
  File "/opt/src/rastervision/registry.py", line 179, in initialize_config
    verbosity=verbosity)
  File "/opt/src/rastervision/rv_config.py", line 146, in __init__
    ConfigIniEnv(config_file_locations),
  File "/usr/local/lib/python3.5/dist-packages/everett/manager.py", line 602, in __init__
    self.cfg.update(ConfigIniEnv.parse_ini_file(path))
  File "/usr/local/lib/python3.5/dist-packages/everett/manager.py", line 606, in parse_ini_file
    cfgobj = ConfigObj(path)
  File "/usr/local/lib/python3.5/dist-packages/configobj.py", line 1229, in __init__
    self._load(infile, configspec)
  File "/usr/local/lib/python3.5/dist-packages/configobj.py", line 1318, in _load
    raise error
  File "<string>", line None
configobj.ParseError: Parse error in value at line 2.
```


Discussion:

Resolving patch:
diff --git a/rastervision/plugin.py b/rastervision/plugin.py
--- a/rastervision/plugin.py
+++ b/rastervision/plugin.py
@@ -13,6 +13,24 @@ class PluginError(Exception):
     pass
 
 
+def load_conf_list(s):
+    """Loads a list of items from the config.
+
+    Lists should be comma separated.
+
+    This takes into account that previous versions of Raster Vision
+    allowed for a `[ "module" ]` like syntax, even though that didn't
+    work for multi-value lists.
+    """
+    try:
+        # A comma separated list of values will be transformed to
+        # having a list-like string, with ' instead of ". Replacing
+        # single quotes with double quotes lets us parse it as a JSON list.
+        return json.loads(s.replace("'", '"'))
+    except json.JSONDecodeError:
+        return list(map(lambda x: x.strip(), s.split(',')))
+
+
 class PluginRegistry:
     @staticmethod
     def get_instance():
@@ -38,11 +56,11 @@ def __init__(self, plugin_config, rv_home):
         self.experiment_runners = {}
         self.filesystems = []
 
-        plugin_files = json.loads(plugin_config('files', default='[]'))
+        plugin_files = load_conf_list(plugin_config('files', default='[]'))
         self._load_from_files(plugin_files)
         self.plugin_files = plugin_files
 
-        plugin_modules = json.loads(plugin_config('modules', default='[]'))
+        plugin_modules = load_conf_list(plugin_config('modules', default='[]'))
         self._load_from_modules(plugin_modules)
         self.plugin_modules = plugin_modules
 


Test patch:
diff --git a/tests/data-files/plugins/default b/tests/data-files/plugins/default
--- a/tests/data-files/plugins/default
+++ b/tests/data-files/plugins/default
@@ -1,2 +1,2 @@
 [PLUGINS]
-modules = [ "tests.test_plugin" ]
\ No newline at end of file
+modules = tests.test_plugin,tests.test_plugin_2
\ No newline at end of file
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -10,16 +10,18 @@ class TestPlugin(unittest.TestCase):
     def test_single_plugin_from_path(self):
         config = {
             'PLUGINS_files':
-            '["{}"]'.format(data_file_path('plugins/noop_augmentor.py'))
+            '["{}"]'.format(
+                data_file_path('plugins/noop_raster_transformer.py'))
         }
         rv._registry.initialize_config(config_overrides=config)
 
         try:
-            augmentor = rv.AugmentorConfig.builder('NOOP_AUGMENTOR') \
-                                          .build() \
-                                          .create_augmentor()
+            transformer = rv.RasterTransformerConfig.builder('NOOP_TRANSFORMER') \
+                                                    .build() \
+                                                    .create_transformer()
 
-            self.assertIsInstance(augmentor, rv.augmentor.Augmentor)
+            self.assertIsInstance(transformer,
+                                  rv.data.raster_transformer.RasterTransformer)
         finally:
             # Reset config
             rv._registry.initialize_config()
@@ -40,13 +42,13 @@ def test_plugin_from_module(self):
 
     def test_runs_noop_experiment_from_plugins(self):
         # set the env var to have rv pick up this configuration
-        # which adds the tests.test_plugin module as a plugin.
+        # which adds the tests.test_plugin and tests.test_plugin_2 modules
+        # as a plugin.
         old_rv_config = os.environ.get('RV_CONFIG')
         os.environ['RV_CONFIG'] = data_file_path('plugins/default')
 
         try:
             plugin_files = [
-                data_file_path('plugins/noop_augmentor.py'),
                 data_file_path('plugins/noop_task.py'),
                 data_file_path('plugins/noop_backend.py'),
                 data_file_path('plugins/noop_raster_transformer.py'),
diff --git a/tests/data-files/plugins/noop_augmentor.py b/tests/test_plugin_2.py
similarity index 94%
rename from tests/data-files/plugins/noop_augmentor.py
rename to tests/test_plugin_2.py
--- a/tests/data-files/plugins/noop_augmentor.py
+++ b/tests/test_plugin_2.py
@@ -3,14 +3,12 @@
                                     AugmentorConfigBuilder)
 from rastervision.protos.augmentor_pb2 import AugmentorConfig as AugmentorConfigMsg
 
-from .noop_utils import noop
-
 NOOP_AUGMENTOR = 'NOOP_AUGMENTOR'
 
 
 class NoopAugmentor(Augmentor):
     def process(self, training_data, tmp_dir):
-        return noop(training_data)
+        return training_data
 
 
 class NoopAugmentorConfig(AugmentorConfig):