Skip to content

Commit 8a9820c

Browse files
authored
Merge pull request #276 from rmarren1/ide
IDE support
2 parents a7005b2 + 5858699 commit 8a9820c

File tree

7 files changed

+463
-26
lines changed

7 files changed

+463
-26
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.23.0 - 2018-08-01
2+
## Added
3+
- Dash components are now generated at build-time and then imported rather than generated when a module is imported. This should reduce the time it takes to import Dash component libraries, and makes Dash compatible with IDEs.
4+
15
## 0.22.1 - 2018-08-01
26
## Fixed
37
- Raise a more informative error if a non JSON serializable value is returned from a callback [#273](https://github.com/plotly/dash/pull/273)
@@ -7,6 +11,7 @@
711
- Assets files & index customization [#286](https://github.com/plotly/dash/pull/286)
812
- Raise an error if there is no layout present when the server is running [#294](https://github.com/plotly/dash/pull/294)
913

14+
1015
## 0.21.1 - 2018-04-10
1116
## Added
1217
- `aria-*` and `data-*` attributes are now supported in all dash html components. (#40)

dash/development/base_component.py

+133-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import collections
22
import copy
3+
import os
4+
import inspect
35

46

57
def is_number(s):
@@ -18,7 +20,56 @@ def _check_if_has_indexable_children(item):
1820
raise KeyError
1921

2022

23+
def _explicitize_args(func):
24+
# Python 2
25+
if hasattr(func, 'func_code'):
26+
varnames = func.func_code.co_varnames
27+
# Python 3
28+
else:
29+
varnames = func.__code__.co_varnames
30+
31+
def wrapper(*args, **kwargs):
32+
if '_explicit_args' in kwargs.keys():
33+
raise Exception('Variable _explicit_args should not be set.')
34+
kwargs['_explicit_args'] = \
35+
list(
36+
set(
37+
list(varnames[:len(args)]) + [k for k, _ in kwargs.items()]
38+
)
39+
)
40+
if 'self' in kwargs['_explicit_args']:
41+
kwargs['_explicit_args'].remove('self')
42+
return func(*args, **kwargs)
43+
44+
# If Python 3, we can set the function signature to be correct
45+
if hasattr(inspect, 'signature'):
46+
# pylint: disable=no-member
47+
new_sig = inspect.signature(wrapper).replace(
48+
parameters=inspect.signature(func).parameters.values()
49+
)
50+
wrapper.__signature__ = new_sig
51+
return wrapper
52+
53+
2154
class Component(collections.MutableMapping):
55+
class _UNDEFINED(object):
56+
def __repr__(self):
57+
return 'undefined'
58+
59+
def __str__(self):
60+
return 'undefined'
61+
62+
UNDEFINED = _UNDEFINED()
63+
64+
class _REQUIRED(object):
65+
def __repr__(self):
66+
return 'required'
67+
68+
def __str__(self):
69+
return 'required'
70+
71+
REQUIRED = _REQUIRED()
72+
2273
def __init__(self, **kwargs):
2374
# pylint: disable=super-init-not-called
2475
for k, v in list(kwargs.items()):
@@ -214,9 +265,9 @@ def __len__(self):
214265

215266

216267
# pylint: disable=unused-argument
217-
def generate_class(typename, props, description, namespace):
268+
def generate_class_string(typename, props, description, namespace):
218269
"""
219-
Dynamically generate classes to have nicely formatted docstrings,
270+
Dynamically generate class strings to have nicely formatted docstrings,
220271
keyword arguments, and repr
221272
222273
Inspired by http://jameso.be/2013/08/06/namedtuple.html
@@ -230,6 +281,7 @@ def generate_class(typename, props, description, namespace):
230281
231282
Returns
232283
-------
284+
string
233285
234286
"""
235287
# TODO _prop_names, _type, _namespace, available_events,
@@ -250,7 +302,8 @@ def generate_class(typename, props, description, namespace):
250302
# not all component authors will supply those.
251303
c = '''class {typename}(Component):
252304
"""{docstring}"""
253-
def __init__(self, {default_argtext}):
305+
@_explicitize_args
306+
def __init__(self, {default_argtext}, **kwargs):
254307
self._prop_names = {list_of_valid_keys}
255308
self._type = '{typename}'
256309
self._namespace = '{namespace}'
@@ -261,11 +314,15 @@ def __init__(self, {default_argtext}):
261314
self.available_wildcard_properties =\
262315
{list_of_valid_wildcard_attr_prefixes}
263316
317+
_explicit_args = kwargs.pop('_explicit_args')
318+
_locals = locals()
319+
_locals.update(kwargs) # For wildcard attrs
320+
args = {{k: _locals[k] for k in _explicit_args if k != 'children'}}
321+
264322
for k in {required_args}:
265-
if k not in kwargs:
323+
if k not in args:
266324
raise TypeError(
267325
'Required argument `' + k + '` was not specified.')
268-
269326
super({typename}, self).__init__({argtext})
270327
271328
def __repr__(self):
@@ -290,13 +347,13 @@ def __repr__(self):
290347
return (
291348
'{typename}(' +
292349
repr(getattr(self, self._prop_names[0], None)) + ')')
293-
'''
350+
'''
294351

295352
filtered_props = reorder_props(filter_props(props))
296353
# pylint: disable=unused-variable
297354
list_of_valid_wildcard_attr_prefixes = repr(parse_wildcards(props))
298355
# pylint: disable=unused-variable
299-
list_of_valid_keys = repr(list(filtered_props.keys()))
356+
list_of_valid_keys = repr(list(map(str, filtered_props.keys())))
300357
# pylint: disable=unused-variable
301358
docstring = create_docstring(
302359
component_name=typename,
@@ -306,19 +363,82 @@ def __repr__(self):
306363

307364
# pylint: disable=unused-variable
308365
events = '[' + ', '.join(parse_events(props)) + ']'
366+
prop_keys = list(props.keys())
309367
if 'children' in props:
310-
default_argtext = 'children=None, **kwargs'
368+
prop_keys.remove('children')
369+
default_argtext = "children=None, "
311370
# pylint: disable=unused-variable
312-
argtext = 'children=children, **kwargs'
371+
argtext = 'children=children, **args'
313372
else:
314-
default_argtext = '**kwargs'
315-
argtext = '**kwargs'
373+
default_argtext = ""
374+
argtext = '**args'
375+
default_argtext += ", ".join(
376+
[('{:s}=Component.REQUIRED'.format(p)
377+
if props[p]['required'] else
378+
'{:s}=Component.UNDEFINED'.format(p))
379+
for p in prop_keys
380+
if not p.endswith("-*") and
381+
p not in ['dashEvents', 'fireEvent', 'setProps']]
382+
)
316383

317384
required_args = required_props(props)
385+
return c.format(**locals())
386+
387+
388+
# pylint: disable=unused-argument
389+
def generate_class_file(typename, props, description, namespace):
390+
"""
391+
Generate a python class file (.py) given a class string
318392
319-
scope = {'Component': Component}
393+
Parameters
394+
----------
395+
typename
396+
props
397+
description
398+
namespace
399+
400+
Returns
401+
-------
402+
403+
"""
404+
import_string =\
405+
"# AUTO GENERATED FILE - DO NOT EDIT\n\n" + \
406+
"from dash.development.base_component import " + \
407+
"Component, _explicitize_args\n\n\n"
408+
class_string = generate_class_string(
409+
typename,
410+
props,
411+
description,
412+
namespace
413+
)
414+
file_name = "{:s}.py".format(typename)
415+
416+
file_path = os.path.join(namespace, file_name)
417+
with open(file_path, 'w') as f:
418+
f.write(import_string)
419+
f.write(class_string)
420+
421+
422+
# pylint: disable=unused-argument
423+
def generate_class(typename, props, description, namespace):
424+
"""
425+
Generate a python class object given a class string
426+
427+
Parameters
428+
----------
429+
typename
430+
props
431+
description
432+
namespace
433+
434+
Returns
435+
-------
436+
437+
"""
438+
string = generate_class_string(typename, props, description, namespace)
439+
scope = {'Component': Component, '_explicitize_args': _explicitize_args}
320440
# pylint: disable=exec-used
321-
exec(c.format(**locals()), scope)
441+
exec(string, scope)
322442
result = scope[typename]
323443
return result
324444

dash/development/component_loader.py

+66-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import collections
22
import json
3+
import os
34
from .base_component import generate_class
5+
from .base_component import generate_class_file
6+
7+
8+
def _get_metadata(metadata_path):
9+
# Start processing
10+
with open(metadata_path) as data_file:
11+
json_string = data_file.read()
12+
data = json\
13+
.JSONDecoder(object_pairs_hook=collections.OrderedDict)\
14+
.decode(json_string)
15+
return data
416

517

618
def load_components(metadata_path,
@@ -20,12 +32,7 @@ def load_components(metadata_path,
2032

2133
components = []
2234

23-
# Start processing
24-
with open(metadata_path) as data_file:
25-
json_string = data_file.read()
26-
data = json\
27-
.JSONDecoder(object_pairs_hook=collections.OrderedDict)\
28-
.decode(json_string)
35+
data = _get_metadata(metadata_path)
2936

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

4956
return components
57+
58+
59+
def generate_classes(namespace, metadata_path='lib/metadata.json'):
60+
"""Load React component metadata into a format Dash can parse,
61+
then create python class files.
62+
63+
Usage: generate_classes()
64+
65+
Keyword arguments:
66+
namespace -- name of the generated python package (also output dir)
67+
68+
metadata_path -- a path to a JSON file created by
69+
[`react-docgen`](https://github.com/reactjs/react-docgen).
70+
71+
Returns:
72+
"""
73+
74+
data = _get_metadata(metadata_path)
75+
imports_path = os.path.join(namespace, '_imports_.py')
76+
77+
# Make sure the file doesn't exist, as we use append write
78+
if os.path.exists(imports_path):
79+
os.remove(imports_path)
80+
81+
# Iterate over each property name (which is a path to the component)
82+
for componentPath in data:
83+
componentData = data[componentPath]
84+
85+
# Extract component name from path
86+
# e.g. src/components/MyControl.react.js
87+
# TODO Make more robust - some folks will write .jsx and others
88+
# will be on windows. Unfortunately react-docgen doesn't include
89+
# the name of the component atm.
90+
name = componentPath.split('/').pop().split('.')[0]
91+
generate_class_file(
92+
name,
93+
componentData['props'],
94+
componentData['description'],
95+
namespace
96+
)
97+
98+
# Add an import statement for this component
99+
with open(imports_path, 'a') as f:
100+
f.write('from .{0:s} import {0:s}\n'.format(name))
101+
102+
# Add the __all__ value so we can import * from _imports_
103+
all_imports = [p.split('/').pop().split('.')[0] for p in data]
104+
with open(imports_path, 'a') as f:
105+
array_string = '[\n'
106+
for a in all_imports:
107+
array_string += ' "{:s}",\n'.format(a)
108+
array_string += ']\n'
109+
f.write('\n\n__all__ = {:s}'.format(array_string))

dash/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.22.1'
1+
__version__ = '0.23.0'

0 commit comments

Comments
 (0)