diff --git a/dev_requirements.txt b/dev_requirements.txt index d185829a..d79977f2 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -11,7 +11,7 @@ dpd-static-support>=0.0.4 grip pandas pylint -pytest>=3.6 +pytest>=4.6 pytest-django pytest-cov python-coveralls diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 73d6c6b1..64aab965 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -29,6 +29,7 @@ import inspect from dash import Dash +from dash._utils import split_callback_id from flask import Flask from django.urls import reverse @@ -288,7 +289,9 @@ def __init__(self): def after_request(self, *args, **kwargs): pass def errorhandler(self, *args, **kwargs): # pylint: disable=no-self-use - return args[0] + def eh_func(f): + return args[0] + return eh_func def add_url_rule(self, *args, **kwargs): route = kwargs['endpoint'] self.endpoints[route] = kwargs @@ -555,6 +558,10 @@ def dispatch_with_args(self, body, argMap): if da: da.update_current_state(c['id'], c['property'], v) + # Dash 1.11 introduces a set of outputs + outputs_list = body.get('outputs') or split_callback_id(output) + argMap['outputs_list'] = outputs_list + # Special: intercept case of insufficient arguments # This happens when a propery has been updated with a pipe component # TODO see if this can be attacked from the client end diff --git a/django_plotly_dash/tests.py b/django_plotly_dash/tests.py index 6b4700d5..5ff82d25 100644 --- a/django_plotly_dash/tests.py +++ b/django_plotly_dash/tests.py @@ -32,6 +32,7 @@ #pylint: disable=bare-except + def test_dash_app(): 'Test the import and formation of the dash app orm wrappers' @@ -42,6 +43,7 @@ def test_dash_app(): assert stateless_a.app_name assert str(stateless_a) == stateless_a.app_name + def test_util_error_cases(settings): 'Test handling of missing settings' @@ -61,6 +63,7 @@ def test_util_error_cases(settings): assert http_endpoint("fred") == '^dpd/views/fred/$' assert not insert_demo_migrations() + def test_demo_routing(): 'Test configuration options for the demo' @@ -68,6 +71,7 @@ def test_demo_routing(): assert pipe_ws_endpoint_name() == 'ws/channel' assert insert_demo_migrations() + def test_local_serving(settings): 'Test local serve settings' @@ -76,6 +80,7 @@ def test_local_serving(settings): assert static_asset_root() == 'dpd/assets' assert full_asset_path('fred.jim', 'harry') == 'dpd/assets/fred/jim/harry' + @pytest.mark.django_db def test_direct_access(client): 'Check direct use of a stateless application using demo test data' @@ -106,6 +111,7 @@ def test_direct_access(client): assert did_fail + @pytest.mark.django_db def test_updating(client): 'Check updating of an app using demo test data' @@ -118,7 +124,7 @@ def test_updating(client): ('', {'ident':'simpleexample-1'}),]: url = reverse('the_django_plotly_dash:%s%s' % (prefix, route_name), kwargs=arg_map) - response = client.post(url, json.dumps({'output':{'id':'output-size', 'property':'children'}, + response = client.post(url, json.dumps({'output': 'output-size.children', 'inputs':[{'id':'dropdown-color', 'property':'value', 'value':'blue'}, @@ -127,9 +133,10 @@ def test_updating(client): 'value':'medium'}, ]}), content_type="application/json") - assert response.content == b'{"response": {"props": {"children": "The chosen T-shirt is a medium blue one."}}}' + assert response.content == b'{"response": {"output-size": {"children": "The chosen T-shirt is a medium blue one."}}, "multi": true}' assert response.status_code == 200 + @pytest.mark.django_db def test_injection_app_access(client): 'Check direct use of a stateless application using demo test data' @@ -160,6 +167,7 @@ def test_injection_app_access(client): assert did_fail + @pytest.mark.django_db def test_injection_updating_multiple_callbacks(client): 'Check updating of an app using demo test data for multiple callbacks' @@ -192,6 +200,7 @@ def test_injection_updating_multiple_callbacks(client): assert 'children' in resp_detail['output-two'] assert resp_detail['output-two']['children'] == "Output 2: 10 purple-ish yellow with a hint of greeny orange" + @pytest.mark.django_db def test_injection_updating(client): 'Check updating of an app using demo test data' @@ -203,13 +212,14 @@ def test_injection_updating(client): for prefix, arg_map in [('app-', {'ident':'dash_example_1'}),]: url = reverse('the_django_plotly_dash:%s%s' % (prefix, route_name), kwargs=arg_map) - response = client.post(url, json.dumps({'output':{'id':'test-output-div', 'property':'children'}, + response = client.post(url, json.dumps({#'output':{'id':'test-output-div', 'property':'children'}, + 'output': "test-output-div.children", 'inputs':[{'id':'my-dropdown1', 'property':'value', 'value':'TestIt'}, ]}), content_type="application/json") - rStart = b'{"response": {"props": {"children":' + rStart = b'{"response": {"test-output-div": {"children": [{"props": {"id": "line-area-graph2"' assert response.content[:len(rStart)] == rStart assert response.status_code == 200 @@ -221,19 +231,19 @@ def test_injection_updating(client): 'value':'TestIt'}, ]}), content_type="application/json") - rStart = b'{"response": {"props": {"children":' + rStart = b'{"response": {"test-output-div": {"children": [{"props": {"id": "line-area-graph2"' assert response.content[:len(rStart)] == rStart assert response.status_code == 200 - # Second variant has a single-entry mulitple property output + # Second variant has a single-entry mulitple property output response = client.post(url, json.dumps({'output':'..test-output-div.children..', 'inputs':[{'id':'my-dropdown1', 'property':'value', 'value':'TestIt'}, ]}), content_type="application/json") - rStart = b'{"response": {"props": {"children":' + rStart = b'{"response": {"test-output-div": {"children": {"props": {"id": "line-area-graph2"' assert response.content[:len(rStart)] == rStart assert response.status_code == 200 @@ -241,7 +251,7 @@ def test_injection_updating(client): have_thrown = False try: - client.post(url, json.dumps({'output':{'id':'test-output-div2', 'property':'children'}, + client.post(url, json.dumps({'output': 'test-output-div2.children', 'inputs':[{'id':'my-dropdown2', 'property':'value', 'value':'TestIt'}, @@ -255,18 +265,19 @@ def test_injection_updating(client): session['django_plotly_dash'] = {'django_to_dash_context': 'Test 789 content'} session.save() - response3 = client.post(url, json.dumps({'output':{'id':'test-output-div2', 'property':'children'}, + response3 = client.post(url, json.dumps({'output': 'test-output-div2.children', 'inputs':[{'id':'my-dropdown2', 'property':'value', 'value':'TestIt'}, ]}), content_type="application/json") - rStart3 = b'{"response": {"props": {"children":' + rStart3 = b'{"response": {"test-output-div2": {"children": [{"props": {"children": ["You have ' assert response3.content[:len(rStart3)] == rStart3 assert response3.status_code == 200 assert response3.content.find(b'Test 789 content') > 0 + @pytest.mark.django_db def test_argument_settings(settings, client): 'Test the setting that controls how initial arguments are propagated through to the dash app' @@ -308,6 +319,7 @@ def test_argument_settings(settings, client): assert store_initial_arguments(client, None) is None assert get_initial_arguments(client, None) is None + def test_stateless_lookup_noop(): 'Test no-op stateless lookup' @@ -318,6 +330,7 @@ def test_stateless_lookup_noop(): with pytest.raises(ImportError): lh_hook("not an app") + def test_middleware_artifacts(): 'Import and vaguely exercise middleware objects' @@ -334,6 +347,7 @@ def test_middleware_artifacts(): assert cc._encode("fred") == b'fred' + def test_finders(): 'Import and vaguely exercise staticfiles finders' @@ -347,6 +361,7 @@ def test_finders(): assert dadf is not None assert daf is not None + @pytest.mark.django_db def test_app_loading(client): diff --git a/django_plotly_dash/version.py b/django_plotly_dash/version.py index 84b27293..0d7d52f2 100644 --- a/django_plotly_dash/version.py +++ b/django_plotly_dash/version.py @@ -23,4 +23,4 @@ ''' -__version__ = "1.3.1" +__version__ = "1.4.0" diff --git a/requirements.txt b/requirements.txt index b1017ff1..de2ed3a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -dash<1.11 -dash-core-components==1.9.0 +dash>=1.11 +dash-core-components dash-html-components -dash-renderer==1.3.0 +dash-renderer plotly dpd-components diff --git a/setup.py b/setup.py index 58fb2d46..be023957 100644 --- a/setup.py +++ b/setup.py @@ -41,10 +41,10 @@ 'Documentation': 'http://django-plotly-dash.readthedocs.io/', }, install_requires = ['plotly', - 'dash<1.11', - 'dash-core-components==1.9.0', + 'dash>=1.11', + 'dash-core-components', 'dash-html-components', - 'dash-renderer==1.3.0', + 'dash-renderer', 'dpd-components', 'Django>=3', 'Flask>=1.0.2'],