From 76d5fad2569f4e42bddd43714a520c43a0335576 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 11 Oct 2019 18:33:18 -0400 Subject: [PATCH 01/81] cleanup: break out validation, clean up usage - new _validate.py to centralize error checking - dash exceptions auto-dedent - clean up usage and cruft --- dash/__init__.py | 4 +- dash/_callback_context.py | 3 + dash/_utils.py | 4 +- dash/_validate.py | 431 +++++++++++++++++++++++ dash/dash.py | 711 +++++++------------------------------- dash/dependencies.py | 10 +- dash/exceptions.py | 6 +- 7 files changed, 565 insertions(+), 604 deletions(-) create mode 100644 dash/_validate.py diff --git a/dash/__init__.py b/dash/__init__.py index ead91983cd..647c457edd 100644 --- a/dash/__init__.py +++ b/dash/__init__.py @@ -4,6 +4,4 @@ from . import exceptions # noqa: F401 from . import resources # noqa: F401 from .version import __version__ # noqa: F401 -from ._callback_context import CallbackContext as _CallbackContext - -callback_context = _CallbackContext() +from ._callback_context import callback_context # noqa: F401 diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 04284cc6b7..231c19b3e4 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -39,3 +39,6 @@ def triggered(self): @has_context def response(self): return getattr(flask.g, 'dash_response') + + +callback_context = CallbackContext() diff --git a/dash/_utils.py b/dash/_utils.py index 6b0b36a6fd..f7952af39b 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -7,7 +7,7 @@ import collections import subprocess import logging -from io import open # pylint: disable=redefined-builtin +import io from functools import wraps import future.utils as utils from . import exceptions @@ -186,7 +186,7 @@ def run_command_with_process(cmd): def compute_md5(path): - with open(path, encoding="utf-8") as fp: + with io.open(path, encoding="utf-8") as fp: return hashlib.md5(fp.read().encode("utf-8")).hexdigest() diff --git a/dash/_validate.py b/dash/_validate.py new file mode 100644 index 0000000000..f6d3560748 --- /dev/null +++ b/dash/_validate.py @@ -0,0 +1,431 @@ +import collections +from itertools import chain +import pprint +import re + +from .development.base_component import Component +from .dependencies import Input, Output, State +from . import exceptions +from ._utils import create_callback_id, patch_collections_abc + +# py2/3 json.dumps-compatible strings - these are equivalent in py3, not in py2 +_strings = (type(u""), type("")) + + +def validate_callback(app, layout, output, inputs, state): + is_multi = isinstance(output, (list, tuple)) + validate_ids = not app.config.suppress_callback_exceptions + + if layout is None and validate_ids: + # Without a layout, we can't do validation on the IDs and + # properties of the elements in the callback. + raise exceptions.LayoutIsNotDefined( + """ + Attempting to assign a callback to the application but + the `layout` property has not been assigned. + Assign the `layout` property before assigning callbacks. + Alternatively, suppress this warning by setting + `suppress_callback_exceptions=True` + """ + ) + + outputs = output if is_multi else [output] + for args, cls in [(outputs, Output), (inputs, Input), (state, State)]: + validate_callback_args(args, cls, layout, validate_ids) + + if state and not inputs: + raise exceptions.MissingInputsException( + """ + This callback has {} `State` element{} but no `Input` elements. + + Without `Input` elements, this callback will never get called. + + (Subscribing to Input components will cause the + callback to be called whenever their values change.) + """.format( + len(state), "s" if len(state) > 1 else "" + ) + ) + + for i in inputs: + bad = None + if is_multi: + for o in output: + if o == i: + bad = o + else: + if output == i: + bad = output + if bad: + raise exceptions.SameInputOutputException( + "Same output and input: {}".format(bad) + ) + + if is_multi: + if len(set(output)) != len(output): + raise exceptions.DuplicateCallbackOutput( + "Same output was used more than once in a " + "multi output callback!\nDuplicates:\n{}".format( + ",\n".join(str(x) for x in output if output.count(x) > 1) + ) + ) + + callback_id = create_callback_id(output) + + callbacks = set( + chain( + *( + x[2:-2].split("...") if x.startswith("..") else [x] + for x in app.callback_map + ) + ) + ) + + if is_multi: + dups = callbacks.intersection(str(y) for y in output) + if dups: + raise exceptions.DuplicateCallbackOutput( + """ + Multi output {} contains an `Output` object + that was already assigned. + Duplicates: + {} + """.format( + callback_id, pprint.pformat(dups) + ) + ) + else: + if callback_id in callbacks: + raise exceptions.DuplicateCallbackOutput( + """ + You have already assigned a callback to the output + with ID "{}" and property "{}". An output can only have + a single callback function. Try combining your inputs and + callback functions together into one function. + """.format( + output.component_id, output.component_property + ) + ) + + +def validate_callback_args(args, cls, layout, validate_ids): + name = cls.__name__ + if not isinstance(args, (list, tuple)): + raise exceptions.IncorrectTypeException( + """ + The {} argument `{}` must be a list or tuple of + `dash.dependencies.{}`s. + """.format( + name.lower(), str(args), name + ) + ) + + for arg in args: + if not isinstance(arg, cls): + raise exceptions.IncorrectTypeException( + """ + The {} argument `{}` must be of type `dash.dependencies.{}`. + """.format( + name.lower(), str(arg), name + ) + ) + + if not isinstance(arg.component_id, _strings): + raise exceptions.IncorrectTypeException( + """ + component_id must be a string or dict, found {!r} + """.format( + arg.component_id + ) + ) + + if not isinstance(arg.component_property, _strings): + raise exceptions.IncorrectTypeException( + """ + component_property must be a string, found {!r} + """.format( + arg.component_property + ) + ) + + invalid_characters = ["."] + if any(x in arg.component_id for x in invalid_characters): + raise exceptions.InvalidComponentIdError( + """ + The element `{}` contains {} in its ID. + Periods are not allowed in IDs. + """.format( + arg.component_id, invalid_characters + ) + ) + + if validate_ids: + top_id = getattr(layout, "id", None) + arg_id = arg.component_id + arg_prop = getattr(arg, "component_property", None) + if arg_id not in layout and arg_id != top_id: + raise exceptions.NonExistentIdException( + """ + Attempting to assign a callback to the component with + id "{}" but no components with that id exist in the layout. + + Here is a list of IDs in layout: + {} + + If you are assigning callbacks to components that are + generated by other callbacks (and therefore not in the + initial layout), you can suppress this exception by setting + `suppress_callback_exceptions=True`. + """.format( + arg_id, [k for k in layout] + ([top_id] if top_id else []) + ) + ) + + component = layout if top_id == arg_id else layout[arg_id] + + if ( + arg_prop + and arg_prop not in component.available_properties + and not any( + arg_prop.startswith(w) + for w in component.available_wildcard_properties + ) + ): + raise exceptions.NonExistentPropException( + """ + Attempting to assign a callback with the property "{0}" + but component "{1}" doesn't have "{0}" as a property. + + Here are the available properties in "{1}": + {2} + """.format( + arg_prop, arg_id, component.available_properties + ) + ) + + if hasattr(arg, "component_event"): + raise exceptions.NonExistentEventException( + """ + Events have been removed. + Use the associated property instead. + """ + ) + + +def validate_multi_return(output, output_value, callback_id): + if not isinstance(output_value, (list, tuple)): + raise exceptions.InvalidCallbackReturnValue( + """ + The callback {} is a multi-output. + Expected the output type to be a list or tuple but got: + {}. + """.format( + callback_id, repr(output_value) + ) + ) + + if not len(output_value) == len(output): + raise exceptions.InvalidCallbackReturnValue( + """ + Invalid number of output values for {}. + Expected {}, got {} + """.format( + callback_id, len(output), len(output_value) + ) + ) + + +def fail_callback_output(output_value, output): + valid = _strings + (dict, int, float, type(None), Component) + + def _raise_invalid(bad_val, outer_val, path, index=None, toplevel=False): + bad_type = type(bad_val).__name__ + outer_id = ( + "(id={:s})".format(outer_val.id) if getattr(outer_val, "id", False) else "" + ) + outer_type = type(outer_val).__name__ + if toplevel: + location = """ + The value in question is either the only value returned, + or is in the top level of the returned list, + """ + else: + index_string = "[*]" if index is None else "[{:d}]".format(index) + location = """ + The value in question is located at + {} {} {} + {}, + """.format( + index_string, outer_type, outer_id, path + ) + + raise exceptions.InvalidCallbackReturnValue( + """ + The callback for `{output}` + returned a {object:s} having type `{type}` + which is not JSON serializable. + + {location} + and has string representation + `{bad_val}` + + In general, Dash properties can only be + dash components, strings, dictionaries, numbers, None, + or lists of those. + """.format( + output=repr(output), + object="tree with one value" if not toplevel else "value", + type=bad_type, + location=location, + bad_val=bad_val, + ) + ) + + def _value_is_valid(val): + return isinstance(val, valid) + + def _validate_value(val, index=None): + # val is a Component + if isinstance(val, Component): + # pylint: disable=protected-access + for p, j in val._traverse_with_paths(): + # check each component value in the tree + if not _value_is_valid(j): + _raise_invalid(bad_val=j, outer_val=val, path=p, index=index) + + # Children that are not of type Component or + # list/tuple not returned by traverse + child = getattr(j, "children", None) + if not isinstance(child, (tuple, collections.MutableSequence)): + if child and not _value_is_valid(child): + _raise_invalid( + bad_val=child, + outer_val=val, + path=p + "\n" + "[*] " + type(child).__name__, + index=index, + ) + + # Also check the child of val, as it will not be returned + child = getattr(val, "children", None) + if not isinstance(child, (tuple, collections.MutableSequence)): + if child and not _value_is_valid(child): + _raise_invalid( + bad_val=child, + outer_val=val, + path=type(child).__name__, + index=index, + ) + + # val is not a Component, but is at the top level of tree + elif not _value_is_valid(val): + _raise_invalid( + bad_val=val, + outer_val=type(val).__name__, + path="", + index=index, + toplevel=True, + ) + + if isinstance(output_value, list): + for i, val in enumerate(output_value): + _validate_value(val, index=i) + else: + _validate_value(output_value) + + # if we got this far, raise a generic JSON error + raise exceptions.InvalidCallbackReturnValue( + """ + The callback for property `{property:s}` of component `{id:s}` + returned a value which is not JSON serializable. + + In general, Dash properties can only be dash components, strings, + dictionaries, numbers, None, or lists of those. + """.format( + property=output.component_property, id=output.component_id + ) + ) + + +def check_obsolete(kwargs): + for key in kwargs: + if key in ["components_cache_max_age", "static_folder"]: + raise exceptions.ObsoleteKwargException( + """ + {} is no longer a valid keyword argument in Dash since v1.0. + See https://dash.plot.ly for details. + """.format( + key + ) + ) + # any other kwarg mimic the built-in exception + raise TypeError("Dash() got an unexpected keyword argument '" + key + "'") + + +def validate_js_path(registered_paths, package_name, path_in_package_dist): + if package_name not in registered_paths: + raise exceptions.DependencyException( + """ + Error loading dependency. "{}" is not a registered library. + Registered libraries are: + {} + """.format( + package_name, list(registered_paths.keys()) + ) + ) + + if path_in_package_dist not in registered_paths[package_name]: + raise exceptions.DependencyException( + """ + "{}" is registered but the path requested is not valid. + The path requested: "{}" + List of registered paths: {} + """.format( + package_name, path_in_package_dist, registered_paths + ) + ) + + +def validate_index(name, checks, index): + missing = [i for check, i in checks if not re.compile(check).search(index)] + if missing: + plural = "s" if len(missing) > 1 else "" + raise exceptions.InvalidIndexException( + "Missing item{pl} {items} in {name}.".format( + items=", ".join(missing), pl=plural, name=name + ) + ) + + +def validate_layout_type(value): + if not isinstance(value, (Component, patch_collections_abc("Callable"))): + raise exceptions.NoLayoutException( + "Layout must be a dash component " + "or a function that returns a dash component." + ) + + +def validate_layout(layout, layout_value): + if layout is None: + raise exceptions.NoLayoutException( + """ + The layout was `None` at the time that `run_server` was called. + Make sure to set the `layout` attribute of your application + before running the server. + """ + ) + + layout_id = getattr(layout_value, "id", None) + + component_ids = {layout_id} if layout_id else set() + # pylint: disable=protected-access + for component in layout_value._traverse(): + component_id = getattr(component, "id", None) + if component_id and component_id in component_ids: + raise exceptions.DuplicateIdError( + """ + Duplicate component id found in the initial layout: `{}` + """.format( + component_id + ) + ) + component_ids.add(component_id) diff --git a/dash/dash.py b/dash/dash.py index 679ed0ee9a..257f3e3acf 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -11,10 +11,8 @@ import threading import re import logging -import pprint from functools import wraps -from textwrap import dedent import flask from flask_compress import Compress @@ -23,23 +21,25 @@ import plotly import dash_renderer -from .dependencies import Input, Output, State from .fingerprint import build_fingerprint, check_fingerprint from .resources import Scripts, Css -from .development.base_component import Component, ComponentRegistry -from . import exceptions -from ._utils import AttributeDict as _AttributeDict -from ._utils import interpolate_str as _interpolate -from ._utils import format_tag as _format_tag -from ._utils import generate_hash as _generate_hash -from ._utils import patch_collections_abc as _patch_collections_abc -from . import _watch -from ._utils import get_asset_path as _get_asset_path -from ._utils import create_callback_id as _create_callback_id -from ._utils import get_relative_path as _get_relative_path -from ._utils import strip_relative_path as _strip_relative_path -from ._configs import get_combined_config, pathname_configs +from .development.base_component import ComponentRegistry +from .exceptions import PreventUpdate, InvalidResourceError from .version import __version__ +from ._configs import get_combined_config, pathname_configs +from ._utils import ( + AttributeDict, + create_callback_id, + format_tag, + generate_hash, + get_asset_path, + get_relative_path, + interpolate_str, + patch_collections_abc, + strip_relative_path +) +from . import _validate +from . import _watch _default_index = """ @@ -67,15 +67,14 @@ """ -_re_index_entry = re.compile(r"{%app_entry%}") -_re_index_config = re.compile(r"{%config%}") -_re_index_scripts = re.compile(r"{%scripts%}") -_re_renderer_scripts = re.compile(r"{%renderer%}") +_re_index_entry = "{%app_entry%}", "{%app_entry%}" +_re_index_config = "{%config%}", "{%config%}" +_re_index_scripts = "{%scripts%}", "{%scripts%}" -_re_index_entry_id = re.compile(r'id="react-entry-point"') -_re_index_config_id = re.compile(r'id="_dash-config"') -_re_index_scripts_id = re.compile(r'src=".*dash[-_]renderer.*"') -_re_renderer_scripts_id = re.compile(r'id="_dash-renderer') +_re_index_entry_id = 'id="react-entry-point"', "#react-entry-point" +_re_index_config_id = 'id="_dash-config"', "#_dash-config" +_re_index_scripts_id = 'src="[^"]*dash[-_]renderer[^"]*"', "dash-renderer" +_re_renderer_scripts_id = 'id="_dash-renderer', "new DashRenderer" class _NoUpdate(object): @@ -87,6 +86,13 @@ class _NoUpdate(object): no_update = _NoUpdate() +_inline_clientside_template = """ +var clientside = window.dash_clientside = window.dash_clientside || {{}}; +var ns = clientside["{namespace}"] = clientside["{namespace}"] || {{}}; +ns["{function_name}"] = {clientside_function}; +""" + + # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-locals class Dash(object): @@ -231,16 +237,7 @@ def __init__( plugins=None, **obsolete ): - for key in obsolete: - if key in ["components_cache_max_age", "static_folder"]: - raise exceptions.ObsoleteKwargException( - key + " is no longer a valid keyword argument in Dash " - "since v1.0. See https://dash.plot.ly for details." - ) - # any other kwarg mimic the built-in exception - raise TypeError( - "Dash() got an unexpected keyword argument '" + key + "'" - ) + _validate.check_obsolete(obsolete) # We have 3 cases: server is either True (we create the server), False # (defer server creation) or a Flask app instance (we use their server) @@ -258,7 +255,7 @@ def __init__( url_base_pathname, routes_pathname_prefix, requests_pathname_prefix ) - self.config = _AttributeDict( + self.config = AttributeDict( name=name, assets_folder=os.path.join( flask.helpers.get_root_path(name), assets_folder @@ -333,7 +330,7 @@ def __init__( self._cached_layout = None self._setup_dev_tools() - self._hot_reload = _AttributeDict( + self._hot_reload = AttributeDict( hash=None, hard=False, lock=threading.RLock(), @@ -346,7 +343,7 @@ def __init__( self.logger = logging.getLogger(name) self.logger.addHandler(logging.StreamHandler(stream=sys.stdout)) - if isinstance(plugins, _patch_collections_abc("Iterable")): + if isinstance(plugins, patch_collections_abc("Iterable")): for plugin in plugins: plugin.plug(self) @@ -380,65 +377,49 @@ def init_app(self, app=None): # gzip Compress(self.server) - @self.server.errorhandler(exceptions.PreventUpdate) + @self.server.errorhandler(PreventUpdate) def _handle_error(_): """Handle a halted callback and return an empty 204 response.""" return "", 204 - prefix = config.routes_pathname_prefix - self.server.before_first_request(self._setup_server) # add a handler for components suites errors to return 404 - self.server.errorhandler(exceptions.InvalidResourceError)( + self.server.errorhandler(InvalidResourceError)( self._invalid_resources_handler ) - self._add_url("{}_dash-layout".format(prefix), self.serve_layout) - - self._add_url("{}_dash-dependencies".format(prefix), self.dependencies) - - self._add_url( - "{}_dash-update-component".format(prefix), self.dispatch, ["POST"] - ) - self._add_url( - ( - "{}_dash-component-suites" - "/" - "/" - ).format(prefix), + "_dash-component-suites//", self.serve_component_suites, ) - - self._add_url("{}_dash-routes".format(prefix), self.serve_routes) - - self._add_url(prefix, self.index) - - self._add_url("{}_reload-hash".format(prefix), self.serve_reload_hash) + self._add_url("_dash-layout", self.serve_layout) + self._add_url("_dash-dependencies", self.dependencies) + self._add_url("_dash-update-component", self.dispatch, ["POST"]) + self._add_url("_reload-hash", self.serve_reload_hash) + self._add_url("_favicon.ico", self._serve_default_favicon) + self._add_url("", self.index) # catch-all for front-end routes, used by dcc.Location - self._add_url("{}".format(prefix), self.index) - - self._add_url( - "{}_favicon.ico".format(prefix), self._serve_default_favicon - ) + self._add_url("", self.index) def _add_url(self, name, view_func, methods=("GET",)): + full_name = self.config.routes_pathname_prefix + name + self.server.add_url_rule( - name, view_func=view_func, endpoint=name, methods=list(methods) + full_name, view_func=view_func, endpoint=name, methods=list(methods) ) # record the url in Dash.routes so that it can be accessed later # e.g. for adding authentication with flask_login - self.routes.append(name) + self.routes.append(full_name) @property def layout(self): return self._layout def _layout_value(self): - if isinstance(self._layout, _patch_collections_abc("Callable")): + if isinstance(self._layout, patch_collections_abc("Callable")): self._cached_layout = self._layout() else: self._cached_layout = self._layout @@ -446,15 +427,7 @@ def _layout_value(self): @layout.setter def layout(self, value): - if not isinstance(value, Component) and not isinstance( - value, _patch_collections_abc("Callable") - ): - raise exceptions.NoLayoutException( - "Layout must be a dash component " - "or a function that returns " - "a dash component." - ) - + _validate.validate_layout_type(value) self._cached_layout = None self._layout = value @@ -464,18 +437,8 @@ def index_string(self): @index_string.setter def index_string(self, value): - checks = ( - (_re_index_entry.search(value), "app_entry"), - (_re_index_config.search(value), "config"), - (_re_index_scripts.search(value), "scripts"), - ) - missing = [missing for check, missing in checks if not check] - if missing: - raise exceptions.InvalidIndexException( - "Did you forget to include {} in your index string ?".format( - ", ".join("{%" + x + "%}" for x in missing) - ) - ) + checks = (_re_index_entry, _re_index_config, _re_index_scripts) + _validate.validate_index("index string", checks, value) self._index_string = value def serve_layout(self): @@ -522,12 +485,6 @@ def serve_reload_hash(self): } ) - def serve_routes(self): - return flask.Response( - json.dumps(self.routes, cls=plotly.utils.PlotlyJSONEncoder), - mimetype="application/json", - ) - def _collect_and_register_resources(self, resources): # now needs the app context. # template in the necessary component suite JS bundles @@ -593,7 +550,7 @@ def _generate_css_dist_html(self): return "\n".join( [ - _format_tag("link", link, opened=True) + format_tag("link", link, opened=True) if isinstance(link, dict) else ''.format(link) for link in (external_links + links) @@ -637,7 +594,7 @@ def _generate_scripts_html(self): return "\n".join( [ - _format_tag("script", src) + format_tag("script", src) if isinstance(src, dict) else ''.format(src) for src in srcs @@ -677,33 +634,15 @@ def _generate_meta_html(self): if not has_charset: tags.append('') - tags += [_format_tag("meta", x, opened=True) for x in meta_tags] + tags += [format_tag("meta", x, opened=True) for x in meta_tags] return "\n ".join(tags) # Serve the JS bundles for each package - def serve_component_suites(self, package_name, path_in_package_dist): - path_in_package_dist, has_fingerprint = check_fingerprint( - path_in_package_dist - ) - - if package_name not in self.registered_paths: - raise exceptions.DependencyException( - "Error loading dependency.\n" - '"{}" is not a registered library.\n' - "Registered libraries are: {}".format( - package_name, list(self.registered_paths.keys()) - ) - ) + def serve_component_suites(self, package_name, fingerprinted_path): + path_in_pkg, has_fingerprint = check_fingerprint(fingerprinted_path) - if path_in_package_dist not in self.registered_paths[package_name]: - raise exceptions.DependencyException( - '"{}" is registered but the path requested is not valid.\n' - 'The path requested: "{}"\n' - "List of registered paths: {}".format( - package_name, path_in_package_dist, self.registered_paths - ) - ) + _validate.validate_js_path(self.registered_paths, package_name, path_in_pkg) mimetype = ( { @@ -711,19 +650,19 @@ def serve_component_suites(self, package_name, path_in_package_dist): "css": "text/css", "map": "application/json", } - )[path_in_package_dist.split(".")[-1]] + )[path_in_pkg.split(".")[-1]] package = sys.modules[package_name] self.logger.debug( "serving -- package: %s[%s] resource: %s => location: %s", package_name, package.__version__, - path_in_package_dist, + path_in_pkg, package.__path__, ) response = flask.Response( - pkgutil.get_data(package_name, path_in_package_dist), + pkgutil.get_data(package_name, path_in_pkg), mimetype=mimetype, ) @@ -764,7 +703,7 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument self.config.requests_pathname_prefix, __version__ ) - favicon = _format_tag( + favicon = format_tag( "link", {"rel": "icon", "type": "image/x-icon", "href": favicon_url}, opened=True, @@ -782,21 +721,12 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument ) checks = ( - (_re_index_entry_id.search(index), "#react-entry-point"), - (_re_index_config_id.search(index), "#_dash-configs"), - (_re_index_scripts_id.search(index), "dash-renderer"), - (_re_renderer_scripts_id.search(index), "new DashRenderer"), + _re_index_entry_id, + _re_index_config_id, + _re_index_scripts_id, + _re_renderer_scripts_id, ) - missing = [missing for check, missing in checks if not check] - - if missing: - plural = "s" if len(missing) > 1 else "" - raise exceptions.InvalidIndexException( - "Missing element{pl} {ids} in index.".format( - ids=", ".join(missing), pl=plural - ) - ) - + _validate.validate_index("index", checks, index) return index def interpolate_index( @@ -845,7 +775,7 @@ def interpolate_index(self, **kwargs): :param favicon: A favicon tag if found in assets folder. :return: The interpolated HTML string for the index. """ - return _interpolate( + return interpolate_str( self.index_string, metas=metas, title=title, @@ -870,339 +800,31 @@ def dependencies(self): ] ) - def _validate_callback(self, output, inputs, state): - # pylint: disable=too-many-branches + def _insert_callback(self, output, inputs, state): layout = self._cached_layout or self._layout_value() - is_multi = isinstance(output, (list, tuple)) - - if layout is None and not self.config.suppress_callback_exceptions: - # Without a layout, we can't do validation on the IDs and - # properties of the elements in the callback. - raise exceptions.LayoutIsNotDefined( - dedent( - """ - Attempting to assign a callback to the application but - the `layout` property has not been assigned. - Assign the `layout` property before assigning callbacks. - Alternatively, suppress this warning by setting - `suppress_callback_exceptions=True` - """ - ) - ) - - outputs = output if is_multi else [output] - for args, obj, name in [ - (outputs, Output, "Output"), - (inputs, Input, "Input"), - (state, State, "State"), - ]: - - if not isinstance(args, (list, tuple)): - raise exceptions.IncorrectTypeException( - "The {} argument `{}` must be " - "a list or tuple of `dash.dependencies.{}`s.".format( - name.lower(), str(args), name - ) - ) - - for arg in args: - if not isinstance(arg, obj): - raise exceptions.IncorrectTypeException( - "The {} argument `{}` must be " - "of type `dash.{}`.".format( - name.lower(), str(arg), name - ) - ) - - invalid_characters = ["."] - if any(x in arg.component_id for x in invalid_characters): - raise exceptions.InvalidComponentIdError( - "The element `{}` contains {} in its ID. " - "Periods are not allowed in IDs.".format( - arg.component_id, invalid_characters - ) - ) - - if not self.config.suppress_callback_exceptions: - layout_id = getattr(layout, "id", None) - arg_id = arg.component_id - arg_prop = getattr(arg, "component_property", None) - if arg_id not in layout and arg_id != layout_id: - all_ids = [k for k in layout] - if layout_id: - all_ids.append(layout_id) - raise exceptions.NonExistentIdException( - dedent( - """ - Attempting to assign a callback to the - component with the id "{0}" but no - components with id "{0}" exist in the - app\'s layout.\n\n - Here is a list of IDs in layout:\n{1}\n\n - If you are assigning callbacks to components - that are generated by other callbacks - (and therefore not in the initial layout), then - you can suppress this exception by setting - `suppress_callback_exceptions=True`. - """ - ).format(arg_id, all_ids) - ) - - component = ( - layout if layout_id == arg_id else layout[arg_id] - ) + _validate.validate_callback(self, layout, output, inputs, state) + callback_id = create_callback_id(output) - if ( - arg_prop - and arg_prop not in component.available_properties - and not any( - arg_prop.startswith(w) - for w in component.available_wildcard_properties - ) - ): - raise exceptions.NonExistentPropException( - dedent( - """ - Attempting to assign a callback with - the property "{0}" but the component - "{1}" doesn't have "{0}" as a property.\n - Here are the available properties in "{1}": - {2} - """ - ).format( - arg_prop, - arg_id, - component.available_properties, - ) - ) - - if hasattr(arg, "component_event"): - raise exceptions.NonExistentEventException( - dedent( - """ - Events have been removed. - Use the associated property instead. - """ - ) - ) - - if state and not inputs: - raise exceptions.MissingInputsException( - dedent( - """ - This callback has {} `State` {} - but no `Input` elements.\n - Without `Input` elements, this callback - will never get called.\n - (Subscribing to input components will cause the - callback to be called whenever their values change.) - """ - ).format( - len(state), "elements" if len(state) > 1 else "element" - ) - ) - - for i in inputs: - bad = None - if is_multi: - for o in output: - if o == i: - bad = o - else: - if output == i: - bad = output - if bad: - raise exceptions.SameInputOutputException( - "Same output and input: {}".format(bad) - ) - - if is_multi: - if len(set(output)) != len(output): - raise exceptions.DuplicateCallbackOutput( - "Same output was used more than once in a " - "multi output callback!\n Duplicates:\n {}".format( - ",\n".join( - k - for k, v in ( - (str(x), output.count(x)) for x in output - ) - if v > 1 - ) - ) - ) - - callback_id = _create_callback_id(output) - - callbacks = set( - itertools.chain( - *( - x[2:-2].split("...") if x.startswith("..") else [x] - for x in self.callback_map - ) - ) - ) - ns = {"duplicates": set()} - if is_multi: - - def duplicate_check(): - ns["duplicates"] = callbacks.intersection( - str(y) for y in output - ) - return ns["duplicates"] - - else: - - def duplicate_check(): - return callback_id in callbacks - - if duplicate_check(): - if is_multi: - msg = dedent( - """ - Multi output {} contains an `Output` object - that was already assigned. - Duplicates: - {} - """ - ).format(callback_id, pprint.pformat(ns["duplicates"])) - else: - msg = dedent( - """ - You have already assigned a callback to the output - with ID "{}" and property "{}". An output can only have - a single callback function. Try combining your inputs and - callback functions together into one function. - """ - ).format(output.component_id, output.component_property) - raise exceptions.DuplicateCallbackOutput(msg) - - @staticmethod - def _validate_callback_output(output_value, output): - valid = [str, dict, int, float, type(None), Component] - - def _raise_invalid( - bad_val, outer_val, path, index=None, toplevel=False - ): - bad_type = type(bad_val).__name__ - outer_id = ( - "(id={:s})".format(outer_val.id) - if getattr(outer_val, "id", False) - else "" - ) - outer_type = type(outer_val).__name__ - raise exceptions.InvalidCallbackReturnValue( - dedent( - """ - The callback for `{output:s}` - returned a {object:s} having type `{type:s}` - which is not JSON serializable. - - {location_header:s}{location:s} - and has string representation - `{bad_val}` - - In general, Dash properties can only be - dash components, strings, dictionaries, numbers, None, - or lists of those. - """ - ).format( - output=repr(output), - object="tree with one value" if not toplevel else "value", - type=bad_type, - location_header=( - "The value in question is located at" - if not toplevel - else "The value in question is either the only value " - "returned,\nor is in the top level of the returned " - "list," - ), - location=( - "\n" - + ( - "[{:d}] {:s} {:s}".format( - index, outer_type, outer_id - ) - if index is not None - else ("[*] " + outer_type + " " + outer_id) - ) - + "\n" - + path - + "\n" - ) - if not toplevel - else "", - bad_val=bad_val, - ) - ) - - def _value_is_valid(val): - return ( - # pylint: disable=unused-variable - any([isinstance(val, x) for x in valid]) - or type(val).__name__ == "unicode" - ) - - def _validate_value(val, index=None): - # val is a Component - if isinstance(val, Component): - # pylint: disable=protected-access - for p, j in val._traverse_with_paths(): - # check each component value in the tree - if not _value_is_valid(j): - _raise_invalid( - bad_val=j, outer_val=val, path=p, index=index - ) - - # Children that are not of type Component or - # list/tuple not returned by traverse - child = getattr(j, "children", None) - if not isinstance( - child, (tuple, collections.MutableSequence) - ): - if child and not _value_is_valid(child): - _raise_invalid( - bad_val=child, - outer_val=val, - path=p + "\n" + "[*] " + type(child).__name__, - index=index, - ) - - # Also check the child of val, as it will not be returned - child = getattr(val, "children", None) - if not isinstance(child, (tuple, collections.MutableSequence)): - if child and not _value_is_valid(child): - _raise_invalid( - bad_val=child, - outer_val=val, - path=type(child).__name__, - index=index, - ) - - # val is not a Component, but is at the top level of tree - else: - if not _value_is_valid(val): - _raise_invalid( - bad_val=val, - outer_val=type(val).__name__, - path="", - index=index, - toplevel=True, - ) + self.callback_map[callback_id] = { + "inputs": [ + {"id": c.component_id, "property": c.component_property} + for c in inputs + ], + "state": [ + {"id": c.component_id, "property": c.component_property} + for c in state + ], + } - if isinstance(output_value, list): - for i, val in enumerate(output_value): - _validate_value(val, index=i) - else: - _validate_value(output_value) + return callback_id - # pylint: disable=dangerous-default-value def clientside_callback( - self, clientside_function, output, inputs=[], state=[] + self, clientside_function, output, inputs, state=() ): """Create a callback that updates the output by calling a clientside (JavaScript) function instead of a Python function. - Unlike `@app.calllback`, `clientside_callback` is not a decorator: + Unlike `@app.callback`, `clientside_callback` is not a decorator: it takes either a `dash.dependencies.ClientsideFunction(namespace, function_name)` argument that describes which JavaScript function to call @@ -1259,8 +881,7 @@ def clientside_callback( ) ``` """ - self._validate_callback(output, inputs, state) - callback_id = _create_callback_id(output) + callback_id = self._insert_callback(output, inputs, state) # If JS source is explicitly given, create a namespace and function # name, then inject the code. @@ -1274,13 +895,11 @@ def clientside_callback( function_name = '{}'.format(out0.component_property) self._inline_scripts.append( - """ - var clientside = window.dash_clientside = window.dash_clientside || {{}}; - var ns = clientside["{0}"] = clientside["{0}"] || {{}}; - ns["{1}"] = {2}; - """.format(namespace.replace('"', '\\"'), - function_name.replace('"', '\\"'), - clientside_function) + _inline_clientside_template.format( + namespace=namespace.replace('"', '\\"'), + function_name=function_name.replace('"', '\\"'), + clientside_function=clientside_function + ) ) # Callback is stored in an external asset. @@ -1288,72 +907,24 @@ def clientside_callback( namespace = clientside_function.namespace function_name = clientside_function.function_name - self.callback_map[callback_id] = { - "inputs": [ - {"id": c.component_id, "property": c.component_property} - for c in inputs - ], - "state": [ - {"id": c.component_id, "property": c.component_property} - for c in state - ], - "clientside_function": { - "namespace": namespace, - "function_name": function_name, - }, + self.callback_map[callback_id]["clientside_function"] = { + "namespace": namespace, + "function_name": function_name, } - # TODO - Update nomenclature. - # "Parents" and "Children" should refer to the DOM tree - # and not the dependency tree. - # The dependency tree should use the nomenclature - # "observer" and "controller". - # "observers" listen for changes from their "controllers". For example, - # if a graph depends on a dropdown, the graph is the "observer" and the - # dropdown is a "controller". In this case the graph's "dependency" is - # the dropdown. - # TODO - Check this map for recursive or other ill-defined non-tree - # relationships - # pylint: disable=dangerous-default-value - def callback(self, output, inputs=[], state=[]): - self._validate_callback(output, inputs, state) - - callback_id = _create_callback_id(output) + def callback(self, output, inputs, state=()): + callback_id = self._insert_callback(output, inputs, state) multi = isinstance(output, (list, tuple)) - self.callback_map[callback_id] = { - "inputs": [ - {"id": c.component_id, "property": c.component_property} - for c in inputs - ], - "state": [ - {"id": c.component_id, "property": c.component_property} - for c in state - ], - } - def wrap_func(func): @wraps(func) def add_context(*args, **kwargs): # don't touch the comment on the next line - used by debugger output_value = func(*args, **kwargs) # %% callback invoked %% if multi: - if not isinstance(output_value, (list, tuple)): - raise exceptions.InvalidCallbackReturnValue( - "The callback {} is a multi-output.\n" - "Expected the output type to be a list" - " or tuple but got {}.".format( - callback_id, repr(output_value) - ) - ) - - if not len(output_value) == len(output): - raise exceptions.InvalidCallbackReturnValue( - "Invalid number of output values for {}.\n" - " Expected {} got {}".format( - callback_id, len(output), len(output_value) - ) - ) + _validate.validate_multi_return( + output, output_value, callback_id + ) component_ids = collections.defaultdict(dict) has_update = False @@ -1365,12 +936,12 @@ def add_context(*args, **kwargs): component_ids[o_id][o_prop] = val if not has_update: - raise exceptions.PreventUpdate + raise PreventUpdate response = {"response": component_ids, "multi": True} else: if isinstance(output_value, _NoUpdate): - raise exceptions.PreventUpdate + raise PreventUpdate response = { "response": { @@ -1383,23 +954,7 @@ def add_context(*args, **kwargs): response, cls=plotly.utils.PlotlyJSONEncoder ) except TypeError: - self._validate_callback_output(output_value, output) - raise exceptions.InvalidCallbackReturnValue( - dedent( - """ - The callback for property `{property:s}` - of component `{id:s}` returned a value - which is not JSON serializable. - - In general, Dash properties can only be - dash components, strings, dictionaries, numbers, None, - or lists of those. - """ - ).format( - property=output.component_property, - id=output.component_id, - ) - ) + _validate.fail_callback_output(output_value, output) return jsonResponse @@ -1436,53 +991,23 @@ def dispatch(self): mimetype="application/json" ) + def pluck_val(_props, component_registration): + for c in _props: + if ( + c["property"] == component_registration["property"] and + c["id"] == component_registration["id"] + ): + return c.get("value", None) + for component_registration in self.callback_map[output]["inputs"]: - args.append( - [ - c.get("value", None) - for c in inputs - if c["property"] == component_registration["property"] - and c["id"] == component_registration["id"] - ][0] - ) + args.append(pluck_val(inputs, component_registration)) for component_registration in self.callback_map[output]["state"]: - args.append( - [ - c.get("value", None) - for c in state - if c["property"] == component_registration["property"] - and c["id"] == component_registration["id"] - ][0] - ) + args.append(pluck_val(state, component_registration)) response.set_data(self.callback_map[output]["callback"](*args)) return response - def _validate_layout(self): - if self.layout is None: - raise exceptions.NoLayoutException( - "The layout was `None` " - "at the time that `run_server` was called. " - "Make sure to set the `layout` attribute of your application " - "before running the server." - ) - - to_validate = self._layout_value() - - layout_id = getattr(self.layout, "id", None) - - component_ids = {layout_id} if layout_id else set() - # pylint: disable=protected-access - for component in to_validate._traverse(): - component_id = getattr(component, "id", None) - if component_id and component_id in component_ids: - raise exceptions.DuplicateIdError( - "Duplicate component id found" - " in the initial layout: `{}`".format(component_id) - ) - component_ids.add(component_id) - def _setup_server(self): # Apply _force_eager_loading overrides from modules eager_loading = self.config.eager_loading @@ -1497,7 +1022,7 @@ def _setup_server(self): if self.config.include_assets_files: self._walk_assets_directory() - self._validate_layout() + _validate.validate_layout(self.layout, self._layout_value()) self._generate_scripts_html() self._generate_css_dist_html() @@ -1559,7 +1084,7 @@ def _serve_default_favicon(): ) def get_asset_url(self, path): - asset = _get_asset_path( + asset = get_asset_path( self.config.requests_pathname_prefix, path, self.config.assets_url_path.lstrip("/"), @@ -1604,7 +1129,7 @@ def display_content(path): return chapters.page_2 ``` """ - asset = _get_relative_path( + asset = get_relative_path( self.config.requests_pathname_prefix, path, ) @@ -1658,14 +1183,14 @@ def display_content(path): `page-1/sub-page-1` ``` """ - return _strip_relative_path( + return strip_relative_path( self.config.requests_pathname_prefix, path, ) def _setup_dev_tools(self, **kwargs): debug = kwargs.get("debug", False) - dev_tools = self._dev_tools = _AttributeDict() + dev_tools = self._dev_tools = AttributeDict() for attr in ( "ui", @@ -1798,7 +1323,7 @@ def enable_dev_tools( if dev_tools.hot_reload: _reload = self._hot_reload - _reload.hash = _generate_hash() + _reload.hash = generate_hash() component_packages_dist = [ os.path.dirname(package.path) @@ -1856,7 +1381,7 @@ def _on_assets_change(self, filename, modified, deleted): _reload = self._hot_reload with _reload.lock: _reload.hard = True - _reload.hash = _generate_hash() + _reload.hash = generate_hash() if self.config.assets_folder in filename: asset_path = ( diff --git a/dash/dependencies.py b/dash/dependencies.py index 3d9b583c1a..6bc5413f0c 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -5,13 +5,13 @@ def __init__(self, component_id, component_property): self.component_property = component_property def __str__(self): - return '{}.{}'.format( + return "{}.{}".format( self.component_id, self.component_property ) def __repr__(self): - return '<{} `{}`>'.format(self.__class__.__name__, self) + return "<{} `{}`>".format(self.__class__.__name__, self) def __eq__(self, other): return isinstance(other, DashDependency) and str(self) == str(other) @@ -25,11 +25,11 @@ class Output(DashDependency): # pylint: disable=too-few-public-methods class Input(DashDependency): # pylint: disable=too-few-public-methods - """Input of callback trigger an update when it is updated.""" + """Input of callback: trigger an update when it is updated.""" class State(DashDependency): # pylint: disable=too-few-public-methods - """Use the value of a state in a callback but don't trigger updates.""" + """Use the value of a State in a callback but don't trigger updates.""" class ClientsideFunction: @@ -47,7 +47,7 @@ def __init__(self, namespace=None, function_name=None): self.function_name = function_name def __repr__(self): - return 'ClientsideFunction({}, {})'.format( + return "ClientsideFunction({}, {})".format( self.namespace, self.function_name ) diff --git a/dash/exceptions.py b/dash/exceptions.py index 4756da912d..9bf0c099f3 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -1,5 +1,9 @@ +from textwrap import dedent + + class DashException(Exception): - pass + def __init__(self, msg=""): + super(DashException, self).__init__(dedent(msg).strip()) class ObsoleteKwargException(DashException): From 53a952bc24c9da31d7a0abdf716a63e7f97934a4 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 15 Oct 2019 17:45:32 -0400 Subject: [PATCH 02/81] wildcards back end --- dash/_utils.py | 8 +- dash/_validate.py | 287 +++++++++++++++++++---------- dash/dash.py | 12 +- dash/dependencies.py | 98 ++++++++-- dash/development/base_component.py | 21 +++ dash/exceptions.py | 4 + 6 files changed, 310 insertions(+), 120 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index f7952af39b..79062c6fe8 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -163,7 +163,13 @@ def create_callback_id(output): if isinstance(output, (list, tuple)): return "..{}..".format( "...".join( - "{}.{}".format(x.component_id, x.component_property) + "{}.{}".format( + # A single dot within a dict id key or value is OK + # but in case of multiple dots together escape each dot + # with `\` so we don't mistake it for multi-outputs + x.component_id_str().replace(".", "\\."), + x.component_property + ) for x in output ) ) diff --git a/dash/_validate.py b/dash/_validate.py index f6d3560748..12cee11791 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -1,15 +1,10 @@ import collections -from itertools import chain -import pprint import re -from .development.base_component import Component -from .dependencies import Input, Output, State +from .development.base_component import Component, _strings +from .dependencies import Input, Output, State, ANY, ALLSMALLER, PREVIOUS from . import exceptions -from ._utils import create_callback_id, patch_collections_abc - -# py2/3 json.dumps-compatible strings - these are equivalent in py3, not in py2 -_strings = (type(u""), type("")) +from ._utils import patch_collections_abc def validate_callback(app, layout, output, inputs, state): @@ -52,58 +47,89 @@ def validate_callback(app, layout, output, inputs, state): if is_multi: for o in output: if o == i: + # Note: different but overlapping wildcards compare as equal bad = o - else: - if output == i: - bad = output + elif output == i: + bad = output if bad: raise exceptions.SameInputOutputException( "Same output and input: {}".format(bad) ) - if is_multi: - if len(set(output)) != len(output): - raise exceptions.DuplicateCallbackOutput( - "Same output was used more than once in a " - "multi output callback!\nDuplicates:\n{}".format( - ",\n".join(str(x) for x in output if output.count(x) > 1) + if is_multi and len(set(output)) != len(output): + raise exceptions.DuplicateCallbackOutput( + """ + Same output was used more than once in a multi output callback! + Duplicates: + {} + """.format( + ",\n".join(str(x) for x in output if output.count(x) > 1) + ) + ) + + any_keys = get_wildcard_keys(outputs[0], (ANY,)) + for out in outputs[1:]: + if get_wildcard_keys(out, (ANY,)) != any_keys: + raise exceptions.InconsistentCallbackWildcards( + """ + All `Output` items must have matching wildcard `ANY` values. + `ALL` wildcards need not match, only `ANY`. + + Output {} does not match the first output {}. + """.format( + out, outputs[0] ) ) - callback_id = create_callback_id(output) + matched_wildcards = (ANY, ALLSMALLER, PREVIOUS) + for dep in list(inputs) + list(state): + wildcard_keys = get_wildcard_keys(dep, matched_wildcards) + if wildcard_keys - any_keys: + raise exceptions.InconsistentCallbackWildcards( + """ + `Input` and `State` items can only have {} + wildcards on keys where the `Output`(s) have `ANY` wildcards. + `ALL` wildcards need not match, and you need not match every + `ANY` in the `Output`(s). - callbacks = set( - chain( - *( - x[2:-2].split("...") if x.startswith("..") else [x] - for x in app.callback_map + This callback has `ANY` on keys {}. + {} has these wildcards on keys {}. + """.format( + matched_wildcards, any_keys, dep, wildcard_keys + ) ) - ) - ) - if is_multi: - dups = callbacks.intersection(str(y) for y in output) - if dups: + dups = set() + for out in outputs: + for used_out in app.used_outputs: + if out == used_out: + dups.add(str(used_out)) + if dups: + if is_multi or len(dups) > 1 or str(output) != list(dups)[0]: raise exceptions.DuplicateCallbackOutput( """ - Multi output {} contains an `Output` object - that was already assigned. - Duplicates: + One or more `Output` is already set by a callback. + Note that two wildcard outputs can refer to the same component + even if they don't match exactly. + + The new callback lists output(s): + {} + Already used: {} """.format( - callback_id, pprint.pformat(dups) + ", ".join([str(o) for o in outputs]), + ", ".join(dups) ) ) - else: - if callback_id in callbacks: + else: raise exceptions.DuplicateCallbackOutput( """ - You have already assigned a callback to the output - with ID "{}" and property "{}". An output can only have - a single callback function. Try combining your inputs and - callback functions together into one function. + {} was already assigned to a callback. + Any given output can only have one callback that sets it. + Try combining your inputs and callback functions together + into one function. """.format( - output.component_id, output.component_property + repr(output) ) ) @@ -130,7 +156,30 @@ def validate_callback_args(args, cls, layout, validate_ids): ) ) - if not isinstance(arg.component_id, _strings): + if not isinstance(getattr(arg, "component_property", None), _strings): + raise exceptions.IncorrectTypeException( + """ + component_property must be a string, found {!r} + """.format( + arg.component_property + ) + ) + + if hasattr(arg, "component_event"): + raise exceptions.NonExistentEventException( + """ + Events have been removed. + Use the associated property instead. + """ + ) + + if isinstance(arg.component_id, dict): + validate_id_dict(arg, layout, validate_ids, cls.allowed_wildcards) + + elif isinstance(arg.component_id, _strings): + validate_id_string(arg, layout, validate_ids) + + else: raise exceptions.IncorrectTypeException( """ component_id must be a string or dict, found {!r} @@ -139,77 +188,112 @@ def validate_callback_args(args, cls, layout, validate_ids): ) ) - if not isinstance(arg.component_property, _strings): + +def validate_id_dict(arg, layout, validate_ids, wildcards): + arg_id = arg.component_id + + def id_match(c): + c_id = getattr(c, "id", None) + return isinstance(c_id, dict) and all( + k in c and v in wildcards or v == c_id.get(k) + ) + + if validate_ids: + component = None + if id_match(layout): + component = layout + else: + for c in layout._traverse(): + if id_match(c): + component = c + break + if component: + # for wildcards it's not unusual to have no matching components + # initially; this isn't a problem and we shouldn't force users to + # set suppress_callback_exceptions in this case; but if we DO have + # a matching component, we can check that the prop is valid + validate_prop_for_component(arg, component) + + for k, v in arg_id.items(): + if not (k and isinstance(k, _strings)): raise exceptions.IncorrectTypeException( """ - component_property must be a string, found {!r} + Wildcard ID keys must be non-empty strings, + found {!r} in id {!r} """.format( - arg.component_property + k, arg_id ) ) - - invalid_characters = ["."] - if any(x in arg.component_id for x in invalid_characters): - raise exceptions.InvalidComponentIdError( + if not (v in wildcards or isinstance(v, _strings + (int, float, bool))): + wildcard_msg = ( + ",\n or wildcards: {}".format(wildcards) + if wildcards else "" + ) + raise exceptions.IncorrectTypeException( """ - The element `{}` contains {} in its ID. - Periods are not allowed in IDs. + Wildcard {} ID values must be strings, numbers, bools{} + found {!r} in id {!r} """.format( - arg.component_id, invalid_characters + arg.__class__.__name__, wildcard_msg, k, arg_id ) ) - if validate_ids: - top_id = getattr(layout, "id", None) - arg_id = arg.component_id - arg_prop = getattr(arg, "component_property", None) - if arg_id not in layout and arg_id != top_id: - raise exceptions.NonExistentIdException( - """ - Attempting to assign a callback to the component with - id "{}" but no components with that id exist in the layout. - - Here is a list of IDs in layout: - {} - - If you are assigning callbacks to components that are - generated by other callbacks (and therefore not in the - initial layout), you can suppress this exception by setting - `suppress_callback_exceptions=True`. - """.format( - arg_id, [k for k in layout] + ([top_id] if top_id else []) - ) - ) - component = layout if top_id == arg_id else layout[arg_id] +def validate_id_string(arg, layout, validate_ids): + arg_id = arg.component_id - if ( - arg_prop - and arg_prop not in component.available_properties - and not any( - arg_prop.startswith(w) - for w in component.available_wildcard_properties - ) - ): - raise exceptions.NonExistentPropException( - """ - Attempting to assign a callback with the property "{0}" - but component "{1}" doesn't have "{0}" as a property. - - Here are the available properties in "{1}": - {2} - """.format( - arg_prop, arg_id, component.available_properties - ) - ) + invalid_chars = ".{" + invalid_found = [x for x in invalid_chars if x in arg_id] + if invalid_found: + raise exceptions.InvalidComponentIdError( + """ + The element `{}` contains `{}` in its ID. + Characters `{}` are not allowed in IDs. + """.format( + arg_id, "`, `".join(invalid_found), "`, `".join(invalid_chars) + ) + ) + + if validate_ids: + top_id = getattr(layout, "id", None) + if arg_id not in layout and arg_id != top_id: + raise exceptions.NonExistentIdException( + """ + Attempting to assign a callback to the component with + id "{}" but no components with that id exist in the layout. + + Here is a list of IDs in layout: + {} - if hasattr(arg, "component_event"): - raise exceptions.NonExistentEventException( - """ - Events have been removed. - Use the associated property instead. - """ + If you are assigning callbacks to components that are + generated by other callbacks (and therefore not in the + initial layout), you can suppress this exception by setting + `suppress_callback_exceptions=True`. + """.format( + arg_id, [k for k in layout] + ([top_id] if top_id else []) ) + ) + + component = layout if top_id == arg_id else layout[arg_id] + validate_prop_for_component(arg, component) + + +def validate_prop_for_component(arg, component): + arg_prop = arg.component_property + if arg_prop not in component.available_properties and not any( + arg_prop.startswith(w) for w in component.available_wildcard_properties + ): + raise exceptions.NonExistentPropException( + """ + Attempting to assign a callback with the property "{0}" + but component "{1}" doesn't have "{0}" as a property. + + Here are the available properties in "{1}": + {2} + """.format( + arg_prop, arg.component_id, component.available_properties + ) + ) def validate_multi_return(output, output_value, callback_id): @@ -224,7 +308,7 @@ def validate_multi_return(output, output_value, callback_id): ) ) - if not len(output_value) == len(output): + if len(output_value) != len(output): raise exceptions.InvalidCallbackReturnValue( """ Invalid number of output values for {}. @@ -346,6 +430,13 @@ def _validate_value(val, index=None): ) +def get_wildcard_keys(dep, wildcards): + _id = dep.component_id + if not isinstance(_id, dict): + return set() + return {k for k, v in _id.items() if v in wildcards} + + def check_obsolete(kwargs): for key in kwargs: if key in ["components_cache_max_age", "static_folder"]: diff --git a/dash/dash.py b/dash/dash.py index 257f3e3acf..fb92984b2f 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -305,6 +305,7 @@ def __init__( # list of dependencies self.callback_map = {} + self.used_outputs = [] # list of inline scripts self._inline_scripts = [] @@ -806,15 +807,10 @@ def _insert_callback(self, output, inputs, state): callback_id = create_callback_id(output) self.callback_map[callback_id] = { - "inputs": [ - {"id": c.component_id, "property": c.component_property} - for c in inputs - ], - "state": [ - {"id": c.component_id, "property": c.component_property} - for c in state - ], + "inputs": [c.to_dict() for c in inputs], + "state": [c.to_dict() for c in state], } + self.used_outputs.extend(output if callback_id.startswith("..") else [output]) return callback_id diff --git a/dash/dependencies.py b/dash/dependencies.py index 6bc5413f0c..a570886d64 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -1,20 +1,90 @@ -class DashDependency: - # pylint: disable=too-few-public-methods +class _Wildcard: # pylint: disable=too-few-public-methods + def __init__(self, name): + self._name = name + + def __str__(self): + return self._name + + def __repr__(self): + return "<{}>".format(self) + + def to_json(self): + # used in serializing wildcards - arrays are not allowed as + # id values, so make the wildcards look like length-1 arrays. + return '["{}"]'.format(self._name) + + +ANY = _Wildcard("ANY") +ALL = _Wildcard("ALL") +ALLSMALLER = _Wildcard("ALLSMALLER") +PREVIOUS = _Wildcard("PREVIOUS") + + +class DashDependency: # pylint: disable=too-few-public-methods def __init__(self, component_id, component_property): self.component_id = component_id self.component_property = component_property def __str__(self): - return "{}.{}".format( - self.component_id, - self.component_property - ) + return "{}.{}".format(self.component_id_str(), self.component_property) def __repr__(self): return "<{} `{}`>".format(self.__class__.__name__, self) + def component_id_str(self): + i = self.component_id + + def json(k, v): + vstr = v.to_json() if hasattr(v, "to_json") else json.dumps(v) + return "{}:{}".format(json.dumps(k), vstr) + + if isinstance(i, dict): + return ("{" + ",".join(json(k, i[k]) for k in sorted(i)) + "}") + + return i + + def to_dict(self): + return {"id": self.component_id_str(), "property": self.component_property} + def __eq__(self, other): - return isinstance(other, DashDependency) and str(self) == str(other) + """ + We use "==" to denote two deps that refer to the same prop on + the same component. In the case of wildcard deps, this means + the same prop on *at least one* of the same components. + """ + return ( + isinstance(other, DashDependency) + and self.component_property == other.component_property + and self._id_matches(other) + ) + + def _id_matches(self, other): + other_id = other.component_id + if isinstance(self.component_id, dict): + if not isinstance(other_id, dict): + return False + for k, v in self.component_id.items(): + if k not in other_id: + return False + other_v = other_id[k] + if v == other_v: + continue + v_wild = isinstance(v, _Wildcard) + other_wild = isinstance(other_v, _Wildcard) + if v_wild or other_wild: + if not (v_wild and other_wild): + continue # one wild, one not + if v is ALL or other_v is ALL: + continue # either ALL + if v is ANY or other_v is ANY: + return False # ANY and either ALLSMALLER or PREVIOUS + else: + return False + return True + elif isinstance(other_id, dict): + return False + else: + return self.component_id == other_id def __hash__(self): return hash(str(self)) @@ -23,17 +93,22 @@ def __hash__(self): class Output(DashDependency): # pylint: disable=too-few-public-methods """Output of a callback.""" + allowed_wildcards = (ANY, ALL) + class Input(DashDependency): # pylint: disable=too-few-public-methods """Input of callback: trigger an update when it is updated.""" + allowed_wildcards = (ANY, ALL, ALLSMALLER, PREVIOUS) + class State(DashDependency): # pylint: disable=too-few-public-methods """Use the value of a State in a callback but don't trigger updates.""" + allowed_wildcards = (ANY, ALL, ALLSMALLER, PREVIOUS) -class ClientsideFunction: - # pylint: disable=too-few-public-methods + +class ClientsideFunction: # pylint: disable=too-few-public-methods def __init__(self, namespace=None, function_name=None): if namespace.startswith('_dashprivate_'): @@ -47,7 +122,4 @@ def __init__(self, namespace=None, function_name=None): self.function_name = function_name def __repr__(self): - return "ClientsideFunction({}, {})".format( - self.namespace, - self.function_name - ) + return "ClientsideFunction({}, {})".format(self.namespace, self.function_name) diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 6f0b2ee21d..50483ef0a2 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -7,6 +7,9 @@ MutableSequence = patch_collections_abc("MutableSequence") +# py2/3 json.dumps-compatible strings - these are equivalent in py3, not in py2 +_strings = (type(u""), type("")) + # pylint: disable=no-init,too-few-public-methods class ComponentRegistry: @@ -122,6 +125,24 @@ def __init__(self, **kwargs): "Prop {} has value {}\n".format(k, repr(v)) ) + if k == "id": + if isinstance(v, dict): + for id_key, id_val in v.items(): + if not isinstance(id_key, _strings): + raise TypeError( + "dict id keys must be strings,\n" + + "found {!r} in id {!r}".format(id_key, v) + ) + if not isinstance(id_val, _strings + (int, float, bool)): + raise TypeError( + "dict id values must be strings, numbers or bools,\n" + + "found {!r} in id {!r}".format(id_val, v) + ) + elif not isinstance(v, _strings): + raise TypeError( + "`id` prop must be a string or dict, not {!r}".format(v) + ) + setattr(self, k, v) def to_plotly_json(self): diff --git a/dash/exceptions.py b/dash/exceptions.py index 9bf0c099f3..3f3aca5967 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -64,6 +64,10 @@ class DuplicateCallbackOutput(CantHaveMultipleOutputs): pass +class InconsistentCallbackWildcards(CallbackException): + pass + + class PreventUpdate(CallbackException): pass From 505d56195b0e5d1d260afc5859a63eadd8034f31 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 23 Oct 2019 09:44:13 -0400 Subject: [PATCH 03/81] remove useless `switch` in dependencyGraph.js --- dash-renderer/src/reducers/dependencyGraph.js | 85 +++++++++---------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/dash-renderer/src/reducers/dependencyGraph.js b/dash-renderer/src/reducers/dependencyGraph.js index b023275ab4..77063322dd 100644 --- a/dash-renderer/src/reducers/dependencyGraph.js +++ b/dash-renderer/src/reducers/dependencyGraph.js @@ -5,61 +5,58 @@ import {isMultiOutputProp, parseMultipleOutputs} from '../utils'; const initialGraph = {}; const graphs = (state = initialGraph, action) => { - switch (action.type) { - case 'COMPUTE_GRAPHS': { - const dependencies = action.payload; - const inputGraph = new DepGraph(); - const multiGraph = new DepGraph(); - - dependencies.forEach(function registerDependency(dependency) { - const {output, inputs} = dependency; - - // Multi output supported will be a string already - // Backward compatibility by detecting object. - let outputId; - if (type(output) === 'Object') { - outputId = `${output.id}.${output.property}`; - } else { - outputId = output; - if (isMultiOutputProp(output)) { - parseMultipleOutputs(output).forEach(out => { - multiGraph.addNode(out); - inputs.forEach(i => { - const inputId = `${i.id}.${i.property}`; - if (!multiGraph.hasNode(inputId)) { - multiGraph.addNode(inputId); - } - multiGraph.addDependency(inputId, out); - }); - }); - } else { - multiGraph.addNode(output); + if (action.type === 'COMPUTE_GRAPHS') { + const dependencies = action.payload; + const inputGraph = new DepGraph(); + const multiGraph = new DepGraph(); + + dependencies.forEach(function registerDependency(dependency) { + const {output, inputs} = dependency; + + // Multi output supported will be a string already + // Backward compatibility by detecting object. + let outputId; + if (type(output) === 'Object') { + outputId = `${output.id}.${output.property}`; + } else { + outputId = output; + if (isMultiOutputProp(output)) { + parseMultipleOutputs(output).forEach(out => { + multiGraph.addNode(out); inputs.forEach(i => { const inputId = `${i.id}.${i.property}`; if (!multiGraph.hasNode(inputId)) { multiGraph.addNode(inputId); } - multiGraph.addDependency(inputId, output); + multiGraph.addDependency(inputId, out); }); - } + }); + } else { + multiGraph.addNode(output); + inputs.forEach(i => { + const inputId = `${i.id}.${i.property}`; + if (!multiGraph.hasNode(inputId)) { + multiGraph.addNode(inputId); + } + multiGraph.addDependency(inputId, output); + }); } + } - inputs.forEach(inputObject => { - const inputId = `${inputObject.id}.${inputObject.property}`; - inputGraph.addNode(outputId); - if (!inputGraph.hasNode(inputId)) { - inputGraph.addNode(inputId); - } - inputGraph.addDependency(inputId, outputId); - }); + inputs.forEach(inputObject => { + const inputId = `${inputObject.id}.${inputObject.property}`; + inputGraph.addNode(outputId); + if (!inputGraph.hasNode(inputId)) { + inputGraph.addNode(inputId); + } + inputGraph.addDependency(inputId, outputId); }); + }); - return {InputGraph: inputGraph, MultiGraph: multiGraph}; - } - - default: - return state; + return {InputGraph: inputGraph, MultiGraph: multiGraph}; } + + return state; }; export default graphs; From e2e3566d99a3d4e58325e6cfc6e9708d33d4ee69 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 23 Oct 2019 10:06:44 -0400 Subject: [PATCH 04/81] remove obsolete pre-multi-output fallback from dependencyGraph.js --- dash-renderer/src/reducers/dependencyGraph.js | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/dash-renderer/src/reducers/dependencyGraph.js b/dash-renderer/src/reducers/dependencyGraph.js index 77063322dd..4278e09cd1 100644 --- a/dash-renderer/src/reducers/dependencyGraph.js +++ b/dash-renderer/src/reducers/dependencyGraph.js @@ -1,4 +1,3 @@ -import {type} from 'ramda'; import {DepGraph} from 'dependency-graph'; import {isMultiOutputProp, parseMultipleOutputs} from '../utils'; @@ -13,43 +12,35 @@ const graphs = (state = initialGraph, action) => { dependencies.forEach(function registerDependency(dependency) { const {output, inputs} = dependency; - // Multi output supported will be a string already - // Backward compatibility by detecting object. - let outputId; - if (type(output) === 'Object') { - outputId = `${output.id}.${output.property}`; - } else { - outputId = output; - if (isMultiOutputProp(output)) { - parseMultipleOutputs(output).forEach(out => { - multiGraph.addNode(out); - inputs.forEach(i => { - const inputId = `${i.id}.${i.property}`; - if (!multiGraph.hasNode(inputId)) { - multiGraph.addNode(inputId); - } - multiGraph.addDependency(inputId, out); - }); - }); - } else { - multiGraph.addNode(output); + if (isMultiOutputProp(output)) { + parseMultipleOutputs(output).forEach(out => { + multiGraph.addNode(out); inputs.forEach(i => { const inputId = `${i.id}.${i.property}`; if (!multiGraph.hasNode(inputId)) { multiGraph.addNode(inputId); } - multiGraph.addDependency(inputId, output); + multiGraph.addDependency(inputId, out); }); - } + }); + } else { + multiGraph.addNode(output); + inputs.forEach(i => { + const inputId = `${i.id}.${i.property}`; + if (!multiGraph.hasNode(inputId)) { + multiGraph.addNode(inputId); + } + multiGraph.addDependency(inputId, output); + }); } inputs.forEach(inputObject => { const inputId = `${inputObject.id}.${inputObject.property}`; - inputGraph.addNode(outputId); + inputGraph.addNode(output); if (!inputGraph.hasNode(inputId)) { inputGraph.addNode(inputId); } - inputGraph.addDependency(inputId, outputId); + inputGraph.addDependency(inputId, output); }); }); From 05908b2b9c41e3bb7ec5e5505859c7fb7cb78734 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 29 Oct 2019 19:20:28 -0400 Subject: [PATCH 05/81] wildcards front end --- dash-renderer/package-lock.json | 15 +- dash-renderer/package.json | 1 + dash-renderer/src/APIController.react.js | 33 +- dash-renderer/src/TreeContainer.js | 174 +-- dash-renderer/src/actions/api.js | 2 +- dash-renderer/src/actions/constants.js | 20 +- dash-renderer/src/actions/dependencies.js | 967 ++++++++++++ dash-renderer/src/actions/index.js | 1300 ++++++----------- dash-renderer/src/actions/isAppReady.js | 4 +- dash-renderer/src/actions/paths.js | 71 + dash-renderer/src/actions/utils.js | 45 + .../components/core/DocumentTitle.react.js | 7 +- .../src/components/core/Loading.react.js | 7 +- .../src/components/core/Toolbar.react.js | 4 +- .../error/FrontEnd/FrontEndError.react.js | 2 +- dash-renderer/src/reducers/dependencyGraph.js | 47 +- dash-renderer/src/reducers/paths.js | 53 +- .../src/reducers/pendingCallbacks.js | 11 + dash-renderer/src/reducers/reducer.js | 10 +- dash-renderer/src/reducers/requestQueue.js | 13 - dash-renderer/src/reducers/utils.js | 71 - dash-renderer/src/utils.js | 69 - dash/_callback_context.py | 29 +- dash/_utils.py | 43 +- dash/_validate.py | 43 +- dash/dash.py | 97 +- dash/dependencies.py | 44 +- dash/development/base_component.py | 26 +- 28 files changed, 1853 insertions(+), 1355 deletions(-) create mode 100644 dash-renderer/src/actions/dependencies.js create mode 100644 dash-renderer/src/actions/paths.js create mode 100644 dash-renderer/src/actions/utils.js create mode 100644 dash-renderer/src/reducers/pendingCallbacks.js delete mode 100644 dash-renderer/src/reducers/requestQueue.js delete mode 100644 dash-renderer/src/reducers/utils.js delete mode 100644 dash-renderer/src/utils.js diff --git a/dash-renderer/package-lock.json b/dash-renderer/package-lock.json index 5f8198fe40..d79dca0cd5 100644 --- a/dash-renderer/package-lock.json +++ b/dash-renderer/package-lock.json @@ -1,6 +1,6 @@ { "name": "dash-renderer", - "version": "1.2.2", + "version": "1.2.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -5238,6 +5238,14 @@ } } }, + "fast-isnumeric": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-isnumeric/-/fast-isnumeric-1.1.3.tgz", + "integrity": "sha512-MdojHkfLx8pjRNZyGjOhX4HxNPaf0l5R/v5rGZ1bGXCnRPyQIUAe4I1H7QtrlUwuuiDHKdpQTjT3lmueVH2otw==", + "requires": { + "is-string-blank": "^1.0.1" + } + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -7273,6 +7281,11 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, + "is-string-blank": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-string-blank/-/is-string-blank-1.0.1.tgz", + "integrity": "sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==" + }, "is-supported-regexp-flag": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.1.tgz", diff --git a/dash-renderer/package.json b/dash-renderer/package.json index f993949f20..2202fea38f 100644 --- a/dash-renderer/package.json +++ b/dash-renderer/package.json @@ -25,6 +25,7 @@ "prop-types": "15.7.2", "cookie": "^0.3.1", "dependency-graph": "^0.5.0", + "fast-isnumeric": "^1.1.3", "radium": "^0.22.1", "ramda": "^0.26.1", "react-redux": "^4.4.5", diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index ae9f84aa93..65dee36e80 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -1,17 +1,14 @@ import {connect} from 'react-redux'; -import {includes, isEmpty, isNil} from 'ramda'; +import {includes, isEmpty} from 'ramda'; import React, {Component} from 'react'; import PropTypes from 'prop-types'; import TreeContainer from './TreeContainer'; import GlobalErrorContainer from './components/error/GlobalErrorContainer.react'; -import { - computeGraphs, - computePaths, - hydrateInitialOutputs, - setLayout, -} from './actions/index'; -import {applyPersistence} from './persistence'; +import {hydrateInitialOutputs, setGraphs, setPaths, setLayout} from './actions'; +import {computePaths} from './actions/paths'; +import {computeGraphs} from './actions/dependencies'; import apiThunk from './actions/api'; +import {applyPersistence} from './persistence'; import {getAppState} from './reducers/constants'; import {STATUS} from './constants/constants'; @@ -42,7 +39,6 @@ class UnconnectedContainer extends Component { graphs, layout, layoutRequest, - paths, } = props; if (isEmpty(layoutRequest)) { @@ -53,9 +49,8 @@ class UnconnectedContainer extends Component { layoutRequest.content, dispatch ); + dispatch(setPaths(computePaths(finalLayout, []))); dispatch(setLayout(finalLayout)); - } else if (isNil(paths)) { - dispatch(computePaths({subTree: layout, startingPath: []})); } } @@ -67,7 +62,7 @@ class UnconnectedContainer extends Component { dependenciesRequest.status === STATUS.OK && isEmpty(graphs) ) { - dispatch(computeGraphs(dependenciesRequest.content)); + dispatch(setGraphs(computeGraphs(dependenciesRequest.content))); } if ( @@ -77,7 +72,6 @@ class UnconnectedContainer extends Component { // LayoutRequest and its computed stores layoutRequest.status === STATUS.OK && !isEmpty(layout) && - !isNil(paths) && // Hasn't already hydrated appLifecycle === getAppState('STARTED') ) { @@ -118,20 +112,15 @@ class UnconnectedContainer extends Component { return (
Error loading dependencies
); - } else if ( - appLifecycle === getAppState('HYDRATED') && - config.ui === true - ) { - return ( + } else if (appLifecycle === getAppState('HYDRATED')) { + return config.ui === true ? ( - ); - } else if (appLifecycle === getAppState('HYDRATED')) { - return ( + ) : ( component ) : ( @@ -102,49 +104,48 @@ class TreeContainer extends Component { setProps(newProps) { const { - _dashprivate_dependencies, + _dashprivate_graphs, _dashprivate_dispatch, _dashprivate_path, _dashprivate_layout, } = this.props; - const id = this.getLayoutProps().id; - - // Identify the modified props that are required for callbacks - const watchedKeys = filter( - key => - _dashprivate_dependencies && - _dashprivate_dependencies.find( - dependency => - dependency.inputs.find( - input => input.id === id && input.property === key - ) || - dependency.state.find( - state => state.id === id && state.property === key - ) - ) - )(keysIn(newProps)); - - // setProps here is triggered by the UI - record these changes - // for persistence - recordUiEdit(_dashprivate_layout, newProps, _dashprivate_dispatch); - - // Always update this component's props - _dashprivate_dispatch( - updateProps({ - props: newProps, - itempath: _dashprivate_path, - }) + const oldProps = this.getLayoutProps(); + const {id} = oldProps; + const changedProps = pickBy( + (val, key) => !equals(val, oldProps[key]), + newProps ); + const changedKeys = keys(changedProps); + if (changedKeys.length) { + // Identify the modified props that are required for callbacks + const watchedKeys = getWatchedKeys( + id, + changedKeys, + _dashprivate_graphs + ); - // Only dispatch changes to Dash if a watched prop changed - if (watchedKeys.length) { + // setProps here is triggered by the UI - record these changes + // for persistence + recordUiEdit(_dashprivate_layout, newProps, _dashprivate_dispatch); + + // Always update this component's props _dashprivate_dispatch( - notifyObservers({ - id: id, - props: pick(watchedKeys)(newProps), + updateProps({ + props: changedProps, + itempath: _dashprivate_path, }) ); + + // Only dispatch changes to Dash if a watched prop changed + if (watchedKeys.length) { + _dashprivate_dispatch( + notifyObservers({ + id: id, + props: pick(watchedKeys, changedProps), + }) + ); + } } } @@ -179,32 +180,35 @@ class TreeContainer extends Component { const element = Registry.resolve(_dashprivate_layout); - const props = omit(['children'], _dashprivate_layout.props); + const props = dissoc('children', _dashprivate_layout.props); - return _dashprivate_config.props_check ? ( - - - - ) : ( + if (type(props.id) === 'Object') { + // Turn object ids (for wildcards) into hash strings. + // Because of the `dissoc` we're not mutating the layout, + // just the id we pass on to the rendered component + props.id = stringifyId(props.id); + } + + return ( - {React.createElement( - element, - mergeRight(props, {loading_state, setProps}), - ...(Array.isArray(children) ? children : [children]) + {_dashprivate_config.props_check ? ( + + ) : ( + React.createElement( + element, + mergeRight(props, {loading_state, setProps}), + ...(Array.isArray(children) ? children : [children]) + ) )} ); @@ -247,11 +251,10 @@ class TreeContainer extends Component { } TreeContainer.propTypes = { - _dashprivate_dependencies: PropTypes.any, + _dashprivate_graphs: PropTypes.any, _dashprivate_dispatch: PropTypes.func, _dashprivate_layout: PropTypes.object, _dashprivate_loadingState: PropTypes.object, - _dashprivate_requestQueue: PropTypes.any, _dashprivate_config: PropTypes.object, _dashprivate_path: PropTypes.array, }; @@ -294,28 +297,34 @@ function getNestedIds(layout) { return ids; } -function getLoadingState(layout, requestQueue) { +function getLoadingState(layout, pendingCallbacks) { const ids = isLoadingComponent(layout) ? getNestedIds(layout) - : layout && layout.props.id - ? [layout.props.id] - : []; + : layout && layout.props.id && [layout.props.id]; let isLoading = false; let loadingProp; let loadingComponent; - if (requestQueue) { - forEach(r => { - const controllerId = isNil(r.controllerId) ? '' : r.controllerId; - if ( - r.status === 'loading' && - any(id => includes(id, controllerId), ids) - ) { - isLoading = true; - [loadingComponent, loadingProp] = r.controllerId.split('.'); + if (pendingCallbacks && pendingCallbacks.length && ids && ids.length) { + const idStrs = ids.map(stringifyId); + + pendingCallbacks.forEach(cb => { + const {requestId, requestedOutputs} = cb; + if (requestId === undefined) { + return; } - }, requestQueue); + + idStrs.forEach(idStr => { + const props = requestedOutputs[idStr]; + if (props) { + isLoading = true; + // TODO: what about multiple loading components / props? + loadingComponent = idStr; + loadingProp = props[0]; + } + }); + }); } // Set loading state @@ -328,21 +337,20 @@ function getLoadingState(layout, requestQueue) { export const AugmentedTreeContainer = connect( state => ({ - dependencies: state.dependenciesRequest.content, - requestQueue: state.requestQueue, + graphs: state.graphs, + pendingCallbacks: state.pendingCallbacks, config: state.config, }), dispatch => ({dispatch}), (stateProps, dispatchProps, ownProps) => ({ - _dashprivate_dependencies: stateProps.dependencies, + _dashprivate_graphs: stateProps.graphs, _dashprivate_dispatch: dispatchProps.dispatch, _dashprivate_layout: ownProps._dashprivate_layout, _dashprivate_path: ownProps._dashprivate_path, _dashprivate_loadingState: getLoadingState( ownProps._dashprivate_layout, - stateProps.requestQueue + stateProps.pendingCallbacks ), - _dashprivate_requestQueue: stateProps.requestQueue, _dashprivate_config: stateProps.config, }) )(TreeContainer); diff --git a/dash-renderer/src/actions/api.js b/dash-renderer/src/actions/api.js index 089d917c92..36849ee8b9 100644 --- a/dash-renderer/src/actions/api.js +++ b/dash-renderer/src/actions/api.js @@ -1,7 +1,7 @@ /* global fetch: true */ import {mergeDeepRight} from 'ramda'; import {handleAsyncError, getCSRFHeader} from '../actions'; -import {urlBase} from '../utils'; +import {urlBase} from './utils'; function GET(path, fetchConfig) { return fetch( diff --git a/dash-renderer/src/actions/constants.js b/dash-renderer/src/actions/constants.js index 37866de19d..b9fba5047d 100644 --- a/dash-renderer/src/actions/constants.js +++ b/dash-renderer/src/actions/constants.js @@ -1,18 +1,18 @@ const actionList = { - ON_PROP_CHANGE: 'ON_PROP_CHANGE', - SET_REQUEST_QUEUE: 'SET_REQUEST_QUEUE', - COMPUTE_GRAPHS: 'COMPUTE_GRAPHS', - COMPUTE_PATHS: 'COMPUTE_PATHS', - SET_LAYOUT: 'SET_LAYOUT', - SET_APP_LIFECYCLE: 'SET_APP_LIFECYCLE', - SET_CONFIG: 'SET_CONFIG', - ON_ERROR: 'ON_ERROR', - SET_HOOKS: 'SET_HOOKS', + ON_PROP_CHANGE: 1, + SET_REQUEST_QUEUE: 1, + SET_GRAPHS: 1, + SET_PATHS: 1, + SET_LAYOUT: 1, + SET_APP_LIFECYCLE: 1, + SET_CONFIG: 1, + ON_ERROR: 1, + SET_HOOKS: 1, }; export const getAction = action => { if (actionList[action]) { - return actionList[action]; + return action; } throw new Error(`${action} is not defined.`); }; diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js new file mode 100644 index 0000000000..06c79b4459 --- /dev/null +++ b/dash-renderer/src/actions/dependencies.js @@ -0,0 +1,967 @@ +import {DepGraph} from 'dependency-graph'; +import isNumeric from 'fast-isnumeric'; +import { + any, + ap, + assoc, + clone, + dissoc, + equals, + evolve, + flatten, + forEachObjIndexed, + isEmpty, + map, + mergeDeepRight, + mergeRight, + omit, + partition, + path, + props, + unnest, + values, + zipObj, +} from 'ramda'; + +import {getPath} from './paths'; + +import {crawlLayout} from './utils'; + +/* + * If this update is for multiple outputs, then it has + * starting & trailing `..` and each propId pair is separated + * by `...`, e.g. + * "..output-1.value...output-2.value...output-3.value...output-4.value.." + */ +export const isMultiOutputProp = idAndProp => idAndProp.startsWith('..'); + +const ALL = {wild: 'ALL', multi: 1}; +const ANY = {wild: 'ANY'}; +const ALLSMALLER = {wild: 'ALLSMALLER', multi: 1, expand: 1}; +const wildcards = {ALL, ANY, ALLSMALLER}; + +/* + * If this ID is a wildcard, it is a stringified JSON object + * the "{" character is disallowed from regular string IDs + */ +const isWildcardId = idStr => idStr.startsWith('{'); + +/* + * Turn stringified wildcard IDs into objects. + * Wildcards are encoded as single-item arrays containing the wildcard name + * as a string. + */ +function parseWildcardId(idStr) { + return map( + val => (Array.isArray(val) ? wildcards[val[0]] : val), + JSON.parse(idStr) + ); +} + +/* + * If this update is for multiple outputs, then it has + * starting & trailing `..` and each propId pair is separated + * by `...`, e.g. + * "..output-1.value...output-2.value...output-3.value...output-4.value.." + */ +function parseMultipleOutputs(outputIdAndProp) { + return outputIdAndProp.split('...').map(o => o.replace('..', '')); +} + +export function splitIdAndProp(idAndProp) { + // since wildcard ids can have . in them but props can't, + // look for the last . in the string and split there + const dotPos = idAndProp.lastIndexOf('.'); + const idStr = idAndProp.substr(0, dotPos); + return { + id: parseIfWildcard(idStr), + property: idAndProp.substr(dotPos + 1), + }; +} + +/* + * Check if this ID is a stringified object, and if so parse it to that object + */ +export function parseIfWildcard(idStr) { + return isWildcardId(idStr) ? parseWildcardId(idStr) : idStr; +} + +export const combineIdAndProp = ({id, property}) => + `${stringifyId(id)}.${property}`; + +/* + * JSON.stringify - for the object form - but ensuring keys are sorted + */ +export function stringifyId(id) { + if (typeof id !== 'object') { + return id; + } + const parts = Object.keys(id) + .sort() + .map(k => JSON.stringify(k) + ':' + JSON.stringify(id[k])); + return '{' + parts.join(',') + '}'; +} + +/* + * id dict values can be numbers, strings, and booleans. + * We need a definite ordering that will work across types, + * even if sane users would not mix types. + * - numeric strings are treated as numbers + * - booleans come after numbers, before strings. false, then true. + * - non-numeric strings come last + */ +function idValSort(a, b) { + const bIsNumeric = isNumeric(b); + if (isNumeric(a)) { + if (bIsNumeric) { + const aN = Number(a); + const bN = Number(b); + return aN > bN ? 1 : aN < bN ? -1 : 0; + } + return -1; + } + if (bIsNumeric) { + return 1; + } + const aIsBool = typeof a === 'boolean'; + if (aIsBool !== (typeof b === 'boolean')) { + return aIsBool ? -1 : 1; + } + return a > b ? 1 : a < b ? -1 : 0; +} + +/* + * Provide a value known to be before or after v, according to idValSort + */ +const valBefore = v => (isNumeric(v) ? v - 1 : 0); +const valAfter = v => (typeof v === 'string' ? v + 'z' : 'z'); + +function addMap(depMap, id, prop, dependency) { + const idMap = (depMap[id] = depMap[id] || {}); + const callbacks = (idMap[prop] = idMap[prop] || []); + callbacks.push(dependency); +} + +function addPattern(depMap, idSpec, prop, dependency) { + const keys = Object.keys(idSpec).sort(); + const keyStr = keys.join(','); + const values = props(keys, idSpec); + const keyCallbacks = (depMap[keyStr] = depMap[keyStr] || {}); + const propCallbacks = (keyCallbacks[prop] = keyCallbacks[prop] || []); + let valMatch = false; + for (let i = 0; i < propCallbacks.length; i++) { + if (equals(values, propCallbacks[i].values)) { + valMatch = propCallbacks[i]; + break; + } + } + if (!valMatch) { + valMatch = {keys, values, callbacks: []}; + propCallbacks.push(valMatch); + } + valMatch.callbacks.push(dependency); +} + +export function computeGraphs(dependencies) { + const inputGraph = new DepGraph(); + // multiGraph is just for finding circular deps + const multiGraph = new DepGraph(); + + const wildcardPlaceholders = {}; + + const fixIds = map(evolve({id: parseIfWildcard})); + const parsedDependencies = map( + evolve({inputs: fixIds, state: fixIds}), + dependencies + ); + + /* + * For regular ids, outputMap and inputMap are: + * {[id]: {[prop]: [callback, ...]}} + * where callbacks are the matching specs from the original + * dependenciesRequest, but with outputs parsed to look like inputs, + * and a list anyKeys added if the outputs have ANY wildcards. + * For outputMap there should only ever be one callback per id/prop + * but for inputMap there may be many. + * + * For wildcard ids, outputPatterns and inputPatterns are: + * { + * [keystr]: { + * [prop]: [ + * {keys: [...], values: [...], callbacks: [callback, ...]}, + * {...} + * ] + * } + * } + * keystr is a stringified ordered list of keys in the id + * keys is the same ordered list (just copied for convenience) + * values is an array of explicit or wildcard values for each key in keys + */ + const outputMap = {}; + const inputMap = {}; + const outputPatterns = {}; + const inputPatterns = {}; + + parsedDependencies.forEach(dependency => { + const {output, inputs} = dependency; + const outputStrs = isMultiOutputProp(output) + ? parseMultipleOutputs(output) + : [output]; + const outputs = outputStrs.map(outputStr => { + const outputObj = splitIdAndProp(outputStr); + outputObj.out = true; + return outputObj; + }); + + // TODO: what was this (and exactChange) about??? + // const depWildcardExact = {}; + + outputs.concat(inputs).forEach(item => { + const {id} = item; + if (typeof id === 'object') { + forEachObjIndexed((val, key) => { + if (!wildcardPlaceholders[key]) { + wildcardPlaceholders[key] = { + exact: [], + // exactChange: false, + expand: 0, + }; + } + const keyPlaceholders = wildcardPlaceholders[key]; + if (val && val.wild) { + if (val.expand) { + keyPlaceholders.expand += 1; + } + } else if (keyPlaceholders.exact.indexOf(val) === -1) { + keyPlaceholders.exact.push(val); + // if (depWildcardExact[key]) { + // if (depWildcardExact[key] !== val) { + // keyPlaceholders.exactChange = true; + // } + // } + // else { + // depWildcardExact[key] = val; + // } + } + }, id); + } + }); + }); + + forEachObjIndexed(keyPlaceholders => { + const {exact, expand} = keyPlaceholders; + const vals = exact.slice().sort(idValSort); + if (expand) { + for (let i = 0; i < expand; i++) { + if (exact.length) { + vals.splice(0, 0, [valBefore(vals[0])]); + vals.push(valAfter(vals[vals.length - 1])); + } else { + vals.push(i); + } + } + } else if (!exact.length) { + // only ANY/ALL - still need a value + vals.push(0); + } + keyPlaceholders.vals = vals; + }, wildcardPlaceholders); + + function makeAllIds(idSpec, outIdFinal) { + let idList = [{}]; + forEachObjIndexed((val, key) => { + const testVals = wildcardPlaceholders[key].vals; + const outValIndex = testVals.indexOf(outIdFinal[key]); + let newVals = [val]; + if (val && val.wild) { + if (val === ALLSMALLER) { + if (outValIndex > 0) { + newVals = testVals.slice(0, outValIndex); + } else { + // no smaller items - delete all outputs. + newVals = []; + } + } else { + // ANY or ALL + // ANY *is* ALL for outputs, ie we don't already have a + // value specified in `outIdFinal` + newVals = + outValIndex === -1 || val === ALL + ? testVals + : [outIdFinal[key]]; + } + } + // replicates everything in idList once for each item in + // newVals, attaching each value at key. + idList = ap(ap([assoc(key)], newVals), idList); + }, idSpec); + return idList; + } + + parsedDependencies.forEach(function registerDependency(dependency) { + const {output, inputs} = dependency; + + // multiGraph - just for testing circularity + + function addInputToMulti(inIdProp, outIdProp) { + multiGraph.addNode(inIdProp); + multiGraph.addDependency(inIdProp, outIdProp); + } + + function addOutputToMulti(outIdFinal, outIdProp) { + multiGraph.addNode(outIdProp); + inputs.forEach(inObj => { + const {id: inId, property} = inObj; + if (typeof inId === 'object') { + const inIdList = makeAllIds(inId, outIdFinal); + inIdList.forEach(id => { + addInputToMulti( + combineIdAndProp({id, property}), + outIdProp + ); + }); + } else { + addInputToMulti(combineIdAndProp(inObj), outIdProp); + } + }); + } + + const outStrs = isMultiOutputProp(output) + ? parseMultipleOutputs(output) + : [output]; + + const outputs = outStrs.map(splitIdAndProp); + + // We'll continue to use dep.output as its id, but add outputs as well + // for convenience and symmetry with the structure of inputs and state. + // Also collect ANY keys in the output (all outputs must share these) + // and ALL keys in the first output (need not be shared but we'll use + // the first output for calculations) for later convenience. + const anyKeys = []; + let hasAll = false; + forEachObjIndexed((val, key) => { + if (val === ANY) { + anyKeys.push(key); + } else if (val === ALL) { + hasAll = true; + } + }, outputs[0].id); + anyKeys.sort(); + const finalDependency = mergeRight( + {hasAll, anyKeys, outputs}, + dependency + ); + + outputs.forEach(({id: outId, property}) => { + if (typeof outId === 'object') { + const outIdList = makeAllIds(outId, {}); + outIdList.forEach(id => { + addOutputToMulti(id, combineIdAndProp({id, property})); + }); + + addPattern(outputPatterns, outId, property, finalDependency); + } else { + addOutputToMulti({}, outId); + addMap(outputMap, outId, property, finalDependency); + } + }); + + inputs.forEach(inputObject => { + const {id: inId, property: inProp} = inputObject; + if (typeof inId === 'object') { + addPattern(inputPatterns, inId, inProp, finalDependency); + } else { + addMap(inputMap, inId, inProp, finalDependency); + // inputGraph - this is the one we'll use for dispatching updates + // TODO: get rid of this, use the precalculated mappings + const inputId = combineIdAndProp(inputObject); + inputGraph.addNode(output); + inputGraph.addNode(inputId); + inputGraph.addDependency(inputId, output); + } + }); + }); + + return { + InputGraph: inputGraph, + MultiGraph: multiGraph, + outputMap, + inputMap, + outputPatterns, + inputPatterns, + }; +} + +/* + * Do the given id values `vals` match the pattern `patternVals`? + * `keys`, `patternVals`, and `vals` are all arrays, and we already know that + * we're only looking at ids with the same keys as the pattern. + * + * Optionally, include another reference set of the same - to ensure the + * correct matching of ANY or ALLSMALLER between input and output items. + */ +function idMatch(keys, vals, patternVals, refKeys, refVals, refPatternVals) { + for (let i = 0; i < keys.length; i++) { + const val = vals[i]; + const patternVal = patternVals[i]; + if (patternVal.wild) { + // If we have a second id, compare the wildcard values. + // Without a second id, all wildcards pass at this stage. + if (refKeys && patternVal !== ALL) { + const refIndex = refKeys.indexOf(keys[i]); + const refPatternVal = refPatternVals[refIndex]; + // Sanity check. Shouldn't ever fail this, if the back end + // did its job validating callbacks. + // You can't resolve an input against an input, because + // two ALLSMALLER's wouldn't make sense! + if (patternVal === ALLSMALLER && refPatternVal === ALLSMALLER) { + throw new Error( + 'invalid wildcard id pair: ' + + JSON.stringify({ + keys, + patternVals, + vals, + refKeys, + refPatternVals, + refVals, + }) + ); + } + if ( + idValSort(val, refVals[refIndex]) !== + (patternVal === ALLSMALLER + ? -1 + : refPatternVal === ALLSMALLER + ? 1 + : 0) + ) { + return false; + } + } + } else if (val !== patternVal) { + return false; + } + } + return true; +} + +function getAnyVals(patternVals, vals) { + const matches = []; + for (let i = 0; i < patternVals.length; i++) { + if (patternVals[i] === ANY) { + matches.push(vals[i]); + } + } + return matches.length ? JSON.stringify(matches) : ''; +} + +const resolveDeps = (refKeys, refVals, refPatternVals) => paths => ({ + id: idPattern, + property, +}) => { + if (typeof idPattern === 'string') { + const path = getPath(paths, idPattern); + return path ? [{id: idPattern, property, path}] : []; + } + const keys = Object.keys(idPattern).sort(); + const patternVals = props(keys, idPattern); + const keyStr = keys.join(','); + const keyPaths = paths.objs[keyStr]; + if (!keyPaths) { + return []; + } + const result = []; + keyPaths.forEach(({values: vals, path}) => { + if ( + idMatch(keys, vals, patternVals, refKeys, refVals, refPatternVals) + ) { + result.push({id: zipObj(keys, vals), property, path}); + } + }); + return result; +}; + +/* + * Create a pending callback object. Includes the original callback definition, + * its resolved ID (including the value of all ANY wildcards), + * accessors to find all inputs, outputs, and state involved in this + * callback (lazy as not all users will want all of these), + * placeholders for which other callbacks this one is blockedBy or blocking, + * and a boolean for whether it has been dispatched yet. + */ +const makeResolvedCallback = (callback, resolve, anyVals) => ({ + callback, + anyVals, + resolvedId: callback.output + anyVals, + getOutputs: paths => callback.outputs.map(resolve(paths)), + getInputs: paths => callback.inputs.map(resolve(paths)), + getState: paths => callback.state.map(resolve(paths)), + blockedBy: {}, + blocking: {}, + changedPropIds: {}, + initialCall: false, + requestId: 0, + requestedOutputs: {}, +}); + +let nextRequestId = 0; + +/* + * Give a callback a new requestId. + */ +export function setNewRequestId(callback) { + nextRequestId++; + return assoc('requestId', nextRequestId, callback); +} + +/* + * Does this item (input / output / state) support multiple values? + * string IDs do not; wildcard IDs only do if they contain ALL or ALLSMALLER + */ +export function isMultiValued({id}) { + return typeof id === 'object' && any(v => v.multi, values(id)); +} + +/* + * For a given output id and prop, find the callback generating it. + * If no callback is found, returns false. + * If one is found, returns: + * { + * callback: the callback spec {outputs, inputs, state etc} + * anyVals: stringified list of resolved ANY keys we matched + * resolvedId: the "outputs" id string plus ANY values we matched + * getOutputs: accessor function to give all resolved outputs of this + * callback. Takes `paths` as argument to apply when the callback is + * dispatched, in case a previous callback has altered the layout. + * The result is a list of {id (string or object), property (string)} + * getInputs: same for inputs + * getState: same for state + * blockedBy: an object of {[resolvedId]: 1} blocking this callback + * blocking: an object of {[resolvedId]: 1} this callback is blocking + * changedPropIds: an object of {[idAndProp]: 1} triggering this callback + * initialCall: boolean, if true we don't require any changedPropIds + * to keep this callback around, as it's the initial call to populate + * this value on page load or changing part of the layout. + * By default this is true for callbacks generated by + * getCallbackByOutput, false from getCallbacksByInput. + * requestId: integer: starts at 0. when this callback is dispatched it will + * get a unique requestId, but if it gets added again the requestId will + * be reset to 0, and we'll know to ignore the response of the first + * request. + * requestedOutputs: object of {[idStr]: [props]} listing all the props + * actually requested for update. + * } + */ +function getCallbackByOutput(graphs, paths, id, prop) { + let resolve; + let callback; + let anyVals = ''; + if (typeof id === 'string') { + // standard id version + const callbacks = (graphs.outputMap[id] || {})[prop]; + if (callbacks) { + callback = callbacks[0]; + resolve = resolveDeps(); + } + } else { + // wildcard version + const keys = Object.keys(id).sort(); + const vals = props(keys, id); + const keyStr = keys.join(','); + const patterns = (graphs.outputPatterns[keyStr] || {})[prop]; + if (patterns) { + for (let i = 0; i < patterns.length; i++) { + const patternVals = patterns[i].values; + if (idMatch(keys, vals, patternVals)) { + callback = patterns[i].callbacks[0]; + resolve = resolveDeps(keys, vals, patternVals); + anyVals = getAnyVals(patternVals, vals); + break; + } + } + } + } + if (!resolve) { + return false; + } + + return makeResolvedCallback(callback, resolve, anyVals); +} + +/* + * If there are ALL keys we need to reduce a set of outputs resolved + * from an input to one item per combination of ANY values. + * That will give one result per callback invocation. + */ +function reduceALLOuts(outs, anyKeys, hasAll) { + if (!hasAll) { + return outs; + } + if (!anyKeys.length) { + // If there's ALL but no ANY, there's only one invocation + // of the callback, so just base it off the first output. + return [outs[0]]; + } + const anySeen = {}; + return outs.filter(i => { + const matchKeys = JSON.stringify(props(anyKeys, i.id)); + if (!anySeen[matchKeys]) { + anySeen[matchKeys] = 1; + return true; + } + return false; + }); +} + +function addResolvedFromOutputs(callback, outPattern, outs, matches) { + const out0Keys = Object.keys(outPattern.id).sort(); + const out0PatternVals = props(out0Keys, outPattern.id); + outs.forEach(({id: outId}) => { + const outVals = props(out0Keys, outId); + matches.push( + makeResolvedCallback( + callback, + resolveDeps(out0Keys, outVals, out0PatternVals), + getAnyVals(out0PatternVals, outVals) + ) + ); + }); +} + +/* + * For a given id and prop find all callbacks it's an input of. + * + * Returns an array of objects: + * {callback, resolvedId, getOutputs, getInputs, getState} + * See getCallbackByOutput for details. + * + * Note that if the original input contains an ALLSMALLER wildcard, + * there may be many entries for the same callback, but any given output + * (with an ANY corresponding to the input's ALLSMALLER) will only appear + * in one entry. + */ +export function getCallbacksByInput(graphs, paths, id, prop) { + const matches = []; + const idAndProp = combineIdAndProp({id, property: prop}); + + if (typeof id === 'string') { + // standard id version + const callbacks = (graphs.inputMap[id] || {})[prop]; + if (!callbacks) { + return []; + } + + const baseResolve = resolveDeps(); + callbacks.forEach(callback => { + const {anyKeys, hasALL} = callback; + if (anyKeys) { + const out0Pattern = callback.outputs[0]; + const out0Set = reduceALLOuts( + baseResolve(paths)(out0Pattern), + anyKeys, + hasALL + ); + addResolvedFromOutputs(callback, out0Pattern, out0Set, matches); + } else { + matches.push(makeResolvedCallback(callback, baseResolve, '')); + } + }); + } else { + // wildcard version + const keys = Object.keys(id).sort(); + const vals = props(keys, id); + const keyStr = keys.join(','); + const patterns = (graphs.inputPatterns[keyStr] || {})[prop]; + if (!patterns) { + return []; + } + patterns.forEach(pattern => { + if (idMatch(keys, vals, pattern.values)) { + const resolve = resolveDeps(keys, vals, pattern.values); + pattern.callbacks.forEach(callback => { + const out0Pattern = callback.outputs[0]; + const {anyKeys, hasALL} = callback; + const out0Set = reduceALLOuts( + resolve(paths)(out0Pattern), + anyKeys, + hasALL + ); + + addResolvedFromOutputs( + callback, + out0Pattern, + out0Set, + matches + ); + }); + } + }); + } + matches.forEach(match => { + match.changedPropIds[idAndProp] = 1; + }); + return matches; +} + +export function getWatchedKeys(id, newProps, graphs) { + if (!(id && graphs && newProps.length)) { + return []; + } + + if (typeof id === 'string') { + const inputs = graphs.inputMap[id]; + return inputs ? newProps.filter(newProp => inputs[newProp]) : []; + } + + const keys = Object.keys(id).sort(); + const vals = props(keys, id); + const keyStr = keys.join(','); + const keyPatterns = graphs.inputPatterns[keyStr]; + if (!keyPatterns) { + return []; + } + return newProps.filter(prop => { + const patterns = keyPatterns[prop]; + return ( + patterns && + patterns.some(pattern => idMatch(keys, vals, pattern.values)) + ); + }); +} + +/* + * Return a list of all callbacks referencing a chunk of the layout, + * either as inputs or outputs. + * + * opts.outputsOnly: boolean, set true when crawling the *whole* layout, + * because outputs are enough to get everything. + * + * Returns an array of objects: + * {callback, resolvedId, getOutputs, getInputs, getState, ...etc} + * See getCallbackByOutput for details. + */ +export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { + const {outputsOnly} = opts || {}; + const foundCbIds = {}; + const callbacks = []; + + function addCallback(callback) { + if (callback) { + const foundIndex = foundCbIds[callback.resolvedId]; + if (foundIndex !== undefined) { + callbacks[foundIndex].changedPropIds = mergeRight( + callbacks[foundIndex].changedPropIds, + callback.changedPropIds + ); + } else { + foundCbIds[callback.resolvedId] = callbacks.length; + callbacks.push(callback); + } + } + } + + function handleOneId(id, outIdCallbacks, inIdCallbacks) { + if (outIdCallbacks) { + for (const property in outIdCallbacks) { + const cb = getCallbackByOutput(graphs, paths, id, property); + // callbacks found in the layout by output should always run, + // ie this is the initial call of this callback even if it's + // not the page initialization but just a new layout chunk + cb.initialCall = true; + addCallback(cb); + } + } + if (!outputsOnly && inIdCallbacks) { + for (const property in inIdCallbacks) { + getCallbacksByInput(graphs, paths, id, property).forEach( + addCallback + ); + } + } + } + + crawlLayout(layoutChunk, child => { + const id = path(['props', 'id'], child); + if (id) { + if (typeof id === 'string') { + handleOneId(id, graphs.outputMap[id], graphs.inputMap[id]); + } else { + const keyStr = Object.keys(id) + .sort() + .join(','); + handleOneId( + id, + graphs.outputPatterns[keyStr], + graphs.inputPatterns[keyStr] + ); + } + } + }); + + // We still need to follow these forward in order to capture blocks and, + // if based on a partial layout, any knock-on effects in the full layout. + return followForward(graphs, paths, callbacks); +} + +export function removePendingCallback( + pendingCallbacks, + paths, + removeResolvedId, + skippedProps +) { + const finalPendingCallbacks = []; + pendingCallbacks.forEach(pending => { + const {blockedBy, blocking, changedPropIds, resolvedId} = pending; + if (resolvedId !== removeResolvedId) { + finalPendingCallbacks.push( + mergeRight(pending, { + blockedBy: dissoc(removeResolvedId, blockedBy), + blocking: dissoc(removeResolvedId, blocking), + changedPropIds: omit(skippedProps, changedPropIds), + }) + ); + } + }); + // If any callback no longer has any changed inputs, it shouldn't fire. + // This will repeat recursively until all unneeded callbacks are pruned + if (skippedProps.length) { + for (let i = 0; i < finalPendingCallbacks.length; i++) { + const cb = finalPendingCallbacks[i]; + if (!cb.initialCall && isEmpty(cb.changedPropIds)) { + return removePendingCallback( + finalPendingCallbacks, + paths, + cb.resolvedId, + flatten(cb.getOutputs(paths)).map(combineIdAndProp) + ); + } + } + } + return finalPendingCallbacks; +} + +/* + * Split the list of pending callbacks into ready (not blocked by any others) + * and blocked. Sort the ready callbacks by how many each is blocking, on the + * theory that the most important ones to dispatch are the ones with the most + * others depending on them. + */ +export function findReadyCallbacks(pendingCallbacks) { + const [readyCallbacks, blockedCallbacks] = partition( + pending => isEmpty(pending.blockedBy) && !pending.requestId, + pendingCallbacks + ); + readyCallbacks.sort((a, b) => { + return Object.keys(b.blocking).length - Object.keys(a.blocking).length; + }); + + return {readyCallbacks, blockedCallbacks}; +} + +function addBlock(callbacks, blockingId, blockedId) { + callbacks.forEach(({blockedBy, blocking, resolvedId}) => { + if (resolvedId === blockingId || blocking[blockingId]) { + blocking[blockedId] = 1; + } else if (resolvedId === blockedId || blockedBy[blockedId]) { + blockedBy[blockingId] = 1; + } + }); +} + +function collectIds(callbacks) { + const allResolvedIds = {}; + callbacks.forEach(({resolvedId}, i) => { + allResolvedIds[resolvedId] = i; + }); + return allResolvedIds; +} + +/* + * Take a list of callbacks and follow them all forward, ie see if any of their + * outputs are inputs of another callback. Any new callbacks get added to the + * list. All that come after another get marked as blocked by that one, whether + * they were in the initial list or not. + */ +export function followForward(graphs, paths, callbacks_) { + const callbacks = clone(callbacks_); + const allResolvedIds = collectIds(callbacks); + let i; + let callback; + + const followOutput = ({id, property}) => { + const nextCBs = getCallbacksByInput(graphs, paths, id, property); + nextCBs.forEach(nextCB => { + let existingIndex = allResolvedIds[nextCB.resolvedId]; + if (existingIndex === undefined) { + existingIndex = callbacks.length; + callbacks.push(nextCB); + allResolvedIds[nextCB.resolvedId] = existingIndex; + } else { + const existingCB = callbacks[existingIndex]; + existingCB.changedPropIds = mergeRight( + existingCB.changedPropIds, + nextCB.changedPropIds + ); + } + addBlock(callbacks, callback.resolvedId, nextCB.resolvedId); + }); + }; + + // Using a for loop instead of forEach because followOutput may extend the + // callbacks array, and we want to continue into these new elements. + for (i = 0; i < callbacks.length; i++) { + callback = callbacks[i]; + const outputs = unnest(callback.getOutputs(paths)); + outputs.forEach(followOutput); + } + return callbacks; +} + +function mergeAllBlockers(cb1, cb2) { + function mergeBlockers(a, b) { + if (cb1[a][cb2.resolvedId] && !cb2[b][cb1.resolvedId]) { + cb2[b] = mergeRight({[cb1.resolvedId]: 1}, cb1[b], cb2[b]); + cb1[a] = mergeRight({[cb2.resolvedId]: 1}, cb2[a], cb1[b]); + } + } + mergeBlockers('blockedBy', 'blocking'); + mergeBlockers('blocking', 'blockedBy'); +} + +/* + * Given two arrays of pending callbacks, merge them into one so that + * each will only fire once, and any extra blockages from combining the lists + * will be accounted for. + */ +export function mergePendingCallbacks(cb1, cb2) { + if (!cb2.length) { + return cb1; + } + if (!cb1.length) { + return cb2; + } + const finalCallbacks = clone(cb1); + const callbacks2 = clone(cb2); + const allResolvedIds = collectIds(finalCallbacks); + + callbacks2.forEach((callback, i) => { + const existingIndex = allResolvedIds[callback.resolvedId]; + if (existingIndex !== undefined) { + finalCallbacks.forEach(finalCb => { + mergeAllBlockers(finalCb, callback); + }); + callbacks2.slice(i + 1).forEach(cb2 => { + mergeAllBlockers(cb2, callback); + }); + finalCallbacks[existingIndex] = mergeDeepRight( + finalCallbacks[existingIndex], + callback + ); + } else { + allResolvedIds[callback.resolvedId] = finalCallbacks.length; + finalCallbacks.push(callback); + } + }); + + return finalCallbacks; +} diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index fc582775a7..bf108a56df 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -1,47 +1,56 @@ /* global fetch:true, Promise:true, document:true */ import { - adjust, - any, - append, + assoc, concat, - findIndex, - findLastIndex, flatten, - flip, has, - includes, - intersection, - isEmpty, keys, lensPath, - mergeLeft, + map, mergeDeepRight, once, path, + pick, + pickBy, pluck, propEq, - reject, - slice, - sort, type, uniq, view, + without, + zip, } from 'ramda'; import {createAction} from 'redux-actions'; -import {crawlLayout, hasId} from '../reducers/utils'; import {getAppState} from '../reducers/constants'; import {getAction} from './constants'; import cookie from 'cookie'; -import {uid, urlBase, isMultiOutputProp, parseMultipleOutputs} from '../utils'; +import {urlBase} from './utils'; +import { + combineIdAndProp, + findReadyCallbacks, + followForward, + getCallbacksByInput, + getCallbacksInLayout, + isMultiOutputProp, + isMultiValued, + mergePendingCallbacks, + removePendingCallback, + parseIfWildcard, + setNewRequestId, + splitIdAndProp, + stringifyId, +} from './dependencies'; +import {computePaths, getPath} from './paths'; import {STATUS} from '../constants/constants'; import {applyPersistence, prunePersistence} from '../persistence'; import isAppReady from './isAppReady'; export const updateProps = createAction(getAction('ON_PROP_CHANGE')); +export const setPendingCallbacks = createAction('SET_PENDING_CALLBACKS'); export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE')); -export const computeGraphs = createAction(getAction('COMPUTE_GRAPHS')); -export const computePaths = createAction(getAction('COMPUTE_PATHS')); +export const setGraphs = createAction(getAction('SET_GRAPHS')); +export const setPaths = createAction(getAction('SET_PATHS')); export const setAppLifecycle = createAction(getAction('SET_APP_LIFECYCLE')); export const setConfig = createAction(getAction('SET_CONFIG')); export const setHooks = createAction(getAction('SET_HOOKS')); @@ -70,13 +79,11 @@ export function getCSRFHeader() { } function triggerDefaultState(dispatch, getState) { - const {graphs} = getState(); - const {InputGraph, MultiGraph} = graphs; - const allNodes = InputGraph.overallOrder(); - // overallOrder will assert circular dependencies for multi output. + const {graphs, paths, layout} = getState(); + // overallOrder will assert circular dependencies for multi output. try { - MultiGraph.overallOrder(); + graphs.MultiGraph.overallOrder(); } catch (err) { dispatch( onError({ @@ -89,939 +96,490 @@ function triggerDefaultState(dispatch, getState) { ); } - const inputNodeIds = []; - allNodes.reverse(); - allNodes.forEach(nodeId => { - const componentId = nodeId.split('.')[0]; - /* - * Filter out the outputs, - * inputs that aren't leaves, - * and the invisible inputs - */ - if ( - InputGraph.dependenciesOf(nodeId).length > 0 && - InputGraph.dependantsOf(nodeId).length === 0 && - has(componentId, getState().paths) - ) { - inputNodeIds.push(nodeId); - } + const initialCallbacks = getCallbacksInLayout(graphs, paths, layout, { + outputsOnly: true, }); - - reduceInputIds(inputNodeIds, InputGraph).forEach(inputOutput => { - const [componentId, componentProp] = inputOutput.input.split('.'); - // Get the initial property - const propLens = lensPath( - concat(getState().paths[componentId], ['props', componentProp]) - ); - const propValue = view(propLens, getState().layout); - - dispatch( - notifyObservers({ - id: componentId, - props: {[componentProp]: propValue}, - excludedOutputs: inputOutput.excludedOutputs, - }) - ); - }); -} - -export function redo() { - return function(dispatch, getState) { - const history = getState().history; - dispatch(createAction('REDO')()); - const next = history.future[0]; - - // Update props - dispatch( - createAction('REDO_PROP_CHANGE')({ - itempath: getState().paths[next.id], - props: next.props, - }) - ); - - // Notify observers - dispatch( - notifyObservers({ - id: next.id, - props: next.props, - }) - ); - }; + dispatch(startCallbacks(initialCallbacks)); } -const UNDO = createAction('UNDO')(); -export function undo() { - return undo_revert(UNDO); -} +export const redo = move_history('REDO'); +export const undo = move_history('UNDO'); +export const revert = move_history('REVERT'); -const REVERT = createAction('REVERT')(); -export function revert() { - return undo_revert(REVERT); -} - -function undo_revert(undo_or_revert) { +function move_history(changeType) { return function(dispatch, getState) { - const history = getState().history; - dispatch(undo_or_revert); - const previous = history.past[history.past.length - 1]; + const {history, paths} = getState(); + dispatch(createAction(changeType)()); + const {id, props} = + changeType === 'REDO' + ? history.future[0] + : history.past[history.past.length - 1]; // Update props dispatch( createAction('UNDO_PROP_CHANGE')({ - itempath: getState().paths[previous.id], - props: previous.props, + itempath: getPath(paths, id), + props, }) ); // Notify observers - dispatch( - notifyObservers({ - id: previous.id, - props: previous.props, - }) - ); + dispatch(notifyObservers({id, props})); }; } -function reduceInputIds(nodeIds, InputGraph) { - /* - * Create input-output(s) pairs, - * sort by number of outputs, - * and remove redundant inputs (inputs that update the same output) - */ - const inputOutputPairs = nodeIds.map(nodeId => ({ - input: nodeId, - // TODO - Does this include grandchildren? - outputs: InputGraph.dependenciesOf(nodeId), - excludedOutputs: [], - })); - - const sortedInputOutputPairs = sort( - (a, b) => b.outputs.length - a.outputs.length, - inputOutputPairs - ); - - /* - * In some cases, we may have unique outputs but inputs that could - * trigger components to update multiple times. - * - * For example, [A, B] => C and [A, D] => E - * The unique inputs might be [A, B, D] but that is redundant. - * We only need to update B and D or just A. - * - * In these cases, we'll supply an additional list of outputs - * to exclude. - */ - sortedInputOutputPairs.forEach((pair, i) => { - const outputsThatWillBeUpdated = flatten( - pluck('outputs', slice(0, i, sortedInputOutputPairs)) - ); - pair.outputs.forEach(output => { - if (includes(output, outputsThatWillBeUpdated)) { - pair.excludedOutputs.push(output); +function unwrapIfNotMulti(paths, idProps, spec, anyVals, depType) { + if (isMultiValued(spec)) { + return idProps; + } + if (idProps.length !== 1) { + if (!idProps.length) { + if (typeof spec.id === 'string') { + throw new ReferenceError( + 'A nonexistent object was used in an `' + + depType + + '` of a Dash callback. The id of this object is `' + + spec.id + + '` and the property is `' + + spec.property + + '`. The string ids in the current layout are: [' + + keys(paths.strs).join(', ') + + ']' + ); } - }); - }); - - return sortedInputOutputPairs; + // TODO: unwrapped list of wildcard ids? + // eslint-disable-next-line no-console + console.log(paths.objs); + throw new ReferenceError( + 'A nonexistent object was used in an `' + + depType + + '` of a Dash callback. The id of this object is ' + + JSON.stringify(spec.id) + + (anyVals ? ' with ANY values ' + anyVals : '') + + ' and the property is `' + + spec.property + + '`. The wildcard ids currently available are logged above.' + ); + } + throw new ReferenceError( + 'Multiple objects were found for an `' + + depType + + '` of a callback that only takes one value. The id spec is ' + + JSON.stringify(spec.id) + + (anyVals ? ' with ANY values ' + anyVals : '') + + ' and the property is `' + + spec.property + + '`. The objects we found are: ' + + JSON.stringify(map(pick(['id', 'property']), idProps)) + ); + } + return idProps[0]; } -export function notifyObservers(payload) { +export function startCallbacks(callbacks) { return async function(dispatch, getState) { - const {id, props, excludedOutputs} = payload; - - const { - dependenciesRequest, - graphs, - layout, - paths, - requestQueue, - } = getState(); - - const {InputGraph} = graphs; - /* - * Figure out all of the output id's that depend on this input. - * This includes id's that are direct children as well as - * grandchildren. - * grandchildren will get filtered out in a later stage. - */ - let outputObservers = []; + return await fireReadyCallbacks(dispatch, getState, callbacks); + }; +} - const changedProps = keys(props); - changedProps.forEach(propName => { - const node = `${id}.${propName}`; - if (!InputGraph.hasNode(node)) { - return; - } - InputGraph.dependenciesOf(node).forEach(outputId => { - /* - * Multiple input properties that update the same - * output can change at once. - * For example, `n_clicks` and `n_clicks_previous` - * on a button component. - * We only need to update the output once for this - * update, so keep outputObservers unique. - */ - if (!includes(outputId, outputObservers)) { - outputObservers.push(outputId); - } - }); +async function fireReadyCallbacks(dispatch, getState, callbacks) { + const {readyCallbacks, blockedCallbacks} = findReadyCallbacks(callbacks); + const {config, hooks, layout, paths} = getState(); + + // We want to calculate all the outputs only once, but we need them + // for pendingCallbacks which we're going to dispatch prior to + // initiating the queue. So first loop over readyCallbacks to + // generate the output lists, then dispatch pendingCallbacks, + // then loop again to fire off the requests. + const outputStash = {}; + const requestedCallbacks = readyCallbacks.map(cb => { + const cbOut = setNewRequestId(cb); + + const {requestId, getOutputs} = cbOut; + const allOutputs = getOutputs(paths); + const flatOutputs = flatten(allOutputs); + const allPropIds = []; + + const reqOut = {}; + flatOutputs.forEach(({id, property}) => { + const idStr = stringifyId(id); + const idOut = (reqOut[idStr] = reqOut[idStr] || []); + idOut.push(property); + allPropIds.push(combineIdAndProp({id: idStr, property})); }); + cbOut.requestedOutputs = reqOut; - if (excludedOutputs) { - outputObservers = reject( - flip(includes)(excludedOutputs), - outputObservers - ); - } + outputStash[requestId] = {allOutputs, allPropIds}; - if (isEmpty(outputObservers)) { - return; - } + return cbOut; + }); - /* - * There may be several components that depend on this input. - * And some components may depend on other components before - * updating. Get this update order straightened out. - */ - const depOrder = InputGraph.overallOrder(); - outputObservers = sort( - (a, b) => depOrder.indexOf(b) - depOrder.indexOf(a), - outputObservers - ); - const queuedObservers = []; - outputObservers.forEach(function filterObservers(outputIdAndProp) { - let outputIds; - if (isMultiOutputProp(outputIdAndProp)) { - outputIds = parseMultipleOutputs(outputIdAndProp).map( - e => e.split('.')[0] - ); - } else { - outputIds = [outputIdAndProp.split('.')[0]]; - } + const ids = uniq( + pluck( + 'id', + flatten( + requestedCallbacks.map(cb => + concat(cb.getInputs(paths), cb.getState(paths)) + ) + ) + ) + ); - /* - * before we make the POST to update the output, check - * that the output doesn't depend on any other inputs that - * that depend on the same controller. - * if the output has another input with a shared controller, - * then don't update this output yet. - * when each dependency updates, it'll dispatch its own - * `notifyObservers` action which will allow this - * component to update. - * - * for example, if A updates B and C (A -> [B, C]) and B updates C - * (B -> C), then when A updates, this logic will - * reject C from the queue since it will end up getting updated - * by B. - * - * in this case, B will already be in queuedObservers by the time - * this loop hits C because of the overallOrder sorting logic - */ - - const controllers = InputGraph.dependantsOf(outputIdAndProp); - - const controllersInFutureQueue = intersection( - queuedObservers, - controllers - ); + await isAppReady(layout, paths, ids); - /* - * check that the output hasn't been triggered to update already - * by a different input. - * - * for example: - * Grandparent -> [Parent A, Parent B] -> Child - * - * when Grandparent changes, it will trigger Parent A and Parent B - * to each update Child. - * one of the components (Parent A or Parent B) will queue up - * the change for Child. if this update has already been queued up, - * then skip the update for the other component - */ - const controllerIsInExistingQueue = any( - r => - includes(r.controllerId, controllers) && - r.status === 'loading', - requestQueue - ); + const allCallbacks = concat(requestedCallbacks, blockedCallbacks); + dispatch(setPendingCallbacks(allCallbacks)); - /* - * TODO - Place throttling logic here? - * - * Only process the last two requests for a _single_ output - * at a time. - * - * For example, if A -> B, and A is changed 10 times, then: - * 1 - processing the first two requests - * 2 - if more than 2 requests come in while the first two - * are being processed, then skip updating all of the - * requests except for the last 2 - */ - - /* - * also check that this observer is actually in the current - * component tree. - * observers don't actually need to be rendered at the moment - * of a controller change. - * for example, perhaps the user has hidden one of the observers - */ - - if ( - controllersInFutureQueue.length === 0 && - any(e => has(e, getState().paths))(outputIds) && - !controllerIsInExistingQueue - ) { - queuedObservers.push(outputIdAndProp); - } - }); - - /** - * Determine the id of all components used as input or state in the callbacks - * triggered by the props change. - * - * Wait for all components associated to these ids to be ready before initiating - * the callbacks. - */ - const deps = queuedObservers.map(output => - dependenciesRequest.content.find( - dependency => dependency.output === output - ) + function fireNext() { + return fireReadyCallbacks( + dispatch, + getState, + getState().pendingCallbacks ); + } - const ids = uniq( - flatten( - deps.map(dep => [ - dep.inputs.map(input => input.id), - dep.state.map(state => state.id), - ]) + let hasClientSide = false; + + const queue = requestedCallbacks.map(cb => { + const {output, inputs, state, clientside_function} = cb.callback; + const {requestId, resolvedId} = cb; + const {allOutputs, allPropIds} = outputStash[requestId]; + const outputs = allOutputs.map((out, i) => + unwrapIfNotMulti( + paths, + map(pick(['id', 'property']), out), + cb.callback.outputs[i], + cb.anyVals, + 'Output' ) ); - await isAppReady(layout, paths, ids); + const payload = { + output, + outputs: isMultiOutputProp(output) ? outputs : outputs[0], + inputs: fillVals(paths, layout, cb, inputs, 'Input'), + changedPropIds: keys(cb.changedPropIds), + }; + if (cb.callback.state.length) { + payload.state = fillVals(paths, layout, cb, state, 'State'); + } - /* - * record the set of output IDs that will eventually need to be - * updated in a queue. not all of these requests will be fired in this - * action - */ - const newRequestQueue = queuedObservers.map(i => ({ - controllerId: i, - status: 'loading', - uid: uid(), - requestTime: Date.now(), - })); - dispatch(setRequestQueue(concat(requestQueue, newRequestQueue))); - - const promises = []; - for (let i = 0; i < queuedObservers.length; i++) { - const outputIdAndProp = queuedObservers[i]; - const requestUid = newRequestQueue[i].uid; - - promises.push( - updateOutput( - outputIdAndProp, - getState, - requestUid, - dispatch, - changedProps.map(prop => `${id}.${prop}`) - ) + function updatePending(pendingCallbacks, skippedProps) { + const newPending = removePendingCallback( + pendingCallbacks, + getState().paths, + resolvedId, + skippedProps ); + dispatch(setPendingCallbacks(newPending)); } - /* eslint-disable consistent-return */ - return Promise.all(promises); - /* eslint-enable consistent-return */ - }; -} - -function updateOutput( - outputIdAndProp, - getState, - requestUid, - dispatch, - changedPropIds -) { - const {config, layout, graphs, dependenciesRequest, hooks} = getState(); - const {InputGraph} = graphs; + function handleData(data) { + let {pendingCallbacks} = getState(); + if (!requestIsActive(pendingCallbacks, resolvedId, requestId)) { + return; + } + const updated = []; + Object.entries(data).forEach(([id, props]) => { + const parsedId = parseIfWildcard(id); - const getThisRequestIndex = () => { - const postRequestQueue = getState().requestQueue; - const thisRequestIndex = findIndex( - propEq('uid', requestUid), - postRequestQueue - ); - return thisRequestIndex; - }; + const appliedProps = doUpdateProps( + dispatch, + getState, + parsedId, + props + ); + if (appliedProps) { + Object.keys(appliedProps).forEach(property => { + updated.push(combineIdAndProp({id, property})); + }); - const updateRequestQueue = (rejected, status) => { - const postRequestQueue = getState().requestQueue; - const thisRequestIndex = getThisRequestIndex(); - if (thisRequestIndex === -1) { - // It was already pruned away - return; + if (has('children', appliedProps)) { + // If components changed, need to update paths, + // check if all pending callbacks are still + // valid, and add all callbacks associated with + // new components, either as inputs or outputs + pendingCallbacks = updateChildPaths( + dispatch, + getState, + pendingCallbacks, + parsedId, + appliedProps.children + ); + } + } + }); + updatePending(pendingCallbacks, without(updated, allPropIds)); } - const updatedQueue = adjust( - thisRequestIndex, - mergeLeft({ - status: status, - responseTime: Date.now(), - rejected, - }), - postRequestQueue - ); - // We don't need to store any requests before this one - const thisControllerId = - postRequestQueue[thisRequestIndex].controllerId; - const prunedQueue = updatedQueue.filter((queueItem, index) => { - return ( - queueItem.controllerId !== thisControllerId || - index >= thisRequestIndex - ); - }); - - dispatch(setRequestQueue(prunedQueue)); - }; - - /* - * Construct a payload of the input and state. - * For example: - * { - * inputs: [{'id': 'input1', 'property': 'new value'}], - * state: [{'id': 'state1', 'property': 'existing value'}] - * } - */ - - // eslint-disable-next-line no-unused-vars - const [outputComponentId, _] = outputIdAndProp.split('.'); - const payload = { - output: outputIdAndProp, - changedPropIds, - }; - - const { - inputs, - state, - clientside_function, - } = dependenciesRequest.content.find( - dependency => dependency.output === outputIdAndProp - ); - const validKeys = keys(getState().paths); - payload.inputs = inputs.map(inputObject => { - // Make sure the component id exists in the layout - if (!includes(inputObject.id, validKeys)) { - throw new ReferenceError( - 'An invalid input object was used in an ' + - '`Input` of a Dash callback. ' + - 'The id of this object is `' + - inputObject.id + - '` and the property is `' + - inputObject.property + - '`. The list of ids in the current layout is ' + - '`[' + - validKeys.join(', ') + - ']`' - ); + function handleError(err) { + const {pendingCallbacks} = getState(); + if (requestIsActive(pendingCallbacks, resolvedId, requestId)) { + // Skip all prop updates from this callback, and remove + // it from the pending list so callbacks it was blocking + // that have other changed inputs will still fire. + updatePending(pendingCallbacks, allPropIds); + } + let message = `Callback error updating ${JSON.stringify( + payload.outputs + )}`; + if (clientside_function) { + const {namespace: ns, function_name: fn} = clientside_function; + message += ` via clientside function ${ns}.${fn}`; + } + handleAsyncError(err, message, dispatch); } - const propLens = lensPath( - concat(getState().paths[inputObject.id], [ - 'props', - inputObject.property, - ]) - ); - return { - id: inputObject.id, - property: inputObject.property, - value: view(propLens, layout), - }; - }); - const inputsPropIds = inputs.map(p => `${p.id}.${p.property}`); - - payload.changedPropIds = changedPropIds.filter(p => - includes(p, inputsPropIds) - ); - - if (state.length > 0) { - payload.state = state.map(stateObject => { - // Make sure the component id exists in the layout - if (!includes(stateObject.id, validKeys)) { - throw new ReferenceError( - 'An invalid input object was used in a ' + - '`State` object of a Dash callback. ' + - 'The id of this object is `' + - stateObject.id + - '` and the property is `' + - stateObject.property + - '`. The list of ids in the current layout is ' + - '`[' + - validKeys.join(', ') + - ']`' - ); + if (clientside_function) { + try { + handleData(handleClientside(clientside_function, payload)); + } catch (err) { + handleError(err); } - const propLens = lensPath( - concat(getState().paths[stateObject.id], [ - 'props', - stateObject.property, - ]) - ); - return { - id: stateObject.id, - property: stateObject.property, - value: view(propLens, layout), - }; - }); - } - - function doUpdateProps(id, updatedProps) { - const {layout, paths} = getState(); - const itempath = paths[id]; - if (!itempath) { - return false; + hasClientSide = true; + return null; } - // This is a callback-generated update. - // Check if this invalidates existing persisted prop values, - // or if persistence changed, whether this updates other props. - const updatedProps2 = prunePersistence( - path(itempath, layout), - updatedProps, - dispatch - ); - - // In case the update contains whole components, see if any of - // those components have props to update to persist user edits. - const {props} = applyPersistence({props: updatedProps2}, dispatch); + return handleServerside(config, payload, hooks) + .then(handleData) + .catch(handleError) + .then(fireNext); + }); + const done = Promise.all(queue); + return hasClientSide ? fireNext().then(done) : done; +} - dispatch( - updateProps({ - itempath, - props, - source: 'response', - }) - ); +function fillVals(paths, layout, cb, specs, depType) { + const getter = depType === 'Input' ? cb.getInputs : cb.getState; + return getter(paths).map((inputList, i) => + unwrapIfNotMulti( + paths, + inputList.map(({id, property, path: path_}) => ({ + id, + property, + value: path(path_, layout).props[property], + })), + specs[i], + cb.anyVals, + depType + ) + ); +} - return props; +function handleServerside(config, payload, hooks) { + if (hooks.request_pre !== null) { + hooks.request_pre(payload); } - // Clientside hook - if (clientside_function) { - let returnValue; - - /* - * Create the dash_clientside namespace if it doesn't exist and inject - * no_update and PreventUpdate. - */ - if (!window.dash_clientside) { - window.dash_clientside = {}; - } + return fetch( + `${urlBase(config)}_dash-update-component`, + mergeDeepRight(config.fetch, { + method: 'POST', + headers: getCSRFHeader(), + body: JSON.stringify(payload), + }) + ).then(res => { + const {status} = res; + if (status === STATUS.OK) { + return res.json().then(data => { + const {multi, response} = data; + if (hooks.request_post !== null) { + hooks.request_post(payload, response); + } - if (!window.dash_clientside.no_update) { - Object.defineProperty(window.dash_clientside, 'no_update', { - value: {description: 'Return to prevent updating an Output.'}, - writable: false, - }); + if (multi) { + return response; + } - Object.defineProperty(window.dash_clientside, 'PreventUpdate', { - value: {description: 'Throw to prevent updating all Outputs.'}, - writable: false, + const {output} = payload; + const id = output.substr(0, output.lastIndexOf('.')); + return {[id]: response.props}; }); } + if (status === STATUS.PREVENT_UPDATE) { + return {}; + } + throw res; + }); +} - try { - returnValue = window.dash_clientside[clientside_function.namespace][ - clientside_function.function_name - ]( - ...pluck('value', payload.inputs), - ...(has('state', payload) ? pluck('value', payload.state) : []) - ); - } catch (e) { - /* - * Prevent all updates. - */ - if (e === window.dash_clientside.PreventUpdate) { - updateRequestQueue(true, STATUS.PREVENT_UPDATE); - return; - } +const getVals = input => + Array.isArray(input) ? pluck('value', input) : input.value; - /* eslint-disable no-console */ - console.error( - `The following error occurred while executing ${clientside_function.namespace}.${clientside_function.function_name} ` + - `in order to update component "${payload.output}" ⋁⋁⋁` - ); - console.error(e); - /* eslint-enable no-console */ +const zipIfArray = (a, b) => (Array.isArray(a) ? zip(a, b) : [[a, b]]); - /* - * Update the request queue by treating an unsuccessful clientside - * like a failed serverside response via same request queue - * mechanism - */ +function handleClientside(clientside_function, payload) { + const dc = (window.dash_clientside = window.dash_clientside || {}); + if (!dc.no_update) { + Object.defineProperty(dc, 'no_update', { + value: {description: 'Return to prevent updating an Output.'}, + writable: false, + }); - updateRequestQueue(true, STATUS.CLIENTSIDE_ERROR); - return; - } + Object.defineProperty(dc, 'PreventUpdate', { + value: {description: 'Throw to prevent updating all Outputs.'}, + writable: false, + }); + } - // Returning promises isn't support atm - if (type(returnValue) === 'Promise') { - /* eslint-disable no-console */ - console.error( - 'The clientside function ' + - `${clientside_function.namespace}.${clientside_function.function_name} ` + - 'returned a Promise instead of a value. Promises are not ' + - 'supported in Dash clientside right now, but may be in the ' + - 'future.' - ); - /* eslint-enable no-console */ - updateRequestQueue(true, STATUS.CLIENTSIDE_ERROR); - return; - } + const {inputs, outputs, state} = payload; - function updateClientsideOutput(outputIdAndProp, outputValue) { - const [outputId, outputProp] = outputIdAndProp.split('.'); - const updatedProps = { - [outputProp]: outputValue, - }; - - /* - * Update the request queue by treating a successful clientside - * like a successful serverside response (200 status code) - */ - updateRequestQueue(false, STATUS.OK); - - /* - * Prevent update. - */ - if (outputValue === window.dash_clientside.no_update) { - return; - } + let returnValue; - // Update the layout with the new result - const appliedProps = doUpdateProps(outputId, updatedProps); - - /* - * This output could itself be a serverside or clientside input - * to another function - */ - if (appliedProps) { - dispatch( - notifyObservers({ - id: outputId, - props: appliedProps, - }) - ); - } + try { + const {namespace, function_name} = clientside_function; + let args = inputs.map(getVals); + if (state) { + args = concat(args, state.map(getVals)); } - - if (isMultiOutputProp(payload.output)) { - parseMultipleOutputs(payload.output).forEach((outputPropId, i) => { - updateClientsideOutput(outputPropId, returnValue[i]); - }); - } else { - updateClientsideOutput(payload.output, returnValue); + returnValue = dc[namespace][function_name](...args); + } catch (e) { + if (e === dc.PreventUpdate) { + return {}; } - - /* - * Note that unlike serverside updates, we're not handling - * children as components right now, so we don't need to - * crawl the computed result to check for nested components - * or properties that might trigger other inputs. - * In the future, we could handle this case. - */ - return; + throw e; } - if (hooks.request_pre !== null) { - hooks.request_pre(payload); + if (type(returnValue) === 'Promise') { + throw new Error( + 'The clientside function returned a Promise. ' + + 'Promises are not supported in Dash clientside ' + + 'right now, but may be in the future.' + ); } - /* eslint-disable consistent-return */ - return fetch( - `${urlBase(config)}_dash-update-component`, - mergeDeepRight(config.fetch, { - /* eslint-enable consistent-return */ + const data = {}; + zipIfArray(outputs, returnValue).forEach(([outi, reti]) => { + zipIfArray(outi, reti).forEach(([outij, retij]) => { + const {id, property} = outij; + const idStr = stringifyId(id); + const dataForId = (data[idStr] = data[idStr] || {}); + if (retij !== dc.no_update) { + dataForId[property] = retij; + } + }); + }); + return data; +} - method: 'POST', - headers: getCSRFHeader(), - body: JSON.stringify(payload), - }) - ) - .then(function handleResponse(res) { - const isRejected = () => { - const latestRequestIndex = findLastIndex( - propEq('controllerId', outputIdAndProp), - getState().requestQueue - ); - /* - * Note that if the latest request is still `loading` - * or even if the latest request failed, - * we still reject this response in favor of waiting - * for the latest request to finish. - */ - const rejected = latestRequestIndex > getThisRequestIndex(); - return rejected; - }; - - if (res.status !== STATUS.OK) { - // update the status of this request - updateRequestQueue(true, res.status); - - /* - * This is a 204 response code, there's no content to process. - */ - if (res.status === STATUS.PREVENT_UPDATE) { - return; - } +function requestIsActive(pendingCallbacks, resolvedId, requestId) { + const thisCallback = pendingCallbacks.find( + propEq('resolvedId', resolvedId) + ); + // could be inactivated if it was requested again, in which case it could + // potentially even have finished and been removed from the list + return thisCallback && thisCallback.requestId === requestId; +} - /* - * eject into `catch` handler below to display error - * message in ui - */ - throw res; - } +function doUpdateProps(dispatch, getState, id, updatedProps) { + const {layout, paths} = getState(); + const itempath = getPath(paths, id); + if (!itempath) { + return false; + } - /* - * Check to see if another request has already come back - * _after_ this one. - * If so, ignore this request. - */ - if (isRejected()) { - updateRequestQueue(true, res.status); - return; - } + // This is a callback-generated update. + // Check if this invalidates existing persisted prop values, + // or if persistence changed, whether this updates other props. + const updatedProps2 = prunePersistence( + path(itempath, layout), + updatedProps, + dispatch + ); - res.json().then(function handleJson(data) { - /* - * Even if the `res` was received in the correct order, - * the remainder of the response (res.json()) could happen - * at different rates causing the parsed responses to - * get out of order - */ - if (isRejected()) { - updateRequestQueue(true, res.status); - return; - } + // In case the update contains whole components, see if any of + // those components have props to update to persist user edits. + const {props} = applyPersistence({props: updatedProps2}, dispatch); - updateRequestQueue(false, res.status); + dispatch( + updateProps({ + itempath, + props, + source: 'response', + }) + ); - // Fire custom request_post hook if any - if (hooks.request_post !== null) { - hooks.request_post(payload, data.response); - } + return props; +} - /* - * it's possible that this output item is no longer visible. - * for example, the could still be request running when - * the user switched the chapter - * - * if it's not visible, then ignore the rest of the updates - * to the store - */ +function updateChildPaths(dispatch, getState, pendingCallbacks, id, children) { + const {paths: oldPaths, graphs} = getState(); + const childrenPath = concat(getPath(oldPaths, id), ['props', 'children']); + const paths = computePaths(children, childrenPath, oldPaths); + dispatch(setPaths(paths)); + + // Prune now-nonexistent changedPropIds and mark callbacks with + // now-nonexistent outputs + const removeIds = []; + let cleanedCallbacks = pendingCallbacks.map(callback => { + const {changedPropIds, getOutputs, resolvedId} = callback; + if (!flatten(getOutputs(paths)).length) { + removeIds.push(resolvedId); + return callback; + } - const multi = data.multi; + let omittedProps = false; + const newChangedProps = pickBy((_, propId) => { + if (getPath(paths, splitIdAndProp(propId).id)) { + return true; + } + omittedProps = true; + return false; + }, changedPropIds); - const handleResponse = ([outputIdAndProp, props]) => { - // Backward compatibility - const pathKey = multi ? outputIdAndProp : outputComponentId; + return omittedProps + ? assoc('changedPropIds', newChangedProps, callback) + : callback; + }); - const appliedProps = doUpdateProps(pathKey, props); - if (!appliedProps) { - return; - } + // Remove the callbacks we marked above + removeIds.forEach(resolvedId => { + const cb = cleanedCallbacks.find(propEq('resolvedId', resolvedId)); + if (cb) { + cleanedCallbacks = removePendingCallback( + pendingCallbacks, + paths, + resolvedId, + flatten(cb.getOutputs(paths)).map(combineIdAndProp) + ); + } + }); - dispatch( - notifyObservers({ - id: pathKey, - props: appliedProps, - }) - ); - - /* - * If the response includes children, then we need to update our - * paths store. - * TODO - Do we need to wait for updateProps to finish? - */ - if (has('children', appliedProps)) { - const newChildren = appliedProps.children; - dispatch( - computePaths({ - subTree: newChildren, - startingPath: concat( - getState().paths[pathKey], - ['props', 'children'] - ), - }) - ); + const newCallbacks = getCallbacksInLayout(graphs, paths, children); + return mergePendingCallbacks(cleanedCallbacks, newCallbacks); +} - /* - * if children contains objects with IDs, then we - * need to dispatch a propChange for all of these - * new children components - */ - if ( - includes(type(newChildren), ['Array', 'Object']) && - !isEmpty(newChildren) - ) { - /* - * TODO: We're just naively crawling - * the _entire_ layout to recompute the - * the dependency graphs. - * We don't need to do this - just need - * to compute the subtree - */ - const newProps = {}; - crawlLayout(newChildren, function appendIds(child) { - if (hasId(child)) { - keys(child.props).forEach(childProp => { - const componentIdAndProp = `${child.props.id}.${childProp}`; - if ( - has( - componentIdAndProp, - InputGraph.nodes - ) - ) { - newProps[componentIdAndProp] = { - id: child.props.id, - props: { - [childProp]: - child.props[childProp], - }, - }; - } - }); - } - }); - - /* - * Organize props by shared outputs so that we - * only make one request per output component - * (even if there are multiple inputs). - * - * For example, we might render 10 inputs that control - * a single output. If that is the case, we only want - * to make a single call, not 10 calls. - */ - - /* - * In some cases, the new item will be an output - * with its inputs already rendered (not rendered) - * as part of this update. - * For example, a tab with global controls that - * renders different content containers without any - * additional inputs. - * - * In that case, we'll call `updateOutput` with that output - * and just "pretend" that one if its inputs changed. - * - * If we ever add logic that informs the user on - * "which input changed", we'll have to account for this - * special case (no input changed?) - */ - - const outputIds = []; - keys(newProps).forEach(idAndProp => { - if ( - // It's an output - InputGraph.dependenciesOf(idAndProp) - .length === 0 && - /* - * And none of its inputs are generated in this - * request - */ - intersection( - InputGraph.dependantsOf(idAndProp), - keys(newProps) - ).length === 0 - ) { - outputIds.push(idAndProp); - delete newProps[idAndProp]; - } - }); - - // Dispatch updates to inputs - const reducedNodeIds = reduceInputIds( - keys(newProps), - InputGraph - ); - const depOrder = InputGraph.overallOrder(); - const sortedNewProps = sort( - (a, b) => - depOrder.indexOf(a.input) - - depOrder.indexOf(b.input), - reducedNodeIds - ); - sortedNewProps.forEach(function(inputOutput) { - const payload = newProps[inputOutput.input]; - payload.excludedOutputs = - inputOutput.excludedOutputs; - dispatch(notifyObservers(payload)); - }); - - // Dispatch updates to lone outputs - outputIds.forEach(idAndProp => { - const requestUid = uid(); - dispatch( - setRequestQueue( - append( - { - // TODO - Are there any implications of doing this?? - controllerId: null, - status: 'loading', - uid: requestUid, - requestTime: Date.now(), - }, - getState().requestQueue - ) - ) - ); - updateOutput( - idAndProp, - - getState, - requestUid, - dispatch, - changedPropIds - ); - }); - } - } - }; - if (multi) { - Object.entries(data.response).forEach(handleResponse); - } else { - handleResponse([outputIdAndProp, data.response.props]); - } - }); - }) - .catch(err => { - const message = `Callback error updating ${ - isMultiOutputProp(payload.output) - ? parseMultipleOutputs(payload.output).join(', ') - : payload.output - }`; - handleAsyncError(err, message, dispatch); +export function notifyObservers({id, props}) { + return async function(dispatch, getState) { + const {graphs, paths, pendingCallbacks} = getState(); + + const changedProps = keys(props); + let finalCallbacks = pendingCallbacks; + + changedProps.forEach(propName => { + const newCBs = getCallbacksByInput(graphs, paths, id, propName); + if (newCBs.length) { + finalCallbacks = mergePendingCallbacks( + finalCallbacks, + followForward(graphs, paths, newCBs) + ); + } }); + dispatch(startCallbacks(finalCallbacks)); + }; } export function handleAsyncError(err, message, dispatch) { // Handle html error responses - const errText = - err && typeof err.text === 'function' - ? err.text() - : Promise.resolve(err); - - errText.then(text => { - dispatch( - onError({ - type: 'backEnd', - error: { - message, - html: text, - }, - }) - ); - }); + if (err && typeof err.text === 'function') { + err.text().then(text => { + const error = {message, html: text}; + dispatch(onError({type: 'backEnd', error})); + }); + } else { + const error = err instanceof Error ? err : {message, html: err}; + dispatch(onError({type: 'backEnd', error})); + } } export function serialize(state) { diff --git a/dash-renderer/src/actions/isAppReady.js b/dash-renderer/src/actions/isAppReady.js index c83019da54..14c43be6d8 100644 --- a/dash-renderer/src/actions/isAppReady.js +++ b/dash-renderer/src/actions/isAppReady.js @@ -2,11 +2,13 @@ import {path} from 'ramda'; import {isReady} from '@plotly/dash-component-plugins'; import Registry from '../registry'; +import {getPath} from './paths'; export default (layout, paths, targets) => { const promises = []; + targets.forEach(id => { - const pathOfId = paths[id]; + const pathOfId = getPath(paths, id); if (!pathOfId) { return; } diff --git a/dash-renderer/src/actions/paths.js b/dash-renderer/src/actions/paths.js new file mode 100644 index 0000000000..643f8207f6 --- /dev/null +++ b/dash-renderer/src/actions/paths.js @@ -0,0 +1,71 @@ +import { + concat, + filter, + find, + forEachObjIndexed, + path, + propEq, + props, +} from 'ramda'; + +import {crawlLayout} from './utils'; + +/* + * state.paths has structure: + * { + * strs: {[id]: path} // for regular string ids + * objs: {[keyStr]: [{values, path}]} // for wildcard ids + * } + * keyStr: sorted keys of the id, joined with ',' into one string + * values: array of values in the id, in order of keys + */ + +export function computePaths(subTree, startingPath, oldPaths) { + const {strs: oldStrs, objs: oldObjs} = oldPaths || {strs: {}, objs: {}}; + + const diffHead = path => startingPath.some((v, i) => path[i] !== v); + + const spLen = startingPath.length; + // if we're updating a subtree, clear out all of the existing items + const strs = spLen ? filter(diffHead, oldStrs) : {}; + const objs = {}; + if (spLen) { + forEachObjIndexed((oldValPaths, oldKeys) => { + const newVals = filter(({path}) => diffHead(path), oldValPaths); + if (newVals.length) { + objs[oldKeys] = newVals; + } + }, oldObjs); + } + + crawlLayout(subTree, function assignPath(child, itempath) { + const id = path(['props', 'id'], child); + if (id) { + if (typeof id === 'object') { + const keys = Object.keys(id).sort(); + const values = props(keys, id); + const keyStr = keys.join(','); + const paths = (objs[keyStr] = objs[keyStr] || []); + paths.push({values, path: concat(startingPath, itempath)}); + } else { + strs[id] = concat(startingPath, itempath); + } + } + }); + + return {strs, objs}; +} + +export function getPath(paths, id) { + if (typeof id === 'object') { + const keys = Object.keys(id).sort(); + const keyStr = keys.join(','); + const keyPaths = paths.objs[keyStr]; + if (!keyPaths) { + return false; + } + const values = props(keys, id); + return find(propEq('values', values), keyPaths).path; + } + return paths.strs[id]; +} diff --git a/dash-renderer/src/actions/utils.js b/dash-renderer/src/actions/utils.js new file mode 100644 index 0000000000..087e262a2a --- /dev/null +++ b/dash-renderer/src/actions/utils.js @@ -0,0 +1,45 @@ +import {append, concat, has, path, type} from 'ramda'; + +/* + * requests_pathname_prefix is the new config parameter introduced in + * dash==0.18.0. The previous versions just had url_base_pathname + */ +export function urlBase(config) { + const hasUrlBase = has('url_base_pathname', config); + const hasReqPrefix = has('requests_pathname_prefix', config); + if (type(config) !== 'Object' || (!hasUrlBase && !hasReqPrefix)) { + throw new Error( + ` + Trying to make an API request but neither + "url_base_pathname" nor "requests_pathname_prefix" + is in \`config\`. \`config\` is: `, + config + ); + } + + const base = hasReqPrefix + ? config.requests_pathname_prefix + : config.url_base_pathname; + + return base.charAt(base.length - 1) === '/' ? base : base + '/'; +} + +const propsChildren = ['props', 'children']; + +// crawl a layout object or children array, apply a function on every object +export const crawlLayout = (object, func, currentPath = []) => { + if (Array.isArray(object)) { + // children array + object.forEach((child, i) => { + crawlLayout(child, func, append(i, currentPath)); + }); + } else if (type(object) === 'Object') { + func(object, currentPath); + + const children = path(propsChildren, object); + if (children) { + const newPath = concat(currentPath, propsChildren); + crawlLayout(children, func, newPath); + } + } +}; diff --git a/dash-renderer/src/components/core/DocumentTitle.react.js b/dash-renderer/src/components/core/DocumentTitle.react.js index a719fdae9d..6261c30fcc 100644 --- a/dash-renderer/src/components/core/DocumentTitle.react.js +++ b/dash-renderer/src/components/core/DocumentTitle.react.js @@ -1,7 +1,6 @@ /* global document:true */ import {connect} from 'react-redux'; -import {any} from 'ramda'; import {Component} from 'react'; import PropTypes from 'prop-types'; @@ -14,7 +13,7 @@ class DocumentTitle extends Component { } componentWillReceiveProps(props) { - if (any(r => r.status === 'loading', props.requestQueue)) { + if (props.pendingCallbacks.length) { document.title = 'Updating...'; } else { document.title = this.state.initialTitle; @@ -31,9 +30,9 @@ class DocumentTitle extends Component { } DocumentTitle.propTypes = { - requestQueue: PropTypes.array.isRequired, + pendingCallbacks: PropTypes.array.isRequired, }; export default connect(state => ({ - requestQueue: state.requestQueue, + pendingCallbacks: state.pendingCallbacks, }))(DocumentTitle); diff --git a/dash-renderer/src/components/core/Loading.react.js b/dash-renderer/src/components/core/Loading.react.js index f0701eb38e..999684a8dc 100644 --- a/dash-renderer/src/components/core/Loading.react.js +++ b/dash-renderer/src/components/core/Loading.react.js @@ -1,19 +1,18 @@ import {connect} from 'react-redux'; -import {any} from 'ramda'; import React from 'react'; import PropTypes from 'prop-types'; function Loading(props) { - if (any(r => r.status === 'loading', props.requestQueue)) { + if (props.pendingCallbacks.length) { return
; } return null; } Loading.propTypes = { - requestQueue: PropTypes.array.isRequired, + pendingCallbacks: PropTypes.array.isRequired, }; export default connect(state => ({ - requestQueue: state.requestQueue, + pendingCallbacks: state.pendingCallbacks, }))(Loading); diff --git a/dash-renderer/src/components/core/Toolbar.react.js b/dash-renderer/src/components/core/Toolbar.react.js index 38d947aef7..7d5941658d 100644 --- a/dash-renderer/src/components/core/Toolbar.react.js +++ b/dash-renderer/src/components/core/Toolbar.react.js @@ -33,7 +33,7 @@ function UnconnectedToolbar(props) { }, styles.parentSpanStyle )} - onClick={() => dispatch(undo())} + onClick={() => dispatch(undo)} >
dispatch(redo())} + onClick={() => dispatch(redo)} >
{ - if (action.type === 'COMPUTE_GRAPHS') { - const dependencies = action.payload; - const inputGraph = new DepGraph(); - const multiGraph = new DepGraph(); - - dependencies.forEach(function registerDependency(dependency) { - const {output, inputs} = dependency; - - if (isMultiOutputProp(output)) { - parseMultipleOutputs(output).forEach(out => { - multiGraph.addNode(out); - inputs.forEach(i => { - const inputId = `${i.id}.${i.property}`; - if (!multiGraph.hasNode(inputId)) { - multiGraph.addNode(inputId); - } - multiGraph.addDependency(inputId, out); - }); - }); - } else { - multiGraph.addNode(output); - inputs.forEach(i => { - const inputId = `${i.id}.${i.property}`; - if (!multiGraph.hasNode(inputId)) { - multiGraph.addNode(inputId); - } - multiGraph.addDependency(inputId, output); - }); - } - - inputs.forEach(inputObject => { - const inputId = `${inputObject.id}.${inputObject.property}`; - inputGraph.addNode(output); - if (!inputGraph.hasNode(inputId)) { - inputGraph.addNode(inputId); - } - inputGraph.addDependency(inputId, output); - }); - }); - - return {InputGraph: inputGraph, MultiGraph: multiGraph}; + if (action.type === 'SET_GRAPHS') { + return action.payload; } - return state; }; diff --git a/dash-renderer/src/reducers/paths.js b/dash-renderer/src/reducers/paths.js index cf11c993f8..fd48700108 100644 --- a/dash-renderer/src/reducers/paths.js +++ b/dash-renderer/src/reducers/paths.js @@ -1,57 +1,12 @@ -import {crawlLayout, hasPropsId} from './utils'; -import { - concat, - equals, - filter, - isEmpty, - isNil, - keys, - mergeRight, - omit, - slice, -} from 'ramda'; import {getAction} from '../actions/constants'; -const initialPaths = null; +const initialPaths = {strs: {}, objs: {}}; const paths = (state = initialPaths, action) => { - switch (action.type) { - case getAction('COMPUTE_PATHS'): { - const {subTree, startingPath} = action.payload; - let oldState = state; - if (isNil(state)) { - oldState = {}; - } - let newState; - - // if we're updating a subtree, clear out all of the existing items - if (!isEmpty(startingPath)) { - const removeKeys = filter( - k => - equals( - startingPath, - slice(0, startingPath.length, oldState[k]) - ), - keys(oldState) - ); - newState = omit(removeKeys, oldState); - } else { - newState = mergeRight({}, oldState); - } - - crawlLayout(subTree, function assignPath(child, itempath) { - if (hasPropsId(child)) { - newState[child.props.id] = concat(startingPath, itempath); - } - }); - - return newState; - } - - default: { - return state; - } + if (action.type === getAction('SET_PATHS')) { + return action.payload; } + return state; }; export default paths; diff --git a/dash-renderer/src/reducers/pendingCallbacks.js b/dash-renderer/src/reducers/pendingCallbacks.js new file mode 100644 index 0000000000..70a2cd3f86 --- /dev/null +++ b/dash-renderer/src/reducers/pendingCallbacks.js @@ -0,0 +1,11 @@ +const pendingCallbacks = (state = [], action) => { + switch (action.type) { + case 'SET_PENDING_CALLBACKS': + return action.payload; + + default: + return state; + } +}; + +export default pendingCallbacks; diff --git a/dash-renderer/src/reducers/reducer.js b/dash-renderer/src/reducers/reducer.js index 1123134675..01dad8effb 100644 --- a/dash-renderer/src/reducers/reducer.js +++ b/dash-renderer/src/reducers/reducer.js @@ -13,7 +13,7 @@ import {combineReducers} from 'redux'; import layout from './layout'; import graphs from './dependencyGraph'; import paths from './paths'; -import requestQueue from './requestQueue'; +import pendingCallbacks from './pendingCallbacks'; import appLifecycle from './appLifecycle'; import history from './history'; import error from './error'; @@ -34,7 +34,7 @@ function mainReducer() { layout, graphs, paths, - requestQueue, + pendingCallbacks, config, history, error, @@ -50,7 +50,8 @@ function mainReducer() { function getInputHistoryState(itempath, props, state) { const {graphs, layout, paths} = state; const {InputGraph} = graphs; - const keyObj = filter(equals(itempath), paths); + // TODO: wildcards? + const keyObj = filter(equals(itempath), paths.strs); let historyEntry; if (!isEmpty(keyObj)) { const id = keys(keyObj)[0]; @@ -58,11 +59,12 @@ function getInputHistoryState(itempath, props, state) { keys(props).forEach(propKey => { const inputKey = `${id}.${propKey}`; if ( + // TODO: wildcards? InputGraph.hasNode(inputKey) && InputGraph.dependenciesOf(inputKey).length > 0 ) { historyEntry.props[propKey] = view( - lensPath(concat(paths[id], ['props', propKey])), + lensPath(concat(paths.strs[id], ['props', propKey])), layout ); } diff --git a/dash-renderer/src/reducers/requestQueue.js b/dash-renderer/src/reducers/requestQueue.js deleted file mode 100644 index 995285a91d..0000000000 --- a/dash-renderer/src/reducers/requestQueue.js +++ /dev/null @@ -1,13 +0,0 @@ -import {clone} from 'ramda'; - -const requestQueue = (state = [], action) => { - switch (action.type) { - case 'SET_REQUEST_QUEUE': - return clone(action.payload); - - default: - return state; - } -}; - -export default requestQueue; diff --git a/dash-renderer/src/reducers/utils.js b/dash-renderer/src/reducers/utils.js deleted file mode 100644 index 2dab3b7318..0000000000 --- a/dash-renderer/src/reducers/utils.js +++ /dev/null @@ -1,71 +0,0 @@ -import { - allPass, - append, - compose, - flip, - has, - is, - prop, - reduce, - type, -} from 'ramda'; - -const extend = reduce(flip(append)); - -const hasProps = allPass([is(Object), has('props')]); - -export const hasPropsId = allPass([ - hasProps, - compose( - has('id'), - prop('props') - ), -]); - -export const hasPropsChildren = allPass([ - hasProps, - compose( - has('children'), - prop('props') - ), -]); - -// crawl a layout object, apply a function on every object -export const crawlLayout = (object, func, path = []) => { - func(object, path); - - /* - * object may be a string, a number, or null - * R.has will return false for both of those types - */ - if (hasPropsChildren(object)) { - const newPath = extend(path, ['props', 'children']); - if (Array.isArray(object.props.children)) { - object.props.children.forEach((child, i) => { - crawlLayout(child, func, append(i, newPath)); - }); - } else { - crawlLayout(object.props.children, func, newPath); - } - } else if (is(Array, object)) { - /* - * Sometimes when we're updating a sub-tree - * (like when we're responding to a callback) - * that returns `{children: [{...}, {...}]}` - * then we'll need to start crawling from - * an array instead of an object. - */ - - object.forEach((child, i) => { - crawlLayout(child, func, append(i, path)); - }); - } -}; - -export function hasId(child) { - return ( - type(child) === 'Object' && - has('props', child) && - has('id', child.props) - ); -} diff --git a/dash-renderer/src/utils.js b/dash-renderer/src/utils.js deleted file mode 100644 index 623cfcb335..0000000000 --- a/dash-renderer/src/utils.js +++ /dev/null @@ -1,69 +0,0 @@ -import {has, type} from 'ramda'; - -/* - * requests_pathname_prefix is the new config parameter introduced in - * dash==0.18.0. The previous versions just had url_base_pathname - */ -export function urlBase(config) { - const hasUrlBase = has('url_base_pathname', config); - const hasReqPrefix = has('requests_pathname_prefix', config); - if (type(config) !== 'Object' || (!hasUrlBase && !hasReqPrefix)) { - throw new Error( - ` - Trying to make an API request but neither - "url_base_pathname" nor "requests_pathname_prefix" - is in \`config\`. \`config\` is: `, - config - ); - } - - const base = hasReqPrefix - ? config.requests_pathname_prefix - : config.url_base_pathname; - - return base.charAt(base.length - 1) === '/' ? base : base + '/'; -} - -export function uid() { - function s4() { - const h = 0x10000; - return Math.floor((1 + Math.random()) * h) - .toString(16) - .substring(1); - } - return ( - s4() + - s4() + - '-' + - s4() + - '-' + - s4() + - '-' + - s4() + - '-' + - s4() + - s4() + - s4() - ); -} - -export function isMultiOutputProp(outputIdAndProp) { - /* - * If this update is for multiple outputs, then it has - * starting & trailing `..` and each propId pair is separated - * by `...`, e.g. - * "..output-1.value...output-2.value...output-3.value...output-4.value.." - */ - - return outputIdAndProp.startsWith('..'); -} - -export function parseMultipleOutputs(outputIdAndProp) { - /* - * If this update is for multiple outputs, then it has - * starting & trailing `..` and each propId pair is separated - * by `...`, e.g. - * "..output-1.value...output-2.value...output-3.value...output-4.value.." - */ - return outputIdAndProp.split('...').map(o => o.replace('..', '')); -} diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 231c19b3e4..7a31afd7f3 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -10,9 +10,9 @@ def assert_context(*args, **kwargs): if not flask.has_request_context(): raise exceptions.MissingCallbackContextException( ( - 'dash.callback_context.{} ' - 'is only available from a callback!' - ).format(getattr(func, '__name__')) + "dash.callback_context.{} " + "is only available from a callback!" + ).format(getattr(func, "__name__")) ) return func(*args, **kwargs) return assert_context @@ -23,22 +23,37 @@ class CallbackContext: @property @has_context def inputs(self): - return getattr(flask.g, 'input_values', {}) + return getattr(flask.g, "input_values", {}) @property @has_context def states(self): - return getattr(flask.g, 'state_values', {}) + return getattr(flask.g, "state_values", {}) @property @has_context def triggered(self): - return getattr(flask.g, 'triggered_inputs', []) + return getattr(flask.g, "triggered_inputs", []) + + @property + @has_context + def outputs_list(self): + return getattr(flask.g, "outputs_list", []) + + @property + @has_context + def inputs_list(self): + return getattr(flask.g, "inputs_list", []) + + @property + @has_context + def states_list(self): + return getattr(flask.g, "states_list", []) @property @has_context def response(self): - return getattr(flask.g, 'dash_response') + return getattr(flask.g, "dash_response") callback_context = CallbackContext() diff --git a/dash/_utils.py b/dash/_utils.py index 79062c6fe8..4c9f4149ab 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -8,12 +8,16 @@ import subprocess import logging import io +import json from functools import wraps import future.utils as utils from . import exceptions logger = logging.getLogger() +# py2/3 json.dumps-compatible strings - these are equivalent in py3, not in py2 +_strings = (type(u""), type("")) + def interpolate_str(template, **data): s = template @@ -72,6 +76,7 @@ def get_relative_path(requests_pathname, path): ] ) + def strip_relative_path(requests_pathname, path): if path is None: return None @@ -174,7 +179,43 @@ def create_callback_id(output): ) ) - return "{}.{}".format(output.component_id, output.component_property) + return "{}.{}".format( + output.component_id_str().replace(".", "\\."), + output.component_property + ) + + +# inverse of create_callback_id - should only be relevant if an old renderer is +# hooked up to a new back end, which will only happen in special cases like +# embedded +def split_callback_id(callback_id): + if callback_id.startswith(".."): + return [split_callback_id(oi) for oi in callback_id[2:-2].split("...")] + + id_, prop = callback_id.rsplit(".", 1) + return {"id": id_, "property": prop} + + +def stringify_id(id_): + if isinstance(id_, dict): + return json.dumps(id_, sort_keys=True, separators=(",", ":")) + return id_ + + +def inputs_to_dict(inputs): + inputs = {} + for i in inputs: + for ii in (i if isinstance(i, list) else [i]): + id_str = stringify_id(ii["id"]) + inputs["{}.{}".format(id_str, ii["property"])] = ii.get("value") + return inputs + + +def inputs_to_vals(inputs): + return [ + [ii.get("value") for ii in i] if isinstance(i, list) else i.get("value") + for i in inputs + ] def run_command_with_process(cmd): diff --git a/dash/_validate.py b/dash/_validate.py index 12cee11791..8d0768ef1e 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -1,10 +1,10 @@ import collections import re -from .development.base_component import Component, _strings -from .dependencies import Input, Output, State, ANY, ALLSMALLER, PREVIOUS +from .development.base_component import Component +from .dependencies import Input, Output, State, ANY, ALLSMALLER from . import exceptions -from ._utils import patch_collections_abc +from ._utils import patch_collections_abc, _strings, stringify_id def validate_callback(app, layout, output, inputs, state): @@ -81,7 +81,7 @@ def validate_callback(app, layout, output, inputs, state): ) ) - matched_wildcards = (ANY, ALLSMALLER, PREVIOUS) + matched_wildcards = (ANY, ALLSMALLER) for dep in list(inputs) + list(state): wildcard_keys = get_wildcard_keys(dep, matched_wildcards) if wildcard_keys - any_keys: @@ -196,6 +196,7 @@ def id_match(c): c_id = getattr(c, "id", None) return isinstance(c_id, dict) and all( k in c and v in wildcards or v == c_id.get(k) + for k, v in arg_id.items() ) if validate_ids: @@ -296,7 +297,7 @@ def validate_prop_for_component(arg, component): ) -def validate_multi_return(output, output_value, callback_id): +def validate_multi_return(outputs_list, output_value, callback_id): if not isinstance(output_value, (list, tuple)): raise exceptions.InvalidCallbackReturnValue( """ @@ -308,16 +309,40 @@ def validate_multi_return(output, output_value, callback_id): ) ) - if len(output_value) != len(output): + if len(output_value) != len(outputs_list): raise exceptions.InvalidCallbackReturnValue( """ Invalid number of output values for {}. Expected {}, got {} """.format( - callback_id, len(output), len(output_value) + callback_id, len(outputs_list), len(output_value) ) ) + for i, outi in enumerate(outputs_list): + if isinstance(outi, list): + vi = output_value[i] + if not isinstance(vi, (list, tuple)): + raise exceptions.InvalidCallbackReturnValue( + """ + The callback {} ouput {} is a wildcard multi-output. + Expected the output type to be a list or tuple but got: + {}. + """.format( + callback_id, i, repr(vi) + ) + ) + + if len(vi) != len(outi): + raise exceptions.InvalidCallbackReturnValue( + """ + Invalid number of output values for {}. + Expected {}, got {} + """.format( + callback_id, len(vi), len(outi) + ) + ) + def fail_callback_output(output_value, output): valid = _strings + (dict, int, float, type(None), Component) @@ -505,12 +530,12 @@ def validate_layout(layout, layout_value): """ ) - layout_id = getattr(layout_value, "id", None) + layout_id = stringify_id(getattr(layout_value, "id", None)) component_ids = {layout_id} if layout_id else set() # pylint: disable=protected-access for component in layout_value._traverse(): - component_id = getattr(component, "id", None) + component_id = stringify_id(getattr(component, "id", None)) if component_id and component_id in component_ids: raise exceptions.DuplicateIdError( """ diff --git a/dash/dash.py b/dash/dash.py index fb92984b2f..7fe922b941 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -34,8 +34,12 @@ generate_hash, get_asset_path, get_relative_path, + inputs_to_dict, + inputs_to_vals, interpolate_str, patch_collections_abc, + split_callback_id, + stringify_id, strip_relative_path ) from . import _validate @@ -917,33 +921,39 @@ def wrap_func(func): def add_context(*args, **kwargs): # don't touch the comment on the next line - used by debugger output_value = func(*args, **kwargs) # %% callback invoked %% - if multi: - _validate.validate_multi_return( - output, output_value, callback_id - ) - component_ids = collections.defaultdict(dict) - has_update = False - for i, o in enumerate(output): - val = output_value[i] - if not isinstance(val, _NoUpdate): - has_update = True - o_id, o_prop = o.component_id, o.component_property - component_ids[o_id][o_prop] = val + if isinstance(output_value, _NoUpdate): + raise PreventUpdate - if not has_update: - raise PreventUpdate + output_spec = flask.g.outputs_list + # wrap single outputs so we can treat them all the same + # for validation and response creation + if not multi: + output_value, output_spec = [output_value], [output_spec] - response = {"response": component_ids, "multi": True} - else: - if isinstance(output_value, _NoUpdate): - raise PreventUpdate + _validate.validate_multi_return( + output_spec, output_value, callback_id + ) - response = { - "response": { - "props": {output.component_property: output_value} - } - } + component_ids = collections.defaultdict(dict) + has_update = False + for val, spec in zip(output_value, output_spec): + if isinstance(val, _NoUpdate): + continue + for vali, speci in ( + zip(val, spec) + if isinstance(spec, list) + else [[val, spec]] + ): + if not isinstance(vali, _NoUpdate): + has_update = True + id_str = stringify_id(speci["id"]) + component_ids[id_str][speci["property"]] = vali + + if not has_update: + raise PreventUpdate + + response = {"response": component_ids, "multi": True} try: jsonResponse = json.dumps( @@ -962,44 +972,23 @@ def add_context(*args, **kwargs): def dispatch(self): body = flask.request.get_json() - inputs = body.get("inputs", []) - state = body.get("state", []) + flask.g.inputs_list = inputs = body.get("inputs", []) + flask.g.states_list = state = body.get("state", []) output = body["output"] + flask.g.outputs_list = body.get("outputs") or split_callback_id(output) - args = [] - - flask.g.input_values = input_values = { - "{}.{}".format(x["id"], x["property"]): x.get("value") - for x in inputs - } - flask.g.state_values = { - "{}.{}".format(x["id"], x["property"]): x.get("value") - for x in state - } - changed_props = body.get("changedPropIds") - flask.g.triggered_inputs = ( - [{"prop_id": x, "value": input_values[x]} for x in changed_props] - if changed_props - else [] - ) + flask.g.input_values = input_values = inputs_to_dict(inputs) + flask.g.state_values = inputs_to_dict(state) + changed_props = body.get("changedPropIds", []) + flask.g.triggered_inputs = [ + {"prop_id": x, "value": input_values.get(x)} for x in changed_props + ] response = flask.g.dash_response = flask.Response( mimetype="application/json" ) - def pluck_val(_props, component_registration): - for c in _props: - if ( - c["property"] == component_registration["property"] and - c["id"] == component_registration["id"] - ): - return c.get("value", None) - - for component_registration in self.callback_map[output]["inputs"]: - args.append(pluck_val(inputs, component_registration)) - - for component_registration in self.callback_map[output]["state"]: - args.append(pluck_val(state, component_registration)) + args = inputs_to_vals(inputs) + inputs_to_vals(state) response.set_data(self.callback_map[output]["callback"](*args)) return response diff --git a/dash/dependencies.py b/dash/dependencies.py index a570886d64..bf10c3e61f 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -1,3 +1,6 @@ +import json + + class _Wildcard: # pylint: disable=too-few-public-methods def __init__(self, name): self._name = name @@ -17,7 +20,6 @@ def to_json(self): ANY = _Wildcard("ANY") ALL = _Wildcard("ALL") ALLSMALLER = _Wildcard("ALLSMALLER") -PREVIOUS = _Wildcard("PREVIOUS") class DashDependency: # pylint: disable=too-few-public-methods @@ -34,17 +36,23 @@ def __repr__(self): def component_id_str(self): i = self.component_id - def json(k, v): + def _dump(v): + return json.dumps(v, sort_keys=True, separators=(",", ":")) + + def _json(k, v): vstr = v.to_json() if hasattr(v, "to_json") else json.dumps(v) return "{}:{}".format(json.dumps(k), vstr) if isinstance(i, dict): - return ("{" + ",".join(json(k, i[k]) for k in sorted(i)) + "}") + return ("{" + ",".join(_json(k, i[k]) for k in sorted(i)) + "}") return i def to_dict(self): - return {"id": self.component_id_str(), "property": self.component_property} + return { + "id": self.component_id_str(), + "property": self.component_property + } def __eq__(self, other): """ @@ -59,13 +67,18 @@ def __eq__(self, other): ) def _id_matches(self, other): + my_id = self.component_id other_id = other.component_id - if isinstance(self.component_id, dict): - if not isinstance(other_id, dict): + self_dict = isinstance(my_id, dict) + other_dict = isinstance(other_id, dict) + + if self_dict != other_dict: + return False + if self_dict: + if set(my_id.keys()) != set(other_id.keys()): return False - for k, v in self.component_id.items(): - if k not in other_id: - return False + + for k, v in my_id.items(): other_v = other_id[k] if v == other_v: continue @@ -77,14 +90,13 @@ def _id_matches(self, other): if v is ALL or other_v is ALL: continue # either ALL if v is ANY or other_v is ANY: - return False # ANY and either ALLSMALLER or PREVIOUS + return False # one ANY, one ALLSMALLER else: return False return True - elif isinstance(other_id, dict): - return False - else: - return self.component_id == other_id + + # both strings + return my_id == other_id def __hash__(self): return hash(str(self)) @@ -99,13 +111,13 @@ class Output(DashDependency): # pylint: disable=too-few-public-methods class Input(DashDependency): # pylint: disable=too-few-public-methods """Input of callback: trigger an update when it is updated.""" - allowed_wildcards = (ANY, ALL, ALLSMALLER, PREVIOUS) + allowed_wildcards = (ANY, ALL, ALLSMALLER) class State(DashDependency): # pylint: disable=too-few-public-methods """Use the value of a State in a callback but don't trigger updates.""" - allowed_wildcards = (ANY, ALL, ALLSMALLER, PREVIOUS) + allowed_wildcards = (ANY, ALL, ALLSMALLER) class ClientsideFunction: # pylint: disable=too-few-public-methods diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 50483ef0a2..cc8cd90734 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -3,13 +3,10 @@ import sys from future.utils import with_metaclass -from .._utils import patch_collections_abc +from .._utils import patch_collections_abc, _strings, stringify_id MutableSequence = patch_collections_abc("MutableSequence") -# py2/3 json.dumps-compatible strings - these are equivalent in py3, not in py2 -_strings = (type(u""), type("")) - # pylint: disable=no-init,too-few-public-methods class ComponentRegistry: @@ -112,7 +109,7 @@ def __init__(self, **kwargs): raise TypeError( "{} received an unexpected keyword argument: `{}`".format( error_string_prefix, k - ) + "\nAllowed arguments: {}".format( # pylint: disable=no-member + ) + "\nAllowed arguments: {}".format( # pylint: disable=no-member ", ".join(sorted(self._prop_names)) ) ) @@ -266,16 +263,16 @@ def _traverse(self): for t in self._traverse_with_paths(): yield t[1] + @staticmethod + def _id_str(component): + id_ = stringify_id(getattr(component, "id", "")) + return id_ and " (id={:s})".format(id_) + def _traverse_with_paths(self): """Yield each item with its path in the tree.""" children = getattr(self, "children", None) children_type = type(children).__name__ - children_id = ( - "(id={:s})".format(children.id) - if getattr(children, "id", False) - else "" - ) - children_string = children_type + " " + children_id + children_string = children_type + self._id_str(children) # children is just a component if isinstance(children, Component): @@ -287,12 +284,10 @@ def _traverse_with_paths(self): # children is a list of components elif isinstance(children, (tuple, MutableSequence)): for idx, i in enumerate(children): - list_path = "[{:d}] {:s} {}".format( + list_path = "[{:d}] {:s}{}".format( idx, type(i).__name__, - "(id={:s})".format(i.id) - if getattr(i, "id", False) - else "", + self._id_str(i), ) yield list_path, i @@ -305,7 +300,6 @@ def __iter__(self): """Yield IDs in the tree of children.""" for t in self._traverse(): if isinstance(t, Component) and getattr(t, "id", None) is not None: - yield t.id def __len__(self): From aef7cca23a45f4b8ece04eb9e0531d33b38c4219 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 29 Jan 2020 22:51:10 -0500 Subject: [PATCH 06/81] python linting and a little wildcard validation fixing --- dash/_utils.py | 3 +- dash/_validate.py | 225 +++++++++++++++++++++++-------------------- dash/dependencies.py | 2 +- 3 files changed, 126 insertions(+), 104 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index 4c9f4149ab..86949ce53c 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -205,7 +205,8 @@ def stringify_id(id_): def inputs_to_dict(inputs): inputs = {} for i in inputs: - for ii in (i if isinstance(i, list) else [i]): + inputsi = i if isinstance(i, list) else [i] + for ii in inputsi: id_str = stringify_id(ii["id"]) inputs["{}.{}".format(id_str, ii["property"])] = ii.get("value") return inputs diff --git a/dash/_validate.py b/dash/_validate.py index 8d0768ef1e..8acd33b0ff 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -24,114 +24,25 @@ def validate_callback(app, layout, output, inputs, state): """ ) - outputs = output if is_multi else [output] - for args, cls in [(outputs, Output), (inputs, Input), (state, State)]: - validate_callback_args(args, cls, layout, validate_ids) - - if state and not inputs: + if not inputs: raise exceptions.MissingInputsException( """ - This callback has {} `State` element{} but no `Input` elements. - + This callback has no `Input` elements. Without `Input` elements, this callback will never get called. - (Subscribing to Input components will cause the - callback to be called whenever their values change.) - """.format( - len(state), "s" if len(state) > 1 else "" - ) - ) - - for i in inputs: - bad = None - if is_multi: - for o in output: - if o == i: - # Note: different but overlapping wildcards compare as equal - bad = o - elif output == i: - bad = output - if bad: - raise exceptions.SameInputOutputException( - "Same output and input: {}".format(bad) - ) - - if is_multi and len(set(output)) != len(output): - raise exceptions.DuplicateCallbackOutput( + Subscribing to Input components will cause the + callback to be called whenever their values change. """ - Same output was used more than once in a multi output callback! - Duplicates: - {} - """.format( - ",\n".join(str(x) for x in output if output.count(x) > 1) - ) ) - any_keys = get_wildcard_keys(outputs[0], (ANY,)) - for out in outputs[1:]: - if get_wildcard_keys(out, (ANY,)) != any_keys: - raise exceptions.InconsistentCallbackWildcards( - """ - All `Output` items must have matching wildcard `ANY` values. - `ALL` wildcards need not match, only `ANY`. - - Output {} does not match the first output {}. - """.format( - out, outputs[0] - ) - ) - - matched_wildcards = (ANY, ALLSMALLER) - for dep in list(inputs) + list(state): - wildcard_keys = get_wildcard_keys(dep, matched_wildcards) - if wildcard_keys - any_keys: - raise exceptions.InconsistentCallbackWildcards( - """ - `Input` and `State` items can only have {} - wildcards on keys where the `Output`(s) have `ANY` wildcards. - `ALL` wildcards need not match, and you need not match every - `ANY` in the `Output`(s). - - This callback has `ANY` on keys {}. - {} has these wildcards on keys {}. - """.format( - matched_wildcards, any_keys, dep, wildcard_keys - ) - ) + outputs = output if is_multi else [output] - dups = set() - for out in outputs: - for used_out in app.used_outputs: - if out == used_out: - dups.add(str(used_out)) - if dups: - if is_multi or len(dups) > 1 or str(output) != list(dups)[0]: - raise exceptions.DuplicateCallbackOutput( - """ - One or more `Output` is already set by a callback. - Note that two wildcard outputs can refer to the same component - even if they don't match exactly. + for args, cls in [(outputs, Output), (inputs, Input), (state, State)]: + validate_callback_args(args, cls, layout, validate_ids) - The new callback lists output(s): - {} - Already used: - {} - """.format( - ", ".join([str(o) for o in outputs]), - ", ".join(dups) - ) - ) - else: - raise exceptions.DuplicateCallbackOutput( - """ - {} was already assigned to a callback. - Any given output can only have one callback that sets it. - Try combining your inputs and callback functions together - into one function. - """.format( - repr(output) - ) - ) + prevent_duplicate_outputs(app, outputs) + prevent_input_output_overlap(inputs, outputs) + prevent_inconsistent_wildcards(outputs, inputs, state) def validate_callback_args(args, cls, layout, validate_ids): @@ -189,6 +100,117 @@ def validate_callback_args(args, cls, layout, validate_ids): ) +def prevent_duplicate_outputs(app, outputs): + for i, out in enumerate(outputs): + for out2 in outputs[i + 1:]: + if out == out2: + # Note: different but overlapping wildcards compare as equal + if str(out) == str(out2): + raise exceptions.DuplicateCallbackOutput( + """ + Same output {} was used more than once in a callback! + """.format( + str(out) + ) + ) + raise exceptions.DuplicateCallbackOutput( + """ + Two outputs in a callback can match the same ID! + {} and {} + """.format( + str(out), str(out2) + ) + ) + + dups = set() + for out in outputs: + for used_out in app.used_outputs: + if out == used_out: + dups.add(str(used_out)) + if dups: + dups = list(dups) + if len(outputs) > 1 or len(dups) > 1 or str(outputs[0]) != dups[0]: + raise exceptions.DuplicateCallbackOutput( + """ + One or more `Output` is already set by a callback. + Note that two wildcard outputs can refer to the same component + even if they don't match exactly. + + The new callback lists output(s): + {} + Already used: + {} + """.format( + ", ".join([str(out) for out in outputs]), + ", ".join(dups) + ) + ) + raise exceptions.DuplicateCallbackOutput( + """ + {} was already assigned to a callback. + Any given output can only have one callback that sets it. + Try combining your inputs and callback functions together + into one function. + """.format( + repr(outputs[0]) + ) + ) + + +def prevent_input_output_overlap(inputs, outputs): + for in_ in inputs: + for out in outputs: + if out == in_: + # Note: different but overlapping wildcards compare as equal + if str(out) == str(in_): + raise exceptions.SameInputOutputException( + "Same `Output` and `Input`: {}".format(out) + ) + raise exceptions.SameInputOutputException( + """ + An `Input` and an `Output` in one callback + can match the same ID! + {} and {} + """.format( + str(in_), str(out) + ) + ) + + +def prevent_inconsistent_wildcards(outputs, inputs, state): + any_keys = get_wildcard_keys(outputs[0], (ANY,)) + for out in outputs[1:]: + if get_wildcard_keys(out, (ANY,)) != any_keys: + raise exceptions.InconsistentCallbackWildcards( + """ + All `Output` items must have matching wildcard `ANY` values. + `ALL` wildcards need not match, only `ANY`. + + Output {} does not match the first output {}. + """.format( + out, outputs[0] + ) + ) + + matched_wildcards = (ANY, ALLSMALLER) + for dep in list(inputs) + list(state): + wildcard_keys = get_wildcard_keys(dep, matched_wildcards) + if wildcard_keys - any_keys: + raise exceptions.InconsistentCallbackWildcards( + """ + `Input` and `State` items can only have {} + wildcards on keys where the `Output`(s) have `ANY` wildcards. + `ALL` wildcards need not match, and you need not match every + `ANY` in the `Output`(s). + + This callback has `ANY` on keys {}. + {} has these wildcards on keys {}. + """.format( + matched_wildcards, any_keys, dep, wildcard_keys + ) + ) + + def validate_id_dict(arg, layout, validate_ids, wildcards): arg_id = arg.component_id @@ -204,7 +226,7 @@ def id_match(c): if id_match(layout): component = layout else: - for c in layout._traverse(): + for c in layout._traverse(): # pylint: disable=protected-access if id_match(c): component = c break @@ -533,8 +555,7 @@ def validate_layout(layout, layout_value): layout_id = stringify_id(getattr(layout_value, "id", None)) component_ids = {layout_id} if layout_id else set() - # pylint: disable=protected-access - for component in layout_value._traverse(): + for component in layout_value._traverse(): # pylint: disable=protected-access component_id = stringify_id(getattr(component, "id", None)) if component_id and component_id in component_ids: raise exceptions.DuplicateIdError( diff --git a/dash/dependencies.py b/dash/dependencies.py index bf10c3e61f..e6f0cb6234 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -44,7 +44,7 @@ def _json(k, v): return "{}:{}".format(json.dumps(k), vstr) if isinstance(i, dict): - return ("{" + ",".join(_json(k, i[k]) for k in sorted(i)) + "}") + return "{" + ",".join(_json(k, i[k]) for k in sorted(i)) + "}" return i From 58eaa965521d68b1c969e6e4b4b8b7341a645a05 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 29 Jan 2020 23:55:07 -0500 Subject: [PATCH 07/81] update tests for pendingCallbacks and new paths structure --- dash/testing/browser.py | 5 +- dash/testing/dash_page.py | 24 +- tests/integration/callbacks/state_path.json | 287 +++++++++--------- .../callbacks/test_basic_callback.py | 15 +- .../test_layout_paths_with_callbacks.py | 19 +- .../callbacks/test_multiple_callbacks.py | 3 +- .../integration/renderer/test_dependencies.py | 4 +- .../renderer/test_due_diligence.py | 7 +- 8 files changed, 181 insertions(+), 183 deletions(-) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index e89b894867..f23323ff2d 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -152,11 +152,8 @@ def percy_snapshot(self, name="", wait_for_callbacks=False): # as diff reference for the build run. logger.error( "wait_for_callbacks failed => status of invalid rqs %s", - list( - _ for _ in self.redux_state_rqs if not _.get("responseTime") - ), + self.redux_state_rqs, ) - logger.debug("full content of the rqs => %s", self.redux_state_rqs) self.percy_runner.snapshot(name=snapshot_name) diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py index 5c0445e596..855bf127e1 100644 --- a/dash/testing/dash_page.py +++ b/dash/testing/dash_page.py @@ -33,7 +33,15 @@ def redux_state_paths(self): @property def redux_state_rqs(self): return self.driver.execute_script( - "return window.store.getState().requestQueue" + """ + return window.store.getState().requestQueue.map(function(cb) { + var out = {}; + for (var key in cb) { + if (typeof cb[key] !== 'function') { out[key] = cb[key]; } + } + return out; + }) + """ ) @property @@ -41,19 +49,7 @@ def window_store(self): return self.driver.execute_script("return window.store") def _wait_for_callbacks(self): - if self.window_store: - # note that there is still a small chance of FP (False Positive) - # where we get two earlier requests in the queue, this returns - # True but there are still more requests to come - return self.redux_state_rqs and all( - ( - _.get("responseTime") - for _ in self.redux_state_rqs - if _.get("controllerId") - ) - ) - - return True + return not self.window_store or self.redux_state_rqs == [] def get_local_storage(self, store_id="local"): return self.driver.execute_script( diff --git a/tests/integration/callbacks/state_path.json b/tests/integration/callbacks/state_path.json index 7c6bf0ff87..94ca6b4dcd 100644 --- a/tests/integration/callbacks/state_path.json +++ b/tests/integration/callbacks/state_path.json @@ -1,146 +1,155 @@ { "chapter1": { - "toc": ["props", "children", 0], - "body": ["props", "children", 1], - "chapter1-header": [ - "props", - "children", - 1, - "props", - "children", - "props", - "children", - 0 - ], - "chapter1-controls": [ - "props", - "children", - 1, - "props", - "children", - "props", - "children", - 1 - ], - "chapter1-label": [ - "props", - "children", - 1, - "props", - "children", - "props", - "children", - 2 - ], - "chapter1-graph": [ - "props", - "children", - 1, - "props", - "children", - "props", - "children", - 3 - ] + "objs": {}, + "strs": { + "toc": ["props", "children", 0], + "body": ["props", "children", 1], + "chapter1-header": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 0 + ], + "chapter1-controls": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 1 + ], + "chapter1-label": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 2 + ], + "chapter1-graph": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 3 + ] + } }, "chapter2": { - "toc": ["props", "children", 0], - "body": ["props", "children", 1], - "chapter2-header": [ - "props", - "children", - 1, - "props", - "children", - "props", - "children", - 0 - ], - "chapter2-controls": [ - "props", - "children", - 1, - "props", - "children", - "props", - "children", - 1 - ], - "chapter2-label": [ - "props", - "children", - 1, - "props", - "children", - "props", - "children", - 2 - ], - "chapter2-graph": [ - "props", - "children", - 1, - "props", - "children", - "props", - "children", - 3 - ] + "objs": {}, + "strs": { + "toc": ["props", "children", 0], + "body": ["props", "children", 1], + "chapter2-header": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 0 + ], + "chapter2-controls": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 1 + ], + "chapter2-label": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 2 + ], + "chapter2-graph": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 3 + ] + } }, "chapter3": { - "toc": ["props", "children", 0], - "body": ["props", "children", 1], - "chapter3-header": [ - "props", - "children", - 1, - "props", - "children", - 0, - "props", - "children", - "props", - "children", - 0 - ], - "chapter3-label": [ - "props", - "children", - 1, - "props", - "children", - 0, - "props", - "children", - "props", - "children", - 1 - ], - "chapter3-graph": [ - "props", - "children", - 1, - "props", - "children", - 0, - "props", - "children", - "props", - "children", - 2 - ], - "chapter3-controls": [ - "props", - "children", - 1, - "props", - "children", - 0, - "props", - "children", - "props", - "children", - 3 - ] + "objs": {}, + "strs": { + "toc": ["props", "children", 0], + "body": ["props", "children", 1], + "chapter3-header": [ + "props", + "children", + 1, + "props", + "children", + 0, + "props", + "children", + "props", + "children", + 0 + ], + "chapter3-label": [ + "props", + "children", + 1, + "props", + "children", + 0, + "props", + "children", + "props", + "children", + 1 + ], + "chapter3-graph": [ + "props", + "children", + 1, + "props", + "children", + 0, + "props", + "children", + "props", + "children", + 2 + ], + "chapter3-controls": [ + "props", + "children", + 1, + "props", + "children", + 0, + "props", + "children", + "props", + "children", + 3 + ] + } } -} \ No newline at end of file +} diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index 48fd53a7b6..f13be38be8 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -1,12 +1,10 @@ from multiprocessing import Value -from bs4 import BeautifulSoup - import dash_core_components as dcc import dash_html_components as html import dash_table import dash -from dash.dependencies import Input, Output, State +from dash.dependencies import Input, Output from dash.exceptions import PreventUpdate @@ -42,8 +40,7 @@ def update_output(value): "hello world" ), "initial count + each key stroke" - rqs = dash_duo.redux_state_rqs - assert len(rqs) == 1 + assert dash_duo.redux_state_rqs == [] assert dash_duo.get_logs() == [] @@ -98,7 +95,9 @@ def update_input(value): dash_duo.percy_snapshot(name="callback-generating-function-1") - assert dash_duo.redux_state_paths == { + paths = dash_duo.redux_state_paths + assert paths["objs"] == {} + assert paths["strs"] == { "input": ["props", "children", 0], "output": ["props", "children", 1], "sub-input-1": [ @@ -136,9 +135,7 @@ def update_input(value): "#sub-output-1", pad_input.attrs["value"] + "deadbeef" ) - rqs = dash_duo.redux_state_rqs - assert rqs, "request queue is not empty" - assert all((rq["status"] == 200 and not rq["rejected"] for rq in rqs)) + assert dash_duo.redux_state_rqs == [], "pendingCallbacks is empty" dash_duo.percy_snapshot(name="callback-generating-function-2") assert dash_duo.get_logs() == [], "console is clean" diff --git a/tests/integration/callbacks/test_layout_paths_with_callbacks.py b/tests/integration/callbacks/test_layout_paths_with_callbacks.py index 3f4f5219fa..061e22cbc0 100644 --- a/tests/integration/callbacks/test_layout_paths_with_callbacks.py +++ b/tests/integration/callbacks/test_layout_paths_with_callbacks.py @@ -152,7 +152,7 @@ def check_chapter(chapter): '#{}-graph:not(.dash-graph--pending)'.format(chapter) ) - for key in dash_duo.redux_state_paths: + for key in dash_duo.redux_state_paths["strs"]: assert dash_duo.find_elements( "#{}".format(key) ), "each element should exist in the dom" @@ -169,20 +169,18 @@ def check_chapter(chapter): wait.until( lambda: ( dash_duo.driver.execute_script( - "return document." - 'querySelector("#{}-graph:not(.dash-graph--pending) .js-plotly-plot").'.format( + 'return document.querySelector("' + + "#{}-graph:not(.dash-graph--pending) .js-plotly-plot".format( chapter ) - + "layout.title.text" + + '").layout.title.text' ) == value ), TIMEOUT, ) - rqs = dash_duo.redux_state_rqs - assert rqs, "request queue is not empty" - assert all((rq["status"] == 200 and not rq["rejected"] for rq in rqs)) + assert dash_duo.redux_state_rqs == [], "pendingCallbacks is empty" def check_call_counts(chapters, count): for chapter in chapters: @@ -224,12 +222,15 @@ def check_call_counts(chapters, count): dash_duo.find_elements('input[type="radio"]')[3].click() # switch to 4 dash_duo.wait_for_text_to_equal("#body", "Just a string") dash_duo.percy_snapshot(name="chapter-4") - for key in dash_duo.redux_state_paths: + + paths = dash_duo.redux_state_paths + assert paths["objs"] == {} + for key in paths["strs"]: assert dash_duo.find_elements( "#{}".format(key) ), "each element should exist in the dom" - assert dash_duo.redux_state_paths == { + assert paths["strs"] == { "toc": ["props", "children", 0], "body": ["props", "children", 1], } diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index 7be877dd1f..84c8af1d78 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -31,8 +31,7 @@ def update_output(n_clicks): dash_duo.find_element("#output").text == "3" ), "clicked button 3 times" - rqs = dash_duo.redux_state_rqs - assert len(rqs) == 1 and not rqs[0]["rejected"] + assert dash_duo.redux_state_rqs == [] dash_duo.percy_snapshot( name="test_callbacks_called_multiple_times_and_out_of_order" diff --git a/tests/integration/renderer/test_dependencies.py b/tests/integration/renderer/test_dependencies.py index ef62c4406f..6213d71f7b 100644 --- a/tests/integration/renderer/test_dependencies.py +++ b/tests/integration/renderer/test_dependencies.py @@ -40,8 +40,6 @@ def update_output_2(value): assert output_1_call_count.value == 2 and output_2_call_count.value == 0 - rqs = dash_duo.redux_state_rqs - assert len(rqs) == 1 - assert rqs[0]["controllerId"] == "output-1.children" and not rqs[0]['rejected'] + assert dash_duo.redux_state_rqs == [] assert dash_duo.get_logs() == [] diff --git a/tests/integration/renderer/test_due_diligence.py b/tests/integration/renderer/test_due_diligence.py index 88ef5fbf29..720d78392c 100644 --- a/tests/integration/renderer/test_due_diligence.py +++ b/tests/integration/renderer/test_due_diligence.py @@ -87,7 +87,9 @@ def test_rddd001_initial_state(dash_duo): r.json() == [] ), "no dependencies present in app as no callbacks are defined" - assert dash_duo.redux_state_paths == { + paths = dash_duo.redux_state_paths + assert paths["objs"] == {} + assert paths["strs"] == { abbr: [ int(token) if token in string.digits @@ -102,8 +104,7 @@ def test_rddd001_initial_state(dash_duo): ) }, "paths should reflect to the component hierarchy" - rqs = dash_duo.redux_state_rqs - assert not rqs, "no callback => no requestQueue" + assert dash_duo.redux_state_rqs == [], "no callback => no pendingCallbacks" dash_duo.percy_snapshot(name="layout") assert dash_duo.get_logs() == [], "console has no errors" From 8305bbf90442eb215a83b0bbe880cf994cb59245 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 30 Jan 2020 00:39:46 -0500 Subject: [PATCH 08/81] fix fix redux_state_rqs --- dash/testing/dash_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py index 855bf127e1..ef24ae4a78 100644 --- a/dash/testing/dash_page.py +++ b/dash/testing/dash_page.py @@ -34,7 +34,7 @@ def redux_state_paths(self): def redux_state_rqs(self): return self.driver.execute_script( """ - return window.store.getState().requestQueue.map(function(cb) { + return window.store.getState().pendingCallbacks.map(function(cb) { var out = {}; for (var key in cb) { if (typeof cb[key] !== 'function') { out[key] = cb[key]; } From b517f8bcb805df3b100c8d227b1c3b056c1a0bd1 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 30 Jan 2020 00:48:21 -0500 Subject: [PATCH 09/81] fix isAppReady test --- dash-renderer/tests/isAppReady.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dash-renderer/tests/isAppReady.test.js b/dash-renderer/tests/isAppReady.test.js index a85ea81240..4202c845d9 100644 --- a/dash-renderer/tests/isAppReady.test.js +++ b/dash-renderer/tests/isAppReady.test.js @@ -19,7 +19,7 @@ describe('isAppReady', () => { let done = false; Promise.resolve(isAppReady( [{ namespace: '__components', type: 'b', props: { id: 'comp1' } }], - { comp1: [0] }, + { strs: { comp1: [0] }, objs: {} }, ['comp1'] )).then(() => { done = true @@ -33,7 +33,7 @@ describe('isAppReady', () => { let done = false; Promise.resolve(isAppReady( [{ namespace: '__components', type: 'a', props: { id: 'comp1' } }], - { comp1: [0] }, + { strs: { comp1: [0] }, objs: {} }, ['comp1'] )).then(() => { done = true @@ -47,4 +47,4 @@ describe('isAppReady', () => { await new Promise(r => setTimeout(r, WAIT)); expect(done).toEqual(true); }); -}); \ No newline at end of file +}); From 830ab23b72d388c5e5002115d94421c013b0e275 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 30 Jan 2020 01:08:46 -0500 Subject: [PATCH 10/81] more robust moveHistory --- dash-renderer/src/actions/index.js | 33 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index bf108a56df..8fdc1aea8b 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -102,29 +102,30 @@ function triggerDefaultState(dispatch, getState) { dispatch(startCallbacks(initialCallbacks)); } -export const redo = move_history('REDO'); -export const undo = move_history('UNDO'); -export const revert = move_history('REVERT'); +export const redo = moveHistory('REDO'); +export const undo = moveHistory('UNDO'); +export const revert = moveHistory('REVERT'); -function move_history(changeType) { +function moveHistory(changeType) { return function(dispatch, getState) { const {history, paths} = getState(); dispatch(createAction(changeType)()); const {id, props} = - changeType === 'REDO' + (changeType === 'REDO' ? history.future[0] - : history.past[history.past.length - 1]; - - // Update props - dispatch( - createAction('UNDO_PROP_CHANGE')({ - itempath: getPath(paths, id), - props, - }) - ); + : history.past[history.past.length - 1]) || {}; + if (id) { + // Update props + dispatch( + createAction('UNDO_PROP_CHANGE')({ + itempath: getPath(paths, id), + props, + }) + ); - // Notify observers - dispatch(notifyObservers({id, props})); + // Notify observers + dispatch(notifyObservers({id, props})); + } }; } From e0fea66bf207dec48c79f72815084f095ea6549b Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 30 Jan 2020 01:44:11 -0500 Subject: [PATCH 11/81] ugh py2 unicode fix --- dash/_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dash/_utils.py b/dash/_utils.py index 86949ce53c..bf4c92c846 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -16,7 +16,8 @@ logger = logging.getLogger() # py2/3 json.dumps-compatible strings - these are equivalent in py3, not in py2 -_strings = (type(u""), type("")) +# note because we import unicode_literals u"" and "" are both unicode +_strings = (type(u""), type(utils.bytes_to_native_str(b""))) def interpolate_str(template, **data): From 3acf89b5aa6f080f8c4edb26c189d003952e003f Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 30 Jan 2020 02:06:15 -0500 Subject: [PATCH 12/81] fix single-output as multi --- dash-renderer/src/actions/dependencies.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 06c79b4459..441e199723 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -65,7 +65,7 @@ function parseWildcardId(idStr) { * "..output-1.value...output-2.value...output-3.value...output-4.value.." */ function parseMultipleOutputs(outputIdAndProp) { - return outputIdAndProp.split('...').map(o => o.replace('..', '')); + return outputIdAndProp.substr(2, outputIdAndProp.length - 4).split('...'); } export function splitIdAndProp(idAndProp) { From 822b935b83aa569cb71b5fdcb1e7aec02d4d2655 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 30 Jan 2020 18:33:31 -0500 Subject: [PATCH 13/81] fix circular dep check and initial callback prevention --- dash-renderer/src/actions/dependencies.js | 56 +++++++++++-------- .../integration/renderer/test_multi_output.py | 6 +- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 441e199723..6123886e67 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -1,6 +1,7 @@ import {DepGraph} from 'dependency-graph'; import isNumeric from 'fast-isnumeric'; import { + all, any, ap, assoc, @@ -170,10 +171,15 @@ export function computeGraphs(dependencies) { const wildcardPlaceholders = {}; const fixIds = map(evolve({id: parseIfWildcard})); - const parsedDependencies = map( - evolve({inputs: fixIds, state: fixIds}), - dependencies - ); + const parsedDependencies = map(dep => { + const {output} = dep; + const out = evolve({inputs: fixIds, state: fixIds}, dep); + out.outputs = map( + outi => assoc('out', true, splitIdAndProp(outi)), + isMultiOutputProp(output) ? parseMultipleOutputs(output) : [output] + ); + return out; + }, dependencies); /* * For regular ids, outputMap and inputMap are: @@ -203,15 +209,7 @@ export function computeGraphs(dependencies) { const inputPatterns = {}; parsedDependencies.forEach(dependency => { - const {output, inputs} = dependency; - const outputStrs = isMultiOutputProp(output) - ? parseMultipleOutputs(output) - : [output]; - const outputs = outputStrs.map(outputStr => { - const outputObj = splitIdAndProp(outputStr); - outputObj.out = true; - return outputObj; - }); + const {outputs, inputs} = dependency; // TODO: what was this (and exactChange) about??? // const depWildcardExact = {}; @@ -299,7 +297,7 @@ export function computeGraphs(dependencies) { } parsedDependencies.forEach(function registerDependency(dependency) { - const {output, inputs} = dependency; + const {output, outputs, inputs} = dependency; // multiGraph - just for testing circularity @@ -326,12 +324,6 @@ export function computeGraphs(dependencies) { }); } - const outStrs = isMultiOutputProp(output) - ? parseMultipleOutputs(output) - : [output]; - - const outputs = outStrs.map(splitIdAndProp); - // We'll continue to use dep.output as its id, but add outputs as well // for convenience and symmetry with the structure of inputs and state. // Also collect ANY keys in the output (all outputs must share these) @@ -352,7 +344,8 @@ export function computeGraphs(dependencies) { dependency ); - outputs.forEach(({id: outId, property}) => { + outputs.forEach(outIdProp => { + const {id: outId, property} = outIdProp; if (typeof outId === 'object') { const outIdList = makeAllIds(outId, {}); outIdList.forEach(id => { @@ -361,7 +354,7 @@ export function computeGraphs(dependencies) { addPattern(outputPatterns, outId, property, finalDependency); } else { - addOutputToMulti({}, outId); + addOutputToMulti({}, combineIdAndProp(outIdProp)); addMap(outputMap, outId, property, finalDependency); } }); @@ -800,7 +793,24 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { // We still need to follow these forward in order to capture blocks and, // if based on a partial layout, any knock-on effects in the full layout. - return followForward(graphs, paths, callbacks); + const finalCallbacks = followForward(graphs, paths, callbacks); + + // Exception to the `initialCall` case of callbacks found by output: + // if *every* input to this callback is itself an output of another + // callback earlier in the chain, we remove the `initialCall` flag + // so that if all of those prior callbacks abort all of their outputs, + // this later callback never runs. + // See test inin003 "callback2 is never triggered, even on initial load" + finalCallbacks.forEach(cb => { + if (cb.initialCall && !isEmpty(cb.blockedBy)) { + const inputs = flatten(cb.getInputs(paths)); + if (all(i => cb.changedPropIds[combineIdAndProp(i)], inputs)) { + cb.initialCall = false; + } + } + }); + + return finalCallbacks; } export function removePendingCallback( diff --git a/tests/integration/renderer/test_multi_output.py b/tests/integration/renderer/test_multi_output.py index 6a97ccd0b6..70db83633d 100644 --- a/tests/integration/renderer/test_multi_output.py +++ b/tests/integration/renderer/test_multi_output.py @@ -138,8 +138,10 @@ def set_bc(a): dev_tools_hot_reload=False ) - # the UI still renders the output triggered by callback - dash_duo.wait_for_text_to_equal("#c", "X" * 100) + # the UI still renders the output triggered by callback. + # The new system does NOT loop infinitely like it used to, each callback + # is invoked no more than once. + dash_duo.wait_for_text_to_equal("#c", "X") err_text = dash_duo.find_element("span.dash-fe-error__title").text assert err_text == "Circular Dependencies" From 97f9aa03b52eb02ab652c335d434bfd84fd31493 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 30 Jan 2020 23:37:56 -0500 Subject: [PATCH 14/81] fix persistence edge case for pendingCallbacks world --- dash-renderer/src/AccessDenied.react.js | 1 - dash-renderer/src/actions/index.js | 56 ++++++++++++++++++------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/dash-renderer/src/AccessDenied.react.js b/dash-renderer/src/AccessDenied.react.js index f70625548a..bb0c361c27 100644 --- a/dash-renderer/src/AccessDenied.react.js +++ b/dash-renderer/src/AccessDenied.react.js @@ -31,7 +31,6 @@ function AccessDenied(props) { { - /* eslint no-empty: ["error", { "allowEmptyCatch": true }] */ try { document.cookie = `${constants.OAUTH_COOKIE_NAME}=; ` + diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 8fdc1aea8b..c78385947e 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -4,6 +4,7 @@ import { concat, flatten, has, + isEmpty, keys, lensPath, map, @@ -123,7 +124,6 @@ function moveHistory(changeType) { }) ); - // Notify observers dispatch(notifyObservers({id, props})); } }; @@ -308,6 +308,24 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { appliedProps.children ); } + + // persistence edge case: if you explicitly update the + // persistence key, other props may change that require us + // to fire additional callbacks + const addedProps = pickBy( + (v, k) => !(k in props), + appliedProps + ); + if (!isEmpty(addedProps)) { + const {graphs, paths} = getState(); + pendingCallbacks = includeObservers( + id, + addedProps, + graphs, + paths, + pendingCallbacks + ); + } } }); updatePending(pendingCallbacks, without(updated, allPropIds)); @@ -553,23 +571,33 @@ function updateChildPaths(dispatch, getState, pendingCallbacks, id, children) { export function notifyObservers({id, props}) { return async function(dispatch, getState) { const {graphs, paths, pendingCallbacks} = getState(); - - const changedProps = keys(props); - let finalCallbacks = pendingCallbacks; - - changedProps.forEach(propName => { - const newCBs = getCallbacksByInput(graphs, paths, id, propName); - if (newCBs.length) { - finalCallbacks = mergePendingCallbacks( - finalCallbacks, - followForward(graphs, paths, newCBs) - ); - } - }); + const finalCallbacks = includeObservers( + id, + props, + graphs, + paths, + pendingCallbacks + ); dispatch(startCallbacks(finalCallbacks)); }; } +function includeObservers(id, props, graphs, paths, pendingCallbacks) { + const changedProps = keys(props); + let finalCallbacks = pendingCallbacks; + + changedProps.forEach(propName => { + const newCBs = getCallbacksByInput(graphs, paths, id, propName); + if (newCBs.length) { + finalCallbacks = mergePendingCallbacks( + finalCallbacks, + followForward(graphs, paths, newCBs) + ); + } + }); + return finalCallbacks; +} + export function handleAsyncError(err, message, dispatch) { // Handle html error responses if (err && typeof err.text === 'function') { From 2019f8f6192eac0733918b485adaac2ea3db5acd Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 30 Jan 2020 23:39:26 -0500 Subject: [PATCH 15/81] fix callback_context --- dash/_utils.py | 4 ++-- dash/dash.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dash/_utils.py b/dash/_utils.py index bf4c92c846..08def4d9e0 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -203,9 +203,9 @@ def stringify_id(id_): return id_ -def inputs_to_dict(inputs): +def inputs_to_dict(inputs_list): inputs = {} - for i in inputs: + for i in inputs_list: inputsi = i if isinstance(i, list) else [i] for ii in inputsi: id_str = stringify_id(ii["id"]) diff --git a/dash/dash.py b/dash/dash.py index 7fe922b941..0a87bebb9d 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -925,7 +925,7 @@ def add_context(*args, **kwargs): if isinstance(output_value, _NoUpdate): raise PreventUpdate - output_spec = flask.g.outputs_list + output_spec = self._outputs_list # wrap single outputs so we can treat them all the same # for validation and response creation if not multi: @@ -975,7 +975,8 @@ def dispatch(self): flask.g.inputs_list = inputs = body.get("inputs", []) flask.g.states_list = state = body.get("state", []) output = body["output"] - flask.g.outputs_list = body.get("outputs") or split_callback_id(output) + outputs_list = body.get("outputs") or split_callback_id(output) + flask.g.outputs_list = self._outputs_list = outputs_list flask.g.input_values = input_values = inputs_to_dict(inputs) flask.g.state_values = inputs_to_dict(state) @@ -991,6 +992,7 @@ def dispatch(self): args = inputs_to_vals(inputs) + inputs_to_vals(state) response.set_data(self.callback_map[output]["callback"](*args)) + del self._outputs_list return response def _setup_server(self): From 126f53a5cd0f71d656d03a6a4cac43a07d41adc4 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 30 Jan 2020 23:40:08 -0500 Subject: [PATCH 16/81] update tests --- tests/integration/test_integration.py | 20 ++++++++--- tests/integration/test_render.py | 51 +++++++++++---------------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 35fe3ad9af..4902d3da5d 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -486,8 +486,8 @@ def overlapping_multi_output(n_clicks): "no part of an existing multi-output can be used in another" ) assert ( - "{'output1.children'}" in err.value.args[0] - or "set(['output1.children'])" in err.value.args[0] + "Already used:" in err.value.args[0] and + "output1.children" in err.value.args[0] ) dash_duo.start_server(app) @@ -824,7 +824,7 @@ def test_inin018_output_input_invalid_callback(): def failure(children): pass - msg = "Same output and input: input-output.children" + msg = "Same `Output` and `Input`: input-output.children" assert err.value.args[0] == msg # Multi output version. @@ -837,7 +837,7 @@ def failure(children): def failure2(children): pass - msg = "Same output and input: input-output.children" + msg = "Same `Output` and `Input`: input-output.children" assert err.value.args[0] == msg @@ -907,6 +907,10 @@ def single(a): return set([1]) with pytest.raises(InvalidCallbackReturnValue): + # _outputs_list (normally callback_context.outputs_list) is provided + # by the dispatcher from the request. Here we're calling locally so + # we need to mock it. + app._outputs_list = {"id": "b", "property": "children"} single("aaa") pytest.fail("not serializable") @@ -918,6 +922,10 @@ def multi(a): return [1, set([2])] with pytest.raises(InvalidCallbackReturnValue): + app._outputs_list = [ + {"id": "c", "property": "children"}, + {"id": "d", "property": "children"} + ] multi("aaa") pytest.fail("nested non-serializable") @@ -929,6 +937,10 @@ def multi2(a): return ["abc"] with pytest.raises(InvalidCallbackReturnValue): + app._outputs_list = [ + {"id": "e", "property": "children"}, + {"id": "f", "property": "children"} + ] multi2("aaa") pytest.fail("wrong-length list") diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index a230edf903..36f9383565 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -47,29 +47,6 @@ def wait_for_text_to_equal(self, selector, assertion_text, timeout=TIMEOUT): ) ) - def request_queue_assertions( - self, check_rejected=True, expected_length=None): - request_queue = self.driver.execute_script( - 'return window.store.getState().requestQueue' - ) - self.assertTrue( - all([ - (r['status'] == 200) - for r in request_queue - ]) - ) - - if check_rejected: - self.assertTrue( - all([ - (r['rejected'] is False) - for r in request_queue - ]) - ) - - if expected_length is not None: - self.assertEqual(len(request_queue), expected_length) - def click_undo(self): undo_selector = '._dash-undo-redo span:first-child div:last-child' undo = self.wait_for_element_by_css_selector(undo_selector) @@ -511,11 +488,10 @@ def update_output(n_clicks): self.assertEqual(call_count.value, 3) self.wait_for_text_to_equal('#output1', '2') self.wait_for_text_to_equal('#output2', '3') - request_queue = self.driver.execute_script( - 'return window.store.getState().requestQueue' + pending_count = self.driver.execute_script( + 'return window.store.getState().pendingCallbacks.length' ) - self.assertFalse(request_queue[0]['rejected']) - self.assertEqual(len(request_queue), 1) + self.assertEqual(pending_count, 0) def test_callbacks_with_shared_grandparent(self): app = dash.Dash() @@ -892,10 +868,25 @@ def update_output(value): self.wait_for_text_to_equal('#output-1', 'fire request hooks') self.wait_for_text_to_equal('#output-pre', 'request_pre changed this text!') - self.wait_for_text_to_equal('#output-pre-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') self.wait_for_text_to_equal('#output-post', 'request_post changed this text!') - self.wait_for_text_to_equal('#output-post-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') - self.wait_for_text_to_equal('#output-post-response', '{"props":{"children":"fire request hooks"}}') + pre_payload = self.wait_for_element_by_css_selector('#output-pre-payload').text + post_payload = self.wait_for_element_by_css_selector('#output-post-payload').text + post_response = self.wait_for_element_by_css_selector('#output-post-response').text + self.assertEqual(json.loads(pre_payload), { + "output": "output-1.children", + "outputs": {"id": "output-1", "property": "children"}, + "changedPropIds": ["input.value"], + "inputs": [{"id": "input", "property": "value", "value": "fire request hooks"}] + }) + self.assertEqual(json.loads(post_payload), { + "output": "output-1.children", + "outputs": {"id": "output-1", "property": "children"}, + "changedPropIds": ["input.value"], + "inputs": [{"id": "input", "property": "value", "value": "fire request hooks"}] + }) + self.assertEqual(json.loads(post_response), { + "output-1": {"children": "fire request hooks"} + }) self.percy_snapshot(name='request-hooks render') def test_graphs_in_tabs_do_not_share_state(self): From 53a81c3cf6b6838e2c8f9c267e6442d13cdd41e9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 30 Jan 2020 23:46:47 -0500 Subject: [PATCH 17/81] lint --- dash/dash.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 0a87bebb9d..21d3056054 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -348,6 +348,10 @@ def __init__( self.logger = logging.getLogger(name) self.logger.addHandler(logging.StreamHandler(stream=sys.stdout)) + # outputs in the current callback - this is also in callback_context + # but it's used internally to validate outputs, so we keep a local copy + self._outputs_list = None + if isinstance(plugins, patch_collections_abc("Iterable")): for plugin in plugins: plugin.plug(self) @@ -992,7 +996,7 @@ def dispatch(self): args = inputs_to_vals(inputs) + inputs_to_vals(state) response.set_data(self.callback_map[output]["callback"](*args)) - del self._outputs_list + self._outputs_list = None return response def _setup_server(self): From b4ad9633a338656e3f9e9fd1dc9dcc4700886e71 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 31 Jan 2020 00:18:43 -0500 Subject: [PATCH 18/81] bring back previous error message formatting --- dash-renderer/src/actions/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index c78385947e..a08ed52771 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -339,9 +339,9 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { // that have other changed inputs will still fire. updatePending(pendingCallbacks, allPropIds); } - let message = `Callback error updating ${JSON.stringify( - payload.outputs - )}`; + let message = `Callback error updating ${ + map(combineIdAndProp, flatten(payload.outputs)).join(', ') + }`; if (clientside_function) { const {namespace: ns, function_name: fn} = clientside_function; message += ` via clientside function ${ns}.${fn}`; From 04c328923613ea739a9f10da820847f2c20987e5 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 31 Jan 2020 00:21:19 -0500 Subject: [PATCH 19/81] lint --- dash-renderer/src/actions/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index a08ed52771..bb3ad6e0d3 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -339,9 +339,10 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { // that have other changed inputs will still fire. updatePending(pendingCallbacks, allPropIds); } - let message = `Callback error updating ${ - map(combineIdAndProp, flatten(payload.outputs)).join(', ') - }`; + let message = `Callback error updating ${map( + combineIdAndProp, + flatten(payload.outputs) + ).join(', ')}`; if (clientside_function) { const {namespace: ns, function_name: fn} = clientside_function; message += ` via clientside function ${ns}.${fn}`; From 8a246c1deaf2f386ceffd1bcde65c3ed477b5f6c Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 31 Jan 2020 19:22:35 -0500 Subject: [PATCH 20/81] fix error reporting for single-output callbacks --- dash-renderer/src/actions/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index bb3ad6e0d3..5064b1e0d5 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -341,7 +341,7 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { } let message = `Callback error updating ${map( combineIdAndProp, - flatten(payload.outputs) + flatten([payload.outputs]) ).join(', ')}`; if (clientside_function) { const {namespace: ns, function_name: fn} = clientside_function; From 4a320129688ecba660b1fd6983c64331c1e71cd3 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 3 Feb 2020 08:51:45 -0500 Subject: [PATCH 21/81] ANY -> MATCH --- dash-renderer/src/actions/dependencies.js | 32 +++++++++++------------ dash-renderer/src/actions/index.js | 4 +-- dash/_validate.py | 18 ++++++------- dash/dependencies.py | 12 ++++----- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 6123886e67..07cc6fba91 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -37,9 +37,9 @@ import {crawlLayout} from './utils'; export const isMultiOutputProp = idAndProp => idAndProp.startsWith('..'); const ALL = {wild: 'ALL', multi: 1}; -const ANY = {wild: 'ANY'}; +const MATCH = {wild: 'MATCH'}; const ALLSMALLER = {wild: 'ALLSMALLER', multi: 1, expand: 1}; -const wildcards = {ALL, ANY, ALLSMALLER}; +const wildcards = {ALL, MATCH, ALLSMALLER}; /* * If this ID is a wildcard, it is a stringified JSON object @@ -186,7 +186,7 @@ export function computeGraphs(dependencies) { * {[id]: {[prop]: [callback, ...]}} * where callbacks are the matching specs from the original * dependenciesRequest, but with outputs parsed to look like inputs, - * and a list anyKeys added if the outputs have ANY wildcards. + * and a list anyKeys added if the outputs have MATCH wildcards. * For outputMap there should only ever be one callback per id/prop * but for inputMap there may be many. * @@ -259,7 +259,7 @@ export function computeGraphs(dependencies) { } } } else if (!exact.length) { - // only ANY/ALL - still need a value + // only MATCH/ALL - still need a value vals.push(0); } keyPlaceholders.vals = vals; @@ -280,8 +280,8 @@ export function computeGraphs(dependencies) { newVals = []; } } else { - // ANY or ALL - // ANY *is* ALL for outputs, ie we don't already have a + // MATCH or ALL + // MATCH *is* ALL for outputs, ie we don't already have a // value specified in `outIdFinal` newVals = outValIndex === -1 || val === ALL @@ -326,13 +326,13 @@ export function computeGraphs(dependencies) { // We'll continue to use dep.output as its id, but add outputs as well // for convenience and symmetry with the structure of inputs and state. - // Also collect ANY keys in the output (all outputs must share these) + // Also collect MATCH keys in the output (all outputs must share these) // and ALL keys in the first output (need not be shared but we'll use // the first output for calculations) for later convenience. const anyKeys = []; let hasAll = false; forEachObjIndexed((val, key) => { - if (val === ANY) { + if (val === MATCH) { anyKeys.push(key); } else if (val === ALL) { hasAll = true; @@ -391,7 +391,7 @@ export function computeGraphs(dependencies) { * we're only looking at ids with the same keys as the pattern. * * Optionally, include another reference set of the same - to ensure the - * correct matching of ANY or ALLSMALLER between input and output items. + * correct matching of MATCH or ALLSMALLER between input and output items. */ function idMatch(keys, vals, patternVals, refKeys, refVals, refPatternVals) { for (let i = 0; i < keys.length; i++) { @@ -441,7 +441,7 @@ function idMatch(keys, vals, patternVals, refKeys, refVals, refPatternVals) { function getAnyVals(patternVals, vals) { const matches = []; for (let i = 0; i < patternVals.length; i++) { - if (patternVals[i] === ANY) { + if (patternVals[i] === MATCH) { matches.push(vals[i]); } } @@ -476,7 +476,7 @@ const resolveDeps = (refKeys, refVals, refPatternVals) => paths => ({ /* * Create a pending callback object. Includes the original callback definition, - * its resolved ID (including the value of all ANY wildcards), + * its resolved ID (including the value of all MATCH wildcards), * accessors to find all inputs, outputs, and state involved in this * callback (lazy as not all users will want all of these), * placeholders for which other callbacks this one is blockedBy or blocking, @@ -521,8 +521,8 @@ export function isMultiValued({id}) { * If one is found, returns: * { * callback: the callback spec {outputs, inputs, state etc} - * anyVals: stringified list of resolved ANY keys we matched - * resolvedId: the "outputs" id string plus ANY values we matched + * anyVals: stringified list of resolved MATCH keys we matched + * resolvedId: the "outputs" id string plus MATCH values we matched * getOutputs: accessor function to give all resolved outputs of this * callback. Takes `paths` as argument to apply when the callback is * dispatched, in case a previous callback has altered the layout. @@ -583,7 +583,7 @@ function getCallbackByOutput(graphs, paths, id, prop) { /* * If there are ALL keys we need to reduce a set of outputs resolved - * from an input to one item per combination of ANY values. + * from an input to one item per combination of MATCH values. * That will give one result per callback invocation. */ function reduceALLOuts(outs, anyKeys, hasAll) { @@ -591,7 +591,7 @@ function reduceALLOuts(outs, anyKeys, hasAll) { return outs; } if (!anyKeys.length) { - // If there's ALL but no ANY, there's only one invocation + // If there's ALL but no MATCH, there's only one invocation // of the callback, so just base it off the first output. return [outs[0]]; } @@ -630,7 +630,7 @@ function addResolvedFromOutputs(callback, outPattern, outs, matches) { * * Note that if the original input contains an ALLSMALLER wildcard, * there may be many entries for the same callback, but any given output - * (with an ANY corresponding to the input's ALLSMALLER) will only appear + * (with an MATCH corresponding to the input's ALLSMALLER) will only appear * in one entry. */ export function getCallbacksByInput(graphs, paths, id, prop) { diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 5064b1e0d5..90262764cc 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -156,7 +156,7 @@ function unwrapIfNotMulti(paths, idProps, spec, anyVals, depType) { depType + '` of a Dash callback. The id of this object is ' + JSON.stringify(spec.id) + - (anyVals ? ' with ANY values ' + anyVals : '') + + (anyVals ? ' with MATCH values ' + anyVals : '') + ' and the property is `' + spec.property + '`. The wildcard ids currently available are logged above.' @@ -167,7 +167,7 @@ function unwrapIfNotMulti(paths, idProps, spec, anyVals, depType) { depType + '` of a callback that only takes one value. The id spec is ' + JSON.stringify(spec.id) + - (anyVals ? ' with ANY values ' + anyVals : '') + + (anyVals ? ' with MATCH values ' + anyVals : '') + ' and the property is `' + spec.property + '`. The objects we found are: ' + diff --git a/dash/_validate.py b/dash/_validate.py index 8acd33b0ff..a5f9a2aaf2 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -2,7 +2,7 @@ import re from .development.base_component import Component -from .dependencies import Input, Output, State, ANY, ALLSMALLER +from .dependencies import Input, Output, State, MATCH, ALLSMALLER from . import exceptions from ._utils import patch_collections_abc, _strings, stringify_id @@ -178,13 +178,13 @@ def prevent_input_output_overlap(inputs, outputs): def prevent_inconsistent_wildcards(outputs, inputs, state): - any_keys = get_wildcard_keys(outputs[0], (ANY,)) + any_keys = get_wildcard_keys(outputs[0], (MATCH,)) for out in outputs[1:]: - if get_wildcard_keys(out, (ANY,)) != any_keys: + if get_wildcard_keys(out, (MATCH,)) != any_keys: raise exceptions.InconsistentCallbackWildcards( """ - All `Output` items must have matching wildcard `ANY` values. - `ALL` wildcards need not match, only `ANY`. + All `Output` items must have matching wildcard `MATCH` values. + `ALL` wildcards need not match, only `MATCH`. Output {} does not match the first output {}. """.format( @@ -192,18 +192,18 @@ def prevent_inconsistent_wildcards(outputs, inputs, state): ) ) - matched_wildcards = (ANY, ALLSMALLER) + matched_wildcards = (MATCH, ALLSMALLER) for dep in list(inputs) + list(state): wildcard_keys = get_wildcard_keys(dep, matched_wildcards) if wildcard_keys - any_keys: raise exceptions.InconsistentCallbackWildcards( """ `Input` and `State` items can only have {} - wildcards on keys where the `Output`(s) have `ANY` wildcards. + wildcards on keys where the `Output`(s) have `MATCH` wildcards. `ALL` wildcards need not match, and you need not match every - `ANY` in the `Output`(s). + `MATCH` in the `Output`(s). - This callback has `ANY` on keys {}. + This callback has `MATCH` on keys {}. {} has these wildcards on keys {}. """.format( matched_wildcards, any_keys, dep, wildcard_keys diff --git a/dash/dependencies.py b/dash/dependencies.py index e6f0cb6234..7b2f7fdb69 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -17,7 +17,7 @@ def to_json(self): return '["{}"]'.format(self._name) -ANY = _Wildcard("ANY") +MATCH = _Wildcard("MATCH") ALL = _Wildcard("ALL") ALLSMALLER = _Wildcard("ALLSMALLER") @@ -89,8 +89,8 @@ def _id_matches(self, other): continue # one wild, one not if v is ALL or other_v is ALL: continue # either ALL - if v is ANY or other_v is ANY: - return False # one ANY, one ALLSMALLER + if v is MATCH or other_v is MATCH: + return False # one MATCH, one ALLSMALLER else: return False return True @@ -105,19 +105,19 @@ def __hash__(self): class Output(DashDependency): # pylint: disable=too-few-public-methods """Output of a callback.""" - allowed_wildcards = (ANY, ALL) + allowed_wildcards = (MATCH, ALL) class Input(DashDependency): # pylint: disable=too-few-public-methods """Input of callback: trigger an update when it is updated.""" - allowed_wildcards = (ANY, ALL, ALLSMALLER) + allowed_wildcards = (MATCH, ALL, ALLSMALLER) class State(DashDependency): # pylint: disable=too-few-public-methods """Use the value of a State in a callback but don't trigger updates.""" - allowed_wildcards = (ANY, ALL, ALLSMALLER) + allowed_wildcards = (MATCH, ALL, ALLSMALLER) class ClientsideFunction: # pylint: disable=too-few-public-methods From abceaf44ed6979fc16cb11bee4c55f3dc64d6134 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 3 Feb 2020 22:25:38 -0500 Subject: [PATCH 22/81] wildcards initial test --- dash-renderer/src/TreeContainer.js | 4 +- tests/integration/callbacks/test_wildcards.py | 218 ++++++++++++++++++ 2 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 tests/integration/callbacks/test_wildcards.py diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index 9f22a94ce8..12f1f834cb 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -183,8 +183,8 @@ class TreeContainer extends Component { const props = dissoc('children', _dashprivate_layout.props); if (type(props.id) === 'Object') { - // Turn object ids (for wildcards) into hash strings. - // Because of the `dissoc` we're not mutating the layout, + // Turn object ids (for wildcards) into unique strings. + // Because of the `dissoc` above we're not mutating the layout, // just the id we pass on to the rendered component props.id = stringifyId(props.id); } diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py new file mode 100644 index 0000000000..f964f2c4ab --- /dev/null +++ b/tests/integration/callbacks/test_wildcards.py @@ -0,0 +1,218 @@ +from multiprocessing import Value +import re + +import dash_html_components as html +import dash_core_components as dcc +import dash +from dash.dependencies import Input, Output, State, ALL, ALLSMALLER, MATCH + + +def css_escape(s): + sel = re.sub('[\\{\\}\\"\\\'.:,]', lambda m: '\\' + m.group(0), s) + print(sel) + return sel + + +def todo_app(): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Div('Dash To-Do list'), + dcc.Input(id="new-item"), + html.Button("Add", id="add"), + html.Button("Clear Done", id="clear-done"), + html.Div(id="list-container"), + html.Hr(), + html.Div(id="totals") + ]) + + style_todo = {"display": "inline", "margin": "10px"} + style_done = {"textDecoration": "line-through", "color": "#888"} + style_done.update(style_todo) + + app.list_calls = Value('i', 0) + app.style_calls = Value('i', 0) + app.preceding_calls = Value('i', 0) + app.total_calls = Value('i', 0) + + @app.callback( + [ + Output("list-container", "children"), + Output("new-item", "value") + ], + [ + Input("add", "n_clicks"), + Input("new-item", "n_submit"), + Input("clear-done", "n_clicks") + ], + [ + State("new-item", "value"), + State({"item": ALL}, "children"), + State({"item": ALL, "action": "done"}, "value") + ] + ) + def edit_list(add, add2, clear, new_item, items, items_done): + app.list_calls.value += 1 + triggered = [t["prop_id"] for t in dash.callback_context.triggered] + adding = len( + [1 for i in triggered if i in ("add.n_clicks", "new-item.n_submit")] + ) + clearing = len([1 for i in triggered if i == "clear-done.n_clicks"]) + new_spec = [ + (text, done) for text, done in zip(items, items_done) + if not (clearing and done) + ] + if adding: + new_spec.append((new_item, [])) + new_list = [ + html.Div([ + dcc.Checklist( + id={"item": i, "action": "done"}, + options=[{"label": "", "value": "done"}], + value=done, + style={"display": "inline"} + ), + html.Div( + text, + id={"item": i}, + style=style_done if done else style_todo + ), + html.Div(id={"item": i, "preceding": True}, style=style_todo) + ], style={"clear": "both"}) + for i, (text, done) in enumerate(new_spec) + ] + return [new_list, "" if adding else new_item] + + @app.callback( + Output({"item": MATCH}, "style"), + [Input({"item": MATCH, "action": "done"}, "value")] + ) + def mark_done(done): + app.style_calls.value += 1 + return style_done if done else style_todo + + @app.callback( + Output({"item": MATCH, "preceding": True}, "children"), + [ + Input({"item": ALLSMALLER, "action": "done"}, "value"), + Input({"item": MATCH, "action": "done"}, "value") + ] + ) + def show_preceding(done_before, this_done): + app.preceding_calls.value += 1 + if this_done: + return "" + all_before = len(done_before) + done_before = len([1 for d in done_before if d]) + out = "{} of {} preceding items are done".format(done_before, all_before) + if all_before == done_before: + out += " DO THIS NEXT!" + return out + + @app.callback( + Output("totals", "children"), + [Input({"item": ALL, "action": "done"}, "value")] + ) + def show_totals(done): + app.total_calls.value += 1 + count_all = len(done) + count_done = len([d for d in done if d]) + result = "{} of {} items completed".format(count_done, count_all) + if count_all: + result += " - {}%".format(int(100 * count_done / count_all)) + return result + + return app + + +def test_cbwc001_todo_app(dash_duo): + app = todo_app() + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#totals", "0 of 0 items completed") + assert app.list_calls.value == 1 + assert app.style_calls.value == 0 + assert app.preceding_calls.value == 0 + assert app.total_calls.value == 1 + + new_item = dash_duo.find_element("#new-item") + add_item = dash_duo.find_element("#add") + clear_done = dash_duo.find_element("#clear-done") + + def assert_count(items): + assert len(dash_duo.find_elements("#list-container>div")) == items + + def get_done_item(item): + selector = css_escape('#{"action":"done","item":%d} input' % item) + return dash_duo.find_element(selector) + + def assert_item(item, text, done, prefix="", suffix=""): + text_el = dash_duo.find_element(css_escape('#{"item":%d}' % item)) + assert text_el.text == text, "item {} text".format(item) + + note_el = dash_duo.find_element( + css_escape('#{"item":%d,"preceding":true}' % item) + ) + expected_note = "" if done else (prefix + " preceding items are done" + suffix) + assert note_el.text == expected_note, "item {} note".format(item) + + assert bool(get_done_item(item).get_attribute('checked')) == done + + new_item.send_keys("apples") + add_item.click() + dash_duo.wait_for_text_to_equal("#totals", "0 of 1 items completed - 0%") + assert_count(1) + + new_item.send_keys("bananas") + add_item.click() + dash_duo.wait_for_text_to_equal("#totals", "0 of 2 items completed - 0%") + assert_count(2) + + new_item.send_keys("carrots") + add_item.click() + dash_duo.wait_for_text_to_equal("#totals", "0 of 3 items completed - 0%") + assert_count(3) + + new_item.send_keys("dates") + add_item.click() + dash_duo.wait_for_text_to_equal("#totals", "0 of 4 items completed - 0%") + assert_count(4) + assert_item(0, "apples", False, "0 of 0", " DO THIS NEXT!") + assert_item(1, "bananas", False, "0 of 1") + assert_item(2, "carrots", False, "0 of 2") + assert_item(3, "dates", False, "0 of 3") + + get_done_item(2).click() + dash_duo.wait_for_text_to_equal("#totals", "1 of 4 items completed - 25%") + assert_item(0, "apples", False, "0 of 0", " DO THIS NEXT!") + assert_item(1, "bananas", False, "0 of 1") + assert_item(2, "carrots", True) + assert_item(3, "dates", False, "1 of 3") + + get_done_item(0).click() + dash_duo.wait_for_text_to_equal("#totals", "2 of 4 items completed - 50%") + assert_item(0, "apples", True) + assert_item(1, "bananas", False, "1 of 1", " DO THIS NEXT!") + assert_item(2, "carrots", True) + assert_item(3, "dates", False, "2 of 3") + + clear_done.click() + dash_duo.wait_for_text_to_equal("#totals", "0 of 2 items completed - 0%") + assert_count(2) + assert_item(0, "bananas", False, "0 of 0", " DO THIS NEXT!") + assert_item(1, "dates", False, "0 of 1") + + get_done_item(0).click() + dash_duo.wait_for_text_to_equal("#totals", "1 of 2 items completed - 50%") + assert_item(0, "bananas", True) + assert_item(1, "dates", False, "1 of 1", " DO THIS NEXT!") + + get_done_item(1).click() + dash_duo.wait_for_text_to_equal("#totals", "2 of 2 items completed - 100%") + assert_item(0, "bananas", True) + assert_item(1, "dates", True) + + clear_done.click() + # TODO - totals currently broken + # dash_duo.wait_for_text_to_equal("#totals", "0 of 0 items completed") + # assert_count(0) From e896169f79248ae965c903f21ca164f71ae03150 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 3 Feb 2020 22:26:29 -0500 Subject: [PATCH 23/81] some test tweaks - linting, and hopefully robustifying graphs in percy --- .../test_layout_paths_with_callbacks.py | 9 +++++-- tests/integration/renderer/test_iframe.py | 4 ++- .../integration/renderer/test_persistence.py | 20 +++++++++++--- .../renderer/test_state_and_input.py | 8 +++--- tests/integration/test_race_conditions.py | 2 +- tests/integration/test_scripts.py | 26 ++++--------------- 6 files changed, 36 insertions(+), 33 deletions(-) diff --git a/tests/integration/callbacks/test_layout_paths_with_callbacks.py b/tests/integration/callbacks/test_layout_paths_with_callbacks.py index 061e22cbc0..8ac9d61a5d 100644 --- a/tests/integration/callbacks/test_layout_paths_with_callbacks.py +++ b/tests/integration/callbacks/test_layout_paths_with_callbacks.py @@ -117,12 +117,17 @@ def callback(value): return { "data": [ { - "x": ["Call Counter"], + "x": ["Call Counter for: {}".format(counterId)], "y": [call_counts[counterId].value], "type": "bar", } ], - "layout": {"title": value}, + "layout": { + "title": value, + "width": 500, + "height": 400, + "margin": {"autoexpand": False} + }, } return callback diff --git a/tests/integration/renderer/test_iframe.py b/tests/integration/renderer/test_iframe.py index 6a4a93f52c..1f4cc9191d 100644 --- a/tests/integration/renderer/test_iframe.py +++ b/tests/integration/renderer/test_iframe.py @@ -31,7 +31,9 @@ def update_output(n_clicks): @app.server.after_request def apply_cors(response): response.headers["Access-Control-Allow-Origin"] = "*" - response.headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Authorization" + response.headers["Access-Control-Allow-Headers"] = ( + "Origin, X-Requested-With, Content-Type, Accept, Authorization" + ) return response dash_duo.start_server(app) diff --git a/tests/integration/renderer/test_persistence.py b/tests/integration/renderer/test_persistence.py index 70c62d49cc..5c004daa16 100644 --- a/tests/integration/renderer/test_persistence.py +++ b/tests/integration/renderer/test_persistence.py @@ -99,12 +99,18 @@ def test_rdps001_local_reload(dash_duo): rename_and_hide(dash_duo) # callback output - dash_duo.wait_for_text_to_equal('#out', 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME)) + dash_duo.wait_for_text_to_equal( + '#out', + 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME) + ) check_table_names(dash_duo, [NEW_NAME]) dash_duo.wait_for_page() # callback gets persisted values, not the values provided with the layout - dash_duo.wait_for_text_to_equal('#out', 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME)) + dash_duo.wait_for_text_to_equal( + '#out', + 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME) + ) check_table_names(dash_duo, [NEW_NAME]) # new persistence reverts @@ -118,7 +124,10 @@ def test_rdps001_local_reload(dash_duo): # put back the old persistence, get the old values app.persistence.value = 1 dash_duo.wait_for_page() - dash_duo.wait_for_text_to_equal('#out', 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME)) + dash_duo.wait_for_text_to_equal( + '#out', + 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME) + ) check_table_names(dash_duo, [NEW_NAME]) # falsy persistence disables it @@ -146,7 +155,10 @@ def test_rdps002_session_reload(dash_duo): dash_duo.wait_for_page() # callback gets persisted values, not the values provided with the layout - dash_duo.wait_for_text_to_equal('#out', 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME)) + dash_duo.wait_for_text_to_equal( + '#out', + 'names: [{}, b]; hidden: [c1]'.format(NEW_NAME) + ) check_table_names(dash_duo, [NEW_NAME]) diff --git a/tests/integration/renderer/test_state_and_input.py b/tests/integration/renderer/test_state_and_input.py index 7ffe1ddbbe..3192ce950f 100644 --- a/tests/integration/renderer/test_state_and_input.py +++ b/tests/integration/renderer/test_state_and_input.py @@ -30,8 +30,8 @@ def update_output(input, state): dash_duo.start_server(app) - input_ = lambda: dash_duo.find_element("#input") - output_ = lambda: dash_duo.find_element("#output") + def input_(): return dash_duo.find_element("#input") + def output_(): return dash_duo.find_element("#output") assert ( output_().text == 'input="Initial Input", state="Initial State"' @@ -81,8 +81,8 @@ def update_output(input, n_clicks, state): dash_duo.start_server(app) - btn = lambda: dash_duo.find_element("#button") - output = lambda: dash_duo.find_element("#output") + def btn(): return dash_duo.find_element("#button") + def output(): return dash_duo.find_element("#output") assert ( output().text == 'input="Initial Input", state="Initial State"' diff --git a/tests/integration/test_race_conditions.py b/tests/integration/test_race_conditions.py index b597068e3c..b2616de2cb 100644 --- a/tests/integration/test_race_conditions.py +++ b/tests/integration/test_race_conditions.py @@ -42,7 +42,7 @@ def delay(): def element_text(id): try: return self.driver.find_element_by_id(id).text - except: + except Exception: return '' app.server.before_request(delay) diff --git a/tests/integration/test_scripts.py b/tests/integration/test_scripts.py index 3900ea6979..ed9d5660f2 100644 --- a/tests/integration/test_scripts.py +++ b/tests/integration/test_scripts.py @@ -1,31 +1,15 @@ -from multiprocessing import Value -import datetime import time import pytest -from bs4 import BeautifulSoup -from selenium.webdriver.common.keys import Keys - -import dash_dangerously_set_inner_html -import dash_flow_example +from selenium.webdriver.common.by import By import dash_html_components as html import dash_core_components as dcc -from dash import Dash, callback_context, no_update - -from dash.dependencies import Input, Output, State -from dash.exceptions import ( - PreventUpdate, - DuplicateCallbackOutput, - CallbackException, - MissingCallbackContextException, - InvalidCallbackReturnValue, - IncorrectTypeException, - NonExistentIdException, -) -from dash.testing.wait import until -from selenium.webdriver.common.by import By +from dash import Dash + +from dash.dependencies import Input, Output +from dash.exceptions import PreventUpdate def findSyncPlotlyJs(scripts): From 7c188301ef463a5e30ee5e781ecbf69917a0ce68 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Feb 2020 19:05:52 -0500 Subject: [PATCH 24/81] trigger wildcard callbacks when items deleted from an array input --- dash-renderer/src/actions/dependencies.js | 36 +++++++++++++++-- dash-renderer/src/actions/index.js | 40 +++++++++++++++++-- tests/integration/callbacks/test_wildcards.py | 6 +-- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 07cc6fba91..343d9e78df 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -728,13 +728,18 @@ export function getWatchedKeys(id, newProps, graphs) { * * opts.outputsOnly: boolean, set true when crawling the *whole* layout, * because outputs are enough to get everything. + * opts.removedArrayInputsOnly: boolean, set true to only look for inputs in + * wildcard arrays (ALL or ALLSMALLER), no outputs. This gets used to tell + * when the new *absence* of a given component should trigger a callback. + * opts.newPaths: paths object after the edit - to be used with + * removedArrayInputsOnly to determine if the callback still has its outputs * * Returns an array of objects: * {callback, resolvedId, getOutputs, getInputs, getState, ...etc} * See getCallbackByOutput for details. */ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { - const {outputsOnly} = opts || {}; + const {outputsOnly, removedArrayInputsOnly, newPaths} = opts || {}; const foundCbIds = {}; const callbacks = []; @@ -753,6 +758,26 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { } } + function addCallbackIfArray(idStr) { + return cb => + cb.getInputs(paths).some(ini => { + if ( + Array.isArray(ini) && + ini.some(inij => stringifyId(inij.id) === idStr) + ) { + // This callback should trigger even with no changedProps, + // since the props that changed no longer exist. + if (flatten(cb.getOutputs(newPaths)).length) { + cb.initialCall = true; + cb.changedPropIds = {}; + addCallback(cb); + } + return true; + } + return false; + }); + } + function handleOneId(id, outIdCallbacks, inIdCallbacks) { if (outIdCallbacks) { for (const property in outIdCallbacks) { @@ -765,9 +790,12 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { } } if (!outputsOnly && inIdCallbacks) { + const idStr = removedArrayInputsOnly && stringifyId(id); for (const property in inIdCallbacks) { getCallbacksByInput(graphs, paths, id, property).forEach( - addCallback + removedArrayInputsOnly + ? addCallbackIfArray(idStr) + : addCallback ); } } @@ -776,7 +804,7 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { crawlLayout(layoutChunk, child => { const id = path(['props', 'id'], child); if (id) { - if (typeof id === 'string') { + if (typeof id === 'string' && !removedArrayInputsOnly) { handleOneId(id, graphs.outputMap[id], graphs.inputMap[id]); } else { const keyStr = Object.keys(id) @@ -784,7 +812,7 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { .join(','); handleOneId( id, - graphs.outputPatterns[keyStr], + !removedArrayInputsOnly && graphs.outputPatterns[keyStr], graphs.inputPatterns[keyStr] ); } diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 90262764cc..26e846631e 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -284,6 +284,8 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { Object.entries(data).forEach(([id, props]) => { const parsedId = parseIfWildcard(id); + const {layout: oldLayout, paths: oldPaths} = getState(); + const appliedProps = doUpdateProps( dispatch, getState, @@ -296,16 +298,25 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { }); if (has('children', appliedProps)) { + const oldChildren = path( + concat(getPath(oldPaths, parsedId), [ + 'props', + 'children', + ]), + oldLayout + ); // If components changed, need to update paths, // check if all pending callbacks are still // valid, and add all callbacks associated with - // new components, either as inputs or outputs + // new components, either as inputs or outputs, + // or components removed from ALL/ALLSMALLER inputs pendingCallbacks = updateChildPaths( dispatch, getState, pendingCallbacks, parsedId, - appliedProps.children + appliedProps.children, + oldChildren ); } @@ -522,7 +533,14 @@ function doUpdateProps(dispatch, getState, id, updatedProps) { return props; } -function updateChildPaths(dispatch, getState, pendingCallbacks, id, children) { +function updateChildPaths( + dispatch, + getState, + pendingCallbacks, + id, + children, + oldChildren +) { const {paths: oldPaths, graphs} = getState(); const childrenPath = concat(getPath(oldPaths, id), ['props', 'children']); const paths = computePaths(children, childrenPath, oldPaths); @@ -566,7 +584,21 @@ function updateChildPaths(dispatch, getState, pendingCallbacks, id, children) { }); const newCallbacks = getCallbacksInLayout(graphs, paths, children); - return mergePendingCallbacks(cleanedCallbacks, newCallbacks); + + // Wildcard callbacks with array inputs (ALL / ALLSMALLER) need to trigger + // even due to the deletion of components + const deletedComponentCallbacks = getCallbacksInLayout( + graphs, + oldPaths, + oldChildren, + {removedArrayInputsOnly: true, newPaths: paths} + ); + + const allNewCallbacks = mergePendingCallbacks( + newCallbacks, + deletedComponentCallbacks + ); + return mergePendingCallbacks(cleanedCallbacks, allNewCallbacks); } export function notifyObservers({id, props}) { diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index f964f2c4ab..bffdbe1c7d 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -213,6 +213,6 @@ def assert_item(item, text, done, prefix="", suffix=""): assert_item(1, "dates", True) clear_done.click() - # TODO - totals currently broken - # dash_duo.wait_for_text_to_equal("#totals", "0 of 0 items completed") - # assert_count(0) + # This was a tricky one - trigger based on deleted components + dash_duo.wait_for_text_to_equal("#totals", "0 of 0 items completed") + assert_count(0) From 817d790929e35358428f9810d3f9e1e21e4f6669 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Feb 2020 22:39:41 -0500 Subject: [PATCH 25/81] extra tests & debugging to help with reported errors --- dash/_validate.py | 9 ++- dash/dash.py | 9 +++ .../callbacks/test_basic_callback.py | 65 ++++++++++++++++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/dash/_validate.py b/dash/_validate.py index a5f9a2aaf2..b23328fa2d 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -350,18 +350,21 @@ def validate_multi_return(outputs_list, output_value, callback_id): The callback {} ouput {} is a wildcard multi-output. Expected the output type to be a list or tuple but got: {}. + output spec: {} """.format( - callback_id, i, repr(vi) + callback_id, i, repr(vi), repr(outi) ) ) if len(vi) != len(outi): raise exceptions.InvalidCallbackReturnValue( """ - Invalid number of output values for {}. + Invalid number of output values for {} item {}. Expected {}, got {} + output spec: {} + output value: {} """.format( - callback_id, len(vi), len(outi) + callback_id, i, len(vi), len(outi), repr(outi), repr(vi) ) ) diff --git a/dash/dash.py b/dash/dash.py index 21d3056054..2eebb57438 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -949,6 +949,15 @@ def add_context(*args, **kwargs): if isinstance(spec, list) else [[val, spec]] ): + if not spec: + raise ValueError( + ( + "missing output spec\n" + "callback_id: {!r}\n" + "output spec: {!r}\n" + "return value: {!r}" + ).format(callback_id, output_spec, output_value) + ) if not isinstance(vali, _NoUpdate): has_update = True id_str = stringify_id(speci["id"]) diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index f13be38be8..bc29e9b694 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -168,6 +168,67 @@ def update_graph(n_clicks): dash_duo.start_server(app) - dash_duo.find_element('#btn').click() - assert dash_duo.find_element('#output').text == "Bye" + dash_duo.find_element("#btn").click() + assert dash_duo.find_element("#output").text == "Bye" assert dash_duo.get_logs() == [] + + +def test_cbsc004_children_types(dash_duo): + app = dash.Dash() + app.layout = html.Div([ + html.Button(id="btn"), + html.Div("init", id="out") + ]) + + outputs = [ + [None, ""], + ["a string", "a string"], + [123, "123"], + [123.45, "123.45"], + [[6, 7, 8], "678"], + [["a", "list", "of", "strings"], "alistofstrings"], + [["strings", 2, "numbers"], "strings2numbers"], + [["a string", html.Div("and a div")], "a string\nand a div"] + ] + + @app.callback(Output("out", "children"), [Input("btn", "n_clicks")]) + def set_children(n): + if n is None or n > len(outputs): + return dash.no_update + return outputs[n - 1][0] + + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#out", "init") + + for children, text in outputs: + dash_duo.find_element("#btn").click() + dash_duo.wait_for_text_to_equal("#out", text) + + +def test_cbsc005_array_of_objects(dash_duo): + app = dash.Dash() + app.layout = html.Div([ + html.Button(id="btn"), + dcc.Dropdown(id="dd"), + html.Div(id="out") + ]) + + @app.callback(Output("dd", "options"), [Input("btn", "n_clicks")]) + def set_options(n): + return [ + {"label": "opt{}".format(i), "value": i} + for i in range(n or 0) + ] + + @app.callback(Output("out", "children"), [Input("dd", "options")]) + def set_out(opts): + print(repr(opts)) + return len(opts) + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#out", "0") + for i in range(5): + dash_duo.find_element("#btn").click() + dash_duo.wait_for_text_to_equal("#out", str(i + 1)) + dash_duo.select_dcc_dropdown("#dd", "opt{}".format(i)) From a7766327b23f142cc15e269c72a07fa5c0d9f605 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 5 Feb 2020 17:17:33 -0500 Subject: [PATCH 26/81] thread-safe passing of outputs_list to callback wrapper --- dash/dash.py | 22 +++++----------------- tests/integration/test_integration.py | 16 +++++++--------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 2eebb57438..8f9271c937 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -348,10 +348,6 @@ def __init__( self.logger = logging.getLogger(name) self.logger.addHandler(logging.StreamHandler(stream=sys.stdout)) - # outputs in the current callback - this is also in callback_context - # but it's used internally to validate outputs, so we keep a local copy - self._outputs_list = None - if isinstance(plugins, patch_collections_abc("Iterable")): for plugin in plugins: plugin.plug(self) @@ -923,13 +919,14 @@ def callback(self, output, inputs, state=()): def wrap_func(func): @wraps(func) def add_context(*args, **kwargs): + output_spec = kwargs.pop("outputs_list") + # don't touch the comment on the next line - used by debugger output_value = func(*args, **kwargs) # %% callback invoked %% if isinstance(output_value, _NoUpdate): raise PreventUpdate - output_spec = self._outputs_list # wrap single outputs so we can treat them all the same # for validation and response creation if not multi: @@ -949,15 +946,6 @@ def add_context(*args, **kwargs): if isinstance(spec, list) else [[val, spec]] ): - if not spec: - raise ValueError( - ( - "missing output spec\n" - "callback_id: {!r}\n" - "output spec: {!r}\n" - "return value: {!r}" - ).format(callback_id, output_spec, output_value) - ) if not isinstance(vali, _NoUpdate): has_update = True id_str = stringify_id(speci["id"]) @@ -989,7 +977,7 @@ def dispatch(self): flask.g.states_list = state = body.get("state", []) output = body["output"] outputs_list = body.get("outputs") or split_callback_id(output) - flask.g.outputs_list = self._outputs_list = outputs_list + flask.g.outputs_list = outputs_list flask.g.input_values = input_values = inputs_to_dict(inputs) flask.g.state_values = inputs_to_dict(state) @@ -1004,8 +992,8 @@ def dispatch(self): args = inputs_to_vals(inputs) + inputs_to_vals(state) - response.set_data(self.callback_map[output]["callback"](*args)) - self._outputs_list = None + func = self.callback_map[output]["callback"] + response.set_data(func(*args, outputs_list=outputs_list)) return response def _setup_server(self): diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 4902d3da5d..a60b43e30c 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -907,11 +907,9 @@ def single(a): return set([1]) with pytest.raises(InvalidCallbackReturnValue): - # _outputs_list (normally callback_context.outputs_list) is provided - # by the dispatcher from the request. Here we're calling locally so - # we need to mock it. - app._outputs_list = {"id": "b", "property": "children"} - single("aaa") + # outputs_list (normally callback_context.outputs_list) is provided + # by the dispatcher from the request. + single("aaa", outputs_list={"id": "b", "property": "children"}) pytest.fail("not serializable") @app.callback( @@ -922,11 +920,11 @@ def multi(a): return [1, set([2])] with pytest.raises(InvalidCallbackReturnValue): - app._outputs_list = [ + outputs_list = [ {"id": "c", "property": "children"}, {"id": "d", "property": "children"} ] - multi("aaa") + multi("aaa", outputs_list=outputs_list) pytest.fail("nested non-serializable") @app.callback( @@ -937,11 +935,11 @@ def multi2(a): return ["abc"] with pytest.raises(InvalidCallbackReturnValue): - app._outputs_list = [ + outputs_list = [ {"id": "e", "property": "children"}, {"id": "f", "property": "children"} ] - multi2("aaa") + multi2("aaa", outputs_list=outputs_list) pytest.fail("wrong-length list") From a28a62d306801bba71002fc876b75edeb64f8630 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 5 Feb 2020 17:23:23 -0500 Subject: [PATCH 27/81] robustify wildcard test --- tests/integration/callbacks/test_wildcards.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index bffdbe1c7d..97d9f4bff7 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -147,14 +147,14 @@ def get_done_item(item): return dash_duo.find_element(selector) def assert_item(item, text, done, prefix="", suffix=""): - text_el = dash_duo.find_element(css_escape('#{"item":%d}' % item)) - assert text_el.text == text, "item {} text".format(item) - - note_el = dash_duo.find_element( - css_escape('#{"item":%d,"preceding":true}' % item) + dash_duo.wait_for_text_to_equal( + css_escape('#{"item":%d}' % item), text ) + expected_note = "" if done else (prefix + " preceding items are done" + suffix) - assert note_el.text == expected_note, "item {} note".format(item) + dash_duo.wait_for_text_to_equal( + css_escape('#{"item":%d,"preceding":true}' % item), expected_note + ) assert bool(get_done_item(item).get_attribute('checked')) == done From 95e69365d96ddc6a3b28cf43dc085e2fe1086822 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 5 Feb 2020 22:45:43 -0500 Subject: [PATCH 28/81] split out existing callback_context tests to their own file --- .../callbacks/test_callback_context.py | 75 +++++++++++++++++++ tests/integration/test_integration.py | 69 +---------------- 2 files changed, 76 insertions(+), 68 deletions(-) create mode 100644 tests/integration/callbacks/test_callback_context.py diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py new file mode 100644 index 0000000000..e7fcab18ef --- /dev/null +++ b/tests/integration/callbacks/test_callback_context.py @@ -0,0 +1,75 @@ +import pytest +import dash_html_components as html +import dash_core_components as dcc + +from dash import Dash, callback_context + +from dash.dependencies import Input, Output + +from dash.exceptions import PreventUpdate, MissingCallbackContextException + + +def test_cbcx001_modified_response(dash_duo): + app = Dash(__name__) + app.layout = html.Div( + [dcc.Input(id="input", value="ab"), html.Div(id="output")] + ) + + @app.callback(Output("output", "children"), [Input("input", "value")]) + def update_output(value): + callback_context.response.set_cookie( + "dash cookie", value + " - cookie" + ) + return value + " - output" + + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#output", "ab - output") + input1 = dash_duo.find_element("#input") + + input1.send_keys("cd") + + dash_duo.wait_for_text_to_equal("#output", "abcd - output") + cookie = dash_duo.driver.get_cookie("dash cookie") + # cookie gets json encoded + assert cookie["value"] == '"abcd - cookie"' + + assert not dash_duo.get_logs() + + +def test_cbcx002_triggered(dash_duo): + app = Dash(__name__) + + btns = ["btn-{}".format(x) for x in range(1, 6)] + + app.layout = html.Div( + [ + html.Div([html.Button(btn, id=btn) for btn in btns]), + html.Div(id="output"), + ] + ) + + @app.callback( + Output("output", "children"), [Input(x, "n_clicks") for x in btns] + ) + def on_click(*args): + if not callback_context.triggered: + raise PreventUpdate + trigger = callback_context.triggered[0] + return "Just clicked {} for the {} time!".format( + trigger["prop_id"].split(".")[0], trigger["value"] + ) + + dash_duo.start_server(app) + + for i in range(1, 5): + for btn in btns: + dash_duo.find_element("#" + btn).click() + dash_duo.wait_for_text_to_equal( + "#output", "Just clicked {} for the {} time!".format(btn, i) + ) + + +def test_cbcx003_no_callback_context(): + for attr in ["inputs", "states", "triggered", "response"]: + with pytest.raises(MissingCallbackContextException): + getattr(callback_context, attr) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index a60b43e30c..7c23526fd5 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -13,14 +13,13 @@ import dash_html_components as html import dash_core_components as dcc -from dash import Dash, callback_context, no_update +from dash import Dash, no_update from dash.dependencies import Input, Output, State from dash.exceptions import ( PreventUpdate, DuplicateCallbackOutput, CallbackException, - MissingCallbackContextException, InvalidCallbackReturnValue, IncorrectTypeException, NonExistentIdException, @@ -755,33 +754,6 @@ def update_output(value): dash_duo.percy_snapshot(name="request-hooks interpolated") -def test_inin016_modified_response(dash_duo): - app = Dash(__name__) - app.layout = html.Div( - [dcc.Input(id="input", value="ab"), html.Div(id="output")] - ) - - @app.callback(Output("output", "children"), [Input("input", "value")]) - def update_output(value): - callback_context.response.set_cookie( - "dash cookie", value + " - cookie" - ) - return value + " - output" - - dash_duo.start_server(app) - dash_duo.wait_for_text_to_equal("#output", "ab - output") - input1 = dash_duo.find_element("#input") - - input1.send_keys("cd") - - dash_duo.wait_for_text_to_equal("#output", "abcd - output") - cookie = dash_duo.driver.get_cookie("dash cookie") - # cookie gets json encoded - assert cookie["value"] == '"abcd - cookie"' - - assert not dash_duo.get_logs() - - def test_inin017_late_component_register(dash_duo): app = Dash() @@ -943,45 +915,6 @@ def multi2(a): pytest.fail("wrong-length list") -def test_inin021_callback_context(dash_duo): - app = Dash(__name__) - - btns = ["btn-{}".format(x) for x in range(1, 6)] - - app.layout = html.Div( - [ - html.Div([html.Button(btn, id=btn) for btn in btns]), - html.Div(id="output"), - ] - ) - - @app.callback( - Output("output", "children"), [Input(x, "n_clicks") for x in btns] - ) - def on_click(*args): - if not callback_context.triggered: - raise PreventUpdate - trigger = callback_context.triggered[0] - return "Just clicked {} for the {} time!".format( - trigger["prop_id"].split(".")[0], trigger["value"] - ) - - dash_duo.start_server(app) - - for i in range(1, 5): - for btn in btns: - dash_duo.find_element("#" + btn).click() - dash_duo.wait_for_text_to_equal( - "#output", "Just clicked {} for the {} time!".format(btn, i) - ) - - -def test_inin022_no_callback_context(): - for attr in ["inputs", "states", "triggered", "response"]: - with pytest.raises(MissingCallbackContextException): - getattr(callback_context, attr) - - def test_inin023_wrong_callback_id(): app = Dash(__name__) app.layout = html.Div( From 8186fa576f57909abcd5b21a2cc57eed92e12cf2 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 6 Feb 2020 12:44:36 -0500 Subject: [PATCH 29/81] backward compatibility fix for callback_context.triggered --- dash/_callback_context.py | 14 +++++++- .../callbacks/test_callback_context.py | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/dash/_callback_context.py b/dash/_callback_context.py index 7a31afd7f3..ee0243590b 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -18,6 +18,14 @@ def assert_context(*args, **kwargs): return assert_context +class FalsyList(list): + def __bool__(self): + return False + + +falsy_triggered = FalsyList([{"prop_id": ".", "value": None}]) + + # pylint: disable=no-init class CallbackContext: @property @@ -33,7 +41,11 @@ def states(self): @property @has_context def triggered(self): - return getattr(flask.g, "triggered_inputs", []) + # For backward compatibility: previously `triggered` always had a + # value - to avoid breaking existing apps, add a dummy item but + # make the list still look falsy. So `if ctx.triggered` will make it + # look empty, but you can still do `triggered[0]["prop_id"].split(".")` + return getattr(flask.g, "triggered_inputs", []) or falsy_triggered @property @has_context diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index e7fcab18ef..5fd435eb85 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -73,3 +73,36 @@ def test_cbcx003_no_callback_context(): for attr in ["inputs", "states", "triggered", "response"]: with pytest.raises(MissingCallbackContextException): getattr(callback_context, attr) + + +def test_cbcx004_triggered_backward_compat(dash_duo): + app = Dash(__name__) + app.layout = html.Div([ + html.Button("click!", id="btn"), + html.Div(id="out") + ]) + + @app.callback(Output("out", "children"), [Input("btn", "n_clicks")]) + def report_triggered(n): + triggered = callback_context.triggered + bool_val = "truthy" if triggered else "falsy" + split_propid = repr(triggered[0]["prop_id"].split(".")) + full_val = repr(triggered) + return "triggered is {}, has prop/id {}, and full value {}".format( + bool_val, split_propid, full_val + ) + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal( + "#out", + "triggered is falsy, has prop/id ['', ''], and full value " + "[{'prop_id': '.', 'value': None}]" + ) + + dash_duo.find_element("#btn").click() + dash_duo.wait_for_text_to_equal( + "#out", + "triggered is truthy, has prop/id ['btn', 'n_clicks'], and full value " + "[{'prop_id': 'btn.n_clicks', 'value': 1}]" + ) From db070b31ad5abae34b962a7531d7c26d6ab30b1a Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 9 Feb 2020 22:13:34 -0500 Subject: [PATCH 30/81] handle callback queue race condition new callbacks can't delete existing ones added elsewhere --- dash-renderer/src/actions/index.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 26e846631e..6cdf4d9ac9 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -229,6 +229,29 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { await isAppReady(layout, paths, ids); const allCallbacks = concat(requestedCallbacks, blockedCallbacks); + + // because of the async step above, make sure we haven't separately added + // more callbacks to the queue - this particular step should only be adding + // callbacks, not removing them. + const existingCallbacks = getState().pendingCallbacks; + if (existingCallbacks.length) { + const resolvedIds = {}; + allCallbacks.forEach((cb, i) => { + resolvedIds[cb.resolvedId] = i; + }); + existingCallbacks.forEach(existingCB => { + const iAll = resolvedIds[existingCB.resolvedId]; + if (iAll === undefined) { + allCallbacks.push(existingCB); + } else if (existingCB.requestId && !allCallbacks[iAll].requestId) { + // already requested put the requested one in the queue + allCallbacks[iAll] = existingCB; + } + // otherwise either both are blocked, fine, either one will do... + // or both have been requested - either way keep the newer one + }); + } + dispatch(setPendingCallbacks(allCallbacks)); function fireNext() { From 7a192b28bf1653a10cd66a6dc92023d4e17ca8d3 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 10 Feb 2020 09:20:35 -0500 Subject: [PATCH 31/81] simpler - and hopefully more robust - solution to callback race issue just set pendingCallbacks before the async await instead of after --- dash-renderer/src/actions/index.js | 44 +++++------------------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 6cdf4d9ac9..ad91507918 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -177,7 +177,7 @@ function unwrapIfNotMulti(paths, idProps, spec, anyVals, depType) { return idProps[0]; } -export function startCallbacks(callbacks) { +function startCallbacks(callbacks) { return async function(dispatch, getState) { return await fireReadyCallbacks(dispatch, getState, callbacks); }; @@ -215,45 +215,15 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { return cbOut; }); - const ids = uniq( - pluck( - 'id', - flatten( - requestedCallbacks.map(cb => - concat(cb.getInputs(paths), cb.getState(paths)) - ) - ) - ) - ); - - await isAppReady(layout, paths, ids); - const allCallbacks = concat(requestedCallbacks, blockedCallbacks); - - // because of the async step above, make sure we haven't separately added - // more callbacks to the queue - this particular step should only be adding - // callbacks, not removing them. - const existingCallbacks = getState().pendingCallbacks; - if (existingCallbacks.length) { - const resolvedIds = {}; - allCallbacks.forEach((cb, i) => { - resolvedIds[cb.resolvedId] = i; - }); - existingCallbacks.forEach(existingCB => { - const iAll = resolvedIds[existingCB.resolvedId]; - if (iAll === undefined) { - allCallbacks.push(existingCB); - } else if (existingCB.requestId && !allCallbacks[iAll].requestId) { - // already requested put the requested one in the queue - allCallbacks[iAll] = existingCB; - } - // otherwise either both are blocked, fine, either one will do... - // or both have been requested - either way keep the newer one - }); - } - dispatch(setPendingCallbacks(allCallbacks)); + const ids = requestedCallbacks.map(cb => [ + cb.getInputs(paths), + cb.getState(paths), + ]); + await isAppReady(layout, paths, uniq(pluck('id', flatten(ids)))); + function fireNext() { return fireReadyCallbacks( dispatch, From 13acfda08ba6647a9ef0e130fae2f456a597d4ce Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 10 Feb 2020 10:24:11 -0500 Subject: [PATCH 32/81] Py2 fix for callback context backward compatibility shim --- dash/_callback_context.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dash/_callback_context.py b/dash/_callback_context.py index ee0243590b..f6a7714f0e 100644 --- a/dash/_callback_context.py +++ b/dash/_callback_context.py @@ -20,6 +20,11 @@ def assert_context(*args, **kwargs): class FalsyList(list): def __bool__(self): + # for Python 3 + return False + + def __nonzero__(self): + # for Python 2 return False From 3a38f58f270f428715d73f06332a58317f0a3100 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 10 Feb 2020 10:40:29 -0500 Subject: [PATCH 33/81] fix callback_context test for py2 --- .../integration/callbacks/test_callback_context.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index 5fd435eb85..fa909948cc 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -1,4 +1,6 @@ +import json import pytest + import dash_html_components as html import dash_core_components as dcc @@ -86,8 +88,8 @@ def test_cbcx004_triggered_backward_compat(dash_duo): def report_triggered(n): triggered = callback_context.triggered bool_val = "truthy" if triggered else "falsy" - split_propid = repr(triggered[0]["prop_id"].split(".")) - full_val = repr(triggered) + split_propid = json.dumps(triggered[0]["prop_id"].split(".")) + full_val = json.dumps(triggered) return "triggered is {}, has prop/id {}, and full value {}".format( bool_val, split_propid, full_val ) @@ -96,13 +98,13 @@ def report_triggered(n): dash_duo.wait_for_text_to_equal( "#out", - "triggered is falsy, has prop/id ['', ''], and full value " - "[{'prop_id': '.', 'value': None}]" + 'triggered is falsy, has prop/id ["", ""], and full value ' + '[{"prop_id": ".", "value": null}]' ) dash_duo.find_element("#btn").click() dash_duo.wait_for_text_to_equal( "#out", - "triggered is truthy, has prop/id ['btn', 'n_clicks'], and full value " - "[{'prop_id': 'btn.n_clicks', 'value': 1}]" + 'triggered is truthy, has prop/id ["btn", "n_clicks"], and full value ' + '[{"prop_id": "btn.n_clicks", "value": 1}]' ) From 1aa6bcfaccb0d57f03fc55814bdc51e937d958eb Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 28 Feb 2020 11:33:04 -0500 Subject: [PATCH 34/81] fix unique key warning from error components --- .../src/components/error/FrontEnd/FrontEndError.react.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/components/error/FrontEnd/FrontEndError.react.js b/dash-renderer/src/components/error/FrontEnd/FrontEndError.react.js index 7d69ed2a88..0bf64e5895 100644 --- a/dash-renderer/src/components/error/FrontEnd/FrontEndError.react.js +++ b/dash-renderer/src/components/error/FrontEnd/FrontEndError.react.js @@ -100,8 +100,8 @@ function UnconnectedErrorContent({error, base}) { - {error.stack.split('\n').map(line => ( -

{line}

+ {error.stack.split('\n').map((line, i) => ( +

{line}

))}
From 1cd18bd4bd8f67b19217ef6a8934e2c61ac2ea78 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 29 Feb 2020 09:14:22 -0500 Subject: [PATCH 35/81] fix & test for dcc.Location multi-path case --- dash-renderer/src/actions/dependencies.js | 75 ++++++++++++++++--- dash-renderer/src/actions/index.js | 40 +--------- .../callbacks/test_basic_callback.py | 57 ++++++++++++++ 3 files changed, 125 insertions(+), 47 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 343d9e78df..14d1adbcbe 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -11,13 +11,16 @@ import { evolve, flatten, forEachObjIndexed, + includes, isEmpty, map, mergeDeepRight, mergeRight, - omit, + mergeWith, partition, path, + pickBy, + propEq, props, unnest, values, @@ -69,7 +72,7 @@ function parseMultipleOutputs(outputIdAndProp) { return outputIdAndProp.substr(2, outputIdAndProp.length - 4).split('...'); } -export function splitIdAndProp(idAndProp) { +function splitIdAndProp(idAndProp) { // since wildcard ids can have . in them but props can't, // look for the last . in the string and split there const dotPos = idAndProp.lastIndexOf('.'); @@ -497,6 +500,9 @@ const makeResolvedCallback = (callback, resolve, anyVals) => ({ requestedOutputs: {}, }); +const DIRECT = 2; +const INDIRECT = 1; + let nextRequestId = 0; /* @@ -531,7 +537,11 @@ export function isMultiValued({id}) { * getState: same for state * blockedBy: an object of {[resolvedId]: 1} blocking this callback * blocking: an object of {[resolvedId]: 1} this callback is blocking - * changedPropIds: an object of {[idAndProp]: 1} triggering this callback + * changedPropIds: an object of {[idAndProp]: v} triggering this callback + * v = DIRECT (2): the prop was changed in the front end, so dependent + * callbacks *MUST* be executed. + * v = INDIRECT (1): the prop is expected to be changed by a callback, + * but if this is prevented, dependent callbacks may be pruned. * initialCall: boolean, if true we don't require any changedPropIds * to keep this callback around, as it's the initial call to populate * this value on page load or changing part of the layout. @@ -633,7 +643,7 @@ function addResolvedFromOutputs(callback, outPattern, outs, matches) { * (with an MATCH corresponding to the input's ALLSMALLER) will only appear * in one entry. */ -export function getCallbacksByInput(graphs, paths, id, prop) { +export function getCallbacksByInput(graphs, paths, id, prop, changeType) { const matches = []; const idAndProp = combineIdAndProp({id, property: prop}); @@ -691,7 +701,7 @@ export function getCallbacksByInput(graphs, paths, id, prop) { }); } matches.forEach(match => { - match.changedPropIds[idAndProp] = 1; + match.changedPropIds[idAndProp] = changeType || DIRECT; }); return matches; } @@ -747,7 +757,8 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { if (callback) { const foundIndex = foundCbIds[callback.resolvedId]; if (foundIndex !== undefined) { - callbacks[foundIndex].changedPropIds = mergeRight( + callbacks[foundIndex].changedPropIds = mergeWith( + Math.max, callbacks[foundIndex].changedPropIds, callback.changedPropIds ); @@ -855,7 +866,10 @@ export function removePendingCallback( mergeRight(pending, { blockedBy: dissoc(removeResolvedId, blockedBy), blocking: dissoc(removeResolvedId, blocking), - changedPropIds: omit(skippedProps, changedPropIds), + changedPropIds: pickBy( + (v, k) => v === DIRECT || includes(k, skippedProps), + changedPropIds + ), }) ); } @@ -927,7 +941,7 @@ export function followForward(graphs, paths, callbacks_) { let callback; const followOutput = ({id, property}) => { - const nextCBs = getCallbacksByInput(graphs, paths, id, property); + const nextCBs = getCallbacksByInput(graphs, paths, id, property, INDIRECT); nextCBs.forEach(nextCB => { let existingIndex = allResolvedIds[nextCB.resolvedId]; if (existingIndex === undefined) { @@ -936,7 +950,8 @@ export function followForward(graphs, paths, callbacks_) { allResolvedIds[nextCB.resolvedId] = existingIndex; } else { const existingCB = callbacks[existingIndex]; - existingCB.changedPropIds = mergeRight( + existingCB.changedPropIds = mergeWith( + Math.max, existingCB.changedPropIds, nextCB.changedPropIds ); @@ -1003,3 +1018,45 @@ export function mergePendingCallbacks(cb1, cb2) { return finalCallbacks; } + +/* + * Remove callbacks whose outputs or changed inputs have been removed + * from the layout + */ +export function pruneRemovedCallbacks(pendingCallbacks, paths) { + const removeIds = []; + let cleanedCallbacks = pendingCallbacks.map(callback => { + const {changedPropIds, getOutputs, resolvedId} = callback; + if (!flatten(getOutputs(paths)).length) { + removeIds.push(resolvedId); + return callback; + } + + let omittedProps = false; + const newChangedProps = pickBy((_, propId) => { + if (getPath(paths, splitIdAndProp(propId).id)) { + return true; + } + omittedProps = true; + return false; + }, changedPropIds); + + return omittedProps + ? assoc('changedPropIds', newChangedProps, callback) + : callback; + }); + + removeIds.forEach(resolvedId => { + const cb = cleanedCallbacks.find(propEq('resolvedId', resolvedId)); + if (cb) { + cleanedCallbacks = removePendingCallback( + pendingCallbacks, + paths, + resolvedId, + flatten(cb.getOutputs(paths)).map(combineIdAndProp) + ); + } + }); + + return cleanedCallbacks; +} diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index ad91507918..2efd7af376 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -1,6 +1,5 @@ /* global fetch:true, Promise:true, document:true */ import { - assoc, concat, flatten, has, @@ -37,8 +36,8 @@ import { mergePendingCallbacks, removePendingCallback, parseIfWildcard, + pruneRemovedCallbacks, setNewRequestId, - splitIdAndProp, stringifyId, } from './dependencies'; import {computePaths, getPath} from './paths'; @@ -539,42 +538,7 @@ function updateChildPaths( const paths = computePaths(children, childrenPath, oldPaths); dispatch(setPaths(paths)); - // Prune now-nonexistent changedPropIds and mark callbacks with - // now-nonexistent outputs - const removeIds = []; - let cleanedCallbacks = pendingCallbacks.map(callback => { - const {changedPropIds, getOutputs, resolvedId} = callback; - if (!flatten(getOutputs(paths)).length) { - removeIds.push(resolvedId); - return callback; - } - - let omittedProps = false; - const newChangedProps = pickBy((_, propId) => { - if (getPath(paths, splitIdAndProp(propId).id)) { - return true; - } - omittedProps = true; - return false; - }, changedPropIds); - - return omittedProps - ? assoc('changedPropIds', newChangedProps, callback) - : callback; - }); - - // Remove the callbacks we marked above - removeIds.forEach(resolvedId => { - const cb = cleanedCallbacks.find(propEq('resolvedId', resolvedId)); - if (cb) { - cleanedCallbacks = removePendingCallback( - pendingCallbacks, - paths, - resolvedId, - flatten(cb.getOutputs(paths)).map(combineIdAndProp) - ); - } - }); + const cleanedCallbacks = pruneRemovedCallbacks(pendingCallbacks, paths); const newCallbacks = getCallbacksInLayout(graphs, paths, children); diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index bc29e9b694..fa1854cbaf 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -1,5 +1,8 @@ +import json from multiprocessing import Value +import pytest + import dash_core_components as dcc import dash_html_components as html import dash_table @@ -232,3 +235,57 @@ def set_out(opts): dash_duo.find_element("#btn").click() dash_duo.wait_for_text_to_equal("#out", str(i + 1)) dash_duo.select_dcc_dropdown("#dd", "opt{}".format(i)) + + +@pytest.mark.parametrize("refresh", [False, True]) +def test_cbsc006_parallel_updates(refresh, dash_duo): + # This is a funny case, that seems to mostly happen with dcc.Location + # but in principle could happen in other cases too: + # A callback chain (in this case the initial hydration) is set to update a + # value, but after that callback is queued and before it returns, that value + # is also set explicitly from the front end (in this case Location.pathname, + # which gets set in its componentDidMount during the render process, and + # callbacks are delayed until after rendering is finished because of the + # async table) + # At one point in the wildcard PR #1103, changing from requestQueue to + # pendingCallbacks, calling PreventUpdate in the callback would also skip + # any callbacks that depend on pathname, despite the new front-end-provided + # value. + + app = dash.Dash() + + app.layout = html.Div([ + dcc.Location(id='loc', refresh=refresh), + html.Button('Update path', id='btn'), + dash_table.DataTable(id='t', columns=[{'name': 'a', 'id': 'a'}]), + html.Div(id='out') + ]) + + @app.callback(Output('t', 'data'), [Input('loc', 'pathname')]) + def set_data(path): + return [{'a': (path or repr(path)) + ':a'}] + + @app.callback( + Output('out', 'children'), + [Input('loc', 'pathname'), Input('t', 'data')] + ) + def set_out(path, data): + return json.dumps(data) + ' - ' + (path or repr(path)) + + @app.callback(Output('loc', 'pathname'), [Input('btn', 'n_clicks')]) + def set_path(n): + if not n: + raise PreventUpdate + + return '/{0}'.format(n) + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal('#out', '[{"a": "/:a"}] - /') + dash_duo.find_element("#btn").click() + # the refresh=True case here is testing that we really do get the right + # pathname, not the prevented default value from the layout. + dash_duo.wait_for_text_to_equal("#out", '[{"a": "/1:a"}] - /1') + if not refresh: + dash_duo.find_element("#btn").click() + dash_duo.wait_for_text_to_equal("#out", '[{"a": "/2:a"}] - /2') From 316cdd87263bcd59e9196993d9eb57443e03d7b6 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 29 Feb 2020 18:08:51 -0500 Subject: [PATCH 36/81] fix the fix for dcc.Location multi-path init case --- dash-renderer/src/actions/dependencies.js | 31 +++++++--- .../callbacks/test_basic_callback.py | 57 +++++++++++++++++-- 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 14d1adbcbe..a98ebd7a0f 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -1,7 +1,6 @@ import {DepGraph} from 'dependency-graph'; import isNumeric from 'fast-isnumeric'; import { - all, any, ap, assoc, @@ -803,7 +802,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { if (!outputsOnly && inIdCallbacks) { const idStr = removedArrayInputsOnly && stringifyId(id); for (const property in inIdCallbacks) { - getCallbacksByInput(graphs, paths, id, property).forEach( + getCallbacksByInput( + graphs, + paths, + id, + property, + INDIRECT + ).forEach( removedArrayInputsOnly ? addCallbackIfArray(idStr) : addCallback @@ -843,9 +848,15 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { finalCallbacks.forEach(cb => { if (cb.initialCall && !isEmpty(cb.blockedBy)) { const inputs = flatten(cb.getInputs(paths)); - if (all(i => cb.changedPropIds[combineIdAndProp(i)], inputs)) { - cb.initialCall = false; - } + cb.initialCall = false; + inputs.forEach(i => { + const propId = combineIdAndProp(i); + if (cb.changedPropIds[propId]) { + cb.changedPropIds[propId] = INDIRECT; + } else { + cb.initialCall = true; + } + }); } }); @@ -867,7 +878,7 @@ export function removePendingCallback( blockedBy: dissoc(removeResolvedId, blockedBy), blocking: dissoc(removeResolvedId, blocking), changedPropIds: pickBy( - (v, k) => v === DIRECT || includes(k, skippedProps), + (v, k) => v === DIRECT || !includes(k, skippedProps), changedPropIds ), }) @@ -941,7 +952,13 @@ export function followForward(graphs, paths, callbacks_) { let callback; const followOutput = ({id, property}) => { - const nextCBs = getCallbacksByInput(graphs, paths, id, property, INDIRECT); + const nextCBs = getCallbacksByInput( + graphs, + paths, + id, + property, + INDIRECT + ); nextCBs.forEach(nextCB => { let existingIndex = allResolvedIds[nextCB.resolvedId]; if (existingIndex === undefined) { diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index fa1854cbaf..6458dee079 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -7,7 +7,7 @@ import dash_html_components as html import dash_table import dash -from dash.dependencies import Input, Output +from dash.dependencies import Input, Output, State from dash.exceptions import PreventUpdate @@ -163,7 +163,7 @@ def test_cbsc003_callback_with_unloaded_async_component(dash_duo): ) @app.callback(Output("output", "children"), [Input("btn", "n_clicks")]) - def update_graph(n_clicks): + def update_out(n_clicks): if n_clicks is None: raise PreventUpdate @@ -171,12 +171,57 @@ def update_graph(n_clicks): dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#output", "Hello") dash_duo.find_element("#btn").click() - assert dash_duo.find_element("#output").text == "Bye" + dash_duo.wait_for_text_to_equal("#output", "Bye") assert dash_duo.get_logs() == [] -def test_cbsc004_children_types(dash_duo): +@pytest.mark.skip(reason="https://github.com/plotly/dash/issues/1105") +def test_cbsc004_callback_using_unloaded_async_component(dash_duo): + app = dash.Dash() + app.layout = html.Div( + children=[ + dcc.Tabs( + children=[ + dcc.Tab( + children=[ + html.Button(id="btn", children="Update Input"), + html.Div(id="output", children=["Hello"]), + ] + ), + dcc.Tab( + children=dash_table.DataTable( + id="other-table", + columns=[{"id": "a", "name": "A"}], + data=[{"a": "b"}] + ) + ), + ] + ) + ] + ) + + @app.callback( + Output("output", "children"), + [Input("btn", "n_clicks")], + [State("other-table", "data")] + ) + def update_out(n_clicks, data): + if n_clicks is None: + return len(data) + + return json.dumps(data) + ' - ' + str(n_clicks) + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#output", '[{"a": "b"}] - None') + dash_duo.find_element("#btn").click() + dash_duo.wait_for_text_to_equal("#output", '[{"a": "b"}] - 1') + assert dash_duo.get_logs() == [] + + +def test_cbsc005_children_types(dash_duo): app = dash.Dash() app.layout = html.Div([ html.Button(id="btn"), @@ -208,7 +253,7 @@ def set_children(n): dash_duo.wait_for_text_to_equal("#out", text) -def test_cbsc005_array_of_objects(dash_duo): +def test_cbsc006_array_of_objects(dash_duo): app = dash.Dash() app.layout = html.Div([ html.Button(id="btn"), @@ -238,7 +283,7 @@ def set_out(opts): @pytest.mark.parametrize("refresh", [False, True]) -def test_cbsc006_parallel_updates(refresh, dash_duo): +def test_cbsc007_parallel_updates(refresh, dash_duo): # This is a funny case, that seems to mostly happen with dcc.Location # but in principle could happen in other cases too: # A callback chain (in this case the initial hydration) is set to update a From 7e57ed12667d53bd88082e7ac9ee10606727ec2c Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 5 Mar 2020 20:49:55 -0500 Subject: [PATCH 37/81] fix #1105 - isAppReady with an unrendered but in-layout async component --- dash-renderer/src/APIController.react.js | 39 ++++++++--- dash-renderer/src/TreeContainer.js | 24 +++---- dash-renderer/src/actions/isAppReady.js | 18 ++++- dash-renderer/src/actions/paths.js | 6 +- dash-renderer/src/actions/utils.js | 34 ++++++++++ dash-renderer/tests/isAppReady.test.js | 7 +- dash/testing/dash_page.py | 5 +- .../callbacks/test_basic_callback.py | 66 ++++++++++++------- 8 files changed, 148 insertions(+), 51 deletions(-) diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index 65dee36e80..0ec016be07 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -8,6 +8,7 @@ import {hydrateInitialOutputs, setGraphs, setPaths, setLayout} from './actions'; import {computePaths} from './actions/paths'; import {computeGraphs} from './actions/dependencies'; import apiThunk from './actions/api'; +import {EventEmitter} from './actions/utils'; import {applyPersistence} from './persistence'; import {getAppState} from './reducers/constants'; import {STATUS} from './constants/constants'; @@ -19,18 +20,29 @@ class UnconnectedContainer extends Component { constructor(props) { super(props); this.initialization = this.initialization.bind(this); + this.emitReady = this.emitReady.bind(this); this.state = { errorLoading: false, }; + + // Event emitter to communicate when the DOM is ready + this.events = new EventEmitter(); + // Flag to determine if we've really updated the dash components + this.renderedTree = false; } componentDidMount() { this.initialization(this.props); + this.emitReady(); } componentWillReceiveProps(props) { this.initialization(props); } + componentDidUpdate() { + this.emitReady(); + } + initialization(props) { const { appLifecycle, @@ -49,7 +61,9 @@ class UnconnectedContainer extends Component { layoutRequest.content, dispatch ); - dispatch(setPaths(computePaths(finalLayout, []))); + dispatch( + setPaths(computePaths(finalLayout, [], null, this.events)) + ); dispatch(setLayout(finalLayout)); } } @@ -88,6 +102,13 @@ class UnconnectedContainer extends Component { } } + emitReady() { + if (this.renderedTree) { + this.renderedTree = false; + this.events.emit('rendered'); + } + } + render() { const { appLifecycle, @@ -113,19 +134,19 @@ class UnconnectedContainer extends Component {
Error loading dependencies
); } else if (appLifecycle === getAppState('HYDRATED')) { - return config.ui === true ? ( - - - - ) : ( + this.renderedTree = true; + + const tree = ( ); + return config.ui === true ? ( + {tree} + ) : ( + tree + ); } return
Loading...
; diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index 12f1f834cb..5d32e8f83e 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -80,11 +80,7 @@ function CheckedComponent(p) { propTypeErrorHandler(errorMessage, props, type); } - return React.createElement( - element, - mergeRight(props, extraProps), - ...(Array.isArray(children) ? children : [children]) - ); + return createElement(element, props, extraProps, children); } CheckedComponent.propTypes = { @@ -95,6 +91,15 @@ CheckedComponent.propTypes = { extraProps: PropTypes.any, id: PropTypes.string, }; + +function createElement(element, props, extraProps, children) { + const allProps = mergeRight(props, extraProps); + if (Array.isArray(children)) { + return React.createElement(element, allProps, ...children); + } + return React.createElement(element, allProps, children); +} + class TreeContainer extends Component { constructor(props) { super(props); @@ -188,6 +193,7 @@ class TreeContainer extends Component { // just the id we pass on to the rendered component props.id = stringifyId(props.id); } + const extraProps = {loading_state, setProps}; return ( ) : ( - React.createElement( - element, - mergeRight(props, {loading_state, setProps}), - ...(Array.isArray(children) ? children : [children]) - ) + createElement(element, props, extraProps, children) )} ); diff --git a/dash-renderer/src/actions/isAppReady.js b/dash-renderer/src/actions/isAppReady.js index 14c43be6d8..03465dc2f7 100644 --- a/dash-renderer/src/actions/isAppReady.js +++ b/dash-renderer/src/actions/isAppReady.js @@ -3,10 +3,19 @@ import {isReady} from '@plotly/dash-component-plugins'; import Registry from '../registry'; import {getPath} from './paths'; +import {stringifyId} from './dependencies'; export default (layout, paths, targets) => { + if (!targets.length) { + return true; + } const promises = []; + const {events} = paths; + const rendered = new Promise(resolveRendered => { + events.once('rendered', resolveRendered); + }); + targets.forEach(id => { const pathOfId = getPath(paths, id); if (!pathOfId) { @@ -22,7 +31,14 @@ export default (layout, paths, targets) => { const ready = isReady(component); if (ready && typeof ready.then === 'function') { - promises.push(ready); + promises.push( + Promise.race([ + ready, + rendered.then( + () => document.getElementById(stringifyId(id)) && ready + ), + ]) + ); } }); diff --git a/dash-renderer/src/actions/paths.js b/dash-renderer/src/actions/paths.js index 643f8207f6..5ab2ea9420 100644 --- a/dash-renderer/src/actions/paths.js +++ b/dash-renderer/src/actions/paths.js @@ -20,7 +20,7 @@ import {crawlLayout} from './utils'; * values: array of values in the id, in order of keys */ -export function computePaths(subTree, startingPath, oldPaths) { +export function computePaths(subTree, startingPath, oldPaths, events) { const {strs: oldStrs, objs: oldObjs} = oldPaths || {strs: {}, objs: {}}; const diffHead = path => startingPath.some((v, i) => path[i] !== v); @@ -53,7 +53,9 @@ export function computePaths(subTree, startingPath, oldPaths) { } }); - return {strs, objs}; + // We include an event emitter here because it will be used along with + // paths to determine when the app is ready for callbacks. + return {strs, objs, events: events || oldPaths.events}; } export function getPath(paths, id) { diff --git a/dash-renderer/src/actions/utils.js b/dash-renderer/src/actions/utils.js index 087e262a2a..3e85052ce3 100644 --- a/dash-renderer/src/actions/utils.js +++ b/dash-renderer/src/actions/utils.js @@ -43,3 +43,37 @@ export const crawlLayout = (object, func, currentPath = []) => { } } }; + +// There are packages for this but it's simple enough, I just +// adapted it from https://gist.github.com/mudge/5830382 +export class EventEmitter { + constructor() { + this._ev = {}; + } + on(event, listener) { + const events = (this._ev[event] = this._ev[event] || []); + events.push(listener); + return () => this.removeListener(event, listener); + } + removeListener(event, listener) { + const events = this._ev[event]; + if (events) { + const idx = events.indexOf(listener); + if (idx > -1) { + events.splice(idx, 1); + } + } + } + emit(event, ...args) { + const events = this._ev[event]; + if (events) { + events.forEach(listener => listener.apply(this, args)); + } + } + once(event, listener) { + const remove = this.on(event, (...args) => { + remove(); + listener.apply(this, args); + }); + } +} diff --git a/dash-renderer/tests/isAppReady.test.js b/dash-renderer/tests/isAppReady.test.js index 4202c845d9..a16cee04da 100644 --- a/dash-renderer/tests/isAppReady.test.js +++ b/dash-renderer/tests/isAppReady.test.js @@ -1,4 +1,5 @@ import isAppReady from "../src/actions/isAppReady"; +import {EventEmitter} from "../src/actions/utils"; const WAIT = 1000; @@ -15,11 +16,13 @@ describe('isAppReady', () => { }; }); + const emitter = new EventEmitter(); + it('executes if app is ready', async () => { let done = false; Promise.resolve(isAppReady( [{ namespace: '__components', type: 'b', props: { id: 'comp1' } }], - { strs: { comp1: [0] }, objs: {} }, + { strs: { comp1: [0] }, objs: {}, events: emitter }, ['comp1'] )).then(() => { done = true @@ -33,7 +36,7 @@ describe('isAppReady', () => { let done = false; Promise.resolve(isAppReady( [{ namespace: '__components', type: 'a', props: { id: 'comp1' } }], - { strs: { comp1: [0] }, objs: {} }, + { strs: { comp1: [0] }, objs: {}, events: emitter }, ['comp1'] )).then(() => { done = true diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py index ef24ae4a78..44b8658303 100644 --- a/dash/testing/dash_page.py +++ b/dash/testing/dash_page.py @@ -27,7 +27,10 @@ def dash_innerhtml_dom(self): @property def redux_state_paths(self): return self.driver.execute_script( - "return window.store.getState().paths" + """ + var p = window.store.getState().paths; + return {strs: p.strs, objs: p.objs} + """ ) @property diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index 6458dee079..448a5f3643 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -177,47 +177,63 @@ def update_out(n_clicks): assert dash_duo.get_logs() == [] -@pytest.mark.skip(reason="https://github.com/plotly/dash/issues/1105") def test_cbsc004_callback_using_unloaded_async_component(dash_duo): app = dash.Dash() - app.layout = html.Div( - children=[ - dcc.Tabs( - children=[ - dcc.Tab( - children=[ - html.Button(id="btn", children="Update Input"), - html.Div(id="output", children=["Hello"]), - ] - ), - dcc.Tab( - children=dash_table.DataTable( - id="other-table", - columns=[{"id": "a", "name": "A"}], - data=[{"a": "b"}] - ) - ), - ] - ) - ] - ) + app.layout = html.Div([ + dcc.Tabs([ + dcc.Tab("boo!"), + dcc.Tab( + dash_table.DataTable( + id="table", + columns=[{"id": "a", "name": "A"}], + data=[{"a": "b"}] + ) + ), + ]), + html.Button("Update Input", id="btn"), + html.Div("Hello", id="output"), + html.Div(id="output2") + ]) @app.callback( Output("output", "children"), [Input("btn", "n_clicks")], - [State("other-table", "data")] + [State("table", "data")] ) def update_out(n_clicks, data): - if n_clicks is None: - return len(data) + return json.dumps(data) + ' - ' + str(n_clicks) + @app.callback( + Output("output2", "children"), + [Input("btn", "n_clicks")], + [State("table", "derived_viewport_data")] + ) + def update_out2(n_clicks, data): return json.dumps(data) + ' - ' + str(n_clicks) dash_duo.start_server(app) dash_duo.wait_for_text_to_equal("#output", '[{"a": "b"}] - None') + dash_duo.wait_for_text_to_equal("#output2", 'null - None') + dash_duo.find_element("#btn").click() dash_duo.wait_for_text_to_equal("#output", '[{"a": "b"}] - 1') + dash_duo.wait_for_text_to_equal("#output2", 'null - 1') + + dash_duo.find_element(".tab:not(.tab--selected)").click() + dash_duo.wait_for_text_to_equal("#table th", "A") + # table props are in state so no change yet + dash_duo.wait_for_text_to_equal("#output2", 'null - 1') + + # repeat a few times, since one of the failure modes I saw during dev was + # intermittent - but predictably so? + for i in range(2, 10): + expected = '[{"a": "b"}] - ' + str(i) + dash_duo.find_element("#btn").click() + dash_duo.wait_for_text_to_equal("#output", expected) + # now derived props are available + dash_duo.wait_for_text_to_equal("#output2", expected) + assert dash_duo.get_logs() == [] From d39e3ad36e9acfe5e3d84edeedab545662fb8ebb Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 5 Mar 2020 21:34:20 -0500 Subject: [PATCH 38/81] closes #1053 - add test for complex callback graph with preventDefault --- .../callbacks/test_multiple_callbacks.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index 84c8af1d78..30a01b162c 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -2,8 +2,10 @@ from multiprocessing import Value import dash_html_components as html +import dash_core_components as dcc import dash from dash.dependencies import Input, Output +from dash.exceptions import PreventUpdate def test_cbmt001_called_multiple_times_and_out_of_order(dash_duo): @@ -36,3 +38,37 @@ def update_output(n_clicks): dash_duo.percy_snapshot( name="test_callbacks_called_multiple_times_and_out_of_order" ) + + +def test_cbmt002_canceled_intermediate_callback(dash_duo): + # see https://github.com/plotly/dash/issues/1053 + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Input(id='a', value="x"), + html.Div('b', id='b'), + html.Div('c', id='c'), + html.Div(id='out') + ]) + + @app.callback( + Output("out", "children"), + [Input("a", "value"), Input("b", "children"), Input("c", "children")] + ) + def set_out(a, b, c): + return "{}/{}/{}".format(a, b, c) + + @app.callback(Output("b", "children"), [Input("a", "value")]) + def set_b(a): + raise PreventUpdate + + @app.callback(Output("c", "children"), [Input("a", "value")]) + def set_c(a): + return a + + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#out", "x/b/x") + chars = "x" + for i in list(range(10)) * 2: + dash_duo.find_element("#a").send_keys(str(i)) + chars += str(i) + dash_duo.wait_for_text_to_equal("#out", "{0}/b/{0}".format(chars)) From 5b25247639912c21dab042e7663adfb9dade4768 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 5 Mar 2020 22:10:49 -0500 Subject: [PATCH 39/81] close #1071 - add test for chained callbacks with table coming in late --- .../callbacks/test_multiple_callbacks.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index 30a01b162c..5c4ce67679 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -3,6 +3,7 @@ import dash_html_components as html import dash_core_components as dcc +import dash_table import dash from dash.dependencies import Input, Output from dash.exceptions import PreventUpdate @@ -72,3 +73,54 @@ def set_c(a): dash_duo.find_element("#a").send_keys(str(i)) chars += str(i) dash_duo.wait_for_text_to_equal("#out", "{0}/b/{0}".format(chars)) + + +def test_cbmt003_chain_with_table(dash_duo): + # see https://github.com/plotly/dash/issues/1071 + app = dash.Dash(__name__) + app.layout = html.Div([ + html.Div(id="a1"), + html.Div(id="a2"), + html.Div(id="b1"), + html.H1(id="b2"), + html.Button("Update", id="button"), + dash_table.DataTable(id="table"), + ]) + + @app.callback( + # Changing the order of outputs here fixes the issue + [Output("a2", "children"), Output("a1", "children")], + [Input("button", "n_clicks")], + ) + def a12(n): + return "a2: {!s}".format(n), "a1: {!s}".format(n) + + @app.callback(Output("b1", "children"), [Input("a1", "children")]) + def b1(a1): + return "b1: '{!s}'".format(a1) + + @app.callback( + Output("b2", "children"), + [Input("a2", "children"), Input("table", "selected_cells")], + ) + def b2(a2, selected_cells): + return "b2: '{!s}', {!s}".format(a2, selected_cells) + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#a1", "a1: None") + dash_duo.wait_for_text_to_equal("#a2", "a2: None") + dash_duo.wait_for_text_to_equal("#b1", "b1: 'a1: None'") + dash_duo.wait_for_text_to_equal("#b2", "b2: 'a2: None', None") + + dash_duo.find_element("#button").click() + dash_duo.wait_for_text_to_equal("#a1", "a1: 1") + dash_duo.wait_for_text_to_equal("#a2", "a2: 1") + dash_duo.wait_for_text_to_equal("#b1", "b1: 'a1: 1'") + dash_duo.wait_for_text_to_equal("#b2", "b2: 'a2: 1', None") + + dash_duo.find_element("#button").click() + dash_duo.wait_for_text_to_equal("#a1", "a1: 2") + dash_duo.wait_for_text_to_equal("#a2", "a2: 2") + dash_duo.wait_for_text_to_equal("#b1", "b1: 'a1: 2'") + dash_duo.wait_for_text_to_equal("#b2", "b2: 'a2: 2', None") From 569484356c4cdf15b2140103d28117da1e56f56c Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 5 Mar 2020 22:37:29 -0500 Subject: [PATCH 40/81] close #1084 - callback chain with sliders and multiple outputs --- .../callbacks/test_multiple_callbacks.py | 70 +++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index 5c4ce67679..0653fb4273 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -1,6 +1,8 @@ import time from multiprocessing import Value +import pytest + import dash_html_components as html import dash_core_components as dcc import dash_table @@ -45,10 +47,10 @@ def test_cbmt002_canceled_intermediate_callback(dash_duo): # see https://github.com/plotly/dash/issues/1053 app = dash.Dash(__name__) app.layout = html.Div([ - dcc.Input(id='a', value="x"), - html.Div('b', id='b'), - html.Div('c', id='c'), - html.Div(id='out') + dcc.Input(id="a", value="x"), + html.Div("b", id="b"), + html.Div("c", id="c"), + html.Div(id="out") ]) @app.callback( @@ -124,3 +126,63 @@ def b2(a2, selected_cells): dash_duo.wait_for_text_to_equal("#a2", "a2: 2") dash_duo.wait_for_text_to_equal("#b1", "b1: 'a1: 2'") dash_duo.wait_for_text_to_equal("#b2", "b2: 'a2: 2', None") + + +@pytest.mark.parametrize("MULTI", [False, True]) +def test_cbmt004_chain_with_sliders(MULTI, dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.Button("Button", id="button"), + html.Div([ + html.Label(id="label1"), + dcc.Slider(id="slider1", min=0, max=10, value=0), + + ]), + html.Div([ + html.Label(id="label2"), + dcc.Slider(id="slider2", min=0, max=10, value=0), + ]) + ]) + + if MULTI: + @app.callback( + [Output("slider1", "value"), Output("slider2", "value")], + [Input("button", "n_clicks")] + ) + def update_slider_vals(n): + if not n: + raise PreventUpdate + return n, n + else: + @app.callback(Output("slider1", "value"), [Input("button", "n_clicks")]) + def update_slider1_val(n): + if not n: + raise PreventUpdate + return n + + @app.callback(Output("slider2", "value"), [Input("button", "n_clicks")]) + def update_slider2_val(n): + if not n: + raise PreventUpdate + return n + + @app.callback(Output("label1", "children"), [Input("slider1", "value")]) + def update_slider1_label(val): + return "Slider1 value {}".format(val) + + @app.callback(Output("label2", "children"), [Input("slider2", "value")]) + def update_slider2_label(val): + return "Slider2 value {}".format(val) + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#label1", "") + dash_duo.wait_for_text_to_equal("#label2", "") + + dash_duo.find_element("#button").click() + dash_duo.wait_for_text_to_equal("#label1", "Slider1 value 1") + dash_duo.wait_for_text_to_equal("#label2", "Slider2 value 1") + + dash_duo.find_element("#button").click() + dash_duo.wait_for_text_to_equal("#label1", "Slider1 value 2") + dash_duo.wait_for_text_to_equal("#label2", "Slider2 value 2") From 21dc9eb6c5b6a5c25396881e301244f0741c8d9f Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 6 Mar 2020 09:28:14 -0500 Subject: [PATCH 41/81] close #635 - converging callback graph from a multi-output start --- .../callbacks/test_multiple_callbacks.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index 0653fb4273..c9c37f9a5f 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -186,3 +186,43 @@ def update_slider2_label(val): dash_duo.find_element("#button").click() dash_duo.wait_for_text_to_equal("#label1", "Slider1 value 2") dash_duo.wait_for_text_to_equal("#label2", "Slider2 value 2") + + +def test_cbmt005_multi_converging_chain(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.Button("Button 1", id="b1"), + html.Button("Button 2", id="b2"), + dcc.Slider(id="slider1", min=-5, max=5), + dcc.Slider(id="slider2", min=-5, max=5), + html.Div(id="out") + ]) + + @app.callback( + [Output("slider1", "value"), Output("slider2", "value")], + [Input("b1", "n_clicks"), Input("b2", "n_clicks")] + ) + def update_sliders(button1, button2): + if not dash.callback_context.triggered: + raise PreventUpdate + + if dash.callback_context.triggered[0]["prop_id"] == "b1.n_clicks": + return -1, -1 + else: + return 1, 1 + + @app.callback( + Output("out", "children"), + [Input("slider1", "value"), Input("slider2", "value")] + ) + def update_graph(s1, s2): + return "x={}, y={}".format(s1, s2) + + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#out", "") + + dash_duo.find_element("#b1").click() + dash_duo.wait_for_text_to_equal("#out", "x=-1, y=-1") + + dash_duo.find_element("#b2").click() + dash_duo.wait_for_text_to_equal("#out", "x=1, y=1") From 11d0530e87ccd2b42edfe2ef30eb3f1e7415c14d Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 11 Mar 2020 21:27:30 -0400 Subject: [PATCH 42/81] remove commented-out, never-used wildcards code --- dash-renderer/src/actions/dependencies.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index a98ebd7a0f..d4e3c82c4a 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -213,9 +213,6 @@ export function computeGraphs(dependencies) { parsedDependencies.forEach(dependency => { const {outputs, inputs} = dependency; - // TODO: what was this (and exactChange) about??? - // const depWildcardExact = {}; - outputs.concat(inputs).forEach(item => { const {id} = item; if (typeof id === 'object') { @@ -223,7 +220,6 @@ export function computeGraphs(dependencies) { if (!wildcardPlaceholders[key]) { wildcardPlaceholders[key] = { exact: [], - // exactChange: false, expand: 0, }; } @@ -234,14 +230,6 @@ export function computeGraphs(dependencies) { } } else if (keyPlaceholders.exact.indexOf(val) === -1) { keyPlaceholders.exact.push(val); - // if (depWildcardExact[key]) { - // if (depWildcardExact[key] !== val) { - // keyPlaceholders.exactChange = true; - // } - // } - // else { - // depWildcardExact[key] = val; - // } } }, id); } From 3d4daaa2b10854bccc086b2a1cbb1761bd0538b9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 20 Mar 2020 05:13:01 -0400 Subject: [PATCH 43/81] show initialization errors in devtools --- dash-renderer/src/APIController.react.js | 47 ++++++++++++++++++------ 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index 0ec016be07..0d71f11167 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -4,7 +4,13 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import TreeContainer from './TreeContainer'; import GlobalErrorContainer from './components/error/GlobalErrorContainer.react'; -import {hydrateInitialOutputs, setGraphs, setPaths, setLayout} from './actions'; +import { + hydrateInitialOutputs, + setGraphs, + setPaths, + setLayout, + onError, +} from './actions'; import {computePaths} from './actions/paths'; import {computeGraphs} from './actions/dependencies'; import apiThunk from './actions/api'; @@ -48,6 +54,7 @@ class UnconnectedContainer extends Component { appLifecycle, dependenciesRequest, dispatch, + error, graphs, layout, layoutRequest, @@ -76,7 +83,18 @@ class UnconnectedContainer extends Component { dependenciesRequest.status === STATUS.OK && isEmpty(graphs) ) { - dispatch(setGraphs(computeGraphs(dependenciesRequest.content))); + const dispatchError = (message, lines) => + dispatch( + onError({ + type: 'backEnd', + error: {message, html: lines.join('\n')}, + }) + ); + dispatch( + setGraphs( + computeGraphs(dependenciesRequest.content, dispatchError) + ) + ); } if ( @@ -93,6 +111,11 @@ class UnconnectedContainer extends Component { try { dispatch(hydrateInitialOutputs()); } catch (err) { + // Display this error in devtools, unless we have errors + // already, in which case we assume this new one is moot + if (!error.frontEnd.length && !error.backEnd.length) { + dispatch(onError({type: 'backEnd', error: err})); + } errorLoading = true; } finally { this.setState(state => @@ -120,36 +143,38 @@ class UnconnectedContainer extends Component { const {errorLoading} = this.state; + let content; if ( layoutRequest.status && !includes(layoutRequest.status, [STATUS.OK, 'loading']) ) { - return
Error loading layout
; + content =
Error loading layout
; } else if ( errorLoading || (dependenciesRequest.status && !includes(dependenciesRequest.status, [STATUS.OK, 'loading'])) ) { - return ( + content = (
Error loading dependencies
); } else if (appLifecycle === getAppState('HYDRATED')) { this.renderedTree = true; - const tree = ( + content = ( ); - return config.ui === true ? ( - {tree} - ) : ( - tree - ); + } else { + content =
Loading...
; } - return
Loading...
; + return config && config.ui === true ? ( + {content} + ) : ( + content + ); } } UnconnectedContainer.propTypes = { From ad74d6277e2b1c85580b01816a32f41225c65f8e Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 20 Mar 2020 05:13:56 -0400 Subject: [PATCH 44/81] special test method for element to be missing looking for find_elements to be empty adds a long delay --- dash/testing/browser.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index f0f2122365..cd6dc8daef 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -236,6 +236,18 @@ def wait_for_element_by_css_selector(self, selector, timeout=None): ), ) + def wait_for_no_elements(self, selector, timeout=None): + """Explicit wait until an element is NOT found. timeout defaults to + the fixture's `wait_timeout`.""" + until( + # if we use get_elements it waits a long time to see if they appear + # so this one calls out directly to execute_script + lambda: self.driver.execute_script( + "return document.querySelectorAll('{}').length".format(selector) + ) == 0, + timeout if timeout else self._wait_timeout + ) + def wait_for_element_by_id(self, element_id, timeout=None): """Explicit wait until the element is present, timeout if not set, equals to the fixture's `wait_timeout` shortcut to `WebDriverWait` with From e9a47bf74e734b4f2c4897d4809e069c0763efd0 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 20 Mar 2020 05:18:48 -0400 Subject: [PATCH 45/81] move non-layout-linked callback validation to the front end --- dash-renderer/src/actions/dependencies.js | 318 +++++++++++++++- dash/_validate.py | 153 +------- dash/exceptions.py | 25 -- .../devtools/test_callback_validation.py | 344 ++++++++++++++++++ tests/integration/test_integration.py | 154 -------- 5 files changed, 651 insertions(+), 343 deletions(-) create mode 100644 tests/integration/devtools/test_callback_validation.py diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index d4e3c82c4a..b58e2c3ea4 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -1,10 +1,12 @@ import {DepGraph} from 'dependency-graph'; import isNumeric from 'fast-isnumeric'; import { + all, any, ap, assoc, clone, + difference, dissoc, equals, evolve, @@ -12,6 +14,7 @@ import { forEachObjIndexed, includes, isEmpty, + keys, map, mergeDeepRight, mergeRight, @@ -23,6 +26,7 @@ import { props, unnest, values, + zip, zipObj, } from 'ramda'; @@ -42,6 +46,14 @@ const ALL = {wild: 'ALL', multi: 1}; const MATCH = {wild: 'MATCH'}; const ALLSMALLER = {wild: 'ALLSMALLER', multi: 1, expand: 1}; const wildcards = {ALL, MATCH, ALLSMALLER}; +const allowedWildcards = { + Output: {ALL, MATCH}, + Input: wildcards, + State: wildcards, +}; +const wildcardValTypes = ['string', 'number', 'boolean']; + +const idInvalidChars = ['.', '{']; /* * If this ID is a wildcard, it is a stringified JSON object @@ -56,7 +68,7 @@ const isWildcardId = idStr => idStr.startsWith('{'); */ function parseWildcardId(idStr) { return map( - val => (Array.isArray(val) ? wildcards[val[0]] : val), + val => (Array.isArray(val) && wildcards[val[0]]) || val, JSON.parse(idStr) ); } @@ -99,9 +111,10 @@ export function stringifyId(id) { if (typeof id !== 'object') { return id; } + const stringifyVal = v => (v && v.wild) || JSON.stringify(v); const parts = Object.keys(id) .sort() - .map(k => JSON.stringify(k) + ':' + JSON.stringify(id[k])); + .map(k => JSON.stringify(k) + ':' + stringifyVal(id[k])); return '{' + parts.join(',') + '}'; } @@ -165,7 +178,267 @@ function addPattern(depMap, idSpec, prop, dependency) { valMatch.callbacks.push(dependency); } -export function computeGraphs(dependencies) { +function validateDependencies(parsedDependencies, dispatchError) { + const outStrs = {}; + const outObjs = []; + + parsedDependencies.forEach(dep => { + const {inputs, outputs, state} = dep; + let hasOutputs = true; + if (outputs.length === 1 && !outputs[0].id && !outputs[0].property) { + hasOutputs = false; + dispatchError('A callback is missing Outputs', [ + 'Please provide an output for this callback:', + JSON.stringify(dep, null, 2), + ]); + } + + const head = + 'In the callback for output(s):\n ' + + outputs.map(combineIdAndProp).join('\n '); + + if (!inputs.length) { + dispatchError('A callback is missing Inputs', [ + head, + 'there are no `Input` elements.', + 'Without `Input` elements, it will never get called.', + '', + 'Subscribing to `Input` components will cause the', + 'callback to be called whenever their values change.', + ]); + } + + const spec = [[outputs, 'Output'], [inputs, 'Input'], [state, 'State']]; + spec.forEach(([args, cls]) => { + if (cls === 'Output' && !hasOutputs) { + // just a quirk of how we pass & parse outputs - if you don't + // provide one, it looks like a single blank output. This is + // actually useful for graceful failure, so we work around it. + return; + } + + if (!Array.isArray(args)) { + dispatchError(`Callback ${cls}(s) must be an Array`, [ + head, + `For ${cls}(s) we found:`, + JSON.stringify(args), + 'but we expected an Array.', + ]); + } + args.forEach((idProp, i) => { + validateArg(idProp, head, cls, i, dispatchError); + }); + }); + + findDuplicateOutputs(outputs, head, dispatchError, outStrs, outObjs); + findInOutOverlap(outputs, inputs, head, dispatchError); + findMismatchedWildcards(outputs, inputs, state, head, dispatchError); + }); +} + +function validateArg({id, property}, head, cls, i, dispatchError) { + if (typeof property !== 'string' || !property) { + dispatchError('Callback property error', [ + head, + `${cls}[${i}].property = ${JSON.stringify(property)}`, + 'but we expected `property` to be a non-empty string.', + ]); + } + + if (typeof id === 'object') { + if (isEmpty(id)) { + dispatchError('Callback item missing ID', [ + head, + `${cls}[${i}].id = {}`, + 'Every item linked to a callback needs an ID', + ]); + } + + forEachObjIndexed((v, k) => { + if (!k) { + dispatchError('Callback wildcard ID error', [ + head, + `${cls}[${i}].id has key "${k}"`, + 'Keys must be non-empty strings.', + ]); + } + + if (typeof v === 'object' && v.wild) { + if (allowedWildcards[cls][v.wild] !== v) { + dispatchError('Callback wildcard ID error', [ + head, + `${cls}[${i}].id["${k}"] = ${v.wild}`, + `Allowed wildcards for ${cls}s are:`, + keys(allowedWildcards[cls]).join(', '), + ]); + } + } else if (!includes(typeof v, wildcardValTypes)) { + dispatchError('Callback wildcard ID error', [ + head, + `${cls}[${i}].id["${k}"] = ${JSON.stringify(v)}`, + 'Wildcard callback ID values must be either wildcards', + 'or constants of one of these types:', + wildcardValTypes.join(', '), + ]); + } + }, id); + } else if (typeof id === 'string') { + if (!id) { + dispatchError('Callback item missing ID', [ + head, + `${cls}[${i}].id = "${id}"`, + 'Every item linked to a callback needs an ID', + ]); + } + const invalidChars = idInvalidChars.filter(c => includes(c, id)); + if (invalidChars.length) { + dispatchError('Callback invalid ID string', [ + head, + `${cls}[${i}].id = '${id}'`, + `characters '${invalidChars.join("', '")}' are not allowed.`, + ]); + } + } else { + dispatchError('Callback ID type error', [ + head, + `${cls}[${i}].id = ${JSON.stringify(id)}`, + 'IDs must be strings or wildcard-compatible objects.', + ]); + } +} + +function findDuplicateOutputs(outputs, head, dispatchError, outStrs, outObjs) { + const newOutputStrs = {}; + const newOutputObjs = []; + outputs.forEach(({id, property}, i) => { + if (typeof id === 'string') { + const idProp = combineIdAndProp({id, property}); + if (newOutputStrs[idProp]) { + dispatchError('Duplicate callback Outputs', [ + head, + `Output ${i} (${idProp}) is already used by this callback.`, + ]); + } else if (outStrs[idProp]) { + dispatchError('Duplicate callback outputs', [ + head, + `Output ${i} (${idProp}) is already in use.`, + 'Any given output can only have one callback that sets it.', + 'To resolve this situation, try combining these into', + 'one callback function, distinguishing the trigger', + 'by using `dash.callback_context` if necessary.', + ]); + } else { + newOutputStrs[idProp] = 1; + } + } else { + const idObj = {id, property}; + const selfOverlap = wildcardOverlap(idObj, newOutputObjs); + const otherOverlap = selfOverlap || wildcardOverlap(idObj, outObjs); + if (selfOverlap || otherOverlap) { + const idProp = combineIdAndProp(idObj); + const idProp2 = combineIdAndProp(selfOverlap || otherOverlap); + dispatchError('Overlapping wildcard callback outputs', [ + head, + `Output ${i} (${idProp})`, + `overlaps another output (${idProp2})`, + `used in ${selfOverlap ? 'this' : 'a different'} callback.`, + ]); + } else { + newOutputObjs.push(idObj); + } + } + }); + keys(newOutputStrs).forEach(k => { + outStrs[k] = 1; + }); + newOutputObjs.forEach(idObj => { + outObjs.push(idObj); + }); +} + +function findInOutOverlap(outputs, inputs, head, dispatchError) { + outputs.forEach((out, outi) => { + const {id: outId, property: outProp} = out; + inputs.forEach((in_, ini) => { + const {id: inId, property: inProp} = in_; + if (outProp !== inProp || typeof outId !== typeof inId) { + return; + } + if (typeof outId === 'string') { + if (outId === inId) { + dispatchError('Same `Input` and `Output`', [ + head, + `Input ${ini} (${combineIdAndProp(in_)})`, + `matches Output ${outi} (${combineIdAndProp(out)})`, + ]); + } + } else if (wildcardOverlap(in_, [out])) { + dispatchError('Same `Input` and `Output`', [ + head, + `Input ${ini} (${combineIdAndProp(in_)})`, + 'can match the same component(s) as', + `Output ${outi} (${combineIdAndProp(out)})`, + ]); + } + }); + }); +} + +function findMismatchedWildcards(outputs, inputs, state, head, dispatchError) { + const {anyKeys: out0AnyKeys} = findWildcardKeys(outputs[0].id); + outputs.forEach((out, outi) => { + if (outi && !equals(findWildcardKeys(out.id).anyKeys, out0AnyKeys)) { + dispatchError('Mismatched `MATCH` wildcards across `Output`s', [ + head, + `Output ${outi} (${combineIdAndProp(out)})`, + 'does not have MATCH wildcards on the same keys as', + `Output 0 (${combineIdAndProp(outputs[0])}).`, + 'MATCH wildcards must be on the same keys for all Outputs.', + 'ALL wildcards need not match, only MATCH.', + ]); + } + }); + [[inputs, 'Input'], [state, 'State']].forEach(([args, cls]) => { + args.forEach((arg, i) => { + const {anyKeys, allsmallerKeys} = findWildcardKeys(arg.id); + const allWildcardKeys = anyKeys.concat(allsmallerKeys); + const diff = difference(allWildcardKeys, out0AnyKeys); + if (diff.length) { + diff.sort(); + dispatchError('`Input` / `State` wildcards not in `Output`s', [ + head, + `${cls} ${i} (${combineIdAndProp(arg)})`, + `has MATCH or ALLSMALLER on key(s) ${diff.join(', ')}`, + `where Output 0 (${combineIdAndProp(outputs[0])})`, + 'does not have a MATCH wildcard. Inputs and State do not', + 'need every MATCH from the Output(s), but they cannot have', + 'extras beyond the Output(s).', + ]); + } + }); + }); +} + +const matchWildKeys = ([a, b]) => a === b || (a && a.wild) || (b && b.wild); + +function wildcardOverlap({id, property}, objs) { + const idKeys = keys(id).sort(); + const idVals = props(idKeys, id); + for (const obj of objs) { + const {id: id2, property: property2} = obj; + if ( + property2 === property && + typeof id2 !== 'string' && + equals(keys(id2).sort(), idKeys) && + all(matchWildKeys, zip(idVals, props(idKeys, id2))) + ) { + return obj; + } + } + return false; +} + +export function computeGraphs(dependencies, dispatchError) { const inputGraph = new DepGraph(); // multiGraph is just for finding circular deps const multiGraph = new DepGraph(); @@ -183,6 +456,8 @@ export function computeGraphs(dependencies) { return out; }, dependencies); + validateDependencies(parsedDependencies, dispatchError); + /* * For regular ids, outputMap and inputMap are: * {[id]: {[prop]: [callback, ...]}} @@ -319,18 +594,9 @@ export function computeGraphs(dependencies) { // Also collect MATCH keys in the output (all outputs must share these) // and ALL keys in the first output (need not be shared but we'll use // the first output for calculations) for later convenience. - const anyKeys = []; - let hasAll = false; - forEachObjIndexed((val, key) => { - if (val === MATCH) { - anyKeys.push(key); - } else if (val === ALL) { - hasAll = true; - } - }, outputs[0].id); - anyKeys.sort(); + const {anyKeys, hasALL} = findWildcardKeys(outputs[0].id); const finalDependency = mergeRight( - {hasAll, anyKeys, outputs}, + {hasALL, anyKeys, outputs}, dependency ); @@ -375,6 +641,26 @@ export function computeGraphs(dependencies) { }; } +function findWildcardKeys(id) { + const anyKeys = []; + const allsmallerKeys = []; + let hasALL = false; + if (typeof id === 'object') { + forEachObjIndexed((val, key) => { + if (val === MATCH) { + anyKeys.push(key); + } else if (val === ALLSMALLER) { + allsmallerKeys.push(key); + } else if (val === ALL) { + hasALL = true; + } + }, id); + anyKeys.sort(); + allsmallerKeys.sort(); + } + return {anyKeys, allsmallerKeys, hasALL}; +} + /* * Do the given id values `vals` match the pattern `patternVals`? * `keys`, `patternVals`, and `vals` are all arrays, and we already know that @@ -583,8 +869,8 @@ function getCallbackByOutput(graphs, paths, id, prop) { * from an input to one item per combination of MATCH values. * That will give one result per callback invocation. */ -function reduceALLOuts(outs, anyKeys, hasAll) { - if (!hasAll) { +function reduceALLOuts(outs, anyKeys, hasALL) { + if (!hasALL) { return outs; } if (!anyKeys.length) { diff --git a/dash/_validate.py b/dash/_validate.py index b23328fa2d..e208901d88 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -2,7 +2,7 @@ import re from .development.base_component import Component -from .dependencies import Input, Output, State, MATCH, ALLSMALLER +from .dependencies import Input, Output, State from . import exceptions from ._utils import patch_collections_abc, _strings, stringify_id @@ -24,26 +24,11 @@ def validate_callback(app, layout, output, inputs, state): """ ) - if not inputs: - raise exceptions.MissingInputsException( - """ - This callback has no `Input` elements. - Without `Input` elements, this callback will never get called. - - Subscribing to Input components will cause the - callback to be called whenever their values change. - """ - ) - outputs = output if is_multi else [output] for args, cls in [(outputs, Output), (inputs, Input), (state, State)]: validate_callback_args(args, cls, layout, validate_ids) - prevent_duplicate_outputs(app, outputs) - prevent_input_output_overlap(inputs, outputs) - prevent_inconsistent_wildcards(outputs, inputs, state) - def validate_callback_args(args, cls, layout, validate_ids): name = cls.__name__ @@ -100,117 +85,6 @@ def validate_callback_args(args, cls, layout, validate_ids): ) -def prevent_duplicate_outputs(app, outputs): - for i, out in enumerate(outputs): - for out2 in outputs[i + 1:]: - if out == out2: - # Note: different but overlapping wildcards compare as equal - if str(out) == str(out2): - raise exceptions.DuplicateCallbackOutput( - """ - Same output {} was used more than once in a callback! - """.format( - str(out) - ) - ) - raise exceptions.DuplicateCallbackOutput( - """ - Two outputs in a callback can match the same ID! - {} and {} - """.format( - str(out), str(out2) - ) - ) - - dups = set() - for out in outputs: - for used_out in app.used_outputs: - if out == used_out: - dups.add(str(used_out)) - if dups: - dups = list(dups) - if len(outputs) > 1 or len(dups) > 1 or str(outputs[0]) != dups[0]: - raise exceptions.DuplicateCallbackOutput( - """ - One or more `Output` is already set by a callback. - Note that two wildcard outputs can refer to the same component - even if they don't match exactly. - - The new callback lists output(s): - {} - Already used: - {} - """.format( - ", ".join([str(out) for out in outputs]), - ", ".join(dups) - ) - ) - raise exceptions.DuplicateCallbackOutput( - """ - {} was already assigned to a callback. - Any given output can only have one callback that sets it. - Try combining your inputs and callback functions together - into one function. - """.format( - repr(outputs[0]) - ) - ) - - -def prevent_input_output_overlap(inputs, outputs): - for in_ in inputs: - for out in outputs: - if out == in_: - # Note: different but overlapping wildcards compare as equal - if str(out) == str(in_): - raise exceptions.SameInputOutputException( - "Same `Output` and `Input`: {}".format(out) - ) - raise exceptions.SameInputOutputException( - """ - An `Input` and an `Output` in one callback - can match the same ID! - {} and {} - """.format( - str(in_), str(out) - ) - ) - - -def prevent_inconsistent_wildcards(outputs, inputs, state): - any_keys = get_wildcard_keys(outputs[0], (MATCH,)) - for out in outputs[1:]: - if get_wildcard_keys(out, (MATCH,)) != any_keys: - raise exceptions.InconsistentCallbackWildcards( - """ - All `Output` items must have matching wildcard `MATCH` values. - `ALL` wildcards need not match, only `MATCH`. - - Output {} does not match the first output {}. - """.format( - out, outputs[0] - ) - ) - - matched_wildcards = (MATCH, ALLSMALLER) - for dep in list(inputs) + list(state): - wildcard_keys = get_wildcard_keys(dep, matched_wildcards) - if wildcard_keys - any_keys: - raise exceptions.InconsistentCallbackWildcards( - """ - `Input` and `State` items can only have {} - wildcards on keys where the `Output`(s) have `MATCH` wildcards. - `ALL` wildcards need not match, and you need not match every - `MATCH` in the `Output`(s). - - This callback has `MATCH` on keys {}. - {} has these wildcards on keys {}. - """.format( - matched_wildcards, any_keys, dep, wildcard_keys - ) - ) - - def validate_id_dict(arg, layout, validate_ids, wildcards): arg_id = arg.component_id @@ -238,7 +112,10 @@ def id_match(c): validate_prop_for_component(arg, component) for k, v in arg_id.items(): - if not (k and isinstance(k, _strings)): + # Need to keep key type validation on the Python side, since + # non-string keys will be converted to strings in json.dumps and may + # cause unwanted collisions + if not (isinstance(k, _strings)): raise exceptions.IncorrectTypeException( """ Wildcard ID keys must be non-empty strings, @@ -247,19 +124,6 @@ def id_match(c): k, arg_id ) ) - if not (v in wildcards or isinstance(v, _strings + (int, float, bool))): - wildcard_msg = ( - ",\n or wildcards: {}".format(wildcards) - if wildcards else "" - ) - raise exceptions.IncorrectTypeException( - """ - Wildcard {} ID values must be strings, numbers, bools{} - found {!r} in id {!r} - """.format( - arg.__class__.__name__, wildcard_msg, k, arg_id - ) - ) def validate_id_string(arg, layout, validate_ids): @@ -480,13 +344,6 @@ def _validate_value(val, index=None): ) -def get_wildcard_keys(dep, wildcards): - _id = dep.component_id - if not isinstance(_id, dict): - return set() - return {k for k, v in _id.items() if v in wildcards} - - def check_obsolete(kwargs): for key in kwargs: if key in ["components_cache_max_age", "static_folder"]: diff --git a/dash/exceptions.py b/dash/exceptions.py index 3f3aca5967..f1af19964b 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -30,18 +30,10 @@ class NonExistentEventException(CallbackException): pass -class UndefinedLayoutException(CallbackException): - pass - - class IncorrectTypeException(CallbackException): pass -class MissingInputsException(CallbackException): - pass - - class LayoutIsNotDefined(CallbackException): pass @@ -55,19 +47,6 @@ class InvalidComponentIdError(IDsCantContainPeriods): pass -class CantHaveMultipleOutputs(CallbackException): - pass - - -# Renamed for less confusion with multi output. -class DuplicateCallbackOutput(CantHaveMultipleOutputs): - pass - - -class InconsistentCallbackWildcards(CallbackException): - pass - - class PreventUpdate(CallbackException): pass @@ -100,10 +79,6 @@ class ResourceException(DashException): pass -class SameInputOutputException(CallbackException): - pass - - class MissingCallbackContextException(CallbackException): pass diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py new file mode 100644 index 0000000000..b5f189f3c5 --- /dev/null +++ b/tests/integration/devtools/test_callback_validation.py @@ -0,0 +1,344 @@ +import dash_core_components as dcc +import dash_html_components as html +import dash +from dash.dependencies import Input, Output, State, MATCH, ALL, ALLSMALLER +from dash.testing.wait import until_not + +debugging = dict( + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, +) + + +def check_error(dash_duo, index, message, snippets): + # This is not fully general - despite the selectors below, it only applies + # to front-end errors with no back-end errors in the list. + # Also the index is as on the page, which is opposite the execution order. + + found_message = dash_duo.find_elements(".dash-fe-error__title")[index].text + assert found_message == message + + if not snippets: + return + + dash_duo.find_elements(".test-devtools-error-toggle")[index].click() + + found_text = dash_duo.wait_for_element(".dash-backend-error").text + for snip in snippets: + assert snip in found_text + + # hide the error detail again - so only one detail is be visible at a time + dash_duo.find_elements(".test-devtools-error-toggle")[index].click() + dash_duo.wait_for_no_elements(".dash-backend-error") + + +def test_dvcv001_blank(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div() + + @app.callback([], []) + def x(): + return 42 + + dash_duo.start_server(app, **debugging) + + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2") + + check_error(dash_duo, 0, "A callback is missing Inputs", [ + "there are no `Input` elements." + ]) + check_error(dash_duo, 1, "A callback is missing Outputs", [ + "Please provide an output for this callback:" + ]) + + +def test_dvcv002_blank_id_prop(dash_duo): + # TODO: remove suppress_callback_exceptions after we move that part to FE + app = dash.Dash(__name__, suppress_callback_exceptions=True) + app.layout = html.Div([html.Div(id="a")]) + + @app.callback([Output("a", "children"), Output("", "")], [Input("", "")]) + def x(a): + return a + + dash_duo.start_server(app, **debugging) + + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "6") + + # the first 2 are just artifacts... the other 4 we care about + check_error(dash_duo, 0, "Circular Dependencies", []) + check_error(dash_duo, 1, "Same `Input` and `Output`", []) + + check_error(dash_duo, 2, "Callback item missing ID", [ + 'Input[0].id = ""', + "Every item linked to a callback needs an ID", + ]) + check_error(dash_duo, 3, "Callback property error", [ + 'Input[0].property = ""', + "expected `property` to be a non-empty string.", + ]) + check_error(dash_duo, 4, "Callback item missing ID", [ + 'Output[1].id = ""', + "Every item linked to a callback needs an ID", + ]) + check_error(dash_duo, 5, "Callback property error", [ + 'Output[1].property = ""', + "expected `property` to be a non-empty string.", + ]) + + +def test_dvcv003_duplicate_outputs_same_callback(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div([html.Div(id="a"), html.Div(id="b")]) + + @app.callback( + [Output("a", "children"), Output("a", "children")], + [Input("b", "children")] + ) + def x(b): + return b, b + + @app.callback( + [Output({"a": 1}, "children"), Output({"a": ALL}, "children")], + [Input("b", "children")] + ) + def y(b): + return b, b + + dash_duo.start_server(app, **debugging) + + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2") + + check_error(dash_duo, 0, "Overlapping wildcard callback outputs", [ + 'Output 1 ({"a":ALL}.children)', + 'overlaps another output ({"a":1}.children)', + "used in this callback", + ]) + check_error(dash_duo, 1, "Duplicate callback Outputs", [ + "Output 1 (a.children) is already used by this callback." + ]) + + +def test_dvcv004_duplicate_outputs_across_callbacks(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div([html.Div(id="a"), html.Div(id="b"), html.Div(id="c")]) + + @app.callback( + [Output("a", "children"), Output("a", "style")], + [Input("b", "children")] + ) + def x(b): + return b, b + + @app.callback(Output("b", "children"), [Input("b", "style")]) + def y(b): + return b + + @app.callback(Output("a", "children"), [Input("b", "children")]) + def x2(b): + return b + + @app.callback( + [Output("b", "children"), Output("b", "style")], + [Input("c", "children")] + ) + def y2(c): + return c + + @app.callback( + [Output({"a": 1}, "children"), Output({"b": ALL, "c": 1}, "children")], + [Input("b", "children")] + ) + def z(b): + return b, b + + @app.callback( + [Output({"a": ALL}, "children"), Output({"b": 1, "c": ALL}, "children")], + [Input("b", "children")] + ) + def z2(b): + return b, b + + @app.callback( + Output({"a": MATCH}, "children"), + [Input({"a": MATCH, "b": 1}, "children")] + ) + def z3(ab): + return ab + + dash_duo.start_server(app, **debugging) + + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "5") + + check_error(dash_duo, 0, "Overlapping wildcard callback outputs", [ + 'Output 0 ({"a":MATCH}.children)', + 'overlaps another output ({"a":1}.children)', + "used in a different callback.", + ]) + + check_error(dash_duo, 1, "Overlapping wildcard callback outputs", [ + 'Output 1 ({"b":1,"c":ALL}.children)', + 'overlaps another output ({"b":ALL,"c":1}.children)', + "used in a different callback.", + ]) + + check_error(dash_duo, 2, "Overlapping wildcard callback outputs", [ + 'Output 0 ({"a":ALL}.children)', + 'overlaps another output ({"a":1}.children)', + "used in a different callback.", + ]) + + check_error(dash_duo, 3, "Duplicate callback outputs", [ + "Output 0 (b.children) is already in use." + ]) + + check_error(dash_duo, 4, "Duplicate callback outputs", [ + "Output 0 (a.children) is already in use." + ]) + + +def test_dvcv005_input_output_overlap(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div([html.Div(id="a"), html.Div(id="b"), html.Div(id="c")]) + + @app.callback(Output("a", "children"), [Input("a", "children")]) + def x(a): + return a + + @app.callback( + [Output("b", "children"), Output("c", "children")], + [Input("c", "children")] + ) + def y(c): + return c, c + + @app.callback(Output({"a": ALL}, "children"), [Input({"a": 1}, "children")]) + def x2(a): + return [a] + + @app.callback( + [Output({"b": MATCH}, "children"), Output({"b": MATCH, "c": 1}, "children")], + [Input({"b": MATCH, "c": 1}, "children")] + ) + def y2(c): + return c, c + + dash_duo.start_server(app, **debugging) + + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "6") + + check_error(dash_duo, 0, "Dependency Cycle Found: a.children -> a.children", []) + check_error(dash_duo, 1, "Circular Dependencies", []) + + check_error(dash_duo, 2, "Same `Input` and `Output`", [ + 'Input 0 ({"b":MATCH,"c":1}.children)', + "can match the same component(s) as", + 'Output 1 ({"b":MATCH,"c":1}.children)', + ]) + + check_error(dash_duo, 3, "Same `Input` and `Output`", [ + 'Input 0 ({"a":1}.children)', + "can match the same component(s) as", + 'Output 0 ({"a":ALL}.children)', + ]) + + check_error(dash_duo, 4, "Same `Input` and `Output`", [ + "Input 0 (c.children)", + "matches Output 1 (c.children)", + ]) + + check_error(dash_duo, 5, "Same `Input` and `Output`", [ + "Input 0 (a.children)", + "matches Output 0 (a.children)", + ]) + + +def test_dvcv006_inconsistent_wildcards(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div() + + @app.callback( + [Output({"b": MATCH}, "children"), Output({"b": ALL, "c": 1}, "children")], + [Input({"b": MATCH, "c": 2}, "children")] + ) + def x(c): + return c, [c] + + @app.callback( + [Output({"a": MATCH}, "children")], + [Input({"b": MATCH}, "children"), Input({"c": ALLSMALLER}, "children")], + [State({"d": MATCH, "dd": MATCH}, "children"), State({"e": ALL}, "children")] + ) + def y(b, c, d, e): + return b + c + d + e + + dash_duo.start_server(app, **debugging) + + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") + + check_error(dash_duo, 0, "`Input` / `State` wildcards not in `Output`s", [ + 'State 0 ({"d":MATCH,"dd":MATCH}.children)', + "has MATCH or ALLSMALLER on key(s) d, dd", + 'where Output 0 ({"a":MATCH}.children)', + ]) + + check_error(dash_duo, 1, "`Input` / `State` wildcards not in `Output`s", [ + 'Input 1 ({"c":ALLSMALLER}.children)', + "has MATCH or ALLSMALLER on key(s) c", + 'where Output 0 ({"a":MATCH}.children)', + ]) + + check_error(dash_duo, 2, "`Input` / `State` wildcards not in `Output`s", [ + 'Input 0 ({"b":MATCH}.children)', + "has MATCH or ALLSMALLER on key(s) b", + 'where Output 0 ({"a":MATCH}.children)', + ]) + + check_error(dash_duo, 3, "Mismatched `MATCH` wildcards across `Output`s", [ + 'Output 1 ({"b":ALL,"c":1}.children)', + "does not have MATCH wildcards on the same keys as", + 'Output 0 ({"b":MATCH}.children).', + ]) + + +def test_dvcv007_disallowed_ids(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div() + + @app.callback( + Output({"": 1, "a": [4], "c": ALLSMALLER}, "children"), + [Input({"b": {"c": 1}}, "children")] + ) + def y(b): + return b + + dash_duo.start_server(app, **debugging) + + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") + + check_error(dash_duo, 0, "Callback wildcard ID error", [ + 'Input[0].id["b"] = {"c":1}', + "Wildcard callback ID values must be either wildcards", + "or constants of one of these types:", + "string, number, boolean", + ]) + + check_error(dash_duo, 1, "Callback wildcard ID error", [ + 'Output[0].id["c"] = ALLSMALLER', + "Allowed wildcards for Outputs are:", + "ALL, MATCH", + ]) + + check_error(dash_duo, 2, "Callback wildcard ID error", [ + 'Output[0].id["a"] = [4]', + "Wildcard callback ID values must be either wildcards", + "or constants of one of these types:", + "string, number, boolean", + ]) + + check_error(dash_duo, 3, "Callback wildcard ID error", [ + 'Output[0].id has key ""', + "Keys must be non-empty strings." + ]) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 7c23526fd5..89ab27b449 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -1,6 +1,5 @@ from multiprocessing import Value import datetime -import time import pytest from copy import copy @@ -18,8 +17,6 @@ from dash.dependencies import Input, Output, State from dash.exceptions import ( PreventUpdate, - DuplicateCallbackOutput, - CallbackException, InvalidCallbackReturnValue, IncorrectTypeException, NonExistentIdException, @@ -383,125 +380,6 @@ def create_layout(): assert dash_duo.find_element("#a").text == "Hello World" -def test_inin011_multi_output(dash_duo): - app = Dash(__name__) - - app.layout = html.Div( - [ - html.Button("OUTPUT", id="output-btn"), - html.Table( - [ - html.Thead( - [html.Tr([html.Th("Output 1"), html.Th("Output 2")])] - ), - html.Tbody( - [ - html.Tr( - [html.Td(id="output1"), html.Td(id="output2")] - ) - ] - ), - ] - ), - html.Div(id="output3"), - html.Div(id="output4"), - html.Div(id="output5"), - ] - ) - - @app.callback( - [Output("output1", "children"), Output("output2", "children")], - [Input("output-btn", "n_clicks")], - [State("output-btn", "n_clicks_timestamp")], - ) - def on_click(n_clicks, n_clicks_timestamp): - if n_clicks is None: - raise PreventUpdate - - return n_clicks, n_clicks_timestamp - - # Dummy callback for DuplicateCallbackOutput test. - @app.callback( - Output("output3", "children"), [Input("output-btn", "n_clicks")] - ) - def dummy_callback(n_clicks): - if n_clicks is None: - raise PreventUpdate - - return "Output 3: {}".format(n_clicks) - - with pytest.raises(DuplicateCallbackOutput) as err: - - @app.callback( - Output("output1", "children"), [Input("output-btn", "n_clicks")] - ) - def on_click_duplicate(n_clicks): - if n_clicks is None: - raise PreventUpdate - return "something else" - - pytest.fail("multi output can't be included in a single output") - - assert "output1" in err.value.args[0] - - with pytest.raises(DuplicateCallbackOutput) as err: - - @app.callback( - [Output("output3", "children"), Output("output4", "children")], - [Input("output-btn", "n_clicks")], - ) - def on_click_duplicate_multi(n_clicks): - if n_clicks is None: - raise PreventUpdate - return "something else" - - pytest.fail("multi output cannot contain a used single output") - - assert "output3" in err.value.args[0] - - with pytest.raises(DuplicateCallbackOutput) as err: - - @app.callback( - [Output("output5", "children"), Output("output5", "children")], - [Input("output-btn", "n_clicks")], - ) - def on_click_same_output(n_clicks): - return n_clicks - - pytest.fail("same output cannot be used twice in one callback") - - assert "output5" in err.value.args[0] - - with pytest.raises(DuplicateCallbackOutput) as err: - - @app.callback( - [Output("output1", "children"), Output("output5", "children")], - [Input("output-btn", "n_clicks")], - ) - def overlapping_multi_output(n_clicks): - return n_clicks - - pytest.fail( - "no part of an existing multi-output can be used in another" - ) - assert ( - "Already used:" in err.value.args[0] and - "output1.children" in err.value.args[0] - ) - - dash_duo.start_server(app) - - t = time.time() - - btn = dash_duo.find_element("#output-btn") - btn.click() - time.sleep(1) - - dash_duo.wait_for_text_to_equal("#output1", "1") - - assert int(dash_duo.find_element("#output2").text) > t - - def test_inin012_multi_output_no_update(dash_duo): app = Dash(__name__) @@ -781,38 +659,6 @@ def update_output(value): dash_duo.find_element("#inserted-input") -def test_inin018_output_input_invalid_callback(): - app = Dash(__name__) - app.layout = html.Div( - [html.Div("child", id="input-output"), html.Div(id="out")] - ) - - with pytest.raises(CallbackException) as err: - - @app.callback( - Output("input-output", "children"), - [Input("input-output", "children")], - ) - def failure(children): - pass - - msg = "Same `Output` and `Input`: input-output.children" - assert err.value.args[0] == msg - - # Multi output version. - with pytest.raises(CallbackException) as err: - - @app.callback( - [Output("out", "children"), Output("input-output", "children")], - [Input("input-output", "children")], - ) - def failure2(children): - pass - - msg = "Same `Output` and `Input`: input-output.children" - assert err.value.args[0] == msg - - def test_inin019_callback_dep_types(): app = Dash(__name__) app.layout = html.Div( From 5b27dd7fa03fa680073950a4aa1052f83060034c Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 25 Mar 2020 22:35:21 -0400 Subject: [PATCH 46/81] move layout-linked callback validation to the front end --- dash-renderer/src/APIController.react.js | 17 +- dash-renderer/src/actions/dependencies.js | 192 +++++++++++++++--- dash-renderer/src/actions/index.js | 10 + dash/_validate.py | 77 ------- dash/dash.py | 1 + dash/exceptions.py | 12 -- .../devtools/test_callback_validation.py | 170 ++++++++++++++++ tests/integration/test_integration.py | 50 ----- 8 files changed, 356 insertions(+), 173 deletions(-) diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index 0d71f11167..0ef112dd77 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -5,11 +5,12 @@ import PropTypes from 'prop-types'; import TreeContainer from './TreeContainer'; import GlobalErrorContainer from './components/error/GlobalErrorContainer.react'; import { + dispatchError, hydrateInitialOutputs, + onError, setGraphs, setPaths, setLayout, - onError, } from './actions'; import {computePaths} from './actions/paths'; import {computeGraphs} from './actions/dependencies'; @@ -83,16 +84,12 @@ class UnconnectedContainer extends Component { dependenciesRequest.status === STATUS.OK && isEmpty(graphs) ) { - const dispatchError = (message, lines) => - dispatch( - onError({ - type: 'backEnd', - error: {message, html: lines.join('\n')}, - }) - ); dispatch( setGraphs( - computeGraphs(dependenciesRequest.content, dispatchError) + computeGraphs( + dependenciesRequest.content, + dispatchError(dispatch) + ) ) ); } @@ -109,7 +106,7 @@ class UnconnectedContainer extends Component { ) { let errorLoading = false; try { - dispatch(hydrateInitialOutputs()); + dispatch(hydrateInitialOutputs(dispatchError(dispatch))); } catch (err) { // Display this error in devtools, unless we have errors // already, in which case we assume this new one is moot diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index b58e2c3ea4..0f6ee4676f 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -13,6 +13,7 @@ import { flatten, forEachObjIndexed, includes, + intersection, isEmpty, keys, map, @@ -34,6 +35,8 @@ import {getPath} from './paths'; import {crawlLayout} from './utils'; +import Registry from '../registry'; + /* * If this update is for multiple outputs, then it has * starting & trailing `..` and each propId pair is separated @@ -438,6 +441,145 @@ function wildcardOverlap({id, property}, objs) { return false; } +export function validateCallbacksToLayout(state_, dispatchError) { + const {config, graphs, layout, paths} = state_; + const {outputMap, inputMap, outputPatterns, inputPatterns} = graphs; + const validateIds = !config.suppress_callback_exceptions; + + function tail(callbacks) { + return ( + 'This ID was used in the callback(s) for Output(s):\n ' + + callbacks + .map(({outputs}) => outputs.map(combineIdAndProp).join(', ')) + .join('\n ') + ); + } + + function missingId(id, cls, callbacks) { + dispatchError('ID not found in layout', [ + `Attempting to connect a callback ${cls} item to component:`, + ` "${stringifyId(id)}"`, + 'but no components with that id exist in the layout.', + '', + 'If you are assigning callbacks to components that are', + 'generated by other callbacks (and therefore not in the', + 'initial layout), you can suppress this exception by setting', + '`suppress_callback_exceptions=True`.', + tail(callbacks) + ]); + } + + function validateProp(id, idPath, prop, cls, callbacks) { + const component = path(idPath, layout); + const element = Registry.resolve(component); + if (element && !element.propTypes[prop]) { + // look for wildcard props (ie data-* etc) + for (const propName in element.propTypes) { + const last = propName.length - 1; + if ( + propName.charAt(last) === '*' && + prop.substr(0, last) === propName.substr(0, last) + ) { + return; + } + } + const {type, namespace} = component; + dispatchError('Invalid prop for this component', [ + `Property "${prop}" was used with component ID:`, + ` ${JSON.stringify(id)}`, + `in one of the ${cls} items of a callback.`, + `This ID is assigned to a ${namespace}.${type} component`, + 'in the layout, which does not support this property.', + tail(callbacks), + ]); + } + } + + function validateIdPatternProp(id, property, cls, callbacks) { + resolveDeps()(paths)({id, property}).forEach(dep => { + const {id: idResolved, path: idPath} = dep; + validateProp(idResolved, idPath, property, cls, callbacks); + }); + } + + const callbackIdsCheckedForState = {}; + + function validateState(callback) { + const {state, output} = callback; + + // ensure we don't check the same callback for state multiple times + if (callbackIdsCheckedForState[output]) { + return; + } + callbackIdsCheckedForState[output] = 1; + + const cls = 'State'; + + state.forEach(({id, property}) => { + if (typeof id === 'string') { + const idPath = getPath(paths, id); + if (!idPath) { + if (validateIds) { + missingId(id, cls, [callback]); + } + } + else { + validateProp(id, idPath, property, cls, [callback]); + } + } + // Only validate props for State object ids that we don't need to + // resolve them to specific inputs or outputs + else if (!intersection([MATCH, ALLSMALLER], values(id)).length) { + validateIdPatternProp(id, property, cls, [callback]); + } + }); + } + + function validateMap(map, cls, doState) { + for (const id in map) { + const idProps = map[id]; + const idPath = getPath(paths, id); + if (!idPath) { + if (validateIds) { + missingId(id, cls, flatten(values(idProps))) + } + } + else { + for (const property in idProps) { + const callbacks = idProps[property]; + validateProp(id, idPath, property, cls, callbacks); + if (doState) { + // It would be redundant to check state on both inputs + // and outputs - so only set doState for outputs. + callbacks.forEach(validateState); + } + } + } + } + } + + validateMap(outputMap, 'Output', true); + validateMap(inputMap, 'Input'); + + function validatePatterns(patterns, cls, doState) { + for (const keyStr in patterns) { + const keyPatterns = patterns[keyStr]; + for (const property in keyPatterns) { + keyPatterns[property].forEach(({keys, values, callbacks}) => { + const id = zipObj(keys, values); + validateIdPatternProp(id, property, cls, callbacks); + if (doState) { + callbacks.forEach(validateState); + } + }); + } + } + } + + validatePatterns(outputPatterns, 'Output', true); + validatePatterns(inputPatterns, 'Input'); +} + export function computeGraphs(dependencies, dispatchError) { const inputGraph = new DepGraph(); // multiGraph is just for finding circular deps @@ -724,31 +866,33 @@ function getAnyVals(patternVals, vals) { return matches.length ? JSON.stringify(matches) : ''; } -const resolveDeps = (refKeys, refVals, refPatternVals) => paths => ({ - id: idPattern, - property, -}) => { - if (typeof idPattern === 'string') { - const path = getPath(paths, idPattern); - return path ? [{id: idPattern, property, path}] : []; - } - const keys = Object.keys(idPattern).sort(); - const patternVals = props(keys, idPattern); - const keyStr = keys.join(','); - const keyPaths = paths.objs[keyStr]; - if (!keyPaths) { - return []; - } - const result = []; - keyPaths.forEach(({values: vals, path}) => { - if ( - idMatch(keys, vals, patternVals, refKeys, refVals, refPatternVals) - ) { - result.push({id: zipObj(keys, vals), property, path}); +function resolveDeps(refKeys, refVals, refPatternVals) { + return paths => ({ + id: idPattern, + property, + }) => { + if (typeof idPattern === 'string') { + const path = getPath(paths, idPattern); + return path ? [{id: idPattern, property, path}] : []; } - }); - return result; -}; + const keys = Object.keys(idPattern).sort(); + const patternVals = props(keys, idPattern); + const keyStr = keys.join(','); + const keyPaths = paths.objs[keyStr]; + if (!keyPaths) { + return []; + } + const result = []; + keyPaths.forEach(({values: vals, path}) => { + if ( + idMatch(keys, vals, patternVals, refKeys, refVals, refPatternVals) + ) { + result.push({id: zipObj(keys, vals), property, path}); + } + }); + return result; + }; +} /* * Create a pending callback object. Includes the original callback definition, diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 2efd7af376..84a5137204 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -39,6 +39,7 @@ import { pruneRemovedCallbacks, setNewRequestId, stringifyId, + validateCallbacksToLayout, } from './dependencies'; import {computePaths, getPath} from './paths'; import {STATUS} from '../constants/constants'; @@ -57,8 +58,17 @@ export const setHooks = createAction(getAction('SET_HOOKS')); export const setLayout = createAction(getAction('SET_LAYOUT')); export const onError = createAction(getAction('ON_ERROR')); +export const dispatchError = dispatch => (message, lines) => + dispatch( + onError({ + type: 'backEnd', + error: {message, html: lines.join('\n')}, + }) + ); + export function hydrateInitialOutputs() { return function(dispatch, getState) { + validateCallbacksToLayout(getState(), dispatchError(dispatch)); triggerDefaultState(dispatch, getState); dispatch(setAppLifecycle(getAppState('HYDRATED'))); }; diff --git a/dash/_validate.py b/dash/_validate.py index e208901d88..430bac57b1 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -11,19 +11,6 @@ def validate_callback(app, layout, output, inputs, state): is_multi = isinstance(output, (list, tuple)) validate_ids = not app.config.suppress_callback_exceptions - if layout is None and validate_ids: - # Without a layout, we can't do validation on the IDs and - # properties of the elements in the callback. - raise exceptions.LayoutIsNotDefined( - """ - Attempting to assign a callback to the application but - the `layout` property has not been assigned. - Assign the `layout` property before assigning callbacks. - Alternatively, suppress this warning by setting - `suppress_callback_exceptions=True` - """ - ) - outputs = output if is_multi else [output] for args, cls in [(outputs, Output), (inputs, Input), (state, State)]: @@ -88,29 +75,6 @@ def validate_callback_args(args, cls, layout, validate_ids): def validate_id_dict(arg, layout, validate_ids, wildcards): arg_id = arg.component_id - def id_match(c): - c_id = getattr(c, "id", None) - return isinstance(c_id, dict) and all( - k in c and v in wildcards or v == c_id.get(k) - for k, v in arg_id.items() - ) - - if validate_ids: - component = None - if id_match(layout): - component = layout - else: - for c in layout._traverse(): # pylint: disable=protected-access - if id_match(c): - component = c - break - if component: - # for wildcards it's not unusual to have no matching components - # initially; this isn't a problem and we shouldn't force users to - # set suppress_callback_exceptions in this case; but if we DO have - # a matching component, we can check that the prop is valid - validate_prop_for_component(arg, component) - for k, v in arg_id.items(): # Need to keep key type validation on the Python side, since # non-string keys will be converted to strings in json.dumps and may @@ -141,47 +105,6 @@ def validate_id_string(arg, layout, validate_ids): ) ) - if validate_ids: - top_id = getattr(layout, "id", None) - if arg_id not in layout and arg_id != top_id: - raise exceptions.NonExistentIdException( - """ - Attempting to assign a callback to the component with - id "{}" but no components with that id exist in the layout. - - Here is a list of IDs in layout: - {} - - If you are assigning callbacks to components that are - generated by other callbacks (and therefore not in the - initial layout), you can suppress this exception by setting - `suppress_callback_exceptions=True`. - """.format( - arg_id, [k for k in layout] + ([top_id] if top_id else []) - ) - ) - - component = layout if top_id == arg_id else layout[arg_id] - validate_prop_for_component(arg, component) - - -def validate_prop_for_component(arg, component): - arg_prop = arg.component_property - if arg_prop not in component.available_properties and not any( - arg_prop.startswith(w) for w in component.available_wildcard_properties - ): - raise exceptions.NonExistentPropException( - """ - Attempting to assign a callback with the property "{0}" - but component "{1}" doesn't have "{0}" as a property. - - Here are the available properties in "{1}": - {2} - """.format( - arg_prop, arg.component_id, component.available_properties - ) - ) - def validate_multi_return(outputs_list, output_value, callback_id): if not isinstance(output_value, (list, tuple)): diff --git a/dash/dash.py b/dash/dash.py index 5a6da4f192..9931923bbc 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -465,6 +465,7 @@ def _config(self): "ui": self._dev_tools.ui, "props_check": self._dev_tools.props_check, "show_undo_redo": self.config.show_undo_redo, + "suppress_callback_exceptions": self.config.suppress_callback_exceptions, } if self._dev_tools.hot_reload: config["hot_reload"] = { diff --git a/dash/exceptions.py b/dash/exceptions.py index f1af19964b..54439735fc 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -18,14 +18,6 @@ class CallbackException(DashException): pass -class NonExistentIdException(CallbackException): - pass - - -class NonExistentPropException(CallbackException): - pass - - class NonExistentEventException(CallbackException): pass @@ -34,10 +26,6 @@ class IncorrectTypeException(CallbackException): pass -class LayoutIsNotDefined(CallbackException): - pass - - class IDsCantContainPeriods(CallbackException): pass diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index b5f189f3c5..882ce4c4e6 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -342,3 +342,173 @@ def y(b): 'Output[0].id has key ""', "Keys must be non-empty strings." ]) + + +def bad_id_app(**kwargs): + app = dash.Dash(__name__, **kwargs) + app.layout = html.Div( + [ + html.Div( + [html.Div(id="inner-div"), dcc.Input(id="inner-input")], id="outer-div" + ), + dcc.Input(id="outer-input"), + ], + id="main", + ) + + @app.callback(Output("nuh-uh", "children"), [Input("inner-input", "value")]) + def f(a): + return a + + @app.callback(Output("outer-input", "value"), [Input("yeah-no", "value")]) + def g(a): + return a + + @app.callback( + [Output("inner-div", "children"), Output("nope", "children")], + [Input("inner-input", "value")], + [State("what", "children")] + ) + def g2(a): + return [a, a] + + # the right way + @app.callback(Output("inner-div", "style"), [Input("inner-input", "value")]) + def h(a): + return a + + return app + + +def test_dvcv008_wrong_callback_id(dash_duo): + dash_duo.start_server(bad_id_app(), **debugging) + + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") + + check_error(dash_duo, 0, "ID not found in layout", [ + "Attempting to connect a callback Input item to component:", + '"yeah-no"', + "but no components with that id exist in the layout.", + "If you are assigning callbacks to components that are", + "generated by other callbacks (and therefore not in the", + "initial layout), you can suppress this exception by setting", + "`suppress_callback_exceptions=True`.", + "This ID was used in the callback(s) for Output(s):", + "outer-input.value" + ]) + + check_error(dash_duo, 1, "ID not found in layout", [ + "Attempting to connect a callback Output item to component:", + '"nope"', + "but no components with that id exist in the layout.", + "This ID was used in the callback(s) for Output(s):", + "inner-div.children, nope.children" + ]) + + check_error(dash_duo, 2, "ID not found in layout", [ + "Attempting to connect a callback State item to component:", + '"what"', + "but no components with that id exist in the layout.", + "This ID was used in the callback(s) for Output(s):", + "inner-div.children, nope.children" + ]) + + check_error(dash_duo, 3, "ID not found in layout", [ + "Attempting to connect a callback Output item to component:", + '"nuh-uh"', + "but no components with that id exist in the layout.", + "This ID was used in the callback(s) for Output(s):", + "nuh-uh.children" + ]) + + +def test_dvcv009_suppress_callback_exceptions(dash_duo): + dash_duo.start_server(bad_id_app(suppress_callback_exceptions=True), **debugging) + + dash_duo.find_element('.dash-debug-menu') + dash_duo.wait_for_no_elements('.test-devtools-error-count') + + +def test_dvcv010_bad_props(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + html.Div( + [html.Div(id="inner-div"), dcc.Input(id="inner-input")], id="outer-div" + ), + dcc.Input(id={"a": 1}), + ], + id="main", + ) + + @app.callback( + Output("inner-div", "xyz"), + # "data-xyz" is OK, does not give an error + [Input("inner-input", "pdq"), Input("inner-div", "data-xyz")], + [State("inner-div", "value")] + ) + def xyz(a, b, c): + a if b else c + + @app.callback( + Output({"a": MATCH}, "no"), + [Input({"a": MATCH}, "never")], + # "boo" will not error because we don't check State MATCH/ALLSMALLER + [State({"a": MATCH}, "boo"), State({"a": ALL}, "nope")] + ) + def f(a, b, c): + return a if b else c + + dash_duo.start_server(app, **debugging) + + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "6") + + check_error(dash_duo, 0, "Invalid prop for this component", [ + 'Property "never" was used with component ID:', + '{"a":1}', + "in one of the Input items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + "in the layout, which does not support this property.", + "This ID was used in the callback(s) for Output(s):", + '{"a":MATCH}.no' + ]) + + check_error(dash_duo, 1, "Invalid prop for this component", [ + 'Property "nope" was used with component ID:', + '{"a":1}', + "in one of the State items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + '{"a":MATCH}.no' + ]) + + check_error(dash_duo, 2, "Invalid prop for this component", [ + 'Property "no" was used with component ID:', + '{"a":1}', + "in one of the Output items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + '{"a":MATCH}.no' + ]) + + check_error(dash_duo, 3, "Invalid prop for this component", [ + 'Property "pdq" was used with component ID:', + '"inner-input"', + "in one of the Input items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + "inner-div.xyz" + ]) + + check_error(dash_duo, 4, "Invalid prop for this component", [ + 'Property "value" was used with component ID:', + '"inner-div"', + "in one of the State items of a callback.", + "This ID is assigned to a dash_html_components.Div component", + "inner-div.xyz" + ]) + + check_error(dash_duo, 5, "Invalid prop for this component", [ + 'Property "xyz" was used with component ID:', + '"inner-div"', + "in one of the Output items of a callback.", + "This ID is assigned to a dash_html_components.Div component", + "inner-div.xyz" + ]) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 89ab27b449..99e4e5f515 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -19,7 +19,6 @@ PreventUpdate, InvalidCallbackReturnValue, IncorrectTypeException, - NonExistentIdException, ) from dash.testing.wait import until @@ -759,52 +758,3 @@ def multi2(a): ] multi2("aaa", outputs_list=outputs_list) pytest.fail("wrong-length list") - - -def test_inin023_wrong_callback_id(): - app = Dash(__name__) - app.layout = html.Div( - [ - html.Div( - [html.Div(id="inner-div"), dcc.Input(id="inner-input")], id="outer-div" - ), - dcc.Input(id="outer-input"), - ], - id="main", - ) - - ids = ["main", "inner-div", "inner-input", "outer-div", "outer-input"] - - with pytest.raises(NonExistentIdException) as err: - - @app.callback(Output("nuh-uh", "children"), [Input("inner-input", "value")]) - def f(a): - return a - - assert '"nuh-uh"' in err.value.args[0] - for component_id in ids: - assert component_id in err.value.args[0] - - with pytest.raises(NonExistentIdException) as err: - - @app.callback(Output("inner-div", "children"), [Input("yeah-no", "value")]) - def g(a): - return a - - assert '"yeah-no"' in err.value.args[0] - for component_id in ids: - assert component_id in err.value.args[0] - - with pytest.raises(NonExistentIdException) as err: - - @app.callback( - [Output("inner-div", "children"), Output("nope", "children")], - [Input("inner-input", "value")], - ) - def g2(a): - return [a, a] - - # the right way - @app.callback(Output("inner-div", "children"), [Input("inner-input", "value")]) - def h(a): - return a From 1ace29fcebc6372431b7cbfe151fff4eca6da2d2 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Mar 2020 15:42:16 -0400 Subject: [PATCH 47/81] add npm-run-all for formatting --- dash-renderer/package-lock.json | 164 ++++++++++++++++++++++++++++++++ dash-renderer/package.json | 1 + 2 files changed, 165 insertions(+) diff --git a/dash-renderer/package-lock.json b/dash-renderer/package-lock.json index c931a4e646..5ea11c3458 100644 --- a/dash-renderer/package-lock.json +++ b/dash-renderer/package-lock.json @@ -11040,6 +11040,12 @@ "readable-stream": "^2.0.1" } }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true + }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", @@ -11665,6 +11671,63 @@ "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=", "dev": true }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + } + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -12528,6 +12591,12 @@ "integrity": "sha512-OYMyqkKzK7blWO/+XZYP6w8hH0LDvkBvdvKukti+7kqYFCiEAk+gI3DWnryapc0Dau05ugGTy0foQ6mqn4AHYA==", "dev": true }, + "pidtree": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", + "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -15284,6 +15353,12 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", @@ -15974,6 +16049,95 @@ } } }, + "string.prototype.padend": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz", + "integrity": "sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + } + } + }, "string.prototype.trimleft": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", diff --git a/dash-renderer/package.json b/dash-renderer/package.json index 9b116e0726..6e1d57f94a 100644 --- a/dash-renderer/package.json +++ b/dash-renderer/package.json @@ -55,6 +55,7 @@ "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-react": "^7.18.3", "jest": "^25.1.0", + "npm-run-all": "^4.1.5", "prettier": "^1.19.1", "prettier-eslint": "^9.0.1", "prettier-eslint-cli": "^5.0.0", From a3fe1b41556dc6d9f9111597d817ce00962651ca Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Mar 2020 16:06:53 -0400 Subject: [PATCH 48/81] lint/format --- dash-renderer/src/APIController.react.js | 4 +- dash-renderer/src/actions/dependencies.js | 35 +- dash/_utils.py | 7 +- dash/_validate.py | 19 +- dash/_watch.py | 2 +- dash/dash.py | 33 +- dash/dependencies.py | 5 +- dash/development/base_component.py | 4 +- dash/development/build_process.py | 4 +- dash/development/component_generator.py | 4 +- dash/development/component_loader.py | 2 +- dash/testing/application_runners.py | 8 +- dash/testing/browser.py | 22 +- dash/testing/dash_page.py | 2 +- dash/testing/plugin.py | 10 +- dash/testing/wait.py | 4 +- .../callbacks/test_basic_callback.py | 95 ++- .../callbacks/test_callback_context.py | 26 +- .../test_layout_paths_with_callbacks.py | 8 +- .../callbacks/test_multiple_callbacks.py | 88 +-- tests/integration/callbacks/test_wildcards.py | 88 ++- .../devtools/test_callback_validation.py | 574 +++++++++++------- .../devtools/test_devtools_error_handling.py | 2 +- .../integration/devtools/test_props_check.py | 2 +- .../integration/renderer/test_persistence.py | 6 +- .../renderer/test_state_and_input.py | 14 +- tests/integration/test_integration.py | 10 +- tests/integration/test_render.py | 48 +- tests/unit/test_configs.py | 6 +- 29 files changed, 630 insertions(+), 502 deletions(-) diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index 1034647e47..d19748f76e 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -54,7 +54,9 @@ const UnconnectedContainer = props => { dispatch ); dispatch( - setPaths(computePaths(finalLayout, [], null, events.current)) + setPaths( + computePaths(finalLayout, [], null, events.current) + ) ); dispatch(setLayout(finalLayout)); } diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 0f6ee4676f..7df7156a68 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -211,7 +211,11 @@ function validateDependencies(parsedDependencies, dispatchError) { ]); } - const spec = [[outputs, 'Output'], [inputs, 'Input'], [state, 'State']]; + const spec = [ + [outputs, 'Output'], + [inputs, 'Input'], + [state, 'State'], + ]; spec.forEach(([args, cls]) => { if (cls === 'Output' && !hasOutputs) { // just a quirk of how we pass & parse outputs - if you don't @@ -401,7 +405,10 @@ function findMismatchedWildcards(outputs, inputs, state, head, dispatchError) { ]); } }); - [[inputs, 'Input'], [state, 'State']].forEach(([args, cls]) => { + [ + [inputs, 'Input'], + [state, 'State'], + ].forEach(([args, cls]) => { args.forEach((arg, i) => { const {anyKeys, allsmallerKeys} = findWildcardKeys(arg.id); const allWildcardKeys = anyKeys.concat(allsmallerKeys); @@ -465,7 +472,7 @@ export function validateCallbacksToLayout(state_, dispatchError) { 'generated by other callbacks (and therefore not in the', 'initial layout), you can suppress this exception by setting', '`suppress_callback_exceptions=True`.', - tail(callbacks) + tail(callbacks), ]); } @@ -522,8 +529,7 @@ export function validateCallbacksToLayout(state_, dispatchError) { if (validateIds) { missingId(id, cls, [callback]); } - } - else { + } else { validateProp(id, idPath, property, cls, [callback]); } } @@ -541,10 +547,9 @@ export function validateCallbacksToLayout(state_, dispatchError) { const idPath = getPath(paths, id); if (!idPath) { if (validateIds) { - missingId(id, cls, flatten(values(idProps))) + missingId(id, cls, flatten(values(idProps))); } - } - else { + } else { for (const property in idProps) { const callbacks = idProps[property]; validateProp(id, idPath, property, cls, callbacks); @@ -867,10 +872,7 @@ function getAnyVals(patternVals, vals) { } function resolveDeps(refKeys, refVals, refPatternVals) { - return paths => ({ - id: idPattern, - property, - }) => { + return paths => ({id: idPattern, property}) => { if (typeof idPattern === 'string') { const path = getPath(paths, idPattern); return path ? [{id: idPattern, property, path}] : []; @@ -885,7 +887,14 @@ function resolveDeps(refKeys, refVals, refPatternVals) { const result = []; keyPaths.forEach(({values: vals, path}) => { if ( - idMatch(keys, vals, patternVals, refKeys, refVals, refPatternVals) + idMatch( + keys, + vals, + patternVals, + refKeys, + refVals, + refPatternVals + ) ) { result.push({id: zipObj(keys, vals), property, path}); } diff --git a/dash/_utils.py b/dash/_utils.py index 4019bc119e..51c476c9c3 100644 --- a/dash/_utils.py +++ b/dash/_utils.py @@ -17,7 +17,7 @@ # py2/3 json.dumps-compatible strings - these are equivalent in py3, not in py2 # note because we import unicode_literals u"" and "" are both unicode -_strings = (type(u""), type(utils.bytes_to_native_str(b""))) +_strings = (type(""), type(utils.bytes_to_native_str(b""))) def interpolate_str(template, **data): @@ -165,15 +165,14 @@ def create_callback_id(output): # but in case of multiple dots together escape each dot # with `\` so we don't mistake it for multi-outputs x.component_id_str().replace(".", "\\."), - x.component_property + x.component_property, ) for x in output ) ) return "{}.{}".format( - output.component_id_str().replace(".", "\\."), - output.component_property + output.component_id_str().replace(".", "\\."), output.component_property ) diff --git a/dash/_validate.py b/dash/_validate.py index 430bac57b1..68f0ec77ff 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -7,17 +7,16 @@ from ._utils import patch_collections_abc, _strings, stringify_id -def validate_callback(app, layout, output, inputs, state): +def validate_callback(output, inputs, state): is_multi = isinstance(output, (list, tuple)) - validate_ids = not app.config.suppress_callback_exceptions outputs = output if is_multi else [output] for args, cls in [(outputs, Output), (inputs, Input), (state, State)]: - validate_callback_args(args, cls, layout, validate_ids) + validate_callback_args(args, cls) -def validate_callback_args(args, cls, layout, validate_ids): +def validate_callback_args(args, cls): name = cls.__name__ if not isinstance(args, (list, tuple)): raise exceptions.IncorrectTypeException( @@ -57,10 +56,10 @@ def validate_callback_args(args, cls, layout, validate_ids): ) if isinstance(arg.component_id, dict): - validate_id_dict(arg, layout, validate_ids, cls.allowed_wildcards) + validate_id_dict(arg) elif isinstance(arg.component_id, _strings): - validate_id_string(arg, layout, validate_ids) + validate_id_string(arg) else: raise exceptions.IncorrectTypeException( @@ -72,14 +71,14 @@ def validate_callback_args(args, cls, layout, validate_ids): ) -def validate_id_dict(arg, layout, validate_ids, wildcards): +def validate_id_dict(arg): arg_id = arg.component_id - for k, v in arg_id.items(): + for k in arg_id: # Need to keep key type validation on the Python side, since # non-string keys will be converted to strings in json.dumps and may # cause unwanted collisions - if not (isinstance(k, _strings)): + if not isinstance(k, _strings): raise exceptions.IncorrectTypeException( """ Wildcard ID keys must be non-empty strings, @@ -90,7 +89,7 @@ def validate_id_dict(arg, layout, validate_ids, wildcards): ) -def validate_id_string(arg, layout, validate_ids): +def validate_id_string(arg): arg_id = arg.component_id invalid_chars = ".{" diff --git a/dash/_watch.py b/dash/_watch.py index 34c523478c..65c87e284a 100644 --- a/dash/_watch.py +++ b/dash/_watch.py @@ -11,7 +11,7 @@ def watch(folders, on_change, pattern=None, sleep_time=0.1): def walk(): walked = [] for folder in folders: - for current, _, files, in os.walk(folder): + for current, _, files in os.walk(folder): for f in files: if pattern and not pattern.search(f): continue diff --git a/dash/dash.py b/dash/dash.py index 5b17f88ad0..752eb7ee6b 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -40,7 +40,7 @@ patch_collections_abc, split_callback_id, stringify_id, - strip_relative_path + strip_relative_path, ) from . import _validate from . import _watch @@ -282,7 +282,7 @@ def __init__( external_scripts=external_scripts or [], external_stylesheets=external_stylesheets or [], suppress_callback_exceptions=get_combined_config( - "suppress_callback_exceptions", suppress_callback_exceptions, False, + "suppress_callback_exceptions", suppress_callback_exceptions, False ), show_undo_redo=show_undo_redo, ) @@ -388,9 +388,7 @@ def _handle_error(_): self.server.before_first_request(self._setup_server) # add a handler for components suites errors to return 404 - self.server.errorhandler(InvalidResourceError)( - self._invalid_resources_handler - ) + self.server.errorhandler(InvalidResourceError)(self._invalid_resources_handler) self._add_url( "_dash-component-suites//", @@ -497,7 +495,7 @@ def _collect_and_register_resources(self, resources): def _relative_url_path(relative_package_path="", namespace=""): module_path = os.path.join( - os.path.dirname(sys.modules[namespace].__file__), relative_package_path, + os.path.dirname(sys.modules[namespace].__file__), relative_package_path ) modified = int(os.stat(module_path).st_mtime) @@ -602,9 +600,9 @@ def _generate_scripts_html(self): ) def _generate_config_html(self): - return ( - '' - ).format(json.dumps(self._config())) + return ('').format( + json.dumps(self._config()) + ) def _generate_renderer(self): return ( @@ -654,7 +652,7 @@ def serve_component_suites(self, package_name, fingerprinted_path): ) response = flask.Response( - pkgutil.get_data(package_name, path_in_pkg), mimetype=mimetype, + pkgutil.get_data(package_name, path_in_pkg), mimetype=mimetype ) if has_fingerprint: @@ -792,8 +790,7 @@ def dependencies(self): ) def _insert_callback(self, output, inputs, state): - layout = self._cached_layout or self._layout_value() - _validate.validate_callback(self, layout, output, inputs, state) + _validate.validate_callback(output, inputs, state) callback_id = create_callback_id(output) self.callback_map[callback_id] = { @@ -882,7 +879,7 @@ def clientside_callback(self, clientside_function, output, inputs, state=()): _inline_clientside_template.format( namespace=namespace.replace('"', '\\"'), function_name=function_name.replace('"', '\\"'), - clientside_function=clientside_function + clientside_function=clientside_function, ) ) @@ -916,9 +913,7 @@ def add_context(*args, **kwargs): if not multi: output_value, output_spec = [output_value], [output_spec] - _validate.validate_multi_return( - output_spec, output_value, callback_id - ) + _validate.validate_multi_return(output_spec, output_value, callback_id) component_ids = collections.defaultdict(dict) has_update = False @@ -926,9 +921,7 @@ def add_context(*args, **kwargs): if isinstance(val, _NoUpdate): continue for vali, speci in ( - zip(val, spec) - if isinstance(spec, list) - else [[val, spec]] + zip(val, spec) if isinstance(spec, list) else [[val, spec]] ): if not isinstance(vali, _NoUpdate): has_update = True @@ -1047,7 +1040,7 @@ def _invalid_resources_handler(err): @staticmethod def _serve_default_favicon(): return flask.Response( - pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon", + pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon" ) def get_asset_url(self, path): diff --git a/dash/dependencies.py b/dash/dependencies.py index 5a3fb9b37c..fa79b842d5 100644 --- a/dash/dependencies.py +++ b/dash/dependencies.py @@ -49,10 +49,7 @@ def _json(k, v): return i def to_dict(self): - return { - "id": self.component_id_str(), - "property": self.component_property - } + return {"id": self.component_id_str(), "property": self.component_property} def __eq__(self, other): """ diff --git a/dash/development/base_component.py b/dash/development/base_component.py index 3ee18dd635..b68b359941 100644 --- a/dash/development/base_component.py +++ b/dash/development/base_component.py @@ -284,9 +284,7 @@ def _traverse_with_paths(self): elif isinstance(children, (tuple, MutableSequence)): for idx, i in enumerate(children): list_path = "[{:d}] {:s}{}".format( - idx, - type(i).__name__, - self._id_str(i), + idx, type(i).__name__, self._id_str(i) ) yield list_path, i diff --git a/dash/development/build_process.py b/dash/development/build_process.py index 283c3ad460..f621d1e6cf 100644 --- a/dash/development/build_process.py +++ b/dash/development/build_process.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) coloredlogs.install( - fmt="%(asctime)s,%(msecs)03d %(levelname)s - %(message)s", datefmt="%H:%M:%S", + fmt="%(asctime)s,%(msecs)03d %(levelname)s - %(message)s", datefmt="%H:%M:%S" ) @@ -150,7 +150,7 @@ def __init__(self): """dash-renderer's path is binding with the dash folder hierarchy.""" super(Renderer, self).__init__( self._concat( - os.path.dirname(__file__), os.pardir, os.pardir, "dash-renderer", + os.path.dirname(__file__), os.pardir, os.pardir, "dash-renderer" ), ( ("@babel", "polyfill", "dist", "polyfill.min.js", None), diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py index 9702b72d3d..fd1c1d62ec 100644 --- a/dash/development/component_generator.py +++ b/dash/development/component_generator.py @@ -31,7 +31,7 @@ class _CombinedFormatter( - argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter, + argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter ): pass @@ -144,7 +144,7 @@ def cli(): ) parser.add_argument("components_source", help="React components source directory.") parser.add_argument( - "project_shortname", help="Name of the project to export the classes files.", + "project_shortname", help="Name of the project to export the classes files." ) parser.add_argument( "-p", diff --git a/dash/development/component_loader.py b/dash/development/component_loader.py index 648c171cd5..d7b06e7f3b 100644 --- a/dash/development/component_loader.py +++ b/dash/development/component_loader.py @@ -52,7 +52,7 @@ def load_components(metadata_path, namespace="default_namespace"): # the name of the component atm. name = componentPath.split("/").pop().split(".")[0] component = generate_class( - name, componentData["props"], componentData["description"], namespace, + name, componentData["props"], componentData["description"], namespace ) components.append(component) diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index 0b52a9a0e7..a14f421abd 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -15,11 +15,7 @@ import flask import requests -from dash.testing.errors import ( - NoAppFoundError, - TestingTimeoutError, - ServerCloseError, -) +from dash.testing.errors import NoAppFoundError, TestingTimeoutError, ServerCloseError import dash.testing.wait as wait @@ -262,7 +258,7 @@ def start(self, app, start_timeout=2, cwd=None): # app is a string chunk, we make a temporary folder to store app.R # and its relevants assets self._tmp_app_path = os.path.join( - "/tmp" if not self.is_windows else os.getenv("TEMP"), uuid.uuid4().hex, + "/tmp" if not self.is_windows else os.getenv("TEMP"), uuid.uuid4().hex ) try: os.mkdir(self.tmp_app_path) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 8033332987..c9b1aa985e 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -20,18 +20,9 @@ MoveTargetOutOfBoundsException, ) -from dash.testing.wait import ( - text_to_equal, - style_to_equal, - contains_text, - until, -) +from dash.testing.wait import text_to_equal, style_to_equal, contains_text, until from dash.testing.dash_page import DashPageMixin -from dash.testing.errors import ( - DashAppLoadingError, - BrowserError, - TestingTimeoutError, -) +from dash.testing.errors import DashAppLoadingError, BrowserError, TestingTimeoutError from dash.testing.consts import SELENIUM_GRID_DEFAULT @@ -234,8 +225,9 @@ def wait_for_no_elements(self, selector, timeout=None): # so this one calls out directly to execute_script lambda: self.driver.execute_script( "return document.querySelectorAll('{}').length".format(selector) - ) == 0, - timeout if timeout else self._wait_timeout + ) + == 0, + timeout if timeout else self._wait_timeout, ) def wait_for_element_by_id(self, element_id, timeout=None): @@ -339,7 +331,7 @@ def select_dcc_dropdown(self, elem_or_selector, value=None, index=None): return logger.error( - "cannot find matching option using value=%s or index=%s", value, index, + "cannot find matching option using value=%s or index=%s", value, index ) def toggle_window(self): @@ -482,7 +474,7 @@ def clear_input(self, elem_or_selector): ).perform() def zoom_in_graph_by_ratio( - self, elem_or_selector, start_fraction=0.5, zoom_box_fraction=0.2, compare=True, + self, elem_or_selector, start_fraction=0.5, zoom_box_fraction=0.2, compare=True ): """Zoom out a graph with a zoom box fraction of component dimension default start at middle with a rectangle of 1/5 of the dimension use diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py index ce102918da..63b30d407a 100644 --- a/dash/testing/dash_page.py +++ b/dash/testing/dash_page.py @@ -4,7 +4,7 @@ class DashPageMixin(object): def _get_dash_dom_by_attribute(self, attr): return BeautifulSoup( - self.find_element(self.dash_entry_locator).get_attribute(attr), "lxml", + self.find_element(self.dash_entry_locator).get_attribute(attr), "lxml" ) @property diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index 5d107510c0..0881e14394 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -4,11 +4,7 @@ try: - from dash.testing.application_runners import ( - ThreadedRunner, - ProcessRunner, - RRunner, - ) + from dash.testing.application_runners import ThreadedRunner, ProcessRunner, RRunner from dash.testing.browser import Browser from dash.testing.composite import DashComposite, DashRComposite except ImportError: @@ -26,7 +22,7 @@ def pytest_addoption(parser): ) dash.addoption( - "--remote", action="store_true", help="instruct pytest to use selenium grid", + "--remote", action="store_true", help="instruct pytest to use selenium grid" ) dash.addoption( @@ -37,7 +33,7 @@ def pytest_addoption(parser): ) dash.addoption( - "--headless", action="store_true", help="set this flag to run in headless mode", + "--headless", action="store_true", help="set this flag to run in headless mode" ) dash.addoption( diff --git a/dash/testing/wait.py b/dash/testing/wait.py index 1f87fef733..1cf1b0dd8e 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -10,7 +10,7 @@ def until( - wait_cond, timeout, poll=0.1, msg="expected condition not met within timeout", + wait_cond, timeout, poll=0.1, msg="expected condition not met within timeout" ): # noqa: C0330 res = wait_cond() logger.debug( @@ -31,7 +31,7 @@ def until( def until_not( - wait_cond, timeout, poll=0.1, msg="expected condition met within timeout", + wait_cond, timeout, poll=0.1, msg="expected condition met within timeout" ): # noqa: C0330 res = wait_cond() logger.debug( diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index 0d5aa10593..3d84d7a07f 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -174,51 +174,55 @@ def update_out(n_clicks): def test_cbsc004_callback_using_unloaded_async_component(dash_duo): app = dash.Dash() - app.layout = html.Div([ - dcc.Tabs([ - dcc.Tab("boo!"), - dcc.Tab( - dash_table.DataTable( - id="table", - columns=[{"id": "a", "name": "A"}], - data=[{"a": "b"}] - ) + app.layout = html.Div( + [ + dcc.Tabs( + [ + dcc.Tab("boo!"), + dcc.Tab( + dash_table.DataTable( + id="table", + columns=[{"id": "a", "name": "A"}], + data=[{"a": "b"}], + ) + ), + ] ), - ]), - html.Button("Update Input", id="btn"), - html.Div("Hello", id="output"), - html.Div(id="output2") - ]) + html.Button("Update Input", id="btn"), + html.Div("Hello", id="output"), + html.Div(id="output2"), + ] + ) @app.callback( Output("output", "children"), [Input("btn", "n_clicks")], - [State("table", "data")] + [State("table", "data")], ) def update_out(n_clicks, data): - return json.dumps(data) + ' - ' + str(n_clicks) + return json.dumps(data) + " - " + str(n_clicks) @app.callback( Output("output2", "children"), [Input("btn", "n_clicks")], - [State("table", "derived_viewport_data")] + [State("table", "derived_viewport_data")], ) def update_out2(n_clicks, data): - return json.dumps(data) + ' - ' + str(n_clicks) + return json.dumps(data) + " - " + str(n_clicks) dash_duo.start_server(app) dash_duo.wait_for_text_to_equal("#output", '[{"a": "b"}] - None') - dash_duo.wait_for_text_to_equal("#output2", 'null - None') + dash_duo.wait_for_text_to_equal("#output2", "null - None") dash_duo.find_element("#btn").click() dash_duo.wait_for_text_to_equal("#output", '[{"a": "b"}] - 1') - dash_duo.wait_for_text_to_equal("#output2", 'null - 1') + dash_duo.wait_for_text_to_equal("#output2", "null - 1") dash_duo.find_element(".tab:not(.tab--selected)").click() dash_duo.wait_for_text_to_equal("#table th", "A") # table props are in state so no change yet - dash_duo.wait_for_text_to_equal("#output2", 'null - 1') + dash_duo.wait_for_text_to_equal("#output2", "null - 1") # repeat a few times, since one of the failure modes I saw during dev was # intermittent - but predictably so? @@ -234,10 +238,7 @@ def update_out2(n_clicks, data): def test_cbsc005_children_types(dash_duo): app = dash.Dash() - app.layout = html.Div([ - html.Button(id="btn"), - html.Div("init", id="out") - ]) + app.layout = html.Div([html.Button(id="btn"), html.Div("init", id="out")]) outputs = [ [None, ""], @@ -247,7 +248,7 @@ def test_cbsc005_children_types(dash_duo): [[6, 7, 8], "678"], [["a", "list", "of", "strings"], "alistofstrings"], [["strings", 2, "numbers"], "strings2numbers"], - [["a string", html.Div("and a div")], "a string\nand a div"] + [["a string", html.Div("and a div")], "a string\nand a div"], ] @app.callback(Output("out", "children"), [Input("btn", "n_clicks")]) @@ -266,18 +267,13 @@ def set_children(n): def test_cbsc006_array_of_objects(dash_duo): app = dash.Dash() - app.layout = html.Div([ - html.Button(id="btn"), - dcc.Dropdown(id="dd"), - html.Div(id="out") - ]) + app.layout = html.Div( + [html.Button(id="btn"), dcc.Dropdown(id="dd"), html.Div(id="out")] + ) @app.callback(Output("dd", "options"), [Input("btn", "n_clicks")]) def set_options(n): - return [ - {"label": "opt{}".format(i), "value": i} - for i in range(n or 0) - ] + return [{"label": "opt{}".format(i), "value": i} for i in range(n or 0)] @app.callback(Output("out", "children"), [Input("dd", "options")]) def set_out(opts): @@ -310,34 +306,35 @@ def test_cbsc007_parallel_updates(refresh, dash_duo): app = dash.Dash() - app.layout = html.Div([ - dcc.Location(id='loc', refresh=refresh), - html.Button('Update path', id='btn'), - dash_table.DataTable(id='t', columns=[{'name': 'a', 'id': 'a'}]), - html.Div(id='out') - ]) + app.layout = html.Div( + [ + dcc.Location(id="loc", refresh=refresh), + html.Button("Update path", id="btn"), + dash_table.DataTable(id="t", columns=[{"name": "a", "id": "a"}]), + html.Div(id="out"), + ] + ) - @app.callback(Output('t', 'data'), [Input('loc', 'pathname')]) + @app.callback(Output("t", "data"), [Input("loc", "pathname")]) def set_data(path): - return [{'a': (path or repr(path)) + ':a'}] + return [{"a": (path or repr(path)) + ":a"}] @app.callback( - Output('out', 'children'), - [Input('loc', 'pathname'), Input('t', 'data')] + Output("out", "children"), [Input("loc", "pathname"), Input("t", "data")] ) def set_out(path, data): - return json.dumps(data) + ' - ' + (path or repr(path)) + return json.dumps(data) + " - " + (path or repr(path)) - @app.callback(Output('loc', 'pathname'), [Input('btn', 'n_clicks')]) + @app.callback(Output("loc", "pathname"), [Input("btn", "n_clicks")]) def set_path(n): if not n: raise PreventUpdate - return '/{0}'.format(n) + return "/{0}".format(n) dash_duo.start_server(app) - dash_duo.wait_for_text_to_equal('#out', '[{"a": "/:a"}] - /') + dash_duo.wait_for_text_to_equal("#out", '[{"a": "/:a"}] - /') dash_duo.find_element("#btn").click() # the refresh=True case here is testing that we really do get the right # pathname, not the prevented default value from the layout. diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index fa909948cc..bddca9c6de 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -13,15 +13,11 @@ def test_cbcx001_modified_response(dash_duo): app = Dash(__name__) - app.layout = html.Div( - [dcc.Input(id="input", value="ab"), html.Div(id="output")] - ) + app.layout = html.Div([dcc.Input(id="input", value="ab"), html.Div(id="output")]) @app.callback(Output("output", "children"), [Input("input", "value")]) def update_output(value): - callback_context.response.set_cookie( - "dash cookie", value + " - cookie" - ) + callback_context.response.set_cookie("dash cookie", value + " - cookie") return value + " - output" dash_duo.start_server(app) @@ -44,15 +40,10 @@ def test_cbcx002_triggered(dash_duo): btns = ["btn-{}".format(x) for x in range(1, 6)] app.layout = html.Div( - [ - html.Div([html.Button(btn, id=btn) for btn in btns]), - html.Div(id="output"), - ] + [html.Div([html.Button(btn, id=btn) for btn in btns]), html.Div(id="output")] ) - @app.callback( - Output("output", "children"), [Input(x, "n_clicks") for x in btns] - ) + @app.callback(Output("output", "children"), [Input(x, "n_clicks") for x in btns]) def on_click(*args): if not callback_context.triggered: raise PreventUpdate @@ -79,10 +70,7 @@ def test_cbcx003_no_callback_context(): def test_cbcx004_triggered_backward_compat(dash_duo): app = Dash(__name__) - app.layout = html.Div([ - html.Button("click!", id="btn"), - html.Div(id="out") - ]) + app.layout = html.Div([html.Button("click!", id="btn"), html.Div(id="out")]) @app.callback(Output("out", "children"), [Input("btn", "n_clicks")]) def report_triggered(n): @@ -99,12 +87,12 @@ def report_triggered(n): dash_duo.wait_for_text_to_equal( "#out", 'triggered is falsy, has prop/id ["", ""], and full value ' - '[{"prop_id": ".", "value": null}]' + '[{"prop_id": ".", "value": null}]', ) dash_duo.find_element("#btn").click() dash_duo.wait_for_text_to_equal( "#out", 'triggered is truthy, has prop/id ["btn", "n_clicks"], and full value ' - '[{"prop_id": "btn.n_clicks", "value": 1}]' + '[{"prop_id": "btn.n_clicks", "value": 1}]', ) diff --git a/tests/integration/callbacks/test_layout_paths_with_callbacks.py b/tests/integration/callbacks/test_layout_paths_with_callbacks.py index ed3b94a088..80656d5b5f 100644 --- a/tests/integration/callbacks/test_layout_paths_with_callbacks.py +++ b/tests/integration/callbacks/test_layout_paths_with_callbacks.py @@ -119,7 +119,7 @@ def callback(value): "title": value, "width": 500, "height": 400, - "margin": {"autoexpand": False} + "margin": {"autoexpand": False}, }, } @@ -165,8 +165,8 @@ def check_chapter(chapter): wait.until( lambda: ( dash_duo.driver.execute_script( - 'return document.querySelector("' + - "#{}-graph:not(.dash-graph--pending) .js-plotly-plot".format( + 'return document.querySelector("' + + "#{}-graph:not(.dash-graph--pending) .js-plotly-plot".format( chapter ) + '").layout.title.text' @@ -234,7 +234,7 @@ def check_call_counts(chapters, count): dash_duo.find_elements('input[type="radio"]')[0].click() wait.until( - lambda: dash_duo.redux_state_paths == EXPECTED_PATHS["chapter1"], TIMEOUT, + lambda: dash_duo.redux_state_paths == EXPECTED_PATHS["chapter1"], TIMEOUT ) check_chapter("chapter1") dash_duo.percy_snapshot(name="chapter-1-again") diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index ecf1bf57f2..a86a31175c 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -42,16 +42,18 @@ def update_output(n_clicks): def test_cbmt002_canceled_intermediate_callback(dash_duo): # see https://github.com/plotly/dash/issues/1053 app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="a", value="x"), - html.Div("b", id="b"), - html.Div("c", id="c"), - html.Div(id="out") - ]) + app.layout = html.Div( + [ + dcc.Input(id="a", value="x"), + html.Div("b", id="b"), + html.Div("c", id="c"), + html.Div(id="out"), + ] + ) @app.callback( Output("out", "children"), - [Input("a", "value"), Input("b", "children"), Input("c", "children")] + [Input("a", "value"), Input("b", "children"), Input("c", "children")], ) def set_out(a, b, c): return "{}/{}/{}".format(a, b, c) @@ -76,14 +78,16 @@ def set_c(a): def test_cbmt003_chain_with_table(dash_duo): # see https://github.com/plotly/dash/issues/1071 app = dash.Dash(__name__) - app.layout = html.Div([ - html.Div(id="a1"), - html.Div(id="a2"), - html.Div(id="b1"), - html.H1(id="b2"), - html.Button("Update", id="button"), - dash_table.DataTable(id="table"), - ]) + app.layout = html.Div( + [ + html.Div(id="a1"), + html.Div(id="a2"), + html.Div(id="b1"), + html.H1(id="b2"), + html.Button("Update", id="button"), + dash_table.DataTable(id="table"), + ] + ) @app.callback( # Changing the order of outputs here fixes the issue @@ -127,29 +131,37 @@ def b2(a2, selected_cells): @pytest.mark.parametrize("MULTI", [False, True]) def test_cbmt004_chain_with_sliders(MULTI, dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - html.Button("Button", id="button"), - html.Div([ - html.Label(id="label1"), - dcc.Slider(id="slider1", min=0, max=10, value=0), - - ]), - html.Div([ - html.Label(id="label2"), - dcc.Slider(id="slider2", min=0, max=10, value=0), - ]) - ]) + app.layout = html.Div( + [ + html.Button("Button", id="button"), + html.Div( + [ + html.Label(id="label1"), + dcc.Slider(id="slider1", min=0, max=10, value=0), + ] + ), + html.Div( + [ + html.Label(id="label2"), + dcc.Slider(id="slider2", min=0, max=10, value=0), + ] + ), + ] + ) if MULTI: + @app.callback( [Output("slider1", "value"), Output("slider2", "value")], - [Input("button", "n_clicks")] + [Input("button", "n_clicks")], ) def update_slider_vals(n): if not n: raise PreventUpdate return n, n + else: + @app.callback(Output("slider1", "value"), [Input("button", "n_clicks")]) def update_slider1_val(n): if not n: @@ -186,17 +198,19 @@ def update_slider2_label(val): def test_cbmt005_multi_converging_chain(dash_duo): app = dash.Dash(__name__) - app.layout = html.Div([ - html.Button("Button 1", id="b1"), - html.Button("Button 2", id="b2"), - dcc.Slider(id="slider1", min=-5, max=5), - dcc.Slider(id="slider2", min=-5, max=5), - html.Div(id="out") - ]) + app.layout = html.Div( + [ + html.Button("Button 1", id="b1"), + html.Button("Button 2", id="b2"), + dcc.Slider(id="slider1", min=-5, max=5), + dcc.Slider(id="slider2", min=-5, max=5), + html.Div(id="out"), + ] + ) @app.callback( [Output("slider1", "value"), Output("slider2", "value")], - [Input("b1", "n_clicks"), Input("b2", "n_clicks")] + [Input("b1", "n_clicks"), Input("b2", "n_clicks")], ) def update_sliders(button1, button2): if not dash.callback_context.triggered: @@ -209,7 +223,7 @@ def update_sliders(button1, button2): @app.callback( Output("out", "children"), - [Input("slider1", "value"), Input("slider2", "value")] + [Input("slider1", "value"), Input("slider2", "value")], ) def update_graph(s1, s2): return "x={}, y={}".format(s1, s2) diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index 97d9f4bff7..e718457b16 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -8,7 +8,7 @@ def css_escape(s): - sel = re.sub('[\\{\\}\\"\\\'.:,]', lambda m: '\\' + m.group(0), s) + sel = re.sub("[\\{\\}\\\"\\'.:,]", lambda m: "\\" + m.group(0), s) print(sel) return sel @@ -16,40 +16,39 @@ def css_escape(s): def todo_app(): app = dash.Dash(__name__) - app.layout = html.Div([ - html.Div('Dash To-Do list'), - dcc.Input(id="new-item"), - html.Button("Add", id="add"), - html.Button("Clear Done", id="clear-done"), - html.Div(id="list-container"), - html.Hr(), - html.Div(id="totals") - ]) + app.layout = html.Div( + [ + html.Div("Dash To-Do list"), + dcc.Input(id="new-item"), + html.Button("Add", id="add"), + html.Button("Clear Done", id="clear-done"), + html.Div(id="list-container"), + html.Hr(), + html.Div(id="totals"), + ] + ) style_todo = {"display": "inline", "margin": "10px"} style_done = {"textDecoration": "line-through", "color": "#888"} style_done.update(style_todo) - app.list_calls = Value('i', 0) - app.style_calls = Value('i', 0) - app.preceding_calls = Value('i', 0) - app.total_calls = Value('i', 0) + app.list_calls = Value("i", 0) + app.style_calls = Value("i", 0) + app.preceding_calls = Value("i", 0) + app.total_calls = Value("i", 0) @app.callback( - [ - Output("list-container", "children"), - Output("new-item", "value") - ], + [Output("list-container", "children"), Output("new-item", "value")], [ Input("add", "n_clicks"), Input("new-item", "n_submit"), - Input("clear-done", "n_clicks") + Input("clear-done", "n_clicks"), ], [ State("new-item", "value"), State({"item": ALL}, "children"), - State({"item": ALL, "action": "done"}, "value") - ] + State({"item": ALL, "action": "done"}, "value"), + ], ) def edit_list(add, add2, clear, new_item, items, items_done): app.list_calls.value += 1 @@ -59,33 +58,35 @@ def edit_list(add, add2, clear, new_item, items, items_done): ) clearing = len([1 for i in triggered if i == "clear-done.n_clicks"]) new_spec = [ - (text, done) for text, done in zip(items, items_done) + (text, done) + for text, done in zip(items, items_done) if not (clearing and done) ] if adding: new_spec.append((new_item, [])) new_list = [ - html.Div([ - dcc.Checklist( - id={"item": i, "action": "done"}, - options=[{"label": "", "value": "done"}], - value=done, - style={"display": "inline"} - ), - html.Div( - text, - id={"item": i}, - style=style_done if done else style_todo - ), - html.Div(id={"item": i, "preceding": True}, style=style_todo) - ], style={"clear": "both"}) + html.Div( + [ + dcc.Checklist( + id={"item": i, "action": "done"}, + options=[{"label": "", "value": "done"}], + value=done, + style={"display": "inline"}, + ), + html.Div( + text, id={"item": i}, style=style_done if done else style_todo + ), + html.Div(id={"item": i, "preceding": True}, style=style_todo), + ], + style={"clear": "both"}, + ) for i, (text, done) in enumerate(new_spec) ] return [new_list, "" if adding else new_item] @app.callback( Output({"item": MATCH}, "style"), - [Input({"item": MATCH, "action": "done"}, "value")] + [Input({"item": MATCH, "action": "done"}, "value")], ) def mark_done(done): app.style_calls.value += 1 @@ -95,8 +96,8 @@ def mark_done(done): Output({"item": MATCH, "preceding": True}, "children"), [ Input({"item": ALLSMALLER, "action": "done"}, "value"), - Input({"item": MATCH, "action": "done"}, "value") - ] + Input({"item": MATCH, "action": "done"}, "value"), + ], ) def show_preceding(done_before, this_done): app.preceding_calls.value += 1 @@ -110,8 +111,7 @@ def show_preceding(done_before, this_done): return out @app.callback( - Output("totals", "children"), - [Input({"item": ALL, "action": "done"}, "value")] + Output("totals", "children"), [Input({"item": ALL, "action": "done"}, "value")] ) def show_totals(done): app.total_calls.value += 1 @@ -147,16 +147,14 @@ def get_done_item(item): return dash_duo.find_element(selector) def assert_item(item, text, done, prefix="", suffix=""): - dash_duo.wait_for_text_to_equal( - css_escape('#{"item":%d}' % item), text - ) + dash_duo.wait_for_text_to_equal(css_escape('#{"item":%d}' % item), text) expected_note = "" if done else (prefix + " preceding items are done" + suffix) dash_duo.wait_for_text_to_equal( css_escape('#{"item":%d,"preceding":true}' % item), expected_note ) - assert bool(get_done_item(item).get_attribute('checked')) == done + assert bool(get_done_item(item).get_attribute("checked")) == done new_item.send_keys("apples") add_item.click() diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index 882ce4c4e6..74e4c424c1 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -2,13 +2,9 @@ import dash_html_components as html import dash from dash.dependencies import Input, Output, State, MATCH, ALL, ALLSMALLER -from dash.testing.wait import until_not debugging = dict( - debug=True, - use_reloader=False, - use_debugger=True, - dev_tools_hot_reload=False, + debug=True, use_reloader=False, use_debugger=True, dev_tools_hot_reload=False ) @@ -46,12 +42,15 @@ def x(): dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2") - check_error(dash_duo, 0, "A callback is missing Inputs", [ - "there are no `Input` elements." - ]) - check_error(dash_duo, 1, "A callback is missing Outputs", [ - "Please provide an output for this callback:" - ]) + check_error( + dash_duo, 0, "A callback is missing Inputs", ["there are no `Input` elements."] + ) + check_error( + dash_duo, + 1, + "A callback is missing Outputs", + ["Please provide an output for this callback:"], + ) def test_dvcv002_blank_id_prop(dash_duo): @@ -71,22 +70,30 @@ def x(a): check_error(dash_duo, 0, "Circular Dependencies", []) check_error(dash_duo, 1, "Same `Input` and `Output`", []) - check_error(dash_duo, 2, "Callback item missing ID", [ - 'Input[0].id = ""', - "Every item linked to a callback needs an ID", - ]) - check_error(dash_duo, 3, "Callback property error", [ - 'Input[0].property = ""', - "expected `property` to be a non-empty string.", - ]) - check_error(dash_duo, 4, "Callback item missing ID", [ - 'Output[1].id = ""', - "Every item linked to a callback needs an ID", - ]) - check_error(dash_duo, 5, "Callback property error", [ - 'Output[1].property = ""', - "expected `property` to be a non-empty string.", - ]) + check_error( + dash_duo, + 2, + "Callback item missing ID", + ['Input[0].id = ""', "Every item linked to a callback needs an ID"], + ) + check_error( + dash_duo, + 3, + "Callback property error", + ['Input[0].property = ""', "expected `property` to be a non-empty string."], + ) + check_error( + dash_duo, + 4, + "Callback item missing ID", + ['Output[1].id = ""', "Every item linked to a callback needs an ID"], + ) + check_error( + dash_duo, + 5, + "Callback property error", + ['Output[1].property = ""', "expected `property` to be a non-empty string."], + ) def test_dvcv003_duplicate_outputs_same_callback(dash_duo): @@ -94,15 +101,14 @@ def test_dvcv003_duplicate_outputs_same_callback(dash_duo): app.layout = html.Div([html.Div(id="a"), html.Div(id="b")]) @app.callback( - [Output("a", "children"), Output("a", "children")], - [Input("b", "children")] + [Output("a", "children"), Output("a", "children")], [Input("b", "children")] ) def x(b): return b, b @app.callback( [Output({"a": 1}, "children"), Output({"a": ALL}, "children")], - [Input("b", "children")] + [Input("b", "children")], ) def y(b): return b, b @@ -111,14 +117,22 @@ def y(b): dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2") - check_error(dash_duo, 0, "Overlapping wildcard callback outputs", [ - 'Output 1 ({"a":ALL}.children)', - 'overlaps another output ({"a":1}.children)', - "used in this callback", - ]) - check_error(dash_duo, 1, "Duplicate callback Outputs", [ - "Output 1 (a.children) is already used by this callback." - ]) + check_error( + dash_duo, + 0, + "Overlapping wildcard callback outputs", + [ + 'Output 1 ({"a":ALL}.children)', + 'overlaps another output ({"a":1}.children)', + "used in this callback", + ], + ) + check_error( + dash_duo, + 1, + "Duplicate callback Outputs", + ["Output 1 (a.children) is already used by this callback."], + ) def test_dvcv004_duplicate_outputs_across_callbacks(dash_duo): @@ -126,8 +140,7 @@ def test_dvcv004_duplicate_outputs_across_callbacks(dash_duo): app.layout = html.Div([html.Div(id="a"), html.Div(id="b"), html.Div(id="c")]) @app.callback( - [Output("a", "children"), Output("a", "style")], - [Input("b", "children")] + [Output("a", "children"), Output("a", "style")], [Input("b", "children")] ) def x(b): return b, b @@ -141,29 +154,27 @@ def x2(b): return b @app.callback( - [Output("b", "children"), Output("b", "style")], - [Input("c", "children")] + [Output("b", "children"), Output("b", "style")], [Input("c", "children")] ) def y2(c): return c @app.callback( [Output({"a": 1}, "children"), Output({"b": ALL, "c": 1}, "children")], - [Input("b", "children")] + [Input("b", "children")], ) def z(b): return b, b @app.callback( [Output({"a": ALL}, "children"), Output({"b": 1, "c": ALL}, "children")], - [Input("b", "children")] + [Input("b", "children")], ) def z2(b): return b, b @app.callback( - Output({"a": MATCH}, "children"), - [Input({"a": MATCH, "b": 1}, "children")] + Output({"a": MATCH}, "children"), [Input({"a": MATCH, "b": 1}, "children")] ) def z3(ab): return ab @@ -172,31 +183,52 @@ def z3(ab): dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "5") - check_error(dash_duo, 0, "Overlapping wildcard callback outputs", [ - 'Output 0 ({"a":MATCH}.children)', - 'overlaps another output ({"a":1}.children)', - "used in a different callback.", - ]) + check_error( + dash_duo, + 0, + "Overlapping wildcard callback outputs", + [ + 'Output 0 ({"a":MATCH}.children)', + 'overlaps another output ({"a":1}.children)', + "used in a different callback.", + ], + ) - check_error(dash_duo, 1, "Overlapping wildcard callback outputs", [ - 'Output 1 ({"b":1,"c":ALL}.children)', - 'overlaps another output ({"b":ALL,"c":1}.children)', - "used in a different callback.", - ]) + check_error( + dash_duo, + 1, + "Overlapping wildcard callback outputs", + [ + 'Output 1 ({"b":1,"c":ALL}.children)', + 'overlaps another output ({"b":ALL,"c":1}.children)', + "used in a different callback.", + ], + ) - check_error(dash_duo, 2, "Overlapping wildcard callback outputs", [ - 'Output 0 ({"a":ALL}.children)', - 'overlaps another output ({"a":1}.children)', - "used in a different callback.", - ]) + check_error( + dash_duo, + 2, + "Overlapping wildcard callback outputs", + [ + 'Output 0 ({"a":ALL}.children)', + 'overlaps another output ({"a":1}.children)', + "used in a different callback.", + ], + ) - check_error(dash_duo, 3, "Duplicate callback outputs", [ - "Output 0 (b.children) is already in use." - ]) + check_error( + dash_duo, + 3, + "Duplicate callback outputs", + ["Output 0 (b.children) is already in use."], + ) - check_error(dash_duo, 4, "Duplicate callback outputs", [ - "Output 0 (a.children) is already in use." - ]) + check_error( + dash_duo, + 4, + "Duplicate callback outputs", + ["Output 0 (a.children) is already in use."], + ) def test_dvcv005_input_output_overlap(dash_duo): @@ -208,8 +240,7 @@ def x(a): return a @app.callback( - [Output("b", "children"), Output("c", "children")], - [Input("c", "children")] + [Output("b", "children"), Output("c", "children")], [Input("c", "children")] ) def y(c): return c, c @@ -220,7 +251,7 @@ def x2(a): @app.callback( [Output({"b": MATCH}, "children"), Output({"b": MATCH, "c": 1}, "children")], - [Input({"b": MATCH, "c": 1}, "children")] + [Input({"b": MATCH, "c": 1}, "children")], ) def y2(c): return c, c @@ -232,27 +263,41 @@ def y2(c): check_error(dash_duo, 0, "Dependency Cycle Found: a.children -> a.children", []) check_error(dash_duo, 1, "Circular Dependencies", []) - check_error(dash_duo, 2, "Same `Input` and `Output`", [ - 'Input 0 ({"b":MATCH,"c":1}.children)', - "can match the same component(s) as", - 'Output 1 ({"b":MATCH,"c":1}.children)', - ]) + check_error( + dash_duo, + 2, + "Same `Input` and `Output`", + [ + 'Input 0 ({"b":MATCH,"c":1}.children)', + "can match the same component(s) as", + 'Output 1 ({"b":MATCH,"c":1}.children)', + ], + ) - check_error(dash_duo, 3, "Same `Input` and `Output`", [ - 'Input 0 ({"a":1}.children)', - "can match the same component(s) as", - 'Output 0 ({"a":ALL}.children)', - ]) + check_error( + dash_duo, + 3, + "Same `Input` and `Output`", + [ + 'Input 0 ({"a":1}.children)', + "can match the same component(s) as", + 'Output 0 ({"a":ALL}.children)', + ], + ) - check_error(dash_duo, 4, "Same `Input` and `Output`", [ - "Input 0 (c.children)", - "matches Output 1 (c.children)", - ]) + check_error( + dash_duo, + 4, + "Same `Input` and `Output`", + ["Input 0 (c.children)", "matches Output 1 (c.children)"], + ) - check_error(dash_duo, 5, "Same `Input` and `Output`", [ - "Input 0 (a.children)", - "matches Output 0 (a.children)", - ]) + check_error( + dash_duo, + 5, + "Same `Input` and `Output`", + ["Input 0 (a.children)", "matches Output 0 (a.children)"], + ) def test_dvcv006_inconsistent_wildcards(dash_duo): @@ -261,7 +306,7 @@ def test_dvcv006_inconsistent_wildcards(dash_duo): @app.callback( [Output({"b": MATCH}, "children"), Output({"b": ALL, "c": 1}, "children")], - [Input({"b": MATCH, "c": 2}, "children")] + [Input({"b": MATCH, "c": 2}, "children")], ) def x(c): return c, [c] @@ -269,7 +314,7 @@ def x(c): @app.callback( [Output({"a": MATCH}, "children")], [Input({"b": MATCH}, "children"), Input({"c": ALLSMALLER}, "children")], - [State({"d": MATCH, "dd": MATCH}, "children"), State({"e": ALL}, "children")] + [State({"d": MATCH, "dd": MATCH}, "children"), State({"e": ALL}, "children")], ) def y(b, c, d, e): return b + c + d + e @@ -278,29 +323,49 @@ def y(b, c, d, e): dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") - check_error(dash_duo, 0, "`Input` / `State` wildcards not in `Output`s", [ - 'State 0 ({"d":MATCH,"dd":MATCH}.children)', - "has MATCH or ALLSMALLER on key(s) d, dd", - 'where Output 0 ({"a":MATCH}.children)', - ]) + check_error( + dash_duo, + 0, + "`Input` / `State` wildcards not in `Output`s", + [ + 'State 0 ({"d":MATCH,"dd":MATCH}.children)', + "has MATCH or ALLSMALLER on key(s) d, dd", + 'where Output 0 ({"a":MATCH}.children)', + ], + ) - check_error(dash_duo, 1, "`Input` / `State` wildcards not in `Output`s", [ - 'Input 1 ({"c":ALLSMALLER}.children)', - "has MATCH or ALLSMALLER on key(s) c", - 'where Output 0 ({"a":MATCH}.children)', - ]) + check_error( + dash_duo, + 1, + "`Input` / `State` wildcards not in `Output`s", + [ + 'Input 1 ({"c":ALLSMALLER}.children)', + "has MATCH or ALLSMALLER on key(s) c", + 'where Output 0 ({"a":MATCH}.children)', + ], + ) - check_error(dash_duo, 2, "`Input` / `State` wildcards not in `Output`s", [ - 'Input 0 ({"b":MATCH}.children)', - "has MATCH or ALLSMALLER on key(s) b", - 'where Output 0 ({"a":MATCH}.children)', - ]) + check_error( + dash_duo, + 2, + "`Input` / `State` wildcards not in `Output`s", + [ + 'Input 0 ({"b":MATCH}.children)', + "has MATCH or ALLSMALLER on key(s) b", + 'where Output 0 ({"a":MATCH}.children)', + ], + ) - check_error(dash_duo, 3, "Mismatched `MATCH` wildcards across `Output`s", [ - 'Output 1 ({"b":ALL,"c":1}.children)', - "does not have MATCH wildcards on the same keys as", - 'Output 0 ({"b":MATCH}.children).', - ]) + check_error( + dash_duo, + 3, + "Mismatched `MATCH` wildcards across `Output`s", + [ + 'Output 1 ({"b":ALL,"c":1}.children)', + "does not have MATCH wildcards on the same keys as", + 'Output 0 ({"b":MATCH}.children).', + ], + ) def test_dvcv007_disallowed_ids(dash_duo): @@ -309,7 +374,7 @@ def test_dvcv007_disallowed_ids(dash_duo): @app.callback( Output({"": 1, "a": [4], "c": ALLSMALLER}, "children"), - [Input({"b": {"c": 1}}, "children")] + [Input({"b": {"c": 1}}, "children")], ) def y(b): return b @@ -318,30 +383,47 @@ def y(b): dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") - check_error(dash_duo, 0, "Callback wildcard ID error", [ - 'Input[0].id["b"] = {"c":1}', - "Wildcard callback ID values must be either wildcards", - "or constants of one of these types:", - "string, number, boolean", - ]) + check_error( + dash_duo, + 0, + "Callback wildcard ID error", + [ + 'Input[0].id["b"] = {"c":1}', + "Wildcard callback ID values must be either wildcards", + "or constants of one of these types:", + "string, number, boolean", + ], + ) - check_error(dash_duo, 1, "Callback wildcard ID error", [ - 'Output[0].id["c"] = ALLSMALLER', - "Allowed wildcards for Outputs are:", - "ALL, MATCH", - ]) + check_error( + dash_duo, + 1, + "Callback wildcard ID error", + [ + 'Output[0].id["c"] = ALLSMALLER', + "Allowed wildcards for Outputs are:", + "ALL, MATCH", + ], + ) - check_error(dash_duo, 2, "Callback wildcard ID error", [ - 'Output[0].id["a"] = [4]', - "Wildcard callback ID values must be either wildcards", - "or constants of one of these types:", - "string, number, boolean", - ]) + check_error( + dash_duo, + 2, + "Callback wildcard ID error", + [ + 'Output[0].id["a"] = [4]', + "Wildcard callback ID values must be either wildcards", + "or constants of one of these types:", + "string, number, boolean", + ], + ) - check_error(dash_duo, 3, "Callback wildcard ID error", [ - 'Output[0].id has key ""', - "Keys must be non-empty strings." - ]) + check_error( + dash_duo, + 3, + "Callback wildcard ID error", + ['Output[0].id has key ""', "Keys must be non-empty strings."], + ) def bad_id_app(**kwargs): @@ -367,7 +449,7 @@ def g(a): @app.callback( [Output("inner-div", "children"), Output("nope", "children")], [Input("inner-input", "value")], - [State("what", "children")] + [State("what", "children")], ) def g2(a): return [a, a] @@ -385,48 +467,68 @@ def test_dvcv008_wrong_callback_id(dash_duo): dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") - check_error(dash_duo, 0, "ID not found in layout", [ - "Attempting to connect a callback Input item to component:", - '"yeah-no"', - "but no components with that id exist in the layout.", - "If you are assigning callbacks to components that are", - "generated by other callbacks (and therefore not in the", - "initial layout), you can suppress this exception by setting", - "`suppress_callback_exceptions=True`.", - "This ID was used in the callback(s) for Output(s):", - "outer-input.value" - ]) - - check_error(dash_duo, 1, "ID not found in layout", [ - "Attempting to connect a callback Output item to component:", - '"nope"', - "but no components with that id exist in the layout.", - "This ID was used in the callback(s) for Output(s):", - "inner-div.children, nope.children" - ]) - - check_error(dash_duo, 2, "ID not found in layout", [ - "Attempting to connect a callback State item to component:", - '"what"', - "but no components with that id exist in the layout.", - "This ID was used in the callback(s) for Output(s):", - "inner-div.children, nope.children" - ]) - - check_error(dash_duo, 3, "ID not found in layout", [ - "Attempting to connect a callback Output item to component:", - '"nuh-uh"', - "but no components with that id exist in the layout.", - "This ID was used in the callback(s) for Output(s):", - "nuh-uh.children" - ]) + check_error( + dash_duo, + 0, + "ID not found in layout", + [ + "Attempting to connect a callback Input item to component:", + '"yeah-no"', + "but no components with that id exist in the layout.", + "If you are assigning callbacks to components that are", + "generated by other callbacks (and therefore not in the", + "initial layout), you can suppress this exception by setting", + "`suppress_callback_exceptions=True`.", + "This ID was used in the callback(s) for Output(s):", + "outer-input.value", + ], + ) + + check_error( + dash_duo, + 1, + "ID not found in layout", + [ + "Attempting to connect a callback Output item to component:", + '"nope"', + "but no components with that id exist in the layout.", + "This ID was used in the callback(s) for Output(s):", + "inner-div.children, nope.children", + ], + ) + + check_error( + dash_duo, + 2, + "ID not found in layout", + [ + "Attempting to connect a callback State item to component:", + '"what"', + "but no components with that id exist in the layout.", + "This ID was used in the callback(s) for Output(s):", + "inner-div.children, nope.children", + ], + ) + + check_error( + dash_duo, + 3, + "ID not found in layout", + [ + "Attempting to connect a callback Output item to component:", + '"nuh-uh"', + "but no components with that id exist in the layout.", + "This ID was used in the callback(s) for Output(s):", + "nuh-uh.children", + ], + ) def test_dvcv009_suppress_callback_exceptions(dash_duo): dash_duo.start_server(bad_id_app(suppress_callback_exceptions=True), **debugging) - dash_duo.find_element('.dash-debug-menu') - dash_duo.wait_for_no_elements('.test-devtools-error-count') + dash_duo.find_element(".dash-debug-menu") + dash_duo.wait_for_no_elements(".test-devtools-error-count") def test_dvcv010_bad_props(dash_duo): @@ -445,7 +547,7 @@ def test_dvcv010_bad_props(dash_duo): Output("inner-div", "xyz"), # "data-xyz" is OK, does not give an error [Input("inner-input", "pdq"), Input("inner-div", "data-xyz")], - [State("inner-div", "value")] + [State("inner-div", "value")], ) def xyz(a, b, c): a if b else c @@ -454,7 +556,7 @@ def xyz(a, b, c): Output({"a": MATCH}, "no"), [Input({"a": MATCH}, "never")], # "boo" will not error because we don't check State MATCH/ALLSMALLER - [State({"a": MATCH}, "boo"), State({"a": ALL}, "nope")] + [State({"a": MATCH}, "boo"), State({"a": ALL}, "nope")], ) def f(a, b, c): return a if b else c @@ -463,52 +565,82 @@ def f(a, b, c): dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "6") - check_error(dash_duo, 0, "Invalid prop for this component", [ - 'Property "never" was used with component ID:', - '{"a":1}', - "in one of the Input items of a callback.", - "This ID is assigned to a dash_core_components.Input component", - "in the layout, which does not support this property.", - "This ID was used in the callback(s) for Output(s):", - '{"a":MATCH}.no' - ]) - - check_error(dash_duo, 1, "Invalid prop for this component", [ - 'Property "nope" was used with component ID:', - '{"a":1}', - "in one of the State items of a callback.", - "This ID is assigned to a dash_core_components.Input component", - '{"a":MATCH}.no' - ]) - - check_error(dash_duo, 2, "Invalid prop for this component", [ - 'Property "no" was used with component ID:', - '{"a":1}', - "in one of the Output items of a callback.", - "This ID is assigned to a dash_core_components.Input component", - '{"a":MATCH}.no' - ]) - - check_error(dash_duo, 3, "Invalid prop for this component", [ - 'Property "pdq" was used with component ID:', - '"inner-input"', - "in one of the Input items of a callback.", - "This ID is assigned to a dash_core_components.Input component", - "inner-div.xyz" - ]) - - check_error(dash_duo, 4, "Invalid prop for this component", [ - 'Property "value" was used with component ID:', - '"inner-div"', - "in one of the State items of a callback.", - "This ID is assigned to a dash_html_components.Div component", - "inner-div.xyz" - ]) - - check_error(dash_duo, 5, "Invalid prop for this component", [ - 'Property "xyz" was used with component ID:', - '"inner-div"', - "in one of the Output items of a callback.", - "This ID is assigned to a dash_html_components.Div component", - "inner-div.xyz" - ]) + check_error( + dash_duo, + 0, + "Invalid prop for this component", + [ + 'Property "never" was used with component ID:', + '{"a":1}', + "in one of the Input items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + "in the layout, which does not support this property.", + "This ID was used in the callback(s) for Output(s):", + '{"a":MATCH}.no', + ], + ) + + check_error( + dash_duo, + 1, + "Invalid prop for this component", + [ + 'Property "nope" was used with component ID:', + '{"a":1}', + "in one of the State items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + '{"a":MATCH}.no', + ], + ) + + check_error( + dash_duo, + 2, + "Invalid prop for this component", + [ + 'Property "no" was used with component ID:', + '{"a":1}', + "in one of the Output items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + '{"a":MATCH}.no', + ], + ) + + check_error( + dash_duo, + 3, + "Invalid prop for this component", + [ + 'Property "pdq" was used with component ID:', + '"inner-input"', + "in one of the Input items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + "inner-div.xyz", + ], + ) + + check_error( + dash_duo, + 4, + "Invalid prop for this component", + [ + 'Property "value" was used with component ID:', + '"inner-div"', + "in one of the State items of a callback.", + "This ID is assigned to a dash_html_components.Div component", + "inner-div.xyz", + ], + ) + + check_error( + dash_duo, + 5, + "Invalid prop for this component", + [ + 'Property "xyz" was used with component ID:', + '"inner-div"', + "in one of the Output items of a callback.", + "This ID is assigned to a dash_html_components.Div component", + "inner-div.xyz", + ], + ) diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index a9773c279e..87be679e5c 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -233,7 +233,7 @@ def test_dveh005_multiple_outputs(dash_duo): app.layout = html.Div( [ html.Button( - id="multi-output", children="trigger multi output update", n_clicks=0, + id="multi-output", children="trigger multi output update", n_clicks=0 ), html.Div(id="multi-1"), html.Div(id="multi-2"), diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py index 2b33ed403e..022d687351 100644 --- a/tests/integration/devtools/test_props_check.py +++ b/tests/integration/devtools/test_props_check.py @@ -186,7 +186,7 @@ def display_content(pathname): return "Initial state" test_case = test_cases[pathname.strip("/")] return html.Div( - id="new-component", children=test_case["component"](**test_case["props"]), + id="new-component", children=test_case["component"](**test_case["props"]) ) dash_duo.start_server( diff --git a/tests/integration/renderer/test_persistence.py b/tests/integration/renderer/test_persistence.py index 7d10f4d812..2fc5672be8 100644 --- a/tests/integration/renderer/test_persistence.py +++ b/tests/integration/renderer/test_persistence.py @@ -386,7 +386,9 @@ def set_out(val): dash_duo.find_element("#persistence-val").send_keys("2") dash_duo.wait_for_text_to_equal("#out", "a") - dash_duo.find_element("#persisted").send_keys(Keys.BACK_SPACE) # persist falsy value + dash_duo.find_element("#persisted").send_keys( + Keys.BACK_SPACE + ) # persist falsy value dash_duo.wait_for_text_to_equal("#out", "") # alpaca not saved with falsy persistence @@ -397,7 +399,7 @@ def set_out(val): dash_duo.find_element("#persistence-val").send_keys("s") dash_duo.wait_for_text_to_equal("#out", "anchovies") dash_duo.find_element("#persistence-val").send_keys("2") - dash_duo.wait_for_text_to_equal('#out', "") + dash_duo.wait_for_text_to_equal("#out", "") def test_rdps011_toggle_persistence2(dash_duo): diff --git a/tests/integration/renderer/test_state_and_input.py b/tests/integration/renderer/test_state_and_input.py index 3192ce950f..a57aeb7fcb 100644 --- a/tests/integration/renderer/test_state_and_input.py +++ b/tests/integration/renderer/test_state_and_input.py @@ -30,8 +30,11 @@ def update_output(input, state): dash_duo.start_server(app) - def input_(): return dash_duo.find_element("#input") - def output_(): return dash_duo.find_element("#output") + def input_(): + return dash_duo.find_element("#input") + + def output_(): + return dash_duo.find_element("#output") assert ( output_().text == 'input="Initial Input", state="Initial State"' @@ -81,8 +84,11 @@ def update_output(input, n_clicks, state): dash_duo.start_server(app) - def btn(): return dash_duo.find_element("#button") - def output(): return dash_duo.find_element("#output") + def btn(): + return dash_duo.find_element("#button") + + def output(): + return dash_duo.find_element("#output") assert ( output().text == 'input="Initial Input", state="Initial State"' diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 24ce635538..b196017862 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -234,7 +234,7 @@ def test_inin006_flow_component(dash_duo): ) @app.callback( - Output("output", "children"), [Input("react", "value"), Input("flow", "value")], + Output("output", "children"), [Input("react", "value"), Input("flow", "value")] ) def display_output(react_value, flow_value): return html.Div( @@ -711,7 +711,7 @@ def single(a): pytest.fail("not serializable") @app.callback( - [Output("c", "children"), Output("d", "children")], [Input("a", "children")], + [Output("c", "children"), Output("d", "children")], [Input("a", "children")] ) def multi(a): return [1, set([2])] @@ -719,13 +719,13 @@ def multi(a): with pytest.raises(InvalidCallbackReturnValue): outputs_list = [ {"id": "c", "property": "children"}, - {"id": "d", "property": "children"} + {"id": "d", "property": "children"}, ] multi("aaa", outputs_list=outputs_list) pytest.fail("nested non-serializable") @app.callback( - [Output("e", "children"), Output("f", "children")], [Input("a", "children")], + [Output("e", "children"), Output("f", "children")], [Input("a", "children")] ) def multi2(a): return ["abc"] @@ -733,7 +733,7 @@ def multi2(a): with pytest.raises(InvalidCallbackReturnValue): outputs_list = [ {"id": "e", "property": "children"}, - {"id": "f", "property": "children"} + {"id": "f", "property": "children"}, ] multi2("aaa", outputs_list=outputs_list) pytest.fail("wrong-length list") diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index ec6fc90209..ed3bc7a180 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -863,23 +863,37 @@ def update_output(value): self.wait_for_text_to_equal("#output-pre", "request_pre changed this text!") self.wait_for_text_to_equal("#output-post", "request_post changed this text!") pre_payload = self.wait_for_element_by_css_selector("#output-pre-payload").text - post_payload = self.wait_for_element_by_css_selector("#output-post-payload").text - post_response = self.wait_for_element_by_css_selector("#output-post-response").text - self.assertEqual(json.loads(pre_payload), { - "output": "output-1.children", - "outputs": {"id": "output-1", "property": "children"}, - "changedPropIds": ["input.value"], - "inputs": [{"id": "input", "property": "value", "value": "fire request hooks"}] - }) - self.assertEqual(json.loads(post_payload), { - "output": "output-1.children", - "outputs": {"id": "output-1", "property": "children"}, - "changedPropIds": ["input.value"], - "inputs": [{"id": "input", "property": "value", "value": "fire request hooks"}] - }) - self.assertEqual(json.loads(post_response), { - "output-1": {"children": "fire request hooks"} - }) + post_payload = self.wait_for_element_by_css_selector( + "#output-post-payload" + ).text + post_response = self.wait_for_element_by_css_selector( + "#output-post-response" + ).text + self.assertEqual( + json.loads(pre_payload), + { + "output": "output-1.children", + "outputs": {"id": "output-1", "property": "children"}, + "changedPropIds": ["input.value"], + "inputs": [ + {"id": "input", "property": "value", "value": "fire request hooks"} + ], + }, + ) + self.assertEqual( + json.loads(post_payload), + { + "output": "output-1.children", + "outputs": {"id": "output-1", "property": "children"}, + "changedPropIds": ["input.value"], + "inputs": [ + {"id": "input", "property": "value", "value": "fire request hooks"} + ], + }, + ) + self.assertEqual( + json.loads(post_response), {"output-1": {"children": "fire request hooks"}} + ) self.percy_snapshot(name="request-hooks render") def test_graphs_in_tabs_do_not_share_state(self): diff --git a/tests/unit/test_configs.py b/tests/unit/test_configs.py index 2eff1a5660..7fccbc1766 100644 --- a/tests/unit/test_configs.py +++ b/tests/unit/test_configs.py @@ -12,11 +12,7 @@ get_combined_config, load_dash_env_vars, ) -from dash._utils import ( - get_asset_path, - get_relative_path, - strip_relative_path, -) +from dash._utils import get_asset_path, get_relative_path, strip_relative_path @pytest.fixture From 2b7edecd7509c7619d85ef01dc3c7047bb1f7bfe Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Mar 2020 21:58:03 -0400 Subject: [PATCH 49/81] I guess we were using redux devtools wrong and it only just now complained? --- dash-renderer/src/store.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/dash-renderer/src/store.js b/dash-renderer/src/store.js index 13b4d43a3f..fc9fa7f465 100644 --- a/dash-renderer/src/store.js +++ b/dash-renderer/src/store.js @@ -19,16 +19,18 @@ const initializeStore = reset => { const reducer = createReducer(); - // only attach logger to middleware in non-production mode - store = - process.env.NODE_ENV === 'production' // eslint-disable-line no-process-env - ? createStore(reducer, applyMiddleware(thunk)) - : createStore( - reducer, - window.__REDUX_DEVTOOLS_EXTENSION__ && - window.__REDUX_DEVTOOLS_EXTENSION__(), - applyMiddleware(thunk) - ); + // eslint-disable-next-line no-process-env + if (process.env.NODE_ENV === 'production') { + store = createStore(reducer, applyMiddleware(thunk)); + } else { + // only attach logger to middleware in non-production mode + const reduxDTEC = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; + if (reduxDTEC) { + store = createStore(reducer, reduxDTEC(applyMiddleware(thunk))); + } else { + store = createStore(reducer, applyMiddleware(thunk)); + } + } if (!reset) { // TODO - Protect this under a debug mode? From d85e79dd2e33ce6b2bc69c97d999d69b323feb91 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 26 Mar 2020 22:12:30 -0400 Subject: [PATCH 50/81] tweak dash import in callback validation tests --- .../devtools/test_callback_validation.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index 74e4c424c1..da75d5c3ff 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -1,6 +1,6 @@ import dash_core_components as dcc import dash_html_components as html -import dash +from dash import Dash from dash.dependencies import Input, Output, State, MATCH, ALL, ALLSMALLER debugging = dict( @@ -31,7 +31,7 @@ def check_error(dash_duo, index, message, snippets): def test_dvcv001_blank(dash_duo): - app = dash.Dash(__name__) + app = Dash(__name__) app.layout = html.Div() @app.callback([], []) @@ -55,7 +55,7 @@ def x(): def test_dvcv002_blank_id_prop(dash_duo): # TODO: remove suppress_callback_exceptions after we move that part to FE - app = dash.Dash(__name__, suppress_callback_exceptions=True) + app = Dash(__name__, suppress_callback_exceptions=True) app.layout = html.Div([html.Div(id="a")]) @app.callback([Output("a", "children"), Output("", "")], [Input("", "")]) @@ -97,7 +97,7 @@ def x(a): def test_dvcv003_duplicate_outputs_same_callback(dash_duo): - app = dash.Dash(__name__) + app = Dash(__name__) app.layout = html.Div([html.Div(id="a"), html.Div(id="b")]) @app.callback( @@ -136,7 +136,7 @@ def y(b): def test_dvcv004_duplicate_outputs_across_callbacks(dash_duo): - app = dash.Dash(__name__) + app = Dash(__name__) app.layout = html.Div([html.Div(id="a"), html.Div(id="b"), html.Div(id="c")]) @app.callback( @@ -232,7 +232,7 @@ def z3(ab): def test_dvcv005_input_output_overlap(dash_duo): - app = dash.Dash(__name__) + app = Dash(__name__) app.layout = html.Div([html.Div(id="a"), html.Div(id="b"), html.Div(id="c")]) @app.callback(Output("a", "children"), [Input("a", "children")]) @@ -301,7 +301,7 @@ def y2(c): def test_dvcv006_inconsistent_wildcards(dash_duo): - app = dash.Dash(__name__) + app = Dash(__name__) app.layout = html.Div() @app.callback( @@ -369,7 +369,7 @@ def y(b, c, d, e): def test_dvcv007_disallowed_ids(dash_duo): - app = dash.Dash(__name__) + app = Dash(__name__) app.layout = html.Div() @app.callback( @@ -427,7 +427,7 @@ def y(b): def bad_id_app(**kwargs): - app = dash.Dash(__name__, **kwargs) + app = Dash(__name__, **kwargs) app.layout = html.Div( [ html.Div( @@ -532,7 +532,7 @@ def test_dvcv009_suppress_callback_exceptions(dash_duo): def test_dvcv010_bad_props(dash_duo): - app = dash.Dash(__name__) + app = Dash(__name__) app.layout = html.Div( [ html.Div( From 05965407bd4b598b3dd892d4729579ff0de8c930 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 27 Mar 2020 08:33:16 -0400 Subject: [PATCH 51/81] halt circularity and cb/layout validation after we find earlier errors --- dash-renderer/src/actions/dependencies.js | 31 +++++++++++++------ .../devtools/test_callback_validation.py | 28 +++++++---------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 7df7156a68..4825a422eb 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -603,7 +603,12 @@ export function computeGraphs(dependencies, dispatchError) { return out; }, dependencies); - validateDependencies(parsedDependencies, dispatchError); + let hasError = false; + const wrappedDE = (message, lines) => { + hasError = true; + dispatchError(message, lines); + } + validateDependencies(parsedDependencies, wrappedDE); /* * For regular ids, outputMap and inputMap are: @@ -632,6 +637,21 @@ export function computeGraphs(dependencies, dispatchError) { const outputPatterns = {}; const inputPatterns = {}; + const finalGraphs = { + InputGraph: inputGraph, + MultiGraph: multiGraph, + outputMap, + inputMap, + outputPatterns, + inputPatterns, + }; + + if (hasError) { + // leave the graphs empty if we found an error, so we don't try to + // execute the broken callbacks. + return finalGraphs; + } + parsedDependencies.forEach(dependency => { const {outputs, inputs} = dependency; @@ -778,14 +798,7 @@ export function computeGraphs(dependencies, dispatchError) { }); }); - return { - InputGraph: inputGraph, - MultiGraph: multiGraph, - outputMap, - inputMap, - outputPatterns, - inputPatterns, - }; + return finalGraphs; } function findWildcardKeys(id) { diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index da75d5c3ff..88246a93c2 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -64,33 +64,32 @@ def x(a): dash_duo.start_server(app, **debugging) - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "6") + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "5") - # the first 2 are just artifacts... the other 4 we care about - check_error(dash_duo, 0, "Circular Dependencies", []) - check_error(dash_duo, 1, "Same `Input` and `Output`", []) + # the first one is just an artifact... the other 4 we care about + check_error(dash_duo, 0, "Same `Input` and `Output`", []) check_error( dash_duo, - 2, + 1, "Callback item missing ID", ['Input[0].id = ""', "Every item linked to a callback needs an ID"], ) check_error( dash_duo, - 3, + 2, "Callback property error", ['Input[0].property = ""', "expected `property` to be a non-empty string."], ) check_error( dash_duo, - 4, + 3, "Callback item missing ID", ['Output[1].id = ""', "Every item linked to a callback needs an ID"], ) check_error( dash_duo, - 5, + 4, "Callback property error", ['Output[1].property = ""', "expected `property` to be a non-empty string."], ) @@ -258,14 +257,11 @@ def y2(c): dash_duo.start_server(app, **debugging) - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "6") - - check_error(dash_duo, 0, "Dependency Cycle Found: a.children -> a.children", []) - check_error(dash_duo, 1, "Circular Dependencies", []) + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") check_error( dash_duo, - 2, + 0, "Same `Input` and `Output`", [ 'Input 0 ({"b":MATCH,"c":1}.children)', @@ -276,7 +272,7 @@ def y2(c): check_error( dash_duo, - 3, + 1, "Same `Input` and `Output`", [ 'Input 0 ({"a":1}.children)', @@ -287,14 +283,14 @@ def y2(c): check_error( dash_duo, - 4, + 2, "Same `Input` and `Output`", ["Input 0 (c.children)", "matches Output 1 (c.children)"], ) check_error( dash_duo, - 5, + 3, "Same `Input` and `Output`", ["Input 0 (a.children)", "matches Output 0 (a.children)"], ) From c28dae6f05a9f49400fd1c372905d5b7b549b8d5 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 27 Mar 2020 09:56:06 -0400 Subject: [PATCH 52/81] flow components have no propTypes --- dash-renderer/src/actions/dependencies.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 4825a422eb..7243fa2895 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -479,7 +479,9 @@ export function validateCallbacksToLayout(state_, dispatchError) { function validateProp(id, idPath, prop, cls, callbacks) { const component = path(idPath, layout); const element = Registry.resolve(component); - if (element && !element.propTypes[prop]) { + + // note: Flow components do not have propTypes, so we can't validate. + if (element && element.propTypes && !element.propTypes[prop]) { // look for wildcard props (ie data-* etc) for (const propName in element.propTypes) { const last = propName.length - 1; @@ -607,7 +609,7 @@ export function computeGraphs(dependencies, dispatchError) { const wrappedDE = (message, lines) => { hasError = true; dispatchError(message, lines); - } + }; validateDependencies(parsedDependencies, wrappedDE); /* From 5a11de5189edcc09c43d5ec27ac5d42430532e1f Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 27 Mar 2020 09:57:07 -0400 Subject: [PATCH 53/81] refactor callback validation tests order-agnostic py2 may alter the order... --- .../devtools/test_callback_validation.py | 616 ++++++++---------- 1 file changed, 276 insertions(+), 340 deletions(-) diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index 88246a93c2..a026af3c6f 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -8,26 +8,44 @@ ) -def check_error(dash_duo, index, message, snippets): +def check_errors(dash_duo, specs): + # Order-agnostic check of all the errors shown. # This is not fully general - despite the selectors below, it only applies # to front-end errors with no back-end errors in the list. - # Also the index is as on the page, which is opposite the execution order. - - found_message = dash_duo.find_elements(".dash-fe-error__title")[index].text - assert found_message == message - - if not snippets: - return - - dash_duo.find_elements(".test-devtools-error-toggle")[index].click() - - found_text = dash_duo.wait_for_element(".dash-backend-error").text - for snip in snippets: - assert snip in found_text - - # hide the error detail again - so only one detail is be visible at a time - dash_duo.find_elements(".test-devtools-error-toggle")[index].click() - dash_duo.wait_for_no_elements(".dash-backend-error") + cnt = len(specs) + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, str(cnt)) + + found = [] + for i in range(cnt): + msg = dash_duo.find_elements(".dash-fe-error__title")[i].text + dash_duo.find_elements(".test-devtools-error-toggle")[i].click() + txt = dash_duo.wait_for_element(".dash-backend-error").text + dash_duo.find_elements(".test-devtools-error-toggle")[i].click() + dash_duo.wait_for_no_elements(".dash-backend-error") + found.append((msg, txt)) + + orig_found = found[:] + + for i, (message, snippets) in enumerate(specs): + for j, (msg, txt) in enumerate(found): + if msg == message and all(snip in txt for snip in snippets): + print(j) + found.pop(j) + break + else: + raise AssertionError( + ( + "error {} ({}) not found with text:\n" + " {}\nThe found messages were:\n---\n{}" + ).format( + i, + message, + "\n ".join(snippets), + "\n---\n".join( + "{}\n{}".format(msg, txt) for msg, txt in orig_found + ), + ) + ) def test_dvcv001_blank(dash_duo): @@ -39,17 +57,15 @@ def x(): return 42 dash_duo.start_server(app, **debugging) - - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2") - - check_error( - dash_duo, 0, "A callback is missing Inputs", ["there are no `Input` elements."] - ) - check_error( + check_errors( dash_duo, - 1, - "A callback is missing Outputs", - ["Please provide an output for this callback:"], + [ + ["A callback is missing Inputs", ["there are no `Input` elements."]], + [ + "A callback is missing Outputs", + ["Please provide an output for this callback:"], + ], + ], ) @@ -64,35 +80,33 @@ def x(a): dash_duo.start_server(app, **debugging) - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "5") - # the first one is just an artifact... the other 4 we care about - check_error(dash_duo, 0, "Same `Input` and `Output`", []) - - check_error( - dash_duo, - 1, - "Callback item missing ID", - ['Input[0].id = ""', "Every item linked to a callback needs an ID"], - ) - check_error( - dash_duo, - 2, - "Callback property error", - ['Input[0].property = ""', "expected `property` to be a non-empty string."], - ) - check_error( - dash_duo, - 3, - "Callback item missing ID", - ['Output[1].id = ""', "Every item linked to a callback needs an ID"], - ) - check_error( - dash_duo, - 4, - "Callback property error", - ['Output[1].property = ""', "expected `property` to be a non-empty string."], - ) + specs = [ + ["Same `Input` and `Output`", []], + [ + "Callback item missing ID", + ['Input[0].id = ""', "Every item linked to a callback needs an ID"], + ], + [ + "Callback property error", + [ + 'Input[0].property = ""', + "expected `property` to be a non-empty string.", + ], + ], + [ + "Callback item missing ID", + ['Output[1].id = ""', "Every item linked to a callback needs an ID"], + ], + [ + "Callback property error", + [ + 'Output[1].property = ""', + "expected `property` to be a non-empty string.", + ], + ], + ] + check_errors(dash_duo, specs) def test_dvcv003_duplicate_outputs_same_callback(dash_duo): @@ -114,24 +128,21 @@ def y(b): dash_duo.start_server(app, **debugging) - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2") - - check_error( - dash_duo, - 0, - "Overlapping wildcard callback outputs", + specs = [ [ - 'Output 1 ({"a":ALL}.children)', - 'overlaps another output ({"a":1}.children)', - "used in this callback", + "Overlapping wildcard callback outputs", + [ + 'Output 1 ({"a":ALL}.children)', + 'overlaps another output ({"a":1}.children)', + "used in this callback", + ], ], - ) - check_error( - dash_duo, - 1, - "Duplicate callback Outputs", - ["Output 1 (a.children) is already used by this callback."], - ) + [ + "Duplicate callback Outputs", + ["Output 1 (a.children) is already used by this callback."], + ], + ] + check_errors(dash_duo, specs) def test_dvcv004_duplicate_outputs_across_callbacks(dash_duo): @@ -180,54 +191,35 @@ def z3(ab): dash_duo.start_server(app, **debugging) - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "5") - - check_error( - dash_duo, - 0, - "Overlapping wildcard callback outputs", + specs = [ [ - 'Output 0 ({"a":MATCH}.children)', - 'overlaps another output ({"a":1}.children)', - "used in a different callback.", + "Overlapping wildcard callback outputs", + [ + 'Output 0 ({"a":MATCH}.children)', + 'overlaps another output ({"a":1}.children)', + "used in a different callback.", + ], ], - ) - - check_error( - dash_duo, - 1, - "Overlapping wildcard callback outputs", [ - 'Output 1 ({"b":1,"c":ALL}.children)', - 'overlaps another output ({"b":ALL,"c":1}.children)', - "used in a different callback.", + "Overlapping wildcard callback outputs", + [ + 'Output 1 ({"b":1,"c":ALL}.children)', + 'overlaps another output ({"b":ALL,"c":1}.children)', + "used in a different callback.", + ], ], - ) - - check_error( - dash_duo, - 2, - "Overlapping wildcard callback outputs", [ - 'Output 0 ({"a":ALL}.children)', - 'overlaps another output ({"a":1}.children)', - "used in a different callback.", + "Overlapping wildcard callback outputs", + [ + 'Output 0 ({"a":ALL}.children)', + 'overlaps another output ({"a":1}.children)', + "used in a different callback.", + ], ], - ) - - check_error( - dash_duo, - 3, - "Duplicate callback outputs", - ["Output 0 (b.children) is already in use."], - ) - - check_error( - dash_duo, - 4, - "Duplicate callback outputs", - ["Output 0 (a.children) is already in use."], - ) + ["Duplicate callback outputs", ["Output 0 (b.children) is already in use."]], + ["Duplicate callback outputs", ["Output 0 (a.children) is already in use."]], + ] + check_errors(dash_duo, specs) def test_dvcv005_input_output_overlap(dash_duo): @@ -257,43 +249,33 @@ def y2(c): dash_duo.start_server(app, **debugging) - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") - - check_error( - dash_duo, - 0, - "Same `Input` and `Output`", + specs = [ [ - 'Input 0 ({"b":MATCH,"c":1}.children)', - "can match the same component(s) as", - 'Output 1 ({"b":MATCH,"c":1}.children)', + "Same `Input` and `Output`", + [ + 'Input 0 ({"b":MATCH,"c":1}.children)', + "can match the same component(s) as", + 'Output 1 ({"b":MATCH,"c":1}.children)', + ], ], - ) - - check_error( - dash_duo, - 1, - "Same `Input` and `Output`", [ - 'Input 0 ({"a":1}.children)', - "can match the same component(s) as", - 'Output 0 ({"a":ALL}.children)', + "Same `Input` and `Output`", + [ + 'Input 0 ({"a":1}.children)', + "can match the same component(s) as", + 'Output 0 ({"a":ALL}.children)', + ], ], - ) - - check_error( - dash_duo, - 2, - "Same `Input` and `Output`", - ["Input 0 (c.children)", "matches Output 1 (c.children)"], - ) - - check_error( - dash_duo, - 3, - "Same `Input` and `Output`", - ["Input 0 (a.children)", "matches Output 0 (a.children)"], - ) + [ + "Same `Input` and `Output`", + ["Input 0 (c.children)", "matches Output 1 (c.children)"], + ], + [ + "Same `Input` and `Output`", + ["Input 0 (a.children)", "matches Output 0 (a.children)"], + ], + ] + check_errors(dash_duo, specs) def test_dvcv006_inconsistent_wildcards(dash_duo): @@ -317,51 +299,41 @@ def y(b, c, d, e): dash_duo.start_server(app, **debugging) - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") - - check_error( - dash_duo, - 0, - "`Input` / `State` wildcards not in `Output`s", + specs = [ [ - 'State 0 ({"d":MATCH,"dd":MATCH}.children)', - "has MATCH or ALLSMALLER on key(s) d, dd", - 'where Output 0 ({"a":MATCH}.children)', + "`Input` / `State` wildcards not in `Output`s", + [ + 'State 0 ({"d":MATCH,"dd":MATCH}.children)', + "has MATCH or ALLSMALLER on key(s) d, dd", + 'where Output 0 ({"a":MATCH}.children)', + ], ], - ) - - check_error( - dash_duo, - 1, - "`Input` / `State` wildcards not in `Output`s", [ - 'Input 1 ({"c":ALLSMALLER}.children)', - "has MATCH or ALLSMALLER on key(s) c", - 'where Output 0 ({"a":MATCH}.children)', + "`Input` / `State` wildcards not in `Output`s", + [ + 'Input 1 ({"c":ALLSMALLER}.children)', + "has MATCH or ALLSMALLER on key(s) c", + 'where Output 0 ({"a":MATCH}.children)', + ], ], - ) - - check_error( - dash_duo, - 2, - "`Input` / `State` wildcards not in `Output`s", [ - 'Input 0 ({"b":MATCH}.children)', - "has MATCH or ALLSMALLER on key(s) b", - 'where Output 0 ({"a":MATCH}.children)', + "`Input` / `State` wildcards not in `Output`s", + [ + 'Input 0 ({"b":MATCH}.children)', + "has MATCH or ALLSMALLER on key(s) b", + 'where Output 0 ({"a":MATCH}.children)', + ], ], - ) - - check_error( - dash_duo, - 3, - "Mismatched `MATCH` wildcards across `Output`s", [ - 'Output 1 ({"b":ALL,"c":1}.children)', - "does not have MATCH wildcards on the same keys as", - 'Output 0 ({"b":MATCH}.children).', + "Mismatched `MATCH` wildcards across `Output`s", + [ + 'Output 1 ({"b":ALL,"c":1}.children)', + "does not have MATCH wildcards on the same keys as", + 'Output 0 ({"b":MATCH}.children).', + ], ], - ) + ] + check_errors(dash_duo, specs) def test_dvcv007_disallowed_ids(dash_duo): @@ -377,49 +349,39 @@ def y(b): dash_duo.start_server(app, **debugging) - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") - - check_error( - dash_duo, - 0, - "Callback wildcard ID error", + specs = [ [ - 'Input[0].id["b"] = {"c":1}', - "Wildcard callback ID values must be either wildcards", - "or constants of one of these types:", - "string, number, boolean", + "Callback wildcard ID error", + [ + 'Input[0].id["b"] = {"c":1}', + "Wildcard callback ID values must be either wildcards", + "or constants of one of these types:", + "string, number, boolean", + ], ], - ) - - check_error( - dash_duo, - 1, - "Callback wildcard ID error", [ - 'Output[0].id["c"] = ALLSMALLER', - "Allowed wildcards for Outputs are:", - "ALL, MATCH", + "Callback wildcard ID error", + [ + 'Output[0].id["c"] = ALLSMALLER', + "Allowed wildcards for Outputs are:", + "ALL, MATCH", + ], ], - ) - - check_error( - dash_duo, - 2, - "Callback wildcard ID error", [ - 'Output[0].id["a"] = [4]', - "Wildcard callback ID values must be either wildcards", - "or constants of one of these types:", - "string, number, boolean", + "Callback wildcard ID error", + [ + 'Output[0].id["a"] = [4]', + "Wildcard callback ID values must be either wildcards", + "or constants of one of these types:", + "string, number, boolean", + ], ], - ) - - check_error( - dash_duo, - 3, - "Callback wildcard ID error", - ['Output[0].id has key ""', "Keys must be non-empty strings."], - ) + [ + "Callback wildcard ID error", + ['Output[0].id has key ""', "Keys must be non-empty strings."], + ], + ] + check_errors(dash_duo, specs) def bad_id_app(**kwargs): @@ -461,63 +423,53 @@ def h(a): def test_dvcv008_wrong_callback_id(dash_duo): dash_duo.start_server(bad_id_app(), **debugging) - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "4") - - check_error( - dash_duo, - 0, - "ID not found in layout", - [ - "Attempting to connect a callback Input item to component:", - '"yeah-no"', - "but no components with that id exist in the layout.", - "If you are assigning callbacks to components that are", - "generated by other callbacks (and therefore not in the", - "initial layout), you can suppress this exception by setting", - "`suppress_callback_exceptions=True`.", - "This ID was used in the callback(s) for Output(s):", - "outer-input.value", + specs = [ + [ + "ID not found in layout", + [ + "Attempting to connect a callback Input item to component:", + '"yeah-no"', + "but no components with that id exist in the layout.", + "If you are assigning callbacks to components that are", + "generated by other callbacks (and therefore not in the", + "initial layout), you can suppress this exception by setting", + "`suppress_callback_exceptions=True`.", + "This ID was used in the callback(s) for Output(s):", + "outer-input.value", + ], ], - ) - - check_error( - dash_duo, - 1, - "ID not found in layout", [ - "Attempting to connect a callback Output item to component:", - '"nope"', - "but no components with that id exist in the layout.", - "This ID was used in the callback(s) for Output(s):", - "inner-div.children, nope.children", + "ID not found in layout", + [ + "Attempting to connect a callback Output item to component:", + '"nope"', + "but no components with that id exist in the layout.", + "This ID was used in the callback(s) for Output(s):", + "inner-div.children, nope.children", + ], ], - ) - - check_error( - dash_duo, - 2, - "ID not found in layout", [ - "Attempting to connect a callback State item to component:", - '"what"', - "but no components with that id exist in the layout.", - "This ID was used in the callback(s) for Output(s):", - "inner-div.children, nope.children", + "ID not found in layout", + [ + "Attempting to connect a callback State item to component:", + '"what"', + "but no components with that id exist in the layout.", + "This ID was used in the callback(s) for Output(s):", + "inner-div.children, nope.children", + ], ], - ) - - check_error( - dash_duo, - 3, - "ID not found in layout", [ - "Attempting to connect a callback Output item to component:", - '"nuh-uh"', - "but no components with that id exist in the layout.", - "This ID was used in the callback(s) for Output(s):", - "nuh-uh.children", + "ID not found in layout", + [ + "Attempting to connect a callback Output item to component:", + '"nuh-uh"', + "but no components with that id exist in the layout.", + "This ID was used in the callback(s) for Output(s):", + "nuh-uh.children", + ], ], - ) + ] + check_errors(dash_duo, specs) def test_dvcv009_suppress_callback_exceptions(dash_duo): @@ -559,84 +511,68 @@ def f(a, b, c): dash_duo.start_server(app, **debugging) - dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "6") - - check_error( - dash_duo, - 0, - "Invalid prop for this component", + specs = [ [ - 'Property "never" was used with component ID:', - '{"a":1}', - "in one of the Input items of a callback.", - "This ID is assigned to a dash_core_components.Input component", - "in the layout, which does not support this property.", - "This ID was used in the callback(s) for Output(s):", - '{"a":MATCH}.no', + "Invalid prop for this component", + [ + 'Property "never" was used with component ID:', + '{"a":1}', + "in one of the Input items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + "in the layout, which does not support this property.", + "This ID was used in the callback(s) for Output(s):", + '{"a":MATCH}.no', + ], ], - ) - - check_error( - dash_duo, - 1, - "Invalid prop for this component", [ - 'Property "nope" was used with component ID:', - '{"a":1}', - "in one of the State items of a callback.", - "This ID is assigned to a dash_core_components.Input component", - '{"a":MATCH}.no', + "Invalid prop for this component", + [ + 'Property "nope" was used with component ID:', + '{"a":1}', + "in one of the State items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + '{"a":MATCH}.no', + ], ], - ) - - check_error( - dash_duo, - 2, - "Invalid prop for this component", [ - 'Property "no" was used with component ID:', - '{"a":1}', - "in one of the Output items of a callback.", - "This ID is assigned to a dash_core_components.Input component", - '{"a":MATCH}.no', + "Invalid prop for this component", + [ + 'Property "no" was used with component ID:', + '{"a":1}', + "in one of the Output items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + '{"a":MATCH}.no', + ], ], - ) - - check_error( - dash_duo, - 3, - "Invalid prop for this component", [ - 'Property "pdq" was used with component ID:', - '"inner-input"', - "in one of the Input items of a callback.", - "This ID is assigned to a dash_core_components.Input component", - "inner-div.xyz", + "Invalid prop for this component", + [ + 'Property "pdq" was used with component ID:', + '"inner-input"', + "in one of the Input items of a callback.", + "This ID is assigned to a dash_core_components.Input component", + "inner-div.xyz", + ], ], - ) - - check_error( - dash_duo, - 4, - "Invalid prop for this component", [ - 'Property "value" was used with component ID:', - '"inner-div"', - "in one of the State items of a callback.", - "This ID is assigned to a dash_html_components.Div component", - "inner-div.xyz", + "Invalid prop for this component", + [ + 'Property "value" was used with component ID:', + '"inner-div"', + "in one of the State items of a callback.", + "This ID is assigned to a dash_html_components.Div component", + "inner-div.xyz", + ], ], - ) - - check_error( - dash_duo, - 5, - "Invalid prop for this component", [ - 'Property "xyz" was used with component ID:', - '"inner-div"', - "in one of the Output items of a callback.", - "This ID is assigned to a dash_html_components.Div component", - "inner-div.xyz", + "Invalid prop for this component", + [ + 'Property "xyz" was used with component ID:', + '"inner-div"', + "in one of the Output items of a callback.", + "This ID is assigned to a dash_html_components.Div component", + "inner-div.xyz", + ], ], - ) + ] + check_errors(dash_duo, specs) From d3d4ec6359a7aaa25ed41c1b345f986263797065 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 27 Mar 2020 12:03:49 -0400 Subject: [PATCH 54/81] one more kind of order independence in callback validation tests --- .../devtools/test_callback_validation.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index a026af3c6f..67285474a4 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -195,24 +195,33 @@ def z3(ab): [ "Overlapping wildcard callback outputs", [ - 'Output 0 ({"a":MATCH}.children)', - 'overlaps another output ({"a":1}.children)', + # depending on the order callbacks get reported to the + # front end, either of these could have been registered first. + # originally this said + # 'Output 0 ({"a":MATCH}.children)' + # 'overlaps another output ({"a":1}.children)' + # but this form is order-independent + '({"a":MATCH}.children)', + "overlaps another output", + '({"a":1}.children)', "used in a different callback.", ], ], [ "Overlapping wildcard callback outputs", [ - 'Output 1 ({"b":1,"c":ALL}.children)', - 'overlaps another output ({"b":ALL,"c":1}.children)', + '({"b":1,"c":ALL}.children)', + "overlaps another output", + '({"b":ALL,"c":1}.children)', "used in a different callback.", ], ], [ "Overlapping wildcard callback outputs", [ - 'Output 0 ({"a":ALL}.children)', - 'overlaps another output ({"a":1}.children)', + '({"a":ALL}.children)', + "overlaps another output", + '({"a":1}.children)', "used in a different callback.", ], ], From 620f48fec3f9bb29f08920edc2128b32b408eb53 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 27 Mar 2020 12:36:28 -0400 Subject: [PATCH 55/81] ugh more order-dependence fixing --- .../devtools/test_callback_validation.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index 67285474a4..d964eff0f6 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -184,7 +184,8 @@ def z2(b): return b, b @app.callback( - Output({"a": MATCH}, "children"), [Input({"a": MATCH, "b": 1}, "children")] + Output({"b": 2, "c": MATCH}, "children"), + [Input({"b": 3, "c": MATCH}, "children")], ) def z3(ab): return ab @@ -197,13 +198,11 @@ def z3(ab): [ # depending on the order callbacks get reported to the # front end, either of these could have been registered first. - # originally this said - # 'Output 0 ({"a":MATCH}.children)' - # 'overlaps another output ({"a":1}.children)' - # but this form is order-independent - '({"a":MATCH}.children)', + # so we use this oder-independent form that just checks for + # both prop_id's and the string "overlaps another output" + '({"b":2,"c":MATCH}.children)', "overlaps another output", - '({"a":1}.children)', + '({"b":ALL,"c":1}.children)', "used in a different callback.", ], ], From 8f80410635c5bf96e65f5cda890eaf5ac3b37254 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 27 Mar 2020 12:59:38 -0400 Subject: [PATCH 56/81] that extra callback out of order really didn't need to be there --- .../devtools/test_callback_validation.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index d964eff0f6..2bd96d18bb 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -183,13 +183,6 @@ def z(b): def z2(b): return b, b - @app.callback( - Output({"b": 2, "c": MATCH}, "children"), - [Input({"b": 3, "c": MATCH}, "children")], - ) - def z3(ab): - return ab - dash_duo.start_server(app, **debugging) specs = [ @@ -200,15 +193,6 @@ def z3(ab): # front end, either of these could have been registered first. # so we use this oder-independent form that just checks for # both prop_id's and the string "overlaps another output" - '({"b":2,"c":MATCH}.children)', - "overlaps another output", - '({"b":ALL,"c":1}.children)', - "used in a different callback.", - ], - ], - [ - "Overlapping wildcard callback outputs", - [ '({"b":1,"c":ALL}.children)', "overlaps another output", '({"b":ALL,"c":1}.children)', From d6b4a21b29fa180df2fb9dc258a9eefa661d1479 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 27 Mar 2020 16:22:06 -0400 Subject: [PATCH 57/81] callback graph update + wildcards turns out this was broken since updating viz.js to v2.x --- dash-renderer/src/actions/dependencies.js | 1 + .../CallbackGraphContainer.react.js | 91 +++++++++++-------- .../error/GlobalErrorContainer.react.js | 13 +-- .../components/error/menu/DebugMenu.react.js | 8 +- 4 files changed, 64 insertions(+), 49 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 7243fa2895..111ffb6293 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -646,6 +646,7 @@ export function computeGraphs(dependencies, dispatchError) { inputMap, outputPatterns, inputPatterns, + callbacks: parsedDependencies, }; if (hasError) { diff --git a/dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainer.react.js b/dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainer.react.js index f2a0dacd1a..ae53f4ca54 100644 --- a/dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainer.react.js +++ b/dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainer.react.js @@ -1,34 +1,43 @@ -import React, {Component} from 'react'; +import React, {useEffect, useRef} from 'react'; +import PropTypes from 'prop-types'; + import './CallbackGraphContainer.css'; -import viz from 'viz.js'; +import Viz from 'viz.js'; +import {Module, render} from 'viz.js/full.render'; -import PropTypes from 'prop-types'; +import {stringifyId} from '../../../actions/dependencies'; + +const CallbackGraphContainer = ({graphs}) => { + const el = useRef(null); + + const viz = useRef(null); + + const makeViz = () => { + viz.current = new Viz({Module, render}); + }; -class CallbackGraphContainer extends Component { - constructor(props) { - super(props); + if (!viz.current) { + makeViz(); } - render() { - const {dependenciesRequest} = this.props; + + useEffect(() => { + const {callbacks} = graphs; const elements = {}; - const callbacks = []; - const links = dependenciesRequest.content.map(({inputs, output}, i) => { - callbacks.push(`cb${i};`); - function recordAndReturn([id, property]) { - elements[id] = elements[id] || {}; - elements[id][property] = true; - return `"${id}.${property}"`; + const callbacksOut = []; + const links = callbacks.map(({inputs, outputs}, i) => { + callbacksOut.push(`cb${i};`); + function recordAndReturn({id, property}) { + const idClean = stringifyId(id) + .replace(/[\{\}".;\[\]()]/g, '') + .replace(/:/g, '-') + .replace(/,/g, '_'); + elements[idClean] = elements[idClean] || {}; + elements[idClean][property] = true; + return `"${idClean}.${property}"`; } - const out_nodes = output - .replace(/^\.\./, '') - .replace(/\.\.$/, '') - .split('...') - .map(o => recordAndReturn(o.split('.'))) - .join(', '); - const in_nodes = inputs - .map(({id, property}) => recordAndReturn([id, property])) - .join(', '); + const out_nodes = outputs.map(recordAndReturn).join(', '); + const in_nodes = inputs.map(recordAndReturn).join(', '); return `{${in_nodes}} -> cb${i} -> {${out_nodes}};`; }); @@ -39,7 +48,7 @@ class CallbackGraphContainer extends Component { graph [penwidth=0]; subgraph callbacks { node [shape=circle, width=0.3, label="", color="#00CC96"]; - ${callbacks.join('\n')} } + ${callbacksOut.join('\n')} } ${Object.entries(elements) .map( @@ -55,19 +64,29 @@ class CallbackGraphContainer extends Component { ${links.join('\n')} }`; - return ( -
- ); - } -} + // eslint-disable-next-line no-console + console.log(dot); + + viz.current + .renderSVGElement(dot) + .then(vizEl => { + el.current.innerHTML = ''; + el.current.appendChild(vizEl); + }) + .catch(e => { + // https://github.com/mdaines/viz.js/wiki/Caveats + makeViz(); + // eslint-disable-next-line no-console + console.error(e); + el.current.innerHTML = 'Error creating callback graph'; + }); + }); + + return
; +}; CallbackGraphContainer.propTypes = { - dependenciesRequest: PropTypes.object, + graphs: PropTypes.object, }; export {CallbackGraphContainer}; diff --git a/dash-renderer/src/components/error/GlobalErrorContainer.react.js b/dash-renderer/src/components/error/GlobalErrorContainer.react.js index 28f344c5c0..6f61b07fcf 100644 --- a/dash-renderer/src/components/error/GlobalErrorContainer.react.js +++ b/dash-renderer/src/components/error/GlobalErrorContainer.react.js @@ -10,14 +10,11 @@ class UnconnectedGlobalErrorContainer extends Component { } render() { - const {error, dependenciesRequest} = this.props; + const {error, graphs, children} = this.props; return (
- -
{this.props.children}
+ +
{children}
); @@ -27,12 +24,12 @@ class UnconnectedGlobalErrorContainer extends Component { UnconnectedGlobalErrorContainer.propTypes = { children: PropTypes.object, error: PropTypes.object, - dependenciesRequest: PropTypes.object, + graphs: PropTypes.object, }; const GlobalErrorContainer = connect(state => ({ error: state.error, - dependenciesRequest: state.dependenciesRequest, + graphs: state.graphs, }))(Radium(UnconnectedGlobalErrorContainer)); export default GlobalErrorContainer; diff --git a/dash-renderer/src/components/error/menu/DebugMenu.react.js b/dash-renderer/src/components/error/menu/DebugMenu.react.js index cce9e9acff..e25c8cc2bc 100644 --- a/dash-renderer/src/components/error/menu/DebugMenu.react.js +++ b/dash-renderer/src/components/error/menu/DebugMenu.react.js @@ -31,7 +31,7 @@ class DebugMenu extends Component { toastsEnabled, callbackGraphOpened, } = this.state; - const {error, dependenciesRequest} = this.props; + const {error, graphs} = this.props; const menuClasses = opened ? 'dash-debug-menu dash-debug-menu--opened' @@ -40,9 +40,7 @@ class DebugMenu extends Component { const menuContent = opened ? (
{callbackGraphOpened ? ( - + ) : null} {error.frontEnd.length > 0 || error.backEnd.length > 0 ? (
@@ -152,7 +150,7 @@ class DebugMenu extends Component { DebugMenu.propTypes = { children: PropTypes.object, error: PropTypes.object, - dependenciesRequest: PropTypes.object, + graphs: PropTypes.object, }; export {DebugMenu}; From f875766b29e25cfacf8bfd8e307be9aedc21b169 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 27 Mar 2020 18:20:41 -0400 Subject: [PATCH 58/81] changelog for wildcards --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13c7a1f19e..edc2b4c85d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,21 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- [#1103](https://github.com/plotly/dash/pull/1103) Wildcard IDs and callbacks. Component IDs can be dictionaries, and callbacks can reference patterns of components, using three different wildcards: `All`, `MATCH`, and `ALLSMALLER`, available from `dash.dependencies`. This lets you create components on demand, and have callbacks respond to any and all of them. To help with this, `dash.callback_context` gets three new entries: `outputs_list`, `inputs_list`, and `states_list`, which contain all the ids, properties, and except for the outputs, the property values from all matched components. + - [#1134](https://github.com/plotly/dash/pull/1134) Allow `dash.run_server()` host and port parameters to be set with environment variables HOST & PORT, respectively ### Changed +- [#1103](https://github.com/plotly/dash/pull/1103) Multiple changes to the callback pipeline: + - `dash.callback_context.triggered` now does NOT reflect any initial values, and DOES reflect EVERY value which has been changed either by activity in the app or as a result of a previous callback. That means that the initial call of a callback with no prerequisite callbacks will list nothing as triggering. For backward compatibility, we continue to provide a length-1 list for `triggered`, but its `id` and `property` are blank strings, and `bool(triggered)` is `False`. + - A callback which returns the same property value as was previously present will not trigger the component to re-render, nor trigger other callbacks using that property as an input. + - Callback validation is now mostly done in the browser, rather than in Python. A few things - mostly type validation, like ensuring IDs are strings or dicts and properties are strings - are still done in Python, but most others, like ensuring outputs are unique, inputs and outputs don't overlap, and (if desired) that IDs are present in the layout, are done in the browser. This means you can define callbacks BEFORE the layout and still validate IDs to the layout; and while developing an app, most errors in callback definitions will not halt the app. + - [#1145](https://github.com/plotly/dash/pull/1145) Update from React 16.8.6 to 16.13.0 ### Fixed +- [#1103](https://github.com/plotly/dash/pull/1103) Fixed multiple bugs with chained callbacks either not triggering, inconsistently triggering, or triggering multiple times. This includes: [#635](https://github.com/plotly/dash/issues/635), [#832](https://github.com/plotly/dash/issues/832), [#1053](https://github.com/plotly/dash/issues/1053), [#1071](https://github.com/plotly/dash/issues/1071), and [#1084](https://github.com/plotly/dash/issues/1084). Also fixed [#1105](https://github.com/plotly/dash/issues/1105): async components that aren't rendered by the page (for example in a background Tab) would block the app from executing callbacks. + - [#1142](https://github.com/plotly/dash/pull/1142) [Persistence](https://dash.plot.ly/persistence): Also persist 0, empty string etc ## [1.9.1] - 2020-02-27 From 57391f42be07f5b0bfd42a677edda8945a89d550 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 30 Mar 2020 21:55:23 -0400 Subject: [PATCH 59/81] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Marc-André Rivet --- CHANGELOG.md | 2 +- dash-renderer/src/reducers/reducer.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edc2b4c85d..1e8a48db79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added -- [#1103](https://github.com/plotly/dash/pull/1103) Wildcard IDs and callbacks. Component IDs can be dictionaries, and callbacks can reference patterns of components, using three different wildcards: `All`, `MATCH`, and `ALLSMALLER`, available from `dash.dependencies`. This lets you create components on demand, and have callbacks respond to any and all of them. To help with this, `dash.callback_context` gets three new entries: `outputs_list`, `inputs_list`, and `states_list`, which contain all the ids, properties, and except for the outputs, the property values from all matched components. +- [#1103](https://github.com/plotly/dash/pull/1103) Wildcard IDs and callbacks. Component IDs can be dictionaries, and callbacks can reference patterns of components, using three different wildcards: `ALL`, `MATCH`, and `ALLSMALLER`, available from `dash.dependencies`. This lets you create components on demand, and have callbacks respond to any and all of them. To help with this, `dash.callback_context` gets three new entries: `outputs_list`, `inputs_list`, and `states_list`, which contain all the ids, properties, and except for the outputs, the property values from all matched components. - [#1134](https://github.com/plotly/dash/pull/1134) Allow `dash.run_server()` host and port parameters to be set with environment variables HOST & PORT, respectively diff --git a/dash-renderer/src/reducers/reducer.js b/dash-renderer/src/reducers/reducer.js index 36004db676..541422378b 100644 --- a/dash-renderer/src/reducers/reducer.js +++ b/dash-renderer/src/reducers/reducer.js @@ -49,7 +49,6 @@ function mainReducer() { function getInputHistoryState(itempath, props, state) { const {graphs, layout, paths} = state; const {InputGraph} = graphs; - // TODO: wildcards? const keyObj = filter(equals(itempath), paths.strs); let historyEntry; if (!isEmpty(keyObj)) { @@ -58,7 +57,6 @@ function getInputHistoryState(itempath, props, state) { keys(props).forEach(propKey => { const inputKey = `${id}.${propKey}`; if ( - // TODO: wildcards? InputGraph.hasNode(inputKey) && InputGraph.dependenciesOf(inputKey).length > 0 ) { From 221e3884d8ccd8aeffd88f295b091b5644fff315 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 31 Mar 2020 21:03:19 -0400 Subject: [PATCH 60/81] fix wildcardOverlap for MATCH/ALLSMALLER case, fix mergeAllBlockers, test these and clientside wildcards --- dash-renderer/src/actions/dependencies.js | 26 +++-- dash-renderer/src/actions/paths.js | 3 +- tests/integration/callbacks/test_wildcards.py | 96 +++++++++++++++++++ 3 files changed, 117 insertions(+), 8 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 111ffb6293..fd5ebf4495 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -31,6 +31,8 @@ import { zipObj, } from 'ramda'; +const mergeMax = mergeWith(Math.max); + import {getPath} from './paths'; import {crawlLayout} from './utils'; @@ -429,7 +431,18 @@ function findMismatchedWildcards(outputs, inputs, state, head, dispatchError) { }); } -const matchWildKeys = ([a, b]) => a === b || (a && a.wild) || (b && b.wild); +const matchWildKeys = ([a, b]) => { + const aWild = a && a.wild; + const bWild = b && b.wild; + if (aWild && bWild) { + // Every wildcard combination overlaps except MATCH<->ALLSMALLER + return !( + (a === MATCH && b === ALLSMALLER) || + (a === ALLSMALLER && b === MATCH) + ); + } + return a === b || aWild || bWild; +}; function wildcardOverlap({id, property}, objs) { const idKeys = keys(id).sort(); @@ -1199,8 +1212,7 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { if (callback) { const foundIndex = foundCbIds[callback.resolvedId]; if (foundIndex !== undefined) { - callbacks[foundIndex].changedPropIds = mergeWith( - Math.max, + callbacks[foundIndex].changedPropIds = mergeMax( callbacks[foundIndex].changedPropIds, callback.changedPropIds ); @@ -1410,8 +1422,7 @@ export function followForward(graphs, paths, callbacks_) { allResolvedIds[nextCB.resolvedId] = existingIndex; } else { const existingCB = callbacks[existingIndex]; - existingCB.changedPropIds = mergeWith( - Math.max, + existingCB.changedPropIds = mergeMax( existingCB.changedPropIds, nextCB.changedPropIds ); @@ -1433,8 +1444,9 @@ export function followForward(graphs, paths, callbacks_) { function mergeAllBlockers(cb1, cb2) { function mergeBlockers(a, b) { if (cb1[a][cb2.resolvedId] && !cb2[b][cb1.resolvedId]) { - cb2[b] = mergeRight({[cb1.resolvedId]: 1}, cb1[b], cb2[b]); - cb1[a] = mergeRight({[cb2.resolvedId]: 1}, cb2[a], cb1[b]); + cb2[b][cb1.resolvedId] = cb1[a][cb2.resolvedId]; + cb2[b] = mergeMax(cb1[b], cb2[b]); + cb1[a] = mergeMax(cb2[a], cb1[a]); } } mergeBlockers('blockedBy', 'blocking'); diff --git a/dash-renderer/src/actions/paths.js b/dash-renderer/src/actions/paths.js index 5ab2ea9420..d9eca5a6be 100644 --- a/dash-renderer/src/actions/paths.js +++ b/dash-renderer/src/actions/paths.js @@ -67,7 +67,8 @@ export function getPath(paths, id) { return false; } const values = props(keys, id); - return find(propEq('values', values), keyPaths).path; + const pathObj = find(propEq('values', values), keyPaths); + return pathObj && pathObj.path; } return paths.strs[id]; } diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index e718457b16..82f314dc26 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -1,5 +1,7 @@ from multiprocessing import Value +import pytest import re +from selenium.webdriver.common.keys import Keys import dash_html_components as html import dash_core_components as dcc @@ -214,3 +216,97 @@ def assert_item(item, text, done, prefix="", suffix=""): # This was a tricky one - trigger based on deleted components dash_duo.wait_for_text_to_equal("#totals", "0 of 0 items completed") assert_count(0) + + +def fibonacci_app(clientside): + # This app tests 2 things in particular: + # - clientside callbacks work the same as server-side + # - callbacks using ALLSMALLER as an input to MATCH of the exact same id/prop + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Input(id="n", type="number", min=0, max=10, value=4), + html.Div(id="series"), + html.Div(id="sum") + ]) + + @app.callback( + Output("series", "children"), [Input("n", "value")] + ) + def items(n): + return [html.Div(id={"i": i}) for i in range(n)] + + if clientside: + app.clientside_callback( + """ + function(vals) { + var len = vals.length; + return len < 2 ? len : +(vals[len - 1] || 0) + +(vals[len - 2] || 0); + } + """, + Output({"i": MATCH}, "children"), + [Input({"i": ALLSMALLER}, "children")] + ) + + app.clientside_callback( + """ + function(vals) { + var sum = vals.reduce(function(a, b) { return +a + +b; }, 0); + return vals.length + ' elements, sum: ' + sum; + } + """, + Output("sum", "children"), + [Input({"i": ALL}, "children")] + ) + + else: + @app.callback( + Output({"i": MATCH}, "children"), + [Input({"i": ALLSMALLER}, "children")] + ) + def sequence(prev): + if (len(prev) < 2): + return len(prev) + return int(prev[-1] or 0) + int(prev[-2] or 0) + + @app.callback(Output("sum", "children"), [Input({"i": ALL}, "children")]) + def show_sum(seq): + return "{} elements, sum: {}".format(len(seq), sum(int(v or 0) for v in seq)) + + return app + + +@pytest.mark.parametrize("clientside", (False, True)) +def test_cbwc002_fibonacci_app(clientside, dash_duo): + app = fibonacci_app(clientside) + dash_duo.start_server(app) + + # app starts with 4 elements: 0, 1, 1, 2 + dash_duo.wait_for_text_to_equal("#sum", "4 elements, sum: 4") + + # add 5th item, "3" + dash_duo.find_element("#n").send_keys(Keys.UP) + dash_duo.wait_for_text_to_equal("#sum", "5 elements, sum: 7") + + # add 6th item, "5" + dash_duo.find_element("#n").send_keys(Keys.UP) + dash_duo.wait_for_text_to_equal("#sum", "6 elements, sum: 12") + + # add 7th item, "8" + dash_duo.find_element("#n").send_keys(Keys.UP) + dash_duo.wait_for_text_to_equal("#sum", "7 elements, sum: 20") + + # back down all the way to no elements + dash_duo.find_element("#n").send_keys(Keys.DOWN) + dash_duo.wait_for_text_to_equal("#sum", "6 elements, sum: 12") + dash_duo.find_element("#n").send_keys(Keys.DOWN) + dash_duo.wait_for_text_to_equal("#sum", "5 elements, sum: 7") + dash_duo.find_element("#n").send_keys(Keys.DOWN) + dash_duo.wait_for_text_to_equal("#sum", "4 elements, sum: 4") + dash_duo.find_element("#n").send_keys(Keys.DOWN) + dash_duo.wait_for_text_to_equal("#sum", "3 elements, sum: 2") + dash_duo.find_element("#n").send_keys(Keys.DOWN) + dash_duo.wait_for_text_to_equal("#sum", "2 elements, sum: 1") + dash_duo.find_element("#n").send_keys(Keys.DOWN) + dash_duo.wait_for_text_to_equal("#sum", "1 elements, sum: 0") + dash_duo.find_element("#n").send_keys(Keys.DOWN) + dash_duo.wait_for_text_to_equal("#sum", "0 elements, sum: 0") From 669ecc1dec9d5698f3da75001e0799f902057a8a Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 1 Apr 2020 02:27:25 -0400 Subject: [PATCH 61/81] fix & test multi-app url routing --- dash/dash.py | 2 +- tests/integration/test_integration.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 3d080c88d3..3338746e1c 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -408,7 +408,7 @@ def _add_url(self, name, view_func, methods=("GET",)): full_name = self.config.routes_pathname_prefix + name self.server.add_url_rule( - full_name, view_func=view_func, endpoint=name, methods=list(methods) + full_name, view_func=view_func, endpoint=full_name, methods=list(methods) ) # record the url in Dash.routes so that it can be accessed later diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index b196017862..8476f9fb90 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -3,6 +3,7 @@ from copy import copy from multiprocessing import Value from selenium.webdriver.common.keys import Keys +import flask import pytest @@ -745,3 +746,28 @@ def test_inin_024_port_env_success(dash_duo): dash_duo.start_server(app, port="12345") assert dash_duo.server_url == "http://localhost:12345" dash_duo.wait_for_text_to_equal("#out", "hi") + + +def nested_app(server, path, text): + app = Dash(__name__, server=server, url_base_pathname=path) + app.layout = html.Div(id="out") + + @app.callback(Output("out", "children"), [Input("out", "n_clicks")]) + def out(n): + return text + + return app + + +def test_inin025_url_base_pathname(dash_br, dash_thread_server): + server = flask.Flask(__name__) + app = nested_app(server, "/app1/", "The first") + nested_app(server, "/app2/", "The second") + + dash_thread_server(app) + + dash_br.server_url = "http://localhost:8050/app1/" + dash_br.wait_for_text_to_equal("#out", "The first") + + dash_br.server_url = "http://localhost:8050/app2/" + dash_br.wait_for_text_to_equal("#out", "The second") From 3f46fe658dd48d17ece406d88015798291b5c6cb Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 1 Apr 2020 03:14:20 -0400 Subject: [PATCH 62/81] clean up renderer package-lock --- dash-renderer/package-lock.json | 5701 ++++++++++++++++++------------- dash-renderer/package.json | 2 +- 2 files changed, 3381 insertions(+), 2322 deletions(-) diff --git a/dash-renderer/package-lock.json b/dash-renderer/package-lock.json index 5ea11c3458..74f49ba041 100644 --- a/dash-renderer/package-lock.json +++ b/dash-renderer/package-lock.json @@ -14,34 +14,33 @@ } }, "@babel/compat-data": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.9.0.tgz", - "integrity": "sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.8.6.tgz", + "integrity": "sha512-CurCIKPTkS25Mb8mz267vU95vy+TyUpnctEX2lV33xWNmHAfjruztgiPBbXZRh3xZZy1CYvGx6XfxyTVS+sk7Q==", "dev": true, "requires": { - "browserslist": "^4.9.1", + "browserslist": "^4.8.5", "invariant": "^2.2.4", "semver": "^5.5.0" } }, "@babel/core": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", - "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.7.tgz", + "integrity": "sha512-rBlqF3Yko9cynC5CCFy6+K/w2N+Sq/ff2BPy+Krp7rHlABIr5epbA7OxVeKoMHB39LZOp1UY5SuLjy6uWi35yA==", "dev": true, "requires": { "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.0", - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helpers": "^7.9.0", - "@babel/parser": "^7.9.0", + "@babel/generator": "^7.8.7", + "@babel/helpers": "^7.8.4", + "@babel/parser": "^7.8.7", "@babel/template": "^7.8.6", - "@babel/traverse": "^7.9.0", - "@babel/types": "^7.9.0", + "@babel/traverse": "^7.8.6", + "@babel/types": "^7.8.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.1", - "json5": "^2.1.2", + "json5": "^2.1.0", "lodash": "^4.17.13", "resolve": "^1.3.2", "semver": "^5.4.1", @@ -57,26 +56,112 @@ "@babel/highlight": "^7.8.3" } }, + "@babel/generator": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz", + "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==", + "dev": true, + "requires": { + "@babel/types": "^7.8.7", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", + "esutils": "^2.0.2", "js-tokens": "^4.0.0" } + }, + "@babel/parser": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", + "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } } } }, "@babel/generator": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.4.tgz", - "integrity": "sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.2.tgz", + "integrity": "sha512-WthSArvAjYLz4TcbKOi88me+KmDJdKSlfwwN8CnUYn9jBkzhq0ZEPuBfkAWIvjJ3AdEV1Cf/+eSQTnp3IDJKlQ==", "dev": true, "requires": { - "@babel/types": "^7.9.0", + "@babel/types": "^7.7.2", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0" @@ -89,6 +174,19 @@ "dev": true, "requires": { "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-builder-binary-assignment-operator-visitor": { @@ -99,27 +197,161 @@ "requires": { "@babel/helper-explode-assignable-expression": "^7.8.3", "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-builder-react-jsx": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.9.0.tgz", - "integrity": "sha512-weiIo4gaoGgnhff54GQ3P5wsUQmnSwpkvU0r6ZHq6TzoSzKy4JxHEgnxNytaKbov2a9z/CVNyzliuCOUPEX3Jw==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.8.3.tgz", + "integrity": "sha512-JT8mfnpTkKNCboTqZsQTdGo3l3Ik3l7QIt9hh0O9DYiwVel37VoJpILKM4YFbP2euF32nkQSb+F9cUk9b7DDXQ==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/types": "^7.9.0" + "@babel/types": "^7.8.3", + "esutils": "^2.0.0" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, - "@babel/helper-builder-react-jsx-experimental": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.9.0.tgz", - "integrity": "sha512-3xJEiyuYU4Q/Ar9BsHisgdxZsRlsShMe90URZ0e6przL26CCs8NJbDoxH94kKT17PcxlMhsCAwZd90evCo26VQ==", + "@babel/helper-call-delegate": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.8.7.tgz", + "integrity": "sha512-doAA5LAKhsFCR0LAFIf+r2RSMmC+m8f/oQ+URnUET/rWeEzC0yTRmAGyWkD4sSu3xwbS7MYQ2u+xlt1V5R56KQ==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-module-imports": "^7.8.3", - "@babel/types": "^7.9.0" + "@babel/helper-hoist-variables": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.7" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz", + "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==", + "dev": true, + "requires": { + "@babel/types": "^7.8.7", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", + "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-compilation-targets": { @@ -136,14 +368,14 @@ } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.8.8", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz", - "integrity": "sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.6.tgz", + "integrity": "sha512-bPyujWfsHhV/ztUkwGHz/RPV1T1TDEsSZDsN42JPehndA+p1KKTh3npvTadux0ZhCrytx9tvjpWNowKby3tM6A==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.8.3", "@babel/helper-regex": "^7.8.3", - "regexpu-core": "^4.7.0" + "regexpu-core": "^4.6.0" } }, "@babel/helper-define-map": { @@ -155,6 +387,76 @@ "@babel/helper-function-name": "^7.8.3", "@babel/types": "^7.8.3", "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", + "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-explode-assignable-expression": { @@ -165,26 +467,134 @@ "requires": { "@babel/traverse": "^7.8.3", "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz", + "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==", + "dev": true, + "requires": { + "@babel/types": "^7.8.7", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", + "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-function-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", - "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.0.tgz", + "integrity": "sha512-tDsJgMUAP00Ugv8O2aGEua5I2apkaQO7lBGUq1ocwN3G23JE5Dcq0uh3GvFTChPa4b40AWiAsLvCZOA2rdnQ7Q==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/helper-get-function-arity": "^7.7.0", + "@babel/template": "^7.7.0", + "@babel/types": "^7.7.0" } }, "@babel/helper-get-function-arity": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", - "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.0.tgz", + "integrity": "sha512-tLdojOTz4vWcEnHWHCuPN5P85JLZWbm5Fx5ZsMEMPhF3Uoe3O7awrbM2nQ04bDOUToH/2tH/ezKEOR8zEYzqyw==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.7.0" } }, "@babel/helper-hoist-variables": { @@ -194,6 +604,19 @@ "dev": true, "requires": { "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-member-expression-to-functions": { @@ -203,6 +626,19 @@ "dev": true, "requires": { "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-module-imports": { @@ -212,12 +648,25 @@ "dev": true, "requires": { "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-module-transforms": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", - "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.8.6.tgz", + "integrity": "sha512-RDnGJSR5EFBJjG3deY0NiL0K9TO8SXxS9n/MPsbPK/s9LbQymuLNtlzvDiNS7IpecuL45cMeLVkA+HfmlrnkRg==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.8.3", @@ -225,17 +674,89 @@ "@babel/helper-simple-access": "^7.8.3", "@babel/helper-split-export-declaration": "^7.8.3", "@babel/template": "^7.8.6", - "@babel/types": "^7.9.0", + "@babel/types": "^7.8.6", "lodash": "^4.17.13" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", - "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", - "dev": true, - "requires": { + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.6.tgz", + "integrity": "sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", + "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "dev": true, + "requires": { "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-plugin-utils": { @@ -264,6 +785,114 @@ "@babel/template": "^7.8.3", "@babel/traverse": "^7.8.3", "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz", + "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==", + "dev": true, + "requires": { + "@babel/types": "^7.8.7", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", + "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-replace-supers": { @@ -276,6 +905,114 @@ "@babel/helper-optimise-call-expression": "^7.8.3", "@babel/traverse": "^7.8.6", "@babel/types": "^7.8.6" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.6.tgz", + "integrity": "sha512-4bpOR5ZBz+wWcMeVtcf7FbjcFzCp+817z2/gHNncIRcM9MmKzUhtWCYAq27RAfUrAFwb+OCG1s9WEaVxfi6cjg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.6", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.6.tgz", + "integrity": "sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-simple-access": { @@ -286,23 +1023,67 @@ "requires": { "@babel/template": "^7.8.3", "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.6.tgz", + "integrity": "sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-split-export-declaration": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", - "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.0.tgz", + "integrity": "sha512-HgYSI8rH08neWlAH3CcdkFg9qX9YsZysZI5GD8LjhQib/mM0jGOZOVkoUiiV2Hu978fRtjtsGsW6w0pKHUWtqA==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.7.0" } }, - "@babel/helper-validator-identifier": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", - "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", - "dev": true - }, "@babel/helper-wrap-function": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz", @@ -313,24 +1094,240 @@ "@babel/template": "^7.8.3", "@babel/traverse": "^7.8.3", "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz", + "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==", + "dev": true, + "requires": { + "@babel/types": "^7.8.7", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", + "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helpers": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.2.tgz", - "integrity": "sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", + "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", "dev": true, "requires": { "@babel/template": "^7.8.3", - "@babel/traverse": "^7.9.0", - "@babel/types": "^7.9.0" - } - }, - "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", - "dev": true, + "@babel/traverse": "^7.8.4", + "@babel/types": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.7.tgz", + "integrity": "sha512-DQwjiKJqH4C3qGiyQCAExJHoZssn49JTMJgZ8SANGgVFdkupcUhLOdkAeoC6kmHZCPfoDG5M0b6cFlSN5wW7Ew==", + "dev": true, + "requires": { + "@babel/types": "^7.8.7", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", + "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/highlight": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", + "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", @@ -338,9 +1335,9 @@ } }, "@babel/parser": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.2.tgz", + "integrity": "sha512-DDaR5e0g4ZTb9aP7cpSZLkACEBdoLGwJDWgHtBhrGX7Q1RjhdoMOfexICj5cqTAtpowjGQWfcvfnQG7G2kAB5w==", "dev": true }, "@babel/plugin-proposal-async-generator-functions": { @@ -384,20 +1381,10 @@ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" } }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz", - "integrity": "sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3" - } - }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz", - "integrity": "sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-8qvuPwU/xxUCt78HocNlv0mXXo0wdh9VT1R04WU8HGOfaOob26pF+9P5/lYjN/q7DHOX1bvX60hnhOvuQUJdbA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3", @@ -415,9 +1402,9 @@ } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz", - "integrity": "sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.8.3.tgz", + "integrity": "sha512-QIoIR9abkVn+seDE3OjA08jWcs3eZ9+wJCKSRgo3WdEU2csFYgdScb+8qHB3+WXsGJD55u+5hWCISI7ejXS+kg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3", @@ -425,12 +1412,12 @@ } }, "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.8.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz", - "integrity": "sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.3.tgz", + "integrity": "sha512-1/1/rEZv2XGweRwwSkLpY+s60za9OZ1hJs4YDqFHCw0kYWYwL5IFljVY1MYBL+weT1l9pokDO2uhSTLVxzoHkQ==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.8", + "@babel/helper-create-regexp-features-plugin": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" } }, @@ -477,6 +1464,14 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } } }, "@babel/plugin-syntax-nullish-coalescing-operator": { @@ -488,15 +1483,6 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz", - "integrity": "sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, "@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", @@ -573,9 +1559,9 @@ } }, "@babel/plugin-transform-classes": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz", - "integrity": "sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.8.6.tgz", + "integrity": "sha512-k9r8qRay/R6v5aWZkrEclEhKO6mc1CCQr2dLsVHBmOQiMpN6I2bpjX3vgnldUWeEI1GHVNByULVxZ4BdP4Hmdg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.8.3", @@ -586,6 +1572,85 @@ "@babel/helper-replace-supers": "^7.8.6", "@babel/helper-split-export-declaration": "^7.8.3", "globals": "^11.1.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", + "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/plugin-transform-computed-properties": { @@ -598,9 +1663,9 @@ } }, "@babel/plugin-transform-destructuring": { - "version": "7.8.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz", - "integrity": "sha512-eRJu4Vs2rmttFCdhPUM3bV0Yo/xPSdPw6ML9KHs/bjB4bLA5HXlbvYXPOD5yASodGod+krjYx21xm1QmL8dCJQ==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.3.tgz", + "integrity": "sha512-H4X646nCkiEcHZUZaRkhE2XVsoz0J/1x3VVujnn96pSoGCtKPA99ZZA+va+gK+92Zycd6OBKCD8tDb/731bhgQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" @@ -636,9 +1701,9 @@ } }, "@babel/plugin-transform-for-of": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz", - "integrity": "sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.6.tgz", + "integrity": "sha512-M0pw4/1/KI5WAxPsdcUL/w2LJ7o89YHN3yLkzNjg7Yl15GlVGgzHyCU+FMeAxevHGsLVmUqbirlUIKTafPmzdw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" @@ -652,6 +1717,76 @@ "requires": { "@babel/helper-function-name": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.7.tgz", + "integrity": "sha512-9JWls8WilDXFGxs0phaXAZgpxTZhSk/yOYH2hTHC0X1yC7Z78IJfvR1vJ+rmJKq3I35td2XzXzN6ZLYlna+r/A==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/plugin-transform-literals": { @@ -673,47 +1808,55 @@ } }, "@babel/plugin-transform-modules-amd": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz", - "integrity": "sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz", + "integrity": "sha512-MadJiU3rLKclzT5kBH4yxdry96odTUwuqrZM+GllFI/VhxfPz+k9MshJM+MwhfkCdxxclSbSBbUGciBngR+kEQ==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.9.0", + "@babel/helper-module-transforms": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", "babel-plugin-dynamic-import-node": "^2.3.0" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz", - "integrity": "sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.8.3.tgz", + "integrity": "sha512-JpdMEfA15HZ/1gNuB9XEDlZM1h/gF/YOH7zaZzQu2xCFRfwc01NXBMHHSTT6hRjlXJJs5x/bfODM3LiCk94Sxg==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.9.0", + "@babel/helper-module-transforms": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", "@babel/helper-simple-access": "^7.8.3", "babel-plugin-dynamic-import-node": "^2.3.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz", - "integrity": "sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.8.3.tgz", + "integrity": "sha512-8cESMCJjmArMYqa9AO5YuMEkE4ds28tMpZcGZB/jl3n0ZzlsxOAi3mC+SKypTfT8gjMupCnd3YiXCkMjj2jfOg==", "dev": true, "requires": { "@babel/helper-hoist-variables": "^7.8.3", - "@babel/helper-module-transforms": "^7.9.0", + "@babel/helper-module-transforms": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", "babel-plugin-dynamic-import-node": "^2.3.0" } }, "@babel/plugin-transform-modules-umd": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz", - "integrity": "sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.8.3.tgz", + "integrity": "sha512-evhTyWhbwbI3/U6dZAnx/ePoV7H6OUG+OjiJFHmhr9FPn0VShjwC2kdxqIuQ/+1P50TMrneGzMeyMTFOjKSnAw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.9.0", + "@babel/helper-module-transforms": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" } }, @@ -746,13 +1889,36 @@ } }, "@babel/plugin-transform-parameters": { - "version": "7.9.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz", - "integrity": "sha512-fzrQFQhp7mIhOzmOtPiKffvCYQSK10NR8t6BBz2yPbeUHb9OLW8RZGtgDRBn8z2hGcwvKDL3vC7ojPTLNxmqEg==", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.7.tgz", + "integrity": "sha512-brYWaEPTRimOctz2NDA3jnBbDi7SVN2T4wYuu0aqSzxC3nozFZngGaw29CJ9ZPweB7k+iFmZuoG3IVPIcXmD2g==", "dev": true, "requires": { + "@babel/helper-call-delegate": "^7.8.7", "@babel/helper-get-function-arity": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" + }, + "dependencies": { + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/plugin-transform-property-literals": { @@ -765,11 +1931,12 @@ } }, "@babel/plugin-transform-react-constant-elements": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.9.0.tgz", - "integrity": "sha512-wXMXsToAUOxJuBBEHajqKLFWcCkOSLshTI2ChCFFj1zDd7od4IOxiwLCOObNUvOpkxLpjIuaIdBMmNt6ocCPAw==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.8.3.tgz", + "integrity": "sha512-glrzN2U+egwRfkNFtL34xIBYTxbbUF2qJTP8HD3qETBBqzAWSeNB821X0GjU06+dNpq/UyCIjI72FmGE5NNkQQ==", "dev": true, "requires": { + "@babel/helper-annotate-as-pure": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3" } }, @@ -780,49 +1947,69 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } } }, "@babel/plugin-transform-react-jsx": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.9.4.tgz", - "integrity": "sha512-Mjqf3pZBNLt854CK0C/kRuXAnE6H/bo7xYojP+WGtX8glDGSibcwnsWwhwoSuRg0+EBnxPC1ouVnuetUIlPSAw==", - "dev": true, - "requires": { - "@babel/helper-builder-react-jsx": "^7.9.0", - "@babel/helper-builder-react-jsx-experimental": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.8.3" - } - }, - "@babel/plugin-transform-react-jsx-development": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.9.0.tgz", - "integrity": "sha512-tK8hWKrQncVvrhvtOiPpKrQjfNX3DtkNLSX4ObuGcpS9p0QrGetKmlySIGR07y48Zft8WVgPakqd/bk46JrMSw==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.8.3.tgz", + "integrity": "sha512-r0h+mUiyL595ikykci+fbwm9YzmuOrUBi0b+FDIKmi3fPQyFokWVEMJnRWHJPPQEjyFJyna9WZC6Viv6UHSv1g==", "dev": true, "requires": { - "@babel/helper-builder-react-jsx-experimental": "^7.9.0", + "@babel/helper-builder-react-jsx": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-jsx": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } } }, "@babel/plugin-transform-react-jsx-self": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.9.0.tgz", - "integrity": "sha512-K2ObbWPKT7KUTAoyjCsFilOkEgMvFG+y0FqOl6Lezd0/13kMkkjHskVsZvblRPj1PHA44PrToaZANrryppzTvQ==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.8.3.tgz", + "integrity": "sha512-01OT7s5oa0XTLf2I8XGsL8+KqV9lx3EZV+jxn/L2LQ97CGKila2YMroTkCEIE0HV/FF7CMSRsIAybopdN9NTdg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-jsx": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } } }, "@babel/plugin-transform-react-jsx-source": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.9.0.tgz", - "integrity": "sha512-K6m3LlSnTSfRkM6FcRk8saNEeaeyG5k7AVkBU2bZK3+1zdkSED3qNdsWrUgQBeTVD2Tp3VMmerxVO2yM5iITmw==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.8.3.tgz", + "integrity": "sha512-PLMgdMGuVDtRS/SzjNEQYUT8f4z1xb2BAT54vM1X5efkVuYBf5WyGUMbpmARcfq3NaglIwz08UVQK4HHHbC6ag==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-syntax-jsx": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } } }, "@babel/plugin-transform-regenerator": { @@ -909,20 +2096,25 @@ "regenerator-runtime": "^0.13.4" }, "dependencies": { + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz", + "integrity": "sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g==" } } }, "@babel/preset-env": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.9.0.tgz", - "integrity": "sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ==", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.8.7.tgz", + "integrity": "sha512-BYftCVOdAYJk5ASsznKAUl53EMhfBbr8CJ1X+AJLfGPscQkwJFiaV/Wn9DPH/7fzm2v6iRYJKYHSqyynTGw0nw==", "dev": true, "requires": { - "@babel/compat-data": "^7.9.0", + "@babel/compat-data": "^7.8.6", "@babel/helper-compilation-targets": "^7.8.7", "@babel/helper-module-imports": "^7.8.3", "@babel/helper-plugin-utils": "^7.8.3", @@ -930,16 +2122,14 @@ "@babel/plugin-proposal-dynamic-import": "^7.8.3", "@babel/plugin-proposal-json-strings": "^7.8.3", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-proposal-numeric-separator": "^7.8.3", - "@babel/plugin-proposal-object-rest-spread": "^7.9.0", + "@babel/plugin-proposal-object-rest-spread": "^7.8.3", "@babel/plugin-proposal-optional-catch-binding": "^7.8.3", - "@babel/plugin-proposal-optional-chaining": "^7.9.0", + "@babel/plugin-proposal-optional-chaining": "^7.8.3", "@babel/plugin-proposal-unicode-property-regex": "^7.8.3", "@babel/plugin-syntax-async-generators": "^7.8.0", "@babel/plugin-syntax-dynamic-import": "^7.8.0", "@babel/plugin-syntax-json-strings": "^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", - "@babel/plugin-syntax-numeric-separator": "^7.8.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", "@babel/plugin-syntax-optional-chaining": "^7.8.0", @@ -948,20 +2138,20 @@ "@babel/plugin-transform-async-to-generator": "^7.8.3", "@babel/plugin-transform-block-scoped-functions": "^7.8.3", "@babel/plugin-transform-block-scoping": "^7.8.3", - "@babel/plugin-transform-classes": "^7.9.0", + "@babel/plugin-transform-classes": "^7.8.6", "@babel/plugin-transform-computed-properties": "^7.8.3", "@babel/plugin-transform-destructuring": "^7.8.3", "@babel/plugin-transform-dotall-regex": "^7.8.3", "@babel/plugin-transform-duplicate-keys": "^7.8.3", "@babel/plugin-transform-exponentiation-operator": "^7.8.3", - "@babel/plugin-transform-for-of": "^7.9.0", + "@babel/plugin-transform-for-of": "^7.8.6", "@babel/plugin-transform-function-name": "^7.8.3", "@babel/plugin-transform-literals": "^7.8.3", "@babel/plugin-transform-member-expression-literals": "^7.8.3", - "@babel/plugin-transform-modules-amd": "^7.9.0", - "@babel/plugin-transform-modules-commonjs": "^7.9.0", - "@babel/plugin-transform-modules-systemjs": "^7.9.0", - "@babel/plugin-transform-modules-umd": "^7.9.0", + "@babel/plugin-transform-modules-amd": "^7.8.3", + "@babel/plugin-transform-modules-commonjs": "^7.8.3", + "@babel/plugin-transform-modules-systemjs": "^7.8.3", + "@babel/plugin-transform-modules-umd": "^7.8.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.8.3", "@babel/plugin-transform-new-target": "^7.8.3", "@babel/plugin-transform-object-super": "^7.8.3", @@ -975,40 +2165,46 @@ "@babel/plugin-transform-template-literals": "^7.8.3", "@babel/plugin-transform-typeof-symbol": "^7.8.4", "@babel/plugin-transform-unicode-regex": "^7.8.3", - "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.9.0", - "browserslist": "^4.9.1", + "@babel/types": "^7.8.7", + "browserslist": "^4.8.5", "core-js-compat": "^3.6.2", "invariant": "^2.2.2", "levenary": "^1.1.1", "semver": "^5.5.0" - } - }, - "@babel/preset-modules": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.3.tgz", - "integrity": "sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" + }, + "dependencies": { + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/preset-react": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.9.4.tgz", - "integrity": "sha512-AxylVB3FXeOTQXNXyiuAQJSvss62FEotbX2Pzx3K/7c+MKJMdSg6Ose6QYllkdCFA8EInCJVw7M/o5QbLuA4ZQ==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.8.3.tgz", + "integrity": "sha512-9hx0CwZg92jGb7iHYQVgi0tOEHP/kM60CtWJQnmbATSPIQQ2xYzfoCI3EdqAhFBeeJwYMdWQuDUHMsuDbH9hyQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.3", "@babel/plugin-transform-react-display-name": "^7.8.3", - "@babel/plugin-transform-react-jsx": "^7.9.4", - "@babel/plugin-transform-react-jsx-development": "^7.9.0", - "@babel/plugin-transform-react-jsx-self": "^7.9.0", - "@babel/plugin-transform-react-jsx-source": "^7.9.0" + "@babel/plugin-transform-react-jsx": "^7.8.3", + "@babel/plugin-transform-react-jsx-self": "^7.8.3", + "@babel/plugin-transform-react-jsx-source": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } } }, "@babel/runtime": { @@ -1037,103 +2233,41 @@ } } }, - "@babel/runtime-corejs3": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.2.tgz", - "integrity": "sha512-HHxmgxbIzOfFlZ+tdeRKtaxWOMUoCG5Mu3wKeUmOxjYrwb3AAHgnmtCUbPPK11/raIWLIBK250t8E2BPO0p7jA==", + "@babel/template": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.0.tgz", + "integrity": "sha512-OKcwSYOW1mhWbnTBgQY5lvg1Fxg+VyfQGjcBduZFljfc044J5iDlnDSfhQ867O17XHiSCxYHUxHg2b7ryitbUQ==", "dev": true, "requires": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", - "dev": true - } - } - }, - "@babel/template": { - "version": "7.8.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", - "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/parser": "^7.8.6", - "@babel/types": "^7.8.6" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - } + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/types": "^7.7.0" } }, "@babel/traverse": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.0.tgz", - "integrity": "sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.2.tgz", + "integrity": "sha512-TM01cXib2+rgIZrGJOLaHV/iZUAxf4A0dt5auY6KNZ+cm6aschuJGqKJM3ROTt3raPUdIDk9siAufIFEleRwtw==", "dev": true, "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.0", - "@babel/helper-function-name": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/parser": "^7.9.0", - "@babel/types": "^7.9.0", + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.2", + "@babel/helper-function-name": "^7.7.0", + "@babel/helper-split-export-declaration": "^7.7.0", + "@babel/parser": "^7.7.2", + "@babel/types": "^7.7.2", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", - "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.8.3" - } - }, - "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.9.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - } } }, "@babel/types": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz", - "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.2.tgz", + "integrity": "sha512-YTf6PXoh3+eZgRCBzzP25Bugd2ngmpQVrk7kXX0i5N9BO7TFBtIgZYs7WtxtOGs8e6A4ZI7ECkbBCEHeXocvOA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.9.0", + "esutils": "^2.0.2", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } @@ -1254,14 +2388,14 @@ "dev": true }, "@jest/console": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.2.1.tgz", - "integrity": "sha512-v3tkMr5AeVm6R23wnZdC5dzXdHPFa6j2uiTC15iHISYkGIilE9O1qmAYKELHPXZifDbz9c8WwzsqoN8K8uG4jg==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.1.0.tgz", + "integrity": "sha512-3P1DpqAMK/L07ag/Y9/Jup5iDEG9P4pRAuZiMQnU0JB3UOvCyYCjCoxr7sIA80SeyUCUKrr24fKAxVpmBgQonA==", "dev": true, "requires": { - "@jest/source-map": "^25.2.1", + "@jest/source-map": "^25.1.0", "chalk": "^3.0.0", - "jest-util": "^25.2.1", + "jest-util": "^25.1.0", "slash": "^3.0.0" }, "dependencies": { @@ -1318,36 +2452,36 @@ } }, "@jest/core": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.2.1.tgz", - "integrity": "sha512-Pe7CVcysOmm69BgdgAuMjRCp6vmcCJy32PxPtArQDgiizIBQElHhE9P34afGwPgSb3+e3WC6XtEm4de7d9BtfQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.1.0.tgz", + "integrity": "sha512-iz05+NmwCmZRzMXvMo6KFipW7nzhbpEawrKrkkdJzgytavPse0biEnCNr2wRlyCsp3SmKaEY+SGv7YWYQnIdig==", "dev": true, "requires": { - "@jest/console": "^25.2.1", - "@jest/reporters": "^25.2.1", - "@jest/test-result": "^25.2.1", - "@jest/transform": "^25.2.1", - "@jest/types": "^25.2.1", + "@jest/console": "^25.1.0", + "@jest/reporters": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/transform": "^25.1.0", + "@jest/types": "^25.1.0", "ansi-escapes": "^4.2.1", "chalk": "^3.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.3", - "jest-changed-files": "^25.2.1", - "jest-config": "^25.2.1", - "jest-haste-map": "^25.2.1", - "jest-message-util": "^25.2.1", - "jest-regex-util": "^25.2.1", - "jest-resolve": "^25.2.1", - "jest-resolve-dependencies": "^25.2.1", - "jest-runner": "^25.2.1", - "jest-runtime": "^25.2.1", - "jest-snapshot": "^25.2.1", - "jest-util": "^25.2.1", - "jest-validate": "^25.2.1", - "jest-watcher": "^25.2.1", + "jest-changed-files": "^25.1.0", + "jest-config": "^25.1.0", + "jest-haste-map": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-regex-util": "^25.1.0", + "jest-resolve": "^25.1.0", + "jest-resolve-dependencies": "^25.1.0", + "jest-runner": "^25.1.0", + "jest-runtime": "^25.1.0", + "jest-snapshot": "^25.1.0", + "jest-util": "^25.1.0", + "jest-validate": "^25.1.0", + "jest-watcher": "^25.1.0", "micromatch": "^4.0.2", "p-each-series": "^2.1.0", - "realpath-native": "^2.0.0", + "realpath-native": "^1.1.0", "rimraf": "^3.0.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" @@ -1488,40 +2622,41 @@ } }, "@jest/environment": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.2.1.tgz", - "integrity": "sha512-aeA3UlUmpblmv2CHBcNA7LvcXlcCtRpXaKKFVooRy9/Jk8B4IZAZMfrML/d+1cG5FpF17s4JVdu1kx0mbnaqTQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.1.0.tgz", + "integrity": "sha512-cTpUtsjU4cum53VqBDlcW0E4KbQF03Cn0jckGPW/5rrE9tb+porD3+hhLtHAwhthsqfyF+bizyodTlsRA++sHg==", "dev": true, "requires": { - "@jest/fake-timers": "^25.2.1", - "@jest/types": "^25.2.1", - "jest-mock": "^25.2.1" + "@jest/fake-timers": "^25.1.0", + "@jest/types": "^25.1.0", + "jest-mock": "^25.1.0" } }, "@jest/fake-timers": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.2.1.tgz", - "integrity": "sha512-H1OC8AktrGTD10NHBauICkRCv7VOOrsgI8xokifAsxJMYhqoKBtJZbk2YpbrtnmdTUnk+qoxPUk+Mufwnl44iQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.1.0.tgz", + "integrity": "sha512-Eu3dysBzSAO1lD7cylZd/CVKdZZ1/43SF35iYBNV1Lvvn2Undp3Grwsv8PrzvbLhqwRzDd4zxrY4gsiHc+wygQ==", "dev": true, "requires": { - "@jest/types": "^25.2.1", - "jest-message-util": "^25.2.1", - "jest-mock": "^25.2.1", - "jest-util": "^25.2.1", + "@jest/types": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-mock": "^25.1.0", + "jest-util": "^25.1.0", "lolex": "^5.0.0" } }, "@jest/reporters": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.2.1.tgz", - "integrity": "sha512-jAnIECIIFVHiASKLpPBpZ9fIgWolKdMwUuyjSlNVixmtX6G83fyiGaOquaAU1ukAxnlKdCLjvH6BYdY+GGbd5Q==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.1.0.tgz", + "integrity": "sha512-ORLT7hq2acJQa8N+NKfs68ZtHFnJPxsGqmofxW7v7urVhzJvpKZG9M7FAcgh9Ee1ZbCteMrirHA3m5JfBtAaDg==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^25.2.1", - "@jest/test-result": "^25.2.1", - "@jest/transform": "^25.2.1", - "@jest/types": "^25.2.1", + "@jest/console": "^25.1.0", + "@jest/environment": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/transform": "^25.1.0", + "@jest/types": "^25.1.0", "chalk": "^3.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", @@ -1531,10 +2666,11 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.0.0", - "jest-haste-map": "^25.2.1", - "jest-resolve": "^25.2.1", - "jest-util": "^25.2.1", - "jest-worker": "^25.2.1", + "jest-haste-map": "^25.1.0", + "jest-resolve": "^25.1.0", + "jest-runtime": "^25.1.0", + "jest-util": "^25.1.0", + "jest-worker": "^25.1.0", "node-notifier": "^6.0.0", "slash": "^3.0.0", "source-map": "^0.6.0", @@ -1602,9 +2738,9 @@ } }, "@jest/source-map": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.2.1.tgz", - "integrity": "sha512-PgScGJm1U27+9Te/cxP4oUFqJ2PX6NhBL2a6unQ7yafCgs8k02c0LSyjSIx/ao0AwcAdCczfAPDf5lJ7zoB/7A==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.1.0.tgz", + "integrity": "sha512-ohf2iKT0xnLWcIUhL6U6QN+CwFWf9XnrM2a6ybL9NXxJjgYijjLSitkYHIdzkd8wFliH73qj/+epIpTiWjRtAA==", "dev": true, "requires": { "callsites": "^3.0.0", @@ -1621,49 +2757,49 @@ } }, "@jest/test-result": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.2.1.tgz", - "integrity": "sha512-E0tlWh2iOELRLbbPEngs3Dsx88vGBQOs6O3w46YeXfMHlwwqzWrlvoeUq6kRlHRm1O8H+EBr60Wtrwh20C+zWQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.1.0.tgz", + "integrity": "sha512-FZzSo36h++U93vNWZ0KgvlNuZ9pnDnztvaM7P/UcTx87aPDotG18bXifkf1Ji44B7k/eIatmMzkBapnAzjkJkg==", "dev": true, "requires": { - "@jest/console": "^25.2.1", - "@jest/transform": "^25.2.1", - "@jest/types": "^25.2.1", + "@jest/console": "^25.1.0", + "@jest/transform": "^25.1.0", + "@jest/types": "^25.1.0", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "@jest/test-sequencer": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.2.1.tgz", - "integrity": "sha512-yEhVlBRS7pg3MGBIQQafYfm2NT5trFa/qoxtLftQoZmyzKx3rPy0oJ+d/8QljK4X2RGY/i7mmQDxE6sGR9UqeQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.1.0.tgz", + "integrity": "sha512-WgZLRgVr2b4l/7ED1J1RJQBOharxS11EFhmwDqknpknE0Pm87HLZVS2Asuuw+HQdfQvm2aXL2FvvBLxOD1D0iw==", "dev": true, "requires": { - "@jest/test-result": "^25.2.1", - "jest-haste-map": "^25.2.1", - "jest-runner": "^25.2.1", - "jest-runtime": "^25.2.1" + "@jest/test-result": "^25.1.0", + "jest-haste-map": "^25.1.0", + "jest-runner": "^25.1.0", + "jest-runtime": "^25.1.0" } }, "@jest/transform": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.2.1.tgz", - "integrity": "sha512-puoD5EfqPeZ5m0dV9l8+PMdOVdRjeWcaEjGkH+eG45l0nPJ2vRcxu8J6CRl/6nQ5ZTHgg7LuM9C6FauNpdRpUA==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.1.0.tgz", + "integrity": "sha512-4ktrQ2TPREVeM+KxB4zskAT84SnmG1vaz4S+51aTefyqn3zocZUnliLLm5Fsl85I3p/kFPN4CRp1RElIfXGegQ==", "dev": true, "requires": { "@babel/core": "^7.1.0", - "@jest/types": "^25.2.1", + "@jest/types": "^25.1.0", "babel-plugin-istanbul": "^6.0.0", "chalk": "^3.0.0", "convert-source-map": "^1.4.0", "fast-json-stable-stringify": "^2.0.0", "graceful-fs": "^4.2.3", - "jest-haste-map": "^25.2.1", - "jest-regex-util": "^25.2.1", - "jest-util": "^25.2.1", + "jest-haste-map": "^25.1.0", + "jest-regex-util": "^25.1.0", + "jest-util": "^25.1.0", "micromatch": "^4.0.2", "pirates": "^4.0.1", - "realpath-native": "^2.0.0", + "realpath-native": "^1.1.0", "slash": "^3.0.0", "source-map": "^0.6.1", "write-file-atomic": "^3.0.0" @@ -1783,9 +2919,9 @@ } }, "@jest/types": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.2.1.tgz", - "integrity": "sha512-WuGFGJ3Rrycg+5ZwQTWKjr21M9psANPAWYD28K42hSeUzhv1H591VXIoq0tjs00mydhNOgVOkKSpzRS3CrOYFw==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.1.0.tgz", + "integrity": "sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", @@ -1930,15 +3066,15 @@ "dev": true }, "@svgr/babel-plugin-transform-svg-component": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.3.0.tgz", - "integrity": "sha512-x+XT8iqm81xRado8GLqDg5eUni8KbgTooqLvVZNUey2s8QBWXy7RbzJP6VX5WB9LgfbA7en6ka1/GfRUHyNhWg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.2.0.tgz", + "integrity": "sha512-t4dq0cNr7c8cuUu1Cwehai/0iXO3dV5876r2QRaLdgQF3C6XOK2vdTvNOwcJ3uRa92revSC3kGL8v8WgJrecRg==", "dev": true }, "@svgr/babel-preset": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.3.0.tgz", - "integrity": "sha512-VQPa3czStMhaDdOpGtzyVwWMZPbe437ocTGtXoa8R/iLTDYLD9BwyQLZZMPglKRBVaWJxqAVA8kbyFJQ9ahVdw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.2.0.tgz", + "integrity": "sha512-sr7486h+SddMU1VgFrajXx/Ws0a1QPzX4wUBM1LgG2PHeZpnm+fQs2MXQNdnfoXRwo7C5mH2I4QDiRVR/49BEg==", "dev": true, "requires": { "@svgr/babel-plugin-add-jsx-attribute": "^5.0.1", @@ -1948,16 +3084,16 @@ "@svgr/babel-plugin-svg-dynamic-title": "^5.0.1", "@svgr/babel-plugin-svg-em-dimensions": "^5.0.1", "@svgr/babel-plugin-transform-react-native-svg": "^5.0.1", - "@svgr/babel-plugin-transform-svg-component": "^5.3.0" + "@svgr/babel-plugin-transform-svg-component": "^5.2.0" } }, "@svgr/core": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.3.0.tgz", - "integrity": "sha512-ijUF5bLsK4xA0hmg9GEEeFC35LPYcIkONmQLCgjW3NDTf0eRBgCCm+ZZFdeDTYnkNeh+fFOfHEkv7U3tEbJnZQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.2.0.tgz", + "integrity": "sha512-vuODnJ0owj/oFi2bzskuSEk6TGuYoMV9hmvBhGuE1QktzMAAjOr0LnvUN5u2eGB6ilGdI7yqUKrZtQ0Tw44mrA==", "dev": true, "requires": { - "@svgr/plugin-jsx": "^5.3.0", + "@svgr/plugin-jsx": "^5.2.0", "camelcase": "^5.3.1", "cosmiconfig": "^6.0.0" } @@ -1972,21 +3108,21 @@ } }, "@svgr/plugin-jsx": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.3.0.tgz", - "integrity": "sha512-duJFQ99ZvIXvxJmEhV/8GoKnPrAMOuK7k+lVyepRYHmR0BZr33RupjpApdleU29teXcJO5oMHYsviiHRCQcU9Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.2.0.tgz", + "integrity": "sha512-z1HWitE5sCNgaXqBGrmCnnnvR/BRTq9B/lsgZ+T8OWABzZHhezqjjDUvkyyyBb3Y+0xExWg5aTh2jxqk7GR9tg==", "dev": true, "requires": { "@babel/core": "^7.7.5", - "@svgr/babel-preset": "^5.3.0", + "@svgr/babel-preset": "^5.2.0", "@svgr/hast-util-to-babel-ast": "^5.0.1", "svg-parser": "^2.0.2" } }, "@svgr/plugin-svgo": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.3.0.tgz", - "integrity": "sha512-rQw558t/pPPA/aWrazLDt3xIuxh9zDuUcQUeR8EO6yiYftkjT7My1MLPz1pha1LhkJCR1ug83fp+TPRYkV9EIQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.2.0.tgz", + "integrity": "sha512-cyqWx026uO3heGG/55j5zfJLtS5sl0dWYawN1JotOqpJDyyR7rraTsnydpwwsOoz0YpESjVjAkXOAfd41lBY9Q==", "dev": true, "requires": { "cosmiconfig": "^6.0.0", @@ -1995,38 +3131,19 @@ } }, "@svgr/webpack": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.3.0.tgz", - "integrity": "sha512-sOuTGg/Hy+YrVaWRlU2iHnIPxr9jCybuPBbfAFFOuoEJgAlWj9UdWLqeFXJdgwae3FNtHsCsPCzEM9PmLtdFvA==", - "dev": true, - "requires": { - "@babel/core": "^7.9.0", - "@babel/plugin-transform-react-constant-elements": "^7.9.0", - "@babel/preset-env": "^7.9.0", - "@babel/preset-react": "^7.9.1", - "@svgr/core": "^5.3.0", - "@svgr/plugin-jsx": "^5.3.0", - "@svgr/plugin-svgo": "^5.3.0", - "loader-utils": "^2.0.0" - }, - "dependencies": { - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - } + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.2.0.tgz", + "integrity": "sha512-y9tMjTtqrvC998aCOgsEP8pb+bdg2RR1v8i+sjT31m4Xb8HGd905K7/GdSwEMM2nlTFbxSUQPZRRvfaJwAvghA==", + "dev": true, + "requires": { + "@babel/core": "^7.4.5", + "@babel/plugin-transform-react-constant-elements": "^7.0.0", + "@babel/preset-env": "^7.4.5", + "@babel/preset-react": "^7.0.0", + "@svgr/core": "^5.2.0", + "@svgr/plugin-jsx": "^5.2.0", + "@svgr/plugin-svgo": "^5.2.0", + "loader-utils": "^1.2.3" } }, "@types/babel__core": { @@ -2154,12 +3271,6 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, - "@types/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", - "dev": true - }, "@types/q": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", @@ -2196,18 +3307,6 @@ "@types/json-schema": "^7.0.3", "@typescript-eslint/typescript-estree": "1.13.0", "eslint-scope": "^4.0.0" - }, - "dependencies": { - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - } } }, "@typescript-eslint/parser": { @@ -2241,177 +3340,178 @@ } }, "@webassemblyjs/ast": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", "dev": true, "requires": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", - "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", "dev": true }, "@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", - "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", "dev": true }, "@webassemblyjs/helper-code-frame": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", - "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", "dev": true, "requires": { - "@webassemblyjs/wast-printer": "1.9.0" + "@webassemblyjs/wast-printer": "1.8.5" } }, "@webassemblyjs/helper-fsm": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", - "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", "dev": true }, "@webassemblyjs/helper-module-context": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", - "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.9.0" + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" } }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", - "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0" + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" } }, "@webassemblyjs/ieee754": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", - "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", - "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", "dev": true, "requires": { "@xtuc/long": "4.2.2" } }, "@webassemblyjs/utf8": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", - "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", - "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/helper-wasm-section": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-opt": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "@webassemblyjs/wast-printer": "1.9.0" + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" } }, "@webassemblyjs/wasm-gen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", - "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" } }, "@webassemblyjs/wasm-opt": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", - "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0" + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" } }, "@webassemblyjs/wasm-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", - "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" } }, "@webassemblyjs/wast-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", - "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/floating-point-hex-parser": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-code-frame": "1.9.0", - "@webassemblyjs/helper-fsm": "1.9.0", + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/wast-printer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", - "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", "@xtuc/long": "4.2.2" } }, @@ -2444,9 +3544,9 @@ } }, "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", "dev": true }, "acorn-globals": { @@ -2457,6 +3557,14 @@ "requires": { "acorn": "^6.0.1", "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "dev": true + } } }, "acorn-jsx": { @@ -2550,13 +3658,24 @@ "dev": true }, "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } } }, "aproba": { @@ -2672,9 +3791,9 @@ }, "dependencies": { "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -2942,16 +4061,16 @@ } }, "babel-jest": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.2.1.tgz", - "integrity": "sha512-OiBpQGYtV4rWMuFneIaEsqJB0VdoOBw4SqwO4hA2EhDY/O8RylQ20JwALkxv8iv+CYnyrZZfF+DELPgrdrkRIw==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.1.0.tgz", + "integrity": "sha512-tz0VxUhhOE2y+g8R2oFrO/2VtVjA1lkJeavlhExuRBg3LdNJY9gwQ+Vcvqt9+cqy71MCTJhewvTB7Qtnnr9SWg==", "dev": true, "requires": { - "@jest/transform": "^25.2.1", - "@jest/types": "^25.2.1", + "@jest/transform": "^25.1.0", + "@jest/types": "^25.1.0", "@types/babel__core": "^7.1.0", "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^25.2.1", + "babel-preset-jest": "^25.1.0", "chalk": "^3.0.0", "slash": "^3.0.0" }, @@ -3043,23 +4162,23 @@ } }, "babel-plugin-jest-hoist": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.2.1.tgz", - "integrity": "sha512-HysbCQfJhxLlyxDbKcB2ucGYV0LjqK4h6dBoI3RtFuOxTiTWK6XGZMsHb0tGh8iJdV4hC6Z2GCHzVvDeh9i0lQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.1.0.tgz", + "integrity": "sha512-oIsopO41vW4YFZ9yNYoLQATnnN46lp+MZ6H4VvPKFkcc2/fkl3CfE/NZZSmnEIEsJRmJAgkVEK0R7Zbl50CpTw==", "dev": true, "requires": { "@types/babel__traverse": "^7.0.6" } }, "babel-preset-jest": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.2.1.tgz", - "integrity": "sha512-zXHJBM5iR8oEO4cvdF83AQqqJf3tJrXy3x8nfu2Nlqvn4cneg4Ca8M7cQvC5S9BzDDy1O0tZ9iXru9J6E3ym+A==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.1.0.tgz", + "integrity": "sha512-eCGn64olaqwUMaugXsTtGAM2I0QTahjEtnRu0ql8Ie+gDWAc1N6wqN0k2NilnyTunM69Pad7gJY7LOtwLimoFQ==", "dev": true, "requires": { "@babel/plugin-syntax-bigint": "^7.0.0", "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "babel-plugin-jest-hoist": "^25.2.1" + "babel-plugin-jest-hoist": "^25.1.0" } }, "bail": { @@ -3168,16 +4287,6 @@ "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -3337,9 +4446,9 @@ "dev": true }, "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", + "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", "dev": true }, "browser-resolve": { @@ -3431,27 +4540,26 @@ } }, "browserslist": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.11.0.tgz", - "integrity": "sha512-WqEC7Yr5wUH5sg6ruR++v2SGOQYpyUdYYd4tZoAq1F7y+QXoLoYGXVbxhtaIqWmAJjtNTRjVD3HuJc1OXTel2A==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.9.1.tgz", + "integrity": "sha512-Q0DnKq20End3raFulq6Vfp1ecB9fh8yUNV55s8sekaDDeqBaCtWlRHCUdaWyUeSSBJM7IbM6HcsyaeYqgeDhnw==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001035", - "electron-to-chromium": "^1.3.380", - "node-releases": "^1.1.52", - "pkg-up": "^3.1.0" + "caniuse-lite": "^1.0.30001030", + "electron-to-chromium": "^1.3.363", + "node-releases": "^1.1.50" }, "dependencies": { "caniuse-lite": { - "version": "1.0.30001038", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001038.tgz", - "integrity": "sha512-zii9quPo96XfOiRD4TrfYGs+QsGZpb2cGiMAzPjtf/hpFgB6zCPZgJb7I1+EATeMw/o+lG8FyRAnI+CWStHcaQ==", + "version": "1.0.30001032", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001032.tgz", + "integrity": "sha512-8joOm7BwcpEN4BfVHtfh0hBXSAPVYk+eUIcNntGtMkUWy/6AKRCDZINCLe3kB1vHhT2vBxBF85Hh9VlPXi/qjA==", "dev": true }, "electron-to-chromium": { - "version": "1.3.386", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.386.tgz", - "integrity": "sha512-M7JHfp32Bq6Am59AWgglh2d3nqe6y8Y94Vcb/AXUsO3DGvKUHYI5ML9+U5oNShfdOEfurrrjKSoSgFt2mz7mpw==", + "version": "1.3.368", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.368.tgz", + "integrity": "sha512-fqzDipW3p+uDkHUHFPrdW3wINRKcJsbnJwBD7hgaQEQwcuLSvNLw6SeUp5gKDpTbmTl7zri7IZfhsdTUTnygJg==", "dev": true } } @@ -3507,9 +4615,9 @@ "dev": true }, "cacache": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", - "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", + "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", "dev": true, "requires": { "bluebird": "^3.5.5", @@ -3715,609 +4823,37 @@ "upath": "^1.1.1" }, "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" }, "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "requires": { - "remove-trailing-separator": "^1.0.1" + "is-extglob": "^2.1.0" } } } }, - "fsevents": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", - "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1", - "node-pre-gyp": "*" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -4418,52 +4954,40 @@ "dev": true }, "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^4.1.0" } } } @@ -4790,9 +5314,10 @@ "dev": true }, "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", + "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", + "dev": true }, "core-js-compat": { "version": "3.6.4", @@ -4812,12 +5337,6 @@ } } }, - "core-js-pure": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz", - "integrity": "sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==", - "dev": true - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -4848,6 +5367,12 @@ "json-parse-better-errors": "^1.0.1", "lines-and-columns": "^1.1.6" } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true } } }, @@ -5013,36 +5538,12 @@ "dev": true }, "csso": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.3.tgz", - "integrity": "sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.0.2.tgz", + "integrity": "sha512-kS7/oeNVXkHWxby5tHVxlhjizRCSv8QdU7hB2FpdAibDU8FjTAolhNjKNTiLzXtUrKT6HwClE81yXwEk1309wg==", "dev": true, "requires": { - "css-tree": "1.0.0-alpha.39" - }, - "dependencies": { - "css-tree": { - "version": "1.0.0-alpha.39", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz", - "integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==", - "dev": true, - "requires": { - "mdn-data": "2.0.6", - "source-map": "^0.6.1" - } - }, - "mdn-data": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz", - "integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "css-tree": "1.0.0-alpha.37" } }, "cssom": { @@ -5180,12 +5681,6 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, "default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", @@ -5325,9 +5820,9 @@ "dev": true }, "diff-sequences": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.1.tgz", - "integrity": "sha512-foe7dXnGlSh3jR1ovJmdv+77VQj98eKCHHwJPbZ2eEf0fHwKbkZicpPxEch9smZ+n2dnF6QFwkOQdLq9hpeJUg==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.1.0.tgz", + "integrity": "sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw==", "dev": true }, "diffie-hellman": { @@ -5636,6 +6131,14 @@ "acorn": "6.1.1", "caporal": "1.3.0", "glob": "^7.1.2" + }, + "dependencies": { + "acorn": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "dev": true + } } }, "es-to-primitive": { @@ -5791,6 +6294,16 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -5812,18 +6325,18 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", "dev": true, "requires": { "is-glob": "^4.0.1" } }, "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", + "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", "dev": true, "requires": { "type-fest": "^0.8.1" @@ -5836,9 +6349,9 @@ "dev": true }, "inquirer": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", - "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.5.tgz", + "integrity": "sha512-6Z5cP+LAO0rzNE7xWjWtT84jxKa5ScLEGLgegPXeO3dGeU8lNe5Ii7SlXH6KVtLGlDuaEhsvsFjrjWjw8j5lFg==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", @@ -5923,6 +6436,15 @@ "signal-exit": "^3.0.2" } }, + "run-async": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", + "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -6001,31 +6523,31 @@ } }, "eslint-config-airbnb": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.1.0.tgz", - "integrity": "sha512-kZFuQC/MPnH7KJp6v95xsLBf63G/w7YqdPfQ0MUanxQ7zcKUNG8j+sSY860g3NwCBOa62apw16J6pRN+AOgXzw==", + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-18.0.1.tgz", + "integrity": "sha512-hLb/ccvW4grVhvd6CT83bECacc+s4Z3/AEyWQdIT2KeTsG9dR7nx1gs7Iw4tDmGKozCNHFn4yZmRm3Tgy+XxyQ==", "dev": true, "requires": { - "eslint-config-airbnb-base": "^14.1.0", + "eslint-config-airbnb-base": "^14.0.0", "object.assign": "^4.1.0", - "object.entries": "^1.1.1" + "object.entries": "^1.1.0" } }, "eslint-config-airbnb-base": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.1.0.tgz", - "integrity": "sha512-+XCcfGyCnbzOnktDVhwsCAx+9DmrzEmuwxyHUJpw+kqBVT744OUBrB09khgFKlK1lshVww6qXGsYPZpavoNjJw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.0.0.tgz", + "integrity": "sha512-2IDHobw97upExLmsebhtfoD3NAKhV4H0CJWP3Uprd/uk+cHuWYOczPVxQ8PxLFUAw7o3Th1RAU8u1DoUpr+cMA==", "dev": true, "requires": { - "confusing-browser-globals": "^1.0.9", + "confusing-browser-globals": "^1.0.7", "object.assign": "^4.1.0", - "object.entries": "^1.1.1" + "object.entries": "^1.1.0" } }, "eslint-config-prettier": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.1.tgz", - "integrity": "sha512-svTy6zh1ecQojvpbJSgH3aei/Rt7C6i090l5f2WQ4aB05lYHeZIR1qL4wZyyILTbtmnbHP5Yn8MrsOJMGa8RkQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz", + "integrity": "sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg==", "dev": true, "requires": { "get-stdin": "^6.0.0" @@ -6220,9 +6742,9 @@ } }, "eslint-plugin-react": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz", - "integrity": "sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.18.3.tgz", + "integrity": "sha512-Bt56LNHAQCoou88s8ViKRjMB2+36XRejCQ1VoLj716KI1MoE99HpTVvIThJ0rvFmG4E4Gsq+UgToEjn+j044Bg==", "dev": true, "requires": { "array-includes": "^3.1.1", @@ -6233,10 +6755,8 @@ "object.fromentries": "^2.0.2", "object.values": "^1.1.1", "prop-types": "^15.7.2", - "resolve": "^1.15.1", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.2", - "xregexp": "^4.3.0" + "resolve": "^1.14.2", + "string.prototype.matchall": "^4.0.2" }, "dependencies": { "array-includes": { @@ -6260,9 +6780,9 @@ } }, "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -6316,6 +6836,30 @@ "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", "dev": true }, + "object.entries": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", + "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "object.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, "resolve": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", @@ -6325,12 +6869,6 @@ "path-parse": "^1.0.6" } }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, "string.prototype.trimleft": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", @@ -6350,22 +6888,13 @@ "define-properties": "^1.1.3", "function-bind": "^1.1.1" } - }, - "xregexp": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", - "integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==", - "dev": true, - "requires": { - "@babel/runtime-corejs3": "^7.8.3" - } } } }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -6388,22 +6917,14 @@ "dev": true }, "espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.0.tgz", + "integrity": "sha512-Xs8airJ7RQolnDIbLtRutmfvSsAe0xqMMAantCN/GMoqf81TFbeI1T7Jpd56qYu1uuh32dOG5W/X9uO+ghPXzA==", "dev": true, "requires": { - "acorn": "^7.1.1", + "acorn": "^7.1.0", "acorn-jsx": "^5.2.0", "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", - "dev": true - } } }, "esprima": { @@ -6413,20 +6934,12 @@ "dev": true }, "esquery": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.2.0.tgz", - "integrity": "sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz", + "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==", "dev": true, "requires": { - "estraverse": "^5.0.0" - }, - "dependencies": { - "estraverse": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.0.0.tgz", - "integrity": "sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A==", - "dev": true - } + "estraverse": "^4.0.0" } }, "esrecurse": { @@ -6636,17 +7149,17 @@ } }, "expect": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-25.2.1.tgz", - "integrity": "sha512-mRvuu0xujdgYuS0S2dZ489PGAcXl60blmsLofaq7heqn+ZcUOox+VWQvrCee/x+/0WBpxDs7pBWuFYNO5U+txQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-25.1.0.tgz", + "integrity": "sha512-wqHzuoapQkhc3OKPlrpetsfueuEiMf3iWh0R8+duCu9PIjXoP7HgD5aeypwTnXUAjC8aMsiVDaWwlbJ1RlQ38g==", "dev": true, "requires": { - "@jest/types": "^25.2.1", + "@jest/types": "^25.1.0", "ansi-styles": "^4.0.0", - "jest-get-type": "^25.2.1", - "jest-matcher-utils": "^25.2.1", - "jest-message-util": "^25.2.1", - "jest-regex-util": "^25.2.1" + "jest-get-type": "^25.1.0", + "jest-matcher-utils": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-regex-util": "^25.1.0" }, "dependencies": { "ansi-styles": { @@ -6720,6 +7233,12 @@ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7006,9 +7525,9 @@ } }, "figgy-pudding": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", - "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", "dev": true }, "figures": { @@ -7030,13 +7549,6 @@ "flat-cache": "^2.0.1" } }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -7269,19 +7781,560 @@ "readable-stream": "1 || 2" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", - "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", - "dev": true, - "optional": true - }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -7523,9 +8576,9 @@ "optional": true }, "handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", + "integrity": "sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==", "dev": true }, "har-schema": { @@ -7702,9 +8755,9 @@ "dev": true }, "html-escaper": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.1.tgz", - "integrity": "sha512-hNX23TjWwD3q56HpWjUHOKj1+4KKlnjv9PcmBUYKVpga+2cnb9nDx/B1o0yO4n+RZXZdiNxzx6B24C9aNMTkkQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", + "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", "dev": true }, "html-tags": { @@ -7926,73 +8979,13 @@ "dev": true }, "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", "dev": true, "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" } }, "imurmurhash": { @@ -8156,9 +9149,9 @@ }, "dependencies": { "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -8248,12 +9241,6 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -8704,6 +9691,112 @@ "semver": "^6.3.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.6.tgz", + "integrity": "sha512-4bpOR5ZBz+wWcMeVtcf7FbjcFzCp+817z2/gHNncIRcM9MmKzUhtWCYAq27RAfUrAFwb+OCG1s9WEaVxfi6cjg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.6", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.6.tgz", + "integrity": "sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.6.tgz", + "integrity": "sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -8785,14 +9878,14 @@ } }, "jest": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest/-/jest-25.2.1.tgz", - "integrity": "sha512-YXvGtrb4YmfM/JraXaM3jc3NnvhVTHxkdRC9Oof4JtJWwgvIdy0/X01QxeWXqKfCaHmlXi/nKrcPI1+bf0w/Ww==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-25.1.0.tgz", + "integrity": "sha512-FV6jEruneBhokkt9MQk0WUFoNTwnF76CLXtwNMfsc0um0TlB/LG2yxUd0KqaFjEJ9laQmVWQWS0sG/t2GsuI0w==", "dev": true, "requires": { - "@jest/core": "^25.2.1", + "@jest/core": "^25.1.0", "import-local": "^3.0.2", - "jest-cli": "^25.2.1" + "jest-cli": "^25.1.0" }, "dependencies": { "ansi-styles": { @@ -8830,33 +9923,101 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, "jest-cli": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.2.1.tgz", - "integrity": "sha512-7moIaOsKvifiHpCUorpSHb3ALHpyZB9SlrFsvkloEo31KTTgFkHZuQw68LJX8FJwY6pg9LoxJJ2Vy4AFmHMclQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.1.0.tgz", + "integrity": "sha512-p+aOfczzzKdo3AsLJlhs8J5EW6ffVidfSZZxXedJ0mHPBOln1DccqFmGCoO8JWd4xRycfmwy1eoQkMsF8oekPg==", "dev": true, "requires": { - "@jest/core": "^25.2.1", - "@jest/test-result": "^25.2.1", - "@jest/types": "^25.2.1", + "@jest/core": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/types": "^25.1.0", "chalk": "^3.0.0", "exit": "^0.1.2", "import-local": "^3.0.2", "is-ci": "^2.0.0", - "jest-config": "^25.2.1", - "jest-util": "^25.2.1", - "jest-validate": "^25.2.1", + "jest-config": "^25.1.0", + "jest-util": "^25.1.0", + "jest-validate": "^25.1.0", "prompts": "^2.0.1", - "realpath-native": "^2.0.0", - "yargs": "^15.3.1" + "realpath-native": "^1.1.0", + "yargs": "^15.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" } }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -8869,12 +10030,12 @@ } }, "jest-changed-files": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.2.1.tgz", - "integrity": "sha512-BB4XjM/dJNUAUtchZ2yJq50VK8XXbmgvt1MUD6kzgzoyz9F0+1ZDQ1yNvLl6pfDwKrMBG9GBY1lzaIBO3JByMg==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.1.0.tgz", + "integrity": "sha512-bdL1aHjIVy3HaBO3eEQeemGttsq1BDlHgWcOjEOIAcga7OOEGWHD2WSu8HhL7I1F0mFFyci8VKU4tRNk+qtwDA==", "dev": true, "requires": { - "@jest/types": "^25.2.1", + "@jest/types": "^25.1.0", "execa": "^3.2.0", "throat": "^5.0.0" }, @@ -8980,29 +10141,28 @@ } }, "jest-config": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.2.1.tgz", - "integrity": "sha512-Kh6a3stGSCtVwucvD9wSMaEQBmU0CfqVjHvf0X0iLfCrZfsezvV+sGgRWQAidaTIvo51yAaL217xOwEETMqh6w==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.1.0.tgz", + "integrity": "sha512-tLmsg4SZ5H7tuhBC5bOja0HEblM0coS3Wy5LTCb2C8ZV6eWLewHyK+3qSq9Bi29zmWQ7ojdCd3pxpx4l4d2uGw==", "dev": true, "requires": { "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^25.2.1", - "@jest/types": "^25.2.1", - "babel-jest": "^25.2.1", + "@jest/test-sequencer": "^25.1.0", + "@jest/types": "^25.1.0", + "babel-jest": "^25.1.0", "chalk": "^3.0.0", - "deepmerge": "^4.2.2", "glob": "^7.1.1", - "jest-environment-jsdom": "^25.2.1", - "jest-environment-node": "^25.2.1", - "jest-get-type": "^25.2.1", - "jest-jasmine2": "^25.2.1", - "jest-regex-util": "^25.2.1", - "jest-resolve": "^25.2.1", - "jest-util": "^25.2.1", - "jest-validate": "^25.2.1", + "jest-environment-jsdom": "^25.1.0", + "jest-environment-node": "^25.1.0", + "jest-get-type": "^25.1.0", + "jest-jasmine2": "^25.1.0", + "jest-regex-util": "^25.1.0", + "jest-resolve": "^25.1.0", + "jest-util": "^25.1.0", + "jest-validate": "^25.1.0", "micromatch": "^4.0.2", - "pretty-format": "^25.2.1", - "realpath-native": "^2.0.0" + "pretty-format": "^25.1.0", + "realpath-native": "^1.1.0" }, "dependencies": { "ansi-styles": { @@ -9101,15 +10261,15 @@ } }, "jest-diff": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.2.1.tgz", - "integrity": "sha512-e/TU8VLBBGQQS9tXA5B5LeT806jh7CHUeHbBfrU9UvA2zTbOTRz71UD6fAP1HAhzUEyCVLU2ZP5e8X16A9b0Fg==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.1.0.tgz", + "integrity": "sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw==", "dev": true, "requires": { "chalk": "^3.0.0", - "diff-sequences": "^25.2.1", - "jest-get-type": "^25.2.1", - "pretty-format": "^25.2.1" + "diff-sequences": "^25.1.0", + "jest-get-type": "^25.1.0", + "pretty-format": "^25.1.0" }, "dependencies": { "ansi-styles": { @@ -9165,25 +10325,25 @@ } }, "jest-docblock": { - "version": "25.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.2.0.tgz", - "integrity": "sha512-M7ZDbghaxFd2unWkyDFGLZDjPpIbDtEbICXSzwGrUBccFwVG/1dhLLAYX3D+98bFksaJuM0iMZGuIQUzKgnkQw==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.1.0.tgz", + "integrity": "sha512-370P/mh1wzoef6hUKiaMcsPtIapY25suP6JqM70V9RJvdKLrV4GaGbfUseUVk4FZJw4oTZ1qSCJNdrClKt5JQA==", "dev": true, "requires": { "detect-newline": "^3.0.0" } }, "jest-each": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.2.1.tgz", - "integrity": "sha512-2vWAaf11IJsSwkEzGph3un4OMSG4v/3hpM2UqJdeU3peGUgUSn75TlXZGQnT0smbnAr4eE+URW1OpE8U9wl0TA==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.1.0.tgz", + "integrity": "sha512-R9EL8xWzoPySJ5wa0DXFTj7NrzKpRD40Jy+zQDp3Qr/2QmevJgkN9GqioCGtAJ2bW9P/MQRznQHQQhoeAyra7A==", "dev": true, "requires": { - "@jest/types": "^25.2.1", + "@jest/types": "^25.1.0", "chalk": "^3.0.0", - "jest-get-type": "^25.2.1", - "jest-util": "^25.2.1", - "pretty-format": "^25.2.1" + "jest-get-type": "^25.1.0", + "jest-util": "^25.1.0", + "pretty-format": "^25.1.0" }, "dependencies": { "ansi-styles": { @@ -9239,58 +10399,67 @@ } }, "jest-environment-jsdom": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.2.1.tgz", - "integrity": "sha512-bUhhhXtgrOgLhsFQFXgao8CQPYAEwtaVvhsF6O0A7Ie2uPONtAKCwuxyOM9WJaz9ag2ci5Pg7i2V2PRfGLl95w==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.1.0.tgz", + "integrity": "sha512-ILb4wdrwPAOHX6W82GGDUiaXSSOE274ciuov0lztOIymTChKFtC02ddyicRRCdZlB5YSrv3vzr1Z5xjpEe1OHQ==", "dev": true, "requires": { - "@jest/environment": "^25.2.1", - "@jest/fake-timers": "^25.2.1", - "@jest/types": "^25.2.1", - "jest-mock": "^25.2.1", - "jest-util": "^25.2.1", - "jsdom": "^15.2.1" + "@jest/environment": "^25.1.0", + "@jest/fake-timers": "^25.1.0", + "@jest/types": "^25.1.0", + "jest-mock": "^25.1.0", + "jest-util": "^25.1.0", + "jsdom": "^15.1.1" } }, "jest-environment-node": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.2.1.tgz", - "integrity": "sha512-HiAAwx4HrkaV9YAyuI56dmPDuTDckJyPpO0BwCu7+Ht2fmlMDhX13HZyyuIGTAIjUrjJiM3paB8tht+0mXtiIA==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.1.0.tgz", + "integrity": "sha512-U9kFWTtAPvhgYY5upnH9rq8qZkj6mYLup5l1caAjjx9uNnkLHN2xgZy5mo4SyLdmrh/EtB9UPpKFShvfQHD0Iw==", "dev": true, "requires": { - "@jest/environment": "^25.2.1", - "@jest/fake-timers": "^25.2.1", - "@jest/types": "^25.2.1", - "jest-mock": "^25.2.1", - "jest-util": "^25.2.1" + "@jest/environment": "^25.1.0", + "@jest/fake-timers": "^25.1.0", + "@jest/types": "^25.1.0", + "jest-mock": "^25.1.0", + "jest-util": "^25.1.0" } }, "jest-get-type": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.1.tgz", - "integrity": "sha512-EYjTiqcDTCRJDcSNKbLTwn/LcDPEE7ITk8yRMNAOjEsN6yp+Uu+V1gx4djwnuj/DvWg0YGmqaBqPVGsPxlvE7w==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.1.0.tgz", + "integrity": "sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw==", "dev": true }, "jest-haste-map": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.2.1.tgz", - "integrity": "sha512-svz3KbQmv9qeomR0LlRjQfoi7lQbZQkC39m7uHFKhqyEuX4F6DH6HayNPSEbTCZDx6d9/ljxfAcxlPpgQvrpvQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.1.0.tgz", + "integrity": "sha512-/2oYINIdnQZAqyWSn1GTku571aAfs8NxzSErGek65Iu5o8JYb+113bZysRMcC/pjE5v9w0Yz+ldbj9NxrFyPyw==", "dev": true, "requires": { - "@jest/types": "^25.2.1", + "@jest/types": "^25.1.0", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "fsevents": "^2.1.2", "graceful-fs": "^4.2.3", - "jest-serializer": "^25.2.1", - "jest-util": "^25.2.1", - "jest-worker": "^25.2.1", + "jest-serializer": "^25.1.0", + "jest-util": "^25.1.0", + "jest-worker": "^25.1.0", "micromatch": "^4.0.2", "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" + "walker": "^1.0.7" }, "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -9309,6 +10478,13 @@ "to-regex-range": "^5.0.1" } }, + "fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "dev": true, + "optional": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -9333,40 +10509,31 @@ "requires": { "is-number": "^7.0.0" } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } } } }, "jest-jasmine2": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.2.1.tgz", - "integrity": "sha512-He8HdO9jx1LdEaof2vjnvKeJeRPYnn+zXz32X8Z/iOUgAgmP7iZActUkiCCiTazSZlaGlY1iK+LOrqnpGQ0+UA==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.1.0.tgz", + "integrity": "sha512-GdncRq7jJ7sNIQ+dnXvpKO2MyP6j3naNK41DTTjEAhLEdpImaDA9zSAZwDhijjSF/D7cf4O5fdyUApGBZleaEg==", "dev": true, "requires": { "@babel/traverse": "^7.1.0", - "@jest/environment": "^25.2.1", - "@jest/source-map": "^25.2.1", - "@jest/test-result": "^25.2.1", - "@jest/types": "^25.2.1", + "@jest/environment": "^25.1.0", + "@jest/source-map": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/types": "^25.1.0", "chalk": "^3.0.0", "co": "^4.6.0", - "expect": "^25.2.1", + "expect": "^25.1.0", "is-generator-fn": "^2.0.0", - "jest-each": "^25.2.1", - "jest-matcher-utils": "^25.2.1", - "jest-message-util": "^25.2.1", - "jest-runtime": "^25.2.1", - "jest-snapshot": "^25.2.1", - "jest-util": "^25.2.1", - "pretty-format": "^25.2.1", + "jest-each": "^25.1.0", + "jest-matcher-utils": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-runtime": "^25.1.0", + "jest-snapshot": "^25.1.0", + "jest-util": "^25.1.0", + "pretty-format": "^25.1.0", "throat": "^5.0.0" }, "dependencies": { @@ -9423,25 +10590,25 @@ } }, "jest-leak-detector": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.2.1.tgz", - "integrity": "sha512-bsxjjFksjLWNqC8aLsN0KO2KQ3tiqPqmFpYt+0y4RLHc1dqaThQL68jra5y1f/yhX3dNC8ugksDvqnGxwxjo4w==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.1.0.tgz", + "integrity": "sha512-3xRI264dnhGaMHRvkFyEKpDeaRzcEBhyNrOG5oT8xPxOyUAblIAQnpiR3QXu4wDor47MDTiHbiFcbypdLcLW5w==", "dev": true, "requires": { - "jest-get-type": "^25.2.1", - "pretty-format": "^25.2.1" + "jest-get-type": "^25.1.0", + "pretty-format": "^25.1.0" } }, "jest-matcher-utils": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.2.1.tgz", - "integrity": "sha512-uuoYY8W6eeVxHUEOvrKIVVTl9X6RP+ohQn2Ta2W8OOLMN6oA8pZUKQEPGxLsSqB3RKfpTueurMLrxDTEZGllsA==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.1.0.tgz", + "integrity": "sha512-KGOAFcSFbclXIFE7bS4C53iYobKI20ZWleAdAFun4W1Wz1Kkej8Ng6RRbhL8leaEvIOjGXhGf/a1JjO8bkxIWQ==", "dev": true, "requires": { "chalk": "^3.0.0", - "jest-diff": "^25.2.1", - "jest-get-type": "^25.2.1", - "pretty-format": "^25.2.1" + "jest-diff": "^25.1.0", + "jest-get-type": "^25.1.0", + "pretty-format": "^25.1.0" }, "dependencies": { "ansi-styles": { @@ -9497,14 +10664,14 @@ } }, "jest-message-util": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.2.1.tgz", - "integrity": "sha512-pxwehr9uPEuCI9bPjBiZxpFMN0+3wny5p7/E3hbV9XjsqREhJJAMf0czvHtgNeUBo2iAiAI9yi9ICKHPOKePEw==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.1.0.tgz", + "integrity": "sha512-Nr/Iwar2COfN22aCqX0kCVbXgn8IBm9nWf4xwGr5Olv/KZh0CZ32RKgZWMVDXGdOahicM10/fgjdimGNX/ttCQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@jest/test-result": "^25.2.1", - "@jest/types": "^25.2.1", + "@jest/test-result": "^25.1.0", + "@jest/types": "^25.1.0", "@types/stack-utils": "^1.0.1", "chalk": "^3.0.0", "micromatch": "^4.0.2", @@ -9608,12 +10775,12 @@ } }, "jest-mock": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.2.1.tgz", - "integrity": "sha512-ZXcmqpCTG1MEm2AP2q9XiJzdbQ655Pnssj+xQMP1thrW2ptEFrd4vSkxTpxk6rnluLPRKagaHmzUpWNxShMvqQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.1.0.tgz", + "integrity": "sha512-28/u0sqS+42vIfcd1mlcg4ZVDmSUYuNvImP4X2lX5hRMLW+CN0BeiKVD4p+ujKKbSPKd3rg/zuhCF+QBLJ4vag==", "dev": true, "requires": { - "@jest/types": "^25.2.1" + "@jest/types": "^25.1.0" } }, "jest-pnp-resolver": { @@ -9623,23 +10790,22 @@ "dev": true }, "jest-regex-util": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.1.tgz", - "integrity": "sha512-wroFVJw62LdqTdkL508ZLV82FrJJWVJMIuYG7q4Uunl1WAPTf4ftPKrqqfec4SvOIlvRZUdEX2TFpWR356YG/w==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.1.0.tgz", + "integrity": "sha512-9lShaDmDpqwg+xAd73zHydKrBbbrIi08Kk9YryBEBybQFg/lBWR/2BDjjiSE7KIppM9C5+c03XiDaZ+m4Pgs1w==", "dev": true }, "jest-resolve": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.2.1.tgz", - "integrity": "sha512-5rVc6khEckNH62adcR+jlYd34/jBO/U22VHf+elmyO6UBHNWXSbfy63+spJRN4GQ/0dbu6Hi6ZVdR58bmNG2Eg==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.1.0.tgz", + "integrity": "sha512-XkBQaU1SRCHj2Evz2Lu4Czs+uIgJXWypfO57L7JYccmAXv4slXA6hzNblmcRmf7P3cQ1mE7fL3ABV6jAwk4foQ==", "dev": true, "requires": { - "@jest/types": "^25.2.1", + "@jest/types": "^25.1.0", "browser-resolve": "^1.11.3", "chalk": "^3.0.0", "jest-pnp-resolver": "^1.2.1", - "realpath-native": "^2.0.0", - "resolve": "^1.15.1" + "realpath-native": "^1.1.0" }, "dependencies": { "ansi-styles": { @@ -9683,15 +10849,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "resolve": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", - "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -9704,39 +10861,39 @@ } }, "jest-resolve-dependencies": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.2.1.tgz", - "integrity": "sha512-fnct/NyrBpBAVUIMa0M876ubufHP/2Rrc038+gCpVT1s7kazV7ZPFlmGfInahCIthbMr644uzt6pnSvmQgTPGg==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.1.0.tgz", + "integrity": "sha512-Cu/Je38GSsccNy4I2vL12ZnBlD170x2Oh1devzuM9TLH5rrnLW1x51lN8kpZLYTvzx9j+77Y5pqBaTqfdzVzrw==", "dev": true, "requires": { - "@jest/types": "^25.2.1", - "jest-regex-util": "^25.2.1", - "jest-snapshot": "^25.2.1" + "@jest/types": "^25.1.0", + "jest-regex-util": "^25.1.0", + "jest-snapshot": "^25.1.0" } }, "jest-runner": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.2.1.tgz", - "integrity": "sha512-eONqmMQ2vvKh9BsmJmPmv22DqezFSnwX97rj0L5LvPxQNXTz+rpx7nWiKA7xlvOykLFcspw6worK3+AzhwHWhQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.1.0.tgz", + "integrity": "sha512-su3O5fy0ehwgt+e8Wy7A8CaxxAOCMzL4gUBftSs0Ip32S0epxyZPDov9Znvkl1nhVOJNf4UwAsnqfc3plfQH9w==", "dev": true, "requires": { - "@jest/console": "^25.2.1", - "@jest/environment": "^25.2.1", - "@jest/test-result": "^25.2.1", - "@jest/types": "^25.2.1", + "@jest/console": "^25.1.0", + "@jest/environment": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/types": "^25.1.0", "chalk": "^3.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.3", - "jest-config": "^25.2.1", - "jest-docblock": "^25.2.0", - "jest-haste-map": "^25.2.1", - "jest-jasmine2": "^25.2.1", - "jest-leak-detector": "^25.2.1", - "jest-message-util": "^25.2.1", - "jest-resolve": "^25.2.1", - "jest-runtime": "^25.2.1", - "jest-util": "^25.2.1", - "jest-worker": "^25.2.1", + "jest-config": "^25.1.0", + "jest-docblock": "^25.1.0", + "jest-haste-map": "^25.1.0", + "jest-jasmine2": "^25.1.0", + "jest-leak-detector": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-resolve": "^25.1.0", + "jest-runtime": "^25.1.0", + "jest-util": "^25.1.0", + "jest-worker": "^25.1.0", "source-map-support": "^0.5.6", "throat": "^5.0.0" }, @@ -9794,36 +10951,36 @@ } }, "jest-runtime": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.2.1.tgz", - "integrity": "sha512-eHEnMrOVeGe8mGDoTZWqCdbsM3RwxsMKVMAj1RTZ4LtRyWqQHKec3RiJiJST5LVj3Mw72clr0U21yoE4p5Mq3w==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.1.0.tgz", + "integrity": "sha512-mpPYYEdbExKBIBB16ryF6FLZTc1Rbk9Nx0ryIpIMiDDkOeGa0jQOKVI/QeGvVGlunKKm62ywcioeFVzIbK03bA==", "dev": true, "requires": { - "@jest/console": "^25.2.1", - "@jest/environment": "^25.2.1", - "@jest/source-map": "^25.2.1", - "@jest/test-result": "^25.2.1", - "@jest/transform": "^25.2.1", - "@jest/types": "^25.2.1", + "@jest/console": "^25.1.0", + "@jest/environment": "^25.1.0", + "@jest/source-map": "^25.1.0", + "@jest/test-result": "^25.1.0", + "@jest/transform": "^25.1.0", + "@jest/types": "^25.1.0", "@types/yargs": "^15.0.0", "chalk": "^3.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.3", - "jest-config": "^25.2.1", - "jest-haste-map": "^25.2.1", - "jest-message-util": "^25.2.1", - "jest-mock": "^25.2.1", - "jest-regex-util": "^25.2.1", - "jest-resolve": "^25.2.1", - "jest-snapshot": "^25.2.1", - "jest-util": "^25.2.1", - "jest-validate": "^25.2.1", - "realpath-native": "^2.0.0", + "jest-config": "^25.1.0", + "jest-haste-map": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-mock": "^25.1.0", + "jest-regex-util": "^25.1.0", + "jest-resolve": "^25.1.0", + "jest-snapshot": "^25.1.0", + "jest-util": "^25.1.0", + "jest-validate": "^25.1.0", + "realpath-native": "^1.1.0", "slash": "^3.0.0", "strip-bom": "^4.0.0", - "yargs": "^15.3.1" + "yargs": "^15.0.0" }, "dependencies": { "ansi-styles": { @@ -9885,31 +11042,30 @@ } }, "jest-serializer": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.2.1.tgz", - "integrity": "sha512-fibDi7M5ffx6c/P66IkvR4FKkjG5ldePAK1WlbNoaU4GZmIAkS9Le/frAwRUFEX0KdnisSPWf+b1RC5jU7EYJQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.1.0.tgz", + "integrity": "sha512-20Wkq5j7o84kssBwvyuJ7Xhn7hdPeTXndnwIblKDR2/sy1SUm6rWWiG9kSCgJPIfkDScJCIsTtOKdlzfIHOfKA==", "dev": true }, "jest-snapshot": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.2.1.tgz", - "integrity": "sha512-5Wd8SEJVTXqQvzkQpuYqQt1QTlRj2XVUV/iaEzO+AeSVg6g5pQWu0z2iLdSBlVeWRrX0MyZn6dhxYGwEq4wW0w==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.1.0.tgz", + "integrity": "sha512-xZ73dFYN8b/+X2hKLXz4VpBZGIAn7muD/DAg+pXtDzDGw3iIV10jM7WiHqhCcpDZfGiKEj7/2HXAEPtHTj0P2A==", "dev": true, "requires": { "@babel/types": "^7.0.0", - "@jest/types": "^25.2.1", - "@types/prettier": "^1.19.0", + "@jest/types": "^25.1.0", "chalk": "^3.0.0", - "expect": "^25.2.1", - "jest-diff": "^25.2.1", - "jest-get-type": "^25.2.1", - "jest-matcher-utils": "^25.2.1", - "jest-message-util": "^25.2.1", - "jest-resolve": "^25.2.1", - "make-dir": "^3.0.0", + "expect": "^25.1.0", + "jest-diff": "^25.1.0", + "jest-get-type": "^25.1.0", + "jest-matcher-utils": "^25.1.0", + "jest-message-util": "^25.1.0", + "jest-resolve": "^25.1.0", + "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "pretty-format": "^25.2.1", - "semver": "^6.3.0" + "pretty-format": "^25.1.0", + "semver": "^7.1.1" }, "dependencies": { "ansi-styles": { @@ -9952,20 +11108,11 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true - }, - "make-dir": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", - "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + }, + "semver": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz", + "integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==", "dev": true }, "supports-color": { @@ -9980,15 +11127,15 @@ } }, "jest-util": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.2.1.tgz", - "integrity": "sha512-oFVMSY/7flrSgEE/B+RApaBZOdLURXRnXCf4COV5td9uRidxudyjA64I1xk2h9Pf3jloSArm96e2FKAbFs0DYg==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.1.0.tgz", + "integrity": "sha512-7did6pLQ++87Qsj26Fs/TIwZMUFBXQ+4XXSodRNy3luch2DnRXsSnmpVtxxQ0Yd6WTipGpbhh2IFP1mq6/fQGw==", "dev": true, "requires": { - "@jest/types": "^25.2.1", + "@jest/types": "^25.1.0", "chalk": "^3.0.0", "is-ci": "^2.0.0", - "make-dir": "^3.0.0" + "mkdirp": "^0.5.1" }, "dependencies": { "ansi-styles": { @@ -10032,21 +11179,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "make-dir": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", - "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -10059,17 +11191,17 @@ } }, "jest-validate": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.2.1.tgz", - "integrity": "sha512-vGtNFPyvylFfTFFfptzqCy5S3cP/N5JJVwm8gsXeZq8jMmvUngfWtuw+Tr5Wjo+dqOle23td8BE0ruGnsONDmw==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.1.0.tgz", + "integrity": "sha512-kGbZq1f02/zVO2+t1KQGSVoCTERc5XeObLwITqC6BTRH3Adv7NZdYqCpKIZLUgpLXf2yISzQ465qOZpul8abXA==", "dev": true, "requires": { - "@jest/types": "^25.2.1", + "@jest/types": "^25.1.0", "camelcase": "^5.3.1", "chalk": "^3.0.0", - "jest-get-type": "^25.2.1", + "jest-get-type": "^25.1.0", "leven": "^3.1.0", - "pretty-format": "^25.2.1" + "pretty-format": "^25.1.0" }, "dependencies": { "ansi-styles": { @@ -10125,16 +11257,16 @@ } }, "jest-watcher": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.2.1.tgz", - "integrity": "sha512-m35rftCYE2EEh01+IIpQMpdB9VXBAjITZvgP4drd/LI3JEJIdd0Pkf/qJZ3oiMQJdqmuwYcTqE+BL40MxVv83Q==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.1.0.tgz", + "integrity": "sha512-Q9eZ7pyaIr6xfU24OeTg4z1fUqBF/4MP6J801lyQfg7CsnZ/TCzAPvCfckKdL5dlBBEKBeHV0AdyjFZ5eWj4ig==", "dev": true, "requires": { - "@jest/test-result": "^25.2.1", - "@jest/types": "^25.2.1", + "@jest/test-result": "^25.1.0", + "@jest/types": "^25.1.0", "ansi-escapes": "^4.2.1", "chalk": "^3.0.0", - "jest-util": "^25.2.1", + "jest-util": "^25.1.0", "string-length": "^3.1.0" }, "dependencies": { @@ -10206,9 +11338,9 @@ } }, "jest-worker": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.2.1.tgz", - "integrity": "sha512-IHnpekk8H/hCUbBlfeaPZzU6v75bqwJp3n4dUrQuQOAgOneI4tx3jV2o8pvlXnDfcRsfkFIUD//HWXpCmR+evQ==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.1.0.tgz", + "integrity": "sha512-ZHhHtlxOWSxCoNOKHGbiLzXnl42ga9CxDr27H36Qn+15pQZd3R/F24jrmjDelw9j/iHUIWMWs08/u2QN50HHOg==", "dev": true, "requires": { "merge-stream": "^2.0.0", @@ -10291,14 +11423,6 @@ "whatwg-url": "^7.0.0", "ws": "^7.0.0", "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", - "dev": true - } } }, "jsesc": { @@ -10344,20 +11468,12 @@ "dev": true }, "json5": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.2.tgz", - "integrity": "sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", "dev": true, "requires": { - "minimist": "^1.2.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } + "minimist": "^1.2.0" } }, "jsprim": { @@ -10610,15 +11726,6 @@ "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", "dev": true }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -10793,9 +11900,9 @@ } }, "loglevel": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz", - "integrity": "sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.6.tgz", + "integrity": "sha512-Sgr5lbboAUBo3eXCSPL4/KoVz3ROKquOjcctxmHIt+vol2DrqTQe3SwkKKuYhEiWB5kYa13YyopJ69deJ1irzQ==", "dev": true }, "loglevel-colored-level-prefix": { @@ -10933,6 +12040,12 @@ "tmpl": "1.0.x" } }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -11019,17 +12132,6 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -11625,9 +12727,9 @@ } }, "node-releases": { - "version": "1.1.52", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.52.tgz", - "integrity": "sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ==", + "version": "1.1.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.50.tgz", + "integrity": "sha512-lgAmPv9eYZ0bGwUYAKlr8MG6K4CvWliWqnkcT2P8mMAgVrH3lqfBPorFlxiG1pHQnqmavJZ9vbMXUTNyMLbrgQ==", "dev": true, "requires": { "semver": "^6.3.0" @@ -11866,9 +12968,9 @@ }, "dependencies": { "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -11957,9 +13059,9 @@ }, "dependencies": { "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -12046,9 +13148,9 @@ }, "dependencies": { "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -12144,94 +13246,15 @@ } }, "object.values": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", - "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", + "es-abstract": "^1.12.0", "function-bind": "^1.1.1", "has": "^1.0.3" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - } } }, "obuf": { @@ -12314,17 +13337,6 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-shim": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", @@ -12561,10 +13573,21 @@ "dev": true }, "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } }, "pbkdf2": { "version": "3.0.17", @@ -12592,9 +13615,9 @@ "dev": true }, "pidtree": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", - "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", "dev": true }, "pify": { @@ -12661,15 +13684,6 @@ "find-up": "^3.0.0" } }, - "pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -12860,9 +13874,9 @@ } }, "postcss-modules-scope": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", - "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.1.tgz", + "integrity": "sha512-OXRUPecnHCg8b9xWvldG/jUpRIGPNRka0r4D4j0ESUU2/5IOnpsjfPPmDprM3Ih8CgZ8FXjWqaniK5v4rWt3oQ==", "dev": true, "requires": { "postcss": "^7.0.6", @@ -13063,6 +14077,12 @@ "vue-eslint-parser": "^2.0.2" }, "dependencies": { + "acorn": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "dev": true + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -13084,12 +14104,6 @@ "restore-cursor": "^2.0.0" } }, - "core-js": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", - "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", - "dev": true - }, "eslint": { "version": "5.16.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", @@ -13134,16 +14148,6 @@ "text-table": "^0.2.0" } }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, "espree": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", @@ -13298,6 +14302,12 @@ "yargs": "^13.2.4" }, "dependencies": { + "acorn": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "dev": true + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -13317,9 +14327,9 @@ "dev": true }, "camelcase-keys": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.1.tgz", - "integrity": "sha512-BPCNVH56RVIxQQIXskp5tLQXUNGQ6sXr7iCv1FHDt81xBOQ/1r6H8SPxf19InVP6DexWar4s87q9thfuk8X9HA==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.1.2.tgz", + "integrity": "sha512-QfFrU0CIw2oltVvpndW32kuJ/9YOJwUnmWrjlXt1nnJZHCaS9i6bfOpg9R4Lw8aZjStkJWM+jc0cdXjWBgVJSw==", "dev": true, "requires": { "camelcase": "^5.3.1", @@ -13336,45 +14346,6 @@ "restore-cursor": "^2.0.0" } }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "core-js": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz", - "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", - "dev": true - }, "eslint": { "version": "5.16.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", @@ -13427,16 +14398,6 @@ } } }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, "espree": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", @@ -13592,49 +14553,16 @@ "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" } }, "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", "dev": true, "requires": { "cliui": "^5.0.0", @@ -13646,7 +14574,7 @@ "string-width": "^3.0.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" + "yargs-parser": "^13.1.1" }, "dependencies": { "find-up": { @@ -13704,16 +14632,6 @@ } } } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -13867,12 +14785,12 @@ } }, "pretty-format": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.2.1.tgz", - "integrity": "sha512-YS+e9oGYIbEeAFgqTU8qeZ3DN2Pz0iaD81ox+iUjLIXVJWeB7Ro/2AnfxRnl/yJJ5R674d7E3jLPuh6bwg0+qw==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.1.0.tgz", + "integrity": "sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ==", "dev": true, "requires": { - "@jest/types": "^25.2.1", + "@jest/types": "^25.1.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^16.12.0" @@ -13910,9 +14828,9 @@ "dev": true }, "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "version": "16.13.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz", + "integrity": "sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==", "dev": true } } @@ -13958,9 +14876,9 @@ "dev": true }, "prompts": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", - "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.1.tgz", + "integrity": "sha512-qIP2lQyCwYbdzcqHIUi2HAxiWixhoM9OdLCWf8txXsapC/X9YdsCoeyRIXE/GP+Q0J37Q7+XN/MFqbUa7IzXNA==", "dev": true, "requires": { "kleur": "^3.0.3", @@ -14000,9 +14918,9 @@ "dev": true }, "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", "dev": true }, "public-encrypt": { @@ -14239,23 +15157,6 @@ "load-json-file": "^2.0.0", "normalize-package-data": "^2.3.2", "path-type": "^2.0.0" - }, - "dependencies": { - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } } }, "read-pkg-up": { @@ -14314,9 +15215,9 @@ } }, "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -14340,10 +15241,13 @@ } }, "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "requires": { + "util.promisify": "^1.0.0" + } }, "rechoir": { "version": "0.7.0", @@ -14413,9 +15317,9 @@ "dev": true }, "regenerate-unicode-properties": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", - "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", + "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", "dev": true, "requires": { "regenerate": "^1.4.0" @@ -14427,9 +15331,9 @@ "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" }, "regenerator-transform": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.4.tgz", - "integrity": "sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.2.tgz", + "integrity": "sha512-V4+lGplCM/ikqi5/mkkpJ06e9Bujq1NFmNLvsCs56zg3ZbzrnUzAtizZ24TXxtRX/W2jcdScwQCnbL0CICTFkQ==", "dev": true, "requires": { "@babel/runtime": "^7.8.4", @@ -14437,18 +15341,18 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz", - "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==", + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.7.tgz", + "integrity": "sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg==", "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } }, "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.4.tgz", + "integrity": "sha512-plpwicqEzfEyTQohIKktWigcLzmNStMGwbOUbykx51/29Z3JOGYldaaNGK7ngNXV+UcoqvIMmloZ48Sr74sd+g==", "dev": true } } @@ -14483,9 +15387,9 @@ }, "dependencies": { "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -14568,17 +15472,17 @@ "dev": true }, "regexpu-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", - "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", + "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", "dev": true, "requires": { "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.2.0", - "regjsgen": "^0.5.1", - "regjsparser": "^0.6.4", + "regenerate-unicode-properties": "^8.1.0", + "regjsgen": "^0.5.0", + "regjsparser": "^0.6.0", "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.2.0" + "unicode-match-property-value-ecmascript": "^1.1.0" } }, "registry-auth-token": { @@ -14607,9 +15511,9 @@ "dev": true }, "regjsparser": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", - "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.3.tgz", + "integrity": "sha512-8uZvYbnfAtEm9Ab8NTb3hdLwL4g/LQzEYP7Xs27T96abJCCE2d6r3cPZPQEsLKy0vRSGVNG+/zVGtLr86HQduA==", "dev": true, "requires": { "jsesc": "~0.5.0" @@ -14957,9 +15861,9 @@ "dev": true }, "run-async": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", - "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", "dev": true, "requires": { "is-promise": "^2.1.0" @@ -15031,27 +15935,6 @@ "micromatch": "^3.1.4", "minimist": "^1.1.1", "walker": "~1.0.5" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } } }, "sax": { @@ -15070,42 +15953,22 @@ } }, "scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.0.tgz", + "integrity": "sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } }, "schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz", + "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==", "dev": true, "requires": { - "ajv": "^6.12.0", + "ajv": "^6.10.2", "ajv-keywords": "^3.4.1" - }, - "dependencies": { - "ajv": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", - "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true - } } }, "select-hose": { @@ -15377,9 +16240,9 @@ }, "dependencies": { "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -15462,9 +16325,9 @@ "dev": true }, "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.4.tgz", + "integrity": "sha512-/ekMoM4NJ59ivGSfKapeG+FWtrmWvA1p6FBZwXrqojw90vJu8lBmrTxCMuBCydKtkaUe2zt4PlxeTKpjwMbyig==", "dev": true }, "slash": { @@ -15971,9 +16834,9 @@ }, "dependencies": { "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -16049,14 +16912,216 @@ } } }, - "string.prototype.padend": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz", - "integrity": "sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA==", + "string.prototype.padend": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz", + "integrity": "sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" + } + }, + "string.prototype.trimright": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + } + } + } + }, + "string.prototype.trimend": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.0.tgz", + "integrity": "sha512-EEJnGqa/xNfIg05SxiPSqRS7S9qwDhYts1TSLR1BQfYUfPe1stofgGKvwERK9+9yf+PpfBMlpBaCHucXGPQfUA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", + "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "string.prototype.trimleft": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" + } + }, + "string.prototype.trimright": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" + } + } + } + }, + "string.prototype.trimleft": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", + "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", + "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.0.tgz", + "integrity": "sha512-iCP8g01NFYiiBOnwG1Xc3WZLyoo+RuBymwIlWncShXDDJYWN6DbnM3odslBJdgCdRlq94B5s63NWAZlcn2CS4w==", "dev": true, "requires": { "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" + "es-abstract": "^1.17.5" }, "dependencies": { "es-abstract": { @@ -16117,47 +17182,29 @@ "dev": true }, "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", + "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" } }, "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", + "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" } } } }, - "string.prototype.trimleft": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", - "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", - "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -16971,9 +18018,9 @@ } }, "terser": { - "version": "4.6.7", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.7.tgz", - "integrity": "sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.4.tgz", + "integrity": "sha512-5fqgBPLgVHZ/fVvqRhhUp9YUiGXhFJ9ZkrZWD9vQtFBR4QIGTnbsb+/kKqSqfgp3WnBwGWAFnedGTtmX1YTn0w==", "dev": true, "requires": { "commander": "^2.20.0", @@ -17233,9 +18280,9 @@ "dev": true }, "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, "tsscmp": { @@ -17344,15 +18391,15 @@ } }, "unicode-match-property-value-ecmascript": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", - "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", + "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", "dev": true }, "unicode-property-aliases-ecmascript": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", - "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", + "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", "dev": true }, "unified": { @@ -17655,9 +18702,9 @@ }, "dependencies": { "es-abstract": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", - "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", "dev": true, "requires": { "es-to-primitive": "^1.2.1", @@ -17850,9 +18897,9 @@ }, "dependencies": { "acorn": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", - "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", "dev": true }, "acorn-jsx": { @@ -17904,12 +18951,12 @@ } }, "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", "dev": true, "requires": { - "browser-process-hrtime": "^1.0.0" + "browser-process-hrtime": "^0.1.2" } }, "w3c-xmlserializer": { @@ -17959,15 +19006,15 @@ "dev": true }, "webpack": { - "version": "4.42.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.1.tgz", - "integrity": "sha512-SGfYMigqEfdGchGhFFJ9KyRpQKnipvEvjc1TwrXEPCM6H5Wywu10ka8o3KGrMzSMxMQKt8aCHUFh5DaQ9UmyRg==", + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.0.tgz", + "integrity": "sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", "acorn": "^6.2.1", "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", @@ -17979,7 +19026,7 @@ "loader-utils": "^1.2.3", "memory-fs": "^0.4.1", "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", + "mkdirp": "^0.5.1", "neo-async": "^2.6.1", "node-libs-browser": "^2.2.1", "schema-utils": "^1.0.0", @@ -17990,36 +19037,11 @@ }, "dependencies": { "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", + "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", "dev": true }, - "mkdirp": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", - "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -18058,17 +19080,6 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, "enhanced-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", @@ -18080,14 +19091,47 @@ "tapable": "^1.0.0" } }, - "import-local": { + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "lcid": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", "dev": true, "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" + "invert-kv": "^2.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" } }, "string-width": { @@ -18119,17 +19163,6 @@ "has-flag": "^3.0.0" } }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } - }, "yargs": { "version": "13.2.4", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", @@ -18148,16 +19181,6 @@ "y18n": "^4.0.0", "yargs-parser": "^13.1.0" } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -18263,15 +19286,11 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, - "import-local": { + "invert-kv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - } + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -18282,6 +19301,43 @@ "number-is-nan": "^1.0.0" } }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -18981,77 +20037,40 @@ } }, "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "dev": true, "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" }, "dependencies": { "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^4.1.0" } } } @@ -19083,9 +20102,9 @@ } }, "ws": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz", - "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz", + "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==", "dev": true }, "x-is-string": { @@ -19140,35 +20159,18 @@ "dev": true }, "yaml": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.8.3.tgz", - "integrity": "sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.7.2.tgz", + "integrity": "sha512-qXROVp90sb83XtAoqE8bP9RwAkTTZbugRUTm5YeFCBfNRPEp2YzTeqWiz7m5OORHzEvrA/qcGS8hp/E+MMROYw==", "dev": true, "requires": { - "@babel/runtime": "^7.8.7" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz", - "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "regenerator-runtime": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", - "dev": true - } + "@babel/runtime": "^7.6.3" } }, "yargs": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", - "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", + "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==", "dev": true, "requires": { "cliui": "^6.0.0", @@ -19181,7 +20183,7 @@ "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^18.1.1" + "yargs-parser": "^16.1.0" }, "dependencies": { "ansi-regex": { @@ -19190,6 +20192,42 @@ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -19255,13 +20293,34 @@ "requires": { "ansi-regex": "^5.0.0" } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs-parser": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", + "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } } } }, "yargs-parser": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.1.tgz", - "integrity": "sha512-KRHEsOM16IX7XuLnMOqImcPNbLVXMNHYAoFc3BKR8Ortl5gzDbtXvvEoGx9imk5E+X1VeNKNlcHr8B8vi+7ipA==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "dev": true, "requires": { "camelcase": "^5.0.0", diff --git a/dash-renderer/package.json b/dash-renderer/package.json index 6e1d57f94a..f954ce49d7 100644 --- a/dash-renderer/package.json +++ b/dash-renderer/package.json @@ -24,8 +24,8 @@ "@babel/polyfill": "7.8.7", "cookie": "^0.4.0", "dependency-graph": "^0.9.0", - "prop-types": "15.7.2", "fast-isnumeric": "^1.1.3", + "prop-types": "15.7.2", "radium": "^0.26.0", "ramda": "^0.27.0", "react": "16.13.0", From 883a6ef80ad42f0ba0ec640a95deb66189bf031f Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 1 Apr 2020 03:21:49 -0400 Subject: [PATCH 63/81] rearrange APIController --- dash-renderer/src/APIController.react.js | 137 +++++++++++------------ 1 file changed, 66 insertions(+), 71 deletions(-) diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index d19748f76e..23e5f34c01 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -34,77 +34,7 @@ const UnconnectedContainer = props => { } const renderedTree = useRef(false); - useEffect(() => { - const { - appLifecycle, - dependenciesRequest, - dispatch, - error, - graphs, - layout, - layoutRequest, - } = props; - - if (isEmpty(layoutRequest)) { - dispatch(apiThunk('_dash-layout', 'GET', 'layoutRequest')); - } else if (layoutRequest.status === STATUS.OK) { - if (isEmpty(layout)) { - const finalLayout = applyPersistence( - layoutRequest.content, - dispatch - ); - dispatch( - setPaths( - computePaths(finalLayout, [], null, events.current) - ) - ); - dispatch(setLayout(finalLayout)); - } - } - - if (isEmpty(dependenciesRequest)) { - dispatch( - apiThunk('_dash-dependencies', 'GET', 'dependenciesRequest') - ); - } else if ( - dependenciesRequest.status === STATUS.OK && - isEmpty(graphs) - ) { - dispatch( - setGraphs( - computeGraphs( - dependenciesRequest.content, - dispatchError(dispatch) - ) - ) - ); - } - - if ( - // dependenciesRequest and its computed stores - dependenciesRequest.status === STATUS.OK && - !isEmpty(graphs) && - // LayoutRequest and its computed stores - layoutRequest.status === STATUS.OK && - !isEmpty(layout) && - // Hasn't already hydrated - appLifecycle === getAppState('STARTED') - ) { - let hasError = false; - try { - dispatch(hydrateInitialOutputs(dispatchError(dispatch))); - } catch (err) { - // Display this error in devtools, unless we have errors - // already, in which case we assume this new one is moot - if (!error.frontEnd.length && !error.backEnd.length) { - dispatch(onError({type: 'backEnd', error: err})); - } - hasError = true; - } finally { - setErrorLoading(hasError); - } - } - }); + useEffect(storeEffect.bind(null, props, events, setErrorLoading)); useEffect(() => { if (renderedTree.current) { @@ -152,6 +82,71 @@ const UnconnectedContainer = props => { ); }; +function storeEffect(props, events, setErrorLoading) { + const { + appLifecycle, + dependenciesRequest, + dispatch, + error, + graphs, + layout, + layoutRequest, + } = props; + + if (isEmpty(layoutRequest)) { + dispatch(apiThunk('_dash-layout', 'GET', 'layoutRequest')); + } else if (layoutRequest.status === STATUS.OK) { + if (isEmpty(layout)) { + const finalLayout = applyPersistence( + layoutRequest.content, + dispatch + ); + dispatch( + setPaths(computePaths(finalLayout, [], null, events.current)) + ); + dispatch(setLayout(finalLayout)); + } + } + + if (isEmpty(dependenciesRequest)) { + dispatch(apiThunk('_dash-dependencies', 'GET', 'dependenciesRequest')); + } else if (dependenciesRequest.status === STATUS.OK && isEmpty(graphs)) { + dispatch( + setGraphs( + computeGraphs( + dependenciesRequest.content, + dispatchError(dispatch) + ) + ) + ); + } + + if ( + // dependenciesRequest and its computed stores + dependenciesRequest.status === STATUS.OK && + !isEmpty(graphs) && + // LayoutRequest and its computed stores + layoutRequest.status === STATUS.OK && + !isEmpty(layout) && + // Hasn't already hydrated + appLifecycle === getAppState('STARTED') + ) { + let hasError = false; + try { + dispatch(hydrateInitialOutputs(dispatchError(dispatch))); + } catch (err) { + // Display this error in devtools, unless we have errors + // already, in which case we assume this new one is moot + if (!error.frontEnd.length && !error.backEnd.length) { + dispatch(onError({type: 'backEnd', error: err})); + } + hasError = true; + } finally { + setErrorLoading(hasError); + } + } +} + UnconnectedContainer.propTypes = { appLifecycle: PropTypes.oneOf([ getAppState('STARTED'), From 14b7e3ca90b0e33e63ef9ebc0178503644848aab Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 1 Apr 2020 03:53:51 -0400 Subject: [PATCH 64/81] TreeContainer ramda & other cleanup --- dash-renderer/src/TreeContainer.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index 5d32e8f83e..3b1c3c13c2 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -121,12 +121,11 @@ class TreeContainer extends Component { (val, key) => !equals(val, oldProps[key]), newProps ); - const changedKeys = keys(changedProps); - if (changedKeys.length) { + if (!isEmpty(changedProps)) { // Identify the modified props that are required for callbacks const watchedKeys = getWatchedKeys( id, - changedKeys, + keys(changedProps), _dashprivate_graphs ); @@ -146,7 +145,7 @@ class TreeContainer extends Component { if (watchedKeys.length) { _dashprivate_dispatch( notifyObservers({ - id: id, + id, props: pick(watchedKeys, changedProps), }) ); From 0675e116fdaa5fa1f8a372497bf41e098f2052c8 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 1 Apr 2020 03:54:33 -0400 Subject: [PATCH 65/81] black --- tests/integration/callbacks/test_wildcards.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index 82f314dc26..d59d66f324 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -223,15 +223,15 @@ def fibonacci_app(clientside): # - clientside callbacks work the same as server-side # - callbacks using ALLSMALLER as an input to MATCH of the exact same id/prop app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="n", type="number", min=0, max=10, value=4), - html.Div(id="series"), - html.Div(id="sum") - ]) - - @app.callback( - Output("series", "children"), [Input("n", "value")] + app.layout = html.Div( + [ + dcc.Input(id="n", type="number", min=0, max=10, value=4), + html.Div(id="series"), + html.Div(id="sum"), + ] ) + + @app.callback(Output("series", "children"), [Input("n", "value")]) def items(n): return [html.Div(id={"i": i}) for i in range(n)] @@ -244,7 +244,7 @@ def items(n): } """, Output({"i": MATCH}, "children"), - [Input({"i": ALLSMALLER}, "children")] + [Input({"i": ALLSMALLER}, "children")], ) app.clientside_callback( @@ -255,22 +255,24 @@ def items(n): } """, Output("sum", "children"), - [Input({"i": ALL}, "children")] + [Input({"i": ALL}, "children")], ) else: + @app.callback( - Output({"i": MATCH}, "children"), - [Input({"i": ALLSMALLER}, "children")] + Output({"i": MATCH}, "children"), [Input({"i": ALLSMALLER}, "children")] ) def sequence(prev): - if (len(prev) < 2): + if len(prev) < 2: return len(prev) return int(prev[-1] or 0) + int(prev[-2] or 0) @app.callback(Output("sum", "children"), [Input({"i": ALL}, "children")]) def show_sum(seq): - return "{} elements, sum: {}".format(len(seq), sum(int(v or 0) for v in seq)) + return "{} elements, sum: {}".format( + len(seq), sum(int(v or 0) for v in seq) + ) return app From 1e75e9b3fb17e37f261d0b7541a49f658c7fabc8 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 1 Apr 2020 03:56:38 -0400 Subject: [PATCH 66/81] fix erroneous changelog message about unchanged props --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e8a48db79..892c4755bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed - [#1103](https://github.com/plotly/dash/pull/1103) Multiple changes to the callback pipeline: - `dash.callback_context.triggered` now does NOT reflect any initial values, and DOES reflect EVERY value which has been changed either by activity in the app or as a result of a previous callback. That means that the initial call of a callback with no prerequisite callbacks will list nothing as triggering. For backward compatibility, we continue to provide a length-1 list for `triggered`, but its `id` and `property` are blank strings, and `bool(triggered)` is `False`. - - A callback which returns the same property value as was previously present will not trigger the component to re-render, nor trigger other callbacks using that property as an input. + - A user interaction which returns the same property value as was previously present will not trigger the component to re-render, nor trigger callbacks using that property as an input. - Callback validation is now mostly done in the browser, rather than in Python. A few things - mostly type validation, like ensuring IDs are strings or dicts and properties are strings - are still done in Python, but most others, like ensuring outputs are unique, inputs and outputs don't overlap, and (if desired) that IDs are present in the layout, are done in the browser. This means you can define callbacks BEFORE the layout and still validate IDs to the layout; and while developing an app, most errors in callback definitions will not halt the app. - [#1145](https://github.com/plotly/dash/pull/1145) Update from React 16.8.6 to 16.13.0 From caceaaaebc91e2acd7fa7d663eb02fc0d1c7eb47 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 1 Apr 2020 05:02:37 -0400 Subject: [PATCH 67/81] fix and test wildcards with same keys for different element classes --- dash-renderer/src/actions/dependencies.js | 12 ++-- tests/integration/callbacks/test_wildcards.py | 58 +++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index fd5ebf4495..00d9fe6155 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -1247,11 +1247,13 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { if (outIdCallbacks) { for (const property in outIdCallbacks) { const cb = getCallbackByOutput(graphs, paths, id, property); - // callbacks found in the layout by output should always run, - // ie this is the initial call of this callback even if it's - // not the page initialization but just a new layout chunk - cb.initialCall = true; - addCallback(cb); + if (cb) { + // callbacks found in the layout by output should always run + // ie this is the initial call of this callback even if it's + // not the page initialization but just a new layout chunk + cb.initialCall = true; + addCallback(cb); + } } } if (!outputsOnly && inIdCallbacks) { diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index d59d66f324..e712130aba 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -312,3 +312,61 @@ def test_cbwc002_fibonacci_app(clientside, dash_duo): dash_duo.wait_for_text_to_equal("#sum", "1 elements, sum: 0") dash_duo.find_element("#n").send_keys(Keys.DOWN) dash_duo.wait_for_text_to_equal("#sum", "0 elements, sum: 0") + + +def test_cbwc003_same_keys(dash_duo): + app = dash.Dash(__name__, suppress_callback_exceptions=True) + + app.layout = html.Div( + [ + html.Button("Add Filter", id="add-filter", n_clicks=0), + html.Div(id="container", children=[]), + ] + ) + + @app.callback( + Output("container", "children"), + [Input("add-filter", "n_clicks")], + [State("container", "children")], + ) + def display_dropdowns(n_clicks, children): + new_element = html.Div( + [ + dcc.Dropdown( + id={"type": "dropdown", "index": n_clicks}, + options=[ + {"label": i, "value": i} for i in ["NYC", "MTL", "LA", "TOKYO"] + ], + ), + html.Div(id={"type": "output", "index": n_clicks}), + ] + ) + return children + [new_element] + + @app.callback( + Output({"type": "output", "index": MATCH}, "children"), + [Input({"type": "dropdown", "index": MATCH}, "value")], + [State({"type": "dropdown", "index": MATCH}, "id")], + ) + def display_output(value, id): + return html.Div("Dropdown {} = {}".format(id["index"], value)) + + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#add-filter", "Add Filter") + dash_duo.select_dcc_dropdown( + '#\\{\\"index\\"\\:0\\,\\"type\\"\\:\\"dropdown\\"\\}', "LA" + ) + dash_duo.wait_for_text_to_equal( + '#\\{\\"index\\"\\:0\\,\\"type\\"\\:\\"output\\"\\}', "Dropdown 0 = LA" + ) + dash_duo.find_element("#add-filter").click() + dash_duo.select_dcc_dropdown( + '#\\{\\"index\\"\\:1\\,\\"type\\"\\:\\"dropdown\\"\\}', "MTL" + ) + dash_duo.wait_for_text_to_equal( + '#\\{\\"index\\"\\:1\\,\\"type\\"\\:\\"output\\"\\}', "Dropdown 1 = MTL" + ) + dash_duo.wait_for_text_to_equal( + '#\\{\\"index\\"\\:0\\,\\"type\\"\\:\\"output\\"\\}', "Dropdown 0 = LA" + ) + dash_duo.wait_for_no_elements(dash_duo.devtools_error_count_locator) From 6238ee0bb577a428a5f8108cd5262a6dbcae476e Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 1 Apr 2020 05:46:06 -0400 Subject: [PATCH 68/81] log errors from the error reducer a bit weird to put side-effects in the reducer but this is the DRYest place --- .../error/CallbackGraph/CallbackGraphContainer.react.js | 3 --- dash-renderer/src/persistence.js | 5 ----- dash-renderer/src/reducers/error.js | 5 +++++ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainer.react.js b/dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainer.react.js index ae53f4ca54..480e65e768 100644 --- a/dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainer.react.js +++ b/dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainer.react.js @@ -64,9 +64,6 @@ const CallbackGraphContainer = ({graphs}) => { ${links.join('\n')} }`; - // eslint-disable-next-line no-console - console.log(dot); - viz.current .renderSVGElement(dot) .then(vizEl => { diff --git a/dash-renderer/src/persistence.js b/dash-renderer/src/persistence.js index 2b97f66bf1..36b8688a3f 100644 --- a/dash-renderer/src/persistence.js +++ b/dash-renderer/src/persistence.js @@ -75,12 +75,7 @@ export const storePrefix = '_dash_persistence.'; function err(e) { const error = typeof e === 'string' ? new Error(e) : e; - // Send this to the console too, so it's still available with debug off - /* eslint-disable-next-line no-console */ - console.error(e); - return createAction('ON_ERROR')({ - myID: storePrefix, type: 'frontEnd', error, }); diff --git a/dash-renderer/src/reducers/error.js b/dash-renderer/src/reducers/error.js index 8c72a64eee..b796024e49 100644 --- a/dash-renderer/src/reducers/error.js +++ b/dash-renderer/src/reducers/error.js @@ -8,6 +8,11 @@ const initialError = { export default function error(state = initialError, action) { switch (action.type) { case 'ON_ERROR': { + // log errors to the console for stack tracing and so they're + // available even with debugging off + /* eslint-disable-next-line no-console */ + console.error(action.payload.error); + if (action.payload.type === 'frontEnd') { return { frontEnd: [ From 380a907184b00dc4d4768bb00fc82b143dfd5b55 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 1 Apr 2020 05:56:31 -0400 Subject: [PATCH 69/81] remove now-obsolete console tests re: persistence --- dash-renderer/tests/persistence.test.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/dash-renderer/tests/persistence.test.js b/dash-renderer/tests/persistence.test.js index 3357af6fe7..cbaa752b65 100644 --- a/dash-renderer/tests/persistence.test.js +++ b/dash-renderer/tests/persistence.test.js @@ -65,8 +65,6 @@ const layoutA = storeType => ({ describe('storage fallbacks and equivalence', () => { const propVal = 42; const propStr = String(propVal); - let originalConsoleErr; - let consoleCalls; let dispatchCalls; const _dispatch = evt => { @@ -87,11 +85,6 @@ describe('storage fallbacks and equivalence', () => { }; dispatchCalls = []; - consoleCalls = []; - originalConsoleErr = console.error; - console.error = msg => { - consoleCalls.push(msg); - }; clearStores(); }); @@ -99,7 +92,6 @@ describe('storage fallbacks and equivalence', () => { afterEach(() => { delete window.my_components; clearStores(); - console.error = originalConsoleErr; }); ['local', 'session'].forEach(storeType => { @@ -111,7 +103,6 @@ describe('storage fallbacks and equivalence', () => { test(`empty ${storeName} works`, () => { recordUiEdit(layout, {p1: propVal}, _dispatch); expect(dispatchCalls).toEqual([]); - expect(consoleCalls).toEqual([]); expect(store.getItem(`${storePrefix}a.p1.true`)).toBe(`[${propStr}]`); }); @@ -123,7 +114,6 @@ describe('storage fallbacks and equivalence', () => { `${storeName} init first try failed; clearing and retrying`, `${storeName} init set/get succeeded after clearing!` ]); - expect(consoleCalls).toEqual(dispatchCalls); expect(store.getItem(`${storePrefix}a.p1.true`)).toBe(`[${propStr}]`); // Boolean so we don't see the very long value if test fails const x = Boolean(store.getItem(`${storePrefix}x.x`)); @@ -138,7 +128,6 @@ describe('storage fallbacks and equivalence', () => { `${storeName} init first try failed; clearing and retrying`, `${storeName} init still failed, falling back to memory` ]); - expect(consoleCalls).toEqual(dispatchCalls); expect(stores.memory.getItem('a.p1.true')).toEqual([propVal]); const x = Boolean(store.getItem('not_ours')); expect(x).toBe(true); @@ -150,14 +139,12 @@ describe('storage fallbacks and equivalence', () => { // initialize and ensure the store is happy recordUiEdit(layout, {p1: propVal}, _dispatch); expect(dispatchCalls).toEqual([]); - expect(consoleCalls).toEqual([]); // now flood it. recordUiEdit(layout, {p1: longString(26)}, _dispatch); expect(dispatchCalls).toEqual([ `a.p1.true failed to save in ${storeName}. Persisted props may be lost.` ]); - expect(consoleCalls).toEqual(dispatchCalls); }); }); From 20b876e268cdb38b7aa348adfdcf6d743db6d9ae Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 2 Apr 2020 18:36:34 -0400 Subject: [PATCH 70/81] remove InputGraph --- dash-renderer/src/actions/dependencies.js | 10 +------ dash-renderer/src/actions/index.js | 30 --------------------- dash-renderer/src/reducers/reducer.js | 32 +++++++---------------- 3 files changed, 10 insertions(+), 62 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 00d9fe6155..72f580f8f0 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -601,7 +601,6 @@ export function validateCallbacksToLayout(state_, dispatchError) { } export function computeGraphs(dependencies, dispatchError) { - const inputGraph = new DepGraph(); // multiGraph is just for finding circular deps const multiGraph = new DepGraph(); @@ -653,7 +652,6 @@ export function computeGraphs(dependencies, dispatchError) { const inputPatterns = {}; const finalGraphs = { - InputGraph: inputGraph, MultiGraph: multiGraph, outputMap, inputMap, @@ -745,7 +743,7 @@ export function computeGraphs(dependencies, dispatchError) { } parsedDependencies.forEach(function registerDependency(dependency) { - const {output, outputs, inputs} = dependency; + const {outputs, inputs} = dependency; // multiGraph - just for testing circularity @@ -804,12 +802,6 @@ export function computeGraphs(dependencies, dispatchError) { addPattern(inputPatterns, inId, inProp, finalDependency); } else { addMap(inputMap, inId, inProp, finalDependency); - // inputGraph - this is the one we'll use for dispatching updates - // TODO: get rid of this, use the precalculated mappings - const inputId = combineIdAndProp(inputObject); - inputGraph.addNode(output); - inputGraph.addNode(inputId); - inputGraph.addDependency(inputId, output); } }); }); diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 8fea2479f9..5554b3efe8 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -4,7 +4,6 @@ import { has, isEmpty, keys, - lensPath, map, mergeDeepRight, once, @@ -15,7 +14,6 @@ import { propEq, type, uniq, - view, without, zip, } from 'ramda'; @@ -609,31 +607,3 @@ export function handleAsyncError(err, message, dispatch) { dispatch(onError({type: 'backEnd', error})); } } - -export function serialize(state) { - // Record minimal input state in the url - const {graphs, paths, layout} = state; - const {InputGraph} = graphs; - const allNodes = InputGraph.nodes; - const savedState = {}; - keys(allNodes).forEach(nodeId => { - const [componentId, componentProp] = nodeId.split('.'); - /* - * Filter out the outputs, - * and the invisible inputs - */ - if ( - InputGraph.dependenciesOf(nodeId).length > 0 && - has(componentId, paths) - ) { - // Get the property - const propLens = lensPath( - concat(paths[componentId], ['props', componentProp]) - ); - const propValue = view(propLens, layout); - savedState[nodeId] = propValue; - } - }); - - return savedState; -} diff --git a/dash-renderer/src/reducers/reducer.js b/dash-renderer/src/reducers/reducer.js index 541422378b..ffdb8794fa 100644 --- a/dash-renderer/src/reducers/reducer.js +++ b/dash-renderer/src/reducers/reducer.js @@ -1,14 +1,8 @@ -import { - concat, - equals, - filter, - forEach, - isEmpty, - keys, - lensPath, - view, -} from 'ramda'; +import {forEach, isEmpty, keys, path} from 'ramda'; import {combineReducers} from 'redux'; + +import {getCallbacksByInput} from '../actions/dependencies'; + import layout from './layout'; import graphs from './dependencyGraph'; import paths from './paths'; @@ -48,22 +42,14 @@ function mainReducer() { function getInputHistoryState(itempath, props, state) { const {graphs, layout, paths} = state; - const {InputGraph} = graphs; - const keyObj = filter(equals(itempath), paths.strs); + const idProps = path(itempath.concat(['props']), layout); + const {id} = idProps || {}; let historyEntry; - if (!isEmpty(keyObj)) { - const id = keys(keyObj)[0]; + if (id) { historyEntry = {id, props: {}}; keys(props).forEach(propKey => { - const inputKey = `${id}.${propKey}`; - if ( - InputGraph.hasNode(inputKey) && - InputGraph.dependenciesOf(inputKey).length > 0 - ) { - historyEntry.props[propKey] = view( - lensPath(concat(paths.strs[id], ['props', propKey])), - layout - ); + if (getCallbacksByInput(graphs, paths, id, propKey).length) { + historyEntry.props[propKey] = idProps[propKey]; } }); } From b5ae43606d009494d67f1c991e32066899d0f810 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 2 Apr 2020 19:38:38 -0400 Subject: [PATCH 71/81] fix for exact duplicate callback outputs --- dash/dash.py | 32 ++++++++--------- .../devtools/test_callback_validation.py | 36 +++++++++++++++++++ 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 3338746e1c..1a854654cb 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -305,9 +305,10 @@ def __init__( "via the Dash constructor" ) - # list of dependencies + # list of dependencies - this one is used by the back end for dispatching self.callback_map = {} - self.used_outputs = [] + # same deps as a list to catch duplicate outputs, and to send to the front end + self._callback_list = [] # list of inline scripts self._inline_scripts = [] @@ -777,27 +778,22 @@ def interpolate_index(self, **kwargs): ) def dependencies(self): - return flask.jsonify( - [ - { - "output": k, - "inputs": v["inputs"], - "state": v["state"], - "clientside_function": v.get("clientside_function", None), - } - for k, v in self.callback_map.items() - ] - ) + return flask.jsonify(self._callback_list) def _insert_callback(self, output, inputs, state): _validate.validate_callback(output, inputs, state) callback_id = create_callback_id(output) - - self.callback_map[callback_id] = { + callback_spec = { + "output": callback_id, "inputs": [c.to_dict() for c in inputs], "state": [c.to_dict() for c in state], + "clientside_function": None + } + self.callback_map[callback_id] = { + "inputs": callback_spec["inputs"], + "state": callback_spec["state"], } - self.used_outputs.extend(output if callback_id.startswith("..") else [output]) + self._callback_list.append(callback_spec) return callback_id @@ -862,7 +858,7 @@ def clientside_callback(self, clientside_function, output, inputs, state=()): ) ``` """ - callback_id = self._insert_callback(output, inputs, state) + self._insert_callback(output, inputs, state) # If JS source is explicitly given, create a namespace and function # name, then inject the code. @@ -888,7 +884,7 @@ def clientside_callback(self, clientside_function, output, inputs, state=()): namespace = clientside_function.namespace function_name = clientside_function.function_name - self.callback_map[callback_id]["clientside_function"] = { + self._callback_list[-1]["clientside_function"] = { "namespace": namespace, "function_name": function_name, } diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index 2bd96d18bb..378143f686 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -568,3 +568,39 @@ def f(a, b, c): ], ] check_errors(dash_duo, specs) + + +def test_dvcv011_duplicate_outputs_simple(dash_duo): + app = Dash(__name__) + + @app.callback(Output("a", "children"), [Input("c", "children")]) + def c(children): + return children + + @app.callback(Output("a", "children"), [Input("b", "children")]) + def c2(children): + return children + + @app.callback([Output("a", "style")], [Input("c", "style")]) + def s(children): + return (children,) + + @app.callback([Output("a", "style")], [Input("b", "style")]) + def s2(children): + return (children,) + + app.layout = html.Div( + [ + html.Div([], id="a"), + html.Div(["Bye"], id="b", style={"color": "red"}), + html.Div(["Hello"], id="c", style={"color": "green"}), + ] + ) + + dash_duo.start_server(app, **debugging) + + specs = [ + ["Duplicate callback outputs", ["Output 0 (a.children) is already in use."]], + ["Duplicate callback outputs", ["Output 0 (a.style) is already in use."]], + ] + check_errors(dash_duo, specs) From e6f850ec46a5c72b6287f0e8c6c6f92dd3ee1e85 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 2 Apr 2020 21:11:01 -0400 Subject: [PATCH 72/81] add more callback cycle tests --- .../devtools/test_callback_validation.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index 378143f686..62be4b13c4 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -604,3 +604,68 @@ def s2(children): ["Duplicate callback outputs", ["Output 0 (a.style) is already in use."]], ] check_errors(dash_duo, specs) + + +def test_dvcv012_circular_2_step(dash_duo): + app = Dash(__name__) + + app.layout = html.Div( + [html.Div([], id="a"), html.Div(["Bye"], id="b"), html.Div(["Hello"], id="c")] + ) + + @app.callback(Output("a", "children"), [Input("b", "children")]) + def callback(children): + return children + + @app.callback(Output("b", "children"), [Input("a", "children")]) + def c2(children): + return children + + dash_duo.start_server(app, **debugging) + + specs = [ + [ + "Circular Dependencies", + [ + "Dependency Cycle Found:", + "a.children -> b.children", + "b.children -> a.children", + ], + ] + ] + check_errors(dash_duo, specs) + + +def test_dvcv013_circular_3_step(dash_duo): + app = Dash(__name__) + + app.layout = html.Div( + [html.Div([], id="a"), html.Div(["Bye"], id="b"), html.Div(["Hello"], id="c")] + ) + + @app.callback(Output("b", "children"), [Input("a", "children")]) + def callback(children): + return children + + @app.callback(Output("c", "children"), [Input("b", "children")]) + def c2(children): + return children + + @app.callback([Output("a", "children")], [Input("c", "children")]) + def c3(children): + return children + + dash_duo.start_server(app, **debugging) + + specs = [ + [ + "Circular Dependencies", + [ + "Dependency Cycle Found:", + "a.children -> b.children", + "b.children -> c.children", + "c.children -> a.children", + ], + ] + ] + check_errors(dash_duo, specs) From 6f772a9339d8f2b9e143f338fe51d581db68b094 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 3 Apr 2020 08:17:36 -0400 Subject: [PATCH 73/81] fix for derived props (eg dcc.Store.modified_timestamp) --- dash-renderer/src/actions/index.js | 4 +++ .../callbacks/test_multiple_callbacks.py | 32 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 5554b3efe8..ee9eb408b1 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -292,6 +292,10 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { props ); if (appliedProps) { + // doUpdateProps can cause new callbacks to be added + // via derived props - update pendingCallbacks + pendingCallbacks = getState().pendingCallbacks; + Object.keys(appliedProps).forEach(property => { updated.push(combineIdAndProp({id, property})); }); diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index a86a31175c..7b46e95c2a 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -7,7 +7,7 @@ import dash_core_components as dcc import dash_table import dash -from dash.dependencies import Input, Output +from dash.dependencies import Input, Output, State from dash.exceptions import PreventUpdate @@ -236,3 +236,33 @@ def update_graph(s1, s2): dash_duo.find_element("#b2").click() dash_duo.wait_for_text_to_equal("#out", "x=1, y=1") + + +def test_cbmt006_derived_props(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + html.Div(id="output"), + html.Button("click", id="btn"), + dcc.Store(id="store"), + ] + ) + + @app.callback( + Output("output", "children"), + [Input("store", "modified_timestamp")], + [State("store", "data")], + ) + def on_data(ts, data): + return data + + @app.callback(Output("store", "data"), [Input("btn", "n_clicks")]) + def on_click(n_clicks): + return n_clicks or 0 + + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal("#output", "0") + dash_duo.find_element("#btn").click() + dash_duo.wait_for_text_to_equal("#output", "1") + dash_duo.find_element("#btn").click() + dash_duo.wait_for_text_to_equal("#output", "2") From 041df7fcada95492d3bcf23e42c832bae5d84aa0 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 3 Apr 2020 16:45:50 -0400 Subject: [PATCH 74/81] --pause option for dash.testing use with a single test case, eg: pytest -k cbmt006 --pause drops you in the python debugger, you can also use the js console --- dash/dash.py | 2 +- dash/testing/browser.py | 10 ++++++++++ dash/testing/plugin.py | 9 +++++++++ tests/integration/callbacks/test_multiple_callbacks.py | 6 +----- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 1a854654cb..09b0ae61eb 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -787,7 +787,7 @@ def _insert_callback(self, output, inputs, state): "output": callback_id, "inputs": [c.to_dict() for c in inputs], "state": [c.to_dict() for c in state], - "clientside_function": None + "clientside_function": None, } self.callback_map[callback_id] = { "inputs": callback_spec["inputs"], diff --git a/dash/testing/browser.py b/dash/testing/browser.py index c9b1aa985e..f67ab30896 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -42,6 +42,7 @@ def __init__( percy_finalize=True, percy_assets_root="", wait_timeout=10, + pause=False, ): self._browser = browser.lower() self._remote_url = remote_url @@ -54,6 +55,7 @@ def __init__( self._wait_timeout = wait_timeout self._percy_finalize = percy_finalize self._percy_run = percy_run + self._pause = pause self._driver = until(self.get_webdriver, timeout=1) self._driver.implicitly_wait(2) @@ -312,6 +314,14 @@ def wait_for_page(self, url=None, timeout=10): ) ) + if self._pause: + try: + import pdb as pdb_ + except ImportError: + import ipdb as pdb_ + + pdb_.set_trace() + def select_dcc_dropdown(self, elem_or_selector, value=None, index=None): dropdown = self._get_element(elem_or_selector) dropdown.click() diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index 0881e14394..724f07c6fd 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -49,6 +49,12 @@ def pytest_addoption(parser): help="set this flag to control percy finalize at CI level", ) + dash.addoption( + "--pause", + action="store_true", + help="pause using pdb after opening the test app, so you can interact with it", + ) + @pytest.mark.tryfirst def pytest_addhooks(pluginmanager): @@ -114,6 +120,7 @@ def dash_br(request, tmpdir): download_path=tmpdir.mkdir("download").strpath, percy_assets_root=request.config.getoption("percy_assets"), percy_finalize=request.config.getoption("nopercyfinalize"), + pause=request.config.getoption("pause"), ) as browser: yield browser @@ -130,6 +137,7 @@ def dash_duo(request, dash_thread_server, tmpdir): download_path=tmpdir.mkdir("download").strpath, percy_assets_root=request.config.getoption("percy_assets"), percy_finalize=request.config.getoption("nopercyfinalize"), + pause=request.config.getoption("pause"), ) as dc: yield dc @@ -146,5 +154,6 @@ def dashr(request, dashr_server, tmpdir): download_path=tmpdir.mkdir("download").strpath, percy_assets_root=request.config.getoption("percy_assets"), percy_finalize=request.config.getoption("nopercyfinalize"), + pause=request.config.getoption("pause"), ) as dc: yield dc diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index 7b46e95c2a..517fb1275f 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -241,11 +241,7 @@ def update_graph(s1, s2): def test_cbmt006_derived_props(dash_duo): app = dash.Dash(__name__) app.layout = html.Div( - [ - html.Div(id="output"), - html.Button("click", id="btn"), - dcc.Store(id="store"), - ] + [html.Div(id="output"), html.Button("click", id="btn"), dcc.Store(id="store"),] ) @app.callback( From 4f5def43e81520e1a71ac8f6728a7bbf6550494b Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 3 Apr 2020 17:53:03 -0400 Subject: [PATCH 75/81] fix the derived props pendingCallbacks fix for multi-output callbacks --- dash-renderer/src/actions/index.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index ee9eb408b1..fd7c27be9f 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -294,7 +294,15 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { if (appliedProps) { // doUpdateProps can cause new callbacks to be added // via derived props - update pendingCallbacks - pendingCallbacks = getState().pendingCallbacks; + // But we may also need to merge in other callbacks that + // we found in an earlier interation of the data loop. + const statePendingCallbacks = getState().pendingCallbacks; + if (statePendingCallbacks !== pendingCallbacks) { + pendingCallbacks = mergePendingCallbacks( + pendingCallbacks, + statePendingCallbacks + ); + } Object.keys(appliedProps).forEach(property => { updated.push(combineIdAndProp({id, property})); From 41fdae14b2daec9ec06c959be21b5af1c21c1294 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 4 Apr 2020 21:30:38 -0400 Subject: [PATCH 76/81] fix layout changedProps edge case callbacks entirely in new layout chunk or not determines whether inputs appear changed or not --- dash-renderer/src/actions/dependencies.js | 31 +++++-- dash-renderer/src/actions/index.js | 6 +- .../callbacks/test_multiple_callbacks.py | 2 +- tests/integration/callbacks/test_wildcards.py | 92 ++++++++++++++++++- 4 files changed, 117 insertions(+), 14 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 72f580f8f0..02d046c2a8 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -23,8 +23,10 @@ import { partition, path, pickBy, + pluck, propEq, props, + startsWith, unnest, values, zip, @@ -1190,13 +1192,16 @@ export function getWatchedKeys(id, newProps, graphs) { * when the new *absence* of a given component should trigger a callback. * opts.newPaths: paths object after the edit - to be used with * removedArrayInputsOnly to determine if the callback still has its outputs + * opts.chunkPath: path to the new chunk - used to determine if any outputs are + * outside of this chunk, because this determines whether inputs inside the + * chunk count as having changed * * Returns an array of objects: * {callback, resolvedId, getOutputs, getInputs, getState, ...etc} * See getCallbackByOutput for details. */ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { - const {outputsOnly, removedArrayInputsOnly, newPaths} = opts || {}; + const {outputsOnly, removedArrayInputsOnly, newPaths, chunkPath} = opts; const foundCbIds = {}; const callbacks = []; @@ -1249,7 +1254,23 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { } } if (!outputsOnly && inIdCallbacks) { - const idStr = removedArrayInputsOnly && stringifyId(id); + const maybeAddCallback = removedArrayInputsOnly + ? addCallbackIfArray(stringifyId(id)) + : addCallback; + let handleThisCallback = maybeAddCallback; + if (chunkPath) { + handleThisCallback = cb => { + if ( + all( + startsWith(chunkPath), + pluck('path', flatten(cb.getOutputs(paths))) + ) + ) { + cb.changedPropIds = {}; + } + maybeAddCallback(cb); + }; + } for (const property in inIdCallbacks) { getCallbacksByInput( graphs, @@ -1257,11 +1278,7 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { id, property, INDIRECT - ).forEach( - removedArrayInputsOnly - ? addCallbackIfArray(idStr) - : addCallback - ); + ).forEach(handleThisCallback); } } } diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index fd7c27be9f..6e59cf45f6 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -559,7 +559,9 @@ function updateChildPaths( const cleanedCallbacks = pruneRemovedCallbacks(pendingCallbacks, paths); - const newCallbacks = getCallbacksInLayout(graphs, paths, children); + const newCallbacks = getCallbacksInLayout(graphs, paths, children, { + chunkPath: childrenPath, + }); // Wildcard callbacks with array inputs (ALL / ALLSMALLER) need to trigger // even due to the deletion of components @@ -567,7 +569,7 @@ function updateChildPaths( graphs, oldPaths, oldChildren, - {removedArrayInputsOnly: true, newPaths: paths} + {removedArrayInputsOnly: true, newPaths: paths, chunkPath: childrenPath} ); const allNewCallbacks = mergePendingCallbacks( diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index 517fb1275f..009c8e7d10 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -241,7 +241,7 @@ def update_graph(s1, s2): def test_cbmt006_derived_props(dash_duo): app = dash.Dash(__name__) app.layout = html.Div( - [html.Div(id="output"), html.Button("click", id="btn"), dcc.Store(id="store"),] + [html.Div(id="output"), html.Button("click", id="btn"), dcc.Store(id="store")] ) @app.callback( diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index e712130aba..2dbfeb368a 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -15,10 +15,10 @@ def css_escape(s): return sel -def todo_app(): +def todo_app(content_callback): app = dash.Dash(__name__) - app.layout = html.Div( + content = html.Div( [ html.Div("Dash To-Do list"), dcc.Input(id="new-item"), @@ -30,6 +30,16 @@ def todo_app(): ] ) + if content_callback: + app.layout = html.Div([html.Div(id="content"), dcc.Location(id="url")]) + + @app.callback(Output("content", "children"), [Input("url", "pathname")]) + def display_content(_): + return content + + else: + app.layout = content + style_todo = {"display": "inline", "margin": "10px"} style_done = {"textDecoration": "line-through", "color": "#888"} style_done.update(style_todo) @@ -127,8 +137,9 @@ def show_totals(done): return app -def test_cbwc001_todo_app(dash_duo): - app = todo_app() +@pytest.mark.parametrize("content_callback", (False, True)) +def test_cbwc001_todo_app(content_callback, dash_duo): + app = todo_app(content_callback) dash_duo.start_server(app) dash_duo.wait_for_text_to_equal("#totals", "0 of 0 items completed") @@ -370,3 +381,76 @@ def display_output(value, id): '#\\{\\"index\\"\\:0\\,\\"type\\"\\:\\"output\\"\\}', "Dropdown 0 = LA" ) dash_duo.wait_for_no_elements(dash_duo.devtools_error_count_locator) + + +def test_cbwc004_layout_chunk_changed_props(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + dcc.Input(id={"type": "input", "index": 1}, value="input-1"), + html.Div(id="container"), + html.Div(id="output-outer"), + html.Button("Show content", id="btn"), + ] + ) + + @app.callback(Output("container", "children"), [Input("btn", "n_clicks")]) + def display_output(n): + if n: + return html.Div( + [ + dcc.Input(id={"type": "input", "index": 2}, value="input-2"), + html.Div(id="output-inner"), + ] + ) + else: + return "No content initially" + + def trigger_info(): + triggered = dash.callback_context.triggered + return "triggered is {} with prop_ids {}".format( + "Truthy" if triggered else "Falsy", + ", ".join(t["prop_id"] for t in triggered), + ) + + @app.callback( + Output("output-inner", "children"), + [Input({"type": "input", "index": ALL}, "value")], + ) + def update_dynamic_output_pattern(wc_inputs): + return trigger_info() + # When this is triggered because output-2 was rendered, + # nothing has changed + + @app.callback( + Output("output-outer", "children"), + [Input({"type": "input", "index": ALL}, "value")], + ) + def update_output_on_page_pattern(value): + return trigger_info() + # When this triggered on page load, + # nothing has changed + # When dcc.Input(id={'type': 'input', 'index': 2}) + # is rendered (from display_output) + # then `{'type': 'input', 'index': 2}` has changed + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#container", "No content initially") + dash_duo.wait_for_text_to_equal( + "#output-outer", "triggered is Falsy with prop_ids ." + ) + + dash_duo.find_element("#btn").click() + dash_duo.wait_for_text_to_equal( + "#output-outer", + 'triggered is Truthy with prop_ids {"index":2,"type":"input"}.value', + ) + dash_duo.wait_for_text_to_equal( + "#output-inner", "triggered is Falsy with prop_ids ." + ) + + dash_duo.find_elements("input")[0].send_keys("X") + trigger_text = 'triggered is Truthy with prop_ids {"index":1,"type":"input"}.value' + dash_duo.wait_for_text_to_equal("#output-outer", trigger_text) + dash_duo.wait_for_text_to_equal("#output-inner", trigger_text) From 0c76dc70ac1a852eb900d499b204aaf1e235d741 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 5 Apr 2020 08:57:58 -0400 Subject: [PATCH 77/81] lint testing/browser --- dash/testing/browser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index f67ab30896..554e3ad5ec 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -30,6 +30,7 @@ class Browser(DashPageMixin): + # pylint: disable=too-many-arguments def __init__( self, browser, From 90eb451ffe4e98b6df13f9eb68132f3f1bc2acb4 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sun, 5 Apr 2020 09:03:58 -0400 Subject: [PATCH 78/81] changelog for --pause option --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9c604272b..5b9baeaaf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [1.10.0] - 2020-04-01 ### Added - [#1103](https://github.com/plotly/dash/pull/1103) Wildcard IDs and callbacks. Component IDs can be dictionaries, and callbacks can reference patterns of components, using three different wildcards: `ALL`, `MATCH`, and `ALLSMALLER`, available from `dash.dependencies`. This lets you create components on demand, and have callbacks respond to any and all of them. To help with this, `dash.callback_context` gets three new entries: `outputs_list`, `inputs_list`, and `states_list`, which contain all the ids, properties, and except for the outputs, the property values from all matched components. +- [#1103](https://github.com/plotly/dash/pull/1103) `dash.testing` option `--pause`: after opening the dash app in a test, will invoke `pdb` for live debugging of both Javascript and Python. Use with a single test case like `pytest -k cbwc001 --pause`. - [#1134](https://github.com/plotly/dash/pull/1134) Allow `dash.run_server()` host and port parameters to be set with environment variables HOST & PORT, respectively From 7c47b0577abec4f15f74e6617658c49fc02f8de1 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 7 Apr 2020 17:15:11 -0400 Subject: [PATCH 79/81] trap ID errors during callback dispatch so we can clear the pendingCallbacks queue and show errors in the devtools --- dash-renderer/src/actions/index.js | 49 +++++++++++-------- .../devtools/test_callback_validation.py | 28 +++++++++-- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 6e59cf45f6..999ade8569 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -244,24 +244,31 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { const {output, inputs, state, clientside_function} = cb.callback; const {requestId, resolvedId} = cb; const {allOutputs, allPropIds} = outputStash[requestId]; - const outputs = allOutputs.map((out, i) => - unwrapIfNotMulti( - paths, - map(pick(['id', 'property']), out), - cb.callback.outputs[i], - cb.anyVals, - 'Output' - ) - ); - const payload = { - output, - outputs: isMultiOutputProp(output) ? outputs : outputs[0], - inputs: fillVals(paths, layout, cb, inputs, 'Input'), - changedPropIds: keys(cb.changedPropIds), - }; - if (cb.callback.state.length) { - payload.state = fillVals(paths, layout, cb, state, 'State'); + let payload; + try { + const outputs = allOutputs.map((out, i) => + unwrapIfNotMulti( + paths, + map(pick(['id', 'property']), out), + cb.callback.outputs[i], + cb.anyVals, + 'Output' + ) + ); + + payload = { + output, + outputs: isMultiOutputProp(output) ? outputs : outputs[0], + inputs: fillVals(paths, layout, cb, inputs, 'Input'), + changedPropIds: keys(cb.changedPropIds), + }; + if (cb.callback.state.length) { + payload.state = fillVals(paths, layout, cb, state, 'State'); + } + } catch (e) { + handleError(e); + return fireNext(); } function updatePending(pendingCallbacks, skippedProps) { @@ -361,10 +368,10 @@ async function fireReadyCallbacks(dispatch, getState, callbacks) { // that have other changed inputs will still fire. updatePending(pendingCallbacks, allPropIds); } - let message = `Callback error updating ${map( - combineIdAndProp, - flatten([payload.outputs]) - ).join(', ')}`; + const outputs = payload + ? map(combineIdAndProp, flatten([payload.outputs])).join(', ') + : output; + let message = `Callback error updating ${outputs}`; if (clientside_function) { const {namespace: ns, function_name: fn} = clientside_function; message += ` via clientside function ${ns}.${fn}`; diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index 62be4b13c4..7558c6c313 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -19,7 +19,7 @@ def check_errors(dash_duo, specs): for i in range(cnt): msg = dash_duo.find_elements(".dash-fe-error__title")[i].text dash_duo.find_elements(".test-devtools-error-toggle")[i].click() - txt = dash_duo.wait_for_element(".dash-backend-error").text + txt = dash_duo.wait_for_element(".dash-backend-error,.dash-fe-error__info").text dash_duo.find_elements(".test-devtools-error-toggle")[i].click() dash_duo.wait_for_no_elements(".dash-backend-error") found.append((msg, txt)) @@ -47,6 +47,9 @@ def check_errors(dash_duo, specs): ) ) + # ensure the errors didn't leave items in the pendingCallbacks queue + assert dash_duo.driver.execute_script('return document.title') == 'Dash' + def test_dvcv001_blank(dash_duo): app = Dash(__name__) @@ -412,6 +415,24 @@ def h(a): return app +# These ones are raised by bad_id_app whether suppressing callback exceptions or not +dispatch_specs = [ + [ + "A nonexistent object was used in an `Input` of a Dash callback. " + "The id of this object is `yeah-no` and the property is `value`. " + "The string ids in the current layout are: " + "[main, outer-div, inner-div, inner-input, outer-input]", [] + ], + [ + "A nonexistent object was used in an `Output` of a Dash callback. " + "The id of this object is `nope` and the property is `children`. " + "The string ids in the current layout are: " + "[main, outer-div, inner-div, inner-input, outer-input]", [] + ] +] + + + def test_dvcv008_wrong_callback_id(dash_duo): dash_duo.start_server(bad_id_app(), **debugging) @@ -461,14 +482,13 @@ def test_dvcv008_wrong_callback_id(dash_duo): ], ], ] - check_errors(dash_duo, specs) + check_errors(dash_duo, dispatch_specs + specs) def test_dvcv009_suppress_callback_exceptions(dash_duo): dash_duo.start_server(bad_id_app(suppress_callback_exceptions=True), **debugging) - dash_duo.find_element(".dash-debug-menu") - dash_duo.wait_for_no_elements(".test-devtools-error-count") + check_errors(dash_duo, dispatch_specs) def test_dvcv010_bad_props(dash_duo): From de5e2890903578555d1691372e68c0110e8c156a Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 7 Apr 2020 17:17:24 -0400 Subject: [PATCH 80/81] black --- .../integration/devtools/test_callback_validation.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index 7558c6c313..1f50c13bcf 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -48,7 +48,7 @@ def check_errors(dash_duo, specs): ) # ensure the errors didn't leave items in the pendingCallbacks queue - assert dash_duo.driver.execute_script('return document.title') == 'Dash' + assert dash_duo.driver.execute_script("return document.title") == "Dash" def test_dvcv001_blank(dash_duo): @@ -421,18 +421,19 @@ def h(a): "A nonexistent object was used in an `Input` of a Dash callback. " "The id of this object is `yeah-no` and the property is `value`. " "The string ids in the current layout are: " - "[main, outer-div, inner-div, inner-input, outer-input]", [] + "[main, outer-div, inner-div, inner-input, outer-input]", + [], ], [ "A nonexistent object was used in an `Output` of a Dash callback. " "The id of this object is `nope` and the property is `children`. " "The string ids in the current layout are: " - "[main, outer-div, inner-div, inner-input, outer-input]", [] - ] + "[main, outer-div, inner-div, inner-input, outer-input]", + [], + ], ] - def test_dvcv008_wrong_callback_id(dash_duo): dash_duo.start_server(bad_id_app(), **debugging) From 8a03f3a1feed780f1ee8d6d47d2d27a00344e5d3 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 7 Apr 2020 17:45:40 -0400 Subject: [PATCH 81/81] fix callback_validation tests --- tests/integration/devtools/test_callback_validation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index 1f50c13bcf..08190c6dad 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -19,7 +19,12 @@ def check_errors(dash_duo, specs): for i in range(cnt): msg = dash_duo.find_elements(".dash-fe-error__title")[i].text dash_duo.find_elements(".test-devtools-error-toggle")[i].click() - txt = dash_duo.wait_for_element(".dash-backend-error,.dash-fe-error__info").text + dash_duo.wait_for_element(".dash-backend-error,.dash-fe-error__info") + has_BE = dash_duo.driver.execute_script( + "return document.querySelectorAll('.dash-backend-error').length" + ) + txt_selector = ".dash-backend-error" if has_BE else ".dash-fe-error__info" + txt = dash_duo.wait_for_element(txt_selector).text dash_duo.find_elements(".test-devtools-error-toggle")[i].click() dash_duo.wait_for_no_elements(".dash-backend-error") found.append((msg, txt))