Skip to content

Commit 7ddd1c9

Browse files
authored
Introduce skip_invalid argument to figure (#1162)
* Add skip_invalid kwarg to graph_objs constructors. When set to True (Default is False) all invalid properties (invalid by name or by value) will be skipped an no exception will be raised due to schema violations. The option applies recursively to all child objects, but it only applies within the scope of the constructor. * Add skip_invalid kwarg to Figure and FigureWidget. Added docstring documentatin of skip_invalid, and added new tests. When set to True (Default is False) all invalid properties (invalid by name or by value) will be skipped an no exception will be raised due to schema violations. The option applies recursively to all child objects, but it only applies within the scope of the constructor.
1 parent 9dbb5cd commit 7ddd1c9

File tree

585 files changed

+4913
-43
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

585 files changed

+4913
-43
lines changed

Diff for: _plotly_utils/basevalidators.py

+42-17
Original file line numberDiff line numberDiff line change
@@ -1890,18 +1890,21 @@ def description(self):
18901890

18911891
return desc
18921892

1893-
def validate_coerce(self, v):
1893+
def validate_coerce(self, v, skip_invalid=False):
18941894
if v is None:
18951895
v = self.data_class()
18961896

18971897
elif isinstance(v, dict):
1898-
v = self.data_class(**v)
1898+
v = self.data_class(skip_invalid=skip_invalid, **v)
18991899

19001900
elif isinstance(v, self.data_class):
19011901
# Copy object
1902-
v = self.data_class(**v.to_plotly_json())
1902+
v = self.data_class(v)
19031903
else:
1904-
self.raise_invalid_val(v)
1904+
if skip_invalid:
1905+
v = self.data_class()
1906+
else:
1907+
self.raise_invalid_val(v)
19051908

19061909
v._plotly_name = self.plotly_name
19071910
return v
@@ -1948,7 +1951,7 @@ def data_class(self):
19481951

19491952
return self._data_class
19501953

1951-
def validate_coerce(self, v):
1954+
def validate_coerce(self, v, skip_invalid=False):
19521955

19531956
if v is None:
19541957
v = []
@@ -1958,19 +1961,26 @@ def validate_coerce(self, v):
19581961
invalid_els = []
19591962
for v_el in v:
19601963
if isinstance(v_el, self.data_class):
1961-
res.append(v_el)
1964+
res.append(self.data_class(v_el))
19621965
elif isinstance(v_el, dict):
1963-
res.append(self.data_class(**v_el))
1966+
res.append(self.data_class(skip_invalid=skip_invalid,
1967+
**v_el))
19641968
else:
1965-
res.append(None)
1966-
invalid_els.append(v_el)
1969+
if skip_invalid:
1970+
res.append(self.data_class())
1971+
else:
1972+
res.append(None)
1973+
invalid_els.append(v_el)
19671974

19681975
if invalid_els:
19691976
self.raise_invalid_elements(invalid_els)
19701977

19711978
v = to_scalar_or_list(res)
19721979
else:
1973-
self.raise_invalid_val(v)
1980+
if skip_invalid:
1981+
v = []
1982+
else:
1983+
self.raise_invalid_val(v)
19741984

19751985
return v
19761986

@@ -2032,7 +2042,7 @@ def class_map(self):
20322042

20332043
return self._class_map
20342044

2035-
def validate_coerce(self, v):
2045+
def validate_coerce(self, v, skip_invalid=False):
20362046

20372047
# Import Histogram2dcontour, this is the deprecated name of the
20382048
# Histogram2dContour trace.
@@ -2062,14 +2072,26 @@ def validate_coerce(self, v):
20622072
trace_type = 'scatter'
20632073

20642074
if trace_type not in self.class_map:
2065-
res.append(None)
2066-
invalid_els.append(v_el)
2075+
if skip_invalid:
2076+
# Treat as scatter trace
2077+
trace = self.class_map['scatter'](
2078+
skip_invalid=skip_invalid, **v_copy)
2079+
res.append(trace)
2080+
else:
2081+
res.append(None)
2082+
invalid_els.append(v_el)
20672083
else:
2068-
trace = self.class_map[trace_type](**v_copy)
2084+
trace = self.class_map[trace_type](
2085+
skip_invalid=skip_invalid, **v_copy)
20692086
res.append(trace)
20702087
else:
2071-
res.append(None)
2072-
invalid_els.append(v_el)
2088+
if skip_invalid:
2089+
# Add empty scatter trace
2090+
trace = self.class_map['scatter']()
2091+
res.append(trace)
2092+
else:
2093+
res.append(None)
2094+
invalid_els.append(v_el)
20732095

20742096
if invalid_els:
20752097
self.raise_invalid_elements(invalid_els)
@@ -2082,6 +2104,9 @@ def validate_coerce(self, v):
20822104
trace.uid = str(uuid.uuid1())
20832105

20842106
else:
2085-
self.raise_invalid_val(v)
2107+
if skip_invalid:
2108+
v = []
2109+
else:
2110+
self.raise_invalid_val(v)
20862111

20872112
return v

Diff for: _plotly_utils/tests/validators/test_basetraces_validator.py

+39-2
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,27 @@
22
from _plotly_utils.basevalidators import BaseDataValidator
33
from plotly.graph_objs import Scatter, Bar, Box
44

5+
56
# Fixtures
67
# --------
78
@pytest.fixture()
89
def validator():
910
return BaseDataValidator(class_strs_map={'scatter': 'Scatter',
10-
'bar': 'Bar',
11-
'box': 'Box'},
11+
'bar': 'Bar',
12+
'box': 'Box'},
1213
plotly_name='prop',
1314
parent_name='parent',
1415
set_uid=True)
1516

