| import base64 |
| import numbers |
| import textwrap |
| import uuid |
| from importlib import import_module |
| import copy |
| import io |
| import re |
| import sys |
| import narwhals.stable.v1 as nw |
|
|
| 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_non_numpy_type(np, v): |
| """ |
| Convert a numpy scalar value to a native Python type. |
| Calling .item() on a datetime64[ns] value returns an integer, since |
| Python datetimes only support microsecond precision. So we cast |
| datetime64[ns] to datetime64[us] to ensure it remains a datetime. |
| |
| Should only be used in contexts where we already know `np` is defined. |
| """ |
| if hasattr(v, "dtype") and v.dtype == np.dtype("datetime64[ns]"): |
| return v.astype("datetime64[us]").item() |
| return v.item() |
|
|
|
|
| |
| |
| 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 to_non_numpy_type(np, v) |
| 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 to_non_numpy_type(np, v) |
| 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") |
|
|
| 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", |
| } |
|
|
| |
| |
| v = nw.from_native(v, allow_series=True, pass_through=True) |
|
|
| if isinstance(v, nw.Series): |
| if v.dtype == nw.Datetime and v.dtype.time_zone is not None: |
| |
| v = v.dt.replace_time_zone(None).to_numpy() |
| else: |
| v = v.to_numpy() |
| elif isinstance(v, nw.DataFrame): |
| schema = v.schema |
| overrides = {} |
| for key, val in schema.items(): |
| if val == nw.Datetime and val.time_zone is not None: |
| |
| overrides[key] = nw.col(key).dt.replace_time_zone(None) |
| if overrides: |
| v = v.with_columns(**overrides) |
| v = v.to_numpy() |
|
|
| 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))) |
| or (isinstance(v, nw.Series)) |
| ): |
| 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__) |
|
|
|
|
| def is_typed_array_spec(v): |
| """ |
| Return whether a value is considered to be a typed array spec for plotly.js |
| """ |
| return isinstance(v, dict) and "bdata" in v and "dtype" in v |
|
|
|
|
| def is_none_or_typed_array_spec(v): |
| return v is None or is_typed_array_spec(v) |
|
|
|
|
| |
| |
| 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 is_none_or_typed_array_spec(v): |
| 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 is_none_or_typed_array_spec(v): |
| 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": [ |
| "arrayOk", |
| "dflt" |
| ] |
| }, |
| """ |
|
|
| def __init__(self, plotly_name, parent_name, array_ok=False, **kwargs): |
| super(BooleanValidator, 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 boolean and must be specified as: |
| - A boolean value: True or False""".format(plotly_name=self.plotly_name) |
| if self.array_ok: |
| desc += """ |
| - A tuple or list of the above""" |
| return desc |
|
|
| def validate_coerce(self, v): |
| if is_none_or_typed_array_spec(v): |
| pass |
| elif self.array_ok and is_simple_array(v): |
| invalid_els = [e for e in v if not isinstance(e, bool)] |
| if invalid_els: |
| self.raise_invalid_elements(invalid_els[:10]) |
| v = to_scalar_or_list(v) |
| 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 is_none_or_typed_array_spec(v): |
| 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 is_none_or_typed_array_spec(v): |
| 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", |
| "extras", |
| "arrayOk" |
| ] |
| }, |
| """ |
|
|
| def __init__( |
| self, |
| plotly_name, |
| parent_name, |
| min=None, |
| max=None, |
| extras=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.extras = extras if extras is not None else [] |
| 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.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 tuple, list, or one-dimensional numpy array of the above""" |
| ) |
|
|
| return desc |
|
|
| def validate_coerce(self, v): |
| if is_none_or_typed_array_spec(v): |
| pass |
| elif v in self.extras: |
| return v |
| 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) and e not in self.extras |
| ] |
|
|
| if invalid_els: |
| self.raise_invalid_elements(invalid_els[:10]) |
|
|
| |
| if self.has_min_max: |
| invalid_els = [ |
| e |
| for e in v |
| if not (isinstance(e, int) and self.min_val <= e <= self.max_val) |
| and e not in self.extras |
| ] |
|
|
| 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 is_none_or_typed_array_spec(v): |
| 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): |
| 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: see https://plotly.com/python/css-colors/ for a list""".format( |
| plotly_name=self.plotly_name |
| ) |
|
|
| 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 is_none_or_typed_array_spec(v): |
| 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 is_none_or_typed_array_spec(v): |
| 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 is_none_or_typed_array_spec(v): |
| 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": [ |
| "arrayOk", |
| "regex" |
| ] |
| } |
| """ |
|
|
| def __init__( |
| self, plotly_name, parent_name, dflt=None, regex=None, array_ok=False, **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*)" |
| self.array_ok = array_ok |
|
|
| 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 |
| ) |
| if self.array_ok: |
| desc += """ |
| - A tuple or list of the above""" |
| return desc |
|
|
| def validate_coerce(self, v): |
| def coerce(value): |
| if not isinstance(value, str): |
| return value, False |
| match = fullmatch(self.regex, value) |
| if not match: |
| return value, False |
| else: |
| digit_str = match.group(1) |
| if len(digit_str) > 0 and int(digit_str) == 0: |
| return value, False |
| elif len(digit_str) > 0 and int(digit_str) == 1: |
| return self.base, True |
| else: |
| return value, True |
|
|
| if v is None: |
| pass |
| elif self.array_ok and is_simple_array(v): |
| values = [] |
| invalid_els = [] |
| for e in v: |
| coerced_e, success = coerce(e) |
| values.append(coerced_e) |
| if not success: |
| invalid_els.append(coerced_e) |
| if len(invalid_els) > 0: |
| self.raise_invalid_elements(invalid_els[:10]) |
| return values |
| else: |
| v, success = coerce(v) |
| if not success: |
| self.raise_invalid_val(self.base) |
| 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 is_none_or_typed_array_spec(v): |
| 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 is_none_or_typed_array_spec(v): |
| 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 is_none_or_typed_array_spec(v): |
| 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""" |
| ).format( |
| plotly_name=self.plotly_name, |
| class_str=self.data_class_str, |
| module_str=self.module_str, |
| ) |
|
|
| 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""" |
| ).format( |
| plotly_name=self.plotly_name, |
| class_str=self.data_class_str, |
| module_str=self.module_str, |
| ) |
|
|
| 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 |
| ) |
|
|