From da5223d4acd6c30477855934c7a6160a3106ad63 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 31 Oct 2018 16:10:09 -0400 Subject: [PATCH 01/15] Refactor component registration. --- dash/development/base_component.py | 22 +++++++++++++- dash/resources.py | 47 +++++------------------------- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 44c95521ce..9172c8a79d 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -3,6 +3,25 @@ import os import inspect from ._all_keywords import kwlist +import keyword +import six + + +class ComponentRegistry(type): + """Just importing a component lib will make it be loaded on the index""" + + component_registry = set() + + def __new__(mcs, name, bases, attributes): + component = type.__new__(mcs, name, bases, attributes) + if name == 'Component': + # Don't do the base component + return component + + module = attributes['__module__'].split('.')[0] + mcs.component_registry.add(module) + + return component def is_number(s): @@ -53,7 +72,8 @@ def wrapper(*args, **kwargs): return wrapper -class Component(collections.MutableMapping): +@six.add_metaclass(ComponentRegistry) +class Component: class _UNDEFINED(object): def __repr__(self): return 'undefined' diff --git a/dash/resources.py b/dash/resources.py index aa1ce871d9..f12c4a99af 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -1,9 +1,9 @@ -from copy import copy import json import warnings import os +import sys -from .development.base_component import Component +from .development.base_component import ComponentRegistry # pylint: disable=old-style-class @@ -60,40 +60,15 @@ def _filter_resources(self, all_resources, dev_bundles=False): def get_all_resources(self, dev_bundles=False): all_resources = [] - if self.config.infer_from_layout: - all_resources = ( - self.get_inferred_resources() + self._resources - ) - else: - all_resources = self._resources - return self._filter_resources(all_resources, dev_bundles) - - def get_inferred_resources(self): - namespaces = [] - resources = [] - layout = self.layout - - def extract_resource_from_component(component): - # pylint: disable=protected-access - if (isinstance(component, Component) and - component._namespace not in namespaces): + for mod in ComponentRegistry.component_registry: + # take the component lib module and take the _resource_dist. + m = sys.modules[mod] + all_resources.extend(getattr(m, self.resource_name, [])) - namespaces.append(component._namespace) + all_resources.extend(self._resources) - if hasattr(component, self.resource_name): - - component_resources = copy( - getattr(component, self.resource_name) - ) - for r in component_resources: - r['namespace'] = component._namespace - resources.extend(component_resources) - - extract_resource_from_component(layout) - for t in layout.traverse(): - extract_resource_from_component(t) - return resources + return self._filter_resources(all_resources, dev_bundles) class Css: @@ -111,9 +86,6 @@ def append_css(self, stylesheet): def get_all_css(self): return self._resources.get_all_resources() - def get_inferred_css_dist(self): - return self._resources.get_inferred_resources() - # pylint: disable=old-style-class, no-init, too-few-public-methods class config: infer_from_layout = True @@ -134,9 +106,6 @@ def append_script(self, script): def get_all_scripts(self, dev_bundles=False): return self._resources.get_all_resources(dev_bundles) - def get_inferred_scripts(self): - return self._resources.get_inferred_resources() - # pylint: disable=old-style-class, no-init, too-few-public-methods class config: infer_from_layout = True From ed8595cb7794e5c91d6165021719f74f15b08548 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 31 Oct 2018 16:35:21 -0400 Subject: [PATCH 02/15] Use abc.ABCMeta as metaclass base for component as MutableMapping. --- dash/development/base_component.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 9172c8a79d..2321d02408 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -4,16 +4,17 @@ import inspect from ._all_keywords import kwlist import keyword +import abc import six -class ComponentRegistry(type): +class ComponentRegistry(abc.ABCMeta): """Just importing a component lib will make it be loaded on the index""" component_registry = set() def __new__(mcs, name, bases, attributes): - component = type.__new__(mcs, name, bases, attributes) + component = abc.ABCMeta.__new__(mcs, name, bases, attributes) if name == 'Component': # Don't do the base component return component @@ -73,7 +74,7 @@ def wrapper(*args, **kwargs): @six.add_metaclass(ComponentRegistry) -class Component: +class Component(collections.MutableMapping): class _UNDEFINED(object): def __repr__(self): return 'undefined' From bb5fea5e29c28a4dcbbcd79f9bfd94f799d6101d Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 31 Oct 2018 16:42:13 -0400 Subject: [PATCH 03/15] Pylint fixes --- dash/development/base_component.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 2321d02408..534fea8663 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -13,6 +13,7 @@ class ComponentRegistry(abc.ABCMeta): component_registry = set() + # pylint: disable=arguments-differ def __new__(mcs, name, bases, attributes): component = abc.ABCMeta.__new__(mcs, name, bases, attributes) if name == 'Component': From c6a3020e29403b11946c2960b8cc240efda7a681 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Wed, 31 Oct 2018 20:01:29 -0400 Subject: [PATCH 04/15] Register the components namespace in component_loader.load_components --- dash/development/base_component.py | 6 ++++-- dash/development/component_loader.py | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 534fea8663..7e2d757859 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -16,11 +16,13 @@ class ComponentRegistry(abc.ABCMeta): # pylint: disable=arguments-differ def __new__(mcs, name, bases, attributes): component = abc.ABCMeta.__new__(mcs, name, bases, attributes) - if name == 'Component': + module = attributes['__module__'].split('.')[0] + if name == 'Component' or module == 'builtins': # Don't do the base component + # and the components loaded dynamically by load_component + # as it doesn't have the namespace. return component - module = attributes['__module__'].split('.')[0] mcs.component_registry.add(module) return component diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 74d2e557d4..d82075ef04 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -3,6 +3,7 @@ import os from .base_component import generate_class from .base_component import generate_class_file +from .base_component import ComponentRegistry def _get_metadata(metadata_path): @@ -30,6 +31,8 @@ def load_components(metadata_path, `type`, `valid_kwargs`, and `setup`. """ + # Register the component lib for index include. + ComponentRegistry.component_registry.add(namespace) components = [] data = _get_metadata(metadata_path) From db2485fcc3605c549674e8ce1b127c72916d47dd Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Thu, 1 Nov 2018 11:16:37 -0400 Subject: [PATCH 05/15] Add dist cache for the registry. --- dash/development/base_component.py | 19 +++++++++++++++++++ dash/resources.py | 10 +--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 7e2d757859..3b7b4fa435 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -5,6 +5,7 @@ from ._all_keywords import kwlist import keyword import abc +import sys import six @@ -12,6 +13,7 @@ class ComponentRegistry(abc.ABCMeta): """Just importing a component lib will make it be loaded on the index""" component_registry = set() + __dist_cache = collections.defaultdict(dict) # pylint: disable=arguments-differ def __new__(mcs, name, bases, attributes): @@ -27,6 +29,23 @@ def __new__(mcs, name, bases, attributes): return component + @classmethod + def get_resources(mcs, resource_name): + cached = mcs.__dist_cache.get(resource_name) + current_len = len(mcs.component_registry) + + if cached and current_len == cached.get('len'): + return cached.get('resources') + + mcs.__dist_cache[resource_name]['resources'] = resources = [] + mcs.__dist_cache[resource_name]['len'] = current_len + + for module_name in mcs.component_registry: + module = sys.modules[module_name] + resources.extend(getattr(module, resource_name, [])) + + return resources + def is_number(s): try: diff --git a/dash/resources.py b/dash/resources.py index f12c4a99af..9dcb78d503 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -1,7 +1,6 @@ import json import warnings import os -import sys from .development.base_component import ComponentRegistry @@ -59,15 +58,8 @@ def _filter_resources(self, all_resources, dev_bundles=False): return filtered_resources def get_all_resources(self, dev_bundles=False): - all_resources = [] - - for mod in ComponentRegistry.component_registry: - # take the component lib module and take the _resource_dist. - m = sys.modules[mod] - all_resources.extend(getattr(m, self.resource_name, [])) - + all_resources = ComponentRegistry.get_resources(self.resource_name) all_resources.extend(self._resources) - return self._filter_resources(all_resources, dev_bundles) From ff3d16a13b97fb5c39fcd27be5f92f21b25f6b68 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Thu, 1 Nov 2018 11:22:37 -0400 Subject: [PATCH 06/15] Rename ComponentRegistry.component_registry -> registry. --- dash/development/base_component.py | 8 ++++---- dash/development/component_loader.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 3b7b4fa435..74381ae7d6 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -12,7 +12,7 @@ class ComponentRegistry(abc.ABCMeta): """Just importing a component lib will make it be loaded on the index""" - component_registry = set() + registry = set() __dist_cache = collections.defaultdict(dict) # pylint: disable=arguments-differ @@ -25,14 +25,14 @@ def __new__(mcs, name, bases, attributes): # as it doesn't have the namespace. return component - mcs.component_registry.add(module) + mcs.registry.add(module) return component @classmethod def get_resources(mcs, resource_name): cached = mcs.__dist_cache.get(resource_name) - current_len = len(mcs.component_registry) + current_len = len(mcs.registry) if cached and current_len == cached.get('len'): return cached.get('resources') @@ -40,7 +40,7 @@ def get_resources(mcs, resource_name): mcs.__dist_cache[resource_name]['resources'] = resources = [] mcs.__dist_cache[resource_name]['len'] = current_len - for module_name in mcs.component_registry: + for module_name in mcs.registry: module = sys.modules[module_name] resources.extend(getattr(module, resource_name, [])) diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index d82075ef04..2b5e70b10f 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -32,7 +32,7 @@ def load_components(metadata_path, """ # Register the component lib for index include. - ComponentRegistry.component_registry.add(namespace) + ComponentRegistry.registry.add(namespace) components = [] data = _get_metadata(metadata_path) From cdb785d390be4a243e8e4a74568fd97cc671c075 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Thu, 1 Nov 2018 11:35:20 -0400 Subject: [PATCH 07/15] Add resources cache. --- dash/resources.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/dash/resources.py b/dash/resources.py index 9dcb78d503..a89d18fc79 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -11,6 +11,10 @@ def __init__(self, resource_name, layout): self._resources = [] self.resource_name = resource_name self.layout = layout + self._cache = { + 'resources': [], + 'len': 0 + } def append_resource(self, resource): self._resources.append(resource) @@ -58,9 +62,16 @@ def _filter_resources(self, all_resources, dev_bundles=False): return filtered_resources def get_all_resources(self, dev_bundles=False): + cur_len = len(self._resources) + if self._cache['resources'] and cur_len == self._cache['len']: + return self._cache['resources'] + all_resources = ComponentRegistry.get_resources(self.resource_name) all_resources.extend(self._resources) - return self._filter_resources(all_resources, dev_bundles) + + self._cache['resources'] = res = self._filter_resources(all_resources, dev_bundles) + self._cache['len'] = cur_len + return res class Css: From f779d27c02d27d9cec93cbed3128808bf28c4848 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Thu, 1 Nov 2018 11:36:29 -0400 Subject: [PATCH 08/15] Line length fix. --- dash/resources.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dash/resources.py b/dash/resources.py index a89d18fc79..2193169ecf 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -69,7 +69,8 @@ def get_all_resources(self, dev_bundles=False): all_resources = ComponentRegistry.get_resources(self.resource_name) all_resources.extend(self._resources) - self._cache['resources'] = res = self._filter_resources(all_resources, dev_bundles) + self._cache['resources'] = res = \ + self._filter_resources(all_resources, dev_bundles) self._cache['len'] = cur_len return res From b002d14822b715ab03671381bfcb32a740fca662 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Thu, 1 Nov 2018 11:41:09 -0400 Subject: [PATCH 09/15] Disable pylint old-style-class --- .pylintrc | 3 ++- dash/resources.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index fba35186cc..fcf0968f10 100644 --- a/.pylintrc +++ b/.pylintrc @@ -57,7 +57,8 @@ confidence= disable=fixme, missing-docstring, invalid-name, - too-many-lines + too-many-lines, + old-style-class # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where diff --git a/dash/resources.py b/dash/resources.py index 2193169ecf..6d6c97b4d1 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -75,8 +75,7 @@ def get_all_resources(self, dev_bundles=False): return res -class Css: - # pylint: disable=old-style-class +class Css: # pylint: disable=old-style-class def __init__(self, layout=None): self._resources = Resources('_css_dist', layout) self._resources.config = self.config From f0cd7f72259b763cbbaa3b9c2e2457e0d8dcf260 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Thu, 1 Nov 2018 11:49:58 -0400 Subject: [PATCH 10/15] Add late component register test. --- tests/test_integration.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_integration.py b/tests/test_integration.py index c507b80a07..ef433030f6 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -531,3 +531,27 @@ def create_layout(): self.startServer(app) time.sleep(0.5) + + def test_late_component_register(self): + app = dash.Dash() + + app.layout = html.Div([ + html.Button('Click me to put a dcc ', id='btn-insert'), + html.Div(id='output') + ]) + + @app.callback(Output('output', 'children'), + [Input('btn-insert', 'n_clicks')]) + def update_output(value): + if value is None: + raise PreventUpdate + + return dcc.Input(id='inserted-input') + + self.startServer(app) + + btn = self.wait_for_element_by_css_selector('#btn-insert') + btn.click() + time.sleep(1) + + self.wait_for_element_by_css_selector('#inserted-input') From b565cbc8874e5f00f3375cd9ff1e8c514d6c8547 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Fri, 2 Nov 2018 21:58:41 -0400 Subject: [PATCH 11/15] Refactor `__new__` to ComponentMeta. --- dash/development/base_component.py | 46 ++++++++++++++++-------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 74381ae7d6..d19702aff6 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -9,12 +9,33 @@ import six -class ComponentRegistry(abc.ABCMeta): - """Just importing a component lib will make it be loaded on the index""" +# pylint: disable=no-init,too-few-public-methods +class ComponentRegistry: + """Holds a registry of the namespaces used by components.""" registry = set() __dist_cache = collections.defaultdict(dict) + @classmethod + def get_resources(cls, resource_name): + cached = cls.__dist_cache.get(resource_name) + current_len = len(cls.registry) + + if cached and current_len == cached.get('len'): + return cached.get('resources') + + cls.__dist_cache[resource_name]['resources'] = resources = [] + cls.__dist_cache[resource_name]['len'] = current_len + + for module_name in cls.registry: + module = sys.modules[module_name] + resources.extend(getattr(module, resource_name, [])) + + return resources + + +class ComponentMeta(abc.ABCMeta): + # pylint: disable=arguments-differ def __new__(mcs, name, bases, attributes): component = abc.ABCMeta.__new__(mcs, name, bases, attributes) @@ -25,27 +46,10 @@ def __new__(mcs, name, bases, attributes): # as it doesn't have the namespace. return component - mcs.registry.add(module) + ComponentRegistry.registry.add(module) return component - @classmethod - def get_resources(mcs, resource_name): - cached = mcs.__dist_cache.get(resource_name) - current_len = len(mcs.registry) - - if cached and current_len == cached.get('len'): - return cached.get('resources') - - mcs.__dist_cache[resource_name]['resources'] = resources = [] - mcs.__dist_cache[resource_name]['len'] = current_len - - for module_name in mcs.registry: - module = sys.modules[module_name] - resources.extend(getattr(module, resource_name, [])) - - return resources - def is_number(s): try: @@ -95,7 +99,7 @@ def wrapper(*args, **kwargs): return wrapper -@six.add_metaclass(ComponentRegistry) +@six.add_metaclass(ComponentMeta) class Component(collections.MutableMapping): class _UNDEFINED(object): def __repr__(self): From 9a65658f8ddd384c6db153b0fe1f1fc0809fc08d Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Mon, 5 Nov 2018 11:05:10 -0500 Subject: [PATCH 12/15] Remove cache bust len from ComponentRegistry. --- dash/development/base_component.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index d19702aff6..de62756e28 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -14,18 +14,16 @@ class ComponentRegistry: """Holds a registry of the namespaces used by components.""" registry = set() - __dist_cache = collections.defaultdict(dict) + __dist_cache = {} @classmethod def get_resources(cls, resource_name): cached = cls.__dist_cache.get(resource_name) - current_len = len(cls.registry) - if cached and current_len == cached.get('len'): - return cached.get('resources') + if cached: + return cached - cls.__dist_cache[resource_name]['resources'] = resources = [] - cls.__dist_cache[resource_name]['len'] = current_len + cls.__dist_cache[resource_name] = resources = [] for module_name in cls.registry: module = sys.modules[module_name] From c8e6fa8991b9b3be9a461855eaa1f4633ec13074 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Mon, 5 Nov 2018 11:14:08 -0500 Subject: [PATCH 13/15] Remove len cache bust from resources, integrate with hot-reload later. --- dash/resources.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/dash/resources.py b/dash/resources.py index 6d6c97b4d1..c01546df35 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -11,10 +11,7 @@ def __init__(self, resource_name, layout): self._resources = [] self.resource_name = resource_name self.layout = layout - self._cache = { - 'resources': [], - 'len': 0 - } + self._resources_cache = [] def append_resource(self, resource): self._resources.append(resource) @@ -62,16 +59,14 @@ def _filter_resources(self, all_resources, dev_bundles=False): return filtered_resources def get_all_resources(self, dev_bundles=False): - cur_len = len(self._resources) - if self._cache['resources'] and cur_len == self._cache['len']: - return self._cache['resources'] + if self._resources_cache: + return self._resources_cache all_resources = ComponentRegistry.get_resources(self.resource_name) all_resources.extend(self._resources) - self._cache['resources'] = res = \ + self._resources_cache = res = \ self._filter_resources(all_resources, dev_bundles) - self._cache['len'] = cur_len return res From 2449ee9595cbefc4d6f16706cdafabb9d412d188 Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 6 Nov 2018 11:04:53 -0500 Subject: [PATCH 14/15] Version bump. --- CHANGELOG.md | 4 ++++ dash/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 530c0c7a87..5936f46e20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.29.0 - 2018-11-06 +## Added +- Added component namespaces registry, collect the resources needed by component library when they are imported instead of crawling the layout. [#444](https://github.com/plotly/dash/pull/444) + ## 0.28.7 - 2018-11-05 ## Fixed - Component generation now uses the same prop name black list in all supported Python versions. Closes [#361](https://github.com/plotly/dash/issues/361). [#450](https://github.com/plotly/dash/pull/450) diff --git a/dash/version.py b/dash/version.py index b9becf56e0..9093e4e468 100644 --- a/dash/version.py +++ b/dash/version.py @@ -1 +1 @@ -__version__ = '0.28.7' +__version__ = '0.29.0' From b731b6eb97e788345cf78819555c3f7cc58a9edf Mon Sep 17 00:00:00 2001 From: Philippe Duval Date: Tue, 6 Nov 2018 11:08:11 -0500 Subject: [PATCH 15/15] Pylint fix. --- dash/development/base_component.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index de62756e28..789253a3f2 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -2,12 +2,12 @@ import copy import os import inspect -from ._all_keywords import kwlist -import keyword import abc import sys import six +from ._all_keywords import kwlist + # pylint: disable=no-init,too-few-public-methods class ComponentRegistry: