diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64fa06ac..06042054 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,21 +1,21 @@ # Contributing to Webviz configuration utility -- [Creating a new container](#creating-a-new-container) - - [Minimal container](#minimal-container) - - [Override container toolbar](#override-container-toolbar) +- [Creating a new plugin](#creating-a-new-plugin) + - [Minimal plugin](#minimal-plugin) + - [Override plugin toolbar](#override-plugin-toolbar) - [Callbacks](#callbacks) - [Data download callback](#data-download-callback) - [User provided arguments](#user-provided-arguments) - [Data input](#data-input) - [Deattaching data from its original source](#deattaching-data-from-its-original-source) - - [Custom ad-hoc containers](#custom-ad-hoc-containers) + - [Custom ad-hoc plugins](#custom-ad-hoc-plugins) - [Run tests](#run-tests) - [Build documentation](#build-documentation) -## Creating a new container +## Creating a new plugin -Most of the development work is towards creating standard containers. -A container usually does three things: +Most of the development work is towards creating standard plugins. +A plugin usually does three things: * It has a `layout` property, consisting of multiple [Dash components](https://dash.plot.ly/getting-started). @@ -26,18 +26,18 @@ A container usually does three things: * It sets some [callbacks](https://dash.plot.ly/getting-started-part-2) to add user interactivity triggering actions in the Python backend. -### Minimal container +### Minimal plugin Of the three things mentioned above, it is only the `layout` proprety that is -mandatory to provide. A minimal container could look like: +mandatory to provide. A minimal plugin could look like: ```python import dash_html_components as html -from webviz_config import WebvizContainerABC +from webviz_config import WebvizPluginABC -class ExampleContainer(WebvizContainerABC): +class ExamplePlugin(WebvizPluginABC): @property def layout(self): @@ -47,16 +47,16 @@ class ExampleContainer(WebvizContainerABC): ]) ``` -If the file containing `ExampleContainer` is saved to [./webviz_config/containers](./webviz_config/containers), -and then added to the corresponding [\_\_init\_\_ file](./webviz_config/containers/__init__.py) -you are done. Alternatively you can create your containers in a separate Python project and `setup.py`. +If the file containing `ExamplePlugin` is saved to [./webviz_config/plugins](./webviz_config/plugins), +and then added to the corresponding [\_\_init\_\_ file](./webviz_config/plugins/__init__.py) +you are done. Alternatively you can create your plugins in a separate Python project and `setup.py`. You can then configure the installation by using something like: ```python setup( ... entry_points={ - 'webviz_config_containers': [ - 'ExampleContainer = my_package.my_module:ExampleContainer' + "webviz_config_plugins": [ + "ExamplePlugin = my_package.my_module:ExamplePlugin" ] }, ... @@ -64,23 +64,23 @@ setup( ``` See [webviz-subsurface](https://github.com/equinor/webviz-subsurface) for example of this usage. -After installation, the user can then include the container through a configuration file, e.g. +After installation, the user can then include the plugin through a configuration file, e.g. ```yaml title: Simple Webviz example pages: - - title: Front page - content: - - container: ExampleContainer + - title: Front page + content: + - ExamplePlugin: ``` -### Override container toolbar +### Override plugin toolbar -In the generated webviz application, your container will as default be given +In the generated webviz application, your plugin will as default be given a button toolbar. The default buttons to appear is stored in the class constant -`WebvizContainerABC.TOOLBAR_BUTTONS`. If you want to override which buttons should +`WebvizPluginABC.TOOLBAR_BUTTONS`. If you want to override which buttons should appear, redefine this class constant in your subclass. To remove all buttons, simply define it as an empty list. See [this section](#data-download-callback) for more information regarding the `data_download` button. @@ -95,12 +95,15 @@ from uuid import uuid4 import dash_html_components as html from dash.dependencies import Input, Output -from webviz_config import WebvizContainerABC +from webviz_config import WebvizPluginABC -class ExampleContainer(WebvizContainerABC): +class ExamplePlugin(WebvizPluginABC): def __init__(self, app): + + super().__init__() + self.button_id = f'submit-button-{uuid4()}' self.div_id = f'output-state-{uuid4()}' @@ -129,15 +132,15 @@ There are three fundamental additions to the minimal example without callbacks: * You add the argument `app` to your `__init__` function. This is a special argument name which will *not* be originating from the user configuration - file, but rather automatically given to the container by the core + file, but rather automatically given to the plugin by the core functionality of `webviz-config`. * You add a class function `set_callbacks` which contains the different callbacks to add. This function is called from the `__init__` function, - such that the callbacks are set when the container instance is created. + such that the callbacks are set when the plugin instance is created. -* Since the components are reusable (i.e. a user can use the container - multiple times within the same application), the container IDs mentioned in +* Since the components are reusable (i.e. a user can use the plugin + multiple times within the same application), the plugin IDs mentioned in the `@app.callback(...)` decorator needs to be unique. One simple way of ensuring this is to create unique IDs in the `__init__` function using [uuid.uuid4()](https://docs.python.org/3/library/uuid.html#uuid.uuid4), @@ -145,45 +148,48 @@ There are three fundamental additions to the minimal example without callbacks: #### Data download callback -There is a [data download button](#override-container-toolbar) provided by -the `WebvizContainerABC` class. However, it will only appear if the corresponding +There is a [data download button](#override-plugin-toolbar) provided by +the `WebvizPluginABC` class. However, it will only appear if the corresponding callback is set. A typical data download callback will look like ```python -@app.callback(self.container_data_output, - [self.container_data_requested]) +@app.callback(self.plugin_data_output, + [self.plugin_data_requested]) def _user_download_data(data_requested): - return WebvizContainerABC.container_data_compress( + return WebvizPluginABC.plugin_data_compress( [{'filename': 'some_file.txt', 'content': 'Some download data'}] ) if data_requested else '' ``` -By letting the container define the callback, the container author is able +By letting the plugin define the callback, the plugin author is able to utilize the whole callback machinery, including e.g. state of the individual -components in the container. This way the data downloaded can e.g. depend on +components in the plugin. This way the data downloaded can e.g. depend on the visual state or user selection. -The attributes `self.container_data_output` and `self.container_data_requested` +The attributes `self.plugin_data_output` and `self.plugin_data_requested` are Dash `Output` and `Input` instances respectively, and are provided by -the base class `WebvizContainer` (i.e. include them as shown here). +the base class `WebvizPluginABC` (i.e. include them as shown here). -The function `WebvizContainerABC.container_data_compress` is a utility function +The function `WebvizPluginABC.plugin_data_compress` is a utility function which takes a list of dictionaries, giving filenames and corresponding data, and compresses them to a zip archive which is then downloaded by the user. ### User provided arguments -Since the containers are reusable and generic, they usually take in some +Since the plugins are reusable and generic, they usually take in some user provided arguments. A minimal example could look like: ```python import dash_html_components as html -from webviz_config import WebvizContainerABC +from webviz_config import WebvizPluginABC -class ExampleContainer(WebvizContainerABC): +class ExamplePlugin(WebvizPluginABC): def __init__(self, title: str, number: int=42): + + super().__init__() + self.title = title self.number = number @@ -202,10 +208,10 @@ title: Simple Webviz example pages: - - title: Front page - content: - - container: ExampleContainer - title: My special title + - title: Front page + content: + - ExamplePlugin: + title: My special title ``` The core functionality of `webviz-config` will provide user friendly @@ -235,29 +241,28 @@ provided the configuration file title: Simple Webviz example pages: - - - title: Front page - content: - - container: ExampleContainer - title: My special title - number: Some text instead of number + - title: Front page + content: + - ExamplePlugin: + title: My special title + number: Some text instead of number ``` this error message will be given: ``` -The value provided for argument `number` given to container `ExampleContainer` is of type `str`. Expected type `int` +The value provided for argument `number` given to plugin `ExamplePlugin` is of type `str`. Expected type `int` ``` -An additional benefit is that if the container author says an argument should +An additional benefit is that if the plugin author says an argument should be of type `pathlib.Path`, the configuration parser will make sure that the user provided path (which is a string to begin with in configuration file, potentially non-absolute and relative to the configuration file itself) is -given to the container as an absolute path, and of type `pathlib.Path`. +given to the plugin as an absolute path, and of type `pathlib.Path`. ### Data input -The containers get data input through ordinary Python functions. +The plugins get data input through ordinary Python functions. Since these functions can be costly to call, we utilize the [flask-caching](https://pythonhosted.org/Flask-Caching/) package. By decorating the costly functions with `@CACHE.memoize(timeout=CACHE.TIMEOUT)` @@ -265,7 +270,7 @@ the result is cached, such that if the same function is called more than once, within the timeout, the cached result will be used instead of starting a new calculation. -Functionality used by multiple containers should be put in a common module. The +Functionality used by multiple plugins should be put in a common module. The applications common cache instance can be imported using ```python from webviz_config.common_cache import CACHE @@ -277,7 +282,7 @@ There are use cases where the generated webviz instance ideally is portable and self-contained. At the same time, there are use cases where the data input ideally comes from "live sources" (simulations still running, production database...). -Asking each container to take care of both these scenarios to the full extent +Asking each plugin to take care of both these scenarios to the full extent involves a lot of duplicate work. The core of `webviz-config` therefore facilitates this transition in the following way. @@ -291,7 +296,7 @@ def get_some_data(some, arguments) -> pd.DataFrame: It takes in some possibly user given arguments, reads e.g. data somewhere from the file system, and then returns a `pd.DataFrame`. If we want `webviz-config` to facilitate the transition to a portable webviz instance, -the container author needs only do three things: +the plugin author needs only do three things: 1) Import the decorator: `from webviz_config.webviz_store import webvizstore` 2) Decorate the function as this: @@ -299,11 +304,11 @@ the container author needs only do three things: @webvizstore def get_some_data(some, arguments) -> pd.DataFrame: ``` -3) In the container class, define a class method `add_webvizstore` which - returns a list of tuples. The first container in each tuple is a reference - to a function, the second container is itself a list of argument combinations. +3) In the plugin class, define a class method `add_webvizstore` which + returns a list of tuples. The first plugin in each tuple is a reference + to a function, the second plugin is itself a list of argument combinations. - The author of the container should provide a list of all the argument + The author of the plugin should provide a list of all the argument combinations that are imaginable during runtime of the application. Since it is a class method, the author has access to all the user provided arguments. @@ -317,12 +322,15 @@ A full example could look like e.g.: import pandas as pd from webviz_config.webviz_store import webvizstore from webviz_config.common_cache import CACHE -from webviz_config import WebvizContainerABC +from webviz_config import WebvizPluginABC -class ExamplePortable(WebvizContainerABC): +class ExamplePortable(WebvizPluginABC): def __init__(self, some_number: int): + + super().__init__() + self.some_number = some_number def add_webvizstore(self): @@ -383,7 +391,7 @@ best performance. ### Common settings -If you create multiple containers that have some settings in common, you can +If you create multiple plugins that have some settings in common, you can _"subscribe"_ to keys in the dictionary `shared_settings`, defined by the user in the configuration file. E.g. assume that the user enters something like this at top level in the configuration file: @@ -416,7 +424,7 @@ 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 containers through +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"]`. @@ -426,21 +434,24 @@ signature is not necessary, however if you do you will get a 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. -### Custom ad-hoc containers +### Custom ad-hoc plugins -It is possible to create custom containers which still can be included through +It is possible to create custom plugins which still can be included through the configuration file, which could be useful for quick prototyping. As an example, assume someone on your project has made the Python file ```python import dash_html_components as html -from webviz_config import WebvizContainerABC +from webviz_config import WebvizPluginABC -class OurCustomContainer(WebvizContainerABC): +class OurCustomPlugin(WebvizPluginABC): def __init__(self, title: str): + + super().__init__() + self.title = title @property @@ -453,24 +464,26 @@ class OurCustomContainer(WebvizContainerABC): If this is saved such that it is available through e.g. a [module](https://docs.python.org/3/tutorial/modules.html) -`ourmodule`, the user can include the custom container the same way as a standard -container, with the only change of also naming the module: +`ourmodule`, the user can include the custom plugin the same way as a standard +plugin, with the only change of also naming the module: ```yaml title: Simple Webviz example pages: - - title: Front page - content: - - container: ourmodule.OurCustomContainer - title: Title of my custom container + - title: Front page + content: + - ourmodule.OurCustomPlugin: + title: Title of my custom plugin ``` Note that this might involve appending your `$PYTHONPATH` environment variable with the path where your custom module is located. The same principle -applies if the custom container is saved in a package with submodule(s), +applies if the custom plugin is saved in a package with submodule(s), ```yaml - - container: ourpackage.ourmodule.OurCustomContainer + ... + - ourpackage.ourmodule.OurCustomPlugin: + ... ``` ## Run tests diff --git a/README.md b/README.md index 9451a0bf..a29947c3 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ This Python package, `webviz-config`, is the core plugin framework. For a real e *Webviz* will create web applications with very :lock: strict security headers and CSP settings, giving an rating of **A+** on e.g. [Mozilla observatory](https://observatory.mozilla.org/). It also facilitates a :whale: Docker setup, where the Python code can be ran completely unpriviliged in a sandbox (both with respect to file system access and network communication). -Example configuration file and information about the standard containers can be seen in [the documentation](https://equinor.github.io/webviz-config/). +Example configuration file and information about the standard plugins can be seen in [the documentation](https://equinor.github.io/webviz-config/). **The workflow can be summarized as this:** 1) The user provides a :book: configuration file following the [yaml](https://en.wikipedia.org/wiki/YAML) standard. @@ -96,13 +96,13 @@ webviz certificate --auto-install ``` Certificate installation guidelines will be given when running the command. -### Creating new containers +### Creating new plugins -If you are interested in creating new containers which can be configured through +If you are interested in creating new plugins which can be configured through the configuration file, take a look at the [contribution guide](./CONTRIBUTING.md). To quickly get started, we recommend using the corresponding -[cookiecutter template](https://github.com/equinor/webviz-container-boilerplate). +[cookiecutter template](https://github.com/equinor/webviz-plugin-boilerplate). ### License diff --git a/docs/build_docs.py b/docs/build_docs.py index 2fb4363e..f7e50351 100644 --- a/docs/build_docs.py +++ b/docs/build_docs.py @@ -1,4 +1,4 @@ -"""Builds automatic documentation of the installed webviz config containers. +"""Builds automatic documentation of the installed webviz config plugins. The documentation is designed to be used by the YAML configuration file end user. Sphinx has not been used due to @@ -9,10 +9,10 @@ is not needed. Overall workflow is: - * Gets all installed containers. + * Finds all installed plugins. * Automatically reads docstring and __init__ function signature (both argument names and which arguments have default values). - * Output the extracted container information in html using jinja2. + * Output the extracted plugin information in html using jinja2. """ import shutil @@ -22,7 +22,7 @@ from collections import defaultdict import jinja2 from markdown import markdown -import webviz_config.containers +import webviz_config.plugins from webviz_config._config_parser import SPECIAL_ARGS SCRIPT_DIR = pathlib.Path(__file__).resolve().parent @@ -48,58 +48,58 @@ def convert_docstring(doc): return "" if doc is None else markdown(doc, extensions=["fenced_code"]) -def get_container_documentation(): - """Get all installed containers, and document them by grabbing docstring +def get_plugin_documentation(): + """Get all installed plugins, and document them by grabbing docstring and input arguments / function signature. """ - containers = inspect.getmembers(webviz_config.containers, inspect.isclass) + plugins = inspect.getmembers(webviz_config.plugins, inspect.isclass) - container_doc = [] + plugin_doc = [] - for container in containers: - reference = container[1] + for plugin in plugins: + reference = plugin[1] - container_info = {} + plugin_info = {} - container_info["name"] = container[0] - container_info["doc"] = convert_docstring(reference.__doc__) + plugin_info["name"] = plugin[0] + plugin_info["doc"] = convert_docstring(reference.__doc__) argspec = inspect.getfullargspec(reference.__init__) - container_info["args"] = [ + plugin_info["args"] = [ arg for arg in argspec.args if arg not in SPECIAL_ARGS ] - container_info["values"] = defaultdict(lambda: "some value") + plugin_info["values"] = defaultdict(lambda: "some value") if argspec.defaults is not None: for arg, default in dict( zip(reversed(argspec.args), reversed(argspec.defaults)) ).items(): - container_info["values"][ + plugin_info["values"][ arg ] = f"{default} # Optional (default value shown here)." module = inspect.getmodule(reference) - container_info["module"] = module.__name__ + plugin_info["module"] = module.__name__ package = inspect.getmodule(module).__package__ - container_info["package"] = package - container_info["package_doc"] = convert_docstring( + plugin_info["package"] = package + plugin_info["package_doc"] = convert_docstring( import_module(package).__doc__ ) - if not container_info["name"].startswith("Example"): - container_doc.append(container_info) + if not plugin_info["name"].startswith("Example"): + plugin_doc.append(plugin_info) - # Sort the containers by package: + # Sort the plugins by package: - package_ordered = defaultdict(lambda: {"containers": []}) + package_ordered = defaultdict(lambda: {"plugins": []}) - for container in sorted(container_doc, key=lambda x: (x["module"], x["name"])): - package = container["package"] - package_ordered[package]["containers"].append(container) - package_ordered[package]["doc"] = container["package_doc"] + for plugin in sorted(plugin_doc, key=lambda x: (x["module"], x["name"])): + package = plugin["package"] + package_ordered[package]["plugins"].append(plugin) + package_ordered[package]["doc"] = plugin["package_doc"] return package_ordered @@ -112,7 +112,7 @@ def get_basic_example(): if __name__ == "__main__": template_data = { - "packages": get_container_documentation(), + "packages": get_plugin_documentation(), "basic_example": get_basic_example(), } diff --git a/docs/templates/index.html.jinja2 b/docs/templates/index.html.jinja2 index 73603f08..c7ebc85e 100644 --- a/docs/templates/index.html.jinja2 +++ b/docs/templates/index.html.jinja2 @@ -13,39 +13,39 @@ -
+

