Skip to content

Commit b5e4131

Browse files
author
Jon M. Mease
committed
Making numpy an optional dependency
Lists/tuples assigned to array properties are no longer coerced into arrays. Instead they are stored internally as lists and converted to tuples on property access (+1 squashed commit) Squashed commits:
1 parent a62e412 commit b5e4131

16 files changed

+515
-252
lines changed

Diff for: _plotly_utils/basevalidators.py

+261-118
Large diffs are not rendered by default.

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

+19-4
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,25 @@ def test_acceptance(val, validator: AnyValidator):
2727

2828
# ### Acceptance of arrays ###
2929
@pytest.mark.parametrize('val', [
30-
[], np.array([]), ['Hello', 'World'], [np.pi, np.e, {}]
30+
23,
31+
'Hello!',
32+
[],
33+
(),
34+
np.array([]),
35+
('Hello', 'World'),
36+
['Hello', 'World'],
37+
[np.pi, np.e, {}]
3138
])
3239
def test_acceptance_array(val, validator_aok: AnyValidator):
3340
coerce_val = validator_aok.validate_coerce(val)
34-
assert isinstance(coerce_val, np.ndarray)
35-
assert coerce_val.dtype == 'object'
36-
assert np.array_equal(coerce_val, val)
41+
if isinstance(val, np.ndarray):
42+
assert isinstance(coerce_val, np.ndarray)
43+
assert coerce_val.dtype == 'object'
44+
assert np.array_equal(coerce_val, val)
45+
elif isinstance(val, (list, tuple)):
46+
assert coerce_val == list(val)
47+
assert validator_aok.present(coerce_val) == tuple(val)
48+
else:
49+
assert coerce_val == val
50+
assert validator_aok.present(coerce_val) == val
51+

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

+27-20
Original file line numberDiff line numberDiff line change
@@ -18,46 +18,53 @@ def validator():
1818
def test_acceptance(validator: BaseDataValidator):
1919
val = [Scatter(mode='lines'), Box(fillcolor='yellow')]
2020
res = validator.validate_coerce(val)
21+
res_present = validator.present(res)
2122

22-
assert isinstance(res, tuple)
23-
assert isinstance(res[0], Scatter)
24-
assert res[0].type == 'scatter'
25-
assert res[0].mode == 'lines'
23+
assert isinstance(res, list)
24+
assert isinstance(res_present, tuple)
2625

27-
assert isinstance(res[1], Box)
28-
assert res[1].type == 'box'
29-
assert res[1].fillcolor == 'yellow'
26+
assert isinstance(res_present[0], Scatter)
27+
assert res_present[0].type == 'scatter'
28+
assert res_present[0].mode == 'lines'
29+
30+
assert isinstance(res_present[1], Box)
31+
assert res_present[1].type == 'box'
32+
assert res_present[1].fillcolor == 'yellow'
3033

3134
# Make sure UIDs are actually unique
32-
assert res[0].uid != res[1].uid
35+
assert res_present[0].uid != res_present[1].uid
3336

3437

3538
def test_acceptance_dict(validator: BaseDataValidator):
3639
val = (dict(type='scatter', mode='lines'),
3740
dict(type='box', fillcolor='yellow'))
3841
res = validator.validate_coerce(val)
42+
res_present = validator.present(res)
3943

40-
assert isinstance(res, tuple)
41-
assert isinstance(res[0], Scatter)
42-
assert res[0].type == 'scatter'
43-
assert res[0].mode == 'lines'
44+
assert isinstance(res, list)
45+
assert isinstance(res_present, tuple)
46+
assert isinstance(res_present[0], Scatter)
47+
assert res_present[0].type == 'scatter'
48+
assert res_present[0].mode == 'lines'
4449

45-
assert isinstance(res[1], Box)
46-
assert res[1].type == 'box'
47-
assert res[1].fillcolor == 'yellow'
50+
assert isinstance(res_present[1], Box)
51+
assert res_present[1].type == 'box'
52+
assert res_present[1].fillcolor == 'yellow'
4853

4954
# Make sure UIDs are actually unique
50-
assert res[0].uid != res[1].uid
55+
assert res_present[0].uid != res_present[1].uid
5156

5257

5358
def test_default_is_scatter(validator: BaseDataValidator):
5459
val = [dict(mode='lines')]
5560
res = validator.validate_coerce(val)
61+
res_present = validator.present(res)
5662

