Merge pull request #521 from campagnola/fix-qstring

Relax type checking on string parameters
This commit is contained in:
Luke Campagnola 2017-08-02 15:30:56 -07:00 committed by GitHub
commit dd672c41b6
4 changed files with 209 additions and 26 deletions

View File

@ -200,7 +200,7 @@ def mkColor(*args):
try: try:
return Colors[c] return Colors[c]
except KeyError: except KeyError:
raise Exception('No color named "%s"' % c) raise ValueError('No color named "%s"' % c)
if len(c) == 3: if len(c) == 3:
r = int(c[0]*2, 16) r = int(c[0]*2, 16)
g = int(c[1]*2, 16) g = int(c[1]*2, 16)
@ -235,18 +235,18 @@ def mkColor(*args):
elif len(args[0]) == 2: elif len(args[0]) == 2:
return intColor(*args[0]) return intColor(*args[0])
else: else:
raise Exception(err) raise TypeError(err)
elif type(args[0]) == int: elif type(args[0]) == int:
return intColor(args[0]) return intColor(args[0])
else: else:
raise Exception(err) raise TypeError(err)
elif len(args) == 3: elif len(args) == 3:
(r, g, b) = args (r, g, b) = args
a = 255 a = 255
elif len(args) == 4: elif len(args) == 4:
(r, g, b, a) = args (r, g, b, a) = args
else: else:
raise Exception(err) raise TypeError(err)
args = [r,g,b,a] args = [r,g,b,a]
args = [0 if np.isnan(a) or np.isinf(a) else a for a in args] args = [0 if np.isnan(a) or np.isinf(a) else a for a in args]
@ -404,22 +404,39 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0)
def eq(a, b): def eq(a, b):
"""The great missing equivalence function: Guaranteed evaluation to a single bool value.""" """The great missing equivalence function: Guaranteed evaluation to a single bool value.
This function has some important differences from the == operator:
1. Returns True if a IS b, even if a==b still evaluates to False, such as with nan values.
2. Tests for equivalence using ==, but silently ignores some common exceptions that can occur
(AtrtibuteError, ValueError).
3. When comparing arrays, returns False if the array shapes are not the same.
4. When comparing arrays of the same shape, returns True only if all elements are equal (whereas
the == operator would return a boolean array).
"""
if a is b: if a is b:
return True return True
try: try:
with warnings.catch_warnings(module=np): # ignore numpy futurewarning (numpy v. 1.10) try:
# Sometimes running catch_warnings(module=np) generates AttributeError ???
catcher = warnings.catch_warnings(module=np) # ignore numpy futurewarning (numpy v. 1.10)
catcher.__enter__()
except Exception:
catcher = None
e = a==b e = a==b
except ValueError: except (ValueError, AttributeError):
return False
except AttributeError:
return False return False
except: except:
print('failed to evaluate equivalence for:') print('failed to evaluate equivalence for:')
print(" a:", str(type(a)), str(a)) print(" a:", str(type(a)), str(a))
print(" b:", str(type(b)), str(b)) print(" b:", str(type(b)), str(b))
raise raise
finally:
if catcher is not None:
catcher.__exit__(None, None, None)
t = type(e) t = type(e)
if t is bool: if t is bool:
return e return e

View File

@ -326,25 +326,12 @@ class SimpleParameter(Parameter):
'int': int, 'int': int,
'float': float, 'float': float,
'bool': bool, 'bool': bool,
'str': self._interpStr, 'str': asUnicode,
'color': self._interpColor, 'color': self._interpColor,
'colormap': self._interpColormap, 'colormap': self._interpColormap,
}[self.opts['type']] }[self.opts['type']]
return fn(v) return fn(v)
def _interpStr(self, v):
if sys.version[0] == '2':
if isinstance(v, QtCore.QString):
v = unicode(v)
elif not isinstance(v, basestring):
raise TypeError("Cannot set str parmeter from object %r" % v)
else:
if isinstance(v, QtCore.QString):
v = str(v)
elif not isinstance(v, str):
raise TypeError("Cannot set str parmeter from object %r" % v)
return v
def _interpColor(self, v): def _interpColor(self, v):
return fn.mkColor(v) return fn.mkColor(v)

View File

