Skip to content

Commit

Permalink
Make special (global) webviz_settings argument available to plugins (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
sigurdp authored Jan 11, 2021
1 parent f6645ad commit 2538035
Show file tree
Hide file tree
Showing 13 changed files with 191 additions and 29 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED] - YYYY-MM-DD

### Changed
- [#368](https://github.com/equinor/webviz-config/pull/368) - Made Webviz global
settings available to plugin implementations through special `webviz_settings`
argument. This argument is an instance of the `WebvizSettings` class and currently
contains both the `shared_settings` and `theme` properties.

### Deprecated
- [#368](https://github.com/equinor/webviz-config/pull/368) - Access to `webviz_settings`
as an attribute on the Dash application instance object (currently being passed to the
plugins as the special `app` argument) has been deprecated.

## [0.2.6] - 2021-01-07

### Fixed
Expand Down
30 changes: 26 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,16 +447,38 @@ def subscribe(some_key, config_folder, portable):
return some_key # The returned value here is put back into shared_settings["some_key"]
```

The (optionally transformed) `shared_settings` are accessible to plugins through
the `app` instance (see [callbacks](#callbacks)). E.g., in this case the wanted settings
are found as `app.webviz_settings["shared_settings"]["some_key"]`.

Stating the input arguments named `config_folder` and/or `portable` in the function
signature is not necessary, however if you do you will get a
[`pathlib.Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
instance representing the absolute path to the configuration file that was used, and/or
a boolean value stating if the Webviz application running is a portable one.

The (optionally transformed) `shared_settings` can be retrieved in a plugin by adding
a specially named `webviz_settings` argument to the plugin's `__init__` function. The
`webviz_settings` argument works similar to the `app` argument in that it is a special
argument name that will not be originating from the configuration file, but will be
automatically given to the plugin by the core functionality of webviz-config.

Shared settings can then be accessed through `webviz_settings`. E.g., in the case
above, the wanted settings are found as `webviz_settings.shared_settings["some_key"]`
as shown in the example below:

```python
from webviz_config import WebvizPluginABC, WebvizSettings

class ExamplePlugin(WebvizPluginABC):

def __init__(self, app, webviz_settings: WebvizSettings, title: str, number: int=42):

super().__init__()

self.title = title
self.number = number
self.some_key = webviz_settings.shared_settings["some_key"]

self.set_callbacks(app)
```

### Custom ad-hoc plugins

It is possible to create custom plugins which still can be included through
Expand Down
30 changes: 18 additions & 12 deletions tests/test_table_plotter.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import time
from pathlib import Path

import dash
from dash.testing.composite import DashComposite

from webviz_config import WebvizSettings
from webviz_config.common_cache import CACHE
from webviz_config.themes import default_theme
from webviz_config.generic_plugins import _table_plotter


def test_table_plotter(dash_duo):
def test_table_plotter(dash_duo: DashComposite) -> None:

app = dash.Dash(__name__)
app.config.suppress_callback_exceptions = True
CACHE.init_app(app.server)
app.webviz_settings = {"theme": default_theme}
csv_file = "./tests/data/example_data.csv"
page = _table_plotter.TablePlotter(app, csv_file)
webviz_settings = WebvizSettings({}, default_theme)
csv_file = Path("./tests/data/example_data.csv")
page = _table_plotter.TablePlotter(app, webviz_settings, csv_file)
app.layout = page.layout
dash_duo.start_server(app)

Expand Down Expand Up @@ -41,14 +45,16 @@ def test_table_plotter(dash_duo):
assert plot_option_dd.text == "Well"


def test_table_plotter_filter(dash_duo):
def test_table_plotter_filter(dash_duo: DashComposite) -> None:

app = dash.Dash(__name__)
app.config.suppress_callback_exceptions = True
CACHE.init_app(app.server)
app.webviz_settings = {"theme": default_theme}
csv_file = "./tests/data/example_data.csv"
page = _table_plotter.TablePlotter(app, csv_file, filter_cols=["Well"])
webviz_settings = WebvizSettings({}, default_theme)
csv_file = Path("./tests/data/example_data.csv")
page = _table_plotter.TablePlotter(
app, webviz_settings, csv_file, filter_cols=["Well"]
)
app.layout = page.layout
dash_duo.start_server(app)

Expand Down Expand Up @@ -77,15 +83,15 @@ def test_table_plotter_filter(dash_duo):
assert plot_option_dd.text == "Well"


def test_initialized_table_plotter(dash_duo):
def test_initialized_table_plotter(dash_duo: DashComposite) -> None:

app = dash.Dash(__name__)
app.css.config.serve_locally = True
app.scripts.config.serve_locally = True
app.config.suppress_callback_exceptions = True
CACHE.init_app(app.server)
app.webviz_settings = {"theme": default_theme}
csv_file = "./tests/data/example_data.csv"
webviz_settings = WebvizSettings({}, default_theme)
csv_file = Path("./tests/data/example_data.csv")
plot_options = dict(
x="Well",
y="Initial reservoir pressure (bar)",
Expand All @@ -94,7 +100,7 @@ def test_initialized_table_plotter(dash_duo):
)

page = _table_plotter.TablePlotter(
app, csv_file, lock=True, plot_options=plot_options
app, webviz_settings, csv_file, lock=True, plot_options=plot_options
)
app.layout = page.layout
dash_duo.start_server(app)
Expand Down
Empty file added tests/unit_tests/__init__.py
Empty file.
36 changes: 36 additions & 0 deletions tests/unit_tests/test_webviz_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import cast

import pytest

from webviz_config import WebvizConfigTheme, WebvizSettings


def test_construction_and_basic_access() -> None:
# pylint: disable=unidiomatic-typecheck
the_shared_settings = {"somenumber": 10, "somestring": "abc"}
the_theme = WebvizConfigTheme("dummyThemeName")
settings_obj = WebvizSettings(the_shared_settings, the_theme)

copy_of_shared_settings = settings_obj.shared_settings
assert copy_of_shared_settings is not the_shared_settings
assert type(copy_of_shared_settings) == type(the_shared_settings)
assert copy_of_shared_settings == the_shared_settings
the_shared_settings["somestring"] = "MODIFIED"
assert copy_of_shared_settings != the_shared_settings

copy_of_theme = settings_obj.theme
assert copy_of_theme is not the_theme
assert type(copy_of_theme) == type(the_theme)
assert copy_of_theme.__dict__ == the_theme.__dict__
the_theme.theme_name = "MODIFIED"
assert copy_of_theme.__dict__ != the_theme.__dict__


def test_construction_with_invalid_types() -> None:
with pytest.raises(TypeError):
theme = WebvizConfigTheme("dummyThemeName")
_settings_obj = WebvizSettings(cast(dict, None), theme)

with pytest.raises(TypeError):
shared_settings = {"somenumber": 10, "somestring": "abc"}
_settings_obj = WebvizSettings(shared_settings, cast(WebvizConfigTheme, None))
1 change: 1 addition & 0 deletions webviz_config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from importlib_metadata import version, PackageNotFoundError # type: ignore

from ._theme_class import WebvizConfigTheme
from ._webviz_settings_class import WebvizSettings
from ._localhost_token import LocalhostToken
from ._is_reload_process import is_reload_process
from ._plugin_abc import WebvizPluginABC, EncodedFile, ZipFileMember
Expand Down
4 changes: 3 additions & 1 deletion webviz_config/_config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .utils import terminal_colors
from .utils._get_webviz_plugins import _get_webviz_plugins

SPECIAL_ARGS = ["self", "app", "_call_signature"]
SPECIAL_ARGS = ["self", "app", "webviz_settings", "_call_signature"]


def _call_signature(
Expand Down Expand Up @@ -113,6 +113,8 @@ def _call_signature(
special_args = ""
if "app" in argspec.args:
special_args += "app=app, "
if "webviz_settings" in argspec.args:
special_args += "webviz_settings=webviz_settings, "

return (
f"{plugin_name}({special_args}**{kwargs})",
Expand Down
28 changes: 28 additions & 0 deletions webviz_config/_webviz_settings_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import copy
from typing import Dict, Any, Mapping

from ._theme_class import WebvizConfigTheme


class WebvizSettings:
"""This class contains global Webviz settings that will be made available
to all plugins through the special argument named webviz_settings.
"""

def __init__(self, shared_settings: Dict[str, Any], theme: WebvizConfigTheme):
if not isinstance(shared_settings, dict):
raise TypeError("shared_settings must be of type dict")

if not isinstance(theme, WebvizConfigTheme):
raise TypeError("theme must be of type WebvizConfigTheme")

self._shared_settings = shared_settings
self._theme = theme

@property
def shared_settings(self) -> Mapping[str, Any]:
return copy.deepcopy(self._shared_settings)

@property
def theme(self) -> WebvizConfigTheme:
return copy.deepcopy(self._theme)
7 changes: 4 additions & 3 deletions webviz_config/generic_plugins/_table_plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from dash import Dash
import webviz_core_components as wcc

from .. import WebvizPluginABC, EncodedFile
from .. import WebvizPluginABC, WebvizSettings, EncodedFile
from ..webviz_store import webvizstore
from ..common_cache import CACHE

Expand Down Expand Up @@ -44,13 +44,14 @@ class TablePlotter(WebvizPluginABC):
def __init__(
self,
app: Dash,
webviz_settings: WebvizSettings,
csv_file: Path,
plot_options: dict = None,
filter_cols: list = None,
filter_defaults: dict = None,
column_color_discrete_maps: dict = None,
lock: bool = False,
):
) -> None:

super().__init__()

Expand All @@ -65,7 +66,7 @@ def __init__(
)
self.filter_defaults = filter_defaults
self.column_color_discrete_maps = column_color_discrete_maps
self.plotly_theme = app.webviz_settings["theme"].plotly_theme
self.plotly_theme = webviz_settings.theme.plotly_theme
self.set_callbacks(app)

def set_filters(self, filter_cols: Optional[list]) -> None:
Expand Down
20 changes: 16 additions & 4 deletions webviz_config/templates/copy_data_template.py.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ from webviz_config.themes import installed_themes
from webviz_config.webviz_store import WEBVIZ_STORAGE
from webviz_config.webviz_assets import WEBVIZ_ASSETS
from webviz_config.common_cache import CACHE
from webviz_config.utils import deprecate_webviz_settings_attribute_in_dash_app

logging.getLogger().setLevel(logging.{{ loglevel }})

Expand All @@ -22,11 +23,22 @@ theme.from_json((Path(__file__).resolve().parent / "theme_settings.json").read_t
app = dash.Dash()
app.config.suppress_callback_exceptions = True

app.webviz_settings = {
"shared_settings": webviz_config.SHARED_SETTINGS_SUBSCRIPTIONS.transformed_settings(
{{ shared_settings }}, {{ config_folder }}, False
# Create the common webviz_setting object that will get passed as an
# argument to all plugins that request it.
webviz_settings: webviz_config.WebvizSettings = webviz_config.WebvizSettings(
shared_settings=webviz_config.SHARED_SETTINGS_SUBSCRIPTIONS.transformed_settings(
{{ shared_settings }}, {{ config_folder }}, {{ False }}
),
"theme": theme,
theme=theme,
)

# Previously, webviz_settings was piggybacked onto the Dash application object.
# For a period of time, keep it but mark access to the webviz_settings attribute
# on the Dash application object as deprecated.
deprecate_webviz_settings_attribute_in_dash_app()
app._deprecated_webviz_settings = {
"shared_settings" : webviz_settings.shared_settings,
"theme" : webviz_settings.theme
}

CACHE.init_app(app.server)
Expand Down
22 changes: 17 additions & 5 deletions webviz_config/templates/webviz_template.py.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ from webviz_config.themes import installed_themes
from webviz_config.common_cache import CACHE
from webviz_config.webviz_store import WEBVIZ_STORAGE
from webviz_config.webviz_assets import WEBVIZ_ASSETS
from webviz_config.utils import deprecate_webviz_settings_attribute_in_dash_app

# We do not want to show INFO regarding werkzeug routing as that is too verbose,
# however we want other log handlers (typically coming from webviz plugin dependencies)
Expand All @@ -44,12 +45,23 @@ server = app.server
app.title = "{{ title }}"
app.config.suppress_callback_exceptions = True

app.webviz_settings = {
"shared_settings": webviz_config.SHARED_SETTINGS_SUBSCRIPTIONS.transformed_settings(
{{ shared_settings }}, {{ config_folder }}, {{ portable }}
# Create the common webviz_setting object that will get passed as an
# argument to all plugins that request it.
webviz_settings: webviz_config.WebvizSettings = webviz_config.WebvizSettings(
shared_settings=webviz_config.SHARED_SETTINGS_SUBSCRIPTIONS.transformed_settings(
{{ shared_settings }}, {{ config_folder }}, {{ portable }}
),
"portable": {{ portable }},
"theme": theme,
theme=theme,
)

# Previously, webviz_settings was piggybacked onto the Dash application object.
# For a period of time, keep it but mark access to the webviz_settings attribute
# on the Dash application object as deprecated.
deprecate_webviz_settings_attribute_in_dash_app()
app._deprecated_webviz_settings = {
"shared_settings" : webviz_settings.shared_settings,
"theme" : webviz_settings.theme,
"portable" : {{ portable }},
}

CACHE.init_app(server)
Expand Down
3 changes: 3 additions & 0 deletions webviz_config/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
from ._available_port import get_available_port
from ._silence_flask_startup import silence_flask_startup
from ._dash_component_utils import calculate_slider_step
from ._deprecate_webviz_settings_attribute_in_dash_app import (
deprecate_webviz_settings_attribute_in_dash_app,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from typing import Any
import warnings

import dash


def _get_deprecated_webviz_settings(self: Any) -> dict:
warnings.warn(
"Accessing webviz_settings through the Dash application object has been deprecated, "
"see https://github.com/equinor/webviz-config/pull/368",
DeprecationWarning,
stacklevel=2,
)
# pylint: disable=protected-access
return self._deprecated_webviz_settings


def deprecate_webviz_settings_attribute_in_dash_app() -> None:
"""Helper that monkey patches dash.Dash application class so that access to
the webviz_settings via the Dash application instance attribute is reported
as being deprecated.
"""
dash.Dash.webviz_settings = property(
_get_deprecated_webviz_settings,
None,
None,
"Property to mark webviz_settings access as deprecated",
)

0 comments on commit 2538035

Please sign in to comment.