57-
assert isinstance(res, tuple)
58-
assert isinstance(res[0], Scatter)
59-
assert res[0].type == 'scatter'
60-
assert res[0].mode == 'lines'
63+
assert isinstance(res, list)
64+
assert isinstance(res_present, tuple)
65+
assert isinstance(res_present[0], Scatter)
66+
assert res_present[0].type == 'scatter'
67+
assert res_present[0].mode == 'lines'
6168

6269

6370
def test_rejection_type(validator: BaseDataValidator):

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,17 @@ def test_rejection_colorscale(val, validator_colorscale: ColorValidator):
9090
@pytest.mark.parametrize('val',
9191
['blue',
9292
['red', 'rgb(255, 0, 0)'],
93+
np.array(['red', 'rgb(255, 0, 0)']),
9394
['hsl(0, 100%, 50%)', 'hsla(0, 100%, 50%, 100%)', 'hsv(0, 100%, 100%)'],
95+
np.array(['hsl(0, 100%, 50%)', 'hsla(0, 100%, 50%, 100%)', 'hsv(0, 100%, 100%)']),
9496
['hsva(0, 100%, 100%, 50%)']])
9597
def test_acceptance_aok(val, validator_aok: ColorValidator):
9698
coerce_val = validator_aok.validate_coerce(val)
9799

98-
if isinstance(val, (list, np.ndarray)):
100+
if isinstance(val, np.ndarray):
99101
assert np.array_equal(coerce_val, val)
102+
elif isinstance(val, list):
103+
assert validator_aok.present(coerce_val) == tuple(val)
100104
else:
101105
assert coerce_val == val
102106

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import pytest
2+
import numpy as np
3+
4+
from _plotly_utils.basevalidators import ColorlistValidator
5+
6+
7+
# Fixtures
8+
# --------
9+
@pytest.fixture()
10+
def validator():
11+
return ColorlistValidator('prop', 'parent')
12+
13+
14+
# Rejection
15+
# ---------
16+
@pytest.mark.parametrize('val',
17+
[set(), 23, 0.5, {}, 'redd'])
18+
def test_rejection_value(validator, val):
19+
with pytest.raises(ValueError) as validation_failure:
20+
validator.validate_coerce(val)
21+
22+
assert 'Invalid value' in str(validation_failure.value)
23+
24+
25+
@pytest.mark.parametrize('val',
26+
[[set()], [23, 0.5], [{}, 'red'],
27+
['blue', 'redd']])
28+
def test_rejection_element(validator, val):
29+
with pytest.raises(ValueError) as validation_failure:
30+
validator.validate_coerce(val)
31+
32+
assert 'Invalid element(s)' in str(validation_failure.value)
33+
34+
35+
# Acceptance
36+
# ----------
37+
@pytest.mark.parametrize('val',
38+
[['blue'],
39+
['red', 'rgb(255, 0, 0)'],
40+
np.array(['red', 'rgb(255, 0, 0)']),
41+
['hsl(0, 100%, 50%)', 'hsla(0, 100%, 50%, 100%)', 'hsv(0, 100%, 100%)'],
42+
np.array(['hsl(0, 100%, 50%)', 'hsla(0, 100%, 50%, 100%)', 'hsv(0, 100%, 100%)']),
43+
['hsva(0, 100%, 100%, 50%)']])
44+
def test_acceptance_aok(val, validator):
45+
coerce_val = validator.validate_coerce(val)
46+
assert isinstance(coerce_val, list)
47+
assert validator.present(coerce_val) == tuple(val)

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

+12-18
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ def named_colorscale(request):
2020
# -----
2121
# ### Acceptance by name ###
2222
def test_acceptance_named(named_colorscale, validator: ColorscaleValidator):
23+
# As-is
2324
assert validator.validate_coerce(named_colorscale) == named_colorscale
2425

26+
# Uppercase
27+
assert (validator.validate_coerce(named_colorscale.upper()) ==
28+
named_colorscale.upper())
2529

