Repository: plotly/dash

Issue:
Support Flow typing in component_loader
## Description
Right now, creating a custom Dash component requires using the `prop-types` library. `react-docgen` used in `dash-components-archetype` [here](https://github.com/plotly/dash-components-archetype/blob/master/package.json#L37) does support Flow types, but the `metadata.json` parser in `component_loader` uses the fields generated from the `PropTypes` attribute of the component. Development of new components would be a potentially better environment if it also supported using Flow (see, e.g., a [`material-ui` `Dialog` component](https://github.com/StratoDem/sd-material-ui/blob/master/src/components/SDDialog.react.js#L57). It would also allow faster porting of React components already written with Flow to Dash components. 

### Necessary pieces
- [ ] The [eslint file](https://github.com/plotly/dash-components-archetype/blob/master/config/eslint/eslintrc-react.json) in `dash-components-archetype` will need to use `eslint-plugin-flowtype`
- [ ] The [babelrc file](https://github.com/plotly/dash-components-archetype/blob/master/config/babel/babelrc) in `dash-components-archetype` will need to use [`transform-class-properties`](https://babeljs.io/docs/plugins/transform-class-properties/) in order to support `static defaultProps = {}` (not required, but would be nice), and the [`react` preset already handles stripping Flow types](https://www.npmjs.com/package/babel-preset-react). 
- [ ] `package.json` in `dash-components-archetype` would need (at least) `eslint-plugin-flowtype` and `babel-plugin-transform-class-properties`
- [x] One of the following two to address the metadata: 
    - [x] `metadata.json` will need to be generated using the generated Flow types (matching the current structure) in `dash-components-archetype`, if they exist or 
    - [x] `component_loader` will need to support Flow types if they are found in the `metadata.json` and the fields generated by `PropTypes` are not found.

Note that some of the code changes would occur in `dash-components-archetype`, but figured here might be a better place to discuss since the `component_loader` is the biggest "Dash user"-facing piece. There may also be other necessary pieces that I missed. 

We've set this structure up in the [`material-ui` ports](https://github.com/StratoDem/sd-material-ui) that we're using internally for our own material Dash components, and very much like it (we're also using jest for testing and would *love* to get the Dash toolchain updated to use [React 16](https://github.com/plotly/dash-components-archetype/issues/35) since the current chain is tied to an older version of React).

If this makes sense, I can create issues specifically on `dash-components-archetype` and plan to get this out in the next couple of weeks. There should not be any breaking changes anywhere from addressing this issue. 

Finally: *insert flamewar about type hinting here* :smile:


Discussion:
> There should not be any breaking changes anywhere from addressing this issue.

If so, then this sounds good to me! I totally respect flow users and the Dash component archetype should be as flexible as possible. 

Could you point me to some code snippets in your `material-ui` project that shows how this currently works?
Would any of these additions to `eslint`, `babelrc`, or `package.json` affect component developers who are not using Flow? If they do affect those developers, then perhaps the setup could ask the developers if they would like to use flow or not:
```
builder-init dash-components-archetype
dash-components-archetype-0.2.11.tgz
[builder-init] Preparing templates for: dash-components-archetype
? Please name your component suite.
? Are you using Flow? (n/Y, default: n)
```

>Could you point me to some code snippets in your material-ui project that shows how this currently works?
> ... 
> Would any of these additions to eslint, babelrc, or package.json affect component developers who are not using Flow?

There's actually not that much more since the babel react preset already supports stripping Flow types. But here's a [`FlatButton` component](https://github.com/StratoDem/sd-material-ui/blob/master/src/components/SDFlatButton.react.js#L163) that uses both Flow types and the `prop-types` package, since that is still needed for the `metadata.json` generation and parsing by Dash. The use of Flow there is just superfluous for paranoid typing. 

The user could actually set up their own eslint to extend the current `dash-components-archetype` `eslint` files, but add in `plugin:flowtype/recommended`. For example, we completely replaced the `dash-components-archetype` `eslint` and `babelrc` in the `sd-material-ui` project without having any issues, and then just [modified the `build-dist` commands](https://github.com/StratoDem/sd-material-ui/blob/master/package.json#L17) to use a custom webpack config (which is definitely not necessary to get Flow set up). This now seems like a completely separate issue, and perhaps meant more for the documentation (e.g., a "How to add Flow" section on the Dash docs) than actually changing code.

Some related discussion about config via `builder` may be here https://github.com/plotly/dash-components-archetype/issues/37#issuecomment-358029388

#### Modified next steps
It does seem like after some further thought and your questions that the only thing really needed to support Flow out of the box is to modify the `component_loader` to handle the `metadata.json` types generated by the `prop-types` library if those exist, and otherwise look for the types in `metadata.json` generated by Flow (which have a different structure). Then a user could just write Flow-typed React code and not see any difference in the generated Dash Python component. This approach also would not modify existing behavior. However, this does have a couple of drawbacks: 
1. A user making a dash component with both `prop-types` and Flow hinting for the props would generate a larger `metadata.json` file than necessary.
   - This would be a rare case since Flow can replace `prop-types`, and the benefit of `prop-types` for runtime checking is gone in the production build anyway
2. Potentially larger burden on the Python end of things to make decisions when parsing `metadata.json`
    - This seems fine as a very minor "compile" time hit.

If it makes sense, I or someone on my team can likely put something together for a `component_loader` that supports Flow-generated `metadata.json` next week.
> If it makes sense, I or someone on my team can likely put something together for a component_loader that supports Flow-generated metadata.json next week.

I think that this makes sense and it seems like much lesser impact. Looking forward to seeing what this looks like!

Resolving patch:
diff --git a/dash/development/base_component.py b/dash/development/base_component.py
--- a/dash/development/base_component.py
+++ b/dash/development/base_component.py
@@ -22,12 +22,12 @@ class Component(collections.MutableMapping):
     def __init__(self, **kwargs):
         # pylint: disable=super-init-not-called
         for k, v in list(kwargs.items()):
-            if k not in self._prop_names:  # pylint: disable=no-member
-                # TODO - What's the right exception here?
-                # pylint: disable=no-member
-                raise Exception(
+            # pylint: disable=no-member
+            if k not in self._prop_names:
+                raise TypeError(
                     'Unexpected keyword argument `{}`'.format(k) +
                     '\nAllowed arguments: {}'.format(
+                        # pylint: disable=no-member
                         ', '.join(sorted(self._prop_names))
                     )
                 )
@@ -186,12 +186,28 @@ def __len__(self):
 
 # pylint: disable=unused-argument
 def generate_class(typename, props, description, namespace):
-    # Dynamically generate classes to have nicely formatted docstrings,
-    # keyword arguments, and repr
-    # Insired by http://jameso.be/2013/08/06/namedtuple.html
-
+    """
+    Dynamically generate classes to have nicely formatted docstrings,
+    keyword arguments, and repr
+
+    Inspired by http://jameso.be/2013/08/06/namedtuple.html
+
+    Parameters
+    ----------
+    typename
+    props
+    description
+    namespace
+
+    Returns
+    -------
+
+    """
+    # TODO _prop_names, _type, _namespace, available_events,
+    # and available_properties
+    # can be modified by a Dash JS developer via setattr
     # TODO - Tab out the repr for the repr of these components to make it
-    # look more like a heirarchical tree
+    # look more like a hierarchical tree
     # TODO - Include "description" "defaultValue" in the repr and docstring
     #
     # TODO - Handle "required"
@@ -204,51 +220,52 @@ def generate_class(typename, props, description, namespace):
     # The solution might be to deal with default values better although
     # not all component authors will supply those.
     c = '''class {typename}(Component):
-        """{docstring}
-        """
-        def __init__(self, {default_argtext}):
-            self._prop_names = {list_of_valid_keys}
-            self._type = '{typename}'
-            self._namespace = '{namespace}'
-            self.available_events = {events}
-            self.available_properties = {list_of_valid_keys}
-
-            for k in {required_args}:
-                if k not in kwargs:
-                    raise Exception(
-                        'Required argument `' + k + '` was not specified.'
-                    )
-
-            super({typename}, self).__init__({argtext})
-
-        def __repr__(self):
-            if(any(getattr(self, c, None) is not None for c in self._prop_names
-                   if c is not self._prop_names[0])):
-
-                return (
-                    '{typename}(' +
-                    ', '.join([c+'='+repr(getattr(self, c, None))
-                               for c in self._prop_names
-                               if getattr(self, c, None) is not None])+')')
+    """{docstring}"""
+    def __init__(self, {default_argtext}):
+        self._prop_names = {list_of_valid_keys}
+        self._type = '{typename}'
+        self._namespace = '{namespace}'
+        self.available_events = {events}
+        self.available_properties = {list_of_valid_keys}
+
+        for k in {required_args}:
+            if k not in kwargs:
+                raise TypeError(
+                    'Required argument `' + k + '` was not specified.')
+
+        super({typename}, self).__init__({argtext})
+
+    def __repr__(self):
+        if(any(getattr(self, c, None) is not None for c in self._prop_names
+               if c is not self._prop_names[0])):
+
+            return (
+                '{typename}(' +
+                ', '.join([c+'='+repr(getattr(self, c, None))
+                           for c in self._prop_names
+                           if getattr(self, c, None) is not None])+')')
 
-            else:
-                return (
-                    '{typename}(' +
-                    repr(getattr(self, self._prop_names[0], None)) + ')')
+        else:
+            return (
+                '{typename}(' +
+                repr(getattr(self, self._prop_names[0], None)) + ')')
     '''
 
-    # pylint: disable=unused-variable
     filtered_props = reorder_props(filter_props(props))
+    # pylint: disable=unused-variable
     list_of_valid_keys = repr(list(filtered_props.keys()))
+    # pylint: disable=unused-variable
     docstring = create_docstring(
-        typename,
-        filtered_props,
-        parse_events(props),
-        description
-    )
+        component_name=typename,
+        props=filtered_props,
+        events=parse_events(props),
+        description=description)
+
+    # pylint: disable=unused-variable
     events = '[' + ', '.join(parse_events(props)) + ']'
     if 'children' in props:
         default_argtext = 'children=None, **kwargs'
+        # pylint: disable=unused-variable
         argtext = 'children=children, **kwargs'
     else:
         default_argtext = '**kwargs'
@@ -266,159 +283,372 @@ def __repr__(self):
 
 
 def required_props(props):
+    """
+    Pull names of required props from the props object
+
+    Parameters
+    ----------
+    props: dict
+
+    Returns
+    -------
+    list
+        List of prop names (str) that are required for the Component
+    """
     return [prop_name for prop_name, prop in list(props.items())
             if prop['required']]
 
 
-def reorder_props(props):
-    # If "children" is a prop, then move it to the front to respect
-    # dash convention
-    if 'children' in props:
-        props = collections.OrderedDict(
-            [('children', props.pop('children'), )] +
-            list(zip(list(props.keys()), list(props.values())))
-        )
-    return props
+def create_docstring(component_name, props, events, description):
+    """
+    Create the Dash component docstring
+
+    Parameters
+    ----------
+    component_name: str
+        Component name
+    props: dict
+        Dictionary with {propName: propMetadata} structure
+    events: list
+        List of Dash events
+    description: str
+        Component description
+
+    Returns
+    -------
+    str
+        Dash component docstring
+    """
+    # Ensure props are ordered with children first
+    props = reorder_props(props=props)
+
+    return (
+        """A {name} component.\n{description}
+
+Keyword arguments:\n{args}
+
+Available events: {events}"""
+    ).format(
+        name=component_name,
+        description=description,
+        args='\n'.join(
+            create_prop_docstring(
+                prop_name=p,
+                type_object=prop['type'] if 'type' in prop
+                else prop['flowType'],
+                required=prop['required'],
+                description=prop['description'],
+                indent_num=0,
+                is_flow_type='flowType' in prop and 'type' not in prop)
+            for p, prop in list(filter_props(props).items())),
+        events=', '.join(events))
 
 
 def parse_events(props):
-    if ('dashEvents' in props and
-            props['dashEvents']['type']['name'] == 'enum'):
+    """
+    Pull out the dashEvents from the Component props
+
+    Parameters
+    ----------
+    props: dict
+        Dictionary with {propName: propMetadata} structure
+
+    Returns
+    -------
+    list
+        List of Dash event strings
+    """
+    if 'dashEvents' in props and props['dashEvents']['type']['name'] == 'enum':
         events = [v['value'] for v in props['dashEvents']['type']['value']]
     else:
         events = []
+
     return events
 
 
-def create_docstring(name, props, events, description):
+def reorder_props(props):
+    """
+    If "children" is in props, then move it to the
+    front to respect dash convention
+
+    Parameters
+    ----------
+    props: dict
+        Dictionary with {propName: propMetadata} structure
+
+    Returns
+    -------
+    dict
+        Dictionary with {propName: propMetadata} structure
+    """
     if 'children' in props:
         props = collections.OrderedDict(
-            [['children', props.pop('children')]] +
-            list(zip(list(props.keys()), list(props.values())))
-        )
-    return '''A {name} component.{description}
+            [('children', props.pop('children'),)] +
+            list(zip(list(props.keys()), list(props.values()))))
+
+    return props
 
-    Keyword arguments:
-    {args}
 
-    Available events: {events}'''.format(
-        name=name,
-        description='\n{}'.format(description),
-        args='\n'.join(
-            ['- {}'.format(argument_doc(
-                p, prop['type'], prop['required'], prop['description']
-            )) for p, prop in list(filter_props(props).items())]
-        ),
-        events=', '.join(events)
-    ).replace('    ', '')
-
-
-def filter_props(args):
-    filtered_args = copy.deepcopy(args)
-    for arg_name, arg in list(filtered_args.items()):
-        if 'type' not in arg:
-            filtered_args.pop(arg_name)
+def filter_props(props):
+    """
+    Filter props from the Component arguments to exclude:
+        - Those without a "type" or a "flowType" field
+        - Those with arg.type.name in {'func', 'symbol', 'instanceOf'}
+        - dashEvents as a name
+
+    Parameters
+    ----------
+    props: dict
+        Dictionary with {propName: propMetadata} structure
+
+    Returns
+    -------
+    dict
+        Filtered dictionary with {propName: propMetadata} structure
+
+    Examples
+    --------
+    ```python
+    prop_args = {
+        'prop1': {
+            'type': {'name': 'bool'},
+            'required': False,
+            'description': 'A description',
+            'flowType': {},
+            'defaultValue': {'value': 'false', 'computed': False},
+        },
+        'prop2': {'description': 'A prop without a type'},
+        'prop3': {
+            'type': {'name': 'func'},
+            'description': 'A function prop',
+        },
+    }
+    # filtered_prop_args is now
+    # {
+    #    'prop1': {
+    #        'type': {'name': 'bool'},
+    #        'required': False,
+    #        'description': 'A description',
+    #        'flowType': {},
+    #        'defaultValue': {'value': 'false', 'computed': False},
+    #    },
+    # }
+    filtered_prop_args = filter_props(prop_args)
+    ```
+    """
+    filtered_props = copy.deepcopy(props)
+
+    for arg_name, arg in list(filtered_props.items()):
+        if 'type' not in arg and 'flowType' not in arg:
+            filtered_props.pop(arg_name)
             continue
 
-        arg_type = arg['type']['name']
-        if arg_type in ['func', 'symbol', 'instanceOf']:
-            filtered_args.pop(arg_name)
+        # Filter out functions and instances --
+        # these cannot be passed from Python
+        if 'type' in arg:  # These come from PropTypes
+            arg_type = arg['type']['name']
+            if arg_type in {'func', 'symbol', 'instanceOf'}:
+                filtered_props.pop(arg_name)
+        elif 'flowType' in arg:  # These come from Flow & handled differently
+            arg_type_name = arg['flowType']['name']
+            if arg_type_name == 'signature':
+                # This does the same as the PropTypes filter above, but "func"
+                # is under "type" if "name" is "signature" vs just in "name"
+                if 'type' not in arg['flowType'] \
+                        or arg['flowType']['type'] != 'object':
+                    filtered_props.pop(arg_name)
+        else:
+            raise ValueError
 
         # dashEvents are a special oneOf property that is used for subscribing
         # to events but it's never set as a property
         if arg_name in ['dashEvents']:
-            filtered_args.pop(arg_name)
-    return filtered_args
-
-
-def js_to_py_type(type_object):
-    js_type_name = type_object['name']
-
-    # wrapping everything in lambda to prevent immediate execution
-    js_to_py_types = {
-        'array': lambda: 'list',
-        'bool': lambda: 'boolean',
-        'number': lambda: 'number',
-        'string': lambda: 'string',
-        'object': lambda: 'dict',
-
-        'any': lambda: 'boolean | number | string | dict | list',
-        'element': lambda: 'dash component',
-        'node': lambda: (
-            'a list of or a singular dash component, string or number'
-        ),
+            filtered_props.pop(arg_name)
+    return filtered_props
+
+
+# pylint: disable=too-many-arguments
+def create_prop_docstring(prop_name, type_object, required, description,
+                          indent_num, is_flow_type=False):
+    """
+    Create the Dash component prop docstring
+
+    Parameters
+    ----------
+    prop_name: str
+        Name of the Dash component prop
+    type_object: dict
+        react-docgen-generated prop type dictionary
+    required: bool
+        Component is required?
+    description: str
+        Dash component description
+    indent_num: int
+        Number of indents to use for the context block
+        (creates 2 spaces for every indent)
+    is_flow_type: bool
+        Does the prop use Flow types? Otherwise, uses PropTypes
+
+    Returns
+    -------
+    str
+        Dash component prop docstring
+    """
+    py_type_name = js_to_py_type(
+        type_object=type_object,
+        is_flow_type=is_flow_type,
+        indent_num=indent_num + 1)
+
+    indent_spacing = '  ' * indent_num
+    if '\n' in py_type_name:
+        return '{indent_spacing}- {name} ({is_required}): {description}. ' \
+               '{name} has the following type: {type}'.format(
+                   indent_spacing=indent_spacing,
+                   name=prop_name,
+                   type=py_type_name,
+                   description=description,
+                   is_required='required' if required else 'optional')
+    return '{indent_spacing}- {name} ({type}' \
+           '{is_required}){description}'.format(
+               indent_spacing=indent_spacing,
+               name=prop_name,
+               type='{}; '.format(py_type_name) if py_type_name else '',
+               description=(
+                   ': {}'.format(description) if description != '' else ''
+               ),
+               is_required='required' if required else 'optional')
+
+
+def map_js_to_py_types_prop_types(type_object):
+    """Mapping from the PropTypes js type object to the Python type"""
+    return dict(
+        array=lambda: 'list',
+        bool=lambda: 'boolean',
+        number=lambda: 'number',
+        string=lambda: 'string',
+        object=lambda: 'dict',
+        any=lambda: 'boolean | number | string | dict | list',
+        element=lambda: 'dash component',
+        node=lambda: 'a list of or a singular dash '
+                     'component, string or number',
 
         # React's PropTypes.oneOf
-        'enum': lambda: 'a value equal to: {}'.format(', '.join([
-            '{}'.format(str(t['value'])) for t in type_object['value']
-        ])),
+        enum=lambda: 'a value equal to: {}'.format(
+            ', '.join(
+                '{}'.format(str(t['value']))
+                for t in type_object['value'])),
 
         # React's PropTypes.oneOfType
-        'union': lambda: '{}'.format(' | '.join([
-            '{}'.format(js_to_py_type(subType))
-            for subType in type_object['value'] if js_to_py_type(subType) != ''
-        ])),
+        union=lambda: '{}'.format(
+            ' | '.join(
+                '{}'.format(js_to_py_type(subType))
+                for subType in type_object['value']
+                if js_to_py_type(subType) != '')),
 
         # React's PropTypes.arrayOf
-        # pylint: disable=too-many-format-args
-        'arrayOf': lambda: 'list'.format(
-            'of {}s'.format(js_to_py_type(type_object['value']))
+        arrayOf=lambda: 'list'.format(  # pylint: disable=too-many-format-args
+            ' of {}s'.format(
+                js_to_py_type(type_object['value']))
             if js_to_py_type(type_object['value']) != ''
-            else ''
-        ),
+            else ''),
 
         # React's PropTypes.objectOf
-        'objectOf': lambda: (
+        objectOf=lambda: (
             'dict with strings as keys and values of type {}'
-        ).format(js_to_py_type(type_object['value'])),
+            ).format(
+                js_to_py_type(type_object['value'])),
 
         # React's PropTypes.shape
-        'shape': lambda: (
-            'dict containing keys {}.\n{}'.format(
-                ', '.join(
-                    ["'{}'".format(t) for t in
-                     list(type_object['value'].keys())]
-                ),
-                'Those keys have the following types: \n{}'.format(
-                    '\n'.join([
-                        '  - ' + argument_doc(
-                            prop_name,
-                            prop,
-                            prop['required'],
-                            prop.get('description', '')
-                        ) for
-                        prop_name, prop in list(type_object['value'].items())
-                    ])
-                )
-            )
-        )
-    }
+        shape=lambda: 'dict containing keys {}.\n{}'.format(
+            ', '.join(
+                "'{}'".format(t)
+                for t in list(type_object['value'].keys())),
+            'Those keys have the following types: \n{}'.format(
+                '\n'.join(create_prop_docstring(
+                    prop_name=prop_name,
+                    type_object=prop,
+                    required=prop['required'],
+                    description=prop.get('description', ''),
+                    indent_num=1)
+                          for prop_name, prop in
+                          list(type_object['value'].items())))),
+    )
 
-    if 'computed' in type_object and type_object['computed']:
-        return ''
-    if js_type_name in js_to_py_types:
-        return js_to_py_types[js_type_name]()
-    return ''
 
+def map_js_to_py_types_flow_types(type_object):
+    """Mapping from the Flow js types to the Python type"""
+    return dict(
+        array=lambda: 'list',
+        boolean=lambda: 'boolean',
+        number=lambda: 'number',
+        string=lambda: 'string',
+        Object=lambda: 'dict',
+        any=lambda: 'bool | number | str | dict | list',
+        Element=lambda: 'dash component',
+        Node=lambda: 'a list of or a singular dash '
+                     'component, string or number',
 
-def argument_doc(arg_name, type_object, required, description):
-    py_type_name = js_to_py_type(type_object)
-    if '\n' in py_type_name:
-        return (
-            '{name} ({is_required}): {description}. '
-            '{name} has the following type: {type}'
-        ).format(
-            name=arg_name,
-            type=py_type_name,
-            description=description,
-            is_required='required' if required else 'optional'
-        )
-
-    return '{name} ({type}{is_required}){description}'.format(
-        name=arg_name,
-        type='{}; '.format(py_type_name) if py_type_name else '',
-        description=(
-            ': {}'.format(description) if description != '' else ''
-        ),
-        is_required='required' if required else 'optional'
+        # React's PropTypes.oneOfType
+        union=lambda: '{}'.format(
+            ' | '.join(
+                '{}'.format(js_to_py_type(subType))
+                for subType in type_object['elements']
+                if js_to_py_type(subType) != '')),
+
+        # Flow's Array type
+        Array=lambda: 'list{}'.format(
+            ' of {}s'.format(
+                js_to_py_type(type_object['elements'][0]))
+            if js_to_py_type(type_object['elements'][0]) != ''
+            else ''),
+
+        # React's PropTypes.shape
+        signature=lambda indent_num: 'dict containing keys {}.\n{}'.format(
+            ', '.join("'{}'".format(d['key'])
+                      for d in type_object['signature']['properties']),
+            '{}Those keys have the following types: \n{}'.format(
+                '  ' * indent_num,
+                '\n'.join(
+                    create_prop_docstring(
+                        prop_name=prop['key'],
+                        type_object=prop['value'],
+                        required=prop['value']['required'],
+                        description=prop['value'].get('description', ''),
+                        indent_num=indent_num,
+                        is_flow_type=True)
+                    for prop in type_object['signature']['properties']))),
     )
+
+
+def js_to_py_type(type_object, is_flow_type=False, indent_num=0):
+    """
+    Convert JS types to Python types for the component definition
+
+    Parameters
+    ----------
+    type_object: dict
+        react-docgen-generated prop type dictionary
+    is_flow_type: bool
+        Does the prop use Flow types? Otherwise, uses PropTypes
+    indent_num: int
+        Number of indents to use for the docstring for the prop
+
+    Returns
+    -------
+    str
+        Python type string
+    """
+    js_type_name = type_object['name']
+    js_to_py_types = map_js_to_py_types_flow_types(type_object=type_object) \
+        if is_flow_type \
+        else map_js_to_py_types_prop_types(type_object=type_object)
+
+    if 'computed' in type_object and type_object['computed'] \
+            or type_object.get('type', '') == 'function':
+        return ''
+    elif js_type_name in js_to_py_types:
+        if js_type_name == 'signature':  # This is a Flow object w/ signature
+            return js_to_py_types[js_type_name](indent_num)
+        # All other types
+        return js_to_py_types[js_type_name]()
+    return ''
diff --git a/dash/version.py b/dash/version.py
--- a/dash/version.py
+++ b/dash/version.py
@@ -1 +1 @@
-__version__ = '0.20.0'
+__version__ = '0.20.1'


Test patch:
diff --git a/tests/development/flow_metadata_test.json b/tests/development/flow_metadata_test.json
new file mode 100644
--- /dev/null
+++ b/tests/development/flow_metadata_test.json
@@ -0,0 +1,368 @@
+{
+  "description": "This is a test description of the component.\nIt's multiple lines long.",
+  "methods": [],
+  "props": {
+    "requiredString": {
+      "required": true,
+      "description": "A required string",
+      "flowType": {
+        "name": "string"
+      }
+    },
+    "optionalString": {
+      "required": false,
+      "description": "A string that isn't required.",
+      "flowType": {
+        "name": "string"
+      },
+      "defaultValue": {
+        "value": "''",
+        "computed": false
+      }
+    },
+    "optionalBoolean": {
+      "required": false,
+      "description": "A boolean test",
+      "flowType": {
+        "name": "boolean"
+      },
+      "defaultValue": {
+        "value": "false",
+        "computed": false
+      }
+    },
+    "optionalFunc": {
+      "required": false,
+      "description": "Dash callback to update props on the server",
+      "flowType": {
+        "name": "signature",
+        "type": "function",
+        "raw": "(props: { modal?: boolean, open?: boolean }) => void",
+        "signature": {
+          "arguments": [
+            {
+              "name": "props",
+              "type": {
+                "name": "signature",
+                "type": "object",
+                "raw": "{ modal?: boolean, open?: boolean }",
+                "signature": {
+                  "properties": [
+                    {
+                      "key": "modal",
+                      "value": {
+                        "name": "boolean",
+                        "required": false
+                      }
+                    },
+                    {
+                      "key": "open",
+                      "value": {
+                        "name": "boolean",
+                        "required": false
+                      }
+                    }
+                  ]
+                }
+              }
+            }
+          ],
+          "return": {
+            "name": "void"
+          }
+        }
+      },
+      "defaultValue": {
+        "value": "() => {}",
+        "computed": false
+      }
+    },
+    "optionalNode": {
+      "required": false,
+      "description": "A node test",
+      "flowType": {
+        "name": "Node"
+      },
+      "defaultValue": {
+        "value": "null",
+        "computed": false
+      }
+    },
+    "optionalArray": {
+      "required": false,
+      "description": "An array test with a particularly \nlong description that covers several lines. It includes the newline character \nand should span 3 lines in total.",
+      "flowType": {
+        "name": "Array",
+        "elements": [
+          {
+            "name": "signature",
+            "type": "object",
+            "raw": "{\n  checked?: boolean,\n  children?: Node,\n  customData: any,\n  disabled?: boolean,\n  label?: string,\n  primaryText: string,\n  secondaryText?: string,\n  style?: Object,\n  value: any,\n}",
+            "signature": {
+              "properties": [
+                {
+                  "key": "checked",
+                  "value": {
+                    "name": "boolean",
+                    "required": false
+                  }
+                },
+                {
+                  "key": "children",
+                  "value": {
+                    "name": "Node",
+                    "required": false
+                  }
+                },
+                {
+                  "key": "customData",
+                  "value": {
+                    "name": "any",
+                    "required": true
+                  }
+                },
+                {
+                  "key": "disabled",
+                  "value": {
+                    "name": "boolean",
+                    "required": false
+                  }
+                },
+                {
+                  "key": "label",
+                  "value": {
+                    "name": "string",
+                    "required": false
+                  }
+                },
+                {
+                  "key": "primaryText",
+                  "value": {
+                    "name": "string",
+                    "required": true
+                  }
+                },
+                {
+                  "key": "secondaryText",
+                  "value": {
+                    "name": "string",
+                    "required": false
+                  }
+                },
+                {
+                  "key": "style",
+                  "value": {
+                    "name": "Object",
+                    "required": false
+                  }
+                },
+                {
+                  "key": "value",
+                  "value": {
+                    "name": "any",
+                    "required": true
+                  }
+                }
+              ]
+            }
+          }
+        ],
+        "raw": "Array<SD_MENU_ITEM>"
+      },
+      "defaultValue": {
+        "value": "[]",
+        "computed": false
+      }
+    },
+    "requiredUnion": {
+      "required": true,
+      "description": "",
+      "flowType": {
+        "name": "union",
+        "raw": "string | number",
+        "elements": [
+          {
+            "name": "string"
+          },
+          {
+            "name": "number"
+          }
+        ]
+      }
+    },
+    "optionalSignature(shape)": {
+      "flowType": {
+        "name": "signature",
+        "type": "object",
+        "raw": "{\n  checked?: boolean,\n  children?: Node,\n  customData: any,\n  disabled?: boolean,\n  label?: string,\n  primaryText: string,\n  secondaryText?: string,\n  style?: Object,\n  value: any,\n}",
+        "signature": {
+          "properties": [
+            {
+              "key": "checked",
+              "value": {
+                "name": "boolean",
+                "required": false
+              }
+            },
+            {
+              "key": "children",
+              "value": {
+                "name": "Node",
+                "required": false
+              }
+            },
+            {
+              "key": "customData",
+              "value": {
+                "name": "any",
+                "required": true,
+                "description": "A test description"
+              }
+            },
+            {
+              "key": "disabled",
+              "value": {
+                "name": "boolean",
+                "required": false
+              }
+            },
+            {
+              "key": "label",
+              "value": {
+                "name": "string",
+                "required": false
+              }
+            },
+            {
+              "key": "primaryText",
+              "value": {
+                "name": "string",
+                "required": true,
+                "description": "Another test description"
+              }
+            },
+            {
+              "key": "secondaryText",
+              "value": {
+                "name": "string",
+                "required": false
+              }
+            },
+            {
+              "key": "style",
+              "value": {
+                "name": "Object",
+                "required": false
+              }
+            },
+            {
+              "key": "value",
+              "value": {
+                "name": "any",
+                "required": true
+              }
+            }
+          ]
+        }
+      },
+      "required": false,
+      "description": "This is a test of an object's shape"
+    },
+    "requiredNested": {
+      "flowType": {
+        "name": "signature",
+        "type": "object",
+        "raw": "{\n  customData: SD_MENU_ITEM,\n  value: any,\n}",
+        "signature": {
+          "properties": [
+            {
+              "key": "customData",
+              "value": {
+                "name": "signature",
+                "type": "object",
+                "raw": "{\n  checked?: boolean,\n  children?: Node,\n  customData: any,\n  disabled?: boolean,\n  label?: string,\n  primaryText: string,\n  secondaryText?: string,\n  style?: Object,\n  value: any,\n}",
+                "signature": {
+                  "properties": [
+                    {
+                      "key": "checked",
+                      "value": {
+                        "name": "boolean",
+                        "required": false
+                      }
+                    },
+                    {
+                      "key": "children",
+                      "value": {
+                        "name": "Node",
+                        "required": false
+                      }
+                    },
+                    {
+                      "key": "customData",
+                      "value": {
+                        "name": "any",
+                        "required": true
+                      }
+                    },
+                    {
+                      "key": "disabled",
+                      "value": {
+                        "name": "boolean",
+                        "required": false
+                      }
+                    },
+                    {
+                      "key": "label",
+                      "value": {
+                        "name": "string",
+                        "required": false
+                      }
+                    },
+                    {
+                      "key": "primaryText",
+                      "value": {
+                        "name": "string",
+                        "required": true
+                      }
+                    },
+                    {
+                      "key": "secondaryText",
+                      "value": {
+                        "name": "string",
+                        "required": false
+                      }
+                    },
+                    {
+                      "key": "style",
+                      "value": {
+                        "name": "Object",
+                        "required": false
+                      }
+                    },
+                    {
+                      "key": "value",
+                      "value": {
+                        "name": "any",
+                        "required": true
+                      }
+                    }
+                  ]
+                },
+                "required": true
+              }
+            },
+            {
+              "key": "value",
+              "value": {
+                "name": "any",
+                "required": true
+              }
+            }
+          ]
+        }
+      },
+      "required": true,
+      "description": ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/development/test_base_component.py b/tests/development/test_base_component.py
--- a/tests/development/test_base_component.py
+++ b/tests/development/test_base_component.py
@@ -755,3 +755,149 @@ def assert_docstring(assertEqual, docstring):
             '        '
             ])[i]
                    )
+
+
+class TestFlowMetaDataConversions(unittest.TestCase):
+    def setUp(self):
+        path = os.path.join('tests', 'development', 'flow_metadata_test.json')
+        with open(path) as data_file:
+            json_string = data_file.read()
+            data = json\
+                .JSONDecoder(object_pairs_hook=collections.OrderedDict)\
+                .decode(json_string)
+            self.data = data
+
+        self.expected_arg_strings = OrderedDict([
+            ['children', 'a list of or a singular dash component, string or number'],
+
+            ['requiredString', 'string'],
+
+            ['optionalString', 'string'],
+
+            ['optionalBoolean', 'boolean'],
+
+            ['optionalFunc', ''],
+
+            ['optionalNode', 'a list of or a singular dash component, string or number'],
+
+            ['optionalArray', 'list'],
+
+            ['requiredUnion', 'string | number'],
+
+            ['optionalSignature(shape)', '\n'.join([
+
+                "dict containing keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', 'style', 'value'.",
+                "Those keys have the following types: ",
+                "- checked (boolean; optional)",
+                "- children (a list of or a singular dash component, string or number; optional)",
+                "- customData (bool | number | str | dict | list; required): A test description",
+                "- disabled (boolean; optional)",
+                "- label (string; optional)",
+                "- primaryText (string; required): Another test description",
+                "- secondaryText (string; optional)",
+                "- style (dict; optional)",
+                "- value (bool | number | str | dict | list; required)"
+
+            ])],
+
+            ['requiredNested', '\n'.join([
+
+                "dict containing keys 'customData', 'value'.",
+                "Those keys have the following types: ",
+                "- customData (required): . customData has the following type: dict containing keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', 'style', 'value'.",
+                "  Those keys have the following types: ",
+                "  - checked (boolean; optional)",
+                "  - children (a list of or a singular dash component, string or number; optional)",
+                "  - customData (bool | number | str | dict | list; required)",
+                "  - disabled (boolean; optional)",
+                "  - label (string; optional)",
+                "  - primaryText (string; required)",
+                "  - secondaryText (string; optional)",
+                "  - style (dict; optional)",
+                "  - value (bool | number | str | dict | list; required)",
+                "- value (bool | number | str | dict | list; required)",
+
+            ])],
+        ])
+
+    def test_docstring(self):
+        docstring = create_docstring(
+            'Flow_component',
+            self.data['props'],
+            parse_events(self.data['props']),
+            self.data['description'],
+        )
+        assert_flow_docstring(self.assertEqual, docstring)
+
+    def test_docgen_to_python_args(self):
+
+        props = self.data['props']
+
+        for prop_name, prop in list(props.items()):
+            self.assertEqual(
+                js_to_py_type(prop['flowType'], is_flow_type=True),
+                self.expected_arg_strings[prop_name]
+            )
+
+
+def assert_flow_docstring(assertEqual, docstring):
+    for i, line in enumerate(docstring.split('\n')):
+        assertEqual(line, ([
+            "A Flow_component component.",
+            "This is a test description of the component.",
+            "It's multiple lines long.",
+            "",
+            "Keyword arguments:",
+            "- requiredString (string; required): A required string",
+            "- optionalString (string; optional): A string that isn't required.",
+            "- optionalBoolean (boolean; optional): A boolean test",
+
+            "- optionalNode (a list of or a singular dash component, string or number; optional): "
+            "A node test",
+
+            "- optionalArray (list; optional): An array test with a particularly ",
+            "long description that covers several lines. It includes the newline character ",
+            "and should span 3 lines in total.",
+
+            "- requiredUnion (string | number; required)",
+
+            "- optionalSignature(shape) (optional): This is a test of an object's shape. "
+            "optionalSignature(shape) has the following type: dict containing keys 'checked', "
+            "'children', 'customData', 'disabled', 'label', 'primaryText', 'secondaryText', "
+            "'style', 'value'.",
+
+            "  Those keys have the following types: ",
+            "  - checked (boolean; optional)",
+            "  - children (a list of or a singular dash component, string or number; optional)",
+            "  - customData (bool | number | str | dict | list; required): A test description",
+            "  - disabled (boolean; optional)",
+            "  - label (string; optional)",
+            "  - primaryText (string; required): Another test description",
+            "  - secondaryText (string; optional)",
+            "  - style (dict; optional)",
+            "  - value (bool | number | str | dict | list; required)",
+
+            "- requiredNested (required): . requiredNested has the following type: dict containing "
+            "keys 'customData', 'value'.",
+
+            "  Those keys have the following types: ",
+
+            "  - customData (required): . customData has the following type: dict containing "
+            "keys 'checked', 'children', 'customData', 'disabled', 'label', 'primaryText', "
+            "'secondaryText', 'style', 'value'.",
+
+            "    Those keys have the following types: ",
+            "    - checked (boolean; optional)",
+            "    - children (a list of or a singular dash component, string or number; optional)",
+            "    - customData (bool | number | str | dict | list; required)",
+            "    - disabled (boolean; optional)",
+            "    - label (string; optional)",
+            "    - primaryText (string; required)",
+            "    - secondaryText (string; optional)",
+            "    - style (dict; optional)",
+            "    - value (bool | number | str | dict | list; required)",
+            "  - value (bool | number | str | dict | list; required)",
+            "",
+            "Available events: "
+        ])[i]
+                    )