Skip to content

Commit bc4fb46

Browse files
malmaudjonmmease
authored andcommitted
Try to coerce all array-compatible objects to Numpy arrays. (#1393)
During validation, check for the presence of `__array_interface__` or `__array__` protocol methods and, if present, convert objects to numpy arrays.
1 parent adc950a commit bc4fb46

File tree

5 files changed

+177
-17
lines changed

5 files changed

+177
-17
lines changed

.circleci/create_conda_optional_env.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ if [ ! -d $HOME/miniconda/envs/circle_optional ]; then
1616
# Create environment
1717
# PYTHON_VERSION=3.6
1818
$HOME/miniconda/bin/conda create -n circle_optional --yes python=$PYTHON_VERSION \
19-
requests six pytz retrying psutil pandas decorator pytest mock nose poppler
19+
requests six pytz retrying psutil pandas decorator pytest mock nose poppler xarray
2020

2121
# Install orca into environment
2222
$HOME/miniconda/bin/conda install --yes -n circle_optional -c plotly plotly-orca

_plotly_utils/basevalidators.py

+47-14
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,26 @@ def fullmatch(regex, string, flags=0):
4343
# Utility functions
4444
# -----------------
4545
def to_scalar_or_list(v):
46+
# Handle the case where 'v' is a non-native scalar-like type,
47+
# such as numpy.float32. Without this case, the object might be
48+
# considered numpy-convertable and therefore promoted to a
49+
# 0-dimensional array, but we instead want it converted to a
50+
# Python native scalar type ('float' in the example above).
51+
# We explicitly check if is has the 'item' method, which conventionally
52+
# converts these types to native scalars. This guards against 'v' already being
53+
# a Python native scalar type since `numpy.isscalar` would return
54+
# True but `numpy.asscalar` will (oddly) raise an error is called with a
55+
# a native Python scalar object.
56+
if np and np.isscalar(v) and hasattr(v, 'item'):
57+
return np.asscalar(v)
4658
if isinstance(v, (list, tuple)):
4759
return [to_scalar_or_list(e) for e in v]
4860
elif np and isinstance(v, np.ndarray):
4961
return [to_scalar_or_list(e) for e in v]
5062
elif pd and isinstance(v, (pd.Series, pd.Index)):
5163
return [to_scalar_or_list(e) for e in v]
64+
elif is_numpy_convertable(v):
65+
return to_scalar_or_list(np.array(v))
5266
else:
5367
return v
5468

@@ -101,16 +115,19 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False):
101115
else:
102116
# DatetimeIndex
103117
v = v.to_pydatetime()
104-
105118
if not isinstance(v, np.ndarray):
106-
# v is not homogenous array
107-
v_list = [to_scalar_or_list(e) for e in v]
119+
# v has its own logic on how to convert itself into a numpy array
120+
if is_numpy_convertable(v):
121+
return copy_to_readonly_numpy_array(np.array(v), kind=kind, force_numeric=force_numeric)
122+
else:
123+
# v is not homogenous array
124+
v_list = [to_scalar_or_list(e) for e in v]
108125

109-
# Lookup dtype for requested kind, if any
110-
dtype = kind_default_dtypes.get(first_kind, None)
126+
# Lookup dtype for requested kind, if any
127+
dtype = kind_default_dtypes.get(first_kind, None)
111128

112-
# construct new array from list
113-
new_v = np.array(v_list, order='C', dtype=dtype)
129+
# construct new array from list
130+
new_v = np.array(v_list, order='C', dtype=dtype)
114131
elif v.dtype.kind in numeric_kinds:
115132
# v is a homogenous numeric array
116133
if kind and v.dtype.kind not in kind:
@@ -148,12 +165,29 @@ def copy_to_readonly_numpy_array(v, kind=None, force_numeric=False):
148165
return new_v
149166

150167