2630
# ### Acceptance as array ###
2731
@pytest.mark.parametrize('val', [
@@ -32,31 +36,21 @@ def test_acceptance_named(named_colorscale, validator: ColorscaleValidator):
3236
def test_acceptance_array(val, validator: ColorscaleValidator):
3337
assert validator.validate_coerce(val) == val
3438

35-
36-
# ### Coercion of scale names ###
37-
def test_coercion_named(named_colorscale, validator: ColorscaleValidator):
38-
# As is
39-
assert validator.validate_coerce(named_colorscale) == named_colorscale
40-
41-
# Uppercase
42-
assert validator.validate_coerce(
43-
named_colorscale.upper()) == named_colorscale
44-
45-
# Lowercase
46-
assert validator.validate_coerce(
47-
named_colorscale.lower()) == named_colorscale
48-
49-
5039
# ### Coercion as array ###
5140
@pytest.mark.parametrize('val', [
5241
([0, 'red'],),
5342
[(0.1, 'rgb(255, 0, 0)'), (0.3, 'GREEN')],
5443
(np.array([0, 'Purple'], dtype='object'), (0.2, 'yellow'), (1.0, 'RGBA(255,0,0,100)')),
5544
])
5645
def test_acceptance_array(val, validator: ColorscaleValidator):
57-
# Compute expected (tuple of tuples where color is lowercase with no spaces)
58-
expected = tuple([tuple([e[0], e[1]]) for e in val])
59-
assert validator.validate_coerce(val) == expected
46+
# Compute expected (tuple of tuples where color is
47+
# lowercase with no spaces)
48+
expected = [[e[0], e[1]] for e in val]
49+
coerce_val = validator.validate_coerce(val)
50+
assert coerce_val == expected
51+
52+
expected_present = tuple([tuple(e) for e in expected])
53+
assert validator.present(coerce_val) == expected_present
6054

6155

6256
# ### Rejection by type ###

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

+28-23
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,49 @@ def validator():
1616
def test_acceptance(validator: CompoundArrayValidator):
1717
val = [Image(opacity=0.5, sizex=120), Image(x=99)]
1818
res = validator.validate_coerce(val)
19+
res_present = validator.present(res)
20+
assert isinstance(res, list)
21+
assert isinstance(res_present, tuple)
22+
assert isinstance(res_present[0], Image)
23+
assert res_present[0].opacity == 0.5
24+
assert res_present[0].sizex == 120
25+
assert res_present[0].x is None
1926

20-
assert isinstance(res, tuple)
21-
assert isinstance(res[0], Image)
22-
assert res[0].opacity == 0.5
23-
assert res[0].sizex == 120
24-
assert res[0].x is None
25-
26-
assert isinstance(res[1], Image)
27-
assert res[1].opacity is None
28-
assert res[1].sizex is None
29-
assert res[1].x == 99
27+
assert isinstance(res_present[1], Image)
28+
assert res_present[1].opacity is None
29+
assert res_present[1].sizex is None
30+
assert res_present[1].x == 99
3031

3132

3233
def test_acceptance_empty(validator: CompoundArrayValidator):
3334
val = [{}]
3435
res = validator.validate_coerce(val)
36+
res_present = validator.present(res)
3537

36-
assert isinstance(res, tuple)
37-
assert isinstance(res[0], Image)
38-
assert res[0].opacity is None
39-
assert res[0].sizex is None
40-
assert res[0].x is None
38+
assert isinstance(res, list)
39+
assert isinstance(res_present, tuple)
40+
assert isinstance(res_present[0], Image)
41+
assert res_present[0].opacity is None
42+
assert res_present[0].sizex is None
43+
assert res_present[0].x is None
4144

4245

4346
def test_acceptance_dict(validator: CompoundArrayValidator):
4447
val = [dict(opacity=0.5, sizex=120), dict(x=99)]
4548
res = validator.validate_coerce(val)
49+
res_present = validator.present(res)
4650

47-
assert isinstance(res, tuple)
48-
assert isinstance(res[0], Image)
49-
assert res[0].opacity == 0.5
50-
assert res[0].sizex == 120
51-
assert res[0].x is None
51+
assert isinstance(res, list)
52+
assert isinstance(res_present, tuple)
53+
assert isinstance(res_present[0], Image)
54+
assert res_present[0].opacity == 0.5
55+
assert res_present[0].sizex == 120
56+
assert res_present[0].x is None
5257

5358
assert isinstance(res[1], Image)
54-
assert res[1].opacity is None
55-
assert res[1].sizex is None
56-
assert res[1].x == 99
59+
assert res_present[1].opacity is None
60+
assert res_present[1].sizex is None
61+
assert res_present[1].x == 99
5762

5863

5964
def test_rejection_type(validator: CompoundArrayValidator):

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

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
from _plotly_utils.basevalidators import DataArrayValidator
33
import numpy as np
4-
4+
import pandas as pd
55

66
# Fixtures
77
# --------
@@ -14,17 +14,26 @@ def validator():
1414
# -----
1515
# ### Acceptance ###
1616
@pytest.mark.parametrize('val', [
17-
[], [1], np.array([2, 3, 4]), [''], (), ('Hello, ', 'world!')
17+
[], [1], [''], (), ('Hello, ', 'world!'), ['A', 1, 'B', 0, 'C']
18+
])
19+
def test_validator_acceptance_simple(val, validator: DataArrayValidator):
20+
coerce_val = validator.validate_coerce(val)
21+
assert isinstance(coerce_val, list)
22+
assert validator.present(coerce_val) == tuple(val)
23+
24+
25+
@pytest.mark.parametrize('val', [
26+
np.array([2, 3, 4]), pd.Series(['a', 'b', 'c'])
1827
])
19-
def test_validator_acceptance(val, validator: DataArrayValidator):
28+
def test_validator_acceptance_homogeneous(val, validator: DataArrayValidator):
2029
coerce_val = validator.validate_coerce(val)
2130
assert isinstance(coerce_val, np.ndarray)
22-
assert np.array_equal(coerce_val, val)
31+
assert np.array_equal(validator.present(coerce_val), val)
2332

2433

2534
# ### Rejection ###
2635
@pytest.mark.parametrize('val', [
27-
'Hello', 23, set(), {},
36+
'Hello', 23, set(), {}, np.array([[1, 2, 3], [4, 5, 6]])
2837
])
2938
def test_rejection(val, validator: DataArrayValidator):
3039
with pytest.raises(ValueError) as validation_failure:

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

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
import numpy as np
3+
import pandas as pd
34
from _plotly_utils.basevalidators import EnumeratedValidator
45

56

@@ -111,14 +112,19 @@ def test_rejection_by_element_aok(val, validator_aok):
111112
# ### Acceptance ###
112113
@pytest.mark.parametrize('val',
113114
['foo', 'bar12', 'bar21',
114-
[], ['bar12'], ['foo', 'bar012', 'baz']])
115+
[], ['bar12'], ('foo', 'bar012', 'baz'),
116+
np.array([]),
117+
np.array(['bar12']),
118+
np.array(['foo', 'bar012', 'baz'])])
115119
def test_acceptance_aok(val, validator_aok_re):
116120
# Values should be accepted and returned unchanged
117121
coerce_val = validator_aok_re.validate_coerce(val)
118-
if isinstance(val, (list, np.ndarray)):
122+
if isinstance(val, (np.ndarray, pd.Series)):
119123
assert np.array_equal(coerce_val, np.array(val, dtype=coerce_val.dtype))
124+
elif isinstance(val, (list, tuple)):
125+
assert validator_aok_re.present(coerce_val) == tuple(val)
120126
else:
121-
assert coerce_val == val
127+
assert validator_aok_re.present(coerce_val) == val
122128