@ -1,7 +1,19 @@
# ~*~ coding: utf8 ~*~
import sys
import pytest
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.parametertree as pt import pyqtgraph.parametertree as pt
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.python2_3 import asUnicode
from pyqtgraph.functions import eq
import numpy as np
app = pg.mkQApp() app = pg.mkQApp()
def _getWidget(param):
return list(param.items.keys())[0].widget
def test_opts(): def test_opts():
paramSpec = [ paramSpec = [
dict(name='bool', type='bool', readonly=True), dict(name='bool', type='bool', readonly=True),
@ -12,7 +24,111 @@ def test_opts():
tree = pt.ParameterTree() tree = pt.ParameterTree()
tree.setParameters(param) tree.setParameters(param)
assert list(param.param('bool').items.keys())[0].widget.isEnabled() is False assert _getWidget(param.param('bool')).isEnabled() is False
assert list(param.param('color').items.keys())[0].widget.isEnabled() is False assert _getWidget(param.param('bool')).isEnabled() is False
def test_types():
paramSpec = [
dict(name='float', type='float'),
dict(name='int', type='int'),
dict(name='str', type='str'),
dict(name='list', type='list', values=['x','y','z']),
dict(name='dict', type='list', values={'x':1, 'y':3, 'z':7}),
dict(name='bool', type='bool'),
dict(name='color', type='color'),
]
param = pt.Parameter.create(name='params', type='group', children=paramSpec)
tree = pt.ParameterTree()
tree.setParameters(param)
all_objs = {
'int0': 0, 'int':7, 'float': -0.35, 'bigfloat': 1e129, 'npfloat': np.float(5),
'npint': np.int(5),'npinf': np.inf, 'npnan': np.nan, 'bool': True,
'complex': 5+3j, 'str': 'xxx', 'unicode': asUnicode('µ'),
'list': [1,2,3], 'dict': {'1': 2}, 'color': pg.mkColor('k'),
'brush': pg.mkBrush('k'), 'pen': pg.mkPen('k'), 'none': None
}
if hasattr(QtCore, 'QString'):
all_objs['qstring'] = QtCore.QString('xxxµ')
# float
types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'npinf', 'npnan', 'bool']
check_param_types(param.child('float'), float, float, 0.0, all_objs, types)
# int
types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'bool']
inttyps = int if sys.version[0] >= '3' else (int, long)
check_param_types(param.child('int'), inttyps, int, 0, all_objs, types)
# str (should be able to make a string out of any type)
types = all_objs.keys()
strtyp = str if sys.version[0] >= '3' else unicode
check_param_types(param.child('str'), strtyp, asUnicode, '', all_objs, types)
# bool (should be able to make a boolean out of any type?)
types = all_objs.keys()
check_param_types(param.child('bool'), bool, bool, False, all_objs, types)
# color
types = ['color', 'int0', 'int', 'float', 'npfloat', 'npint', 'list']
init = QtGui.QColor(128, 128, 128, 255)
check_param_types(param.child('color'), QtGui.QColor, pg.mkColor, init, all_objs, types)
def check_param_types(param, types, map_func, init, objs, keys):
"""Check that parameter setValue() accepts or rejects the correct types and
that value() returns the correct type.
Parameters
----------
param : Parameter instance
types : type or tuple of types
The allowed types for this parameter to return from value().
map_func : function
Converts an input value to the expected output value.
init : object
The expected initial value of the parameter
objs : dict
Contains a variety of objects that will be tested as arguments to
param.setValue().
keys : list
The list of keys indicating the valid objects in *objs*. When
param.setValue() is teasted with each value from *objs*, we expect
an exception to be raised if the associated key is not in *keys*.
"""
val = param.value()
if not isinstance(types, tuple):
types = (types,)
assert val == init and type(val) in types
# test valid input types
good_inputs = [objs[k] for k in keys if k in objs]
good_outputs = map(map_func, good_inputs)
for x,y in zip(good_inputs, good_outputs):
param.setValue(x)
val = param.value()
if not (eq(val, y) and type(val) in types):
raise Exception("Setting parameter %s with value %r should have resulted in %r (types: %r), "
"but resulted in %r (type: %r) instead." % (param, x, y, types, val, type(val)))
# test invalid input types
for k,v in objs.items():
if k in keys:
continue
try:
param.setValue(v)
except (TypeError, ValueError, OverflowError):
continue
except Exception as exc:
raise Exception("Setting %s parameter value to %r raised %r." % (param, v, exc))
raise Exception("Setting %s parameter value to %r should have raised an exception." % (param, v))

View File

@ -1,5 +1,6 @@
import pyqtgraph as pg import pyqtgraph as pg
import numpy as np import numpy as np
import sys
from numpy.testing import assert_array_almost_equal, assert_almost_equal from numpy.testing import assert_array_almost_equal, assert_almost_equal
import pytest import pytest
@ -294,5 +295,67 @@ def test_makeARGB():
pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=np.zeros([3, 2, 2])) pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=np.zeros([3, 2, 2]))
def test_eq():
eq = pg.functions.eq
zeros = [0, 0.0, np.float(0), np.int(0)]
if sys.version[0] < '3':
zeros.append(long(0))
for i,x in enumerate(zeros):
for y in zeros[i:]:
assert eq(x, y)
assert eq(y, x)
assert eq(np.nan, np.nan)
# test
class NotEq(object):
def __eq__(self, x):
return False
noteq = NotEq()
assert eq(noteq, noteq) # passes because they are the same object
assert not eq(noteq, NotEq())
# Should be able to test for equivalence even if the test raises certain
# exceptions
class NoEq(object):
def __init__(self, err):
self.err = err
def __eq__(self, x):
raise self.err
noeq1 = NoEq(AttributeError())
noeq2 = NoEq(ValueError())
noeq3 = NoEq(Exception())
assert eq(noeq1, noeq1)
assert not eq(noeq1, noeq2)
assert not eq(noeq2, noeq1)
with pytest.raises(Exception):
eq(noeq3, noeq2)
# test array equivalence
# note that numpy has a weird behavior here--np.all() always returns True
# if one of the arrays has size=0; eq() will only return True if both arrays
# have the same shape.
a1 = np.zeros((10, 20)).astype('float')
a2 = a1 + 1
a3 = a2.astype('int')
a4 = np.empty((0, 20))
assert not eq(a1, a2)
assert not eq(a1, a3)
assert not eq(a1, a4)
assert eq(a2, a3)
assert not eq(a2, a4)
assert not eq(a3, a4)
assert eq(a4, a4.copy())
assert not eq(a4, a4.T)
if __name__ == '__main__': if __name__ == '__main__':
test_interpolateArray() test_interpolateArray()