diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 74680b10..ebd8f51e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,11 +8,13 @@ on: push: branches: - main + - development # Trigger on any push to a PR that targets master pull_request: branches: - main + - development env: name: DeepCAVE @@ -27,22 +29,20 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | - pip install -e ".[dev]" + pip install . + pip install .[dev] - name: Make docs run: | + cd docs make docs - - name: Make Examples - run: | - make examples - - name: Pull latest gh-pages - if: (contains(github.ref, 'main')) + if: (contains(github.ref, 'development') || contains(github.ref, 'main')) run: | cd .. git clone https://github.com/automl/${{ env.name }}.git --branch gh-pages --single-branch gh-pages @@ -56,7 +56,7 @@ jobs: cp -r ../${{ env.name }}/docs/build/html $branch_name - name: Push to gh-pages - if: (contains(github.ref, 'main')) + if: (contains(github.ref, 'development') || contains(github.ref, 'main')) run: | last_commit=$(git log --pretty=format:"%an: %s") cd ../gh-pages diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index b9fbf771..a8070581 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -6,15 +6,19 @@ on: # When a push occurs on either of these branches push: - branches: - - main - - development + branches-ignore: + - '**' + # branches: + # - main + # - development # When a push occurs on a PR that targets these branches pull_request: - branches: - - main - - development + branches-ignore: + - '**' + # branches: + # - main + # - development jobs: run-all-files: @@ -25,10 +29,10 @@ jobs: with: submodules: recursive - - name: Setup Python 3.8 + - name: Setup Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install pre-commit run: | diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c9599834..3b59e5d9 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,4 +1,4 @@ -name: PyTest +name: tests on: [ push ] jobs: pytest: @@ -11,10 +11,10 @@ jobs: # $CONDA is an environment variable pointing to the root of the miniconda directory echo $CONDA/bin >> $GITHUB_PATH - - name: Setup Python 3.8 + - name: Setup Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8d1cd958..00000000 --- a/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -# syntax=docker/dockerfile:1 - -FROM continuumio/miniconda3 - -# Install linux dependencies -RUN apt-get update -y -RUN apt install -y build-essential -RUN apt-get install -y swig -RUN apt-get install -y redis-server - -# Copy files -COPY . /DeepCAVE -WORKDIR /DeepCAVE - -RUN conda update conda -y - -# Create new environment -RUN conda env create -f environment.yml - -# Make RUN commands use the new environment: -SHELL ["conda", "run", "-n", "DeepCAVE", "/bin/bash", "-c"] - -# Install DeepCAVE -RUN pip install . \ No newline at end of file diff --git a/README.md b/README.md index 7cbfc40a..ab2aae9d 100644 --- a/README.md +++ b/README.md @@ -67,12 +67,10 @@ The webserver as well as the queue/workers can be started by running ``` deepcave --start ``` -or -``` -./start.sh -``` -Visit `http://127.0.0.1:8050/` to get started. +Visit `http://127.0.0.1:8050/` to get started. The following figures gives +you a first impression of DeepCAVE. You can find more screenshots +in the documentation. -![interface](media/interface.png) +![interface](docs/images/plugins/pareto_front.png) diff --git a/deepcave/config.py b/deepcave/config.py index 0c8e0c9e..2c6f2d56 100644 --- a/deepcave/config.py +++ b/deepcave/config.py @@ -1,4 +1,4 @@ -from typing import Type +from typing import Dict, List from pathlib import Path @@ -30,7 +30,7 @@ class Config: # Plugins @property - def PLUGINS(self) -> dict[str, list[Type["Plugin"]]]: + def PLUGINS(self) -> Dict[str, List["Plugin"]]: """ Returns: dictionary [category -> List[Plugins]] @@ -69,7 +69,7 @@ def PLUGINS(self) -> dict[str, list[Type["Plugin"]]]: # Run Converter @property - def AVAILABLE_CONVERTERS(self) -> list[Type["Run"]]: + def AVAILABLE_CONVERTERS(self) -> List["Run"]: from deepcave.runs.converters.bohb import BOHBRun from deepcave.runs.converters.deepcave import DeepCAVERun from deepcave.runs.converters.smac import SMACRun diff --git a/deepcave/evaluators/pending/ablation.py b/deepcave/evaluators/_pending/ablation.py similarity index 100% rename from deepcave/evaluators/pending/ablation.py rename to deepcave/evaluators/_pending/ablation.py diff --git a/deepcave/evaluators/pending/base_evaluator.py b/deepcave/evaluators/_pending/base_evaluator.py similarity index 100% rename from deepcave/evaluators/pending/base_evaluator.py rename to deepcave/evaluators/_pending/base_evaluator.py diff --git a/deepcave/evaluators/ice.py b/deepcave/evaluators/_pending/ice.py similarity index 100% rename from deepcave/evaluators/ice.py rename to deepcave/evaluators/_pending/ice.py diff --git a/deepcave/evaluators/pending/importance.py b/deepcave/evaluators/_pending/importance.py similarity index 100% rename from deepcave/evaluators/pending/importance.py rename to deepcave/evaluators/_pending/importance.py diff --git a/deepcave/evaluators/pending/importance_reworked.py b/deepcave/evaluators/_pending/importance_reworked.py similarity index 100% rename from deepcave/evaluators/pending/importance_reworked.py rename to deepcave/evaluators/_pending/importance_reworked.py diff --git a/deepcave/evaluators/pending/lpi.py b/deepcave/evaluators/_pending/lpi.py similarity index 100% rename from deepcave/evaluators/pending/lpi.py rename to deepcave/evaluators/_pending/lpi.py diff --git a/deepcave/evaluators/pending/lpi_old.py b/deepcave/evaluators/_pending/lpi_old.py similarity index 100% rename from deepcave/evaluators/pending/lpi_old.py rename to deepcave/evaluators/_pending/lpi_old.py diff --git a/deepcave/evaluators/fanova.py b/deepcave/evaluators/fanova.py index 1f8a09fd..e6ed3dd2 100644 --- a/deepcave/evaluators/fanova.py +++ b/deepcave/evaluators/fanova.py @@ -161,8 +161,7 @@ def quantify_importance( if sort: sorted_importance_dict = { - k: v - for k, v in sorted(importance_dict.items(), key=lambda item: item[1][1]) + k: v for k, v in sorted(importance_dict.items(), key=lambda item: item[1][1]) } return sorted_importance_dict @@ -277,15 +276,11 @@ def get_triple_marginals(self, params=None): self.cs_params[combi[1]].name, self.cs_params[combi[2]].name, ] - triple_marginals.append( - (tot_imp, combi_names[0], combi_names[1], combi_names[2]) - ) + triple_marginals.append((tot_imp, combi_names[0], combi_names[1], combi_names[2])) triple_marginal_performance = sorted(triple_marginals, reverse=True) if params: - triple_marginal_performance = triple_marginal_performance[ - : len(list(triplets)) - ] + triple_marginal_performance = triple_marginal_performance[: len(list(triplets))] for marginal, p1, p2, p3 in triple_marginal_performance: self.tot_imp_dict[(p1, p2, p3)] = marginal @@ -348,9 +343,7 @@ def get_triple_marginals(self, params=None): conditional[idx] = True if isinstance(hp, CategoricalHyperparameter): impute_values[idx] = len(hp.choices) - elif isinstance( - hp, (UniformFloatHyperparameter, UniformIntegerHyperparameter) - ): + elif isinstance(hp, (UniformFloatHyperparameter, UniformIntegerHyperparameter)): impute_values[idx] = -1 elif isinstance(hp, Constant): impute_values[idx] = 1 diff --git a/deepcave/plugins/__init__.py b/deepcave/plugins/__init__.py index f7ad9e06..7a057f66 100644 --- a/deepcave/plugins/__init__.py +++ b/deepcave/plugins/__init__.py @@ -25,23 +25,32 @@ class Plugin(Layout, ABC): + """ + Base class for all plugins. + + Attributes + ---------- + id : int + Unique identifier for the plugin. + name : str + Name of the plugin. It is shown in the navigation and in the title. + description : str, optional + Description of the plugin. Displayed below the title. + icon : str, optional + FontAwesome icon. Shown in the navigation. + button_caption : str, optional + Caption of the button. Shown only, if `StaticPlugin` is used. + activate_run_selection : bool, optional + Shows a dropdown to select a run in the inputs layout. + This feature is useful if only one run could be viewed at a time. + Moreover, it prevents the plugin to calculate results across all runs. + """ + id: str name: str - category: Optional[str] = None description: Optional[str] = None - position: int = 99999 icon: str = "far fa-file" - button_caption: str = "Process" - - """ - activate_run_selection: - Shows a dropdown to select a run in the inputs layout. This feature is useful if only one run could be viewed at - a time. Moreover, it prevents the plugin to calculate results across all runs. - - The run can be selected by inputs["run_name"]["value"]. - bool: True if run selection should be shown. - """ activate_run_selection: bool = False def __init__(self) -> None: @@ -56,7 +65,7 @@ def __init__(self) -> None: self.alert_color = "success" self.alert_update_required = False - self.runs: dict[str, AbstractRun] = {} # Set in __call__: run_name -> AbstractRun + self.runs: Dict[str, AbstractRun] = {} # Set in __call__: run_name -> AbstractRun super().__init__() @@ -79,7 +88,6 @@ def check_run_compatibility(run: AbstractRun) -> bool: ------- bool Returns True if the run is compatible. - """ return True @@ -106,6 +114,26 @@ def check_runs_compatibility(self, runs: List[AbstractRun]) -> None: def register_input( self, id: str, attributes: Union[str, Iterable[str]] = ("value",), filter=False ) -> str: + """ + Registers an input variable for the plugin. It is important to register the inputs + because callbacks have to be defined before the server is started. + After registering all inputs, an internal mapping is created. + + Parameters + ---------- + id : str + Specifies the id of the input. + attributes : Union[str, Iterable[str]], optional + Attributes which should be passed to the (dash) component, by default ("value",) + filter : bool, optional + Specifies if the input is a filter, by default False + + Returns + ------- + str + Unique id for the input and plugin. This is necessary because ids are defined globally. + """ + if isinstance(attributes, str): attributes = [attributes] @@ -122,6 +150,24 @@ def register_input( return self.get_internal_input_id(id) def register_output(self, id: str, attribute: str = "value", mpl=False) -> str: + """ + Registers an output variable for the plugin. + + Parameters + ---------- + id : str + Specifies the id of the output. + attribute : str, optional + Attribute, by default "value" + mpl : bool, optional + Specifies if the registration is for matplotlib or default, by default False + + Returns + ------- + str + Unique id for the output and plugin. This is necessary because ids are defined globally. + """ + assert isinstance(attribute, str) if mpl: @@ -142,7 +188,15 @@ def get_internal_input_id(self, id: str) -> str: def get_internal_output_id(self, id: str) -> str: return f"{self.id}-{id}-output" - def register_callbacks(self): + def register_callbacks(self) -> None: + """ + Registers basic callbacks for the plugin. Following callbacks are registered: + - If inputs changes, the changes are pasted back. This is in particular + interest if input dependencies are used. + - Alert messages. + - Raw data dialog. + """ + # We have to call the output layout one time to register # the values # Problem: Inputs/Outputs can't be changed afterwards anymore. @@ -284,6 +338,16 @@ def toggle_modal(n, is_open): return is_open, code def update_alert(self, text: str, color: str = "success"): + """ + Update the alert text and color. Will automatically trigger the alert callback. + + Parameters + ---------- + text : str + The text to display. + color : str, optional + The color to display, by default "success" + """ self.alert_text = text self.alert_color = color self.alert_update_required = True @@ -354,7 +418,7 @@ def _process_raw_outputs(self, inputs, raw_outputs): return outputs - def _list_to_dict(self, values: Iterable[str], input=True) -> dict[str, dict[str, str]]: + def _list_to_dict(self, values: Iterable[str], input=True) -> Dict[str, Dict[str, str]]: """ Maps the given values to a dict, regarding the sorting from either self.inputs or self.outputs. @@ -377,7 +441,7 @@ def _list_to_dict(self, values: Iterable[str], input=True) -> dict[str, dict[str return mapping - def _dict_to_list(self, d: dict[str, dict[str, str]], input=False) -> list[Optional[str]]: + def _dict_to_list(self, d: Dict[str, Dict[str, str]], input=False) -> List[Optional[str]]: """ Maps the given dict to a list, regarding the sorting from either self.inputs or self.outputs. @@ -407,7 +471,7 @@ def _dict_to_list(self, d: dict[str, dict[str, str]], input=False) -> list[Optio return result - def _dict_as_key(self, d: dict[str, Any], remove_filters=False) -> Optional[str]: + def _dict_as_key(self, d: Dict[str, Any], remove_filters=False) -> Optional[str]: """ Converts a dictionary to a key. Only ids from self.inputs are considered. @@ -431,10 +495,15 @@ def _dict_as_key(self, d: dict[str, Any], remove_filters=False) -> Optional[str] return string_to_hash(str(new_d)) - def __call__(self, render_button=False) -> list[Component]: + def __call__(self, render_button: bool = False) -> List[Component]: """ - We overwrite the get_layout method here as we use a different - interface compared to layout. + Returns the components for the plugin. Basically, all blocks and elements of the plugin + are stacked-up here + + Returns + ------- + List[Component] + Layout as list of components. """ self.previous_inputs = {} @@ -602,7 +671,21 @@ def register_out(a, b): return components @staticmethod - def get_run_input_layout(register: Callable[[str, Union[str, list[str]]], str]) -> Component: + def get_run_input_layout(register: Callable[[str, Union[str, List[str]]], str]) -> Component: + """ + Generates the run selection input. + This is only the case if `activate_run_selection` is True. + + Parameters + ---------- + register : Callable[[str, Union[str, List[str]]], str] + The register method to register (user) variables. + + Returns + ------- + Component + The layout of the run selection input. + """ return html.Div( [ dbc.Select( @@ -614,13 +697,27 @@ def get_run_input_layout(register: Callable[[str, Union[str, list[str]]], str]) @staticmethod def load_run_inputs( - runs: dict[str, Run], - groups: dict[str, GroupedRun], - check_run_compatibility: Callable, - ) -> dict[str, Any]: + runs: Dict[str, AbstractRun], + groups: Dict[str, GroupedRun], + check_run_compatibility: Callable[[AbstractRun], bool], + ) -> Dict[str, Any]: """ - Set `run_names` and displays both runs and group runs if - they are compatible. + Loads the options for `get_run_input_layout`. + Both runs and groups are displayed. + + Parameters + ---------- + runs : Dict[str, Run] + The runs to display. + groups : Dict[str, GroupedRun] + The groups to display. + check_run_compatibility : Callable[[AbstractRun], bool] + If a single run is compatible. If not, the run is not shown. + + Returns + ------- + Dict[str, Any] + Both runs and groups, separated by a separator. """ labels = [] @@ -655,14 +752,26 @@ def load_run_inputs( } } - def get_selected_runs(self, inputs: dict[str, Any]) -> list[AbstractRun]: + def get_selected_runs(self, inputs: Dict[str, Any]) -> List[AbstractRun]: """ Parses selected runs from inputs. - If self.activate_run_selection is set return only selected run + If self.activate_run_selection is set, return only selected run. Otherwise, return all + possible runs. - Otherwise, return all possible runs + Parameters + ---------- + inputs : Dict[str, Any] + The inputs to parse. - Can raise PreventUpdate() if activate_run_selection is set, but run_name not available + Returns + ------- + List[AbstractRun] + The selected runs. + + Raises + ------ + PreventUpdate + If `activate_run_selection` is set but `run_name` is not available. """ # Special case: If run selection is active @@ -684,33 +793,124 @@ def get_selected_runs(self, inputs: dict[str, Any]) -> list[AbstractRun]: else: return list(self.all_runs.values()) - def load_inputs(self) -> dict[str, Any]: + def load_inputs(self) -> Dict[str, Any]: + """ + Load the content for the defined inputs in `get_input_layout` and `get_filter_layout`. + This method is necessary to pre-load contents for the inputs. So, if the plugin is + called for the first time or there are no results in the cache, the plugin gets its + content from this method. + + Returns + ------- + Dict[str, Any] + Content to be filled. + """ return {} - def load_dependency_inputs(self, previous_inputs, inputs, selected_run=None): + def load_dependency_inputs( + self, + previous_inputs: Dict[str, Any], + inputs: Dict[str, Any], + selected_run: Optional[Union[AbstractRun, List[AbstractRun]]] = None, + ) -> Dict[str, Any]: + """ + Same as `load_inputs` but called after inputs have changed. Provides a lot of flexibility. + + Parameters + ---------- + previous_inputs : Dict[str, Any] + Previous content of the inputs. + inputs : Dict[str, Any] + Current content of the inputs. + selected_run : Optional[Union[AbstractRun, List[AbstractRun]]], optional + The selected run from the user. In case of `activate_run_selection`, a list of runs + are passed. Defaults to None. + + Returns + ------- + Dict[str, Any] + Content to be filled. + """ + return inputs @staticmethod - def get_input_layout(register) -> list[Component]: + def get_input_layout(register: Callable[[str, Union[str, List[str]]], str]) -> List[Component]: + """ + Layout for the input block. + + Parameters + ---------- + register : Callable[[str, Union[str, List[str]]], str] + The register method to register (user) variables. + + Returns + ------- + List[Component] + Layouts for the input block. + """ + return [] @staticmethod - def get_filter_layout(register): + def get_filter_layout(register: Callable[[str, Union[str, List[str]]], str]): + """ + Layout for the filter block. + + Parameters + ---------- + register : Callable[[str, Union[str, List[str]]], str] + The register method to register (user) variables. + + Returns + ------- + List[Component] + Layouts for the filter block. + """ + return [] @staticmethod - def get_output_layout(register): + def get_output_layout(register: Callable[[str, Union[str, List[str]]], str]): + """ + Layout for the output block. + + Parameters + ---------- + register : Callable[[str, Union[str, List[str]]], str] + The register method to register outputs. + + Returns + ------- + List[Component] + Layouts for the output block. + """ + return [] @staticmethod - def get_mpl_output_layout(register): + def get_mpl_output_layout(register: Callable[[str, Union[str, List[str]]], str]): + """ + Layout for the matplotlib output block. + + Parameters + ---------- + register : Callable[[str, Union[str, List[str]]], str] + The register method to register outputs. + + Returns + ------- + List[Component] + Layout for the matplotlib output block. + """ + return [] def load_outputs( self, inputs: Dict[str, Dict[str, str]], outputs: Dict[str, Union[str, Dict[str, str]]], - runs: Union[AbstractRun, dict[str, AbstractRun]], + runs: Union[AbstractRun, Dict[str, AbstractRun]], ) -> List[Component]: """ Reads in the raw data and prepares them for the layout. @@ -722,7 +922,7 @@ def load_outputs( outputs : Dict[str, Union[str, Dict[str, str]]] Raw outputs from the runs. If `activate_run_selection` is set, a Dict[str, str] is returned. - runs : Union[AbstractRun, dict[str, AbstractRun]] + runs : Union[AbstractRun, Dict[str, AbstractRun]] All selected runs. If `activate_run_selection` is set, only the selected run is returned. @@ -738,7 +938,7 @@ def load_mpl_outputs( self, inputs: Dict[str, Dict[str, str]], outputs: Dict[str, Union[str, Dict[str, str]]], - runs: Union[AbstractRun, dict[str, AbstractRun]], + runs: Union[AbstractRun, Dict[str, AbstractRun]], ) -> List[Component]: """ Reads in the raw data and prepares them for the layout. @@ -750,7 +950,7 @@ def load_mpl_outputs( outputs : Dict[str, Union[str, Dict[str, str]]] Raw outputs from the runs. If `activate_run_selection` is set, a Dict[str, str] is returned. - runs : Union[AbstractRun, dict[str, AbstractRun]] + runs : Union[AbstractRun, Dict[str, AbstractRun]] All selected runs. If `activate_run_selection` is set, only the selected run is returned. @@ -764,7 +964,22 @@ def load_mpl_outputs( @staticmethod @abstractmethod - def process(run: AbstractRun, inputs): + def process(run: AbstractRun, inputs: Dict[str, Any]): + """ + Returns raw data based on a run and input data. + + Warning + ------- + The returned data must be JSON serializable. + + Parameters + ---------- + run : AbstractRun + The run to process. + inputs : Dict[str, Any] + Input data. + """ + pass @staticmethod diff --git a/deepcave/plugins/pending/budget_correlation.py b/deepcave/plugins/_pending/budget_correlation.py similarity index 100% rename from deepcave/plugins/pending/budget_correlation.py rename to deepcave/plugins/_pending/budget_correlation.py diff --git a/deepcave/plugins/pending/configspace.py b/deepcave/plugins/_pending/configspace.py similarity index 100% rename from deepcave/plugins/pending/configspace.py rename to deepcave/plugins/_pending/configspace.py diff --git a/deepcave/plugins/pending/ice.py b/deepcave/plugins/_pending/ice.py similarity index 98% rename from deepcave/plugins/pending/ice.py rename to deepcave/plugins/_pending/ice.py index f786b509..2a690c91 100644 --- a/deepcave/plugins/pending/ice.py +++ b/deepcave/plugins/_pending/ice.py @@ -5,7 +5,7 @@ from dash import dcc, html from dash.exceptions import PreventUpdate -from deepcave.evaluators.ice import ICE as ICEEvaluator +from deepcave.evaluators.pending.ice import ICE as ICEEvaluator from deepcave.plugins.static_plugin import StaticPlugin from deepcave.runs import AbstractRun, check_equality from deepcave.utils.compression import deserialize, serialize diff --git a/deepcave/plugins/pending/template.py b/deepcave/plugins/_pending/template.py similarity index 100% rename from deepcave/plugins/pending/template.py rename to deepcave/plugins/_pending/template.py diff --git a/deepcave/queue.py b/deepcave/queue.py index 40602f7b..d02b2a07 100644 --- a/deepcave/queue.py +++ b/deepcave/queue.py @@ -1,10 +1,8 @@ -from typing import Any, Callable - import redis +from typing import Any, Callable, List, Dict from rq import Queue as _Queue from rq import Worker from rq.job import Job - from deepcave.utils.logs import get_logger logger = get_logger(__name__) @@ -25,11 +23,7 @@ def ready(self): return False def is_processed(self, job_id): - if ( - self.is_running(job_id) - or self.is_pending(job_id) - or self.is_finished(job_id) - ): + if self.is_running(job_id) or self.is_pending(job_id) or self.is_finished(job_id): return True return False @@ -55,7 +49,7 @@ def is_finished(self, job_id): return False - def get_jobs(self, registry="running") -> list[Job]: + def get_jobs(self, registry="running") -> List[Job]: if registry == "running": registry = self._queue.started_job_registry elif registry == "pending": @@ -72,13 +66,13 @@ def get_jobs(self, registry="running") -> list[Job]: return results - def get_running_jobs(self) -> list[Job]: + def get_running_jobs(self) -> List[Job]: return self.get_jobs(registry="running") - def get_pending_jobs(self) -> list[Job]: + def get_pending_jobs(self) -> List[Job]: return self.get_jobs(registry="pending") - def get_finished_jobs(self) -> list[Job]: + def get_finished_jobs(self) -> List[Job]: return self.get_jobs(registry="finished") def delete_job(self, job_id: str): @@ -94,9 +88,7 @@ def delete_job(self, job_id: str): except: pass - def enqueue( - self, func: Callable[[Any], Any], args: Any, job_id: str, meta: dict[str, str] - ): + def enqueue(self, func: Callable[[Any], Any], args: Any, job_id: str, meta: Dict[str, str]): # First check if job_id is already in use if self.is_processed(job_id): logger.debug("Job was not added because it was processed already.") diff --git a/deepcave/runs/__init__.py b/deepcave/runs/__init__.py index ab48da67..dcd0e0e2 100644 --- a/deepcave/runs/__init__.py +++ b/deepcave/runs/__init__.py @@ -50,7 +50,7 @@ class Trial: start_time: float end_time: float status: Status - additional: dict[str, Any] + additional: Dict[str, Any] def __post_init__(self): if isinstance(self.status, int): diff --git a/deepcave/runs/grouped_run.py b/deepcave/runs/grouped_run.py index ebdbae29..a5933d10 100644 --- a/deepcave/runs/grouped_run.py +++ b/deepcave/runs/grouped_run.py @@ -88,7 +88,7 @@ def hash(self) -> str: return string_to_hash(total_hash_str) @property - def run_names(self) -> list[str]: + def run_names(self) -> List[str]: return [run.name for run in self.runs] def get_model(self, config_id): diff --git a/deepcave/runs/handler.py b/deepcave/runs/handler.py index 390234ff..d4db15e4 100644 --- a/deepcave/runs/handler.py +++ b/deepcave/runs/handler.py @@ -1,4 +1,4 @@ -from typing import Optional, Type +from typing import Optional, Type, Dict, List import time from pathlib import Path @@ -6,7 +6,6 @@ from deepcave.config import Config from deepcave.runs import AbstractRun, NotValidRunError from deepcave.runs.grouped_run import GroupedRun -from deepcave.runs.run import Run from deepcave.utils.logs import get_logger @@ -24,21 +23,19 @@ def __init__(self, config: Config, cache: "Cache", run_cache: "RunCache") -> Non self.working_dir: Optional[Path] = None # Available converters - self.available_run_classes: list[Type[Run]] = config.AVAILABLE_CONVERTERS + self.available_run_classes: List[AbstractRun] = config.AVAILABLE_CONVERTERS # Internal state - self.runs: dict[str, Run] = {} # run_name -> Run - self.groups: dict[str, GroupedRun] = {} # group_name -> GroupedRun + self.runs: Dict[str, AbstractRun] = {} # run_name -> Run + self.groups: Dict[str, GroupedRun] = {} # group_name -> GroupedRun # Read from cache self.load_from_cache() def load_from_cache(self): working_dir: Path = Path(self.c.get("working_dir")) - selected_runs: list[str] = self.c.get("selected_run_names") # run_name - groups: dict[str, list[str]] = self.c.get( - "groups" - ) # group_name -> list[run_names] + selected_runs: List[str] = self.c.get("selected_run_names") # run_name + groups: Dict[str, List[str]] = self.c.get("groups") # group_name -> List[run_names] print(f"Resetting working directory to {working_dir}") self.update_working_directory(working_dir) @@ -49,9 +46,7 @@ def load_from_cache(self): print(f"Setting groups to {groups}") self.update_groups(groups) - def update_working_directory( - self, working_directory: Path, force_clear: bool = False - ): + def update_working_directory(self, working_directory: Path, force_clear: bool = False): """ Set working directory. If it is the same as before -> Do nothing. @@ -70,7 +65,7 @@ def update_working_directory( # Set in cache self.c.set("working_dir", value=str(working_directory)) - def update_runs(self, selected_run_names: Optional[list[str]] = None): + def update_runs(self, selected_run_names: Optional[List[str]] = None): """ Loads selected runs and update cache if files changed. @@ -82,7 +77,7 @@ def update_runs(self, selected_run_names: Optional[list[str]] = None): """ if selected_run_names is None: selected_run_names = self.c.get("selected_run_names") - new_runs: dict[str, Run] = {} + new_runs: Dict[str, AbstractRun] = {} class_hint = None for run_name in selected_run_names: @@ -95,9 +90,7 @@ def update_runs(self, selected_run_names: Optional[list[str]] = None): self.runs = new_runs self.c.set("selected_run_names", value=self.get_run_names()) - def update_run( - self, run_name: str, class_hint: Optional[Type[Run]] = None - ) -> Optional[Run]: + def update_run(self, run_name: str, class_hint: Optional[AbstractRun] = None) -> Optional[AbstractRun]: """ Raises @@ -132,7 +125,7 @@ def update_run( self.rc.get(run) return run - def update_groups(self, groups: Optional[dict[str, list[str]]] = None) -> None: + def update_groups(self, groups: Optional[Dict[str, List[str]]] = None) -> None: """ Loads chosen groups @@ -147,9 +140,7 @@ def update_groups(self, groups: Optional[dict[str, list[str]]] = None) -> None: # Add groups groups = { - name: GroupedRun( - name, [self.runs.get(run_name, None) for run_name in run_names] - ) + name: GroupedRun(name, [self.runs.get(run_name, None) for run_name in run_names]) for name, run_names in groups.items() } @@ -171,7 +162,7 @@ def update_groups(self, groups: Optional[dict[str, list[str]]] = None) -> None: def get_working_dir(self) -> Path: return self.working_dir - def get_run_names(self) -> list[str]: + def get_run_names(self) -> List[str]: return list(self.runs.keys()) def from_run_id(self, run_id: str) -> AbstractRun: @@ -203,10 +194,10 @@ def from_run_cache_id(self, run_cache_id: str) -> AbstractRun: f"Searched in {len(self.runs)} runs and {len(self.groups)} groups" ) - def get_groups(self) -> dict[str, GroupedRun]: + def get_groups(self) -> Dict[str, GroupedRun]: return self.groups.copy() - def get_available_run_names(self) -> list[str]: + def get_available_run_names(self) -> List[str]: run_names = [] try: @@ -218,7 +209,7 @@ def get_available_run_names(self) -> list[str]: return run_names - def get_runs(self, include_groups=False) -> dict[str, AbstractRun]: + def get_runs(self, include_groups=False) -> Dict[str, AbstractRun]: """ self.converter.get_run() might be expensive. Therefore, we cache it here, and only reload it, once working directory, run id or the id based on the files changed. @@ -239,9 +230,7 @@ def get_runs(self, include_groups=False) -> dict[str, AbstractRun]: return runs return self.runs - def get_run( - self, run_name: str, class_hint: Optional[Type[Run]] = None - ) -> Optional[Run]: + def get_run(self, run_name: str, class_hint: Optional[AbstractRun] = None) -> Optional[AbstractRun]: """ Try to load run from path by using all available converters, until a sufficient class is found. Try to load them in order by how many runs were already successfully converted from this class diff --git a/deepcave/runs/run.py b/deepcave/runs/run.py index e8cc3784..556373fb 100644 --- a/deepcave/runs/run.py +++ b/deepcave/runs/run.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Any, Optional, Union +from typing import Any, Optional, Union, Dict, List import json from pathlib import Path @@ -34,8 +34,8 @@ def __init__( self, name: str, configspace=None, - objectives: Union[Objective, list[Objective]] = None, - meta: dict[str, Any] = None, + objectives: Union[Objective, List[Objective]] = None, + meta: Dict[str, Any] = None, path: Optional[Union[str, Path]] = None, ): """ @@ -124,15 +124,15 @@ def exists(self) -> bool: def add( self, - costs: Union[list[float], float], - config: Union[dict, Configuration], # either dict or Configuration + costs: Union[List[float], float], + config: Union[Dict, Configuration], # either dict or Configuration budget: float = np.inf, start_time: float = 0.0, end_time: float = 0.0, status: Status = Status.SUCCESS, origin: str = None, model: Union[str, "torch.nn.Module"] = None, - additional: Optional[dict] = None, + additional: Optional[Dict] = None, ): """ diff --git a/deepcave/utils/cache.py b/deepcave/utils/cache.py index 3c36b294..20221a9a 100644 --- a/deepcave/utils/cache.py +++ b/deepcave/utils/cache.py @@ -72,7 +72,7 @@ def set(self, *keys, value) -> None: d[keys[-1]] = value self.write() - def set_dict(self, d: dict) -> None: + def set_dict(self, d: Dict) -> None: """Updates cache to a specific value""" self._data.update(d) diff --git a/deepcave/utils/compression.py b/deepcave/utils/compression.py index f0570e98..b5eae197 100644 --- a/deepcave/utils/compression.py +++ b/deepcave/utils/compression.py @@ -1,14 +1,15 @@ -from typing import TypeVar, Union +from typing import TypeVar, Union, Dict, List import json - import numpy as np import pandas as pd + JSON_DENSE_SEPARATORS = (",", ":") +TYPE = TypeVar("TYPE") -def serialize(data: Union[dict, list, pd.DataFrame]) -> str: +def serialize(data: Union[Dict, List, pd.DataFrame]) -> str: """ Serialize a dataframe to a string. """ @@ -26,9 +27,6 @@ def default(self, obj): return json.dumps(data, cls=Encoder, separators=JSON_DENSE_SEPARATORS) -TYPE = TypeVar("TYPE") - - def deserialize(string: str, dtype: TYPE = pd.DataFrame) -> TYPE: """ Deserialize a dataframe from a string. diff --git a/deepcave/utils/data_structures.py b/deepcave/utils/data_structures.py index fd903abd..322201b8 100644 --- a/deepcave/utils/data_structures.py +++ b/deepcave/utils/data_structures.py @@ -1,4 +1,7 @@ -def update_dict(a: dict[str, dict], b: dict[str, dict]): +from typing import Dict + + +def update_dict(a: Dict[str, Dict], b: Dict[str, Dict]): """ Updates a from b inplace. """ diff --git a/logging.yml b/deepcave/utils/logging.yml similarity index 100% rename from logging.yml rename to deepcave/utils/logging.yml diff --git a/deepcave/utils/styled_plotty.py b/deepcave/utils/styled_plotty.py index 0ea2603e..453f4b6a 100644 --- a/deepcave/utils/styled_plotty.py +++ b/deepcave/utils/styled_plotty.py @@ -1,7 +1,8 @@ +from typing import Tuple import plotly.express as px -def hex_to_rgb(hex_string: str) -> tuple[int, int, int]: +def hex_to_rgb(hex_string: str) -> Tuple[int, int, int]: """ Converts a hex_string to a tuple of rgb values. Requires format including #, e.g.: diff --git a/deepcave/utils/util.py b/deepcave/utils/util.py index 435d36d8..effe9766 100644 --- a/deepcave/utils/util.py +++ b/deepcave/utils/util.py @@ -1,4 +1,4 @@ -from typing import Any, Iterable, Optional, Union +from typing import Any, Iterable, Optional, Union, Tuple, List, Dict import base64 import random @@ -35,14 +35,12 @@ def matplotlib_to_html_image(fig: plt.Figure) -> html.Img: # display any kind of image taken from # https://github.com/plotly/dash/issues/71 encoded_image = base64.b64encode(buffer.read()) - return html.Img( - src=f"data:image/png;base64,{encoded_image.decode()}", className="img-fluid" - ) + return html.Img(src=f"data:image/png;base64,{encoded_image.decode()}", className="img-fluid") def encode_data( data: pd.DataFrame, cs: Optional[ConfigurationSpace] = None -) -> Union[pd.DataFrame, tuple[pd.DataFrame, dict[pd.Series, pd.Series]]]: +) -> Union[pd.DataFrame, Tuple[pd.DataFrame, Dict[pd.Series, pd.Series]]]: # converts only columns with "config." prefix if cs: return _encode(data, cs) @@ -82,9 +80,7 @@ def _transform(data, from_cols, to_cols, choices, transformer_class): if transformer_class is OneHotEncoder: add_kwargs = {"sparse": False} data[to_cols] = pd.DataFrame( - transformer_class(categories=choices, **add_kwargs).fit_transform( - data[from_cols] - ), + transformer_class(categories=choices, **add_kwargs).fit_transform(data[from_cols]), columns=to_cols, index=data.index, ) @@ -127,11 +123,11 @@ def _transform(data, from_cols, to_cols, choices, transformer_class): return data, org_cols -def add_prefix_to_dict(data: dict[str, Any], prefix: str) -> dict[str, Any]: +def add_prefix_to_dict(data: Dict[str, Any], prefix: str) -> Dict[str, Any]: """Adds a prefix to every key in a dictionary""" return {f"{prefix}{key}": value for key, value in data.items()} -def add_prefix_to_list(data: Iterable[str], prefix: str) -> list[str]: +def add_prefix_to_list(data: Iterable[str], prefix: str) -> List[str]: """Adds a prefix to every item in an iterable (e.g. a list). Returns a list""" return [f"{prefix}{item}" for item in data] diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 29df72e3..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,44 +0,0 @@ -version: '3' - -services: - redis: - image: redis:alpine - command: redis-server - ports: - - "6379:6379" - volumes: - - $PWD/redis-data:/var/lib/redis - - $PWD/redis.conf:/usr/local/etc/redis/redis.conf - environment: - - REDIS_REPLICATION_MODE=master - #networks: - # node_net: - # ipv4_address: 172.28.1.4 - - worker: - build: - context: . - dockerfile: Dockerfile - command: python worker.py - volumes: - - .:/DeepCAVE - ports: - - "8050:8050" - - server: - build: - context: . - dockerfile: Dockerfile - command: python server.py - volumes: - - .:/DeepCAVE - ports: - - "8050:8050" - -# Networking for the Redis container -#networks: -# node_net: -# ipam: -# driver: default -# config: -# - subnet: 172.28.0.0/16 \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile index 5c78d34a..3ab82116 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -26,4 +26,4 @@ examples: @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." -docs: html linkcheck +docs: clean examples linkcheck diff --git a/docs/api.rst b/docs/api.rst index 87ed83f5..ee771966 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,7 +6,5 @@ API References :toctree: api :recursive: - deepcave.layouts - deepcave.plugins deepcave.runs - deepcave.utils + deepcave.plugins diff --git a/docs/conf.py b/docs/conf.py index b287ba5d..f4f782c5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,7 @@ }, "sphinx_gallery_conf": { "examples_dirs": "../examples", - "ignore_pattern": "logs/*", + "ignore_pattern": ".*logs$|.*__pycache__$|.*_pending$", }, } diff --git a/docs/images/favicon.ico b/docs/images/favicon.ico deleted file mode 100644 index e5519c73..00000000 Binary files a/docs/images/favicon.ico and /dev/null differ diff --git a/media/interface.png b/media/interface.png deleted file mode 100644 index 13bcf9d1..00000000 Binary files a/media/interface.png and /dev/null differ diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 3ae92625..00000000 --- a/mypy.ini +++ /dev/null @@ -1,8 +0,0 @@ -[mypy] -# Reports any config lines that are not recognized -warn_unused_configs=True -ignore_missing_imports=True -follow_imports=skip -disallow_untyped_defs=True -disallow_incomplete_defs=True -disallow_untyped_decorators=True \ No newline at end of file diff --git a/setup.py b/setup.py index fc849204..9b35e493 100644 --- a/setup.py +++ b/setup.py @@ -49,8 +49,8 @@ def read_file(file_name): packages=setuptools.find_packages( exclude=["*.tests", "*.tests.*", "tests.*", "tests"], ), - package_data={"deepcave": ["logging.yml"]}, - python_requires=">=3.8", + package_data={"deepcave": ["utils/logging.yml"]}, + python_requires=">=3.8, <3.10", install_requires=read_file("./requirements.txt").split("\n"), extras_require=extras_require, entry_points={ @@ -61,7 +61,6 @@ def read_file(file_name): classifiers=[ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Development Status :: 3 - Alpha", "Natural Language :: English", "Environment :: Console", diff --git a/tests/test_utils.py b/tests/test_utils/test_cache.py similarity index 98% rename from tests/test_utils.py rename to tests/test_utils/test_cache.py index 9fc8c2b8..7030261c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils/test_cache.py @@ -127,8 +127,8 @@ def test_cache_file_None(self): """Cache should still work, even when file is None""" cache = Cache(None) - cache.set(1, 2, 3, value=4) - self.assertEqual(4, cache.get(1, 2, 3)) + cache.set("1", "2", "3", value=4) + self.assertEqual(4, cache.get("1", "2", "3")) class TestCompression(unittest.TestCase): @@ -148,11 +148,7 @@ def test_dataframe_conversion(self): df_ser = serialize(df) self.assertEqual( - "{" - '"0":{"0":1,"1":"a"},' - '"1":{"0":2,"1":"b"},' - '"2":{"0":null,"1":"c"}' - "}", + "{" '"0":{"0":1,"1":"a"},' '"1":{"0":2,"1":"b"},' '"2":{"0":null,"1":"c"}' "}", df_ser, ) df_cycled = deserialize(df_ser, dtype=pd.DataFrame) diff --git a/tests/test_utils/test_utils.py b/tests/test_utils/test_utils.py deleted file mode 100644 index 9fc8c2b8..00000000 --- a/tests/test_utils/test_utils.py +++ /dev/null @@ -1,434 +0,0 @@ -import json -import logging -import string -import unittest -from pathlib import Path - -import matplotlib.pyplot as plt -import pandas as pd -from dash import html - -from deepcave.utils.cache import Cache -from deepcave.utils.compression import deserialize, serialize -from deepcave.utils.data_structures import update_dict -from deepcave.utils.files import make_dirs -from deepcave.utils.hash import file_to_hash, string_to_hash -from deepcave.utils.layout import ( - get_checklist_options, - get_radio_options, - get_select_options, - get_slider_marks, -) -from deepcave.utils.logs import get_logger -from deepcave.utils.styled_plotty import get_color, hex_to_rgb -from deepcave.utils.util import ( - add_prefix_to_dict, - add_prefix_to_list, - get_random_string, - matplotlib_to_html_image, -) - - -class TestCache(unittest.TestCase): - def test_cache_from_new_file(self): - cache_file = Path("tests/cache_test/cache.json") - cache_file.unlink(missing_ok=True) - - # Load with new file - self.assertFalse(cache_file.exists()) - cache = Cache(cache_file) - - # Set values - cache.set("a", "b", "c", value=4) - - # Check whether values were written to file - self.assertTrue(cache_file.exists()) - with cache_file.open() as f: - dict_from_file = json.load(f) - - self.assertIn("a", dict_from_file) - self.assertIn("b", dict_from_file["a"]) - self.assertIn("c", dict_from_file["a"]["b"]) - self.assertEqual(4, dict_from_file["a"]["b"]["c"]) - - # Cleanup - cache_file.unlink() - - def test_cache_from_existing_file(self): - # Prepare existing file - cache_file = Path("tests/cache_test/cache2.json") - cache_file.parent.mkdir(parents=True, exist_ok=True) - cache_file.write_text('{"d": {"e": {"f": 32}}}') - - # Load with exising file - self.assertTrue(cache_file.exists()) - cache = Cache(cache_file) - - # Get values - value = cache.get("d", "e", "f") - self.assertEqual(32, value) - - # Cleanup - cache_file.unlink() - - def test_cache_defaults(self): - cache_file = Path("tests/cache_test/cache3.json") - cache_file.unlink(missing_ok=True) - - # Load with new file - self.assertFalse(cache_file.exists()) - defaults = {"i": {"j": 4}, "k": {"l": {"m": 42}}, "v": 9} - cache = Cache(cache_file, defaults=defaults) - - # Test get values - self.assertEqual(4, cache.get("i", "j")) - self.assertEqual(42, cache.get("k", "l", "m")) - self.assertEqual(9, cache.get("v")) - - # Check whether values were written to file - self.assertTrue(cache_file.exists()) - with cache_file.open() as f: - dict_from_file = json.load(f) - - self.assertIn("i", dict_from_file) - self.assertIn("k", dict_from_file) - self.assertIn("v", dict_from_file) - self.assertIn("j", dict_from_file["i"]) - self.assertIn("l", dict_from_file["k"]) - self.assertIn("m", dict_from_file["k"]["l"]) - self.assertEqual(4, dict_from_file["i"]["j"]) - self.assertEqual(42, dict_from_file["k"]["l"]["m"]) - self.assertEqual(9, dict_from_file["v"]) - - # Cleanup - cache_file.unlink() - - def test_cache_has(self): - cache_file = Path("tests/cache_test/cache4.json") - cache_file.unlink(missing_ok=True) - - # Load with new file - self.assertFalse(cache_file.exists()) - defaults = {"i": {"j": 4}, "k": {"l": {"m": 42}}, "v": 9} - cache = Cache(cache_file, defaults=defaults) - - # Test has values - self.assertTrue(cache.has("i")) - self.assertTrue(cache.has("i", "j")) - self.assertTrue(cache.has("k")) - self.assertTrue(cache.has("k", "l")) - self.assertTrue(cache.has("k", "l", "m")) - self.assertTrue(cache.has("v")) - - # Cleanup - cache_file.unlink() - - def test_cache_file_None(self): - """Cache should still work, even when file is None""" - cache = Cache(None) - - cache.set(1, 2, 3, value=4) - self.assertEqual(4, cache.get(1, 2, 3)) - - -class TestCompression(unittest.TestCase): - def test_list_conversion(self): - a = [1, 2, 3, 4, 5, 6] - a_ser = serialize(a) - self.assertEqual("[1,2,3,4,5,6]", a_ser) - a_cycled = deserialize(a_ser, dtype=list) - self.assertIsInstance(a_cycled, list) - for i in range(len(a)): - self.assertEqual(a[i], a_cycled[i]) - - def test_dataframe_conversion(self): - x = [1, 2, None] - y = ["a", "b", "c"] - df = pd.DataFrame([x, y]) - - df_ser = serialize(df) - self.assertEqual( - "{" - '"0":{"0":1,"1":"a"},' - '"1":{"0":2,"1":"b"},' - '"2":{"0":null,"1":"c"}' - "}", - df_ser, - ) - df_cycled = deserialize(df_ser, dtype=pd.DataFrame) - self.assertIsInstance(df_cycled, pd.DataFrame) - self.assertTrue(all((df_cycled.to_numpy() == df.to_numpy()).reshape(-1))) - - -class TestDataStructures(unittest.TestCase): - def test_update_dict(self): - a = {"a": {"b": 1, "c": 2, "d": 3}} - b = {"a": {"b": 4, "e": 5}, "b": {"f": 6}} - - update_dict(a, b) - self.assertIn("a", a) - self.assertIn("b", a) - a_a = a["a"] - self.assertEqual(4, a_a["b"]) - self.assertEqual(2, a_a["c"]) - self.assertEqual(3, a_a["d"]) - self.assertEqual(5, a_a["e"]) - - a_b = a["b"] - self.assertEqual(6, a_b["f"]) - - -class TestFiles(unittest.TestCase): - def test_make_dirs(self): - def _test_path_procedure(argument, is_file=False): - path = Path(argument) - if is_file: - folder = path.parent - else: - folder = path - # Make sure that folder does not exist - if folder.exists(): - folder.rmdir() - - # Test - self.assertFalse(folder.exists()) - make_dirs(argument) - self.assertTrue(folder.exists()) - if is_file: - self.assertFalse(path.exists()) - - # Cleanup - folder.rmdir() - - _test_path_procedure("tests/new_folder") - _test_path_procedure("tests/new_folder/file.txt", is_file=True) - - -class TestHash(unittest.TestCase): - def test_string_to_hash(self): - a = string_to_hash("hello") - self.assertIsInstance(a, str) - - b = string_to_hash("world") - self.assertNotEqual(a, b) - - b = string_to_hash("Hello") - self.assertNotEqual(a, b) - - b = string_to_hash("hello") - self.assertEqual(a, b) - - def test_file_to_hash(self): - file = Path(__file__) - a = file_to_hash(file) - self.assertGreater(len(a), 5) - self.assertIsInstance(a, str) - - -class TestLayout(unittest.TestCase): - @unittest.SkipTest - def test_get_slider_marks(self): - # TODO(dwoiwode): Currently does not work as expected? - default_marks = get_slider_marks() - self.assertIsInstance(default_marks, dict) - a = default_marks[0] - self.assertEqual("None", a) - - abcde = list("ABCDE") - marks = get_slider_marks(abcde) - self.assertEqual(5, len(marks)) - for i, c in enumerate(abcde): - self.assertEqual(c, marks[i]) - - alphabet = string.ascii_uppercase - marks = get_slider_marks(list(alphabet), 10) - self.assertEqual(10, len(marks)) - - def _test_get_select_options(self, method): - # Test empty - options = method(labels=None, values=None, binary=False) - self.assertIsInstance(options, list) - self.assertEqual(0, len(options)) - - # Test binary - options = method(binary=True) - self.assertIsInstance(options, list) - self.assertEqual(2, len(options)) - no, yes = sorted(options, key=lambda o: o["value"]) - self.assertTrue(yes["value"]) - self.assertFalse(no["value"]) - - # Test copy labels - labels = list("ABCDEF") - options = method(labels) - self.assertEqual(len(labels), len(options)) - for expected, actual in zip(labels, options): - self.assertIsInstance(actual, dict) - self.assertEqual(expected, actual["label"]) - self.assertEqual(expected, actual["value"]) - - # Test copy values - values = list("12345678") - options = method(values=values) - self.assertEqual(len(values), len(options)) - for expected, actual in zip(values, options): - self.assertIsInstance(actual, dict) - self.assertEqual(expected, actual["label"]) - self.assertEqual(expected, actual["value"]) - - # Test labels + values - labels = list("ABCDEFGHIJ") - values = list("1234567890") - options = method(labels, values) - self.assertEqual(len(labels), len(options)) - for expected_label, expected_value, actual in zip(labels, values, options): - self.assertIsInstance(actual, dict) - self.assertEqual(expected_label, actual["label"]) - self.assertEqual(expected_value, actual["value"]) - - # Test unequal length labels + values - labels = list("ABCDEFGHIJ") - values = list("1234567") - self.assertRaises(ValueError, lambda: method(labels, values)) - - def test_get_select_options(self): - self._test_get_select_options(get_select_options) - - def test_get_radio_options(self): - self._test_get_select_options(get_radio_options) - - def test_get_checklist_options(self): - self._test_get_select_options(get_checklist_options) - - -class TestLogger(unittest.TestCase): - def test_get_logger(self): - logger = get_logger("TestLogger") - self.assertIsInstance(logger, logging.Logger) - self.assertEqual("TestLogger", logger.name) - - def test_logging_config(self): - mpl_logger = get_logger("matplotlib") - self.assertEqual(logging.INFO, mpl_logger.level) - self.assertFalse(mpl_logger.propagate) - - plugin_logger = get_logger("src.plugins") - self.assertEqual(logging.DEBUG, plugin_logger.level) - self.assertFalse(plugin_logger.propagate) - - -class TestStyledPlottly(unittest.TestCase): - def test_hex_to_rgb(self): - def assert_color(hex_code, expected_r, expected_g, expected_b): - r, g, b = hex_to_rgb(hex_code) - self.assertEqual( - expected_r, - r, - f"r value does not match for {hex_code} (wanted {expected_r}, got {r})", - ) - self.assertEqual( - expected_g, - g, - f"g value does not match for {hex_code} (wanted {expected_g}, got {g})", - ) - self.assertEqual( - expected_b, - b, - f"b value does not match for {hex_code} (wanted {expected_b}, got {b})", - ) - - assert_color("#000000", 0, 0, 0) - assert_color("#FFFFFF", 255, 255, 255) - assert_color("#ffffff", 255, 255, 255) - assert_color("#123456", 18, 52, 86) - - self.assertRaises(ValueError, lambda: hex_to_rgb("#0g0000")) - self.assertRaises(ValueError, lambda: hex_to_rgb("000000")) - - def test_get_color(self): - color_str = get_color(0) - self.assertEqual("rgba(99, 110, 250, 1)", color_str) - - color_str = get_color(1, 0.3) - self.assertEqual("rgba(239, 85, 59, 0.3)", color_str) - - self.assertRaises(IndexError, lambda: get_color(500)) - - -class TestUtil(unittest.TestCase): - def test_random(self): - # Test Length - a = get_random_string(10) - self.assertIsInstance(a, str) - self.assertEqual(10, len(a)) - - # Test random - b = get_random_string(10) - self.assertIsInstance(b, str) - self.assertEqual(10, len(b)) - self.assertNotEqual(a, b) - - # Test different length - c = get_random_string(132) - self.assertIsInstance(c, str) - self.assertEqual(132, len(c)) - - # Test Exception - self.assertRaises(ValueError, lambda: get_random_string(-1)) - - def test_matplotlib_to_html(self): - fig = plt.Figure() - ax = fig.gca() - x = [1, 2, 3, 4, 5] - y = [xx**2 for xx in x] - ax.plot(x, y) - - html_img = matplotlib_to_html_image(fig) - self.assertIsInstance(html_img, html.Img) - - @unittest.SkipTest - def test_encode_data(self): - # TODO(dwoiwode): Test with more knowledge about data structure - pass - - @unittest.SkipTest - def test_encode_data_with_cs(self): - # TODO(dwoiwode): Test with more knowledge about data structure - pass - - def test_add_prefix_to_dict(self): - a = {"a": 4, "b": 1, "c": 9} - a_prefixed = add_prefix_to_dict(a, "run:") - self.assertIn("run:a", a_prefixed) - self.assertIn("run:b", a_prefixed) - self.assertIn("run:c", a_prefixed) - self.assertNotIn("a", a_prefixed) - self.assertNotIn("b", a_prefixed) - self.assertNotIn("c", a_prefixed) - self.assertEqual(a_prefixed["run:a"], 4) - self.assertEqual(a_prefixed["run:b"], 1) - self.assertEqual(a_prefixed["run:c"], 9) - - # Other prefix, same dict - a_prefixed = add_prefix_to_dict(a, "group:") - self.assertIn("group:a", a_prefixed) - self.assertIn("group:b", a_prefixed) - self.assertIn("group:c", a_prefixed) - self.assertNotIn("a", a_prefixed) - self.assertNotIn("b", a_prefixed) - self.assertNotIn("c", a_prefixed) - self.assertEqual(a_prefixed["group:a"], 4) - self.assertEqual(a_prefixed["group:b"], 1) - self.assertEqual(a_prefixed["group:c"], 9) - - def test_add_prefix_to_list(self): - a = ["a", "b", "Hello", "World"] - a_prefixed = add_prefix_to_list(a, "run:") - self.assertIsInstance(a_prefixed, list) - self.assertEqual(4, len(a_prefixed)) - self.assertEqual("run:a", a_prefixed[0]) - self.assertEqual("run:b", a_prefixed[1]) - self.assertEqual("run:Hello", a_prefixed[2]) - - a_prefixed = add_prefix_to_list(a, "job:") - self.assertEqual("job:World", a_prefixed[3])