diff --git a/demo/demo/plotly_apps.py b/demo/demo/plotly_apps.py index 917cf6e9..12735ab7 100644 --- a/demo/demo/plotly_apps.py +++ b/demo/demo/plotly_apps.py @@ -32,6 +32,7 @@ import pandas as pd from django.core.cache import cache +from django.utils.translation import gettext, gettext_lazy import dash from dash import dcc, html @@ -46,6 +47,7 @@ from django_plotly_dash import DjangoDash from django_plotly_dash.consumers import send_to_pipe_channel + #pylint: disable=too-many-arguments, unused-argument, unused-variable app = DjangoDash('SimpleExample') @@ -59,8 +61,8 @@ html.Div(id='output-color'), dcc.RadioItems( id='dropdown-size', - options=[{'label': i, 'value': j} for i, j in [('L', 'large'), - ('M', 'medium'), + options=[{'label': i, 'value': j} for i, j in [('L', gettext('large')), + ('M', gettext_lazy('medium')), ('S', 'small')]], value='medium' ), diff --git a/django_plotly_dash/__init__.py b/django_plotly_dash/__init__.py index 145da77d..ba1e83e4 100644 --- a/django_plotly_dash/__init__.py +++ b/django_plotly_dash/__init__.py @@ -33,3 +33,4 @@ # Monkeypatching import django_plotly_dash._callback +import django_plotly_dash._patches diff --git a/django_plotly_dash/_patches.py b/django_plotly_dash/_patches.py new file mode 100644 index 00000000..43ad594a --- /dev/null +++ b/django_plotly_dash/_patches.py @@ -0,0 +1,139 @@ +''' +Collation of patches made to other python libraries, such as Dash itself. + +If/when the patches are not needed they will be removed from this file. + + +Copyright (c) 2022 Gibbs Consulting and others - see CONTRIBUTIONS.md + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +''' + +import json + + +from plotly.io._json import config +from plotly.utils import PlotlyJSONEncoder + +from _plotly_utils.optional_imports import get_module +from django.utils.encoding import force_text +from django.utils.functional import Promise + + +class DjangoPlotlyJSONEncoder(PlotlyJSONEncoder): + """Augment the PlotlyJSONEncoder class with Django delayed processing""" + def default(self, obj): + if isinstance(obj, Promise): + return force_text(obj) + return super().default(obj) + + +def to_json_django_plotly(plotly_object, pretty=False, engine=None): + """ + Convert a plotly/Dash object to a JSON string representation + + Parameters + ---------- + plotly_object: + A plotly/Dash object represented as a dict, graph_object, or Dash component + + pretty: bool (default False) + True if JSON representation should be pretty-printed, False if + representation should be as compact as possible. + + engine: str (default None) + The JSON encoding engine to use. One of: + - "json" for an engine based on the built-in Python json module + - "orjson" for a faster engine that requires the orjson package + - "auto" for the "orjson" engine if available, otherwise "json" + If not specified, the default engine is set to the current value of + plotly.io.json.config.default_engine. + + Returns + ------- + str + Representation of input object as a JSON string + + See Also + -------- + to_json : Convert a plotly Figure to JSON with validation + """ + orjson = get_module("orjson", should_load=True) + + # Determine json engine + if engine is None: + engine = config.default_engine + + if engine == "auto": + if orjson is not None: + engine = "orjson" + else: + engine = "json" + elif engine not in ["orjson", "json"]: + raise ValueError("Invalid json engine: %s" % engine) + + modules = { + "sage_all": get_module("sage.all", should_load=False), + "np": get_module("numpy", should_load=False), + "pd": get_module("pandas", should_load=False), + "image": get_module("PIL.Image", should_load=False), + } + + # Dump to a JSON string and return + # -------------------------------- + if engine == "json": + opts = {} + if pretty: + opts["indent"] = 2 + else: + # Remove all whitespace + opts["separators"] = (",", ":") + + return json.dumps(plotly_object, cls=DjangoPlotlyJSONEncoder, **opts) + elif engine == "orjson": + JsonConfig.validate_orjson() + opts = orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY + + if pretty: + opts |= orjson.OPT_INDENT_2 + + # Plotly + try: + plotly_object = plotly_object.to_plotly_json() + except AttributeError: + pass + + # Try without cleaning + try: + return orjson.dumps(plotly_object, option=opts).decode("utf8") + except TypeError: + pass + + cleaned = clean_to_json_compatible( + plotly_object, + numpy_allowed=True, + datetime_allowed=True, + modules=modules, + ) + return orjson.dumps(cleaned, option=opts).decode("utf8") + + +import plotly.io.json +plotly.io.json.to_json_plotly = to_json_django_plotly diff --git a/django_plotly_dash/dash_wrapper.py b/django_plotly_dash/dash_wrapper.py index 6cb2cb3e..b408be9d 100644 --- a/django_plotly_dash/dash_wrapper.py +++ b/django_plotly_dash/dash_wrapper.py @@ -36,13 +36,12 @@ from django.urls import reverse from django.utils.text import slugify from flask import Flask -from plotly.utils import PlotlyJSONEncoder from .app_name import app_name, main_view_label from .middleware import EmbeddedHolder from .util import serve_locally as serve_locally_setting from .util import stateless_app_lookup_hook -from .util import static_asset_path +from .util import static_asset_path, DjangoPlotlyJSONEncoder try: from dataclasses import dataclass @@ -480,7 +479,7 @@ def augment_initial_layout(self, base_response, initial_arguments=None): reworked_data = self.walk_tree_and_replace(baseData, overrides) response_data = json.dumps(reworked_data, - cls=PlotlyJSONEncoder) + cls=DjangoPlotlyJSONEncoder) return response_data, base_response.mimetype diff --git a/django_plotly_dash/util.py b/django_plotly_dash/util.py index 03fc89d0..4f8281c1 100644 --- a/django_plotly_dash/util.py +++ b/django_plotly_dash/util.py @@ -25,10 +25,17 @@ import json import uuid + +from _plotly_utils.optional_imports import get_module + + from django.conf import settings from django.core.cache import cache from django.utils.module_loading import import_string +from django_plotly_dash._patches import DjangoPlotlyJSONEncoder + + def _get_settings(): try: the_settings = settings.PLOTLY_DASH @@ -133,3 +140,5 @@ def stateless_app_lookup_hook(): # Default is no additional lookup return lambda _: None + +