Skip to content

IDE support #276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 1, 2018
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 133 additions & 13 deletions dash/development/base_component.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import collections
import copy
import os
import inspect


def is_number(s):
Expand All @@ -18,7 +20,56 @@ def _check_if_has_indexable_children(item):
raise KeyError


def _explicitize_args(func):
# Python 2
if hasattr(func, 'func_code'):
varnames = func.func_code.co_varnames
# Python 3
else:
varnames = func.__code__.co_varnames

def wrapper(*args, **kwargs):
if '_explicit_args' in kwargs.keys():
raise Exception('Variable _explicit_args should not be set.')
kwargs['_explicit_args'] = \
list(
set(
list(varnames[:len(args)]) + [k for k, _ in kwargs.items()]
)
)
if 'self' in kwargs['_explicit_args']:
kwargs['_explicit_args'].remove('self')
return func(*args, **kwargs)

# If Python 3, we can set the function signature to be correct
if hasattr(inspect, 'signature'):
# pylint: disable=no-member
new_sig = inspect.signature(wrapper).replace(
parameters=inspect.signature(func).parameters.values()
)
wrapper.__signature__ = new_sig
return wrapper


class Component(collections.MutableMapping):
class _UNDEFINED(object):
def __repr__(self):
return 'undefined'

def __str__(self):
return 'undefined'

UNDEFINED = _UNDEFINED()

class _REQUIRED(object):
def __repr__(self):
return 'required'

def __str__(self):
return 'required'

REQUIRED = _REQUIRED()

def __init__(self, **kwargs):
# pylint: disable=super-init-not-called
for k, v in list(kwargs.items()):
Expand Down Expand Up @@ -200,9 +251,9 @@ def __len__(self):


