wg-backend-django/dell-env/lib/python3.11/site-packages/_plotly_utils/basevalidators.py

2780 lines
85 KiB
Python
Raw Normal View History

2023-10-30 03:40:43 -04:00
import base64
import numbers
import textwrap
import uuid
from importlib import import_module
import copy
import io
import re
import sys
import warnings
from _plotly_utils.optional_imports import get_module
# back-port of fullmatch from Py3.4+
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)
# Utility functions
# -----------------
def to_scalar_or_list(v):
# Handle the case where 'v' is a non-native scalar-like type,
# such as numpy.float32. Without this case, the object might be
# considered numpy-convertable and therefore promoted to a
# 0-dimensional array, but we instead want it converted to a
# Python native scalar type ('float' in the example above).
# We explicitly check if is has the 'item' method, which conventionally
# converts these types to native scalars.
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")
# Don't force pandas to be loaded, we only want to know if it's already loaded
pd = get_module("pandas", should_load=False)
assert np is not None
# ### Process kind ###
if not kind:
kind = ()
elif isinstance(kind, str):
kind = (kind,)
first_kind = kind[0] if kind else None
# u: unsigned int, i: signed int, f: float
numeric_kinds = {"u", "i", "f"}
kind_default_dtypes = {
"u": "uint32",
"i": "int32",
"f": "float64",
"O": "object",
}
# Handle pandas Series and Index objects
if pd and isinstance(v, (pd.Series, pd.Index)):
if v.dtype.kind in numeric_kinds:
# Get the numeric numpy array so we use fast path below
v = v.values
elif v.dtype.kind == "M":
# Convert datetime Series/Index to numpy array of datetimes
if isinstance(v, pd.Series):
with warnings.catch_warnings():
warnings.simplefilter("ignore", FutureWarning)
# Series.dt.to_pydatetime will return Index[object]
# https://github.com/pandas-dev/pandas/pull/52459
v = np.array(v.dt.to_pydatetime())
else:
# DatetimeIndex
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":
with warnings.catch_warnings():
warnings.simplefilter("ignore", FutureWarning)
# Series.dt.to_pydatetime will return Index[object]
# https://github.com/pandas-dev/pandas/pull/52459
v = [
np.array(row.dt.to_pydatetime()).tolist() for i, row in v.iterrows()
]
if not isinstance(v, np.ndarray):
# v has its own logic on how to convert itself into a numpy array
if is_numpy_convertable(v):
return copy_to_readonly_numpy_array(
np.array(v), kind=kind, force_numeric=force_numeric
)
else:
# v is not homogenous array
v_list = [to_scalar_or_list(e) for e in v]
# Lookup dtype for requested kind, if any
dtype = kind_default_dtypes.get(first_kind, None)
# construct new array from list
new_v = np.array(v_list, order="C", dtype=dtype)
elif v.dtype.kind in numeric_kinds:
# v is a homogenous numeric array
if kind and v.dtype.kind not in kind:
# Kind(s) were specified and this array doesn't match
# Convert to the default dtype for the first kind
dtype = kind_default_dtypes.get(first_kind, None)
new_v = np.ascontiguousarray(v.astype(dtype))
else:
# Either no kind was requested or requested kind is satisfied
new_v = np.ascontiguousarray(v.copy())
else:
# v is a non-numeric homogenous array
new_v = v.copy()
# Handle force numeric param
# --------------------------
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:
# Force non-numeric arrays to have object type
# --------------------------------------------
# Here we make sure that non-numeric arrays have the object
# datatype. This works around cases like np.array([1, 2, '3']) where
# numpy converts the integers to strings and returns array of dtype
# '<U21'
if new_v.dtype.kind not in ["u", "i", "f", "O", "M"]:
new_v = np.array(v, dtype="object")
# Set new array to be read-only
# -----------------------------
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)
# v is essentially a scalar and so shouldn't count as an array
if v_numpy.shape == ():
return False
else:
return True # v_numpy.dtype.kind in ["u", "i", "f", "M", "U"]
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__)
# Validators
# ----------
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):
# Note: numpy array was already coerced into read-only form so
# we don't need to copy it here.
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 None through
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
)
# Save params
# -----------
self.values = values
self.array_ok = array_ok
# coerce_number is rarely used and not implemented
self.coerce_number = coerce_number
self.kwargs = kwargs
# Handle regular expressions
# --------------------------
# Compiled regexs
self.val_regexs = []
# regex replacements that run before the matching regex
# So far, this is only used to cast 'x1' -> 'x' for anchor-style
# enumeration properties
self.regex_replacements = []
# Loop over enumeration values
# ----------------------------
# Look for regular expressions
for v in self.values:
if v and isinstance(v, str) and v[0] == "/" and v[-1] == "/" and len(v) > 1:
# String is a regex with leading and trailing '/' character
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):
# Example: regex_str == r"^y([2-9]|[1-9][0-9]+)?$"
#
# When we see a regular expression like the one above, we want to
# build regular expression replacement params that will remove a
# suffix of 1 from the input string ('y1' -> 'y' in this example)
#
# Why?: Regular expressions like this one are used in enumeration
# properties that refer to subplotids (e.g. layout.annotation.xref)
# The regular expressions forbid suffixes of 1, like 'x1'. But we
# want to accept 'x1' and coerce it into 'x'
#
# To be cautious, we only perform this conversion for enumerated
# values that match the anchor-style regex
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):
# Separate regular values from regular expressions
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
# in_values = regex.fullmatch(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 None through
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 None through
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 None through
pass
elif isinstance(v, str):
pass
elif self.chart_studio and isinstance(v, self.chart_studio.grid_objs.Column):
# Convert to id string
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
)
# Handle min
if min is None and max is not None:
# Max was specified, so make min -inf
self.min_val = float("-inf")
else:
self.min_val = min
# Handle max
if max is None and min is not None:
# Min was specified, so make min inf
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 None through
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)
# Check min/max
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):
# Grab up to the first 10 invalid values
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 # Always numeric numpy array
elif self.array_ok and is_simple_array(v):
# Check numeric
invalid_els = [e for e in v if not isinstance(e, numbers.Number)]
if invalid_els:
self.raise_invalid_elements(invalid_els[:10])
# Check min/max
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:
# Check numeric
if not isinstance(v, numbers.Number):
self.raise_invalid_val(v)
# Check min/max
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
)
# Handle min
if min is None and max is not None:
# Max was specified, so make min -inf
self.min_val = -sys.maxsize - 1
else:
self.min_val = min
# Handle max
if max is None and min is not None:
# Min was specified, so make min inf
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 None through
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)
# Check min/max
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):
# Grab up to the first 10 invalid values
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):
# Check integer type
invalid_els = [e for e in v if not isinstance(e, int)]
if invalid_els:
self.raise_invalid_elements(invalid_els[:10])
# Check min/max
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:
# Check int
if not isinstance(v, int):
# don't let int() cast strings to ints
self.raise_invalid_val(v)
# Check min/max
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 None through
pass
elif self.array_ok and is_array(v):
# If strict, make sure all elements are strings.
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")
# If not strict, let numpy cast elements to strings
v = copy_to_readonly_numpy_array(v, kind="U")
# Check no_blank
if self.no_blank:
invalid_els = v[v == ""][:10].tolist()
if invalid_els:
self.raise_invalid_elements(invalid_els)
# Check values
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]
# Check no_blank
if self.no_blank:
invalid_els = [e for e in v if e == ""]
if invalid_els:
self.raise_invalid_elements(invalid_els)
# Check values
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)):
# Convert value to a string
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
# colorscale_path is the path to the colorscale associated with this
# color property, or None if no such colorscale exists. Only colors
# with an associated colorscale may take on numeric values
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 None through
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"]:
# Numbers are allowed and we have an array of numbers.
# All good
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)
# ### Check that elements have valid colors types ###
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:
# Validate scalar color
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:
# If allow_numbers then any number is ok
return v
elif not isinstance(v, str):
# If not allow_numbers then value must be a string
return None
else:
# Remove spaces so regexes don't need to bother with them.
v_normalized = v.replace(" ", "").lower()
# if ColorValidator.re_hex.fullmatch(v_normalized):
if fullmatch(ColorValidator.re_hex, v_normalized):
# valid hex color (e.g. #f34ab3)
return v
elif fullmatch(ColorValidator.re_rgb_etc, v_normalized):
# elif ColorValidator.re_rgb_etc.fullmatch(v_normalized):
# Valid rgb(a), hsl(a), hsv(a) color
# (e.g. rgba(10, 234, 200, 50%)
return v
elif fullmatch(ColorValidator.re_ddk, v_normalized):
# Valid var(--*) DDK theme variable, inspired by CSS syntax
# (e.g. var(--accent) )
# DDK will crawl & eval var(-- colors for Graph theming
return v
elif v_normalized in ColorValidator.named_colors:
# Valid named color (e.g. 'coral')
return v
else:
# Not a valid color
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 None through
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
)
# named colorscales initialized on first use
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:
# Convert to color list
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:
# Convert to list of lists colorscale
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 firset element is a string, treat as colorsequence
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
# Convert to list of lists colorscale
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
# Convert to list of lists
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):
# Return-type must be immutable
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 None through
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 # Always numeric numpy array
# Normalize v onto the interval [-180, 180)
v = (v + 180) % 360 - 180
elif self.array_ok and is_simple_array(v):
# Check numeric
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:
# Normalize v onto the interval [-180, 180)
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:
# e.g. regex == '/^y([2-9]|[1-9][0-9]+)?$/'
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 = re.fullmatch(self.regex, v)
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:
# Remove 1 suffix (e.g. x1 -> x)
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)
# Flags
desc = (
desc
+ (
"""
- Any combination of {flags} joined with '+' characters
(e.g. '{eg_flag}')"""
).format(flags=self.flags, eg_flag="+".join(self.flags[:2]))
)
# Extras
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
# To be generous we accept flags separated on plus ('+'),
# or comma (',') and we accept whitespace around the flags
split_vals = [e.strip() for e in re.split("[,+]", v)]
# Are all flags valid names?
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 None through
pass
elif self.array_ok and is_array(v):
# Coerce individual strings
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 None through
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
# Instantiate validators for each info array element
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):
# Cases
# 1) self.items is array, self.dimensions is 1
# a) free_length=True
# b) free_length=False
# 2) self.items is array, self.dimensions is 2
# (requires free_length=True)
# 3) self.items is scalar (requires free_length=True)
# a) dimensions=1
# b) dimensions=2
#
# dimensions can be set to '1-2' to indicate the both are accepted
#
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):
# ### Case 1 ###
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
)
)
# ### Case 2 ###
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):
# Update name for 2d
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:
# ### Case 3 ###
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"] # type: str
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:
# Pass None through
return None
elif not is_array(v):
self.raise_invalid_val(v)
# Save off original v value to use in error reporting
orig_v = v
# Convert everything into nested lists
# This way we don't need to worry about nested numpy arrays
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):
# e.g. 2D list as parcoords.dimensions.constraintrange
# check that all items are there for each nested element
for i, row in enumerate(v):
# Check row length
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:
# e.g. 2D list as layout.grid.subplots
# check that all elements match individual validator
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:
# e.g. 1D list passed as layout.grid.subplots
self.raise_invalid_val(orig_v[0], [0])
elif not is_array(self.items):
# e.g. 1D list passed as layout.grid.xaxes
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):
# e.g. 3 element list as layout.xaxis.range
self.raise_invalid_val(orig_v)
elif self.free_length and len(v) > len(self.item_validators):
# e.g. 4 element list as layout.updatemenu.button.args
self.raise_invalid_val(orig_v)
else:
# We have a 1D array of the correct length
for i, (el, validator) in enumerate(zip(v, self.item_validators)):
# Validate coerce elements
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])
):
# 2D case
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:
# 1D case
v = copy.copy(v)
# Call present on each of the item validators
for i, (el, validator) in enumerate(zip(v, self.item_validators)):
# Validate coerce elements
v[i] = validator.present(el)
# Return tuple form of
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):
# Add regex to handle dash length lists
dash_list_regex = r"/^\d+(\.\d+)?(px|%)?((,|\s)\s*\d+(\.\d+)?(px|%)?)*$/"
values = values + [dash_list_regex]
# Call EnumeratedValidator superclass
super(DashValidator, self).__init__(
plotly_name=plotly_name, parent_name=parent_name, values=values, **kwargs
)
def description(self):
# Separate regular values from regular expressions
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):
# Future possibilities:
# - Detect filesystem system paths and convert to URI
# - Validate either url or data uri
pass
elif self._PIL and isinstance(v, self._PIL.Image.Image):
# Convert PIL image to png data uri string
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
)
# Save element class string
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"]:
# Special case. There are no graph_objs.frame.Data or
# graph_objs.frame.Layout classes. These are remapped to
# graph_objs.Data and graph_objs.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":
# Remap template's layout to regular layout
module_str = "plotly.graph_objs"
elif "layout.template.data" in parent_name:
# Remap template's traces to regular traces
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):
# Copy object
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 compound object as-is
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
)
# Save element class string
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 compound object as tuple
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):
# Import trace classes
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
# Import Histogram2dcontour, this is the deprecated name of the
# Histogram2dContour trace.
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:
# Treat as scatter trace
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:
# Restore type in v_el
v_el["type"] = trace_type
else:
if skip_invalid:
# Add empty scatter trace
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)
# Set new UIDs
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:
# Check if v is a template identifier
# (could be any hashable object)
if v in pio.templates:
return copy.deepcopy(pio.templates[v])
# Otherwise, if v is a string, check to see if it consists of
# multiple template names joined on '+' characters
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:
# v is un-hashable
pass
# Check for empty template
if v == {} or isinstance(v, self.data_class) and v.to_plotly_json() == {}:
# Replace empty template with {'data': {'scatter': [{}]}} so that we can
# tell the difference between an un-initialized template and a template
# explicitly set to empty.
return self.data_class(data_scatter=[{}])
return super(BaseTemplateValidator, self).validate_coerce(
v, skip_invalid=skip_invalid
)