From 64d18bf431f2016e22a97c526a64c3f81708a0b8 Mon Sep 17 00:00:00 2001 From: Jaden <jadenpdi@gmail.com> Date: Fri, 22 Mar 2019 14:20:23 -0700 Subject: [PATCH] [plotterparameter] add plotterparameter type Adds schema file, beat.core integration, test files & tests Closes #66 --- beat/core/plotterparameter.py | 290 ++++++++++++++++++ beat/core/prototypes/plotterparameter.json | 5 + beat/core/schema/plotterparameter/1.json | 19 ++ .../plotterparameters/plot/config/1.json | 11 + .../plotterparameters/plot/invalid/1.json | 8 + .../plotterparameters/plot/invalid/2.json | 7 + beat/core/test/test_plotterparameter.py | 61 ++++ 7 files changed, 401 insertions(+) create mode 100644 beat/core/plotterparameter.py create mode 100644 beat/core/prototypes/plotterparameter.json create mode 100644 beat/core/schema/plotterparameter/1.json create mode 100644 beat/core/test/prefix/plotterparameters/plot/config/1.json create mode 100644 beat/core/test/prefix/plotterparameters/plot/invalid/1.json create mode 100644 beat/core/test/prefix/plotterparameters/plot/invalid/2.json create mode 100644 beat/core/test/test_plotterparameter.py diff --git a/beat/core/plotterparameter.py b/beat/core/plotterparameter.py new file mode 100644 index 00000000..887ff053 --- /dev/null +++ b/beat/core/plotterparameter.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +################################################################################### +# # +# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# Redistribution and use in source and binary forms, with or without # +# modification, are permitted provided that the following conditions are met: # +# # +# 1. Redistributions of source code must retain the above copyright notice, this # +# list of conditions and the following disclaimer. # +# # +# 2. Redistributions in binary form must reproduce the above copyright notice, # +# this list of conditions and the following disclaimer in the documentation # +# and/or other materials provided with the distribution. # +# # +# 3. Neither the name of the copyright holder nor the names of its contributors # +# may be used to endorse or promote products derived from this software without # +# specific prior written permission. # +# # +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # +# # +################################################################################### + + +""" +================ +plotterparameter +================ + +Validation for plotterparameters +""" + +import os + +from . import dataformat +from . import schema +from . import prototypes +from . import utils +from . import loader +from . import plotter + +class Storage(utils.Storage): + """Resolves paths for plotterparameters + + Parameters: + + prefix (str): Establishes the prefix of your installation. + + name (str): The name of the plotterparameter object in the format + ``<user>/<plotterparameter-name>/<version>`` + """ + + def __init__(self, prefix, name): + + if name.count("/") != 2: + raise RuntimeError(f"invalid plotterparameter name: {name}") + + self.username, self.name, self.version = name.split("/") + self.fullname = name + self.prefix = prefix + + path = utils.hashed_or_simple(self.prefix, "plotterparameters", name, suffix=".json") + path = path[:-5] + + super(Storage, self).__init__(path) + + +# ---------------------------------------------------------- + + +class Plotterparameter(object): + """Each plotterparameter is a specific configuration for the specified + plotter. Plotterparameters configure all the parameters of the plotter, + much like an experiment contains configurations for the + algorithms'/databases' parameters. + + Parameters: + + prefix (str): Establishes the prefix of your installation. + + data (:py:class:`object`, Optional): The piece of data representing the + plotterparameter. It must validate against the schema defined for + plotterparameters. If a string is passed, it is supposed to be a valid + path to a plotterparameter in the designated prefix area. + + plotter_cache (:py:class:`dict`, Optional): A dictionary mapping + plotter names to loaded plotters. This parameter is optional and, + if passed, may greatly speed-up algorithm loading times as plotters + that are already loaded may be re-used. + + Attributes: + + name (str): The plotterparameter name + + description (str): The short description string, loaded from the JSON + file if one was set. + + documentation (str): The full-length docstring for this object. + + storage (object): A simple object that provides information about file + paths for this plotterparameter + + plotter (object): An object of type :py:class:`.plotter.Plotter` + that represents the plotter to which this plotterparameter is applicable. + + errors (list): A list strings containing errors found while loading this + plotterparameter. + + data (dict): The original data for this plotterparameter, as loaded by our + JSON decoder. + """ + + def __init__( + self, + prefix, + data, + plotter_cache=None, + ): + self._name = None + self.storage = None + self.errors = [] + self.data = None + self.plotter = None + self.prefix = prefix + + plotter_cache = plotter_cache if plotter_cache is not None else {} + self._load(data, plotter_cache) + + def _load(self, data, plotter_cache): + """Loads the plotterparameter""" + + self._load_data(data) + + if self.errors: + return # don't proceed with the rest of validation + + self._load_plotter(plotter_cache) + + if self.errors: + return # don't proceed with the rest of validation + + self._validate_data() + + def _load_data(self, data): + """Loads given plotterparameter data + and the plotterparameter's name + + Parameters: + + data (str): a string (the name of the param), + an object (the param data), + or a tuple/list (the param data & the plotter data) + """ + # first load the raw plotterparameter data, if data isnt None + if isinstance(data, (tuple, list)): # the user has passed a tuple + data, self.plotter = data + elif isinstance(data, str): # user has passed the name + self._name = data + self.storage = Storage(self.prefix, self._name) + if not self.storage.json.exists(): + self.errors.append(f'Plotterparameter declaration file not found: {data}') + return + data = self.storage.json.path # loads data from JSON declaration + + # At this point, `data' can be a dictionary or ``None`` + # Either way, assign something valid to `self.data' + if data is None: # use the dummy plotterparameter + self.data, self.errors = prototypes.load("plotterparameter") + assert not self.errors, "\n * %s" % "\n *".join(self.errors) + else: + # this runs basic validation, including JSON loading if required + self.data, self.errors = schema.validate("plotterparameter", data) + + + def _load_plotter(self, plotter_cache): + """Loads the plotter for the plotterparameter. + Assumes that `self.data' has been calculated. + + Parameters: + + plotter_cache (:py:class:`dict`): a dict mapping plotter names + to already-loaded plotter objects + """ + # find the plotter if it wasnt given + if self.plotter is None: + plotter_name = self.data['plotter'] + + pl = None + if plotter_name in plotter_cache: + pl = plotter_cache[plotter_name] + else: + pl = plotter.Plotter(self.prefix, plotter_name) + + if pl.errors: + self.errors.extend(pl.errors) + return + + plotter_cache[plotter_name] = pl + self.plotter = pl + + + def _validate_data(self): + """Validates that the properties in the plotterparameter's + data properly configure the plotter's fields + """ + for key, val in self.data['data'].items(): + try: + self.plotter.clean_parameter(key, val) + except KeyError: + self.errors.append(f"'{key}' isn't a parameter for plotter {self.plotter.name}") + return + except ValueError: + self.errors.append(f"'{value}' is invalid for parameter {key} of plotter {self.plotter.name}") + return + + + @property + def valid(self): + """A boolean that indicates if this plotterparameter is valid or not""" + return not bool(self.errors) + + @property + def name(self): + """Returns the name of this object""" + return self._name or "__unnamed_plotterparameter__" + + @name.setter + def name(self, value): + self._name = value + self.storage = Storage(self.prefix, value) + + @property + def documentation(self): + """The full-length description for this object""" + + if not self._name: + raise RuntimeError("plotterparameter has no name") + + if self.storage.doc.exists(): + return self.storage.doc.load() + return None + + @documentation.setter + def documentation(self, value): + """Sets the full-length description for this object""" + + if not self._name: + raise RuntimeError("plotterparameter has no name") + + if callable(getattr(value, "read", None)): + self.storage.doc.save(value.read()) + else: + self.storage.doc.save(value) + + def hash(self): + """Returns the hexadecimal hash for the current plotterparameter""" + + if not self._name: + raise RuntimeError("plotterparameter has no name") + + return self.storage.hash() + + def write(self, storage=None): + """Writes contents to prefix location + + Parameters: + + storage (:py:class:`.Storage`, Optional): If you pass a new storage, + then this object will be written to that storage point rather than + its default. + """ + + if storage is None: + if not self._name: + raise RuntimeError("plotterparameter has no name") + storage = self.storage # overwrite + + storage.save(str(self), self.code, self.description) diff --git a/beat/core/prototypes/plotterparameter.json b/beat/core/prototypes/plotterparameter.json new file mode 100644 index 00000000..a2b3ff5a --- /dev/null +++ b/beat/core/prototypes/plotterparameter.json @@ -0,0 +1,5 @@ +{ + "plotter": "plot/unknown/1", + "description": "", + "data": {} +} diff --git a/beat/core/schema/plotterparameter/1.json b/beat/core/schema/plotterparameter/1.json new file mode 100644 index 00000000..f99facb4 --- /dev/null +++ b/beat/core/schema/plotterparameter/1.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Plotter configurator descriptor", + "description": "This schema defines the properties of a plotter configuration file", + + "type": "object", + + "properties": { + "description": { "$ref": "../common/1.json#/definitions/description" }, + "plotter": {"$ref": "../common/1.json#/definitions/reference"}, + "data": {"$ref": "../experiment/common.json#/definitions/parameter_set"}, + "schema_version": { "$ref": "../common/1.json#/definitions/version" } + }, + "required": [ + "plotter", + "data" + ], + "additionalProperties": false +} diff --git a/beat/core/test/prefix/plotterparameters/plot/config/1.json b/beat/core/test/prefix/plotterparameters/plot/config/1.json new file mode 100644 index 00000000..c349d357 --- /dev/null +++ b/beat/core/test/prefix/plotterparameters/plot/config/1.json @@ -0,0 +1,11 @@ +{ + "plotter": "user/scatter/1", + "description": "A plotterparameter for the built-in scatter plotter", + "data": + { + "grid": true, + "legend": "test data", + "mimetype": "image/jpeg", + "yaxis_multiplier": 2.5 + } +} diff --git a/beat/core/test/prefix/plotterparameters/plot/invalid/1.json b/beat/core/test/prefix/plotterparameters/plot/invalid/1.json new file mode 100644 index 00000000..ffc03428 --- /dev/null +++ b/beat/core/test/prefix/plotterparameters/plot/invalid/1.json @@ -0,0 +1,8 @@ +{ + "plotter": "user/scatter/1", + "description": "An invalid plotterparameter for the scatter plotter", + "data": + { + "not_an_option": 2.5 + } +} diff --git a/beat/core/test/prefix/plotterparameters/plot/invalid/2.json b/beat/core/test/prefix/plotterparameters/plot/invalid/2.json new file mode 100644 index 00000000..1fb52304 --- /dev/null +++ b/beat/core/test/prefix/plotterparameters/plot/invalid/2.json @@ -0,0 +1,7 @@ +{ + "plotter": "user/not_a_plotter/1", + "description": "A plotterparameter for a non-existant plotter", + "data": + { + } +} diff --git a/beat/core/test/test_plotterparameter.py b/beat/core/test/test_plotterparameter.py new file mode 100644 index 00000000..4e67deec --- /dev/null +++ b/beat/core/test/test_plotterparameter.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 : + +################################################################################### +# # +# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/ # +# Contact: beat.support@idiap.ch # +# # +# Redistribution and use in source and binary forms, with or without # +# modification, are permitted provided that the following conditions are met: # +# # +# 1. Redistributions of source code must retain the above copyright notice, this # +# list of conditions and the following disclaimer. # +# # +# 2. Redistributions in binary form must reproduce the above copyright notice, # +# this list of conditions and the following disclaimer in the documentation # +# and/or other materials provided with the distribution. # +# # +# 3. Neither the name of the copyright holder nor the names of its contributors # +# may be used to endorse or promote products derived from this software without # +# specific prior written permission. # +# # +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE # +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # +# # +################################################################################### + +import nose.tools + +from . import prefix +from ..plotterparameter import Plotterparameter + +def test_default(): + # test for the "dummy" plotterparameter + p = Plotterparameter(prefix, data=None) + nose.tools.assert_false(p.valid) + +def test_plot_config_1(): + # test for a simple plotterparameter for a simple plotter + p = Plotterparameter(prefix, "plot/config/1") + nose.tools.assert_true(p.valid, "\n * %s" % "\n * ".join(p.errors)) + +def test_plot_invalid_1(): + # test for invalid parameter name + p = Plotterparameter(prefix, "plot/invalid/1") + nose.tools.assert_false(p.valid) + nose.tools.assert_true(p.errors[0] == "'not_an_option' isn't a parameter for plotter user/scatter/1") + +def test_plot_invalid_2(): + # test for invalid "plotter" field + p = Plotterparameter(prefix, "plot/invalid/2") + nose.tools.assert_false(p.valid) + nose.tools.assert_true(p.errors[0] == "Plotter declaration file not found: user/not_a_plotter/1") -- GitLab