# pylint: disable=unused-argument
def generate_class(typename, props, description, namespace):
def generate_class_string(typename, props, description, namespace):
"""
Dynamically generate classes to have nicely formatted docstrings,
Dynamically generate class strings to have nicely formatted docstrings,
keyword arguments, and repr

Inspired by http://jameso.be/2013/08/06/namedtuple.html
Expand All @@ -216,6 +267,7 @@ def generate_class(typename, props, description, namespace):

Returns
-------
string

"""
# TODO _prop_names, _type, _namespace, available_events,
Expand All @@ -236,7 +288,8 @@ def generate_class(typename, props, description, namespace):
# not all component authors will supply those.
c = '''class {typename}(Component):
"""{docstring}"""
def __init__(self, {default_argtext}):
@_explicitize_args
def __init__(self, {default_argtext}, **kwargs):
self._prop_names = {list_of_valid_keys}
self._type = '{typename}'
self._namespace = '{namespace}'
Expand All @@ -247,11 +300,15 @@ def __init__(self, {default_argtext}):
self.available_wildcard_properties =\
{list_of_valid_wildcard_attr_prefixes}

_explicit_args = kwargs.pop('_explicit_args')
_locals = locals()
_locals.update(kwargs) # For wildcard attrs
args = {{k: _locals[k] for k in _explicit_args if k != 'children'}}

for k in {required_args}:
if k not in kwargs:
if k not in args:
raise TypeError(
'Required argument `' + k + '` was not specified.')

super({typename}, self).__init__({argtext})

def __repr__(self):
Expand All @@ -276,13 +333,13 @@ def __repr__(self):
return (
'{typename}(' +
repr(getattr(self, self._prop_names[0], None)) + ')')
'''
'''

filtered_props = reorder_props(filter_props(props))
# pylint: disable=unused-variable
list_of_valid_wildcard_attr_prefixes = repr(parse_wildcards(props))
# pylint: disable=unused-variable
list_of_valid_keys = repr(list(filtered_props.keys()))
list_of_valid_keys = repr(list(map(str, filtered_props.keys())))
# pylint: disable=unused-variable
docstring = create_docstring(
component_name=typename,
Expand All @@ -292,19 +349,82 @@ def __repr__(self):

# pylint: disable=unused-variable
events = '[' + ', '.join(parse_events(props)) + ']'
prop_keys = list(props.keys())
if 'children' in props:
default_argtext = 'children=None, **kwargs'
prop_keys.remove('children')
default_argtext = "children=None, "
# pylint: disable=unused-variable
argtext = 'children=children, **kwargs'
argtext = 'children=children, **args'
else:
default_argtext = '**kwargs'
argtext = '**kwargs'
default_argtext = ""
argtext = '**args'
default_argtext += ", ".join(
[('{:s}=Component.REQUIRED'.format(p)
if props[p]['required'] else
'{:s}=Component.UNDEFINED'.format(p))
for p in prop_keys
if not p.endswith("-*") and
p not in ['dashEvents', 'fireEvent', 'setProps']]
)

required_args = required_props(props)
return c.format(**locals())


# pylint: disable=unused-argument
def generate_class_file(typename, props, description, namespace):
"""
Generate a python class file (.py) given a class string

scope = {'Component': Component}
Parameters
----------
typename
props
description
namespace

Returns
-------

"""
import_string =\
"# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \
"from dash.development.base_component import " + \
"Component, _explicitize_args\n\n\n"
class_string = generate_class_string(
typename,
props,
description,
namespace
)
file_name = "{:s}.py".format(typename)

file_path = os.path.join(namespace, file_name)
with open(file_path, 'w') as f:
f.write(import_string)
f.write(class_string)


# pylint: disable=unused-argument
def generate_class(typename, props, description, namespace):
"""
Generate a python class object given a class string

Parameters
----------
typename
props
description
namespace

Returns
-------

"""
string = generate_class_string(typename, props, description, namespace)
scope = {'Component': Component, '_explicitize_args': _explicitize_args}
# pylint: disable=exec-used
exec(c.format(**locals()), scope)
exec(string, scope)
result = scope[typename]
return result

Expand Down
72 changes: 66 additions & 6 deletions dash/development/component_loader.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import collections
import json
import os
from .base_component import generate_class
from .base_component import generate_class_file


def _get_metadata(metadata_path):
# Start processing
with open(metadata_path) as data_file:
json_string = data_file.read()
data = json\
.JSONDecoder(object_pairs_hook=collections.OrderedDict)\
.decode(json_string)
return data


def load_components(metadata_path,
Expand All @@ -20,12 +32,7 @@ def load_components(metadata_path,

components = []

# Start processing
with open(metadata_path) as data_file:
json_string = data_file.read()
data = json\
.JSONDecoder(object_pairs_hook=collections.OrderedDict)\
.decode(json_string)
data = _get_metadata(metadata_path)

# Iterate over each property name (which is a path to the component)
for componentPath in data:
Expand All @@ -47,3 +54,56 @@ def load_components(metadata_path,
components.append(component)

return components


def generate_classes(namespace, metadata_path='lib/metadata.json'):
"""Load React component metadata into a format Dash can parse,
then create python class files.

Usage: generate_classes()

Keyword arguments:
namespace -- name of the generated python package (also output dir)

metadata_path -- a path to a JSON file created by
[`react-docgen`](https://github.com/reactjs/react-docgen).

Returns:
"""

data = _get_metadata(metadata_path)
imports_path = os.path.join(namespace, '_imports_.py')

# Make sure the file doesn't exist, as we use append write
if os.path.exists(imports_path):
os.remove(imports_path)

# Iterate over each property name (which is a path to the component)
for componentPath in data:
componentData = data[componentPath]

# Extract component name from path
# e.g. src/components/MyControl.react.js
# TODO Make more robust - some folks will write .jsx and others
# will be on windows. Unfortunately react-docgen doesn't include
# the name of the component atm.
name = componentPath.split('/').pop().split('.')[0]
generate_class_file(
name,
componentData['props'],
componentData['description'],
namespace
)

# Add an import statement for this component
with open(imports_path, 'a') as f:
f.write('from .{0:s} import {0:s}\n'.format(name))

# Add the __all__ value so we can import * from _imports_
all_imports = [p.split('/').pop().split('.')[0] for p in data]
with open(imports_path, 'a') as f:
array_string = '[\n'
for a in all_imports:
array_string += ' "{:s}",\n'.format(a)
array_string += ']\n'
f.write('\n\n__all__ = {:s}'.format(array_string))
2 changes: 1 addition & 1 deletion dash/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.21.1'
__version__ = '0.22.0'
82 changes: 82 additions & 0 deletions tests/development/metadata_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# AUTO GENERATED FILE - DO NOT EDIT

from dash.development.base_component import Component, _explicitize_args


class Table(Component):
"""A Table component.
This is a description of the component.
It's multiple lines long.

Keyword arguments:
- children (a list of or a singular dash component, string or number; optional)
- optionalArray (list; optional): Description of optionalArray
- optionalBool (boolean; optional)
- optionalNumber (number; optional)
- optionalObject (dict; optional)
- optionalString (string; optional)
- optionalNode (a list of or a singular dash component, string or number; optional)
- optionalElement (dash component; optional)
- optionalEnum (a value equal to: 'News', 'Photos'; optional)
- optionalUnion (string | number; optional)
- optionalArrayOf (list; optional)
- optionalObjectOf (dict with strings as keys and values of type number; optional)
- optionalObjectWithShapeAndNestedDescription (optional): . optionalObjectWithShapeAndNestedDescription has the following type: dict containing keys 'color', 'fontSize', 'figure'.
Those keys have the following types:
- color (string; optional)
- fontSize (number; optional)
- figure (optional): Figure is a plotly graph object. figure has the following type: dict containing keys 'data', 'layout'.
Those keys have the following types:
- data (list; optional): data is a collection of traces
- layout (dict; optional): layout describes the rest of the figure
- optionalAny (boolean | number | string | dict | list; optional)
- customProp (optional)
- customArrayProp (list; optional)
- data-* (string; optional)
- aria-* (string; optional)
- id (string; optional)

Available events: 'restyle', 'relayout', 'click'"""
@_explicitize_args
def __init__(self, children=None, optionalArray=Component.UNDEFINED, optionalBool=Component.UNDEFINED, optionalFunc=Component.UNDEFINED, optionalNumber=Component.UNDEFINED, optionalObject=Component.UNDEFINED, optionalString=Component.UNDEFINED, optionalSymbol=Component.UNDEFINED, optionalNode=Component.UNDEFINED, optionalElement=Component.UNDEFINED, optionalMessage=Component.UNDEFINED, optionalEnum=Component.UNDEFINED, optionalUnion=Component.UNDEFINED, optionalArrayOf=Component.UNDEFINED, optionalObjectOf=Component.UNDEFINED, optionalObjectWithShapeAndNestedDescription=Component.UNDEFINED, optionalAny=Component.UNDEFINED, customProp=Component.UNDEFINED, customArrayProp=Component.UNDEFINED, id=Component.UNDEFINED, **kwargs):
self._prop_names = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'id']
self._type = 'Table'
self._namespace = 'TableComponents'
self._valid_wildcard_attributes = ['data-', 'aria-']
self.available_events = ['restyle', 'relayout', 'click']
self.available_properties = ['children', 'optionalArray', 'optionalBool', 'optionalNumber', 'optionalObject', 'optionalString', 'optionalNode', 'optionalElement', 'optionalEnum', 'optionalUnion', 'optionalArrayOf', 'optionalObjectOf', 'optionalObjectWithShapeAndNestedDescription', 'optionalAny', 'customProp', 'customArrayProp', 'data-*', 'aria-*', 'id']
self.available_wildcard_properties = ['data-', 'aria-']

_explicit_args = kwargs.pop('_explicit_args')
_locals = locals()
_locals.update(kwargs) # For wildcard attrs
args = {k: _locals[k] for k in _explicit_args if k != 'children'}

for k in []:
if k not in args:
raise TypeError(
'Required argument `' + k + '` was not specified.')
super(Table, self).__init__(children=children, **args)

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])
or any(getattr(self, c, None) is not None
for c in self.__dict__.keys()
if any(c.startswith(wc_attr)
for wc_attr in self._valid_wildcard_attributes))):
props_string = ', '.join([c+'='+repr(getattr(self, c, None))
for c in self._prop_names
if getattr(self, c, None) is not None])
wilds_string = ', '.join([c+'='+repr(getattr(self, c, None))
for c in self.__dict__.keys()
if any([c.startswith(wc_attr)
for wc_attr in
self._valid_wildcard_attributes])])
return ('Table(' + props_string +
(', ' + wilds_string if wilds_string != '' else '') + ')')
else:
return (
'Table(' +
repr(getattr(self, self._prop_names[0], None)) + ')')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you see room for improvement in making these classes even more minimal? I don't think we have to do it in this PR, but in the future it might be nice if these classes were able to inherit more of this logic, mainly because it looks pretty gnarly 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a lot of it could be moved into the base component (pretty much everything that is not an attribute assignment, and even some of those appear to be the same). I'll make a new issue for this

Loading