diff --git a/CHANGELOG.md b/CHANGELOG.md index e894c3a9ae..c50a965c6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.20.1 - 2018-01-30 +## Added +- `component_loader` now has the following behavior to create docstrings +as determined in discussion in [#187](https://github.com/plotly/dash/issues/187): + 1. If a Dash component has `PropTypes`-generated typing, the docstring uses the `PropTypes`, _regardless of whether the component also has Flow types (current behavior)._ + 2. Otherwise if a Dash component has Flow types but _not `PropTypes`_, the docstring now uses the objects generated by `react-docgen` from the Flow types. + ## 0.20.0 - 2018-01-19 ## Added - `exceptions.PreventUpdate` can be raised inside a callback to elegantly prevent diff --git a/dash/development/base_component.py b/dash/development/base_component.py index aee73fdf4d..2bedbab8b3 100644 --- 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 index 2f15b8cd37..abadaefae0 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = '0.20.0' +__version__ = '0.20.1' diff --git a/tests/development/flow_metadata_test.json b/tests/development/flow_metadata_test.json new file mode 100644 index 0000000000..bc934456e0 --- /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" + }, + "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 index 9662fa9978..73e459fde6 100644 --- 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] + )