123129

124130
# ### Reject by elements ###

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

+13-7
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,22 @@ def test_acceptance_aok_list_flaglist(val, validator_extra_aok: FlaglistValidato
136136

137137

138138
# ### Coercion ###
139-
@pytest.mark.parametrize('in_val,coerce_val',
139+
@pytest.mark.parametrize('in_val,expected',
140140
[([' lines ', ' lines + markers ', 'lines ,markers'],
141-
np.array(['lines', 'lines+markers', 'lines+markers'], dtype='unicode')
142-
),
141+
['lines', 'lines+markers', 'lines+markers']),
143142
(np.array(['text +lines']),
144-
np.array(['text+lines'], dtype='unicode')
145-
)
143+
np.array(['text+lines'], dtype='unicode'))
146144
])
147-
def test_coercion_aok(in_val, coerce_val, validator_extra_aok):
148-
assert np.array_equal(validator_extra_aok.validate_coerce(in_val), coerce_val)
145+
def test_coercion_aok(in_val, expected, validator_extra_aok):
146+
coerce_val = validator_extra_aok.validate_coerce(in_val)
147+
if isinstance(in_val, (list, tuple)):
148+
expected == coerce_val
149+
validator_extra_aok.present(coerce_val) == tuple(expected)
150+
else:
151+
assert np.array_equal(coerce_val, coerce_val)
152+
assert np.array_equal(
153+
validator_extra_aok.present(coerce_val),
154+
coerce_val)
149155

150156

151157
# ### Rejection by type ###

0 commit comments

Comments
 (0)