168+
def is_numpy_convertable(v):
169+
"""
170+
Return whether a value is meaningfully convertable to a numpy array
171+
via 'numpy.array'
172+
"""
173+
return hasattr(v, '__array__') or hasattr(v, '__array_interface__')
174+
175+
151176
def is_homogeneous_array(v):
152177
"""
153178
Return whether a value is considered to be a homogeneous array
154-
"""
155-
return ((np and isinstance(v, np.ndarray)) or
156-
(pd and isinstance(v, (pd.Series, pd.Index))))
179+
"""
180+
if ((np and isinstance(v, np.ndarray) or
181+
(pd and isinstance(v, (pd.Series, pd.Index))))):
182+
return True
183+
if is_numpy_convertable(v):
184+
v_numpy = np.array(v)
185+
# v is essentially a scalar and so shouldn't count as an array
186+
if v_numpy.shape == ():
187+
return False
188+
else:
189+
return True
190+
return False
157191

158192

159193
def is_simple_array(v):
@@ -1097,13 +1131,12 @@ def validate_coerce(self, v, should_raise=True):
10971131
# Pass None through
10981132
pass
10991133
elif self.array_ok and is_homogeneous_array(v):
1100-
1101-
v_array = copy_to_readonly_numpy_array(v)
1134+
v = copy_to_readonly_numpy_array(v)
11021135
if (self.numbers_allowed() and
1103-
v_array.dtype.kind in ['u', 'i', 'f']):
1136+
v.dtype.kind in ['u', 'i', 'f']):
11041137
# Numbers are allowed and we have an array of numbers.
11051138
# All good
1106-
v = v_array
1139+
pass
11071140
else:
11081141
validated_v = [
11091142
self.validate_coerce(e, should_raise=False)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import pytest
2+
import numpy as np
3+
import xarray
4+
import datetime
5+
from _plotly_utils.basevalidators import (NumberValidator,
6+
IntegerValidator,
7+
DataArrayValidator,
8+
ColorValidator)
9+
10+
11+
@pytest.fixture
12+
def data_array_validator(request):
13+
return DataArrayValidator('prop', 'parent')
14+
15+
16+
@pytest.fixture
17+
def integer_validator(request):
18+
return IntegerValidator('prop', 'parent', array_ok=True)
19+
20+
21+
@pytest.fixture
22+
def number_validator(request):
23+
return NumberValidator('prop', 'parent', array_ok=True)
24+
25+
26+
@pytest.fixture
27+
def color_validator(request):
28+
return ColorValidator('prop', 'parent', array_ok=True, colorscale_path='')
29+
30+
31+
@pytest.fixture(
32+
params=['int8', 'int16', 'int32', 'int64',
33+
'uint8', 'uint16', 'uint32', 'uint64',
34+
'float16', 'float32', 'float64'])
35+
def numeric_dtype(request):
36+
return request.param
37+
38+
39+
@pytest.fixture(
40+
params=[xarray.DataArray])
41+
def xarray_type(request):
42+
return request.param
43+
44+
45+
@pytest.fixture
46+
def numeric_xarray(request, xarray_type, numeric_dtype):
47+
return xarray_type(np.arange(10, dtype=numeric_dtype))
48+
49+
50+
@pytest.fixture
51+
def color_object_xarray(request, xarray_type):
52+
return xarray_type(['blue', 'green', 'red']*3)
53+
54+
55+
def test_numeric_validator_numeric_xarray(number_validator, numeric_xarray):
56+
res = number_validator.validate_coerce(numeric_xarray)
57+
58+
# Check type
59+
assert isinstance(res, np.ndarray)
60+
61+
# Check dtype
62+
assert res.dtype == numeric_xarray.dtype
63+
64+
# Check values
65+
np.testing.assert_array_equal(res, numeric_xarray)
66+
67+
68+
def test_integer_validator_numeric_xarray(integer_validator, numeric_xarray):
69+
res = integer_validator.validate_coerce(numeric_xarray)
70+
71+
# Check type
72+
assert isinstance(res, np.ndarray)
73+
74+
# Check dtype
75+
if numeric_xarray.dtype.kind in ('u', 'i'):
76+
# Integer and unsigned integer dtype unchanged
77+
assert res.dtype == numeric_xarray.dtype
78+
else:
79+
# Float datatypes converted to default integer type of int32
80+
assert res.dtype == 'int32'
81+
82+
# Check values
83+
np.testing.assert_array_equal(res, numeric_xarray)
84+
85+
86+
def test_data_array_validator(data_array_validator,
87+
numeric_xarray):
88+
res = data_array_validator.validate_coerce(numeric_xarray)
89+
90+
# Check type
91+
assert isinstance(res, np.ndarray)
92+
93+
# Check dtype
94+
assert res.dtype == numeric_xarray.dtype
95+
96+
# Check values
97+
np.testing.assert_array_equal(res, numeric_xarray)
98+
99+
100+
def test_color_validator_numeric(color_validator,
101+
numeric_xarray):
102+
res = color_validator.validate_coerce(numeric_xarray)
103+
104+
# Check type
105+
assert isinstance(res, np.ndarray)
106+
107+
# Check dtype
108+
assert res.dtype == numeric_xarray.dtype
109+
110+
# Check values
111+
np.testing.assert_array_equal(res, numeric_xarray)
112+
113+
114+
def test_color_validator_object(color_validator,
115+
color_object_xarray):
116+
117+
res = color_validator.validate_coerce(color_object_xarray)
118+
119+
# Check type
120+
assert isinstance(res, np.ndarray)
121+
122+
# Check dtype
123+
assert res.dtype == 'object'
124+
125+
# Check values
126+
np.testing.assert_array_equal(res, color_object_xarray)

optional-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ mock==2.0.0
1717
nose==1.3.3
1818
pytest==3.5.1
1919
backports.tempfile==1.0
20-
20+
xarray
2121
## orca ##
2222
psutil
2323

tox.ini

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ deps=
7272
optional: pyshp==1.2.10
7373
optional: pillow==5.2.0
7474
optional: matplotlib==2.2.3
75+
optional: xarray==0.10.9
7576

7677
; CORE ENVIRONMENTS
7778
[testenv:py27-core]
@@ -177,4 +178,4 @@ commands=
177178
basepython={env:PLOTLY_TOX_PYTHON_37:}
178179
commands=
179180
python --version
180-
nosetests {posargs} -x plotly/tests/test_plot_ly
181+
nosetests {posargs} -x plotly/tests/test_plot_ly

0 commit comments

Comments
 (0)