Webviz configuration guide

Fundamental configuration

-A configuration consists of some mandatory properties (title, username, -password) and one or more pages. Each page has a title, and potentially some +A configuration consists of some mandatory properties (e.g. app title) +and one or more pages. Each page has a title, and potentially some content. The content can be one or more items. -Containers represent predefined content, which takes one or more arguments. -A list and description of all available different containers is listed below. +Plugins represent predefined content, which takes one or more arguments. +A list and description of all available different plugins is listed below. -Content which is not containers are interpreted as text paragraphs. +Content which is not plugins are interpreted as text paragraphs. A basic example configuration is shown below. -
{{ basic_example }}
+
{{ basic_example }}
-

Container documentation

+

Plugin documentation

{%- for package in packages.values() %} -{{- package['doc'] -}} +{{- package["doc"] -}} -
+
-{%- for container in package['containers'] %} +{%- for plugin in package["plugins"] %} -{{ container['doc'] }} -
  - container: {{ container['name'] }}
-    {%- for arg in container['args'] %}
-    {{ arg }}: {{ container['values'][arg] }}
+{{ plugin["doc"] }}
+
    - {{ plugin["name"] }}:
+    {%- for arg in plugin["args"] %}
+        {{ arg }}: {{ plugin["values"][arg] }}
     {%- endfor %}
 

