791 lines
25 KiB
Python
791 lines
25 KiB
Python
|
from collections import OrderedDict
|
||
|
import copy
|
||
|
import numbers
|
||
|
import os
|
||
|
import typing
|
||
|
from textwrap import fill, dedent
|
||
|
|
||
|
from typing_extensions import TypedDict, NotRequired, Literal
|
||
|
from dash.development.base_component import _explicitize_args
|
||
|
from dash.exceptions import NonExistentEventException
|
||
|
from ._all_keywords import python_keywords
|
||
|
from ._collect_nodes import collect_nodes, filter_base_nodes
|
||
|
from ._py_prop_typing import (
|
||
|
get_custom_ignore,
|
||
|
get_custom_props,
|
||
|
get_prop_typing,
|
||
|
shapes,
|
||
|
get_custom_imports,
|
||
|
)
|
||
|
from .base_component import Component, ComponentType
|
||
|
|
||
|
import_string = """# AUTO GENERATED FILE - DO NOT EDIT
|
||
|
|
||
|
import typing # noqa: F401
|
||
|
from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401
|
||
|
from dash.development.base_component import Component, _explicitize_args
|
||
|
{custom_imports}
|
||
|
ComponentType = typing.Union[
|
||
|
str,
|
||
|
int,
|
||
|
float,
|
||
|
Component,
|
||
|
None,
|
||
|
typing.Sequence[typing.Union[str, int, float, Component, None]],
|
||
|
]
|
||
|
|
||
|
NumberType = typing.Union[
|
||
|
typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex
|
||
|
]
|
||
|
|
||
|
|
||
|
"""
|
||
|
|
||
|
|
||
|
# pylint: disable=unused-argument,too-many-locals,too-many-branches
|
||
|
def generate_class_string(
|
||
|
typename,
|
||
|
props,
|
||
|
description,
|
||
|
namespace,
|
||
|
prop_reorder_exceptions=None,
|
||
|
max_props=None,
|
||
|
custom_typing_module=None,
|
||
|
):
|
||
|
"""Dynamically generate class strings to have nicely formatted docstrings,
|
||
|
keyword arguments, and repr.
|
||
|
Inspired by http://jameso.be/2013/08/06/namedtuple.html
|
||
|
Parameters
|
||
|
----------
|
||
|
typename
|
||
|
props
|
||
|
description
|
||
|
namespace
|
||
|
prop_reorder_exceptions
|
||
|
Returns
|
||
|
-------
|
||
|
string
|
||
|
"""
|
||
|
# TODO _prop_names, _type, _namespace, 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 hierarchical tree
|
||
|
# TODO - Include "description" "defaultValue" in the repr and docstring
|
||
|
#
|
||
|
# TODO - Handle "required"
|
||
|
#
|
||
|
# TODO - How to handle user-given `null` values? I want to include
|
||
|
# an expanded docstring like Dropdown(value=None, id=None)
|
||
|
# but by templating in those None values, I have no way of knowing
|
||
|
# whether a property is None because the user explicitly wanted
|
||
|
# it to be `null` or whether that was just the default value.
|
||
|
# The solution might be to deal with default values better although
|
||
|
# not all component authors will supply those.
|
||
|
c = '''class {typename}(Component):
|
||
|
"""{docstring}"""
|
||
|
_children_props = {children_props}
|
||
|
_base_nodes = {base_nodes}
|
||
|
_namespace = '{namespace}'
|
||
|
_type = '{typename}'
|
||
|
{shapes}
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
{default_argtext}
|
||
|
):
|
||
|
self._prop_names = {list_of_valid_keys}
|
||
|
self._valid_wildcard_attributes =\
|
||
|
{list_of_valid_wildcard_attr_prefixes}
|
||
|
self.available_properties = {list_of_valid_keys}
|
||
|
self.available_wildcard_properties =\
|
||
|
{list_of_valid_wildcard_attr_prefixes}
|
||
|
_explicit_args = kwargs.pop('_explicit_args')
|
||
|
_locals = locals()
|
||
|
_locals.update(kwargs) # For wildcard attrs and excess named props
|
||
|
args = {args}
|
||
|
{required_validation}
|
||
|
super({typename}, self).__init__({argtext})
|
||
|
|
||
|
setattr({typename}, "__init__", _explicitize_args({typename}.__init__))
|
||
|
'''
|
||
|
|
||
|
filtered_props = (
|
||
|
filter_props(props)
|
||
|
if (prop_reorder_exceptions is not None and typename in prop_reorder_exceptions)
|
||
|
or (prop_reorder_exceptions is not None and "ALL" in prop_reorder_exceptions)
|
||
|
else reorder_props(filter_props(props))
|
||
|
)
|
||
|
wildcard_prefixes = repr(parse_wildcards(props))
|
||
|
list_of_valid_keys = repr(list(map(str, filtered_props.keys())))
|
||
|
custom_ignore = get_custom_ignore(custom_typing_module)
|
||
|
docstring = create_docstring(
|
||
|
component_name=typename,
|
||
|
props=filtered_props,
|
||
|
description=description,
|
||
|
prop_reorder_exceptions=prop_reorder_exceptions,
|
||
|
ignored_props=custom_ignore,
|
||
|
).replace("\r\n", "\n")
|
||
|
required_args = required_props(filtered_props)
|
||
|
is_children_required = "children" in required_args
|
||
|
required_args = [arg for arg in required_args if arg != "children"]
|
||
|
|
||
|
prohibit_events(props)
|
||
|
|
||
|
# pylint: disable=unused-variable
|
||
|
prop_keys = list(props.keys())
|
||
|
if "children" in props and "children" in list_of_valid_keys:
|
||
|
prop_keys.remove("children")
|
||
|
# TODO For dash 3.0, remove the Optional and = None for proper typing.
|
||
|
# Also add the other required props after children.
|
||
|
default_argtext = f"children: typing.Optional[{get_prop_typing('node', '', '', {})}] = None,\n "
|
||
|
args = "{k: _locals[k] for k in _explicit_args if k != 'children'}"
|
||
|
argtext = "children=children, **args"
|
||
|
else:
|
||
|
default_argtext = ""
|
||
|
args = "{k: _locals[k] for k in _explicit_args}"
|
||
|
argtext = "**args"
|
||
|
|
||
|
if len(required_args) == 0:
|
||
|
required_validation = ""
|
||
|
else:
|
||
|
required_validation = f"""
|
||
|
for k in {required_args}:
|
||
|
if k not in args:
|
||
|
raise TypeError(
|
||
|
'Required argument `' + k + '` was not specified.')
|
||
|
"""
|
||
|
|
||
|
if is_children_required:
|
||
|
required_validation += """
|
||
|
if 'children' not in _explicit_args:
|
||
|
raise TypeError('Required argument children was not specified.')
|
||
|
"""
|
||
|
|
||
|
default_arglist = []
|
||
|
|
||
|
for prop_key in prop_keys:
|
||
|
prop = props[prop_key]
|
||
|
if (
|
||
|
prop_key.endswith("-*")
|
||
|
or prop_key in python_keywords
|
||
|
or prop_key == "setProps"
|
||
|
):
|
||
|
continue
|
||
|
|
||
|
type_info = prop.get("type")
|
||
|
|
||
|
if not type_info:
|
||
|
print(f"Invalid prop type for typing: {prop_key}")
|
||
|
default_arglist.append(f"{prop_key} = None")
|
||
|
continue
|
||
|
|
||
|
type_name = type_info.get("name")
|
||
|
|
||
|
custom_props = get_custom_props(custom_typing_module)
|
||
|
typed = get_prop_typing(
|
||
|
type_name,
|
||
|
typename,
|
||
|
prop_key,
|
||
|
type_info,
|
||
|
custom_props=custom_props,
|
||
|
custom_ignore=custom_ignore,
|
||
|
)
|
||
|
|
||
|
arg_value = f"{prop_key}: typing.Optional[{typed}] = None"
|
||
|
|
||
|
default_arglist.append(arg_value)
|
||
|
|
||
|
if max_props:
|
||
|
final_max_props = max_props - (1 if "children" in props else 0)
|
||
|
if len(default_arglist) > final_max_props:
|
||
|
default_arglist = default_arglist[:final_max_props]
|
||
|
docstring += (
|
||
|
"\n\n"
|
||
|
"Note: due to the large number of props for this component,\n"
|
||
|
"not all of them appear in the constructor signature, but\n"
|
||
|
"they may still be used as keyword arguments."
|
||
|
)
|
||
|
|
||
|
default_argtext += ",\n ".join(default_arglist + ["**kwargs"])
|
||
|
nodes = collect_nodes({k: v for k, v in props.items() if k != "children"})
|
||
|
|
||
|
return dedent(
|
||
|
c.format(
|
||
|
typename=typename,
|
||
|
namespace=namespace,
|
||
|
filtered_props=filtered_props,
|
||
|
list_of_valid_wildcard_attr_prefixes=wildcard_prefixes,
|
||
|
list_of_valid_keys=list_of_valid_keys,
|
||
|
docstring=docstring,
|
||
|
default_argtext=default_argtext,
|
||
|
args=args,
|
||
|
argtext=argtext,
|
||
|
required_validation=required_validation,
|
||
|
children_props=nodes,
|
||
|
base_nodes=filter_base_nodes(nodes) + ["children"],
|
||
|
shapes="\n".join(shapes.get(typename, {}).values()),
|
||
|
)
|
||
|
)
|
||
|
|
||
|
|
||
|
def generate_class_file(
|
||
|
typename,
|
||
|
props,
|
||
|
description,
|
||
|
namespace,
|
||
|
prop_reorder_exceptions=None,
|
||
|
max_props=None,
|
||
|
custom_typing_module="dash_prop_typing",
|
||
|
):
|
||
|
"""Generate a Python class file (.py) given a class string.
|
||
|
Parameters
|
||
|
----------
|
||
|
typename
|
||
|
props
|
||
|
description
|
||
|
namespace
|
||
|
prop_reorder_exceptions
|
||
|
Returns
|
||
|
-------
|
||
|
"""
|
||
|
|
||
|
class_string = generate_class_string(
|
||
|
typename,
|
||
|
props,
|
||
|
description,
|
||
|
namespace,
|
||
|
prop_reorder_exceptions,
|
||
|
max_props,
|
||
|
custom_typing_module,
|
||
|
)
|
||
|
|
||
|
custom_imp = get_custom_imports(custom_typing_module)
|
||
|
custom_imp = custom_imp.get(typename) or custom_imp.get("*")
|
||
|
|
||
|
if custom_imp:
|
||
|
imports = import_string.format(
|
||
|
custom_imports="\n" + "\n".join(custom_imp) + "\n\n"
|
||
|
)
|
||
|
else:
|
||
|
imports = import_string.format(custom_imports="")
|
||
|
|
||
|
file_name = f"{typename:s}.py"
|
||
|
|
||
|
file_path = os.path.join(namespace, file_name)
|
||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||
|
f.write(imports)
|
||
|
f.write(class_string)
|
||
|
|
||
|
print(f"Generated {file_name}")
|
||
|
|
||
|
|
||
|
def generate_imports(project_shortname, components):
|
||
|
with open(
|
||
|
os.path.join(project_shortname, "_imports_.py"), "w", encoding="utf-8"
|
||
|
) as f:
|
||
|
component_imports = "\n".join(f"from .{x} import {x}" for x in components)
|
||
|
all_list = ",\n".join(f' "{x}"' for x in components)
|
||
|
imports_string = f"{component_imports}\n\n__all__ = [\n{all_list}\n]"
|
||
|
|
||
|
f.write(imports_string)
|
||
|
|
||
|
|
||
|
def generate_classes_files(project_shortname, metadata, *component_generators):
|
||
|
components = []
|
||
|
for component_path, component_data in metadata.items():
|
||
|
component_name = component_path.split("/")[-1].split(".")[0]
|
||
|
components.append(component_name)
|
||
|
|
||
|
for generator in component_generators:
|
||
|
generator(
|
||
|
component_name,
|
||
|
component_data["props"],
|
||
|
component_data["description"],
|
||
|
project_shortname,
|
||
|
)
|
||
|
|
||
|
return components
|
||
|
|
||
|
|
||
|
def generate_class(
|
||
|
typename, props, description, namespace, prop_reorder_exceptions=None
|
||
|
):
|
||
|
"""Generate a Python class object given a class string.
|
||
|
Parameters
|
||
|
----------
|
||
|
typename
|
||
|
props
|
||
|
description
|
||
|
namespace
|
||
|
Returns
|
||
|
-------
|
||
|
"""
|
||
|
string = generate_class_string(
|
||
|
typename, props, description, namespace, prop_reorder_exceptions
|
||
|
)
|
||
|
scope = {
|
||
|
"Component": Component,
|
||
|
"ComponentType": ComponentType,
|
||
|
"_explicitize_args": _explicitize_args,
|
||
|
"typing": typing,
|
||
|
"numbers": numbers,
|
||
|
"TypedDict": TypedDict,
|
||
|
"NotRequired": NotRequired,
|
||
|
"Literal": Literal,
|
||
|
"NumberType": typing.Union[
|
||
|
typing.SupportsFloat, typing.SupportsComplex, typing.SupportsInt
|
||
|
],
|
||
|
}
|
||
|
# pylint: disable=exec-used
|
||
|
exec(string, scope)
|
||
|
result = scope[typename]
|
||
|
return result
|
||
|
|
||
|
|
||
|
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 create_docstring(
|
||
|
component_name,
|
||
|
props,
|
||
|
description,
|
||
|
prop_reorder_exceptions=None,
|
||
|
ignored_props=tuple(),
|
||
|
):
|
||
|
"""Create the Dash component docstring.
|
||
|
Parameters
|
||
|
----------
|
||
|
component_name: str
|
||
|
Component name
|
||
|
props: dict
|
||
|
Dictionary with {propName: propMetadata} structure
|
||
|
description: str
|
||
|
Component description
|
||
|
Returns
|
||
|
-------
|
||
|
str
|
||
|
Dash component docstring
|
||
|
"""
|
||
|
# Ensure props are ordered with children first
|
||
|
props = (
|
||
|
props
|
||
|
if (
|
||
|
prop_reorder_exceptions is not None
|
||
|
and component_name in prop_reorder_exceptions
|
||
|
)
|
||
|
or (prop_reorder_exceptions is not None and "ALL" in prop_reorder_exceptions)
|
||
|
else reorder_props(props)
|
||
|
)
|
||
|
|
||
|
n = "n" if component_name[0].lower() in "aeiou" else ""
|
||
|
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"],
|
||
|
default=prop.get("defaultValue"),
|
||
|
indent_num=0,
|
||
|
is_flow_type="flowType" in prop and "type" not in prop,
|
||
|
)
|
||
|
for p, prop in filter_props(props, ignored_props).items()
|
||
|
)
|
||
|
|
||
|
return (
|
||
|
f"A{n} {component_name} component.\n{description}\n\nKeyword arguments:\n{args}"
|
||
|
)
|
||
|
|
||
|
|
||
|
def prohibit_events(props):
|
||
|
"""Events have been removed. Raise an error if we see dashEvents or
|
||
|
fireEvents.
|
||
|
Parameters
|
||
|
----------
|
||
|
props: dict
|
||
|
Dictionary with {propName: propMetadata} structure
|
||
|
Raises
|
||
|
-------
|
||
|
?
|
||
|
"""
|
||
|
if "dashEvents" in props or "fireEvents" in props:
|
||
|
raise NonExistentEventException(
|
||
|
"Events are no longer supported by dash. Use properties instead, "
|
||
|
"eg `n_clicks` instead of a `click` event."
|
||
|
)
|
||
|
|
||
|
|
||
|
def parse_wildcards(props):
|
||
|
"""Pull out the wildcard attributes from the Component props.
|
||
|
Parameters
|
||
|
----------
|
||
|
props: dict
|
||
|
Dictionary with {propName: propMetadata} structure
|
||
|
Returns
|
||
|
-------
|
||
|
list
|
||
|
List of Dash valid wildcard prefixes
|
||
|
"""
|
||
|
list_of_valid_wildcard_attr_prefixes = []
|
||
|
for wildcard_attr in ["data-*", "aria-*"]:
|
||
|
if wildcard_attr in props:
|
||
|
list_of_valid_wildcard_attr_prefixes.append(wildcard_attr[:-1])
|
||
|
return list_of_valid_wildcard_attr_prefixes
|
||
|
|
||
|
|
||
|
def reorder_props(props):
|
||
|
"""If "children" is in props, then move it to the front to respect dash
|
||
|
convention, then 'id', then the remaining props sorted by prop name
|
||
|
Parameters
|
||
|
----------
|
||
|
props: dict
|
||
|
Dictionary with {propName: propMetadata} structure
|
||
|
Returns
|
||
|
-------
|
||
|
dict
|
||
|
Dictionary with {propName: propMetadata} structure
|
||
|
"""
|
||
|
|
||
|
# Constructing an OrderedDict with duplicate keys, you get the order
|
||
|
# from the first one but the value from the last.
|
||
|
# Doing this to avoid mutating props, which can cause confusion.
|
||
|
props1 = [("children", "")] if "children" in props else []
|
||
|
props2 = [("id", "")] if "id" in props else []
|
||
|
return OrderedDict(props1 + props2 + sorted(list(props.items())))
|
||
|
|
||
|
|
||
|
def filter_props(props, ignored_props=tuple()):
|
||
|
"""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'}
|
||
|
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 arg_name in ignored_props or ("type" not in arg and "flowType" not in arg):
|
||
|
filtered_props.pop(arg_name)
|
||
|
continue
|
||
|
|
||
|
# 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
|
||
|
|
||
|
return filtered_props
|
||
|
|
||
|
|
||
|
def fix_keywords(txt):
|
||
|
"""
|
||
|
replaces javascript keywords true, false, null with Python keywords
|
||
|
"""
|
||
|
fix_word = {"true": "True", "false": "False", "null": "None"}
|
||
|
for js_keyword, python_keyword in fix_word.items():
|
||
|
txt = txt.replace(js_keyword, python_keyword)
|
||
|
return txt
|
||
|
|
||
|
|
||
|
# pylint: disable=too-many-arguments
|
||
|
# pylint: disable=too-many-locals
|
||
|
def create_prop_docstring(
|
||
|
prop_name,
|
||
|
type_object,
|
||
|
required,
|
||
|
description,
|
||
|
default,
|
||
|
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
|
||
|
default: dict
|
||
|
Either None if a default value is not defined, or
|
||
|
dict containing the key 'value' that defines a
|
||
|
default value for the prop
|
||
|
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
|
||
|
)
|
||
|
indent_spacing = " " * indent_num
|
||
|
|
||
|
default = default["value"] if default else ""
|
||
|
default = fix_keywords(default)
|
||
|
|
||
|
is_required = "optional"
|
||
|
if required:
|
||
|
is_required = "required"
|
||
|
elif default and default not in ["None", "{}", "[]"]:
|
||
|
is_required = "default " + default.replace("\n", "")
|
||
|
|
||
|
# formats description
|
||
|
period = "." if description else ""
|
||
|
description = description.strip().strip(".").replace('"', r"\"") + period
|
||
|
desc_indent = indent_spacing + " "
|
||
|
description = fill(
|
||
|
description,
|
||
|
initial_indent=desc_indent,
|
||
|
subsequent_indent=desc_indent,
|
||
|
break_long_words=False,
|
||
|
break_on_hyphens=False,
|
||
|
)
|
||
|
description = f"\n{description}" if description else ""
|
||
|
colon = ":" if description else ""
|
||
|
description = fix_keywords(description)
|
||
|
|
||
|
if "\n" in py_type_name:
|
||
|
# corrects the type
|
||
|
dict_or_list = "list of dicts" if py_type_name.startswith("list") else "dict"
|
||
|
|
||
|
# format and rewrite the intro to the nested dicts
|
||
|
intro1, intro2, dict_descr = py_type_name.partition("with keys:")
|
||
|
intro = f"`{prop_name}` is a {intro1}{intro2}"
|
||
|
intro = fill(
|
||
|
intro,
|
||
|
initial_indent=desc_indent,
|
||
|
subsequent_indent=desc_indent,
|
||
|
break_long_words=False,
|
||
|
break_on_hyphens=False,
|
||
|
)
|
||
|
|
||
|
# captures optional nested dict description and puts the "or" condition on a new line
|
||
|
if "| dict with keys:" in dict_descr:
|
||
|
dict_part1, dict_part2 = dict_descr.split(" |", 1)
|
||
|
dict_part2 = "".join([desc_indent, "Or", dict_part2])
|
||
|
dict_descr = f"{dict_part1}\n\n {dict_part2}"
|
||
|
|
||
|
# ensures indent is correct if there is a second nested list of dicts
|
||
|
current_indent = dict_descr.lstrip("\n").find("-")
|
||
|
if current_indent == len(indent_spacing):
|
||
|
dict_descr = "".join(
|
||
|
"\n\n " + line for line in dict_descr.splitlines() if line != ""
|
||
|
)
|
||
|
|
||
|
return (
|
||
|
f"\n{indent_spacing}- {prop_name} ({dict_or_list}; {is_required}){colon}"
|
||
|
f"{description}"
|
||
|
f"\n\n{intro}{dict_descr}"
|
||
|
)
|
||
|
tn = f"{py_type_name}; " if py_type_name else ""
|
||
|
return f"\n{indent_spacing}- {prop_name} ({tn}{is_required}){colon}{description}"
|
||
|
|
||
|
|
||
|
def map_js_to_py_types_prop_types(type_object, indent_num):
|
||
|
"""Mapping from the PropTypes js type object to the Python type."""
|
||
|
|
||
|
def shape_or_exact():
|
||
|
return "dict with keys:\n" + "\n".join(
|
||
|
create_prop_docstring(
|
||
|
prop_name=prop_name,
|
||
|
type_object=prop,
|
||
|
required=prop["required"],
|
||
|
description=prop.get("description", ""),
|
||
|
default=prop.get("defaultValue"),
|
||
|
indent_num=indent_num + 2,
|
||
|
)
|
||
|
for prop_name, prop in type_object["value"].items()
|
||
|
)
|
||
|
|
||
|
def array_of():
|
||
|
inner = js_to_py_type(type_object["value"])
|
||
|
if inner:
|
||
|
return "list of " + (
|
||
|
inner + "s"
|
||
|
if inner.split(" ")[0] != "dict"
|
||
|
else inner.replace("dict", "dicts", 1)
|
||
|
)
|
||
|
return "list"
|
||
|
|
||
|
def tuple_of():
|
||
|
elements = [js_to_py_type(element) for element in type_object["elements"]]
|
||
|
return f"list of {len(elements)} elements: [{', '.join(elements)}]"
|
||
|
|
||
|
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: "
|
||
|
+ ", ".join(str(t["value"]) for t in type_object["value"])
|
||
|
),
|
||
|
# React's PropTypes.oneOfType
|
||
|
union=lambda: " | ".join(
|
||
|
js_to_py_type(subType)
|
||
|
for subType in type_object["value"]
|
||
|
if js_to_py_type(subType) != ""
|
||
|
),
|
||
|
# React's PropTypes.arrayOf
|
||
|
arrayOf=array_of,
|
||
|
# React's PropTypes.objectOf
|
||
|
objectOf=lambda: (
|
||
|
"dict with strings as keys and values of type "
|
||
|
+ js_to_py_type(type_object["value"])
|
||
|
),
|
||
|
# React's PropTypes.shape
|
||
|
shape=shape_or_exact,
|
||
|
# React's PropTypes.exact
|
||
|
exact=shape_or_exact,
|
||
|
tuple=tuple_of,
|
||
|
)
|
||
|
|
||
|
|
||
|
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",
|
||
|
# React's PropTypes.oneOfType
|
||
|
union=lambda: " | ".join(
|
||
|
js_to_py_type(subType)
|
||
|
for subType in type_object["elements"]
|
||
|
if js_to_py_type(subType) != ""
|
||
|
),
|
||
|
# Flow's Array type
|
||
|
Array=lambda: "list"
|
||
|
+ (
|
||
|
f' of {js_to_py_type(type_object["elements"][0])}s'
|
||
|
if js_to_py_type(type_object["elements"][0]) != ""
|
||
|
else ""
|
||
|
),
|
||
|
# React's PropTypes.shape
|
||
|
signature=lambda indent_num: (
|
||
|
"dict with keys:\n"
|
||
|
+ "\n".join(
|
||
|
create_prop_docstring(
|
||
|
prop_name=prop["key"],
|
||
|
type_object=prop["value"],
|
||
|
required=prop["value"]["required"],
|
||
|
description=prop["value"].get("description", ""),
|
||
|
default=prop.get("defaultValue"),
|
||
|
indent_num=indent_num + 2,
|
||
|
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, indent_num=indent_num
|
||
|
)
|
||
|
)
|
||
|
|
||
|
if (
|
||
|
"computed" in type_object
|
||
|
and type_object["computed"]
|
||
|
or type_object.get("type", "") == "function"
|
||
|
):
|
||
|
return ""
|
||
|
if 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) # type: ignore[reportCallIssue]
|
||
|
# All other types
|
||
|
return js_to_py_types[js_type_name]() # type: ignore[reportCallIssue]
|
||
|
return ""
|