| | from __future__ import absolute_import |
| |
|
| | import base64 |
| | import numbers |
| | import textwrap |
| | import uuid |
| | from importlib import import_module |
| | import copy |
| | import io |
| | import re |
| | import sys |
| |
|
| | from _plotly_utils.optional_imports import get_module |
| |
|
| |
|
| | |
| | def fullmatch(regex, string, flags=0): |
| | """Emulate python-3.4 re.fullmatch().""" |
| | if "pattern" in dir(regex): |
| | regex_string = regex.pattern |
| | else: |
| | regex_string = regex |
| | return re.match("(?:" + regex_string + r")\Z", string, flags=flags) |
| |
|
| |
|
| | |
| | |
| | def to_scalar_or_list(v): |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | np = get_module("numpy", should_load=False) |
| | pd = get_module("pandas", should_load=False) |
| | if np and np.isscalar(v) and hasattr(v, "item"): |
| | return v.item() |
| | if isinstance(v, (list, tuple)): |
| | return [to_scalar_or_list(e) for e in v] |
| | elif np and isinstance(v, np.ndarray): |
| | if v.ndim == 0: |
| | return v.item() |
| | return [to_scalar_or_list(e) for e in v] |
| | elif pd and isinstance(v, (pd.Series, pd.Index)): |
| | return [to_scalar_or_list(e) for e in v] |
| | elif is_numpy_convertable(v): |
| | return to_scalar_or_list(np.array(v)) |
| | else: |
| | return v |
| |
|
| |
|
| | def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False): |
| | """ |
| | Convert an array-like value into a read-only numpy array |
| | |
| | Parameters |
| | ---------- |
| | v : array like |
| | Array like value (list, tuple, numpy array, pandas series, etc.) |
| | kind : str or tuple of str |
| | If specified, the numpy dtype kind (or kinds) that the array should |
| | have, or be converted to if possible. |
| | If not specified then let numpy infer the datatype |
| | force_numeric : bool |
| | If true, raise an exception if the resulting numpy array does not |
| | have a numeric dtype (i.e. dtype.kind not in ['u', 'i', 'f']) |
| | Returns |
| | ------- |
| | np.ndarray |
| | Numpy array with the 'WRITEABLE' flag set to False |
| | """ |
| | np = get_module("numpy") |
| |
|
| | |
| | pd = get_module("pandas", should_load=False) |
| | assert np is not None |
| |
|
| | |
| | if not kind: |
| | kind = () |
| | elif isinstance(kind, str): |
| | kind = (kind,) |
| |
|
| | first_kind = kind[0] if kind else None |
| |
|
| | |
| | numeric_kinds = {"u", "i", "f"} |
| | kind_default_dtypes = { |
| | "u": "uint32", |
| | "i": "int32", |
| | "f": "float64", |
| | "O": "object", |
| | } |
| |
|
| | |
| | if pd and isinstance(v, (pd.Series, pd.Index)): |
| | if v.dtype.kind in numeric_kinds: |
| | |
| | v = v.values |
| | elif v.dtype.kind == "M": |
| | |
| | if isinstance(v, pd.Series): |
| | v = v.dt.to_pydatetime() |
| | else: |
| | |
| | v = v.to_pydatetime() |
| | elif pd and isinstance(v, pd.DataFrame) and len(set(v.dtypes)) == 1: |
| | dtype = v.dtypes.tolist()[0] |
| | if dtype.kind in numeric_kinds: |
| | v = v.values |
| | elif dtype.kind == "M": |
| | v = [row.dt.to_pydatetime().tolist() for i, row in v.iterrows()] |
| |
|
| | if not isinstance(v, np.ndarray): |
| | |
| | if is_numpy_convertable(v): |
| | return copy_to_readonly_numpy_array( |
| | np.array(v), kind=kind, force_numeric=force_numeric |
| | ) |
| | else: |
| | |
| | v_list = [to_scalar_or_list(e) for e in v] |
| |
|
| | |
| | dtype = kind_default_dtypes.get(first_kind, None) |
| |
|
| | |
| | new_v = np.array(v_list, order="C", dtype=dtype) |
| | elif v.dtype.kind in numeric_kinds: |
| | |
| | if kind and v.dtype.kind not in kind: |
| | |
| | |
| | dtype = kind_default_dtypes.get(first_kind, None) |
| | new_v = np.ascontiguousarray(v.astype(dtype)) |
| | else: |
| | |
| | new_v = np.ascontiguousarray(v.copy()) |
| | else: |
| | |
| | new_v = v.copy() |
| |
|
| | |
| | |
| | if force_numeric and new_v.dtype.kind not in numeric_kinds: |
| | raise ValueError( |
| | "Input value is not numeric and" "force_numeric parameter set to True" |
| | ) |
| |
|
| | if "U" not in kind: |
| | |
| | |
| | |
| | |
| | |
| | |
| | if new_v.dtype.kind not in ["u", "i", "f", "O", "M"]: |
| | new_v = np.array(v, dtype="object") |
| |
|
| | |
| | |
| | new_v.flags["WRITEABLE"] = False |
| |
|
| | return new_v |
| |
|
| |
|
| | def is_numpy_convertable(v): |
| | """ |
| | Return whether a value is meaningfully convertable to a numpy array |
| | via 'numpy.array' |
| | """ |
| | return hasattr(v, "__array__") or hasattr(v, "__array_interface__") |
| |
|
| |
|
| | def is_homogeneous_array(v): |
| | """ |
| | Return whether a value is considered to be a homogeneous array |
| | """ |
| | np = get_module("numpy", should_load=False) |
| | pd = get_module("pandas", should_load=False) |
| | if ( |
| | np |
| | and isinstance(v, np.ndarray) |
| | or (pd and isinstance(v, (pd.Series, pd.Index))) |
| | ): |
| | return True |
| | if is_numpy_convertable(v): |
| | np = get_module("numpy", should_load=True) |
| | if np: |
| | v_numpy = np.array(v) |
| | |
| | if v_numpy.shape == (): |
| | return False |
| | else: |
| | return True |
| | return False |
| |
|
| |
|
| | def is_simple_array(v): |
| | """ |
| | Return whether a value is considered to be an simple array |
| | """ |
| | return isinstance(v, (list, tuple)) |
| |
|
| |
|
| | def is_array(v): |
| | """ |
| | Return whether a value is considered to be an array |
| | """ |
| | return is_simple_array(v) or is_homogeneous_array(v) |
| |
|
| |
|
| | def type_str(v): |
| | """ |
| | Return a type string of the form module.name for the input value v |
| | """ |
| | if not isinstance(v, type): |
| | v = type(v) |
| |
|
| | return "'{module}.{name}'".format(module=v.__module__, name=v.__name__) |
| |
|
| |
|
| | |
| | |
| | class BaseValidator(object): |
| | """ |
| | Base class for all validator classes |
| | """ |
| |
|
| | def __init__(self, plotly_name, parent_name, role=None, **_): |
| | """ |
| | Construct a validator instance |
| | |
| | Parameters |
| | ---------- |
| | plotly_name : str |
| | Name of the property being validated |
| | parent_name : str |
| | Names of all of the ancestors of this property joined on '.' |
| | characters. e.g. |
| | plotly_name == 'range' and parent_name == 'layout.xaxis' |
| | role : str |
| | The role string for the property as specified in |
| | plot-schema.json |
| | """ |
| | self.parent_name = parent_name |
| | self.plotly_name = plotly_name |
| | self.role = role |
| | self.array_ok = False |
| |
|
| | def description(self): |
| | """ |
| | Returns a string that describes the values that are acceptable |
| | to the validator |
| | |
| | Should start with: |
| | The '{plotly_name}' property is a... |
| | |
| | For consistancy, string should have leading 4-space indent |
| | """ |
| | raise NotImplementedError() |
| |
|
| | def raise_invalid_val(self, v, inds=None): |
| | """ |
| | Helper method to raise an informative exception when an invalid |
| | value is passed to the validate_coerce method. |
| | |
| | Parameters |
| | ---------- |
| | v : |
| | Value that was input to validate_coerce and could not be coerced |
| | inds: list of int or None (default) |
| | Indexes to display after property name. e.g. if self.plotly_name |
| | is 'prop' and inds=[2, 1] then the name in the validation error |
| | message will be 'prop[2][1]` |
| | Raises |
| | ------- |
| | ValueError |
| | """ |
| | name = self.plotly_name |
| | if inds: |
| | for i in inds: |
| | name += "[" + str(i) + "]" |
| |
|
| | raise ValueError( |
| | """ |
| | Invalid value of type {typ} received for the '{name}' property of {pname} |
| | Received value: {v} |
| | |
| | {valid_clr_desc}""".format( |
| | name=name, |
| | pname=self.parent_name, |
| | typ=type_str(v), |
| | v=repr(v), |
| | valid_clr_desc=self.description(), |
| | ) |
| | ) |
| |
|
| | def raise_invalid_elements(self, invalid_els): |
| | if invalid_els: |
| | raise ValueError( |
| | """ |
| | Invalid element(s) received for the '{name}' property of {pname} |
| | Invalid elements include: {invalid} |
| | |
| | {valid_clr_desc}""".format( |
| | name=self.plotly_name, |
| | pname=self.parent_name, |
| | invalid=invalid_els[:10], |
| | valid_clr_desc=self.description(), |
| | ) |
| | ) |
| |
|
| | def validate_coerce(self, v): |
| | """ |
| | Validate whether an input value is compatible with this property, |
| | and coerce the value to be compatible of possible. |
| | |
| | Parameters |
| | ---------- |
| | v |
| | The input value to be validated |
| | |
| | Raises |
| | ------ |
| | ValueError |
| | if `v` cannot be coerced into a compatible form |
| | |
| | Returns |
| | ------- |
| | The input `v` in a form that's compatible with this property |
| | """ |
| | raise NotImplementedError() |
| |
|
| | def present(self, v): |
| | """ |
| | Convert output value of a previous call to `validate_coerce` into a |
| | form suitable to be returned to the user on upon property |
| | access. |
| | |
| | Note: The value returned by present must be either immutable or an |
| | instance of BasePlotlyType, otherwise the value could be mutated by |
| | the user and we wouldn't get notified about the change. |
| | |
| | Parameters |
| | ---------- |
| | v |
| | A value that was the ouput of a previous call the |
| | `validate_coerce` method on the same object |
| | |
| | Returns |
| | ------- |
| | |
| | """ |
| | if is_homogeneous_array(v): |
| | |
| | |
| | return v |
| | elif is_simple_array(v): |
| | return tuple(v) |
| | else: |
| | return v |
| |
|
| |
|
| | class DataArrayValidator(BaseValidator): |
| | """ |
| | "data_array": { |
| | "description": "An {array} of data. The value MUST be an |
| | {array}, or we ignore it.", |
| | "requiredOpts": [], |
| | "otherOpts": [ |
| | "dflt" |
| | ] |
| | }, |
| | """ |
| |
|
| | def __init__(self, plotly_name, parent_name, **kwargs): |
| | super(DataArrayValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | self.array_ok = True |
| |
|
| | def description(self): |
| | return """\ |
| | The '{plotly_name}' property is an array that may be specified as a tuple, |
| | list, numpy array, or pandas Series""".format( |
| | plotly_name=self.plotly_name |
| | ) |
| |
|
| | def validate_coerce(self, v): |
| |
|
| | if v is None: |
| | |
| | pass |
| | elif is_homogeneous_array(v): |
| | v = copy_to_readonly_numpy_array(v) |
| | elif is_simple_array(v): |
| | v = to_scalar_or_list(v) |
| | else: |
| | self.raise_invalid_val(v) |
| | return v |
| |
|
| |
|
| | class EnumeratedValidator(BaseValidator): |
| | """ |
| | "enumerated": { |
| | "description": "Enumerated value type. The available values are |
| | listed in `values`.", |
| | "requiredOpts": [ |
| | "values" |
| | ], |
| | "otherOpts": [ |
| | "dflt", |
| | "coerceNumber", |
| | "arrayOk" |
| | ] |
| | }, |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | plotly_name, |
| | parent_name, |
| | values, |
| | array_ok=False, |
| | coerce_number=False, |
| | **kwargs, |
| | ): |
| | super(EnumeratedValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | |
| | |
| | self.values = values |
| | self.array_ok = array_ok |
| | |
| | self.coerce_number = coerce_number |
| | self.kwargs = kwargs |
| |
|
| | |
| | |
| | |
| | self.val_regexs = [] |
| |
|
| | |
| | |
| | |
| | self.regex_replacements = [] |
| |
|
| | |
| | |
| | |
| | for v in self.values: |
| | if v and isinstance(v, str) and v[0] == "/" and v[-1] == "/" and len(v) > 1: |
| | |
| | regex_str = v[1:-1] |
| | self.val_regexs.append(re.compile(regex_str)) |
| | self.regex_replacements.append( |
| | EnumeratedValidator.build_regex_replacement(regex_str) |
| | ) |
| | else: |
| | self.val_regexs.append(None) |
| | self.regex_replacements.append(None) |
| |
|
| | def __deepcopy__(self, memodict={}): |
| | """ |
| | A custom deepcopy method is needed here because compiled regex |
| | objects don't support deepcopy |
| | """ |
| | cls = self.__class__ |
| | return cls(self.plotly_name, self.parent_name, values=self.values) |
| |
|
| | @staticmethod |
| | def build_regex_replacement(regex_str): |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | match = re.match( |
| | r"\^(\w)\(\[2\-9\]\|\[1\-9\]\[0\-9\]\+\)\?\( domain\)\?\$", regex_str |
| | ) |
| |
|
| | if match: |
| | anchor_char = match.group(1) |
| | return "^" + anchor_char + "1$", anchor_char |
| | else: |
| | return None |
| |
|
| | def perform_replacemenet(self, v): |
| | """ |
| | Return v with any applicable regex replacements applied |
| | """ |
| | if isinstance(v, str): |
| | for repl_args in self.regex_replacements: |
| | if repl_args: |
| | v = re.sub(repl_args[0], repl_args[1], v) |
| |
|
| | return v |
| |
|
| | def description(self): |
| |
|
| | |
| | enum_vals = [] |
| | enum_regexs = [] |
| | for v, regex in zip(self.values, self.val_regexs): |
| | if regex is not None: |
| | enum_regexs.append(regex.pattern) |
| | else: |
| | enum_vals.append(v) |
| | desc = """\ |
| | The '{name}' property is an enumeration that may be specified as:""".format( |
| | name=self.plotly_name |
| | ) |
| |
|
| | if enum_vals: |
| | enum_vals_str = "\n".join( |
| | textwrap.wrap( |
| | repr(enum_vals), |
| | initial_indent=" " * 12, |
| | subsequent_indent=" " * 12, |
| | break_on_hyphens=False, |
| | ) |
| | ) |
| |
|
| | desc = ( |
| | desc |
| | + """ |
| | - One of the following enumeration values: |
| | {enum_vals_str}""".format( |
| | enum_vals_str=enum_vals_str |
| | ) |
| | ) |
| |
|
| | if enum_regexs: |
| | enum_regexs_str = "\n".join( |
| | textwrap.wrap( |
| | repr(enum_regexs), |
| | initial_indent=" " * 12, |
| | subsequent_indent=" " * 12, |
| | break_on_hyphens=False, |
| | ) |
| | ) |
| |
|
| | desc = ( |
| | desc |
| | + """ |
| | - A string that matches one of the following regular expressions: |
| | {enum_regexs_str}""".format( |
| | enum_regexs_str=enum_regexs_str |
| | ) |
| | ) |
| |
|
| | if self.array_ok: |
| | desc = ( |
| | desc |
| | + """ |
| | - A tuple, list, or one-dimensional numpy array of the above""" |
| | ) |
| |
|
| | return desc |
| |
|
| | def in_values(self, e): |
| | """ |
| | Return whether a value matches one of the enumeration options |
| | """ |
| | is_str = isinstance(e, str) |
| | for v, regex in zip(self.values, self.val_regexs): |
| | if is_str and regex: |
| | in_values = fullmatch(regex, e) is not None |
| | |
| | else: |
| | in_values = e == v |
| |
|
| | if in_values: |
| | return True |
| |
|
| | return False |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | |
| | pass |
| | elif self.array_ok and is_array(v): |
| | v_replaced = [self.perform_replacemenet(v_el) for v_el in v] |
| |
|
| | invalid_els = [e for e in v_replaced if (not self.in_values(e))] |
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els[:10]) |
| |
|
| | if is_homogeneous_array(v): |
| | v = copy_to_readonly_numpy_array(v) |
| | else: |
| | v = to_scalar_or_list(v) |
| | else: |
| | v = self.perform_replacemenet(v) |
| | if not self.in_values(v): |
| | self.raise_invalid_val(v) |
| | return v |
| |
|
| |
|
| | class BooleanValidator(BaseValidator): |
| | """ |
| | "boolean": { |
| | "description": "A boolean (true/false) value.", |
| | "requiredOpts": [], |
| | "otherOpts": [ |
| | "dflt" |
| | ] |
| | }, |
| | """ |
| |
|
| | def __init__(self, plotly_name, parent_name, **kwargs): |
| | super(BooleanValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | def description(self): |
| | return """\ |
| | The '{plotly_name}' property must be specified as a bool |
| | (either True, or False)""".format( |
| | plotly_name=self.plotly_name |
| | ) |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | |
| | pass |
| | elif not isinstance(v, bool): |
| | self.raise_invalid_val(v) |
| |
|
| | return v |
| |
|
| |
|
| | class SrcValidator(BaseValidator): |
| | def __init__(self, plotly_name, parent_name, **kwargs): |
| | super(SrcValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | self.chart_studio = get_module("chart_studio") |
| |
|
| | def description(self): |
| | return """\ |
| | The '{plotly_name}' property must be specified as a string or |
| | as a plotly.grid_objs.Column object""".format( |
| | plotly_name=self.plotly_name |
| | ) |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | |
| | pass |
| | elif isinstance(v, str): |
| | pass |
| | elif self.chart_studio and isinstance(v, self.chart_studio.grid_objs.Column): |
| | |
| | v = v.id |
| | else: |
| | self.raise_invalid_val(v) |
| |
|
| | return v |
| |
|
| |
|
| | class NumberValidator(BaseValidator): |
| | """ |
| | "number": { |
| | "description": "A number or a numeric value (e.g. a number |
| | inside a string). When applicable, values |
| | greater (less) than `max` (`min`) are coerced to |
| | the `dflt`.", |
| | "requiredOpts": [], |
| | "otherOpts": [ |
| | "dflt", |
| | "min", |
| | "max", |
| | "arrayOk" |
| | ] |
| | }, |
| | """ |
| |
|
| | def __init__( |
| | self, plotly_name, parent_name, min=None, max=None, array_ok=False, **kwargs |
| | ): |
| | super(NumberValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | |
| | if min is None and max is not None: |
| | |
| | self.min_val = float("-inf") |
| | else: |
| | self.min_val = min |
| |
|
| | |
| | if max is None and min is not None: |
| | |
| | self.max_val = float("inf") |
| | else: |
| | self.max_val = max |
| |
|
| | if min is not None or max is not None: |
| | self.has_min_max = True |
| | else: |
| | self.has_min_max = False |
| |
|
| | self.array_ok = array_ok |
| |
|
| | def description(self): |
| | desc = """\ |
| | The '{plotly_name}' property is a number and may be specified as:""".format( |
| | plotly_name=self.plotly_name |
| | ) |
| |
|
| | if not self.has_min_max: |
| | desc = ( |
| | desc |
| | + """ |
| | - An int or float""" |
| | ) |
| |
|
| | else: |
| | desc = ( |
| | desc |
| | + """ |
| | - An int or float in the interval [{min_val}, {max_val}]""".format( |
| | min_val=self.min_val, max_val=self.max_val |
| | ) |
| | ) |
| |
|
| | if self.array_ok: |
| | desc = ( |
| | desc |
| | + """ |
| | - A tuple, list, or one-dimensional numpy array of the above""" |
| | ) |
| |
|
| | return desc |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | |
| | pass |
| | elif self.array_ok and is_homogeneous_array(v): |
| | np = get_module("numpy") |
| | try: |
| | v_array = copy_to_readonly_numpy_array(v, force_numeric=True) |
| | except (ValueError, TypeError, OverflowError): |
| | self.raise_invalid_val(v) |
| |
|
| | |
| | if self.has_min_max: |
| | v_valid = np.logical_and( |
| | self.min_val <= v_array, v_array <= self.max_val |
| | ) |
| |
|
| | if not np.all(v_valid): |
| | |
| | v_invalid = np.logical_not(v_valid) |
| | some_invalid_els = np.array(v, dtype="object")[v_invalid][ |
| | :10 |
| | ].tolist() |
| |
|
| | self.raise_invalid_elements(some_invalid_els) |
| |
|
| | v = v_array |
| | elif self.array_ok and is_simple_array(v): |
| | |
| | invalid_els = [e for e in v if not isinstance(e, numbers.Number)] |
| |
|
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els[:10]) |
| |
|
| | |
| | if self.has_min_max: |
| | invalid_els = [e for e in v if not (self.min_val <= e <= self.max_val)] |
| |
|
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els[:10]) |
| |
|
| | v = to_scalar_or_list(v) |
| | else: |
| | |
| | if not isinstance(v, numbers.Number): |
| | self.raise_invalid_val(v) |
| |
|
| | |
| | if self.has_min_max: |
| | if not (self.min_val <= v <= self.max_val): |
| | self.raise_invalid_val(v) |
| | return v |
| |
|
| |
|
| | class IntegerValidator(BaseValidator): |
| | """ |
| | "integer": { |
| | "description": "An integer or an integer inside a string. When |
| | applicable, values greater (less) than `max` |
| | (`min`) are coerced to the `dflt`.", |
| | "requiredOpts": [], |
| | "otherOpts": [ |
| | "dflt", |
| | "min", |
| | "max", |
| | "arrayOk" |
| | ] |
| | }, |
| | """ |
| |
|
| | def __init__( |
| | self, plotly_name, parent_name, min=None, max=None, array_ok=False, **kwargs |
| | ): |
| | super(IntegerValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | |
| | if min is None and max is not None: |
| | |
| | self.min_val = -sys.maxsize - 1 |
| | else: |
| | self.min_val = min |
| |
|
| | |
| | if max is None and min is not None: |
| | |
| | self.max_val = sys.maxsize |
| | else: |
| | self.max_val = max |
| |
|
| | if min is not None or max is not None: |
| | self.has_min_max = True |
| | else: |
| | self.has_min_max = False |
| |
|
| | self.array_ok = array_ok |
| |
|
| | def description(self): |
| | desc = """\ |
| | The '{plotly_name}' property is a integer and may be specified as:""".format( |
| | plotly_name=self.plotly_name |
| | ) |
| |
|
| | if not self.has_min_max: |
| | desc = ( |
| | desc |
| | + """ |
| | - An int (or float that will be cast to an int)""" |
| | ) |
| | else: |
| | desc = desc + ( |
| | """ |
| | - An int (or float that will be cast to an int) |
| | in the interval [{min_val}, {max_val}]""".format( |
| | min_val=self.min_val, max_val=self.max_val |
| | ) |
| | ) |
| |
|
| | if self.array_ok: |
| | desc = ( |
| | desc |
| | + """ |
| | - A tuple, list, or one-dimensional numpy array of the above""" |
| | ) |
| |
|
| | return desc |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | |
| | pass |
| | elif self.array_ok and is_homogeneous_array(v): |
| | np = get_module("numpy") |
| | v_array = copy_to_readonly_numpy_array( |
| | v, kind=("i", "u"), force_numeric=True |
| | ) |
| |
|
| | if v_array.dtype.kind not in ["i", "u"]: |
| | self.raise_invalid_val(v) |
| |
|
| | |
| | if self.has_min_max: |
| | v_valid = np.logical_and( |
| | self.min_val <= v_array, v_array <= self.max_val |
| | ) |
| |
|
| | if not np.all(v_valid): |
| | |
| | v_invalid = np.logical_not(v_valid) |
| | some_invalid_els = np.array(v, dtype="object")[v_invalid][ |
| | :10 |
| | ].tolist() |
| | self.raise_invalid_elements(some_invalid_els) |
| |
|
| | v = v_array |
| | elif self.array_ok and is_simple_array(v): |
| | |
| | invalid_els = [e for e in v if not isinstance(e, int)] |
| |
|
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els[:10]) |
| |
|
| | |
| | if self.has_min_max: |
| | invalid_els = [e for e in v if not (self.min_val <= e <= self.max_val)] |
| |
|
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els[:10]) |
| |
|
| | v = to_scalar_or_list(v) |
| | else: |
| | |
| | if not isinstance(v, int): |
| | |
| | self.raise_invalid_val(v) |
| |
|
| | |
| | if self.has_min_max: |
| | if not (self.min_val <= v <= self.max_val): |
| | self.raise_invalid_val(v) |
| |
|
| | return v |
| |
|
| |
|
| | class StringValidator(BaseValidator): |
| | """ |
| | "string": { |
| | "description": "A string value. Numbers are converted to strings |
| | except for attributes with `strict` set to true.", |
| | "requiredOpts": [], |
| | "otherOpts": [ |
| | "dflt", |
| | "noBlank", |
| | "strict", |
| | "arrayOk", |
| | "values" |
| | ] |
| | }, |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | plotly_name, |
| | parent_name, |
| | no_blank=False, |
| | strict=False, |
| | array_ok=False, |
| | values=None, |
| | **kwargs, |
| | ): |
| | super(StringValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| | self.no_blank = no_blank |
| | self.strict = strict |
| | self.array_ok = array_ok |
| | self.values = values |
| |
|
| | @staticmethod |
| | def to_str_or_unicode_or_none(v): |
| | """ |
| | Convert a value to a string if it's not None, a string, |
| | or a unicode (on Python 2). |
| | """ |
| | if v is None or isinstance(v, str): |
| | return v |
| | else: |
| | return str(v) |
| |
|
| | def description(self): |
| | desc = """\ |
| | The '{plotly_name}' property is a string and must be specified as:""".format( |
| | plotly_name=self.plotly_name |
| | ) |
| |
|
| | if self.no_blank: |
| | desc = ( |
| | desc |
| | + """ |
| | - A non-empty string""" |
| | ) |
| | elif self.values: |
| | valid_str = "\n".join( |
| | textwrap.wrap( |
| | repr(self.values), |
| | initial_indent=" " * 12, |
| | subsequent_indent=" " * 12, |
| | break_on_hyphens=False, |
| | ) |
| | ) |
| |
|
| | desc = ( |
| | desc |
| | + """ |
| | - One of the following strings: |
| | {valid_str}""".format( |
| | valid_str=valid_str |
| | ) |
| | ) |
| | else: |
| | desc = ( |
| | desc |
| | + """ |
| | - A string""" |
| | ) |
| |
|
| | if not self.strict: |
| | desc = ( |
| | desc |
| | + """ |
| | - A number that will be converted to a string""" |
| | ) |
| |
|
| | if self.array_ok: |
| | desc = ( |
| | desc |
| | + """ |
| | - A tuple, list, or one-dimensional numpy array of the above""" |
| | ) |
| |
|
| | return desc |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | |
| | pass |
| | elif self.array_ok and is_array(v): |
| |
|
| | |
| | if self.strict: |
| | invalid_els = [e for e in v if not isinstance(e, str)] |
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els) |
| |
|
| | if is_homogeneous_array(v): |
| | np = get_module("numpy") |
| |
|
| | |
| | v = copy_to_readonly_numpy_array(v, kind="U") |
| |
|
| | |
| | if self.no_blank: |
| | invalid_els = v[v == ""][:10].tolist() |
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els) |
| |
|
| | |
| | if self.values: |
| | invalid_inds = np.logical_not(np.isin(v, self.values)) |
| | invalid_els = v[invalid_inds][:10].tolist() |
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els) |
| | elif is_simple_array(v): |
| | if not self.strict: |
| | v = [StringValidator.to_str_or_unicode_or_none(e) for e in v] |
| |
|
| | |
| | if self.no_blank: |
| | invalid_els = [e for e in v if e == ""] |
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els) |
| |
|
| | |
| | if self.values: |
| | invalid_els = [e for e in v if v not in self.values] |
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els) |
| |
|
| | v = to_scalar_or_list(v) |
| |
|
| | else: |
| | if self.strict: |
| | if not isinstance(v, str): |
| | self.raise_invalid_val(v) |
| | else: |
| | if isinstance(v, str): |
| | pass |
| | elif isinstance(v, (int, float)): |
| | |
| | v = str(v) |
| | else: |
| | self.raise_invalid_val(v) |
| |
|
| | if self.no_blank and len(v) == 0: |
| | self.raise_invalid_val(v) |
| |
|
| | if self.values and v not in self.values: |
| | self.raise_invalid_val(v) |
| |
|
| | return v |
| |
|
| |
|
| | class ColorValidator(BaseValidator): |
| | """ |
| | "color": { |
| | "description": "A string describing color. Supported formats: |
| | - hex (e.g. '#d3d3d3') |
| | - rgb (e.g. 'rgb(255, 0, 0)') |
| | - rgba (e.g. 'rgb(255, 0, 0, 0.5)') |
| | - hsl (e.g. 'hsl(0, 100%, 50%)') |
| | - hsv (e.g. 'hsv(0, 100%, 100%)') |
| | - named colors(full list: |
| | http://www.w3.org/TR/css3-color/#svg-color)", |
| | "requiredOpts": [], |
| | "otherOpts": [ |
| | "dflt", |
| | "arrayOk" |
| | ] |
| | }, |
| | """ |
| |
|
| | re_hex = re.compile(r"#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})") |
| | re_rgb_etc = re.compile(r"(rgb|hsl|hsv)a?\([\d.]+%?(,[\d.]+%?){2,3}\)") |
| | re_ddk = re.compile(r"var\(\-\-.*\)") |
| |
|
| | named_colors = [ |
| | "aliceblue", |
| | "antiquewhite", |
| | "aqua", |
| | "aquamarine", |
| | "azure", |
| | "beige", |
| | "bisque", |
| | "black", |
| | "blanchedalmond", |
| | "blue", |
| | "blueviolet", |
| | "brown", |
| | "burlywood", |
| | "cadetblue", |
| | "chartreuse", |
| | "chocolate", |
| | "coral", |
| | "cornflowerblue", |
| | "cornsilk", |
| | "crimson", |
| | "cyan", |
| | "darkblue", |
| | "darkcyan", |
| | "darkgoldenrod", |
| | "darkgray", |
| | "darkgrey", |
| | "darkgreen", |
| | "darkkhaki", |
| | "darkmagenta", |
| | "darkolivegreen", |
| | "darkorange", |
| | "darkorchid", |
| | "darkred", |
| | "darksalmon", |
| | "darkseagreen", |
| | "darkslateblue", |
| | "darkslategray", |
| | "darkslategrey", |
| | "darkturquoise", |
| | "darkviolet", |
| | "deeppink", |
| | "deepskyblue", |
| | "dimgray", |
| | "dimgrey", |
| | "dodgerblue", |
| | "firebrick", |
| | "floralwhite", |
| | "forestgreen", |
| | "fuchsia", |
| | "gainsboro", |
| | "ghostwhite", |
| | "gold", |
| | "goldenrod", |
| | "gray", |
| | "grey", |
| | "green", |
| | "greenyellow", |
| | "honeydew", |
| | "hotpink", |
| | "indianred", |
| | "indigo", |
| | "ivory", |
| | "khaki", |
| | "lavender", |
| | "lavenderblush", |
| | "lawngreen", |
| | "lemonchiffon", |
| | "lightblue", |
| | "lightcoral", |
| | "lightcyan", |
| | "lightgoldenrodyellow", |
| | "lightgray", |
| | "lightgrey", |
| | "lightgreen", |
| | "lightpink", |
| | "lightsalmon", |
| | "lightseagreen", |
| | "lightskyblue", |
| | "lightslategray", |
| | "lightslategrey", |
| | "lightsteelblue", |
| | "lightyellow", |
| | "lime", |
| | "limegreen", |
| | "linen", |
| | "magenta", |
| | "maroon", |
| | "mediumaquamarine", |
| | "mediumblue", |
| | "mediumorchid", |
| | "mediumpurple", |
| | "mediumseagreen", |
| | "mediumslateblue", |
| | "mediumspringgreen", |
| | "mediumturquoise", |
| | "mediumvioletred", |
| | "midnightblue", |
| | "mintcream", |
| | "mistyrose", |
| | "moccasin", |
| | "navajowhite", |
| | "navy", |
| | "oldlace", |
| | "olive", |
| | "olivedrab", |
| | "orange", |
| | "orangered", |
| | "orchid", |
| | "palegoldenrod", |
| | "palegreen", |
| | "paleturquoise", |
| | "palevioletred", |
| | "papayawhip", |
| | "peachpuff", |
| | "peru", |
| | "pink", |
| | "plum", |
| | "powderblue", |
| | "purple", |
| | "red", |
| | "rosybrown", |
| | "royalblue", |
| | "rebeccapurple", |
| | "saddlebrown", |
| | "salmon", |
| | "sandybrown", |
| | "seagreen", |
| | "seashell", |
| | "sienna", |
| | "silver", |
| | "skyblue", |
| | "slateblue", |
| | "slategray", |
| | "slategrey", |
| | "snow", |
| | "springgreen", |
| | "steelblue", |
| | "tan", |
| | "teal", |
| | "thistle", |
| | "tomato", |
| | "turquoise", |
| | "violet", |
| | "wheat", |
| | "white", |
| | "whitesmoke", |
| | "yellow", |
| | "yellowgreen", |
| | ] |
| |
|
| | def __init__( |
| | self, plotly_name, parent_name, array_ok=False, colorscale_path=None, **kwargs |
| | ): |
| | super(ColorValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | self.array_ok = array_ok |
| |
|
| | |
| | |
| | |
| | self.colorscale_path = colorscale_path |
| |
|
| | def numbers_allowed(self): |
| | return self.colorscale_path is not None |
| |
|
| | def description(self): |
| |
|
| | named_clrs_str = "\n".join( |
| | textwrap.wrap( |
| | ", ".join(self.named_colors), |
| | width=79 - 16, |
| | initial_indent=" " * 12, |
| | subsequent_indent=" " * 12, |
| | ) |
| | ) |
| |
|
| | valid_color_description = """\ |
| | The '{plotly_name}' property is a color and may be specified as: |
| | - A hex string (e.g. '#ff0000') |
| | - An rgb/rgba string (e.g. 'rgb(255,0,0)') |
| | - An hsl/hsla string (e.g. 'hsl(0,100%,50%)') |
| | - An hsv/hsva string (e.g. 'hsv(0,100%,100%)') |
| | - A named CSS color: |
| | {clrs}""".format( |
| | plotly_name=self.plotly_name, clrs=named_clrs_str |
| | ) |
| |
|
| | if self.colorscale_path: |
| | valid_color_description = ( |
| | valid_color_description |
| | + """ |
| | - A number that will be interpreted as a color |
| | according to {colorscale_path}""".format( |
| | colorscale_path=self.colorscale_path |
| | ) |
| | ) |
| |
|
| | if self.array_ok: |
| | valid_color_description = ( |
| | valid_color_description |
| | + """ |
| | - A list or array of any of the above""" |
| | ) |
| |
|
| | return valid_color_description |
| |
|
| | def validate_coerce(self, v, should_raise=True): |
| | if v is None: |
| | |
| | pass |
| | elif self.array_ok and is_homogeneous_array(v): |
| | v = copy_to_readonly_numpy_array(v) |
| | if self.numbers_allowed() and v.dtype.kind in ["u", "i", "f"]: |
| | |
| | |
| | pass |
| | else: |
| | validated_v = [self.validate_coerce(e, should_raise=False) for e in v] |
| |
|
| | invalid_els = self.find_invalid_els(v, validated_v) |
| |
|
| | if invalid_els and should_raise: |
| | self.raise_invalid_elements(invalid_els) |
| |
|
| | |
| | elif self.numbers_allowed() or invalid_els: |
| | v = copy_to_readonly_numpy_array(validated_v, kind="O") |
| | else: |
| | v = copy_to_readonly_numpy_array(validated_v, kind="U") |
| | elif self.array_ok and is_simple_array(v): |
| | validated_v = [self.validate_coerce(e, should_raise=False) for e in v] |
| |
|
| | invalid_els = self.find_invalid_els(v, validated_v) |
| |
|
| | if invalid_els and should_raise: |
| | self.raise_invalid_elements(invalid_els) |
| | else: |
| | v = validated_v |
| | else: |
| | |
| | validated_v = self.vc_scalar(v) |
| | if validated_v is None and should_raise: |
| | self.raise_invalid_val(v) |
| |
|
| | v = validated_v |
| |
|
| | return v |
| |
|
| | def find_invalid_els(self, orig, validated, invalid_els=None): |
| | """ |
| | Helper method to find invalid elements in orig array. |
| | Elements are invalid if their corresponding element in |
| | the validated array is None. |
| | |
| | This method handles deeply nested list structures |
| | """ |
| | if invalid_els is None: |
| | invalid_els = [] |
| |
|
| | for orig_el, validated_el in zip(orig, validated): |
| | if is_array(orig_el): |
| | self.find_invalid_els(orig_el, validated_el, invalid_els) |
| | else: |
| | if validated_el is None: |
| | invalid_els.append(orig_el) |
| |
|
| | return invalid_els |
| |
|
| | def vc_scalar(self, v): |
| | """Helper to validate/coerce a scalar color""" |
| | return ColorValidator.perform_validate_coerce( |
| | v, allow_number=self.numbers_allowed() |
| | ) |
| |
|
| | @staticmethod |
| | def perform_validate_coerce(v, allow_number=None): |
| | """ |
| | Validate, coerce, and return a single color value. If input cannot be |
| | coerced to a valid color then return None. |
| | |
| | Parameters |
| | ---------- |
| | v : number or str |
| | Candidate color value |
| | |
| | allow_number : bool |
| | True if numbers are allowed as colors |
| | |
| | Returns |
| | ------- |
| | number or str or None |
| | """ |
| |
|
| | if isinstance(v, numbers.Number) and allow_number: |
| | |
| | return v |
| | elif not isinstance(v, str): |
| | |
| | return None |
| | else: |
| | |
| | v_normalized = v.replace(" ", "").lower() |
| |
|
| | |
| | if fullmatch(ColorValidator.re_hex, v_normalized): |
| | |
| | return v |
| | elif fullmatch(ColorValidator.re_rgb_etc, v_normalized): |
| | |
| | |
| | |
| | return v |
| | elif fullmatch(ColorValidator.re_ddk, v_normalized): |
| | |
| | |
| | |
| | return v |
| | elif v_normalized in ColorValidator.named_colors: |
| | |
| | return v |
| | else: |
| | |
| | return None |
| |
|
| |
|
| | class ColorlistValidator(BaseValidator): |
| | """ |
| | "colorlist": { |
| | "description": "A list of colors. Must be an {array} containing |
| | valid colors.", |
| | "requiredOpts": [], |
| | "otherOpts": [ |
| | "dflt" |
| | ] |
| | } |
| | """ |
| |
|
| | def __init__(self, plotly_name, parent_name, **kwargs): |
| | super(ColorlistValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | def description(self): |
| | return """\ |
| | The '{plotly_name}' property is a colorlist that may be specified |
| | as a tuple, list, one-dimensional numpy array, or pandas Series of valid |
| | color strings""".format( |
| | plotly_name=self.plotly_name |
| | ) |
| |
|
| | def validate_coerce(self, v): |
| |
|
| | if v is None: |
| | |
| | pass |
| | elif is_array(v): |
| | validated_v = [ |
| | ColorValidator.perform_validate_coerce(e, allow_number=False) for e in v |
| | ] |
| |
|
| | invalid_els = [ |
| | el for el, validated_el in zip(v, validated_v) if validated_el is None |
| | ] |
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els) |
| |
|
| | v = to_scalar_or_list(v) |
| | else: |
| | self.raise_invalid_val(v) |
| | return v |
| |
|
| |
|
| | class ColorscaleValidator(BaseValidator): |
| | """ |
| | "colorscale": { |
| | "description": "A Plotly colorscale either picked by a name: |
| | (any of Greys, YlGnBu, Greens, YlOrRd, Bluered, |
| | RdBu, Reds, Blues, Picnic, Rainbow, Portland, |
| | Jet, Hot, Blackbody, Earth, Electric, Viridis) |
| | customized as an {array} of 2-element {arrays} |
| | where the first element is the normalized color |
| | level value (starting at *0* and ending at *1*), |
| | and the second item is a valid color string.", |
| | "requiredOpts": [], |
| | "otherOpts": [ |
| | "dflt" |
| | ] |
| | }, |
| | """ |
| |
|
| | def __init__(self, plotly_name, parent_name, **kwargs): |
| | super(ColorscaleValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | |
| | self._named_colorscales = None |
| |
|
| | @property |
| | def named_colorscales(self): |
| | if self._named_colorscales is None: |
| | import inspect |
| | import itertools |
| | from plotly import colors |
| |
|
| | colorscale_members = itertools.chain( |
| | inspect.getmembers(colors.sequential), |
| | inspect.getmembers(colors.diverging), |
| | inspect.getmembers(colors.cyclical), |
| | ) |
| |
|
| | self._named_colorscales = { |
| | c[0].lower(): c[1] |
| | for c in colorscale_members |
| | if isinstance(c, tuple) |
| | and len(c) == 2 |
| | and isinstance(c[0], str) |
| | and isinstance(c[1], list) |
| | and not c[0].endswith("_r") |
| | and not c[0].startswith("_") |
| | } |
| |
|
| | return self._named_colorscales |
| |
|
| | def description(self): |
| | colorscales_str = "\n".join( |
| | textwrap.wrap( |
| | repr(sorted(list(self.named_colorscales))), |
| | initial_indent=" " * 12, |
| | subsequent_indent=" " * 13, |
| | break_on_hyphens=False, |
| | width=80, |
| | ) |
| | ) |
| |
|
| | desc = """\ |
| | The '{plotly_name}' property is a colorscale and may be |
| | specified as: |
| | - A list of colors that will be spaced evenly to create the colorscale. |
| | Many predefined colorscale lists are included in the sequential, diverging, |
| | and cyclical modules in the plotly.colors package. |
| | - A list of 2-element lists where the first element is the |
| | normalized color level value (starting at 0 and ending at 1), |
| | and the second item is a valid color string. |
| | (e.g. [[0, 'green'], [0.5, 'red'], [1.0, 'rgb(0, 0, 255)']]) |
| | - One of the following named colorscales: |
| | {colorscales_str}. |
| | Appending '_r' to a named colorscale reverses it. |
| | """.format( |
| | plotly_name=self.plotly_name, colorscales_str=colorscales_str |
| | ) |
| |
|
| | return desc |
| |
|
| | def validate_coerce(self, v): |
| | v_valid = False |
| |
|
| | if v is None: |
| | v_valid = True |
| | elif isinstance(v, str): |
| | v_lower = v.lower() |
| | if v_lower in self.named_colorscales: |
| | |
| | v = self.named_colorscales[v_lower] |
| | v_valid = True |
| | elif v_lower.endswith("_r") and v_lower[:-2] in self.named_colorscales: |
| | v = self.named_colorscales[v_lower[:-2]][::-1] |
| | v_valid = True |
| | |
| | if v_valid: |
| | |
| | d = len(v) - 1 |
| | v = [[(1.0 * i) / (1.0 * d), x] for i, x in enumerate(v)] |
| |
|
| | elif is_array(v) and len(v) > 0: |
| | |
| | if isinstance(v[0], str): |
| | invalid_els = [ |
| | e for e in v if ColorValidator.perform_validate_coerce(e) is None |
| | ] |
| |
|
| | if len(invalid_els) == 0: |
| | v_valid = True |
| |
|
| | |
| | d = len(v) - 1 |
| | v = [[(1.0 * i) / (1.0 * d), x] for i, x in enumerate(v)] |
| | else: |
| | invalid_els = [ |
| | e |
| | for e in v |
| | if ( |
| | not is_array(e) |
| | or len(e) != 2 |
| | or not isinstance(e[0], numbers.Number) |
| | or not (0 <= e[0] <= 1) |
| | or not isinstance(e[1], str) |
| | or ColorValidator.perform_validate_coerce(e[1]) is None |
| | ) |
| | ] |
| |
|
| | if len(invalid_els) == 0: |
| | v_valid = True |
| |
|
| | |
| | v = [ |
| | [e[0], ColorValidator.perform_validate_coerce(e[1])] for e in v |
| | ] |
| |
|
| | if not v_valid: |
| | self.raise_invalid_val(v) |
| |
|
| | return v |
| |
|
| | def present(self, v): |
| | |
| | if v is None: |
| | return None |
| | elif isinstance(v, str): |
| | return v |
| | else: |
| | return tuple([tuple(e) for e in v]) |
| |
|
| |
|
| | class AngleValidator(BaseValidator): |
| | """ |
| | "angle": { |
| | "description": "A number (in degree) between -180 and 180.", |
| | "requiredOpts": [], |
| | "otherOpts": [ |
| | "dflt", |
| | "arrayOk" |
| | ] |
| | }, |
| | """ |
| |
|
| | def __init__(self, plotly_name, parent_name, array_ok=False, **kwargs): |
| | super(AngleValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| | self.array_ok = array_ok |
| |
|
| | def description(self): |
| | desc = """\ |
| | The '{plotly_name}' property is a angle (in degrees) that may be |
| | specified as a number between -180 and 180{array_ok}. |
| | Numeric values outside this range are converted to the equivalent value |
| | (e.g. 270 is converted to -90). |
| | """.format( |
| | plotly_name=self.plotly_name, |
| | array_ok=", or a list, numpy array or other iterable thereof" |
| | if self.array_ok |
| | else "", |
| | ) |
| |
|
| | return desc |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | |
| | pass |
| | elif self.array_ok and is_homogeneous_array(v): |
| | try: |
| | v_array = copy_to_readonly_numpy_array(v, force_numeric=True) |
| | except (ValueError, TypeError, OverflowError): |
| | self.raise_invalid_val(v) |
| | v = v_array |
| | |
| | v = (v + 180) % 360 - 180 |
| | elif self.array_ok and is_simple_array(v): |
| | |
| | invalid_els = [e for e in v if not isinstance(e, numbers.Number)] |
| |
|
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els[:10]) |
| |
|
| | v = [(x + 180) % 360 - 180 for x in to_scalar_or_list(v)] |
| | elif not isinstance(v, numbers.Number): |
| | self.raise_invalid_val(v) |
| | else: |
| | |
| | v = (v + 180) % 360 - 180 |
| |
|
| | return v |
| |
|
| |
|
| | class SubplotidValidator(BaseValidator): |
| | """ |
| | "subplotid": { |
| | "description": "An id string of a subplot type (given by dflt), |
| | optionally followed by an integer >1. e.g. if |
| | dflt='geo', we can have 'geo', 'geo2', 'geo3', |
| | ...", |
| | "requiredOpts": [ |
| | "dflt" |
| | ], |
| | "otherOpts": [ |
| | "regex" |
| | ] |
| | } |
| | """ |
| |
|
| | def __init__(self, plotly_name, parent_name, dflt=None, regex=None, **kwargs): |
| |
|
| | if dflt is None and regex is None: |
| | raise ValueError("One or both of regex and deflt must be specified") |
| |
|
| | super(SubplotidValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | if dflt is not None: |
| | self.base = dflt |
| | else: |
| | |
| | self.base = re.match(r"/\^(\w+)", regex).group(1) |
| |
|
| | self.regex = self.base + r"(\d*)" |
| |
|
| | def description(self): |
| |
|
| | desc = """\ |
| | The '{plotly_name}' property is an identifier of a particular |
| | subplot, of type '{base}', that may be specified as the string '{base}' |
| | optionally followed by an integer >= 1 |
| | (e.g. '{base}', '{base}1', '{base}2', '{base}3', etc.) |
| | """.format( |
| | plotly_name=self.plotly_name, base=self.base |
| | ) |
| | return desc |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | pass |
| | elif not isinstance(v, str): |
| | self.raise_invalid_val(v) |
| | else: |
| | |
| | match = fullmatch(self.regex, v) |
| | if not match: |
| | is_valid = False |
| | else: |
| | digit_str = match.group(1) |
| | if len(digit_str) > 0 and int(digit_str) == 0: |
| | is_valid = False |
| | elif len(digit_str) > 0 and int(digit_str) == 1: |
| | |
| | v = self.base |
| | is_valid = True |
| | else: |
| | is_valid = True |
| |
|
| | if not is_valid: |
| | self.raise_invalid_val(v) |
| | return v |
| |
|
| |
|
| | class FlaglistValidator(BaseValidator): |
| | """ |
| | "flaglist": { |
| | "description": "A string representing a combination of flags |
| | (order does not matter here). Combine any of the |
| | available `flags` with *+*. |
| | (e.g. ('lines+markers')). Values in `extras` |
| | cannot be combined.", |
| | "requiredOpts": [ |
| | "flags" |
| | ], |
| | "otherOpts": [ |
| | "dflt", |
| | "extras", |
| | "arrayOk" |
| | ] |
| | }, |
| | """ |
| |
|
| | def __init__( |
| | self, plotly_name, parent_name, flags, extras=None, array_ok=False, **kwargs |
| | ): |
| | super(FlaglistValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| | self.flags = flags |
| | self.extras = extras if extras is not None else [] |
| | self.array_ok = array_ok |
| |
|
| | def description(self): |
| |
|
| | desc = ( |
| | """\ |
| | The '{plotly_name}' property is a flaglist and may be specified |
| | as a string containing:""" |
| | ).format(plotly_name=self.plotly_name) |
| |
|
| | |
| | desc = ( |
| | desc |
| | + ( |
| | """ |
| | - Any combination of {flags} joined with '+' characters |
| | (e.g. '{eg_flag}')""" |
| | ).format(flags=self.flags, eg_flag="+".join(self.flags[:2])) |
| | ) |
| |
|
| | |
| | if self.extras: |
| | desc = ( |
| | desc |
| | + ( |
| | """ |
| | OR exactly one of {extras} (e.g. '{eg_extra}')""" |
| | ).format(extras=self.extras, eg_extra=self.extras[-1]) |
| | ) |
| |
|
| | if self.array_ok: |
| | desc = ( |
| | desc |
| | + """ |
| | - A list or array of the above""" |
| | ) |
| |
|
| | return desc |
| |
|
| | def vc_scalar(self, v): |
| | if isinstance(v, str): |
| | v = v.strip() |
| |
|
| | if v in self.extras: |
| | return v |
| |
|
| | if not isinstance(v, str): |
| | return None |
| |
|
| | |
| | |
| | split_vals = [e.strip() for e in re.split("[,+]", v)] |
| |
|
| | |
| | if all(f in self.flags for f in split_vals): |
| | return "+".join(split_vals) |
| | else: |
| | return None |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | |
| | pass |
| | elif self.array_ok and is_array(v): |
| |
|
| | |
| | validated_v = [self.vc_scalar(e) for e in v] |
| |
|
| | invalid_els = [ |
| | el for el, validated_el in zip(v, validated_v) if validated_el is None |
| | ] |
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els) |
| |
|
| | if is_homogeneous_array(v): |
| | v = copy_to_readonly_numpy_array(validated_v, kind="U") |
| | else: |
| | v = to_scalar_or_list(v) |
| | else: |
| |
|
| | validated_v = self.vc_scalar(v) |
| | if validated_v is None: |
| | self.raise_invalid_val(v) |
| |
|
| | v = validated_v |
| |
|
| | return v |
| |
|
| |
|
| | class AnyValidator(BaseValidator): |
| | """ |
| | "any": { |
| | "description": "Any type.", |
| | "requiredOpts": [], |
| | "otherOpts": [ |
| | "dflt", |
| | "values", |
| | "arrayOk" |
| | ] |
| | }, |
| | """ |
| |
|
| | def __init__(self, plotly_name, parent_name, values=None, array_ok=False, **kwargs): |
| | super(AnyValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| | self.values = values |
| | self.array_ok = array_ok |
| |
|
| | def description(self): |
| |
|
| | desc = """\ |
| | The '{plotly_name}' property accepts values of any type |
| | """.format( |
| | plotly_name=self.plotly_name |
| | ) |
| | return desc |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | |
| | pass |
| | elif self.array_ok and is_homogeneous_array(v): |
| | v = copy_to_readonly_numpy_array(v, kind="O") |
| | elif self.array_ok and is_simple_array(v): |
| | v = to_scalar_or_list(v) |
| | return v |
| |
|
| |
|
| | class InfoArrayValidator(BaseValidator): |
| | """ |
| | "info_array": { |
| | "description": "An {array} of plot information.", |
| | "requiredOpts": [ |
| | "items" |
| | ], |
| | "otherOpts": [ |
| | "dflt", |
| | "freeLength", |
| | "dimensions" |
| | ] |
| | } |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | plotly_name, |
| | parent_name, |
| | items, |
| | free_length=None, |
| | dimensions=None, |
| | **kwargs, |
| | ): |
| | super(InfoArrayValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | self.items = items |
| | self.dimensions = dimensions if dimensions else 1 |
| | self.free_length = free_length |
| |
|
| | |
| | self.item_validators = [] |
| | info_array_items = self.items if isinstance(self.items, list) else [self.items] |
| |
|
| | for i, item in enumerate(info_array_items): |
| | element_name = "{name}[{i}]".format(name=plotly_name, i=i) |
| | item_validator = InfoArrayValidator.build_validator( |
| | item, element_name, parent_name |
| | ) |
| | self.item_validators.append(item_validator) |
| |
|
| | def description(self): |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | desc = """\ |
| | The '{plotly_name}' property is an info array that may be specified as:\ |
| | """.format( |
| | plotly_name=self.plotly_name |
| | ) |
| |
|
| | if isinstance(self.items, list): |
| | |
| | if self.dimensions in (1, "1-2"): |
| | upto = " up to" if self.free_length and self.dimensions == 1 else "" |
| | desc += """ |
| | |
| | * a list or tuple of{upto} {N} elements where:\ |
| | """.format( |
| | upto=upto, N=len(self.item_validators) |
| | ) |
| |
|
| | for i, item_validator in enumerate(self.item_validators): |
| | el_desc = item_validator.description().strip() |
| | desc = ( |
| | desc |
| | + """ |
| | ({i}) {el_desc}""".format( |
| | i=i, el_desc=el_desc |
| | ) |
| | ) |
| |
|
| | |
| | if self.dimensions in ("1-2", 2): |
| | assert self.free_length |
| |
|
| | desc += """ |
| | |
| | * a 2D list where:""" |
| | for i, item_validator in enumerate(self.item_validators): |
| | |
| | orig_name = item_validator.plotly_name |
| | item_validator.plotly_name = "{name}[i][{i}]".format( |
| | name=self.plotly_name, i=i |
| | ) |
| |
|
| | el_desc = item_validator.description().strip() |
| | desc = ( |
| | desc |
| | + """ |
| | ({i}) {el_desc}""".format( |
| | i=i, el_desc=el_desc |
| | ) |
| | ) |
| | item_validator.plotly_name = orig_name |
| | else: |
| | |
| | assert self.free_length |
| | item_validator = self.item_validators[0] |
| | orig_name = item_validator.plotly_name |
| |
|
| | if self.dimensions in (1, "1-2"): |
| | item_validator.plotly_name = "{name}[i]".format(name=self.plotly_name) |
| |
|
| | el_desc = item_validator.description().strip() |
| |
|
| | desc += """ |
| | * a list of elements where: |
| | {el_desc} |
| | """.format( |
| | el_desc=el_desc |
| | ) |
| |
|
| | if self.dimensions in ("1-2", 2): |
| | item_validator.plotly_name = "{name}[i][j]".format( |
| | name=self.plotly_name |
| | ) |
| |
|
| | el_desc = item_validator.description().strip() |
| | desc += """ |
| | * a 2D list where: |
| | {el_desc} |
| | """.format( |
| | el_desc=el_desc |
| | ) |
| |
|
| | item_validator.plotly_name = orig_name |
| |
|
| | return desc |
| |
|
| | @staticmethod |
| | def build_validator(validator_info, plotly_name, parent_name): |
| | datatype = validator_info["valType"] |
| | validator_classname = datatype.title().replace("_", "") + "Validator" |
| | validator_class = eval(validator_classname) |
| |
|
| | kwargs = { |
| | k: validator_info[k] |
| | for k in validator_info |
| | if k not in ["valType", "description", "role"] |
| | } |
| |
|
| | return validator_class( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | def validate_element_with_indexed_name(self, val, validator, inds): |
| | """ |
| | Helper to add indexes to a validator's name, call validate_coerce on |
| | a value, then restore the original validator name. |
| | |
| | This makes sure that if a validation error message is raised, the |
| | property name the user sees includes the index(es) of the offending |
| | element. |
| | |
| | Parameters |
| | ---------- |
| | val: |
| | A value to be validated |
| | validator |
| | A validator |
| | inds |
| | List of one or more non-negative integers that represent the |
| | nested index of the value being validated |
| | Returns |
| | ------- |
| | val |
| | validated value |
| | |
| | Raises |
| | ------ |
| | ValueError |
| | if val fails validation |
| | """ |
| | orig_name = validator.plotly_name |
| | new_name = self.plotly_name |
| | for i in inds: |
| | new_name += "[" + str(i) + "]" |
| | validator.plotly_name = new_name |
| | try: |
| | val = validator.validate_coerce(val) |
| | finally: |
| | validator.plotly_name = orig_name |
| |
|
| | return val |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | |
| | return None |
| | elif not is_array(v): |
| | self.raise_invalid_val(v) |
| |
|
| | |
| | orig_v = v |
| |
|
| | |
| | |
| | v = to_scalar_or_list(v) |
| |
|
| | is_v_2d = v and is_array(v[0]) |
| |
|
| | if is_v_2d and self.dimensions in ("1-2", 2): |
| | if is_array(self.items): |
| | |
| | |
| | for i, row in enumerate(v): |
| | |
| | if not is_array(row) or len(row) != len(self.items): |
| | self.raise_invalid_val(orig_v[i], [i]) |
| |
|
| | for j, validator in enumerate(self.item_validators): |
| | row[j] = self.validate_element_with_indexed_name( |
| | v[i][j], validator, [i, j] |
| | ) |
| | else: |
| | |
| | |
| | validator = self.item_validators[0] |
| | for i, row in enumerate(v): |
| | if not is_array(row): |
| | self.raise_invalid_val(orig_v[i], [i]) |
| |
|
| | for j, el in enumerate(row): |
| | row[j] = self.validate_element_with_indexed_name( |
| | el, validator, [i, j] |
| | ) |
| | elif v and self.dimensions == 2: |
| | |
| | self.raise_invalid_val(orig_v[0], [0]) |
| | elif not is_array(self.items): |
| | |
| | validator = self.item_validators[0] |
| | for i, el in enumerate(v): |
| | v[i] = self.validate_element_with_indexed_name(el, validator, [i]) |
| |
|
| | elif not self.free_length and len(v) != len(self.item_validators): |
| | |
| | self.raise_invalid_val(orig_v) |
| | elif self.free_length and len(v) > len(self.item_validators): |
| | |
| | self.raise_invalid_val(orig_v) |
| | else: |
| | |
| | for i, (el, validator) in enumerate(zip(v, self.item_validators)): |
| | |
| | v[i] = validator.validate_coerce(el) |
| |
|
| | return v |
| |
|
| | def present(self, v): |
| | if v is None: |
| | return None |
| | else: |
| | if ( |
| | self.dimensions == 2 |
| | or self.dimensions == "1-2" |
| | and v |
| | and is_array(v[0]) |
| | ): |
| |
|
| | |
| | v = copy.deepcopy(v) |
| | for row in v: |
| | for i, (el, validator) in enumerate(zip(row, self.item_validators)): |
| | row[i] = validator.present(el) |
| |
|
| | return tuple(tuple(row) for row in v) |
| | else: |
| | |
| | v = copy.copy(v) |
| | |
| | for i, (el, validator) in enumerate(zip(v, self.item_validators)): |
| | |
| | v[i] = validator.present(el) |
| |
|
| | |
| | return tuple(v) |
| |
|
| |
|
| | class LiteralValidator(BaseValidator): |
| | """ |
| | Validator for readonly literal values |
| | """ |
| |
|
| | def __init__(self, plotly_name, parent_name, val, **kwargs): |
| | super(LiteralValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| | self.val = val |
| |
|
| | def validate_coerce(self, v): |
| | if v != self.val: |
| | raise ValueError( |
| | """\ |
| | The '{plotly_name}' property of {parent_name} is read-only""".format( |
| | plotly_name=self.plotly_name, parent_name=self.parent_name |
| | ) |
| | ) |
| | else: |
| | return v |
| |
|
| |
|
| | class DashValidator(EnumeratedValidator): |
| | """ |
| | Special case validator for handling dash properties that may be specified |
| | as lists of dash lengths. These are not currently specified in the |
| | schema. |
| | |
| | "dash": { |
| | "valType": "string", |
| | "values": [ |
| | "solid", |
| | "dot", |
| | "dash", |
| | "longdash", |
| | "dashdot", |
| | "longdashdot" |
| | ], |
| | "dflt": "solid", |
| | "role": "style", |
| | "editType": "style", |
| | "description": "Sets the dash style of lines. Set to a dash type |
| | string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or |
| | *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*)." |
| | }, |
| | """ |
| |
|
| | def __init__(self, plotly_name, parent_name, values, **kwargs): |
| |
|
| | |
| | dash_list_regex = r"/^\d+(\.\d+)?(px|%)?((,|\s)\s*\d+(\.\d+)?(px|%)?)*$/" |
| |
|
| | values = values + [dash_list_regex] |
| |
|
| | |
| | super(DashValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, values=values, **kwargs |
| | ) |
| |
|
| | def description(self): |
| |
|
| | |
| | enum_vals = [] |
| | enum_regexs = [] |
| | for v, regex in zip(self.values, self.val_regexs): |
| | if regex is not None: |
| | enum_regexs.append(regex.pattern) |
| | else: |
| | enum_vals.append(v) |
| | desc = """\ |
| | The '{name}' property is an enumeration that may be specified as:""".format( |
| | name=self.plotly_name |
| | ) |
| |
|
| | if enum_vals: |
| | enum_vals_str = "\n".join( |
| | textwrap.wrap( |
| | repr(enum_vals), |
| | initial_indent=" " * 12, |
| | subsequent_indent=" " * 12, |
| | break_on_hyphens=False, |
| | width=80, |
| | ) |
| | ) |
| |
|
| | desc = ( |
| | desc |
| | + """ |
| | - One of the following dash styles: |
| | {enum_vals_str}""".format( |
| | enum_vals_str=enum_vals_str |
| | ) |
| | ) |
| |
|
| | desc = ( |
| | desc |
| | + """ |
| | - A string containing a dash length list in pixels or percentages |
| | (e.g. '5px 10px 2px 2px', '5, 10, 2, 2', '10% 20% 40%', etc.) |
| | """ |
| | ) |
| | return desc |
| |
|
| |
|
| | class ImageUriValidator(BaseValidator): |
| | _PIL = None |
| |
|
| | try: |
| | _PIL = import_module("PIL") |
| | except ImportError: |
| | pass |
| |
|
| | def __init__(self, plotly_name, parent_name, **kwargs): |
| | super(ImageUriValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | def description(self): |
| |
|
| | desc = """\ |
| | The '{plotly_name}' property is an image URI that may be specified as: |
| | - A remote image URI string |
| | (e.g. 'http://www.somewhere.com/image.png') |
| | - A data URI image string |
| | (e.g. 'data:image/png;base64,iVBORw0KGgoAAAANSU') |
| | - A PIL.Image.Image object which will be immediately converted |
| | to a data URI image string |
| | See http://pillow.readthedocs.io/en/latest/reference/Image.html |
| | """.format( |
| | plotly_name=self.plotly_name |
| | ) |
| | return desc |
| |
|
| | def validate_coerce(self, v): |
| | if v is None: |
| | pass |
| | elif isinstance(v, str): |
| | |
| | |
| | |
| | pass |
| | elif self._PIL and isinstance(v, self._PIL.Image.Image): |
| | |
| | v = self.pil_image_to_uri(v) |
| | else: |
| | self.raise_invalid_val(v) |
| |
|
| | return v |
| |
|
| | @staticmethod |
| | def pil_image_to_uri(v): |
| | in_mem_file = io.BytesIO() |
| | v.save(in_mem_file, format="PNG") |
| | in_mem_file.seek(0) |
| | img_bytes = in_mem_file.read() |
| | base64_encoded_result_bytes = base64.b64encode(img_bytes) |
| | base64_encoded_result_str = base64_encoded_result_bytes.decode("ascii") |
| | v = "data:image/png;base64,{base64_encoded_result_str}".format( |
| | base64_encoded_result_str=base64_encoded_result_str |
| | ) |
| | return v |
| |
|
| |
|
| | class CompoundValidator(BaseValidator): |
| | def __init__(self, plotly_name, parent_name, data_class_str, data_docs, **kwargs): |
| | super(CompoundValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | |
| | self.data_class_str = data_class_str |
| | self._data_class = None |
| | self.data_docs = data_docs |
| | self.module_str = CompoundValidator.compute_graph_obj_module_str( |
| | self.data_class_str, parent_name |
| | ) |
| |
|
| | @staticmethod |
| | def compute_graph_obj_module_str(data_class_str, parent_name): |
| | if parent_name == "frame" and data_class_str in ["Data", "Layout"]: |
| | |
| | |
| | |
| |
|
| | parent_parts = parent_name.split(".") |
| | module_str = ".".join(["plotly.graph_objs"] + parent_parts[1:]) |
| | elif parent_name == "layout.template" and data_class_str == "Layout": |
| | |
| | module_str = "plotly.graph_objs" |
| | elif "layout.template.data" in parent_name: |
| | |
| | parent_name = parent_name.replace("layout.template.data.", "") |
| | if parent_name: |
| | module_str = "plotly.graph_objs." + parent_name |
| | else: |
| | module_str = "plotly.graph_objs" |
| | elif parent_name: |
| | module_str = "plotly.graph_objs." + parent_name |
| | else: |
| | module_str = "plotly.graph_objs" |
| |
|
| | return module_str |
| |
|
| | @property |
| | def data_class(self): |
| | if self._data_class is None: |
| | module = import_module(self.module_str) |
| | self._data_class = getattr(module, self.data_class_str) |
| |
|
| | return self._data_class |
| |
|
| | def description(self): |
| |
|
| | desc = ( |
| | """\ |
| | The '{plotly_name}' property is an instance of {class_str} |
| | that may be specified as: |
| | - An instance of :class:`{module_str}.{class_str}` |
| | - A dict of string/value properties that will be passed |
| | to the {class_str} constructor |
| | |
| | Supported dict properties: |
| | {constructor_params_str}""" |
| | ).format( |
| | plotly_name=self.plotly_name, |
| | class_str=self.data_class_str, |
| | module_str=self.module_str, |
| | constructor_params_str=self.data_docs, |
| | ) |
| |
|
| | return desc |
| |
|
| | def validate_coerce(self, v, skip_invalid=False, _validate=True): |
| | if v is None: |
| | v = self.data_class() |
| |
|
| | elif isinstance(v, dict): |
| | v = self.data_class(v, skip_invalid=skip_invalid, _validate=_validate) |
| |
|
| | elif isinstance(v, self.data_class): |
| | |
| | v = self.data_class(v) |
| | else: |
| | if skip_invalid: |
| | v = self.data_class() |
| | else: |
| | self.raise_invalid_val(v) |
| |
|
| | v._plotly_name = self.plotly_name |
| | return v |
| |
|
| | def present(self, v): |
| | |
| | return v |
| |
|
| |
|
| | class TitleValidator(CompoundValidator): |
| | """ |
| | This is a special validator to allow compound title properties |
| | (e.g. layout.title, layout.xaxis.title, etc.) to be set as strings |
| | or numbers. These strings are mapped to the 'text' property of the |
| | compound validator. |
| | """ |
| |
|
| | def __init__(self, *args, **kwargs): |
| | super(TitleValidator, self).__init__(*args, **kwargs) |
| |
|
| | def validate_coerce(self, v, skip_invalid=False): |
| | if isinstance(v, (str, int, float)): |
| | v = {"text": v} |
| | return super(TitleValidator, self).validate_coerce(v, skip_invalid=skip_invalid) |
| |
|
| |
|
| | class CompoundArrayValidator(BaseValidator): |
| | def __init__(self, plotly_name, parent_name, data_class_str, data_docs, **kwargs): |
| | super(CompoundArrayValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | |
| | self.data_class_str = data_class_str |
| | self._data_class = None |
| |
|
| | self.data_docs = data_docs |
| | self.module_str = CompoundValidator.compute_graph_obj_module_str( |
| | self.data_class_str, parent_name |
| | ) |
| |
|
| | def description(self): |
| |
|
| | desc = ( |
| | """\ |
| | The '{plotly_name}' property is a tuple of instances of |
| | {class_str} that may be specified as: |
| | - A list or tuple of instances of {module_str}.{class_str} |
| | - A list or tuple of dicts of string/value properties that |
| | will be passed to the {class_str} constructor |
| | |
| | Supported dict properties: |
| | {constructor_params_str}""" |
| | ).format( |
| | plotly_name=self.plotly_name, |
| | class_str=self.data_class_str, |
| | module_str=self.module_str, |
| | constructor_params_str=self.data_docs, |
| | ) |
| |
|
| | return desc |
| |
|
| | @property |
| | def data_class(self): |
| | if self._data_class is None: |
| | module = import_module(self.module_str) |
| | self._data_class = getattr(module, self.data_class_str) |
| |
|
| | return self._data_class |
| |
|
| | def validate_coerce(self, v, skip_invalid=False): |
| |
|
| | if v is None: |
| | v = [] |
| |
|
| | elif isinstance(v, (list, tuple)): |
| | res = [] |
| | invalid_els = [] |
| | for v_el in v: |
| | if isinstance(v_el, self.data_class): |
| | res.append(self.data_class(v_el)) |
| | elif isinstance(v_el, dict): |
| | res.append(self.data_class(v_el, skip_invalid=skip_invalid)) |
| | else: |
| | if skip_invalid: |
| | res.append(self.data_class()) |
| | else: |
| | res.append(None) |
| | invalid_els.append(v_el) |
| |
|
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els) |
| |
|
| | v = to_scalar_or_list(res) |
| | else: |
| | if skip_invalid: |
| | v = [] |
| | else: |
| | self.raise_invalid_val(v) |
| |
|
| | return v |
| |
|
| | def present(self, v): |
| | |
| | return tuple(v) |
| |
|
| |
|
| | class BaseDataValidator(BaseValidator): |
| | def __init__( |
| | self, class_strs_map, plotly_name, parent_name, set_uid=False, **kwargs |
| | ): |
| | super(BaseDataValidator, self).__init__( |
| | plotly_name=plotly_name, parent_name=parent_name, **kwargs |
| | ) |
| |
|
| | self.class_strs_map = class_strs_map |
| | self._class_map = {} |
| | self.set_uid = set_uid |
| |
|
| | def description(self): |
| |
|
| | trace_types = str(list(self.class_strs_map.keys())) |
| |
|
| | trace_types_wrapped = "\n".join( |
| | textwrap.wrap( |
| | trace_types, |
| | initial_indent=" One of: ", |
| | subsequent_indent=" " * 21, |
| | width=79 - 12, |
| | ) |
| | ) |
| |
|
| | desc = ( |
| | """\ |
| | The '{plotly_name}' property is a tuple of trace instances |
| | that may be specified as: |
| | - A list or tuple of trace instances |
| | (e.g. [Scatter(...), Bar(...)]) |
| | - A single trace instance |
| | (e.g. Scatter(...), Bar(...), etc.) |
| | - A list or tuple of dicts of string/value properties where: |
| | - The 'type' property specifies the trace type |
| | {trace_types} |
| | |
| | - All remaining properties are passed to the constructor of |
| | the specified trace type |
| | |
| | (e.g. [{{'type': 'scatter', ...}}, {{'type': 'bar, ...}}])""" |
| | ).format(plotly_name=self.plotly_name, trace_types=trace_types_wrapped) |
| |
|
| | return desc |
| |
|
| | def get_trace_class(self, trace_name): |
| | |
| | if trace_name not in self._class_map: |
| | trace_module = import_module("plotly.graph_objs") |
| | trace_class_name = self.class_strs_map[trace_name] |
| | self._class_map[trace_name] = getattr(trace_module, trace_class_name) |
| |
|
| | return self._class_map[trace_name] |
| |
|
| | def validate_coerce(self, v, skip_invalid=False, _validate=True): |
| | from plotly.basedatatypes import BaseTraceType |
| |
|
| | |
| | |
| | from plotly.graph_objs import Histogram2dcontour |
| |
|
| | if v is None: |
| | v = [] |
| | else: |
| | if not isinstance(v, (list, tuple)): |
| | v = [v] |
| |
|
| | res = [] |
| | invalid_els = [] |
| | for v_el in v: |
| |
|
| | if isinstance(v_el, BaseTraceType): |
| | if isinstance(v_el, Histogram2dcontour): |
| | v_el = dict(type="histogram2dcontour", **v_el._props) |
| | else: |
| | v_el = v_el._props |
| |
|
| | if isinstance(v_el, dict): |
| | type_in_v_el = "type" in v_el |
| | trace_type = v_el.pop("type", "scatter") |
| |
|
| | if trace_type not in self.class_strs_map: |
| | if skip_invalid: |
| | |
| | trace = self.get_trace_class("scatter")( |
| | skip_invalid=skip_invalid, _validate=_validate, **v_el |
| | ) |
| | res.append(trace) |
| | else: |
| | res.append(None) |
| | invalid_els.append(v_el) |
| | else: |
| | trace = self.get_trace_class(trace_type)( |
| | skip_invalid=skip_invalid, _validate=_validate, **v_el |
| | ) |
| | res.append(trace) |
| |
|
| | if type_in_v_el: |
| | |
| | v_el["type"] = trace_type |
| | else: |
| | if skip_invalid: |
| | |
| | trace = self.get_trace_class("scatter")() |
| | res.append(trace) |
| | else: |
| | res.append(None) |
| | invalid_els.append(v_el) |
| |
|
| | if invalid_els: |
| | self.raise_invalid_elements(invalid_els) |
| |
|
| | v = to_scalar_or_list(res) |
| |
|
| | |
| | if self.set_uid: |
| | for trace in v: |
| | trace.uid = str(uuid.uuid4()) |
| |
|
| | return v |
| |
|
| |
|
| | class BaseTemplateValidator(CompoundValidator): |
| | def __init__(self, plotly_name, parent_name, data_class_str, data_docs, **kwargs): |
| |
|
| | super(BaseTemplateValidator, self).__init__( |
| | plotly_name=plotly_name, |
| | parent_name=parent_name, |
| | data_class_str=data_class_str, |
| | data_docs=data_docs, |
| | **kwargs, |
| | ) |
| |
|
| | def description(self): |
| | compound_description = super(BaseTemplateValidator, self).description() |
| | compound_description += """ |
| | - The name of a registered template where current registered templates |
| | are stored in the plotly.io.templates configuration object. The names |
| | of all registered templates can be retrieved with: |
| | >>> import plotly.io as pio |
| | >>> list(pio.templates) # doctest: +ELLIPSIS |
| | ['ggplot2', 'seaborn', 'simple_white', 'plotly', 'plotly_white', ...] |
| | |
| | - A string containing multiple registered template names, joined on '+' |
| | characters (e.g. 'template1+template2'). In this case the resulting |
| | template is computed by merging together the collection of registered |
| | templates""" |
| |
|
| | return compound_description |
| |
|
| | def validate_coerce(self, v, skip_invalid=False): |
| | import plotly.io as pio |
| |
|
| | try: |
| | |
| | |
| | if v in pio.templates: |
| | return copy.deepcopy(pio.templates[v]) |
| | |
| | |
| | elif isinstance(v, str): |
| | template_names = v.split("+") |
| | if all([name in pio.templates for name in template_names]): |
| | return pio.templates.merge_templates(*template_names) |
| |
|
| | except TypeError: |
| | |
| | pass |
| |
|
| | |
| | if v == {} or isinstance(v, self.data_class) and v.to_plotly_json() == {}: |
| | |
| | |
| | |
| | return self.data_class(data_scatter=[{}]) |
| |
|
| | return super(BaseTemplateValidator, self).validate_coerce( |
| | v, skip_invalid=skip_invalid |
| | ) |
| |
|