From 64dc0c9e2d90fa4bc78e7471a5c6af6111f07a1c Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Jun 2019 13:10:39 -0400 Subject: [PATCH 01/34] remove old misspelled supress->suppress fallback --- dash/dash.py | 11 +++-------- tests/integration/test_render.py | 10 +++++----- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 292d8f207b..9b5058c4ee 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -707,9 +707,7 @@ def _validate_callback(self, output, inputs, state): ) ) - if (layout is None and - not self.config.first('suppress_callback_exceptions', - 'supress_callback_exceptions')): + 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(''' @@ -751,8 +749,7 @@ def _validate_callback(self, output, inputs, state): invalid_characters )) - if (not self.config.first('suppress_callback_exceptions', - 'supress_callback_exceptions') and + if (not self.config.suppress_callback_exceptions and arg.component_id not in layout and arg.component_id != getattr(layout, 'id', None)): raise exceptions.NonExistentIdException(''' @@ -775,9 +772,7 @@ def _validate_callback(self, output, inputs, state): ) ).replace(' ', '')) - if not self.config.first('suppress_callback_exceptions', - 'supress_callback_exceptions'): - + if not self.config.suppress_callback_exceptions: if getattr(layout, 'id', None) == arg.component_id: component = layout else: diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index d087ed12cd..364b9dd119 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -383,7 +383,7 @@ def pad_output(input): call_count = Value('i', 0) # these components don't exist in the initial render - app.config.supress_callback_exceptions = True + app.config.suppress_callback_exceptions = True @app.callback( Output('sub-output-1', 'children'), @@ -536,7 +536,7 @@ def display_chapter(toc_value): call_counts['body'].value += 1 return chapters[toc_value] - app.config.supress_callback_exceptions = True + app.config.suppress_callback_exceptions = True def generate_graph_callback(counterId): def callback(value): @@ -779,7 +779,7 @@ def update_output(value): # callback for component that doesn't yet exist in the dom # in practice, it might get added by some other callback - app.config.supress_callback_exceptions = True + app.config.suppress_callback_exceptions = True output_2_call_count = Value('i', 0) @app.callback( @@ -994,7 +994,7 @@ def test_event_properties_creating_inputs(self): script['namespace'] = 'dash_core_components' app.scripts.append_script(script) - app.config.supress_callback_exceptions = True + app.config.suppress_callback_exceptions = True call_counts = { ids['input-output']: Value('i', 0), ids['button-output']: Value('i', 0) @@ -1164,7 +1164,7 @@ def test_removing_component_while_its_getting_updated(self): ), html.Div(id='body') ]) - app.config.supress_callback_exceptions = True + app.config.suppress_callback_exceptions = True call_counts = { 'body': Value('i', 0), From 3be5dc627e8a8df038f3b5b07be357716eebf6f1 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Jun 2019 13:17:03 -0400 Subject: [PATCH 02/34] remove Dash `**kwargs`, unused except to warn about csrf removal --- dash/dash.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 9b5058c4ee..54fb5013ed 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -9,7 +9,6 @@ import json import pkgutil import threading -import warnings import re import logging import pprint @@ -107,22 +106,12 @@ def __init__( suppress_callback_exceptions=None, components_cache_max_age=None, show_undo_redo=False, - plugins=None, - **kwargs): + plugins=None): # Store some flask-related parameters for use in init_app() self.compress = compress self.name = name - # pylint-disable: too-many-instance-attributes - if 'csrf_protect' in kwargs: - warnings.warn(''' - `csrf_protect` is no longer used, - CSRF protection has been removed as it is no longer - necessary. - See https://github.com/plotly/dash/issues/141 for details. - ''', DeprecationWarning) - self._assets_folder = os.path.join( flask.helpers.get_root_path(name), assets_folder, From e48b12c796a1bc6808332b98bfa702900212caba Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Jun 2019 14:49:22 -0400 Subject: [PATCH 03/34] remove static_folder kwarg - assets_folder is better --- dash/dash.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 54fb5013ed..9e7f7b3513 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -89,7 +89,6 @@ def __init__( self, name='__main__', server=True, - static_folder='static', assets_folder='assets', assets_url_path='/assets', assets_ignore='', @@ -122,7 +121,7 @@ def __init__( # (defer server creation) or a Flask app instance (we use their server) if isinstance(server, bool): if server: - self.server = Flask(name, static_folder=static_folder) + self.server = Flask(name) else: self.server = None elif isinstance(server, Flask): From 50527e1d11ec4c97c862fa446d33b352758381b7 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Jun 2019 14:50:56 -0400 Subject: [PATCH 04/34] remove components_cache_max_age kwarg - we have cache-busting urls --- dash/dash.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 9e7f7b3513..c00663719e 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -35,6 +35,7 @@ from ._utils import get_asset_path as _get_asset_path from ._utils import create_callback_id as _create_callback_id from ._configs import (get_combined_config, pathname_configs) +from .version import __version__ _default_index = ''' @@ -103,7 +104,6 @@ def __init__( external_scripts=None, external_stylesheets=None, suppress_callback_exceptions=None, - components_cache_max_age=None, show_undo_redo=False, plugins=None): @@ -148,10 +148,6 @@ def __init__( 'include_assets_files', include_assets_files, True), 'assets_external_path': get_combined_config( 'assets_external_path', assets_external_path, ''), - 'components_cache_max_age': int(get_combined_config( - 'components_cache_max_age', - components_cache_max_age, - 2678400)), 'show_undo_redo': show_undo_redo }) @@ -542,15 +538,9 @@ def serve_component_suites(self, package_name, path_in_package_dist): 'map': 'application/json' })[path_in_package_dist.split('.')[-1]] - headers = { - 'Cache-Control': 'public, max-age={}'.format( - self.config.components_cache_max_age) - } - return Response( pkgutil.get_data(package_name, path_in_package_dist), - mimetype=mimetype, - headers=headers + mimetype=mimetype ) def index(self, *args, **kwargs): # pylint: disable=unused-argument @@ -568,8 +558,9 @@ def index(self, *args, **kwargs): # pylint: disable=unused-argument favicon_mod_time ) else: - favicon_url = '{}_favicon.ico'.format( - self.config.requests_pathname_prefix) + favicon_url = '{}_favicon.ico?v={}'.format( + self.config.requests_pathname_prefix, + __version__) favicon = _format_tag('link', { 'rel': 'icon', @@ -1245,13 +1236,8 @@ def _invalid_resources_handler(err): return err.args[0], 404 def _serve_default_favicon(self): - headers = { - 'Cache-Control': 'public, max-age={}'.format( - self.config.components_cache_max_age) - } return flask.Response( pkgutil.get_data('dash', 'favicon.ico'), - headers=headers, content_type='image/x-icon', ) From f0282f877b319fff93b2344f1754dfe908413cf9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Jun 2019 15:48:23 -0400 Subject: [PATCH 05/34] remove unused resources.config.infer_from_layout --- dash/dash.py | 5 ----- dash/resources.py | 24 ++++++++---------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index c00663719e..8a2a1c85c4 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -313,11 +313,6 @@ def layout(self, value): self._layout = value - layout_value = self._layout_value() - # pylint: disable=protected-access - self.css._update_layout(layout_value) - self.scripts._update_layout(layout_value) - @property def index_string(self): return self._index_string diff --git a/dash/resources.py b/dash/resources.py index 87cd8b3ab1..144f1b6051 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -7,10 +7,9 @@ class Resources: - def __init__(self, resource_name, layout): + def __init__(self, resource_name): self._resources = [] self.resource_name = resource_name - self.layout = layout def append_resource(self, resource): self._resources.append(resource) @@ -68,18 +67,14 @@ def get_all_resources(self, dev_bundles=False): # pylint: disable=too-few-public-methods class _Config: - def __init__(self, infer_from_layout, serve_locally): - self.infer_from_layout = infer_from_layout + def __init__(self, serve_locally): self.serve_locally = serve_locally class Css: - def __init__(self, layout=None): - self._resources = Resources('_css_dist', layout) - self._resources.config = self.config = _Config(True, True) - - def _update_layout(self, layout): - self._resources.layout = layout + def __init__(self): + self._resources = Resources('_css_dist') + self._resources.config = self.config = _Config(True) def append_css(self, stylesheet): self._resources.append_resource(stylesheet) @@ -89,12 +84,9 @@ def get_all_css(self): class Scripts: - def __init__(self, layout=None): - self._resources = Resources('_js_dist', layout) - self._resources.config = self.config = _Config(True, True) - - def _update_layout(self, layout): - self._resources.layout = layout + def __init__(self): + self._resources = Resources('_js_dist') + self._resources.config = self.config = _Config(True) def append_script(self, script): self._resources.append_resource(script) From ae685ee25795427781ff2490ff1f2c28941837a9 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Jun 2019 16:17:53 -0400 Subject: [PATCH 06/34] new Dash kwarg serve_locally - sets Scripts and Css config --- dash/dash.py | 5 +++-- dash/resources.py | 8 ++++---- tests/unit/dash/test_resources.py | 6 ++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/dash/dash.py b/dash/dash.py index 8a2a1c85c4..5349e3b3c8 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -98,6 +98,7 @@ def __init__( assets_external_path=None, requests_pathname_prefix=None, routes_pathname_prefix=None, + serve_locally=True, compress=True, meta_tags=None, index_string=_default_index, @@ -163,8 +164,8 @@ def __init__( self.renderer = 'var renderer = new DashRenderer();' # static files from the packages - self.css = Css() - self.scripts = Scripts() + self.css = Css(serve_locally) + self.scripts = Scripts(serve_locally) self._external_scripts = external_scripts or [] self._external_stylesheets = external_stylesheets or [] diff --git a/dash/resources.py b/dash/resources.py index 144f1b6051..1c4574061d 100644 --- a/dash/resources.py +++ b/dash/resources.py @@ -72,9 +72,9 @@ def __init__(self, serve_locally): class Css: - def __init__(self): + def __init__(self, serve_locally): self._resources = Resources('_css_dist') - self._resources.config = self.config = _Config(True) + self._resources.config = self.config = _Config(serve_locally) def append_css(self, stylesheet): self._resources.append_resource(stylesheet) @@ -84,9 +84,9 @@ def get_all_css(self): class Scripts: - def __init__(self): + def __init__(self, serve_locally): self._resources = Resources('_js_dist') - self._resources.config = self.config = _Config(True) + self._resources.config = self.config = _Config(serve_locally) def append_script(self, script): self._resources.append_resource(script) diff --git a/tests/unit/dash/test_resources.py b/tests/unit/dash/test_resources.py index 4a32f54878..b841051fb6 100644 --- a/tests/unit/dash/test_resources.py +++ b/tests/unit/dash/test_resources.py @@ -59,6 +59,12 @@ def test_external(mocker): ] +def test_external_kwarg(): + app = dash.Dash(__name__, serve_locally=False) + assert not app.scripts.config.serve_locally + assert not app.css.config.serve_locally + + def test_internal(mocker): mocker.patch('dash_core_components._js_dist') dcc._js_dist = _monkey_patched_js_dist # noqa: W0212, From 7c76583b4daca56ab39a52a9758d18859f60db92 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Jun 2019 16:52:53 -0400 Subject: [PATCH 07/34] helpful errors if old kwargs are used in Dash constructor --- dash/dash.py | 15 ++++++++++++++- dash/exceptions.py | 4 ++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 5349e3b3c8..6a2305e968 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -106,7 +106,20 @@ def __init__( external_stylesheets=None, suppress_callback_exceptions=None, show_undo_redo=False, - plugins=None): + plugins=None, + **kwargs): + + for key in kwargs: + 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.' + ) + else: + # any other kwarg mimic the built-in exception + raise TypeError( + "Dash() got an unexpected keyword argument '" + key + "'" + ) # Store some flask-related parameters for use in init_app() self.compress = compress diff --git a/dash/exceptions.py b/dash/exceptions.py index 0b02966209..27f115f84e 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -2,6 +2,10 @@ class DashException(Exception): pass +class ObsoleteKwargException(DashException): + pass + + class NoLayoutException(DashException): pass From 37cf546a0edebd38f2851633421d2a2b41440540 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Jun 2019 23:12:40 -0400 Subject: [PATCH 08/34] remove leading '/' from default assets_url_path it's more confusing to describe with it present - and we always strip it off in practice --- dash/dash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index 6a2305e968..cef5aa03a8 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -91,7 +91,7 @@ def __init__( name='__main__', server=True, assets_folder='assets', - assets_url_path='/assets', + assets_url_path='assets', assets_ignore='', include_assets_files=True, url_base_pathname=None, From b4185e2a5068644ec427d00f40f96a642a38a3b2 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 4 Jun 2019 23:13:45 -0400 Subject: [PATCH 09/34] docstring for Dash class --- dash/dash.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/dash/dash.py b/dash/dash.py index cef5aa03a8..d289a82026 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -86,6 +86,123 @@ class _NoUpdate(object): # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-locals class Dash(object): + """ + Dash is a framework for building analytical web applications. + No JavaScript required. + + If a parameter can be set by an environment variable, that is listed too. + Values provided here take precedence over environment variables. + + :param name: The name Flask should use for your app. Even if you provide + your own ``server``, ``name`` will be used to help find assets. + Typically ``__name__`` (the magic global var, not a string) is the + best value to use. Default ``'__main__'``, env: ``DASH_APP_NAME`` + :type name: string + + :param server: Sets the Flask server for your app. There are three options: + ``True`` (default): Dash will create a new server + ``False``: The server will be added later via ``app.init_app(server)`` + where ``server`` is a ``flask.Flask`` instance. + ``flask.Flask``: use this pre-existing Flask server. + :type server: boolean or flask.Flask + + :param assets_folder: a path, relative to the current working directory, + for extra files to be used in the browser. Default ``'assets'`` By + default all .js and .css will be loaded immediately, and other files + such as images will be served if requested. + :type assets_folder: string + + :param assets_url_path: The local urls for assets will be: + ``requests_pathname_prefix + assets_url_path + '/' + asset_path`` + where ``asset_path`` is the path to a file inside ``assets_folder``. + Default ``'assets'``. + :type asset_url_path: string + + :param assets_ignore: A regexp, as a string to pass to ``re.compile``, for + assets to omit from immediate loading. Ignored files will still be + served if specifically requested. You cannot use this to prevent access + to sensitive files. + :type assets_ignore: string + + :param assets_external_path: an absolute URL from which to load assets. + Use with ``serve_locally=False``. Dash can still find js and css to + automatically load if you also keep local copies in your assets + folder that Dash can index, but external serving can improve + performance and reduce load on the Dash server. + env: ``DASH_ASSETS_EXTERNAL_PATH`` + :type assets_external_path: string + + :param include_assets_files: Default ``True``, set to ``False`` to prevent + immediate loading of any assets. Assets will still be served if + specifically requested. You cannot use this to prevent access + to sensitive files. env: ``DASH_INCLUDE_ASSETS_FILES`` + :type include_assets_files: boolean + + :param url_base_pathname: A local URL prefix to use app-wide. + Default ``'/'``. Both `requests_pathname_prefix` and + `routes_pathname_prefix` default to `url_base_pathname`. + env: ``DASH_URL_BASE_PATHNAME`` + :type url_base_pathname: string + + :param requests_pathname_prefix: A local URL prefix for file requests. + Defaults to `url_base_pathname`, and must end with + `routes_pathname_prefix`. env: ``DASH_REQUESTS_PATHNAME_PREFIX`` + :type requests_pathname_prefix: string + + :param routes_pathname_prefix: A local URL prefix for JSON requests. + Defaults to ``url_base_pathname``, and must start and end + with ``'/'``. env: ``DASH_ROUTES_PATHNAME_PREFIX`` + :type routes_pathname_prefix: string + + :param serve_locally: If ``True`` (default), assets and dependencies + (Dash and Component js and css) will be served from local URLs. + If ``False`` we will use CDN links where available. + :type serve_locally: boolean + + :param compress: Use gzip to compress files and data served by Flask. + Default ``True`` + :type compress: boolean + + :param meta_tags: html tags to be added to the index page. + Each dict should have the attributes and values for one tag, eg: + ``{'name': 'description', 'content': 'My App'}`` + :type meta_tags: list of dicts + + :param index_string: Override the standard Dash index page. + Must contain the correct insertion markers to interpolate various + content into it depending on the app config and components used. + See https://dash.plot.ly/external-resources for details. + :type index_string: string + + :param external_scripts: Additional JS files to load with the page. + Each entry can be a string (the URL) or a dict with ``src`` (the URL) + and optionally other `` + + + ''' - app.layout = html.Div(id='content') + app.layout = html.Div('Dash app', id='app') - self.startServer(app) + dash_duo.start_server(app) - meta = self.driver.find_elements_by_tag_name('meta') + assert dash_duo.find_element('#custom-header').text == 'My custom header' + assert dash_duo.find_element('#custom-footer').text == 'My custom footer' + assert dash_duo.wait_for_element('#add').text == 'Got added' - # -2 for the meta charset and http-equiv. - self.assertEqual(len(metas), len(meta) - 2, 'Not enough meta tags') + dash_duo.percy_snapshot('custom-index') - for i in range(2, len(meta)): - meta_tag = meta[i] - meta_info = metas[i - 2] - name = meta_tag.get_attribute('name') - content = meta_tag.get_attribute('content') - self.assertEqual(name, meta_info['name']) - self.assertEqual(content, meta_info['content']) - def test_index_customization(self): - app = Dash() +def test_invalid_index_string(dash_duo): + app = Dash() + def will_raise(): app.index_string = ''' @@ -349,638 +355,568 @@ def test_index_customization(self):
My custom header
- {%app_entry%}
- {%config%} - {%scripts%} - {%renderer%}
- - ''' - app.layout = html.Div('Dash app', id='app') - - self.startServer(app) - - time.sleep(0.5) - - header = self.wait_for_element_by_id('custom-header') - footer = self.wait_for_element_by_id('custom-footer') - - self.assertEqual('My custom header', header.text) - self.assertEqual('My custom footer', footer.text) + with pytest.raises(Exception) as err: + will_raise() - add = self.wait_for_element_by_id('add') + exc_msg = str(err.value) + assert '{%app_entry%}' in exc_msg + assert '{%config%}' in exc_msg + assert '{%scripts%}' in exc_msg - self.assertEqual('Got added', add.text) + app.layout = html.Div('Hello World', id='a') - self.percy_snapshot('custom-index') + dash_duo.start_server(app) + assert dash_duo.find_element('#a').text == 'Hello World' - def test_invalid_index_string(self): - app = Dash() - def will_raise(): - app.index_string = ''' - - - - {%metas%} - {%title%} - {%favicon%} - {%css%} - - -
My custom header
-
-
-
- - - ''' +def test_func_layout_accepted(dash_duo): + app = Dash() - with self.assertRaises(Exception) as context: - will_raise() + def create_layout(): + return html.Div('Hello World', id='a') + app.layout = create_layout - app.layout = html.Div() - self.startServer(app) + dash_duo.start_server(app) + assert dash_duo.find_element('#a').text == 'Hello World' - exc_msg = str(context.exception) - self.assertTrue('{%app_entry%}' in exc_msg) - self.assertTrue('{%config%}' in exc_msg) - self.assertTrue('{%scripts%}' in exc_msg) - time.sleep(0.5) - def test_func_layout_accepted(self): +def test_multi_output(dash_duo): + app = Dash(__name__) - app = Dash() + app.layout = html.Div([ + html.Button('OUTPUT', id='output-btn'), - def create_layout(): - return html.Div('Hello World') - app.layout = create_layout - - self.startServer(app) - time.sleep(0.5) - - def test_multi_output(self): - 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.Table([ + html.Thead([ + html.Tr([html.Th('Output 1'), html.Th('Output 2')]) ]), - - 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): + 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) + + # Test that a multi output can't be included in a single output + 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 n_clicks, n_clicks_timestamp + return 'something else' - # Dummy callback for DuplicateCallbackOutput test. - @app.callback(Output('output3', 'children'), + assert 'output1' in err.value.args[0] + + # Test a multi output cannot contain a used single output + with pytest.raises(DuplicateCallbackOutput) as err: + @app.callback([Output('output3', 'children'), + Output('output4', 'children')], [Input('output-btn', 'n_clicks')]) - def dummy_callback(n_clicks): + def on_click_duplicate_multi(n_clicks): if n_clicks is None: raise PreventUpdate - return 'Output 3: {}'.format(n_clicks) - - # Test that a multi output can't be included in a single output - with self.assertRaises(DuplicateCallbackOutput) as context: - @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' - return 'something else' + assert 'output3' in err.value.args[0] - self.assertTrue('output1' in context.exception.args[0]) - - # Test a multi output cannot contain a used single output - with self.assertRaises(DuplicateCallbackOutput) as context: - @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' + 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 - self.assertTrue('output3' in context.exception.args[0]) + assert 'output5' in err.value.args[0] - with self.assertRaises(DuplicateCallbackOutput) as context: - @app.callback([Output('output5', 'children'), - Output('output5', 'children')], - [Input('output-btn', 'n_clicks')]) - def on_click_same_output(n_clicks): - return n_clicks + 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 - self.assertTrue('output5' in context.exception.args[0]) + assert ( + '{\'output1.children\'}' in err.value.args[0] + or "set(['output1.children'])" in err.value.args[0] + ) - with self.assertRaises(DuplicateCallbackOutput) as context: - @app.callback([Output('output1', 'children'), - Output('output5', 'children')], - [Input('output-btn', 'n_clicks')]) - def overlapping_multi_output(n_clicks): - return n_clicks + dash_duo.start_server(app) - self.assertTrue( - '{\'output1.children\'}' in context.exception.args[0] - or "set(['output1.children'])" in context.exception.args[0] - ) + t = time.time() - self.startServer(app) + btn = dash_duo.find_element('#output-btn') + btn.click() + time.sleep(1) - t = time.time() + dash_duo.wait_for_text_to_equal('#output1', '1') - btn = self.wait_for_element_by_id('output-btn') - btn.click() - time.sleep(1) + assert int(dash_duo.find_element('#output2').text) > t - self.wait_for_text_to_equal('#output1', '1') - output2 = self.wait_for_element_by_css_selector('#output2') - self.assertGreater(int(output2.text), t) +def test_multi_output_no_update(dash_duo): + app = Dash(__name__) - def test_multi_output_no_update(self): - app = Dash(__name__) + app.layout = html.Div([ + html.Button('B', 'btn'), + html.P('initial1', 'n1'), + html.P('initial2', 'n2'), + html.P('initial3', 'n3') + ]) - app.layout = html.Div([ - html.Button('B', 'btn'), - html.P('initial1', 'n1'), - html.P('initial2', 'n2'), - html.P('initial3', 'n3') - ]) + @app.callback([Output('n1', 'children'), + Output('n2', 'children'), + Output('n3', 'children')], + [Input('btn', 'n_clicks')]) + def show_clicks(n): + # partial or complete cancelation of updates via no_update + return [ + no_update if n and n > 4 else n, + no_update if n and n > 2 else n, + no_update + ] - @app.callback([Output('n1', 'children'), - Output('n2', 'children'), - Output('n3', 'children')], - [Input('btn', 'n_clicks')]) - def show_clicks(n): - # partial or complete cancelation of updates via no_update - return [ - no_update if n and n > 4 else n, - no_update if n and n > 2 else n, - no_update - ] - - self.startServer(app) - - btn = self.wait_for_element_by_id('btn') - for _ in range(10): - btn.click() - - self.wait_for_text_to_equal('#n1', '4') - self.wait_for_text_to_equal('#n2', '2') - self.wait_for_text_to_equal('#n3', 'initial3') - - def test_no_update_chains(self): - app = Dash(__name__) - - app.layout = html.Div([ - dcc.Input(id='a_in', value='a'), - dcc.Input(id='b_in', value='b'), - html.P('', id='a_out'), - html.P('', id='a_out_short'), - html.P('', id='b_out'), - html.P('', id='ab_out') - ]) + dash_duo.start_server(app) - @app.callback([Output('a_out', 'children'), - Output('a_out_short', 'children')], - [Input('a_in', 'value')]) - def a_out(a): - return (a, a if len(a) < 3 else no_update) - - @app.callback(Output('b_out', 'children'), [Input('b_in', 'value')]) - def b_out(b): - return b - - @app.callback(Output('ab_out', 'children'), - [Input('a_out_short', 'children')], - [State('b_out', 'children')]) - def ab_out(a, b): - return a + ' ' + b - - self.startServer(app) - - a_in = self.wait_for_element_by_id('a_in') - b_in = self.wait_for_element_by_id('b_in') - - b_in.send_keys('b') - a_in.send_keys('a') - self.wait_for_text_to_equal('#a_out', 'aa') - self.wait_for_text_to_equal('#b_out', 'bb') - self.wait_for_text_to_equal('#a_out_short', 'aa') - self.wait_for_text_to_equal('#ab_out', 'aa bb') - - b_in.send_keys('b') - a_in.send_keys('a') - self.wait_for_text_to_equal('#a_out', 'aaa') - self.wait_for_text_to_equal('#b_out', 'bbb') - self.wait_for_text_to_equal('#a_out_short', 'aa') - # ab_out has not been triggered because a_out_short received no_update - self.wait_for_text_to_equal('#ab_out', 'aa bb') - - b_in.send_keys('b') - a_in.send_keys(Keys.END) - a_in.send_keys(Keys.BACKSPACE) - self.wait_for_text_to_equal('#a_out', 'aa') - self.wait_for_text_to_equal('#b_out', 'bbbb') - self.wait_for_text_to_equal('#a_out_short', 'aa') - # now ab_out *is* triggered - a_out_short got a new value - # even though that value is the same as the last value it got - self.wait_for_text_to_equal('#ab_out', 'aa bbbb') - - def test_with_custom_renderer(self): - app = Dash(__name__) + btn = dash_duo.find_element('#btn') + for _ in range(10): + btn.click() - app.index_string = ''' - - - - {%metas%} - {%title%} - {%favicon%} - {%css%} - - -
Testing custom DashRenderer
- {%app_entry%} -
- {%config%} - {%scripts%} - -
-
With request hooks
- - - ''' + }, + request_post: () => { + var output = document.getElementById('output-post') + if(output) { + output.innerHTML = 'request_post ran!'; + } + } + }) + + +
With request hooks
+ + + ''' + + app.layout = html.Div([ + dcc.Input(id='input', value='initial value'), + html.Div( + html.Div([ + html.Div(id='output-1'), + html.Div(id='output-pre'), + html.Div(id='output-post') + ]) + ) + ]) - app.layout = html.Div([ - dcc.Input( - id='input', - value='initial value' - ), - html.Div( - html.Div([ - html.Div(id='output-1'), - html.Div(id='output-pre'), - html.Div(id='output-post') - ]) - ) - ]) + @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + def update_output(value): + return value - @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) - def update_output(value): - return value + dash_duo.start_server(app) - self.startServer(app) + input1 = dash_duo.find_element('#input') + dash_duo.clear_input(input1) - input1 = self.wait_for_element_by_id('input') - chain = (ActionChains(self.driver) - .click(input1) - .send_keys(Keys.HOME) - .key_down(Keys.SHIFT) - .send_keys(Keys.END) - .key_up(Keys.SHIFT) - .send_keys(Keys.DELETE)) - chain.perform() + input1.send_keys('fire request hooks') - input1.send_keys('fire request hooks') + dash_duo.wait_for_text_to_equal('#output-1', 'fire request hooks') + assert dash_duo.find_element('#output-pre').text == 'request_pre!!!' + assert dash_duo.find_element('#output-post').text == 'request_post ran!' - 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-post', 'request_post changed this text!') + dash_duo.percy_snapshot(name='request-hooks') - self.percy_snapshot(name='request-hooks') - def test_with_custom_renderer_interpolated(self): +def test_with_custom_renderer_interpolated(dash_duo): - renderer = ''' - - ''' - - class CustomDash(Dash): - - def interpolate_index(self, **kwargs): - return ''' - - - - My App - - - -
My custom header
- {app_entry} - {config} - {scripts} - {renderer} - - - - '''.format( - app_entry=kwargs['app_entry'], - config=kwargs['config'], - scripts=kwargs['scripts'], - renderer=renderer) - - app = CustomDash() - - app.layout = html.Div([ - dcc.Input( - id='input', - value='initial value' - ), - html.Div( - html.Div([ - html.Div(id='output-1'), - html.Div(id='output-pre'), - html.Div(id='output-post') - ]) - ) - ]) + }, + request_post: () => { + var output = document.getElementById('output-post') + if(output) { + output.innerHTML = 'request_post!!!'; + } + } + }) + + ''' + + class CustomDash(Dash): + + def interpolate_index(self, **kwargs): + return ''' + + + + My App + + + +
My custom header
+ {app_entry} + {config} + {scripts} + {renderer} + + + + '''.format( + app_entry=kwargs['app_entry'], + config=kwargs['config'], + scripts=kwargs['scripts'], + renderer=renderer) + + app = CustomDash() + + app.layout = html.Div([ + dcc.Input(id='input', value='initial value'), + html.Div( + html.Div([ + html.Div(id='output-1'), + html.Div(id='output-pre'), + html.Div(id='output-post') + ]) + ) + ]) - @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) - def update_output(value): - return value + @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + def update_output(value): + return value - self.startServer(app) + dash_duo.start_server(app) - input1 = self.wait_for_element_by_id('input') - chain = (ActionChains(self.driver) - .click(input1) - .send_keys(Keys.HOME) - .key_down(Keys.SHIFT) - .send_keys(Keys.END) - .key_up(Keys.SHIFT) - .send_keys(Keys.DELETE)) - chain.perform() + input1 = dash_duo.find_element('#input') + dash_duo.clear_input(input1) - input1.send_keys('fire request hooks') + input1.send_keys('fire request hooks') - 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-post', 'request_post changed this text!') + dash_duo.wait_for_text_to_equal('#output-1', 'fire request hooks') + assert dash_duo.find_element('#output-pre').text == 'request_pre was here!' + assert dash_duo.find_element('#output-post').text == 'request_post!!!' - self.percy_snapshot(name='request-hooks interpolated') + dash_duo.percy_snapshot(name='request-hooks interpolated') - def test_modified_response(self): - 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' +def test_modified_response(dash_duo): + app = Dash(__name__) + app.layout = html.Div([ + dcc.Input(id='input', value='ab'), + html.Div(id='output') + ]) - self.startServer(app) - self.wait_for_text_to_equal('#output', 'ab - output') - input1 = self.wait_for_element_by_id('input') + @app.callback(Output('output', 'children'), [Input('input', 'value')]) + def update_output(value): + callback_context.response.set_cookie( + 'dash cookie', value + ' - cookie') + return value + ' - output' - input1.send_keys('cd') + dash_duo.start_server(app) + dash_duo.wait_for_text_to_equal('#output', 'ab - output') + input1 = dash_duo.find_element('#input') - self.wait_for_text_to_equal('#output', 'abcd - output') - cookie = self.driver.get_cookie('dash cookie') - # cookie gets json encoded - self.assertEqual(cookie['value'], '"abcd - cookie"') + input1.send_keys('cd') - self.assertTrue(self.is_console_clean()) + 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"' - def test_late_component_register(self): - app = Dash() + assert not dash_duo.get_logs() - app.layout = html.Div([ - html.Button('Click me to put a dcc ', id='btn-insert'), - html.Div(id='output') - ]) - @app.callback(Output('output', 'children'), - [Input('btn-insert', 'n_clicks')]) - def update_output(value): - if value is None: - raise PreventUpdate +def test_late_component_register(dash_duo): + app = Dash() - return dcc.Input(id='inserted-input') + app.layout = html.Div([ + html.Button('Click me to put a dcc ', id='btn-insert'), + html.Div(id='output') + ]) - self.startServer(app) + @app.callback(Output('output', 'children'), + [Input('btn-insert', 'n_clicks')]) + def update_output(value): + if value is None: + raise PreventUpdate - btn = self.wait_for_element_by_css_selector('#btn-insert') - btn.click() - time.sleep(1) + return dcc.Input(id='inserted-input') - self.wait_for_element_by_css_selector('#inserted-input') + dash_duo.start_server(app) - def test_output_input_invalid_callback(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Div('child', id='input-output'), - html.Div(id='out') - ]) + btn = dash_duo.find_element('#btn-insert') + btn.click() - with self.assertRaises(CallbackException) as context: - @app.callback(Output('input-output', 'children'), - [Input('input-output', 'children')]) - def failure(children): - pass + dash_duo.find_element('#inserted-input') - self.assertEqual( - 'Same output and input: input-output.children', - context.exception.args[0] - ) - # Multi output version. - with self.assertRaises(CallbackException) as context: - @app.callback([Output('out', 'children'), - Output('input-output', 'children')], - [Input('input-output', 'children')]) - def failure2(children): - pass - - self.assertEqual( - 'Same output and input: input-output.children', - context.exception.args[0] - ) +def test_output_input_invalid_callback(): + app = Dash(__name__) + app.layout = html.Div([ + html.Div('child', id='input-output'), + html.Div(id='out') + ]) - def test_callback_dep_types(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Div('child', id='in'), - html.Div('state', id='state'), - html.Div(id='out') - ]) + with pytest.raises(CallbackException) as err: + @app.callback(Output('input-output', 'children'), + [Input('input-output', 'children')]) + def failure(children): + pass - with self.assertRaises(IncorrectTypeException): - @app.callback([[Output('out', 'children')]], # extra nesting - [Input('in', 'children')]) - def f(i): - return i - - with self.assertRaises(IncorrectTypeException): - @app.callback(Output('out', 'children'), - Input('in', 'children')) # no nesting - def f2(i): - return i - - with self.assertRaises(IncorrectTypeException): - @app.callback(Output('out', 'children'), - [Input('in', 'children')], - State('state', 'children')) # no nesting - def f3(i): - return i - - # all OK with tuples - @app.callback((Output('out', 'children'),), - (Input('in', 'children'),), - (State('state', 'children'),)) - def f4(i): - return i + msg = 'Same output and input: input-output.children' + assert err.value.args[0] == msg - def test_callback_return_validation(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Div(id='a'), - html.Div(id='b'), - html.Div(id='c'), - html.Div(id='d'), - html.Div(id='e'), - html.Div(id='f') - ]) + # 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 - @app.callback(Output('b', 'children'), [Input('a', 'children')]) - def single(a): - # anything non-serializable, really - return set([1]) + msg = 'Same output and input: input-output.children' + assert err.value.args[0] == msg - with self.assertRaises(InvalidCallbackReturnValue): - single('aaa') - @app.callback([Output('c', 'children'), Output('d', 'children')], - [Input('a', 'children')]) - def multi(a): - # non-serializable inside a list - return [1, set([2])] +def test_callback_dep_types(): + app = Dash(__name__) + app.layout = html.Div([ + html.Div('child', id='in'), + html.Div('state', id='state'), + html.Div(id='out') + ]) - with self.assertRaises(InvalidCallbackReturnValue): - multi('aaa') + with pytest.raises(IncorrectTypeException): + @app.callback([[Output('out', 'children')]], # extra nesting + [Input('in', 'children')]) + def f(i): + return i - @app.callback([Output('e', 'children'), Output('f', 'children')], - [Input('a', 'children')]) - def multi2(a): - # wrong-length list - return ['abc'] + with pytest.raises(IncorrectTypeException): + @app.callback(Output('out', 'children'), + Input('in', 'children')) # no nesting + def f2(i): + return i - with self.assertRaises(InvalidCallbackReturnValue): - multi2('aaa') + with pytest.raises(IncorrectTypeException): + @app.callback(Output('out', 'children'), + [Input('in', 'children')], + State('state', 'children')) # no nesting + def f3(i): + return i - def test_callback_context(self): - app = Dash(__name__) + # all OK with tuples + @app.callback((Output('out', 'children'),), + (Input('in', 'children'),), + (State('state', 'children'),)) + def f4(i): + return i + + +def test_callback_return_validation(): + app = Dash(__name__) + app.layout = html.Div([ + html.Div(id='a'), + html.Div(id='b'), + html.Div(id='c'), + html.Div(id='d'), + html.Div(id='e'), + html.Div(id='f') + ]) + + @app.callback(Output('b', 'children'), [Input('a', 'children')]) + def single(a): + # anything non-serializable, really + return set([1]) + + with pytest.raises(InvalidCallbackReturnValue): + single('aaa') + + @app.callback([Output('c', 'children'), Output('d', 'children')], + [Input('a', 'children')]) + def multi(a): + # non-serializable inside a list + return [1, set([2])] + + with pytest.raises(InvalidCallbackReturnValue): + multi('aaa') + + @app.callback([Output('e', 'children'), Output('f', 'children')], + [Input('a', 'children')]) + def multi2(a): + # wrong-length list + return ['abc'] + + with pytest.raises(InvalidCallbackReturnValue): + multi2('aaa') + + +def test_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(x, id=x) for x 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'] + ) - btns = ['btn-{}'.format(x) for x in range(1, 6)] + dash_duo.start_server(app) - app.layout = html.Div([ - html.Div([ - html.Button(x, id=x) for x in btns - ]), - html.Div(id='output'), - ]) + btn_elements = [ + dash_duo.find_element('#' + x) 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 - trigger = callback_context.triggered[0] - return 'Just clicked {} for the {} time!'.format( - trigger['prop_id'].split('.')[0], trigger['value'] + for i in range(1, 5): + for j, btn in enumerate(btns): + btn_elements[j].click() + dash_duo.wait_for_text_to_equal( + '#output', + 'Just clicked {} for the {} time!'.format( + btn, i + ) ) - self.startServer(app) - - btn_elements = [ - self.wait_for_element_by_id(x) for x in btns - ] - - for i in range(1, 5): - for j, btn in enumerate(btns): - btn_elements[j].click() - self.wait_for_text_to_equal( - '#output', - 'Just clicked {} for the {} time!'.format( - btn, i - ) - ) - def test_no_callback_context(self): - for attr in ['inputs', 'states', 'triggered', 'response']: - with self.assertRaises(MissingCallbackContextException): - getattr(callback_context, attr) +def test_no_callback_context(): + for attr in ['inputs', 'states', 'triggered', 'response']: + with pytest.raises(MissingCallbackContextException): + getattr(callback_context, attr) From 62a18d3cf62921a90155c048fb2980b845644f4e Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 11 Jun 2019 12:14:20 -0400 Subject: [PATCH 33/34] add some tcids --- tests/integration/test_integration.py | 44 +++++++++++++-------------- tests/unit/dash/test_dash_import.py | 2 +- tests/unit/dash/test_utils.py | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index d5fa28e02d..3c1a063fb1 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -24,7 +24,7 @@ from dash.testing.wait import until -def test_simple_callback(dash_duo): +def test_inin001_simple_callback(dash_duo): app = Dash(__name__) app.layout = html.Div([ dcc.Input(id='input', value='initial value'), @@ -57,7 +57,7 @@ def update_output(value): assert not dash_duo.get_logs() -def test_wildcard_callback(dash_duo): +def test_inin002_wildcard_callback(dash_duo): app = Dash(__name__) app.layout = html.Div([ dcc.Input(id='input', value='initial value'), @@ -102,7 +102,7 @@ def update_text(data): assert not dash_duo.get_logs() -def test_aborted_callback(dash_duo): +def test_inin003_aborted_callback(dash_duo): """ Raising PreventUpdate OR returning no_update prevents update and triggering dependencies @@ -157,7 +157,7 @@ def callback2(value): dash_duo.percy_snapshot(name='aborted') -def test_wildcard_data_attributes(dash_duo): +def test_inin004_wildcard_data_attributes(dash_duo): app = Dash() test_time = datetime.datetime(2012, 1, 10, 2, 3) test_date = datetime.date(test_time.year, test_time.month, @@ -216,7 +216,7 @@ def test_wildcard_data_attributes(dash_duo): assert not dash_duo.get_logs() -def test_no_props_component(dash_duo): +def test_inin005_no_props_component(dash_duo): app = Dash() app.layout = html.Div([ dash_dangerously_set_inner_html.DangerouslySetInnerHTML(''' @@ -230,7 +230,7 @@ def test_no_props_component(dash_duo): dash_duo.percy_snapshot(name='no-props-component') -def test_flow_component(dash_duo): +def test_inin006_flow_component(dash_duo): app = Dash() app.layout = html.Div([ @@ -267,7 +267,7 @@ def display_output(react_value, flow_value): dash_duo.percy_snapshot(name='flowtype') -def test_meta_tags(dash_duo): +def test_inin007_meta_tags(dash_duo): metas = [ {'name': 'description', 'content': 'my dash app'}, {'name': 'custom', 'content': 'customized'}, @@ -291,7 +291,7 @@ def test_meta_tags(dash_duo): assert meta_tag.get_attribute('content') == meta_info['content'] -def test_index_customization(dash_duo): +def test_inin008_index_customization(dash_duo): app = Dash() app.index_string = ''' @@ -339,7 +339,7 @@ def test_index_customization(dash_duo): dash_duo.percy_snapshot('custom-index') -def test_invalid_index_string(dash_duo): +def test_inin009_invalid_index_string(dash_duo): app = Dash() def will_raise(): @@ -375,7 +375,7 @@ def will_raise(): assert dash_duo.find_element('#a').text == 'Hello World' -def test_func_layout_accepted(dash_duo): +def test_inin010_func_layout_accepted(dash_duo): app = Dash() def create_layout(): @@ -386,7 +386,7 @@ def create_layout(): assert dash_duo.find_element('#a').text == 'Hello World' -def test_multi_output(dash_duo): +def test_inin011_multi_output(dash_duo): app = Dash(__name__) app.layout = html.Div([ @@ -485,7 +485,7 @@ def overlapping_multi_output(n_clicks): assert int(dash_duo.find_element('#output2').text) > t -def test_multi_output_no_update(dash_duo): +def test_inin012_multi_output_no_update(dash_duo): app = Dash(__name__) app.layout = html.Div([ @@ -518,7 +518,7 @@ def show_clicks(n): dash_duo.wait_for_text_to_equal('#n3', 'initial3') -def test_no_update_chains(dash_duo): +def test_inin013_no_update_chains(dash_duo): app = Dash(__name__) app.layout = html.Div([ @@ -577,7 +577,7 @@ def ab_out(a, b): dash_duo.wait_for_text_to_equal('#ab_out', 'aa bbbb') -def test_with_custom_renderer(dash_duo): +def test_inin014_with_custom_renderer(dash_duo): app = Dash(__name__) app.index_string = ''' @@ -647,7 +647,7 @@ def update_output(value): dash_duo.percy_snapshot(name='request-hooks') -def test_with_custom_renderer_interpolated(dash_duo): +def test_inin015_with_custom_renderer_interpolated(dash_duo): renderer = '''