Repository: cloudtools/troposphere

Issue:
TemplateGenerator fail to parse template Fn::Sub with variable map
I trying to load a SAM template generated from chalice and I encounter this error. I simplify the template as much as possible.

```json
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Resources": {
    "MySNS":{
      "Type" : "AWS::SNS::Topic",
      "Properties" : {
        "DisplayName" : "Display Name",
        "TopicName" : {
          "Fn::Sub": [ "MySNS-Test-${ABC}",
            {
              "ABC": "abc"
            }
          ]
        }
      }
    }
  }
}
``` 

Simple script to reproduce the issue
```python
import json
from troposphere.template_generator import TemplateGenerator

with open("sns.json", 'r') as f:
    json_data = json.loads(f.read())
template = TemplateGenerator(json_data)
```

Console output
```
>python sample.py
Traceback (most recent call last):
  File "/Users/lemjean/Unicorn/xxxxxxxxxx/.venv2/lib/python3.6/site-packages/troposphere/template_generator.py", line 227, in _create_instance
    return cls(*self._convert_definition(args))
TypeError: __init__() takes 2 positional arguments but 3 were given

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "sample.py", line 7, in <module>
    template = TemplateGenerator(json_data)
  File "/Users/lemjean/Unicorn/xxxxxxxxxx/.venv2/lib/python3.6/site-packages/troposphere/template_generator.py", line 63, in __init__
    self._get_resource_type_cls(v)
  File "/Users/lemjean/Unicorn/xxxxxxxxxx/.venv2/lib/python3.6/site-packages/troposphere/template_generator.py", line 162, in _convert_definition
    return self._create_instance(expected_type, args, ref)
  File "/Users/lemjean/Unicorn/xxxxxxxxxx/.venv2/lib/python3.6/site-packages/troposphere/template_generator.py", line 268, in _create_instance
    kwargs[prop_name], prop_name)
  File "/Users/lemjean/Unicorn/xxxxxxxxxx/.venv2/lib/python3.6/site-packages/troposphere/template_generator.py", line 169, in _convert_definition
    function_type, list(definition.values())[0])
  File "/Users/lemjean/Unicorn/xxxxxxxxxx/.venv2/lib/python3.6/site-packages/troposphere/template_generator.py", line 243, in _create_instance
    if '__init__() takes exactly' not in ex.message:
AttributeError: 'TypeError' object has no attribute 'message'
```

More information about my env:
* python 3.6.5
* troposhere installed with `pip install troposphere[policy]`
  * awacs==0.8.1
  * cfn-flip==1.0.3
  * Click==7.0
  * PyYAML==3.13
  * six==1.11.0
  * troposphere==2.3.3



Discussion:

Resolving patch:
diff --git a/troposphere/__init__.py b/troposphere/__init__.py
--- a/troposphere/__init__.py
+++ b/troposphere/__init__.py
@@ -473,7 +473,10 @@ def __init__(self, delimiter, values):
 
 
 class Sub(AWSHelperFn):
-    def __init__(self, input_str, **values):
+    def __init__(self, input_str, dict_values=None, **values):
+        # merge dict
+        if dict_values:
+            values.update(dict_values)
         self.data = {'Fn::Sub': [input_str, values] if values else input_str}
 
 


Test patch:
diff --git a/tests/test_basic.py b/tests/test_basic.py
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -406,14 +406,14 @@ def test_getcidr_withsizemask(self):
 
 class TestSub(unittest.TestCase):
 
-    def test_sub_with_vars(self):
+    def test_sub_without_vars(self):
         s = 'foo ${AWS::Region}'
         raw = Sub(s)
         actual = raw.to_dict()
         expected = {'Fn::Sub': 'foo ${AWS::Region}'}
         self.assertEqual(expected, actual)
 
-    def test_sub_without_vars(self):
+    def test_sub_with_vars_unpakaged(self):
         s = 'foo ${AWS::Region} ${sub1} ${sub2}'
         values = {'sub1': 'uno', 'sub2': 'dos'}
         raw = Sub(s, **values)
@@ -421,6 +421,31 @@ def test_sub_without_vars(self):
         expected = {'Fn::Sub': ['foo ${AWS::Region} ${sub1} ${sub2}', values]}
         self.assertEqual(expected, actual)
 
+    def test_sub_with_vars_not_unpakaged(self):
+        s = 'foo ${AWS::Region} ${sub1} ${sub2}'
+        values = {'sub1': 'uno', 'sub2': 'dos'}
+        raw = Sub(s, values)
+        actual = raw.to_dict()
+        expected = {'Fn::Sub': ['foo ${AWS::Region} ${sub1} ${sub2}', values]}
+        self.assertEqual(expected, actual)
+
+    def test_sub_with_vars_mix(self):
+        s = 'foo ${AWS::Region} ${sub1} ${sub2} ${sub3}'
+        values = {'sub1': 'uno', 'sub2': 'dos'}
+        raw = Sub(s, values, sub3='tres')
+        actual = raw.to_dict()
+        expected = {
+            'Fn::Sub': [
+                'foo ${AWS::Region} ${sub1} ${sub2} ${sub3}',
+                {
+                    'sub1': 'uno',
+                    'sub2': 'dos',
+                    'sub3': 'tres'
+                }
+            ]
+        }
+        self.assertEqual(expected, actual)
+
 
 class TestSplit(unittest.TestCase):