Skip to content

Commit dad2520

Browse files
authored
Merge pull request #5465 from my-tien/legend-arrayok
Add array_ok to SubplotidValidator, used in pie.legend
2 parents 5a0440c + b587428 commit dad2520

File tree

2 files changed

+105
-21
lines changed

2 files changed

+105
-21
lines changed

_plotly_utils/basevalidators.py

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,12 +1746,15 @@ class SubplotidValidator(BaseValidator):
17461746
"dflt"
17471747
],
17481748
"otherOpts": [
1749+
"arrayOk",
17491750
"regex"
17501751
]
17511752
}
17521753
"""
17531754

1754-
def __init__(self, plotly_name, parent_name, dflt=None, regex=None, **kwargs):
1755+
def __init__(
1756+
self, plotly_name, parent_name, dflt=None, regex=None, array_ok=False, **kwargs
1757+
):
17551758
if dflt is None and regex is None:
17561759
raise ValueError("One or both of regex and deflt must be specified")
17571760

@@ -1766,40 +1769,55 @@ def __init__(self, plotly_name, parent_name, dflt=None, regex=None, **kwargs):
17661769
self.base = re.match(r"/\^(\w+)", regex).group(1)
17671770

17681771
self.regex = self.base + r"(\d*)"
1772+
self.array_ok = array_ok
17691773

17701774
def description(self):
17711775
desc = """\
17721776
The '{plotly_name}' property is an identifier of a particular
1773-
subplot, of type '{base}', that may be specified as the string '{base}'
1774-
optionally followed by an integer >= 1
1775-
(e.g. '{base}', '{base}1', '{base}2', '{base}3', etc.)
1776-
""".format(plotly_name=self.plotly_name, base=self.base)
1777+
subplot, of type '{base}', that may be specified as:
1778+
- the string '{base}' optionally followed by an integer >= 1
1779+
(e.g. '{base}', '{base}1', '{base}2', '{base}3', etc.)""".format(
1780+
plotly_name=self.plotly_name, base=self.base
1781+
)
1782+
if self.array_ok:
1783+
desc += """
1784+
- A tuple or list of the above"""
17771785
return desc
17781786

17791787
def validate_coerce(self, v):
1780-
if v is None:
1781-
pass
1782-
elif not isinstance(v, str):
1783-
self.raise_invalid_val(v)
1784-
else:
1785-
# match = re.fullmatch(self.regex, v)
1786-
match = fullmatch(self.regex, v)
1788+
def coerce(value):
1789+
if not isinstance(value, str):
1790+
return value, False
1791+
match = fullmatch(self.regex, value)
17871792
if not match:
1788-
is_valid = False
1793+
return value, False
17891794
else:
17901795
digit_str = match.group(1)
17911796
if len(digit_str) > 0 and int(digit_str) == 0:
1792-
is_valid = False
1797+
return value, False
17931798
elif len(digit_str) > 0 and int(digit_str) == 1:
1794-
# Remove 1 suffix (e.g. x1 -> x)
1795-
v = self.base
1796-
is_valid = True
1799+
return self.base, True
17971800
else:
1798-
is_valid = True
1801+
return value, True
17991802

1800-
if not is_valid:
1801-
self.raise_invalid_val(v)
1802-
return v
1803+
if v is None:
1804+
pass
1805+
elif self.array_ok and is_simple_array(v):
1806+
values = []
1807+
invalid_els = []
1808+
for e in v:
1809+
coerced_e, success = coerce(e)
1810+
values.append(coerced_e)
1811+
if not success:
1812+
invalid_els.append(coerced_e)
1813+
if len(invalid_els) > 0:
1814+
self.raise_invalid_elements(invalid_els[:10])
1815+
return values
1816+
else:
1817+
v, success = coerce(v)
1818+
if not success:
1819+
self.raise_invalid_val(self.base)
1820+
return v
18031821

18041822

18051823
class FlaglistValidator(BaseValidator):

tests/test_plotly_utils/validators/test_subplotid_validator.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,29 @@ def validator():
1111
return SubplotidValidator("prop", "parent", dflt="geo")
1212

1313

14+
@pytest.fixture()
15+
def validator_aok():
16+
return SubplotidValidator("prop", "parent", dflt="legend", array_ok=True)
17+
18+
1419
# Tests
1520

21+
# Array not ok (default)
1622

1723
# Acceptance
24+
25+
1826
@pytest.mark.parametrize("val", ["geo"] + ["geo%d" % i for i in range(2, 10)])
1927
def test_acceptance(val, validator):
2028
assert validator.validate_coerce(val) == val
2129

2230

31+
# Coercion from {base}1 to {base}
32+
def test_coerce(validator):
33+
v = validator.validate_coerce("geo1")
34+
assert ("geo") == v
35+
36+
2337
# Rejection by type
2438
@pytest.mark.parametrize("val", [23, [], {}, set(), np_inf(), np_nan()])
2539
def test_rejection_type(val, validator):
@@ -43,3 +57,55 @@ def test_rejection_value(val, validator):
4357
validator.validate_coerce(val)
4458

4559
assert "Invalid value" in str(validation_failure.value)
60+
61+
62+
# Array ok
63+
64+
# Acceptance
65+
66+
67+
@pytest.mark.parametrize(
68+
"val",
69+
["legend2", ["legend", "legend2"], ["legend", "legend2"]],
70+
)
71+
def test_acceptance_aok(val, validator_aok):
72+
v = validator_aok.validate_coerce(val)
73+
if isinstance(val, tuple):
74+
assert val == tuple(v)
75+
else:
76+
assert val == v
77+
78+
79+
# Coercion from {base}1 to {base}
80+
def test_coerce_aok(validator_aok):
81+
v = validator_aok.validate_coerce(("legend1", "legend2"))
82+
assert ("legend", "legend2") == tuple(v)
83+
84+
85+
# Rejection by type
86+
@pytest.mark.parametrize("val", [23, [2, 3], {}, set(), np_inf(), np_nan()])
87+
def test_rejection_type_aok(val, validator_aok):
88+
with pytest.raises(ValueError) as validation_failure:
89+
validator_aok.validate_coerce(val)
90+
91+
failure_msg = str(validation_failure.value)
92+
assert "Invalid value" in failure_msg or "Invalid elements" in failure_msg
93+
94+
95+
# Rejection by value
96+
@pytest.mark.parametrize(
97+
"val",
98+
[
99+
"", # Cannot be empty
100+
"bogus", # Must begin with 'geo'
101+
"legend0", # If followed by a number the number must be > 1,
102+
["", "legend"],
103+
("bogus", "legend2"),
104+
],
105+
)
106+
def test_rejection_value_aok(val, validator_aok):
107+
with pytest.raises(ValueError) as validation_failure:
108+
validator_aok.validate_coerce(val)
109+
110+
failure_msg = str(validation_failure.value)
111+
assert "Invalid value" in failure_msg or "Invalid elements" in failure_msg

0 commit comments

Comments
 (0)