From 348fa3dedeb61751b370263f93fc2f6bc73436c3 Mon Sep 17 00:00:00 2001 From: Asgeir Nyvoll Date: Thu, 6 Feb 2020 16:10:22 +0100 Subject: [PATCH] Added type hinting --- .gitignore | 2 + .travis.yml | 1 + setup.py | 1 + webviz_config/_build_webviz.py | 5 ++- webviz_config/_certificate.py | 17 ++++--- webviz_config/_config_parser.py | 39 ++++++++-------- webviz_config/_is_reload_process.py | 2 +- webviz_config/_localhost_certificate.py | 6 +-- webviz_config/_localhost_open_browser.py | 14 +++--- webviz_config/_localhost_token.py | 14 +++--- webviz_config/_plugin_abc.py | 33 ++++++++------ .../_shared_settings_subscriptions.py | 13 +++--- webviz_config/_theme_class.py | 30 ++++++------- webviz_config/_write_script.py | 9 +++- webviz_config/command_line.py | 2 +- webviz_config/plugins/_banner_image.py | 5 ++- webviz_config/plugins/_data_table.py | 7 +-- webviz_config/plugins/_embed_pdf.py | 2 +- webviz_config/plugins/_example_assets.py | 2 +- .../plugins/_example_data_download.py | 9 ++-- webviz_config/plugins/_example_plugin.py | 10 +++-- webviz_config/plugins/_example_portable.py | 8 ++-- webviz_config/plugins/_example_tour.py | 6 ++- webviz_config/plugins/_markdown.py | 22 +++++----- webviz_config/plugins/_syntax_highlighter.py | 7 +-- webviz_config/plugins/_table_plotter.py | 39 ++++++++-------- webviz_config/utils/_available_port.py | 4 +- webviz_config/utils/_dash_component_utils.py | 4 +- webviz_config/utils/_silence_flask_startup.py | 8 ++-- webviz_config/webviz_assets.py | 22 +++++----- webviz_config/webviz_store.py | 44 ++++++++++--------- 31 files changed, 218 insertions(+), 169 deletions(-) diff --git a/.gitignore b/.gitignore index b8f39fb0..dfd95b6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .eggs *.egg-info +*~ +.mypy_cache __pycache__ node_modules venv diff --git a/.travis.yml b/.travis.yml index 7298ef6e..579e9bbe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,7 @@ script: - black --check webviz_config tests setup.py - pylint webviz_config tests setup.py - bandit -r -c ./bandit.yml webviz_config tests setup.py + - mypy --package webviz_config --ignore-missing-imports --disallow-untyped-defs --show-error-codes - webviz certificate - pytest tests --forked diff --git a/setup.py b/setup.py index ad862e6e..29a89865 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ "pytest-xdist", "black", "bandit", + "mypy", ] setup( diff --git a/webviz_config/_build_webviz.py b/webviz_config/_build_webviz.py index f1fa96be..11ae99d7 100644 --- a/webviz_config/_build_webviz.py +++ b/webviz_config/_build_webviz.py @@ -3,6 +3,7 @@ import shutil import tempfile import subprocess # nosec +import argparse from yaml import YAMLError @@ -15,7 +16,7 @@ STATIC_FOLDER = os.path.join(os.path.dirname(__file__), "static") -def build_webviz(args): +def build_webviz(args: argparse.Namespace) -> None: if args.theme not in installed_themes: raise ValueError(f"Theme `{args.theme}` is not installed.") @@ -87,7 +88,7 @@ def build_webviz(args): shutil.rmtree(build_directory) -def run_webviz(args, build_directory): +def run_webviz(args: argparse.Namespace, build_directory: str) -> None: print( f"{terminal_colors.YELLOW}" diff --git a/webviz_config/_certificate.py b/webviz_config/_certificate.py index f58ec31c..29396c15 100644 --- a/webviz_config/_certificate.py +++ b/webviz_config/_certificate.py @@ -5,6 +5,7 @@ import getpass import datetime import subprocess # nosec +import argparse from cryptography import x509 from cryptography.x509.oid import NameOID @@ -32,7 +33,7 @@ SERVER_CRT_FILENAME = "server.crt" -def user_data_dir(): +def user_data_dir() -> str: """Returns platform specific path to store user application data """ @@ -45,7 +46,7 @@ def user_data_dir(): return os.path.expanduser("~/.local/share/webviz") -def create_key(key_path): +def create_key(key_path: str) -> rsa.RSAPrivateKey: key = rsa.generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend() @@ -63,7 +64,12 @@ def create_key(key_path): return key -def certificate_template(subject, issuer, public_key, certauthority=False): +def certificate_template( + subject: x509.name.Name, + issuer: x509.name.Name, + public_key: x509.name.Name, + certauthority: bool = False, +) -> x509.base.CertificateBuilder: if certauthority: not_valid_after = datetime.datetime.utcnow() + datetime.timedelta(days=365 * 10) @@ -88,7 +94,7 @@ def certificate_template(subject, issuer, public_key, certauthority=False): ) -def create_ca(args): +def create_ca(args: argparse.Namespace) -> None: directory = user_data_dir() @@ -180,8 +186,7 @@ def create_ca(args): ) -def create_certificate(directory): - +def create_certificate(directory: str) -> None: ca_directory = user_data_dir() ca_key_path = os.path.join(ca_directory, CA_KEY_FILENAME) ca_crt_path = os.path.join(ca_directory, CA_CRT_FILENAME) diff --git a/webviz_config/_config_parser.py b/webviz_config/_config_parser.py index 7e57daa5..832082bd 100644 --- a/webviz_config/_config_parser.py +++ b/webviz_config/_config_parser.py @@ -4,6 +4,7 @@ import inspect import importlib import typing +import types import warnings import yaml @@ -16,26 +17,26 @@ SPECIAL_ARGS = ["self", "app", "container_settings", "_call_signature", "_imports"] -def _get_webviz_plugins(module): +def _get_webviz_plugins(module: types.ModuleType) -> list: """Returns a list of all Webviz plugins in the module given as input. """ - def _is_webviz_plugin(obj): + def _is_webviz_plugin(obj: typing.Any) -> bool: return inspect.isclass(obj) and issubclass(obj, WebvizPluginABC) return [member[0] for member in inspect.getmembers(module, _is_webviz_plugin)] def _call_signature( - module, - module_name, - plugin_name, - shared_settings, - kwargs, - config_folder, - contact_person=None, -): + module: types.ModuleType, + module_name: str, + plugin_name: str, + shared_settings: dict, + kwargs: dict, + config_folder: pathlib.Path, + contact_person: typing.Optional[dict] = None, +) -> tuple: # 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 @@ -158,7 +159,7 @@ class ConfigParser: STANDARD_PLUGINS = _get_webviz_plugins(standard_plugins) - def __init__(self, yaml_file): + def __init__(self, yaml_file: str): ConfigParser.check_for_tabs_in_file(yaml_file) @@ -181,12 +182,12 @@ def __init__(self, yaml_file): ).with_traceback(sys.exc_info()[2]) self._config_folder = pathlib.Path(yaml_file).parent - self._page_ids = [] - self._assets = set() + self._page_ids: typing.List[str] = [] + self._assets: set = set() self.clean_configuration() @staticmethod - def check_for_tabs_in_file(path): + def check_for_tabs_in_file(path: str) -> None: with open(path, "r") as filehandle: # Create a list with unique entries of line numbers containing tabs @@ -209,7 +210,7 @@ def check_for_tabs_in_file(path): f"{terminal_colors.END}" ) - def _generate_page_id(self, title): + def _generate_page_id(self, title: str) -> str: """From the user given title, this function provides a unique human readable page id, not already present in self._page_ids """ @@ -225,7 +226,7 @@ def _generate_page_id(self, title): return page_id - def clean_configuration(self): + def clean_configuration(self) -> None: # pylint: disable=too-many-branches,too-many-statements """Various cleaning and checks of the raw configuration read from the user provided yaml configuration file. @@ -372,13 +373,13 @@ def clean_configuration(self): self.assets.update(getattr(module, plugin_name).ASSETS) @property - def configuration(self): + def configuration(self) -> dict: return self._configuration @property - def shared_settings(self): + def shared_settings(self) -> dict: return self._shared_settings @property - def assets(self): + def assets(self) -> set: return self._assets diff --git a/webviz_config/_is_reload_process.py b/webviz_config/_is_reload_process.py index db70fe15..a6313c9e 100644 --- a/webviz_config/_is_reload_process.py +++ b/webviz_config/_is_reload_process.py @@ -1,7 +1,7 @@ import os -def is_reload_process(): +def is_reload_process() -> bool: """Within the flask reload machinery, it is not straight forward to know if the code is run as the main process (i.e. the process the user directly started), or if the code is a "hot reload process" (see Flask diff --git a/webviz_config/_localhost_certificate.py b/webviz_config/_localhost_certificate.py index 07e4c614..34cfca06 100644 --- a/webviz_config/_localhost_certificate.py +++ b/webviz_config/_localhost_certificate.py @@ -13,7 +13,7 @@ class LocalhostCertificate: only readable by the user running the process, and are deleted on exit. """ - def __init__(self): + def __init__(self) -> None: if not is_reload_process(): self._ssl_temp_dir = os.environ["WEBVIZ_SSL_TEMP_DIR"] = tempfile.mkdtemp() create_certificate(self._ssl_temp_dir) @@ -21,13 +21,13 @@ def __init__(self): else: self._ssl_temp_dir = os.environ["WEBVIZ_SSL_TEMP_DIR"] - def _delete_temp_dir(self): + def _delete_temp_dir(self) -> None: """Delete temporary directory with on-the-fly generated localhost certificates """ shutil.rmtree(self._ssl_temp_dir) @property - def ssl_context(self): + def ssl_context(self) -> tuple: return ( os.path.join(self._ssl_temp_dir, SERVER_CRT_FILENAME), os.path.join(self._ssl_temp_dir, SERVER_KEY_FILENAME), diff --git a/webviz_config/_localhost_open_browser.py b/webviz_config/_localhost_open_browser.py index 0a2c801e..c684d36b 100644 --- a/webviz_config/_localhost_open_browser.py +++ b/webviz_config/_localhost_open_browser.py @@ -11,7 +11,7 @@ class LocalhostOpenBrowser: # pylint: disable=too-few-public-methods - def __init__(self, port, token): + def __init__(self, port: int, token: str): self._port = port self._token = token @@ -19,7 +19,7 @@ def __init__(self, port, token): # Only open new browser tab if not a reload process threading.Thread(target=self._timer).start() - def _timer(self): + def _timer(self) -> None: """Waits until the app is ready, and then opens the page in the default browser. """ @@ -39,14 +39,14 @@ def _timer(self): f"{self._url(with_token=True)}" ) - def _url(self, with_token=False, https=True): + def _url(self, with_token: bool = False, https: bool = True) -> str: return ( f"{'https' if https else 'http'}://localhost:{self._port}" + f"{'?ott=' + self._token if with_token else ''}" ) @staticmethod - def _get_browser_controller(): + def _get_browser_controller() -> webbrowser.BaseBrowser: for browser in ["chrome", "chromium-browser"]: try: return webbrowser.get(using=browser) @@ -57,7 +57,7 @@ def _get_browser_controller(): # preferred browsers are installed: return webbrowser.get() - def _app_ready(self): + def _app_ready(self) -> bool: """Check if the flask instance is ready. """ @@ -67,7 +67,7 @@ def _app_ready(self): try: urllib.request.urlopen(self._url(https=False)) # nosec app_ready = True - except urllib.error.URLError: + except urllib.error.URLError: # type: ignore[attr-defined] # The flask instance has not started app_ready = False except ConnectionResetError: @@ -79,7 +79,7 @@ def _app_ready(self): return app_ready - def _open_new_tab(self): + def _open_new_tab(self) -> None: """Open the url (with token) in the default browser. """ diff --git a/webviz_config/_localhost_token.py b/webviz_config/_localhost_token.py index 043eb926..071f01ea 100644 --- a/webviz_config/_localhost_token.py +++ b/webviz_config/_localhost_token.py @@ -30,7 +30,7 @@ class LocalhostToken: two different localhost applications running simultaneously do not interfere. """ - def __init__(self, app, port): + def __init__(self, app: flask.app.Flask, port: int): self._app = app self._port = port @@ -53,17 +53,17 @@ def __init__(self, app, port): self.set_request_decorators() @staticmethod - def generate_token(): + def generate_token() -> str: return secrets.token_urlsafe(nbytes=64) @property - def one_time_token(self): + def one_time_token(self) -> str: return self._ott - def set_request_decorators(self): + def set_request_decorators(self) -> None: # pylint: disable=inconsistent-return-statements @self._app.before_request - def _check_for_ott_or_cookie(): + def _check_for_ott_or_cookie(): # type: ignore[no-untyped-def] if not self._ott_validated and self._ott == flask.request.args.get("ott"): self._ott_validated = True flask.g.set_cookie_token = True @@ -77,7 +77,9 @@ def _check_for_ott_or_cookie(): flask.abort(401) @self._app.after_request - def _set_cookie_token_in_response(response): + def _set_cookie_token_in_response( + response: flask.wrappers.Response, + ) -> flask.wrappers.Response: if "set_cookie_token" in flask.g and flask.g.set_cookie_token: response.set_cookie( key=f"cookie_token_{self._port}", value=self._cookie_token diff --git a/webviz_config/_plugin_abc.py b/webviz_config/_plugin_abc.py index 4d74033a..91f3713f 100644 --- a/webviz_config/_plugin_abc.py +++ b/webviz_config/_plugin_abc.py @@ -4,8 +4,10 @@ import zipfile import warnings from uuid import uuid4 +from typing import List, Optional, Type, Union import bleach +from dash.development.base_component import Component from dash.dependencies import Input, Output import webviz_core_components as wcc @@ -50,9 +52,9 @@ def layout(self): # 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 = [] + ASSETS: list = [] - def __init__(self): + def __init__(self) -> None: """If a plugin/subclass defines its own `__init__` function (which they usually do), they should remember to call ```python @@ -63,7 +65,7 @@ def __init__(self): self._plugin_uuid = uuid4() - def uuid(self, element: str): + def uuid(self, element: str) -> str: """Typically used to get a unique ID for some given element/component in a plugins layout. If the element string is unique within the plugin, this function returns a string which is guaranteed to be unique also across the @@ -82,14 +84,14 @@ def uuid(self, element: str): @property @abc.abstractmethod - def layout(self): + def layout(self) -> Union[str, Type[Component]]: """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): + def _plugin_wrapper_id(self) -> str: # pylint: disable=attribute-defined-outside-init # We do not have a __init__ method in this abstract base class if not hasattr(self, "_plugin_uuid"): @@ -97,14 +99,14 @@ def _plugin_wrapper_id(self): return f"plugin-wrapper-{self._plugin_uuid}" @property - def plugin_data_output(self): + def plugin_data_output(self) -> Output: # 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): + def container_data_output(self) -> Output: warnings.warn( ("Use 'plugin_data_output' instead of 'container_data_output'"), DeprecationWarning, @@ -112,11 +114,11 @@ def container_data_output(self): return self.plugin_data_output @property - def plugin_data_requested(self): + def plugin_data_requested(self) -> Input: return Input(self._plugin_wrapper_id, "data_requested") @property - def container_data_requested(self): + def container_data_requested(self) -> Input: warnings.warn( ("Use 'plugin_data_requested' instead of 'container_data_requested'"), DeprecationWarning, @@ -124,13 +126,13 @@ def container_data_requested(self): return self.plugin_data_requested @staticmethod - def _reformat_tour_steps(steps): + def _reformat_tour_steps(steps: List[dict]) -> List[dict]: return [ {"selector": "#" + step["id"], "content": step["content"]} for step in steps ] @staticmethod - def plugin_data_compress(content): + def plugin_data_compress(content: List[dict]) -> str: byte_io = io.BytesIO() with zipfile.ZipFile(byte_io, "w") as zipped_data: @@ -142,14 +144,16 @@ def plugin_data_compress(content): return base64.b64encode(byte_io.read()).decode("ascii") @staticmethod - def container_data_compress(content): + def container_data_compress(content: List[dict]) -> str: 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): + def plugin_layout( + self, contact_person: Optional[dict] = None + ) -> Union[str, Type[Component]]: """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, @@ -188,13 +192,14 @@ def plugin_layout(self, contact_person=None): buttons.remove("download_zip") if buttons: + # pylint: disable=no-member 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 + self.tour_steps # type: ignore[attr-defined] ) if "guided_tour" in buttons and hasattr(self, "tour_steps") else [], diff --git a/webviz_config/_shared_settings_subscriptions.py b/webviz_config/_shared_settings_subscriptions.py index 351427a0..63d0ee41 100644 --- a/webviz_config/_shared_settings_subscriptions.py +++ b/webviz_config/_shared_settings_subscriptions.py @@ -1,6 +1,7 @@ import copy import inspect import pathlib +from typing import Callable, List, Dict class SharedSettingsSubscriptions: @@ -12,20 +13,22 @@ class SharedSettingsSubscriptions: they use are reasonable, and/or do some transformations on them. """ - def __init__(self): - self._subscriptions = [] + def __init__(self) -> None: + self._subscriptions: List[Dict] = [] - def subscribe(self, key): + def subscribe(self, key: str) -> Callable: """This is the decorator, which third-party plugin packages will use. """ - def register(function): + def register(function: Callable) -> Callable: self._subscriptions.append({"key": key, "function": function}) return function return register - def transformed_settings(self, shared_settings, config_folder, portable): + def transformed_settings( + self, shared_settings: Dict, config_folder: str, portable: bool + ) -> Dict: """Called from the app template, which returns the `shared_settings` after all third-party package subscriptions have done their (optional) transfomrations. diff --git a/webviz_config/_theme_class.py b/webviz_config/_theme_class.py index 2a1526f7..d3309c10 100644 --- a/webviz_config/_theme_class.py +++ b/webviz_config/_theme_class.py @@ -7,7 +7,7 @@ class WebvizConfigTheme: property is the theme name set at initialization. """ - def __init__(self, theme_name): + def __init__(self, theme_name: str): self.theme_name = theme_name self._csp = { @@ -46,18 +46,18 @@ def __init__(self, theme_name): "payment": "'none'", } - self._external_stylesheets = [] - self._assets = [] - self._plotly_theme = {} + self._external_stylesheets: list = [] + self._assets: list = [] + self._plotly_theme: dict = {} - def to_json(self): + def to_json(self) -> str: return json.dumps(vars(self), indent=4, sort_keys=True) - def from_json(self, json_string): + def from_json(self, json_string: str) -> None: for key, value in json.loads(json_string).items(): setattr(self, key, value) - def adjust_csp(self, dictionary, append=True): + def adjust_csp(self, dictionary: dict, append: bool = True) -> None: """If the default CSP settings needs to be changed, this function can be called by giving in a dictionary with key-value pairs which should be changed. If `append=True`, the CSP sources given in the dictionary @@ -73,41 +73,41 @@ def adjust_csp(self, dictionary, append=True): self._csp[key] = value @property - def csp(self): + def csp(self) -> dict: """Returns the content security policy settings for the theme.""" return self._csp @property - def feature_policy(self): + def feature_policy(self) -> dict: """Returns the feature policy settings for the theme.""" return self._feature_policy @property - def plotly_theme(self): + def plotly_theme(self) -> dict: return copy.deepcopy(self._plotly_theme) @plotly_theme.setter - def plotly_theme(self, plotly_theme): + def plotly_theme(self, plotly_theme: dict) -> None: """Layout object of Plotly graph objects.""" self._plotly_theme = plotly_theme @property - def external_stylesheets(self): + def external_stylesheets(self) -> list: return self._external_stylesheets @external_stylesheets.setter - def external_stylesheets(self, external_stylesheets): + def external_stylesheets(self, external_stylesheets: list) -> None: """Set optional external stylesheets to be used in the Dash application. The input variable `external_stylesheets` should be a list.""" self._external_stylesheets = external_stylesheets @property - def assets(self): + def assets(self) -> list: return self._assets @assets.setter - def assets(self, assets): + def assets(self, assets: list) -> None: """Set optional theme assets to be copied over to the `./assets` folder when the webviz dash application is created. The input variable `assets` should be a list of absolute file paths to the different diff --git a/webviz_config/_write_script.py b/webviz_config/_write_script.py index 65dfffa4..ab6eb97b 100644 --- a/webviz_config/_write_script.py +++ b/webviz_config/_write_script.py @@ -2,14 +2,19 @@ import getpass import datetime import pathlib +import argparse import jinja2 from ._config_parser import ConfigParser -def write_script(args, build_directory, template_filename, output_filename): - +def write_script( + args: argparse.Namespace, + build_directory: str, + template_filename: str, + output_filename: str, +) -> set: config_parser = ConfigParser(args.yaml_file) configuration = config_parser.configuration diff --git a/webviz_config/command_line.py b/webviz_config/command_line.py index c2923a5d..67fb67b0 100644 --- a/webviz_config/command_line.py +++ b/webviz_config/command_line.py @@ -4,7 +4,7 @@ from ._certificate import create_ca -def main(): +def main() -> None: parser = argparse.ArgumentParser( prog=("Creates a Webviz Dash app from a configuration setup") diff --git a/webviz_config/plugins/_banner_image.py b/webviz_config/plugins/_banner_image.py index 5cb43557..ecbd5da9 100644 --- a/webviz_config/plugins/_banner_image.py +++ b/webviz_config/plugins/_banner_image.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import List import dash_html_components as html @@ -20,7 +21,7 @@ class BannerImage(WebvizPluginABC): * `height`: Height of the banner image (in pixels). """ - TOOLBAR_BUTTONS = [] + TOOLBAR_BUTTONS: List[str] = [] def __init__( self, @@ -42,7 +43,7 @@ def __init__( self.image_url = WEBVIZ_ASSETS.add(image) @property - def layout(self): + def layout(self) -> html.Div: style = { "color": self.color, diff --git a/webviz_config/plugins/_data_table.py b/webviz_config/plugins/_data_table.py index 4722d1da..800cf3d2 100644 --- a/webviz_config/plugins/_data_table.py +++ b/webviz_config/plugins/_data_table.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import List import pandas as pd import dash_table @@ -40,11 +41,11 @@ def __init__( self.filtering = filtering self.pagination = pagination - def add_webvizstore(self): + def add_webvizstore(self) -> List[tuple]: return [(get_data, [{"csv_file": self.csv_file}])] @property - def layout(self): + def layout(self) -> dash_table.DataTable: return dash_table.DataTable( columns=[{"name": i, "id": i} for i in self.df.columns], data=self.df.to_dict("records"), @@ -56,5 +57,5 @@ def layout(self): @CACHE.memoize(timeout=CACHE.TIMEOUT) @webvizstore -def get_data(csv_file) -> pd.DataFrame: +def get_data(csv_file: Path) -> pd.DataFrame: return pd.read_csv(csv_file) diff --git a/webviz_config/plugins/_embed_pdf.py b/webviz_config/plugins/_embed_pdf.py index b7efc1fb..8d0b7259 100644 --- a/webviz_config/plugins/_embed_pdf.py +++ b/webviz_config/plugins/_embed_pdf.py @@ -29,7 +29,7 @@ def __init__(self, pdf_file: Path, height: int = 80, width: int = 100): self.width = width @property - def layout(self): + def layout(self) -> html.Embed: style = {"height": f"{self.height}vh", "width": f"{self.width}%"} diff --git a/webviz_config/plugins/_example_assets.py b/webviz_config/plugins/_example_assets.py index 5deeb3e6..e9ae1cf4 100644 --- a/webviz_config/plugins/_example_assets.py +++ b/webviz_config/plugins/_example_assets.py @@ -13,5 +13,5 @@ def __init__(self, picture_path: Path): self.asset_url = WEBVIZ_ASSETS.add(picture_path) @property - def layout(self): + def layout(self) -> html.Img: return html.Img(src=self.asset_url) diff --git a/webviz_config/plugins/_example_data_download.py b/webviz_config/plugins/_example_data_download.py index b85bce9c..6c79cbe9 100644 --- a/webviz_config/plugins/_example_data_download.py +++ b/webviz_config/plugins/_example_data_download.py @@ -1,22 +1,23 @@ import dash_html_components as html +from dash import Dash from .. import WebvizPluginABC class ExampleDataDownload(WebvizPluginABC): - def __init__(self, app, title: str): + def __init__(self, app: Dash, title: str): super().__init__() self.title = title self.set_callbacks(app) @property - def layout(self): + def layout(self) -> html.H1: return html.H1(self.title) - def set_callbacks(self, app): + def set_callbacks(self, app: Dash) -> None: @app.callback(self.container_data_output, [self.container_data_requested]) - def _user_download_data(data_requested): + def _user_download_data(data_requested: bool) -> str: return ( WebvizPluginABC.container_data_compress( [{"filename": "some_file.txt", "content": "Some download data"}] diff --git a/webviz_config/plugins/_example_plugin.py b/webviz_config/plugins/_example_plugin.py index 8255fb0c..49680bdd 100644 --- a/webviz_config/plugins/_example_plugin.py +++ b/webviz_config/plugins/_example_plugin.py @@ -1,11 +1,12 @@ import dash_html_components as html from dash.dependencies import Input, Output +from dash import Dash from .. import WebvizPluginABC class ExamplePlugin(WebvizPluginABC): - def __init__(self, app, title: str): + def __init__(self, app: Dash, title: str): super().__init__() self.title = title @@ -13,7 +14,7 @@ def __init__(self, app, title: str): self.set_callbacks(app) @property - def layout(self): + def layout(self) -> html.Div: return html.Div( [ html.H1(self.title), @@ -24,10 +25,11 @@ def layout(self): ] ) - def set_callbacks(self, app): + def set_callbacks(self, app: Dash) -> None: @app.callback( Output(self.uuid("output-state"), "children"), [Input(self.uuid("submit-button"), "n_clicks")], ) - def _update_output(n_clicks): + def _update_output(n_clicks: int) -> str: + print(type(n_clicks)) return f"Button has been pressed {n_clicks} times." diff --git a/webviz_config/plugins/_example_portable.py b/webviz_config/plugins/_example_portable.py index 3e70d73d..69cb56b3 100644 --- a/webviz_config/plugins/_example_portable.py +++ b/webviz_config/plugins/_example_portable.py @@ -1,3 +1,5 @@ +from typing import List + import pandas as pd from .. import WebvizPluginABC @@ -11,17 +13,17 @@ def __init__(self, some_number: int): self.some_number = some_number - def add_webvizstore(self): + def add_webvizstore(self) -> List[tuple]: return [(input_data_function, [{"some_number": self.some_number}])] @property - def layout(self): + def layout(self) -> str: return str(input_data_function(self.some_number)) @CACHE.memoize(timeout=CACHE.TIMEOUT) @webvizstore -def input_data_function(some_number) -> pd.DataFrame: +def input_data_function(some_number: int) -> pd.DataFrame: print("This time I'm actually doing the calculation...") return pd.DataFrame( data={ diff --git a/webviz_config/plugins/_example_tour.py b/webviz_config/plugins/_example_tour.py index 0057dde7..1a2ed530 100644 --- a/webviz_config/plugins/_example_tour.py +++ b/webviz_config/plugins/_example_tour.py @@ -1,3 +1,5 @@ +from typing import List + import dash_html_components as html from .. import WebvizPluginABC @@ -5,14 +7,14 @@ class ExampleTour(WebvizPluginABC): @property - def tour_steps(self): + def tour_steps(self) -> List[dict]: return [ {"id": self.uuid("blue_text"), "content": "This is the first step"}, {"id": self.uuid("red_text"), "content": "This is the second step"}, ] @property - def layout(self): + def layout(self) -> html.Div: return html.Div( children=[ html.Span( diff --git a/webviz_config/plugins/_markdown.py b/webviz_config/plugins/_markdown.py index 86ef78ce..953f5868 100644 --- a/webviz_config/plugins/_markdown.py +++ b/webviz_config/plugins/_markdown.py @@ -1,8 +1,9 @@ from pathlib import Path +from typing import List +from xml.etree import ElementTree # nosec import bleach import markdown -from markdown.util import etree from markdown.extensions import Extension from markdown.inlinepatterns import ImageInlineProcessor, IMAGE_LINK_RE import dash_core_components as dcc @@ -13,12 +14,12 @@ class _WebvizMarkdownExtension(Extension): - def __init__(self, base_path): + def __init__(self, base_path: Path): self.base_path = base_path super(_WebvizMarkdownExtension, self).__init__() - def extendMarkdown(self, md): + def extendMarkdown(self, md: markdown.core.Markdown) -> None: md.inlinePatterns.register( item=_MarkdownImageProcessor(IMAGE_LINK_RE, md, self.base_path), name="image_link", @@ -27,12 +28,12 @@ def extendMarkdown(self, md): class _MarkdownImageProcessor(ImageInlineProcessor): - def __init__(self, image_link_re, md, base_path): + def __init__(self, image_link_re: str, md: markdown.core.Markdown, base_path: Path): self.base_path = base_path super(_MarkdownImageProcessor, self).__init__(image_link_re, md) - def handleMatch(self, m, data): + def handleMatch(self, m, data: str) -> tuple: # type: ignore[no-untyped-def] image, start, index = super().handleMatch(m, data) if image is None or not image.get("title"): @@ -48,6 +49,7 @@ def handleMatch(self, m, data): ) image_path = Path(src) + if not image_path.is_absolute(): image_path = (self.base_path / image_path).resolve() @@ -67,10 +69,10 @@ def handleMatch(self, m, data): image.set("src", url) image.set("class", "_markdown_image") - container = etree.Element("span") + container = ElementTree.Element("span") container.append(image) - etree.SubElement( + ElementTree.SubElement( container, "span", attrib={"class": "_markdown_image_caption"} ).text = caption @@ -173,14 +175,14 @@ def __init__(self, markdown_file: Path): styles=Markdown.ALLOWED_STYLES, ) - def add_webvizstore(self): + def add_webvizstore(self) -> List[tuple]: return [(get_path, [{"path": self.markdown_file}])] @property - def layout(self): + def layout(self) -> dcc.Markdown: return dcc.Markdown(self.html, dangerously_allow_html=True) @webvizstore -def get_path(path) -> Path: +def get_path(path: Path) -> Path: return path diff --git a/webviz_config/plugins/_syntax_highlighter.py b/webviz_config/plugins/_syntax_highlighter.py index 367d1e82..b3114d1b 100644 --- a/webviz_config/plugins/_syntax_highlighter.py +++ b/webviz_config/plugins/_syntax_highlighter.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import List import dash_core_components as dcc @@ -23,16 +24,16 @@ def __init__(self, filename: Path, dark_theme: bool = False): self.filename = filename self.config = {"theme": "dark"} if dark_theme else {"theme": "light"} - def add_webvizstore(self): + def add_webvizstore(self) -> List[tuple]: return [(get_path, [{"path": self.filename}])] @property - def layout(self): + def layout(self) -> dcc.Markdown: return dcc.Markdown( f"```{get_path(self.filename).read_text()}```", highlight_config=self.config ) @webvizstore -def get_path(path) -> Path: +def get_path(path: Path) -> Path: return path diff --git a/webviz_config/plugins/_table_plotter.py b/webviz_config/plugins/_table_plotter.py index d6ceac2b..0b30229b 100644 --- a/webviz_config/plugins/_table_plotter.py +++ b/webviz_config/plugins/_table_plotter.py @@ -1,5 +1,6 @@ from pathlib import Path from collections import OrderedDict +from typing import Optional, List, Dict, Any import numpy as np import pandas as pd @@ -7,6 +8,7 @@ import dash_html_components as html import dash_core_components as dcc from dash.dependencies import Input, Output +from dash import Dash import webviz_core_components as wcc from .. import WebvizPluginABC @@ -30,7 +32,7 @@ class TablePlotter(WebvizPluginABC): def __init__( self, - app, + app: Dash, csv_file: Path, plot_options: dict = None, filter_cols: list = None, @@ -51,7 +53,7 @@ def __init__( self.plotly_theme = app.webviz_settings["theme"].plotly_theme self.set_callbacks(app) - def set_filters(self, filter_cols): + def set_filters(self, filter_cols: Optional[list]) -> None: self.filter_cols = [] self.use_filter = False if filter_cols: @@ -62,11 +64,11 @@ def set_filters(self, filter_cols): if self.filter_cols: self.use_filter = True - def add_webvizstore(self): + def add_webvizstore(self) -> List[tuple]: return [(get_data, [{"csv_file": self.csv_file}])] @property - def plots(self): + def plots(self) -> dict: """A list of available plots and their options""" return { "scatter": ["x", "y", "size", "color", "facet_col"], @@ -92,7 +94,7 @@ def plots(self): } @property - def plot_args(self): + def plot_args(self) -> dict: """A list of possible plot options and their default values""" return OrderedDict( { @@ -164,7 +166,7 @@ def plot_args(self): } ) - def filter_layout(self): + def filter_layout(self) -> Optional[list]: """Makes dropdowns for each dataframe column used for filtering.""" if not self.use_filter: return None @@ -223,7 +225,7 @@ def filter_layout(self): ) return dropdowns - def plot_option_layout(self): + def plot_option_layout(self) -> List[html.Div]: """Renders a dropdown widget for each plot option""" divs = [] # The plot type dropdown is handled separate @@ -264,17 +266,17 @@ def plot_option_layout(self): return divs @property - def style_options_div(self): + def style_options_div(self) -> Dict[str, str]: """Style for active plot options""" return {"display": "grid"} @property - def style_options_div_hidden(self): + def style_options_div_hidden(self) -> Dict[str, str]: """Style for hidden plot options""" return {"display": "none"} @property - def layout(self): + def layout(self) -> html.Div: return html.Div( children=[ wcc.FlexBox( @@ -297,7 +299,7 @@ def layout(self): ) @property - def plot_output_callbacks(self): + def plot_output_callbacks(self) -> List[Output]: """Creates list of output dependencies for callback The outputs are the graph, and the style of the plot options""" outputs = [] @@ -307,7 +309,7 @@ def plot_output_callbacks(self): return outputs @property - def plot_input_callbacks(self): + def plot_input_callbacks(self) -> List[Input]: """Creates list of input dependencies for callback The inputs are the plot type and the current value for each plot option @@ -320,9 +322,9 @@ def plot_input_callbacks(self): inputs.append(Input(self.uuid(f"filter-{filtcol}"), "value")) return inputs - def set_callbacks(self, app): + def set_callbacks(self, app: Dash) -> None: @app.callback(self.plugin_data_output, [self.plugin_data_requested]) - def _user_download_data(data_requested): + def _user_download_data(data_requested: Optional[int]) -> str: return ( WebvizPluginABC.plugin_data_compress( [ @@ -337,7 +339,7 @@ def _user_download_data(data_requested): ) @app.callback(self.plot_output_callbacks, self.plot_input_callbacks) - def _update_output(*args): + def _update_output(*args: Any) -> tuple: """Updates the graph and shows/hides plot options""" plot_type = args[0] # pylint: disable=protected-access @@ -358,18 +360,19 @@ def _update_output(*args): div_style.append(self.style_options_div) else: div_style.append(self.style_options_div_hidden) - return (plotfunc(data, template=self.plotly_theme, **plotargs), *div_style) @CACHE.memoize(timeout=CACHE.TIMEOUT) @webvizstore -def get_data(csv_file) -> pd.DataFrame: +def get_data(csv_file: Path) -> pd.DataFrame: return pd.read_csv(csv_file, index_col=None) @CACHE.memoize(timeout=CACHE.TIMEOUT) -def filter_dataframe(dframe, columns, column_values): +def filter_dataframe( + dframe: pd.DataFrame, columns: list, column_values: List[list] +) -> pd.DataFrame: df = dframe.copy() if not isinstance(columns, list): columns = [columns] diff --git a/webviz_config/utils/_available_port.py b/webviz_config/utils/_available_port.py index f99ee780..57ab3b05 100644 --- a/webviz_config/utils/_available_port.py +++ b/webviz_config/utils/_available_port.py @@ -2,7 +2,7 @@ import socket -def get_available_port(): +def get_available_port() -> int: """Finds an available port for use in webviz on localhost. If a reload process, it will reuse the same port as found in the parent process by using an inherited environment variable. @@ -17,4 +17,4 @@ def get_available_port(): os.environ["WEBVIZ_PORT"] = str(port) return port - return int(os.environ.get("WEBVIZ_PORT")) + return int(os.environ.get("WEBVIZ_PORT")) # type: ignore[arg-type] diff --git a/webviz_config/utils/_dash_component_utils.py b/webviz_config/utils/_dash_component_utils.py index 38b60a71..d1a89248 100644 --- a/webviz_config/utils/_dash_component_utils.py +++ b/webviz_config/utils/_dash_component_utils.py @@ -1,7 +1,9 @@ import math -def calculate_slider_step(min_value: float, max_value: float, steps=100): +def calculate_slider_step( + min_value: float, max_value: float, steps: int = 100 +) -> float: """Calculates a step value for use in e.g. dcc.RangeSlider() component that will always be rounded. diff --git a/webviz_config/utils/_silence_flask_startup.py b/webviz_config/utils/_silence_flask_startup.py index edd27e21..3e04e325 100644 --- a/webviz_config/utils/_silence_flask_startup.py +++ b/webviz_config/utils/_silence_flask_startup.py @@ -1,7 +1,9 @@ +from typing import Any + import flask -def silence_flask_startup(): +def silence_flask_startup() -> None: # pylint: disable=line-too-long """Calling this function monkey patches the function flask.cli.show_server_banner (https://github.com/pallets/flask/blob/a3f07829ca03bf312b12b3732e917498299fa82d/src/flask/cli.py#L657-L683) @@ -21,7 +23,7 @@ def silence_flask_startup(): (all other information/output from the flask instance is untouched). """ - def silent_function(*_args, **_kwargs): + def silent_function(*_args: Any, **_kwargs: Any) -> None: pass - flask.cli.show_server_banner = silent_function + flask.cli.show_server_banner = silent_function # type: ignore[attr-defined] diff --git a/webviz_config/webviz_assets.py b/webviz_config/webviz_assets.py index 72dd3491..9f305d2f 100644 --- a/webviz_config/webviz_assets.py +++ b/webviz_config/webviz_assets.py @@ -2,7 +2,9 @@ import os import shutil import pathlib +from typing import Optional +from dash import Dash import flask from .utils import terminal_colors @@ -32,22 +34,22 @@ class WebvizAssets: but with same filename) and also assignes URI friendly resource IDs. """ - def __init__(self): - self._assets = {} + def __init__(self) -> None: + self._assets: dict = {} self._portable = False @property - def portable(self): + def portable(self) -> bool: return self._portable @portable.setter - def portable(self, portable): + def portable(self, portable: bool) -> None: self._portable = portable - def _base_folder(self): + def _base_folder(self) -> str: return "assets" if self.portable else "temp" - def add(self, filename): + def add(self, filename: pathlib.Path) -> str: path = pathlib.Path(filename) if filename not in self._assets.values(): @@ -58,7 +60,7 @@ def add(self, filename): return os.path.normcase(os.path.join(self._base_folder(), assigned_id)) - def register_app(self, app): + def register_app(self, app: Dash) -> None: """In non-portable mode, this function can be called by the application. It routes the Dash application to the added assets on disk, making hot reloading and more interactive development of the @@ -66,13 +68,13 @@ def register_app(self, app): """ @app.server.route(f"/{self._base_folder()}/") - def _send_file(asset_id): + def _send_file(asset_id: str) -> Optional[flask.wrappers.Response]: if asset_id in self._assets: # Only serve white listed resources path = pathlib.Path(self._assets[asset_id]) return flask.send_from_directory(path.parent, path.name) return None - def make_portable(self, asset_folder): + def make_portable(self, asset_folder: str) -> None: """Copy over all added assets to the given folder (asset_folder). """ @@ -91,7 +93,7 @@ def make_portable(self, asset_folder): f"{terminal_colors.END}" ) - def _generate_id(self, filename): + def _generate_id(self, filename: str) -> str: """From the filename, create a safe resource id not already present """ asset_id = base_id = re.sub( diff --git a/webviz_config/webviz_store.py b/webviz_config/webviz_store.py index 80277035..b4a13cfd 100644 --- a/webviz_config/webviz_store.py +++ b/webviz_config/webviz_store.py @@ -7,6 +7,7 @@ import inspect import pathlib from collections import defaultdict +from typing import Callable, List, Union, Any import pandas as pd @@ -17,16 +18,15 @@ class WebvizStorage: RETURN_TYPES = [pd.DataFrame, pathlib.Path, io.BytesIO] - def __init__(self): + def __init__(self) -> None: self._use_storage = False - self.storage_functions = set() - self.storage_function_argvalues = defaultdict(dict) + self.storage_functions: set = set() + self.storage_function_argvalues: defaultdict = defaultdict(dict) - def register_function(self, func): + def register_function(self, func: Callable) -> None: """This function is automatically called by the function decorator @webvizstore, registering the function it decorates. """ - return_type = inspect.getfullargspec(func).annotations["return"] if return_type not in WebvizStorage.RETURN_TYPES: @@ -37,23 +37,23 @@ def register_function(self, func): self.storage_functions.add(func) @property - def storage_folder(self): + def storage_folder(self) -> str: return self._storage_folder @storage_folder.setter - def storage_folder(self, path): + def storage_folder(self, path: str) -> None: os.makedirs(path, exist_ok=True) self._storage_folder = path @property - def use_storage(self): + def use_storage(self) -> bool: return self._use_storage @use_storage.setter - def use_storage(self, use_storage): + def use_storage(self, use_storage: bool) -> None: self._use_storage = use_storage - def register_function_arguments(self, functionarguments): + def register_function_arguments(self, functionarguments: List[tuple]) -> None: """The input here is from class functions `add_webvizstore(self)` in the different plugins requested from the configuration file. @@ -74,7 +74,7 @@ def register_function_arguments(self, functionarguments): repr(argtuples) ] = argtuples - def _unique_path(self, func, argtuples): + def _unique_path(self, func: Callable, argtuples: tuple) -> str: """Encodes the argumenttuples as bytes, and then does a sha256 on that. Mutable arguments are accepted in the argument tuples, however it is the plugin author that needs to be responsible for making sure that @@ -89,31 +89,31 @@ def _unique_path(self, func, argtuples): return os.path.join(self.storage_folder, filename) @staticmethod - def _undecorate(func): + def _undecorate(func: Callable) -> Callable: """This unwraps potential multiple level of decorators, to get access to the original function. """ while hasattr(func, "__wrapped__"): - func = func.__wrapped__ + func = func.__wrapped__ # type: ignore[attr-defined] return func @staticmethod - def string(func, kwargs): + def string(func: Callable, kwargs: dict) -> str: strkwargs = ", ".join([f"{k}={v!r}" for k, v in kwargs.items()]) return f"{func.__name__}({strkwargs})" @staticmethod - def _dict_to_tuples(dictionary): + def _dict_to_tuples(dictionary: dict) -> tuple: """Since dictionaries are not hashable, this is a helper function converting a dictionary into a sorted tuple.""" return tuple(sorted(dictionary.items())) @staticmethod - def complete_kwargs(func, kwargs): + def complete_kwargs(func: Callable, kwargs: dict) -> dict: """This takes in a dictionary kwargs, and returns an updated dictionary where missing arguments are added with default values.""" @@ -127,7 +127,9 @@ def complete_kwargs(func, kwargs): return kwargs - def get_stored_data(self, func, *args, **kwargs): + def get_stored_data( + self, func: Callable, *args: Any, **kwargs: Any + ) -> Union[pd.DataFrame, pathlib.Path, io.BytesIO]: argspec = inspect.getfullargspec(func) for arg_name, arg in zip(argspec.args, args): @@ -154,7 +156,7 @@ def get_stored_data(self, func, *args, **kwargs): f"{WebvizStorage.string(func, kwargs)}." ) - def build_store(self): + def build_store(self) -> None: total_calls = sum( len(calls) for calls in self.storage_function_argvalues.values() @@ -193,12 +195,12 @@ def build_store(self): ) -def webvizstore(func): +def webvizstore(func: Callable) -> Callable: WEBVIZ_STORAGE.register_function(func) @functools.wraps(func) - def wrapper_decorator(*args, **kwargs): + def wrapper_decorator(*args: Any, **kwargs: Any) -> Any: if WEBVIZ_STORAGE.use_storage: return WEBVIZ_STORAGE.get_stored_data(func, *args, **kwargs) return func(*args, **kwargs) @@ -210,7 +212,7 @@ def wrapper_decorator(*args, **kwargs): @webvizstore -def get_resource(filename) -> pathlib.Path: +def get_resource(filename: str) -> pathlib.Path: """Utility funtion for getting a filename which works both for non-portable and portable webviz instances."""