diff --git a/examples/basic_example.yaml b/examples/basic_example.yaml index 8b26cccc..73d879bb 100644 --- a/examples/basic_example.yaml +++ b/examples/basic_example.yaml @@ -7,60 +7,60 @@ pages: - title: Front page content: - - container: BannerImage - image: ./example_banner.png - title: My banner image + - BannerImage: + image: ./example_banner.png + title: My banner image - Webviz created from configuration file. - Some other text, potentially with strange letters like Åre, Smørbukk Sør. - title: Markdown example content: - - container: Markdown - markdown_file: ./example-markdown.md + - Markdown: + markdown_file: ./example-markdown.md - title: Table example content: - - container: DataTable - csv_file: ./example_data.csv + - DataTable: + csv_file: ./example_data.csv - title: PDF example content: - - container: EmbedPdf - pdf_file: ./example.pdf + - EmbedPdf: + pdf_file: ./example.pdf - title: Syntax highlighting example content: - - container: SyntaxHighlighter - filename: ./basic_example.yaml + - SyntaxHighlighter: + filename: ./basic_example.yaml - title: Tour example content: - - container: ExampleTour + - ExampleTour: - title: Plot a table content: - - container: TablePlotter - csv_file: ./example_data.csv - filter_cols: + - TablePlotter: + csv_file: ./example_data.csv + filter_cols: - Well - Segment - Average permeability (D) - contact_person: - name: Ola Nordmann - phone: +47 12345678 - email: some@email.com + contact_person: + name: Ola Nordmann + phone: +47 12345678 + email: some@email.com - title: Plot a table (locked) content: - - container: TablePlotter - csv_file: ./example_data.csv - lock: true - plot_options: - x: Well - y: Initial reservoir pressure (bar) - size: Average permeability (D) - facet_col: Segment - contact_person: - name: Kari Nordmann - phone: 12345678 - email: someother@email.com + - TablePlotter: + csv_file: ./example_data.csv + lock: true + plot_options: + x: Well + y: Initial reservoir pressure (bar) + size: Average permeability (D) + facet_col: Segment + contact_person: + name: Kari Nordmann + phone: 12345678 + email: someother@email.com diff --git a/examples/demo_portable.yaml b/examples/demo_portable.yaml index 5b1b1afa..ebe08895 100644 --- a/examples/demo_portable.yaml +++ b/examples/demo_portable.yaml @@ -7,7 +7,7 @@ pages: - title: Front page content: - - container: ExamplePortable - some_number: 42 - - container: ExampleAssets - picture_path: ./example_banner.png + - ExamplePortable: + some_number: 42 + - ExampleAssets: + picture_path: ./example_banner.png diff --git a/examples/example-markdown.md b/examples/example-markdown.md index c63082a9..655f192c 100644 --- a/examples/example-markdown.md +++ b/examples/example-markdown.md @@ -4,7 +4,7 @@ ### This is a subsubtitle -Hi from a Markdown container containing Norwegian letters (æ ø å), some +Hi from a Markdown text file containing Norwegian letters (æ ø å), some **bold** letters, _italic_ letters. _You can also **combine** them._ #### An unordered list diff --git a/setup.py b/setup.py index b29de5ef..d68dca6d 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ # webviz-core-components is part of the webviz-config project, # just located in a separate repository for convenience, # and is therefore pinned exactly here: - "webviz-core-components==0.0.14", + "webviz-core-components==0.0.15", ], tests_require=TESTS_REQUIRES, extras_require={"tests": TESTS_REQUIRES}, diff --git a/tests/data/basic_example.yaml b/tests/data/basic_example.yaml index 6c619aaf..ccfb527d 100644 --- a/tests/data/basic_example.yaml +++ b/tests/data/basic_example.yaml @@ -2,48 +2,45 @@ # The configuration files uses YAML (https://en.wikipedia.org/wiki/YAML). title: Reek Webviz Demonstration -username: some_username -password: some_password - pages: - title: Front page content: - - container: BannerImage - image: ./example_banner.png - title: My banner image + - BannerImage: + image: ./example_banner.png + title: My banner image - Webviz created from configuration file. - Some other text, potentially with strange letters like Åre, Smørbukk Sør. - title: Markdown example content: - - container: Markdown - markdown_file: ./example-markdown.md + - Markdown: + markdown_file: ./example-markdown.md - title: PDF example content: - - container: EmbedPdf - pdf_file: ./example.pdf + - EmbedPdf: + pdf_file: ./example.pdf - title: Syntax highlighting example content: - - container: SyntaxHighlighter - filename: ./basic_example.yaml - dark_theme: true + - SyntaxHighlighter: + filename: ./basic_example.yaml + dark_theme: true - title: Plot a table content: - - container: TablePlotter - csv_file: ./example_data.csv + - TablePlotter: + csv_file: ./example_data.csv - title: Plot a table (locked) content: - - container: TablePlotter - csv_file: ./example_data.csv - lock: true - plot_options: - x: Well - y: Initial reservoir pressure (bar) - size: Average permeability (D) - facet_col: Segment + - TablePlotter: + csv_file: ./example_data.csv + lock: true + plot_options: + x: Well + y: Initial reservoir pressure (bar) + size: Average permeability (D) + facet_col: Segment diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 4f8518cc..48057c3a 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -3,10 +3,10 @@ import dash from webviz_config.common_cache import CACHE -from webviz_config.containers import _data_table +from webviz_config.plugins import _data_table # mocked functions -GET_DATA = "webviz_config.containers._data_table.get_data" +GET_DATA = "webviz_config.plugins._data_table.get_data" def test_data_table(dash_duo): diff --git a/tests/test_example_container.py b/tests/test_example_container.py index b3aaecf1..a36bab10 100644 --- a/tests/test_example_container.py +++ b/tests/test_example_container.py @@ -1,16 +1,16 @@ import dash from webviz_config.common_cache import CACHE -from webviz_config.containers import _example_container +from webviz_config.plugins import _example_plugin -def test_example_container(dash_duo): +def test_example_plugin(dash_duo): app = dash.Dash(__name__) app.config.suppress_callback_exceptions = True CACHE.init_app(app.server) title = "Example" - page = _example_container.ExampleContainer(app, title) + page = _example_plugin.ExamplePlugin(app, title) app.layout = page.layout dash_duo.start_server(app) btn = dash_duo.find_element(f"#{page.button_id}") diff --git a/tests/test_syntax_highlighter.py b/tests/test_syntax_highlighter.py index 7f9da400..3e791377 100644 --- a/tests/test_syntax_highlighter.py +++ b/tests/test_syntax_highlighter.py @@ -4,7 +4,7 @@ import dash from webviz_config.common_cache import CACHE -from webviz_config.containers import _syntax_highlighter +from webviz_config.plugins import _syntax_highlighter def test_syntax_highlighter(dash_duo): @@ -13,9 +13,7 @@ def test_syntax_highlighter(dash_duo): app.config.suppress_callback_exceptions = True CACHE.init_app(app.server) code_file = Path("./tests/data/basic_example.yaml") - with mock.patch( - "webviz_config.containers._syntax_highlighter.get_path" - ) as mock_path: + with mock.patch("webviz_config.plugins._syntax_highlighter.get_path") as mock_path: mock_path.return_value = code_file page = _syntax_highlighter.SyntaxHighlighter(app, code_file) app.layout = page.layout diff --git a/tests/test_table_plotter.py b/tests/test_table_plotter.py index b2691065..6f50923b 100644 --- a/tests/test_table_plotter.py +++ b/tests/test_table_plotter.py @@ -2,7 +2,7 @@ import dash from webviz_config.common_cache import CACHE -from webviz_config.containers import _table_plotter +from webviz_config.plugins import _table_plotter def test_table_plotter(dash_duo): diff --git a/webviz_config/__init__.py b/webviz_config/__init__.py index dc8d0c2d..c6e1150e 100644 --- a/webviz_config/__init__.py +++ b/webviz_config/__init__.py @@ -1,13 +1,22 @@ +import warnings + from pkg_resources import get_distribution, DistributionNotFound from ._localhost_token import LocalhostToken from ._localhost_open_browser import LocalhostOpenBrowser from ._localhost_certificate import LocalhostCertificate from ._is_reload_process import is_reload_process -from ._container_abc import WebvizContainerABC +from ._plugin_abc import WebvizPluginABC, WebvizContainerABC from ._theme_class import WebvizConfigTheme from ._shared_settings_subscriptions import SHARED_SETTINGS_SUBSCRIPTIONS +warnings.simplefilter("ignore", DeprecationWarning) +# See https://github.com/plotly/plotly.py/issues/2045. +# We silence this DeprecationWarning in order to not confuse the end-user. +import plotly.express # pylint: disable=wrong-import-position,wrong-import-order + +warnings.simplefilter("default", DeprecationWarning) + try: __version__ = get_distribution(__name__).version except DistributionNotFound: diff --git a/webviz_config/_config_parser.py b/webviz_config/_config_parser.py index 0ea91dcd..7e57daa5 100644 --- a/webviz_config/_config_parser.py +++ b/webviz_config/_config_parser.py @@ -8,38 +8,39 @@ import yaml -from . import containers as standard_containers -from . import WebvizContainerABC +from . import plugins as standard_plugins +from . import WebvizPluginABC from .utils import terminal_colors +warnings.simplefilter("default", DeprecationWarning) SPECIAL_ARGS = ["self", "app", "container_settings", "_call_signature", "_imports"] -def _get_webviz_containers(module): - """Returns a list of all Webviz Containers +def _get_webviz_plugins(module): + """Returns a list of all Webviz plugins in the module given as input. """ - def _is_webviz_container(obj): - return inspect.isclass(obj) and issubclass(obj, WebvizContainerABC) + def _is_webviz_plugin(obj): + return inspect.isclass(obj) and issubclass(obj, WebvizPluginABC) - return [member[0] for member in inspect.getmembers(module, _is_webviz_container)] + return [member[0] for member in inspect.getmembers(module, _is_webviz_plugin)] def _call_signature( module, module_name, - container_name, + plugin_name, shared_settings, kwargs, config_folder, contact_person=None, ): - # pylint: disable=too-many-branches - """Takes as input the name of a container, the module it is located in, + # pylint: disable=too-many-branches,too-many-statements + """Takes as input the name of a plugin, the module it is located in, together with user given arguments (originating from the configuration file). Returns the equivalent Python code wrt. initiating an instance of - that container (with the given arguments). + that plugin (with the given arguments). Raises ParserError in the following scenarios: * User is missing a required (i.e. no default value) __init__ argument @@ -48,7 +49,7 @@ def _call_signature( * If there is type mismatch between user given argument value, and type hint in __init__ signature (given that type hint exist) """ - argspec = inspect.getfullargspec(getattr(module, container_name).__init__) + argspec = inspect.getfullargspec(getattr(module, plugin_name).__init__) if argspec.defaults is not None: required_args = argspec.args[: -len(argspec.defaults)] @@ -59,9 +60,8 @@ def _call_signature( if arg not in SPECIAL_ARGS and arg not in kwargs: raise ParserError( f"{terminal_colors.RED}{terminal_colors.BOLD}" - f"The container `{container_name}` requires " - f"the argument `{arg}` in your configuration " - "file." + f"`{plugin_name}` requires the argument " + f"`{arg}` in your configuration file." f"{terminal_colors.END}" ) @@ -69,7 +69,7 @@ def _call_signature( if arg in SPECIAL_ARGS: raise ParserError( f"{terminal_colors.RED}{terminal_colors.BOLD}" - f"Container argument `{arg}` not allowed." + f"Argument `{arg}` not allowed." f"{terminal_colors.END}" ) @@ -78,8 +78,7 @@ def _call_signature( raise ParserError( f"{terminal_colors.RED}{terminal_colors.BOLD}" f"The contact information provided for " - f"container `{container_name}` is " - f"not a dictionary. " + f"`{plugin_name}` is not a dictionary. " f"{terminal_colors.END}" ) @@ -89,9 +88,8 @@ def _call_signature( ): raise ParserError( f"{terminal_colors.RED}{terminal_colors.BOLD}" - f"Unrecognized contact information key " - f"given to container `{container_name}`." - f'Should be "name", "phone" and/or "email".' + f"Unrecognized contact information key given to `{plugin_name}`. " + f"Should be 'name', 'phone' and/or 'email'." f"{terminal_colors.END}" ) @@ -100,9 +98,8 @@ def _call_signature( elif arg not in argspec.args: raise ParserError( f"{terminal_colors.RED}{terminal_colors.BOLD}" - "Unrecognized argument. The container " - f"`{container_name}` does not take an " - f"argument `{arg}`." + f"Unrecognized argument. `{plugin_name}` " + f"does not take an argument `{arg}`." f"{terminal_colors.END}" ) @@ -121,7 +118,7 @@ def _call_signature( raise ParserError( f"{terminal_colors.RED}{terminal_colors.BOLD}" f"The value provided for argument `{arg}` " - f"given to container `{container_name}` is " + f"given to `{plugin_name}` is " f"of type `{type(kwargs[arg]).__name__}`. " f"Expected type " f"`{argspec.annotations[arg].__name__}`." @@ -148,8 +145,8 @@ def _call_signature( ) return ( - f"{module_name}.{container_name}({special_args}**{kwargs})", - f"container_layout(contact_person={contact_person})", + f"{module_name}.{plugin_name}({special_args}**{kwargs})", + f"plugin_layout(contact_person={contact_person})", ) @@ -159,7 +156,7 @@ class ParserError(Exception): class ConfigParser: - STANDARD_CONTAINERS = _get_webviz_containers(standard_containers) + STANDARD_PLUGINS = _get_webviz_plugins(standard_plugins) def __init__(self, yaml_file): @@ -229,7 +226,7 @@ def _generate_page_id(self, title): return page_id def clean_configuration(self): - # pylint: disable=too-many-branches + # pylint: disable=too-many-branches,too-many-statements """Various cleaning and checks of the raw configuration read from the user provided yaml configuration file. """ @@ -301,74 +298,78 @@ def clean_configuration(self): f"{terminal_colors.END}" ) - containers = [e for e in page["content"] if isinstance(e, dict)] - - for container in containers: - if "container" not in container: - raise ParserError( - f"{terminal_colors.RED}{terminal_colors.BOLD}" - "Argument `container`, stating name of " - "the container to include, is required." - f"{terminal_colors.END}" + plugins = [e for e in page["content"] if isinstance(e, dict)] + + for plugin in plugins: + if "container" in plugin: + kwargs = {} if plugin is None else {**plugin} + plugin_name = kwargs.pop("container") + warnings.warn( + ( + "The configuration format has changed slightly, removing " + "the need of explicitly typing 'container'. See " + "https://github.com/equinor/webviz-config/pull/174 " + "for how to get rid of this deprecation warning." + ), + DeprecationWarning, ) + else: + plugin_name = next(iter(plugin)) + plugin_variables = next(iter(plugin.values())) + kwargs = {} if plugin_variables is None else {**plugin_variables} - kwargs = {**container} - container_name = kwargs.pop("container") - - if "." not in container_name: - if container_name not in ConfigParser.STANDARD_CONTAINERS: + if "." not in plugin_name: + if plugin_name not in ConfigParser.STANDARD_PLUGINS: raise ParserError( f"{terminal_colors.RED}{terminal_colors.BOLD}" - "You have included an container with " - f"name `{container_name}` in your " + "You have included a plugin with " + f"name `{plugin_name}` in your " "configuration file. This is not a " - "standard container." + "standard plugin." f"{terminal_colors.END}" ) self.configuration["_imports"].add( - ("webviz_config.containers", "standard_containers") + ("webviz_config.plugins", "standard_plugins") ) - container["_call_signature"] = _call_signature( - standard_containers, - "standard_containers", - container_name, + plugin["_call_signature"] = _call_signature( + standard_plugins, + "standard_plugins", + plugin_name, self._shared_settings, kwargs, self._config_folder, ) - self.assets.update( - getattr(standard_containers, container_name).ASSETS - ) + self.assets.update(getattr(standard_plugins, plugin_name).ASSETS) else: - parts = container_name.split(".") + parts = plugin_name.split(".") - container_name = parts[-1] + plugin_name = parts[-1] module_name = ".".join(parts[:-1]) module = importlib.import_module(module_name) - if container_name not in _get_webviz_containers(module): + if plugin_name not in _get_webviz_plugins(module): raise ParserError( f"{terminal_colors.RED}{terminal_colors.BOLD}" f"Module `{module}` does not have a " - f"container named `{container_name}`" + f"plugin named `{plugin_name}`" f"{terminal_colors.END}" ) self.configuration["_imports"].add(module_name) - container["_call_signature"] = _call_signature( + plugin["_call_signature"] = _call_signature( module, module_name, - container_name, + plugin_name, self._shared_settings, kwargs, self._config_folder, ) - self.assets.update(getattr(module, container_name).ASSETS) + self.assets.update(getattr(module, plugin_name).ASSETS) @property def configuration(self): diff --git a/webviz_config/_container_abc.py b/webviz_config/_container_abc.py deleted file mode 100644 index 6633e357..00000000 --- a/webviz_config/_container_abc.py +++ /dev/null @@ -1,134 +0,0 @@ -import io -import abc -import base64 -import zipfile -from uuid import uuid4 - -import bleach -from dash.dependencies import Input, Output -import webviz_core_components as wcc - - -class WebvizContainerABC(abc.ABC): - """All webviz containers need to subclass this abstract base class, - e.g. - - ```python - class MyContainer(WebvizContainerABC): - - def __init__(self): - ... - - def layout(self): - ... - ``` - """ - - # This is the default set of buttons to show in the rendered container - # toolbar. If the list is empty, the subclass container layout will be - # used directly, without any visual encapsulation layout from this - # abstract base class. The containers subclassing this abstract base class - # can override this variable setting by defining a class constant with - # the same name. - # - # Some buttons will only appear if in addition necessary data is available. - # E.g. download of zip archive will only appear if the container also - # has defined the corresponding callback, and contact person will only - # appear if the user configuration file has this information. - TOOLBAR_BUTTONS = [ - "screenshot", - "expand", - "download_zip", - "contact_person", - "guided_tour", - ] - - # List of container specific assets which should be copied - # over to the ./assets folder in the generated webviz app. - # This is typically custom JavaScript and/or CSS files. - # All paths in the returned ASSETS list should be absolute. - ASSETS = [] - - @property - @abc.abstractmethod - def layout(self): - """This is the only required function of a Webviz Container. - It returns a Dash layout which by webviz-config is added to - the main Webviz application. - """ - - @property - def _container_wrapper_id(self): - # pylint: disable=attribute-defined-outside-init - # We do not have a __init__ method in this abstract base class - if not hasattr(self, "_container_wrapper_uuid"): - self._container_wrapper_uuid = uuid4() - return f"container-wrapper-{self._container_wrapper_uuid}" - - @property - def container_data_output(self): - # pylint: disable=attribute-defined-outside-init - # We do not have a __init__ method in this abstract base class - self._add_download_button = True - return Output(self._container_wrapper_id, "zip_base64") - - @property - def container_data_requested(self): - return Input(self._container_wrapper_id, "data_requested") - - @staticmethod - def _reformat_tour_steps(steps): - return [ - {"selector": "#" + step["id"], "content": step["content"]} for step in steps - ] - - @staticmethod - def container_data_compress(content): - byte_io = io.BytesIO() - - with zipfile.ZipFile(byte_io, "w") as zipped_data: - for data in content: - zipped_data.writestr(data["filename"], data["content"]) - - byte_io.seek(0) - - return base64.b64encode(byte_io.read()).decode("ascii") - - def container_layout(self, contact_person=None): - """This function returns (if the class constant SHOW_TOOLBAR is True, - the container layout wrapped into a common webviz config container - component, which provides some useful buttons like download of data, - show data contact person and download container content to png. - - CSV download button will only appear if the container class a property - `csv_string` which should return the appropriate csv data as a string. - - If TOOLBAR_BUTTONS is empty, this functions returns the same - dash layout as the container class provides directly. - """ - - buttons = self.__class__.TOOLBAR_BUTTONS.copy() - - if contact_person is None: - contact_person = {} - else: - # Sanitize the configuration user input - for key in contact_person: - contact_person[key] = bleach.clean(str(contact_person[key])) - - if "download_zip" in buttons and not hasattr(self, "_add_download_button"): - buttons.remove("download_zip") - - if buttons: - return wcc.WebvizContainerPlaceholder( - id=self._container_wrapper_id, - buttons=buttons, - contact_person=contact_person, - children=[self.layout], - tour_steps=WebvizContainerABC._reformat_tour_steps( - self.tour_steps # pylint: disable=no-member - ) - if "guided_tour" in buttons and hasattr(self, "tour_steps") - else [], - ) - return self.layout diff --git a/webviz_config/_plugin_abc.py b/webviz_config/_plugin_abc.py new file mode 100644 index 00000000..a045531a --- /dev/null +++ b/webviz_config/_plugin_abc.py @@ -0,0 +1,192 @@ +import io +import abc +import base64 +import zipfile +import warnings +from uuid import uuid4 + +import bleach +from dash.dependencies import Input, Output +import webviz_core_components as wcc + +warnings.simplefilter("default", DeprecationWarning) + + +class WebvizPluginABC(abc.ABC): + """All webviz plugins need to subclass this abstract base class, + e.g. + + ```python + class MyPlugin(WebvizPluginABC): + + def __init__(self): + ... + + def layout(self): + ... + ``` + """ + + # This is the default set of buttons to show in the rendered plugin + # toolbar. If the list is empty, the subclass plugin layout will be + # used directly, without any visual encapsulation layout from this + # abstract base class. The plugins subclassing this abstract base class + # can override this variable setting by defining a class constant with + # the same name. + # + # Some buttons will only appear if in addition necessary data is available. + # E.g. download of zip archive will only appear if the plugin also + # has defined the corresponding callback, and contact person will only + # appear if the user configuration file has this information. + TOOLBAR_BUTTONS = [ + "screenshot", + "expand", + "download_zip", + "contact_person", + "guided_tour", + ] + + # List of plugin specific assets which should be copied + # over to the ./assets folder in the generated webviz app. + # This is typically custom JavaScript and/or CSS files. + # All paths in the returned ASSETS list should be absolute. + ASSETS = [] + + def __init__(self): + """This function will later be used for e.g. setting a unique ID at + initialization, which then subclasses can use further. + + If a plugin/subclass defines its own `__init__` function + (which they usually do), they should remember to call + ```python + super().__init__() + ``` + in its own `__init__` function in order to also run the parent `__init__`. + """ + + @property + @abc.abstractmethod + def layout(self): + """This is the only required function of a Webviz plugin. + It returns a Dash layout which by webviz-config is added to + the main Webviz application. + """ + + @property + def _plugin_wrapper_id(self): + # pylint: disable=attribute-defined-outside-init + # We do not have a __init__ method in this abstract base class + if not hasattr(self, "_plugin_wrapper_uuid"): + self._plugin_wrapper_uuid = uuid4() + return f"plugin-wrapper-{self._plugin_wrapper_uuid}" + + @property + def plugin_data_output(self): + # pylint: disable=attribute-defined-outside-init + # We do not have a __init__ method in this abstract base class + self._add_download_button = True + return Output(self._plugin_wrapper_id, "zip_base64") + + @property + def container_data_output(self): + warnings.warn( + ("Use 'plugin_data_output' instead of 'container_data_output'"), + DeprecationWarning, + ) + return self.plugin_data_output + + @property + def plugin_data_requested(self): + return Input(self._plugin_wrapper_id, "data_requested") + + @property + def container_data_requested(self): + warnings.warn( + ("Use 'plugin_data_requested' instead of 'container_data_requested'"), + DeprecationWarning, + ) + return self.plugin_data_requested + + @staticmethod + def _reformat_tour_steps(steps): + return [ + {"selector": "#" + step["id"], "content": step["content"]} for step in steps + ] + + @staticmethod + def plugin_data_compress(content): + byte_io = io.BytesIO() + + with zipfile.ZipFile(byte_io, "w") as zipped_data: + for data in content: + zipped_data.writestr(data["filename"], data["content"]) + + byte_io.seek(0) + + return base64.b64encode(byte_io.read()).decode("ascii") + + @staticmethod + def container_data_compress(content): + warnings.warn( + ("Use 'plugin_data_compress' instead of 'container_data_compress'"), + DeprecationWarning, + ) + return WebvizPluginABC.plugin_data_compress(content) + + def plugin_layout(self, contact_person=None): + """This function returns (if the class constant SHOW_TOOLBAR is True, + the plugin layout wrapped into a common webviz config plugin + component, which provides some useful buttons like download of data, + show data contact person and download plugin content to png. + + CSV download button will only appear if the plugin class has a property + `csv_string` which should return the appropriate csv data as a string. + + If `TOOLBAR_BUTTONS` is empty, this functions returns the same + dash layout as the plugin class provides directly. + """ + + if isinstance(self, WebvizContainerABC): + warnings.warn( + ( + "The class name 'WebvizContainerABC' is deprecated. You " + "should change to 'WebvizPluginABC'. If you have a __init__ " + "function, you should at the same time call super().__init__(). " + "See https://github.com/equinor/webviz-config/pull/174 for " + "details. This warning will eventually " + "turn into an error in a future release of webviz-config." + ), + DeprecationWarning, + ) + + buttons = self.__class__.TOOLBAR_BUTTONS.copy() + + if contact_person is None: + contact_person = {} + else: + # Sanitize the configuration user input + for key in contact_person: + contact_person[key] = bleach.clean(str(contact_person[key])) + + if "download_zip" in buttons and not hasattr(self, "_add_download_button"): + buttons.remove("download_zip") + + if buttons: + return wcc.WebvizPluginPlaceholder( + id=self._plugin_wrapper_id, + buttons=buttons, + contact_person=contact_person, + children=[self.layout], + tour_steps=WebvizPluginABC._reformat_tour_steps( + self.tour_steps # pylint: disable=no-member + ) + if "guided_tour" in buttons and hasattr(self, "tour_steps") + else [], + ) + return self.layout + + +# pylint: disable=abstract-method +class WebvizContainerABC(WebvizPluginABC): + """This class only exist during the deprecation period. + """ diff --git a/webviz_config/_shared_settings_subscriptions.py b/webviz_config/_shared_settings_subscriptions.py index 79cb13c3..351427a0 100644 --- a/webviz_config/_shared_settings_subscriptions.py +++ b/webviz_config/_shared_settings_subscriptions.py @@ -8,7 +8,7 @@ class SharedSettingsSubscriptions: under a key called `shared_settings` in the configuration file. Since it originates from a native yaml file, the content is dictionaries/strings/ints/floats/dates. - Third-party container packages might want to check early if the `shared_settings` + Third-party plugin packages might want to check early if the `shared_settings` they use are reasonable, and/or do some transformations on them. """ @@ -16,7 +16,7 @@ def __init__(self): self._subscriptions = [] def subscribe(self, key): - """This is the decorator, which third-party container packages will use. + """This is the decorator, which third-party plugin packages will use. """ def register(function): diff --git a/webviz_config/containers/__init__.py b/webviz_config/containers/__init__.py index f23a9549..828eade3 100644 --- a/webviz_config/containers/__init__.py +++ b/webviz_config/containers/__init__.py @@ -1,37 +1,14 @@ -"""### _Basic containers_ +import warnings -These are the basic Webviz configuration containers, distributed through -the utility itself. -""" +from ..plugins import * -import pkg_resources +warnings.simplefilter("default", DeprecationWarning) -from ._example_container import ExampleContainer -from ._example_data_download import ExampleDataDownload -from ._example_assets import ExampleAssets -from ._example_portable import ExamplePortable -from ._example_tour import ExampleTour -from ._banner_image import BannerImage -from ._data_table import DataTable -from ._syntax_highlighter import SyntaxHighlighter -from ._table_plotter import TablePlotter -from ._embed_pdf import EmbedPdf -from ._markdown import Markdown - -__all__ = [ - "ExampleContainer", - "ExampleAssets", - "ExamplePortable", - "ExampleTour", - "BannerImage", - "DataTable", - "SyntaxHighlighter", - "TablePlotter", - "EmbedPdf", - "Markdown", -] - -for entry_point in pkg_resources.iter_entry_points("webviz_config_containers"): - globals()[entry_point.name] = entry_point.load() - - __all__.append(entry_point.name) +warnings.warn( + ( + "The submodule 'webviz_config.containers' is deprecated. You " + "should change to 'webviz_config.plugins'. This warning will eventually " + "turn into an error in a future release of webviz-config." + ), + DeprecationWarning, +) diff --git a/webviz_config/plugins/__init__.py b/webviz_config/plugins/__init__.py new file mode 100644 index 00000000..fcc2b261 --- /dev/null +++ b/webviz_config/plugins/__init__.py @@ -0,0 +1,53 @@ +"""### _Basic plugins_ + +These are the basic Webviz configuration plugins, distributed through +the utility itself. +""" + +import warnings + +import pkg_resources + +from ._example_plugin import ExamplePlugin +from ._example_data_download import ExampleDataDownload +from ._example_assets import ExampleAssets +from ._example_portable import ExamplePortable +from ._example_tour import ExampleTour +from ._banner_image import BannerImage +from ._data_table import DataTable +from ._syntax_highlighter import SyntaxHighlighter +from ._table_plotter import TablePlotter +from ._embed_pdf import EmbedPdf +from ._markdown import Markdown + +warnings.simplefilter("default", DeprecationWarning) + +__all__ = [ + "ExamplePlugin", + "ExampleAssets", + "ExamplePortable", + "ExampleTour", + "BannerImage", + "DataTable", + "SyntaxHighlighter", + "TablePlotter", + "EmbedPdf", + "Markdown", +] + +for entry_point in pkg_resources.iter_entry_points("webviz_config_containers"): + globals()[entry_point.name] = entry_point.load() + __all__.append(entry_point.name) + + warnings.warn( + ( + "The setup.py entry point name 'webviz_config_containers' is deprecated. " + "You should change to 'webviz_config_plugins'. This warning will " + "eventually turn into an error in a future release of webviz-config." + ), + DeprecationWarning, + ) + +for entry_point in pkg_resources.iter_entry_points("webviz_config_plugins"): + globals()[entry_point.name] = entry_point.load() + __all__.append(entry_point.name) diff --git a/webviz_config/containers/_banner_image.py b/webviz_config/plugins/_banner_image.py similarity index 86% rename from webviz_config/containers/_banner_image.py rename to webviz_config/plugins/_banner_image.py index ccfe9450..5cb43557 100644 --- a/webviz_config/containers/_banner_image.py +++ b/webviz_config/plugins/_banner_image.py @@ -2,15 +2,15 @@ import dash_html_components as html -from .. import WebvizContainerABC +from .. import WebvizPluginABC from ..webviz_assets import WEBVIZ_ASSETS -class BannerImage(WebvizContainerABC): +class BannerImage(WebvizPluginABC): """### Banner image -This container adds a full width _banner image_, with an optional overlayed -title. Useful on e.g. the front page for introducing a field or project. +Adds a full width _banner image_, with an optional overlayed title. +Useful on e.g. the front page for introducing a field or project. * `image`: Path to the picture you want to add. Either absolute path or relative to the configuration file. @@ -31,6 +31,8 @@ def __init__( height: int = 300, ): + super().__init__() + self.image = image self.title = title self.color = color diff --git a/webviz_config/containers/_data_table.py b/webviz_config/plugins/_data_table.py similarity index 87% rename from webviz_config/containers/_data_table.py rename to webviz_config/plugins/_data_table.py index 04eae027..975351ad 100644 --- a/webviz_config/containers/_data_table.py +++ b/webviz_config/plugins/_data_table.py @@ -4,17 +4,16 @@ import pandas as pd import dash_table -from .. import WebvizContainerABC +from .. import WebvizPluginABC from ..webviz_store import webvizstore from ..common_cache import CACHE -class DataTable(WebvizContainerABC): +class DataTable(WebvizPluginABC): """### Data table -This container adds a table to the webviz instance, using tabular data from -a provided csv file. If feature is requested, the data could also come from -a database. +Adds a table to the webviz instance, using tabular data from a provided csv file. +If feature is requested, the data could also come from a database. * `csv_file`: Path to the csv file containing the tabular data. Either absolute path or relative to the configuration file. @@ -34,6 +33,8 @@ def __init__( pagination: bool = True, ): + super().__init__() + self.csv_file = csv_file self.df = get_data(self.csv_file) self.sorting = sorting diff --git a/webviz_config/containers/_embed_pdf.py b/webviz_config/plugins/_embed_pdf.py similarity index 86% rename from webviz_config/containers/_embed_pdf.py rename to webviz_config/plugins/_embed_pdf.py index 1ef04325..b7efc1fb 100644 --- a/webviz_config/containers/_embed_pdf.py +++ b/webviz_config/plugins/_embed_pdf.py @@ -2,14 +2,14 @@ import dash_html_components as html -from .. import WebvizContainerABC +from .. import WebvizPluginABC from ..webviz_assets import WEBVIZ_ASSETS -class EmbedPdf(WebvizContainerABC): +class EmbedPdf(WebvizPluginABC): """### Embed PDF file -This container embeds a given PDF file into the page. +Embeds a given PDF file into the page. * `pdf_file`: Path to the PDF file to include. Either absolute path or relative to the configuration file. @@ -21,6 +21,9 @@ class EmbedPdf(WebvizContainerABC): """ def __init__(self, pdf_file: Path, height: int = 80, width: int = 100): + + super().__init__() + self.pdf_url = WEBVIZ_ASSETS.add(pdf_file) self.height = height self.width = width diff --git a/webviz_config/containers/_example_assets.py b/webviz_config/plugins/_example_assets.py similarity index 74% rename from webviz_config/containers/_example_assets.py rename to webviz_config/plugins/_example_assets.py index 1bc30bb9..5deeb3e6 100644 --- a/webviz_config/containers/_example_assets.py +++ b/webviz_config/plugins/_example_assets.py @@ -2,12 +2,14 @@ import dash_html_components as html -from .. import WebvizContainerABC +from .. import WebvizPluginABC from ..webviz_assets import WEBVIZ_ASSETS -class ExampleAssets(WebvizContainerABC): +class ExampleAssets(WebvizPluginABC): def __init__(self, picture_path: Path): + super().__init__() + self.asset_url = WEBVIZ_ASSETS.add(picture_path) @property diff --git a/webviz_config/containers/_example_data_download.py b/webviz_config/plugins/_example_data_download.py similarity index 78% rename from webviz_config/containers/_example_data_download.py rename to webviz_config/plugins/_example_data_download.py index 8ddaaa50..b85bce9c 100644 --- a/webviz_config/containers/_example_data_download.py +++ b/webviz_config/plugins/_example_data_download.py @@ -1,10 +1,12 @@ import dash_html_components as html -from .. import WebvizContainerABC +from .. import WebvizPluginABC -class ExampleDataDownload(WebvizContainerABC): +class ExampleDataDownload(WebvizPluginABC): def __init__(self, app, title: str): + super().__init__() + self.title = title self.set_callbacks(app) @@ -16,7 +18,7 @@ def set_callbacks(self, app): @app.callback(self.container_data_output, [self.container_data_requested]) def _user_download_data(data_requested): return ( - WebvizContainerABC.container_data_compress( + WebvizPluginABC.container_data_compress( [{"filename": "some_file.txt", "content": "Some download data"}] ) if data_requested diff --git a/webviz_config/containers/_example_container.py b/webviz_config/plugins/_example_plugin.py similarity index 82% rename from webviz_config/containers/_example_container.py rename to webviz_config/plugins/_example_plugin.py index a8df1910..2f736571 100644 --- a/webviz_config/containers/_example_container.py +++ b/webviz_config/plugins/_example_plugin.py @@ -3,11 +3,13 @@ import dash_html_components as html from dash.dependencies import Input, Output -from .. import WebvizContainerABC +from .. import WebvizPluginABC -class ExampleContainer(WebvizContainerABC): +class ExamplePlugin(WebvizPluginABC): def __init__(self, app, title: str): + super().__init__() + self.title = title self.button_id = f"submit-button-{uuid4()}" @@ -30,4 +32,4 @@ def set_callbacks(self, app): Output(self.div_id, "children"), [Input(self.button_id, "n_clicks")] ) def _update_output(n_clicks): - return "Button has been pressed {} times.".format(n_clicks) + return f"Button has been pressed {n_clicks} times." diff --git a/webviz_config/containers/_example_portable.py b/webviz_config/plugins/_example_portable.py similarity index 87% rename from webviz_config/containers/_example_portable.py rename to webviz_config/plugins/_example_portable.py index 8a0a0963..3e70d73d 100644 --- a/webviz_config/containers/_example_portable.py +++ b/webviz_config/plugins/_example_portable.py @@ -1,12 +1,14 @@ import pandas as pd -from .. import WebvizContainerABC +from .. import WebvizPluginABC from ..webviz_store import webvizstore from ..common_cache import CACHE -class ExamplePortable(WebvizContainerABC): +class ExamplePortable(WebvizPluginABC): def __init__(self, some_number: int): + super().__init__() + self.some_number = some_number def add_webvizstore(self): diff --git a/webviz_config/containers/_example_tour.py b/webviz_config/plugins/_example_tour.py similarity index 90% rename from webviz_config/containers/_example_tour.py rename to webviz_config/plugins/_example_tour.py index f11b4958..dd55a864 100644 --- a/webviz_config/containers/_example_tour.py +++ b/webviz_config/plugins/_example_tour.py @@ -2,11 +2,13 @@ import dash_html_components as html -from .. import WebvizContainerABC +from .. import WebvizPluginABC -class ExampleTour(WebvizContainerABC): +class ExampleTour(WebvizPluginABC): def __init__(self): + super().__init__() + self.blue_text_id = f"element-{uuid4()}" self.red_text_id = f"element-{uuid4()}" diff --git a/webviz_config/containers/_markdown.py b/webviz_config/plugins/_markdown.py similarity index 94% rename from webviz_config/containers/_markdown.py rename to webviz_config/plugins/_markdown.py index 1d179cbe..ed4421f6 100644 --- a/webviz_config/containers/_markdown.py +++ b/webviz_config/plugins/_markdown.py @@ -7,7 +7,7 @@ from markdown.inlinepatterns import ImageInlineProcessor, IMAGE_LINK_RE import dash_core_components as html -from .. import WebvizContainerABC +from .. import WebvizPluginABC from ..webviz_assets import WEBVIZ_ASSETS from ..webviz_store import webvizstore @@ -77,7 +77,7 @@ def handleMatch(self, m, data): return container, start, index -class Markdown(WebvizContainerABC): +class Markdown(WebvizPluginABC): """### Include Markdown _Note:_ The markdown syntax for images has been extended to support @@ -87,9 +87,9 @@ class Markdown(WebvizContainerABC): ![width=40%,height=300px](./example_banner.png "Some caption") ``` -This container renders and includes the content from a Markdown file. Images -are supported, and should in the markdown file be given as either relative -paths to the markdown file itself, or absolute paths. +Renders and includes the content from a Markdown file. Images are supported, +and should in the markdown file be given as either relative paths to +the markdown file itself, or absolute paths. * `markdown_file`: Path to the markdown file to render and include. Either absolute path or relative to the configuration file. @@ -155,6 +155,8 @@ class Markdown(WebvizContainerABC): def __init__(self, markdown_file: Path): + super().__init__() + self.markdown_file = markdown_file self.html = bleach.clean( diff --git a/webviz_config/containers/_syntax_highlighter.py b/webviz_config/plugins/_syntax_highlighter.py similarity index 81% rename from webviz_config/containers/_syntax_highlighter.py rename to webviz_config/plugins/_syntax_highlighter.py index 3405ab9c..367d1e82 100644 --- a/webviz_config/containers/_syntax_highlighter.py +++ b/webviz_config/plugins/_syntax_highlighter.py @@ -2,15 +2,14 @@ import dash_core_components as dcc -from .. import WebvizContainerABC +from .. import WebvizPluginABC from ..webviz_store import webvizstore -class SyntaxHighlighter(WebvizContainerABC): +class SyntaxHighlighter(WebvizPluginABC): """### Syntax highlighter -This container adds support for syntax highlighting of code. Language is -automatically detected. +Adds support for syntax highlighting of code. Language is automatically detected. * `filename`: Path to a file containing the code to highlight. * `dark_theme`: If `True`, the code is shown with a dark theme. Default is @@ -19,6 +18,8 @@ class SyntaxHighlighter(WebvizContainerABC): def __init__(self, filename: Path, dark_theme: bool = False): + super().__init__() + self.filename = filename self.config = {"theme": "dark"} if dark_theme else {"theme": "light"} diff --git a/webviz_config/containers/_table_plotter.py b/webviz_config/plugins/_table_plotter.py similarity index 97% rename from webviz_config/containers/_table_plotter.py rename to webviz_config/plugins/_table_plotter.py index edea73e7..8808aa7a 100644 --- a/webviz_config/containers/_table_plotter.py +++ b/webviz_config/plugins/_table_plotter.py @@ -10,17 +10,16 @@ from dash.dependencies import Input, Output import webviz_core_components as wcc -from .. import WebvizContainerABC +from .. import WebvizPluginABC from ..webviz_store import webvizstore from ..common_cache import CACHE -class TablePlotter(WebvizContainerABC): +class TablePlotter(WebvizPluginABC): """### TablePlotter -This container adds a plotter to the webviz instance, using tabular data from -a provided csv file. If feature is requested, the data could also come from -a database. +Adds a plotter to the webviz instance, using tabular data from a provided csv file. +If feature is requested, the data could also come from a database. * `csv_file`: Path to the csv file containing the tabular data. Either absolute path or relative to the configuration file. @@ -38,6 +37,8 @@ def __init__( lock: bool = False, ): + super().__init__() + self.plot_options = plot_options if plot_options else {} self.graph_id = f"graph-id{uuid4()}" self.lock = lock @@ -341,10 +342,10 @@ def plot_input_callbacks(self): return inputs def set_callbacks(self, app): - @app.callback(self.container_data_output, [self.container_data_requested]) + @app.callback(self.plugin_data_output, [self.plugin_data_requested]) def _user_download_data(data_requested): return ( - WebvizContainerABC.container_data_compress( + WebvizPluginABC.plugin_data_compress( [ { "filename": "table_plotter.csv", diff --git a/webviz_config/templates/copy_data_template.py.jinja2 b/webviz_config/templates/copy_data_template.py.jinja2 index b5e081db..9baa57e1 100644 --- a/webviz_config/templates/copy_data_template.py.jinja2 +++ b/webviz_config/templates/copy_data_template.py.jinja2 @@ -40,19 +40,19 @@ WEBVIZ_STORAGE.storage_folder = path.join( # expressions become available in Python 3.8 # (https://www.python.org/dev/peps/pep-0572) -containers = [] +plugins = [] {% for page in pages %} {% for content in page.content %} {% if content is not string %} -containers.append({{ content._call_signature[0] }}) +plugins.append({{ content._call_signature[0] }}) {% endif %} {% endfor %} {% endfor %} -for container in containers: - if hasattr(container, "add_webvizstore"): - WEBVIZ_STORAGE.register_function_arguments(container.add_webvizstore()) +for plugin in plugins: + if hasattr(plugin, "add_webvizstore"): + WEBVIZ_STORAGE.register_function_arguments(plugin.add_webvizstore()) WEBVIZ_STORAGE.build_store() diff --git a/webviz_config/webviz_assets.py b/webviz_config/webviz_assets.py index 4bbf9050..72dd3491 100644 --- a/webviz_config/webviz_assets.py +++ b/webviz_config/webviz_assets.py @@ -16,9 +16,9 @@ class WebvizAssets: applications from the configuration file, this class facilitates handling of static assets. - Individual containers can add assets to a common instance of WebvizAssets + Individual plugins can add assets to a common instance of WebvizAssets by calling the .add(filename) function. This adds the resource, and - at the same time returns the resource URI which the container can use. + at the same time returns the resource URI which the plugin can use. If the webviz instance is in non-portable mode, the Flask/Dash application is routed to the actual location of the files, making hot reload and diff --git a/webviz_config/webviz_store.py b/webviz_config/webviz_store.py index 638ba6e3..090cc062 100644 --- a/webviz_config/webviz_store.py +++ b/webviz_config/webviz_store.py @@ -52,7 +52,7 @@ def use_storage(self, use_storage): def register_function_arguments(self, functionarguments): """The input here is from class functions `add_webvizstore(self)` - in the different containers requested from the configuration file. + in the different plugins requested from the configuration file. The input is as follows: [(func1, argumentcombinations), (func2, argumentcombinations), ...] @@ -74,7 +74,7 @@ def register_function_arguments(self, functionarguments): def _unique_path(self, func, argtuples): """Encodes the argumenttuples as bytes, and then does a sha256 on that. Mutable arguments are accepted in the argument tuples, however it is - the container author that needs to be repsonsible for making sure that + the plugin author that needs to be responsible for making sure that instances representing different input has different values for `__repr__` """