diff --git a/dash/_validate.py b/dash/_validate.py index 5b1cdd0548..5f71f35278 100644 --- a/dash/_validate.py +++ b/dash/_validate.py @@ -2,6 +2,9 @@ import re from textwrap import dedent from keyword import iskeyword +from typing import Any +from typing_extensions import Final + import flask from ._grouping import grouping_len, map_grouping @@ -15,6 +18,8 @@ clean_property_name, ) +TRUNCATE_AT: Final[int] = 100 + def validate_callback(outputs, inputs, state, extra_args, types): Input, Output, State = types @@ -209,11 +214,23 @@ def validate_multi_return(output_lists, output_values, callback_id): ) +def truncate_object(obj: Any, at: int) -> str: + """Return a truncated string representation of an object.""" + obj_stringified = str(obj) + size = len(obj_stringified) + + if size <= at: + return obj_stringified + + return obj_stringified[:at] + f"... (truncated - {size} characters total)" + + def fail_callback_output(output_value, output): valid_children = (str, int, float, type(None), Component) valid_props = (str, int, float, type(None), tuple, MutableSequence) def _raise_invalid(bad_val, outer_val, path, index=None, toplevel=False): + bad_val_stringified = truncate_object(bad_val, TRUNCATE_AT) bad_type = type(bad_val).__name__ outer_id = f"(id={outer_val.id:s})" if getattr(outer_val, "id", False) else "" outer_type = type(outer_val).__name__ @@ -244,7 +261,7 @@ def _raise_invalid(bad_val, outer_val, path, index=None, toplevel=False): {location} and has string representation - `{bad_val}` + `{bad_val_stringified}` In general, Dash properties can only be dash components, strings, dictionaries, numbers, None, diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index fa51cda9d3..d5f51fc78b 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -1,5 +1,5 @@ # -*- coding: UTF-8 -*- -from dash import Dash, Input, Output, html, dcc +from dash import _validate, Dash, Input, Output, html, dcc from dash.exceptions import PreventUpdate @@ -260,3 +260,86 @@ def update_outputs(n_clicks): dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1") dash_duo.find_element(".test-devtools-error-toggle").click() dash_duo.percy_snapshot("devtools - multi output Python exception - open") + + +def test_dveh006_truncate_callback(dash_duo): + app = Dash(__name__) + + from dataclasses import dataclass + from typing import List, Dict, Any + + @dataclass + class GenericItemA: + attribute_1: int + attribute_2: str + + @dataclass + class GenericItemB: + key: str + value: List[str] + + @dataclass + class GenericItemC: + identifier: int + description: str + + @dataclass + class GenericOptions: + option_key: str + option_value: Any + + @dataclass + class GenericDataModel: + ItemListA: List[GenericItemA] + SingleItemB: GenericItemB + NestedItemListC: List[List[GenericItemC]] + Options: GenericOptions + Metadata: Dict + AdditionalInfo: Any + + app.layout = html.P(id="output") + + @app.callback(Output("output", "children"), Input("url", "href")) + def get_width(_): + item_list_a = [ + GenericItemA(attribute_1=123, attribute_2="Alpha"), + GenericItemA(attribute_1=456, attribute_2="Beta") + ] + + single_item_b = GenericItemB(key="Key1", value=["Item1", "Item2"]) + + nested_item_list_c = [ + [GenericItemC(identifier=101, description="Description1")], + [GenericItemC(identifier=102, description="Description2")] + ] + + generic_options = GenericOptions(option_key="Option1", option_value="Value1") + + generic_metadata = {"meta_key": "meta_value"} + + additional_info = "Generic information" + + # Creating an instance of GenericDataModel + generic_data_model = GenericDataModel( + ItemListA=item_list_a, + SingleItemB=single_item_b, + NestedItemListC=nested_item_list_c, + Options=generic_options, + Metadata=generic_metadata, + AdditionalInfo=additional_info + ) + + return generic_data_model + + dash_duo.start_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + ) + + dash_duo.find_element(".dash-debug-menu").click() + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1") + + assert len(get_error_html(dash_duo, 0)) == _validate.TRUNCATE_AT