17+
@pytest.fixture()
18+
def validator_nouid():
19+
return BaseDataValidator(class_strs_map={'scatter': 'Scatter',
20+
'bar': 'Bar',
21+
'box': 'Box'},
22+
plotly_name='prop',
23+
parent_name='parent',
24+
set_uid=False)
25+
1626

1727
# Tests
1828
# -----
@@ -104,3 +114,30 @@ def test_rejection_element_tracetype(validator):
104114
validator.validate_coerce(val)
105115

106116
assert "Invalid element(s)" in str(validation_failure.value)
117+
118+
119+
def test_skip_invalid(validator_nouid):
120+
val = (dict(type='scatter',
121+
mode='lines',
122+
marker={'color': 'green',
123+
'bogus': 23},
124+
line='bad_value'),
125+
dict(type='box',
126+
fillcolor='yellow',
127+
bogus=111),
128+
dict(type='bogus',
129+
mode='lines+markers',
130+
x=[2, 1, 3]))
131+
132+
expected = [dict(type='scatter',
133+
mode='lines',
134+
marker={'color': 'green'}),
135+
dict(type='box',
136+
fillcolor='yellow'),
137+
dict(type='scatter',
138+
mode='lines+markers',
139+
x=[2, 1, 3])]
140+
141+
res = validator_nouid.validate_coerce(val, skip_invalid=True)
142+
143+
assert [el.to_plotly_json() for el in res] == expected

Diff for: _plotly_utils/tests/validators/test_compound_validator.py

+40
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,43 @@ def test_rejection_value(validator):
5959
assert ("Invalid property specified for object of type "
6060
"plotly.graph_objs.scatter.Marker: 'bogus'" in
6161
str(validation_failure.value))
62+
63+
64+
def test_skip_invalid(validator):
65+
val = dict(
66+
color='green',
67+
size=10,
68+
bogus=99, # Bad property name
69+
colorbar={'bgcolor': 'blue',
70+
'bogus_inner': 23 # Bad nested property name
71+
},
72+
opacity='bogus value' # Bad value for valid property
73+
)
74+
75+
expected = dict(
76+
color='green',
77+
size=10,
78+
colorbar={'bgcolor': 'blue'})
79+
80+
res = validator.validate_coerce(val, skip_invalid=True)
81+
assert res.to_plotly_json() == expected
82+
83+
84+
def test_skip_invalid_empty_object(validator):
85+
val = dict(
86+
color='green',
87+
size=10,
88+
colorbar={'bgcolor': 'bad_color', # Bad value for valid property
89+
'bogus_inner': 23 # Bad nested property name
90+
},
91+
opacity=0.5 # Bad value for valid property
92+
)
93+
94+
# The colorbar property should be absent, not None or {}
95+
expected = dict(
96+
color='green',
97+
size=10,
98+
opacity=0.5)
99+
100+
res = validator.validate_coerce(val, skip_invalid=True)
101+
assert res.to_plotly_json() == expected

Diff for: _plotly_utils/tests/validators/test_compoundarray_validator.py

+17
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,20 @@ def test_rejection_value(validator):
8989
assert ("Invalid property specified for object of type "
9090
"plotly.graph_objs.layout.Image" in
9191
str(validation_failure.value))
92+
93+
94+
def test_skip_invalid(validator):
95+
val = [dict(opacity='bad_opacity',
96+
x=23,
97+
sizex=120),
98+
dict(x=99,
99+
bogus={'a': 23},
100+
sizey=300)]
101+
102+
expected = [dict(x=23,
103+
sizex=120),
104+
dict(x=99,
105+
sizey=300)]
106+
107+
res = validator.validate_coerce(val, skip_invalid=True)
108+
assert [el.to_plotly_json() for el in res] == expected

Diff for: codegen/datatypes.py

+8
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ def __init__(self""")
221221
constructor must be a dict or
222222
an instance of {class_name}\"\"\")
223223
224+
# Handle skip_invalid
225+
# -------------------
226+
self._skip_invalid = kwargs.pop('skip_invalid', False)
227+
224228
# Import validators
225229
# -----------------
226230
from plotly.validators{node.parent_dotpath_str} import (
@@ -266,6 +270,10 @@ def __init__(self""")
266270
# Process unknown kwargs
267271
# ----------------------
268272
self._process_kwargs(**dict(arg, **kwargs))
273+
274+
# Reset skip_invalid
275+
# ------------------
276+
self._skip_invalid = False
269277
""")
270278

271279
# Return source string

Diff for: codegen/figure.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,35 @@ class {fig_classname}({base_classname}):\n""")
6767
frames_description = reindent_validator_description(frame_validator, 8)
6868

6969
buffer.write(f"""
70-
def __init__(self, data=None, layout=None, frames=None):
70+
def __init__(self, data=None, layout=None,
71+
frames=None, skip_invalid=False):
7172
\"\"\"
7273
Create a new {fig_classname} instance
7374
7475
Parameters
7576
----------
7677
data
7778
{data_description}
79+
7880
layout
7981
{layout_description}
82+
8083
frames
8184
{frames_description}
85+
86+
skip_invalid: bool
87+
If True, invalid properties in the figure specification will be
88+
skipped silently. If False (default) invalid properties in the
89+
figure specification will result in a ValueError
90+
91+
Raises
92+
------
93+
ValueError
94+
if a property in the specification of data, layout, or frames
95+
is invalid AND skip_invalid is False
8296
\"\"\"
83-
super({fig_classname} ,self).__init__(data, layout, frames)
97+
super({fig_classname} ,self).__init__(data, layout,
98+
frames, skip_invalid)
8499
""")
85100

86101
# ### add_trace methods for each trace type ###

0 commit comments

Comments
 (0)