Repository: azavea/raster-vision

Issue:
Config builders cannot have type annotations in __init__
Trying to build [a config class](https://github.com/raster-foundry/raster-vision-plugin/blob/996044a503d09d311105d07da98b31284b6a6e91/src/rf_raster_vision_plugin/raster_source/config.py) with type annotations, you get:

```
In [7]: RfRasterSourceConfigBuilder(RfRasterSourceConfig).build()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-7-2f92db6db3a6> in <module>()
----> 1 RfRasterSourceConfigBuilder(RfRasterSourceConfig).build()

/opt/src/rastervision/core/config.py in build(self)
     99         """
    100         self.validate()
--> 101         arguments = set(inspect.getargspec(self.config_class).args)
    102         keys = set(self.config.keys())
    103         config = {k: self.config[k] for k in (arguments & keys)}

/usr/lib/python3.5/inspect.py in getargspec(func)
   1043         getfullargspec(func)
   1044     if kwonlyargs or ann:
-> 1045         raise ValueError("Function has keyword-only arguments or annotations"
   1046                          ", use getfullargspec() API which can support them")
   1047     return ArgSpec(args, varargs, varkw, defaults)

ValueError: Function has keyword-only arguments or annotations, use getfullargspec() API which can support them
```

Reproduction
-----

- `docker/run` from the linked repo
- `ipython`

```python
>>> from rf_raster_vision_plugin.raster_source.config import RfRasterSourceConfig, RfRasterSourceConfigBuilder
>>> RfRasterSourceConfigBuilder(RfRasterSourceConfig).build()
```

Expected Behavior
-----

Config builder shouldn't choke on type annotations


Discussion:
It turns out switching to comment-y annotations [is fine](https://github.com/raster-foundry/raster-vision-plugin/blob/2b58f1cfef93e9b529d5a6ccba8da0b30d4a0215/src/rf_raster_vision_plugin/raster_source/config.py#L18-L26), but it's still a bit unfortunate that the api requires a specific style of code-formatting for anything extending a certain class
@jamesmcclain Seems to come from [this change](https://github.com/azavea/raster-vision/commit/5266a24320fb25954ee1b71b72b4914da29cc0b8#diff-83205f8430adad30169d000644c39a2eR94) - and I'm a bit unclear of what this is doing. Would reimplementing with `getfullargspec` as the error message indicates be sufficient?
I am not able to immediately see what that code is doing either.

> @jamesmcclain Seems to come from [this change](https://github.com/azavea/raster-vision/commit/5266a24320fb25954ee1b71b72b4914da29cc0b8#diff-83205f8430adad30169d000644c39a2eR94) - and I'm a bit unclear of what this is doing. Would reimplementing with `getfullargspec` as the error message indicates be sufficient?

I am not able to immediately see what that code is doing right now, either.

I suspect that it is safe to re-implement it.  I will comment again if that view changes after looking at the whole PR that contains the changes that you are referencing above.
```
inspect.getargspec(func)
Get the names and default values of a Python function’s arguments. A tuple of four things is returned: (args, varargs, keywords, defaults). args is a list of the argument names (it may contain nested lists). varargs and keywords are the names of the * and ** arguments or None. defaults is a tuple of default argument values or None if there are no default arguments; if this tuple has n elements, they correspond to the last n elements listed in args.

Changed in version 2.6: Returns a named tuple ArgSpec(args, varargs, keywords, defaults).
```

It looks like what this is doing is insuring that the configuration consists only of things with keys that are valid arguments to the function.  I think that this is there because the `**` turns a dictionary into function arguments, and there were instances when things were failing without it (as a result of other changes in that PR).

Please feel free to change with that in mind.


Resolving patch:
diff --git a/rastervision/core/config.py b/rastervision/core/config.py
--- a/rastervision/core/config.py
+++ b/rastervision/core/config.py
@@ -98,7 +98,7 @@ def build(self):
         """Returns the configuration that is built by this builder.
         """
         self.validate()
-        arguments = set(inspect.getargspec(self.config_class).args)
+        arguments = set(inspect.getfullargspec(self.config_class).args)
         keys = set(self.config.keys())
         config = {k: self.config[k] for k in (arguments & keys)}
         return self.config_class(**config)


Test patch:
diff --git a/tests/core/test_config.py b/tests/core/test_config.py
new file mode 100644
--- /dev/null
+++ b/tests/core/test_config.py
@@ -0,0 +1,29 @@
+from copy import deepcopy
+import unittest
+from rastervision.core.config import ConfigBuilder
+
+
+class DummyConfig(object):
+    def __init__(self, foo: str):
+        self.foo = foo
+
+
+class DummyConfigBuilder(ConfigBuilder):
+    config_class = DummyConfig
+    config = {'foo': 'bar'}
+
+    def __init__(self):
+        return
+
+    def with_foo(self, foo):
+        b = deepcopy(self)
+        b.foo = foo
+        return b
+
+    def from_proto(self, msg):
+        return self.with_foo(msg.foo)
+
+
+class TestConfig(unittest.TestCase):
+    def test_build_with_annotations(self):
+        self.assertTrue(DummyConfigBuilder().build().foo == 'bar')