From c719ad4355e5fae81c95cf1fb32206ae854dcb4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 31 Jul 2017 17:04:53 -0700 Subject: [PATCH 1/4] Check for existence of QtCore.QString before using it --- pyqtgraph/parametertree/parameterTypes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index e3ed8853..ace0c9a4 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -333,13 +333,14 @@ class SimpleParameter(Parameter): return fn(v) def _interpStr(self, v): + isQString = hasattr(QtCore, 'QString') and isinstance(v, QtCore.QString) if sys.version[0] == '2': - if isinstance(v, QtCore.QString): + if isQString: v = unicode(v) elif not isinstance(v, basestring): raise TypeError("Cannot set str parmeter from object %r" % v) else: - if isinstance(v, QtCore.QString): + if isQString: v = str(v) elif not isinstance(v, str): raise TypeError("Cannot set str parmeter from object %r" % v) From b4e722f07bd09e7adfbcead693c09e3f5ee1a974 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 31 Jul 2017 17:16:46 -0700 Subject: [PATCH 2/4] Loosen string type checking a bit; let asUnicode throw errors if it needs to. --- pyqtgraph/parametertree/parameterTypes.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index ace0c9a4..8c1e587d 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -326,26 +326,12 @@ class SimpleParameter(Parameter): 'int': int, 'float': float, 'bool': bool, - 'str': self._interpStr, + 'str': asUnicode, 'color': self._interpColor, 'colormap': self._interpColormap, }[self.opts['type']] return fn(v) - def _interpStr(self, v): - isQString = hasattr(QtCore, 'QString') and isinstance(v, QtCore.QString) - if sys.version[0] == '2': - if isQString: - v = unicode(v) - elif not isinstance(v, basestring): - raise TypeError("Cannot set str parmeter from object %r" % v) - else: - if isQString: - v = str(v) - elif not isinstance(v, str): - raise TypeError("Cannot set str parmeter from object %r" % v) - return v - def _interpColor(self, v): return fn.mkColor(v) From 9094261c542684146a1813470012cd1fbfcabe12 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 2 Aug 2017 15:02:38 -0700 Subject: [PATCH 3/4] Fix eq() bug where calling catch_warnings raised an AttributeError, which would cause eq() to return False Add unit test coverage --- pyqtgraph/functions.py | 37 +++++++++++++----- pyqtgraph/tests/test_functions.py | 63 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index bdbf6d87..1aed6ace 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -200,7 +200,7 @@ def mkColor(*args): try: return Colors[c] except KeyError: - raise Exception('No color named "%s"' % c) + raise ValueError('No color named "%s"' % c) if len(c) == 3: r = int(c[0]*2, 16) g = int(c[1]*2, 16) @@ -235,18 +235,18 @@ def mkColor(*args): elif len(args[0]) == 2: return intColor(*args[0]) else: - raise Exception(err) + raise TypeError(err) elif type(args[0]) == int: return intColor(args[0]) else: - raise Exception(err) + raise TypeError(err) elif len(args) == 3: (r, g, b) = args a = 255 elif len(args) == 4: (r, g, b, a) = args else: - raise Exception(err) + raise TypeError(err) args = [r,g,b,a] 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): - """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: return True try: - with warnings.catch_warnings(module=np): # ignore numpy futurewarning (numpy v. 1.10) - e = a==b - except ValueError: - return False - except AttributeError: + 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 + except (ValueError, AttributeError): return False except: print('failed to evaluate equivalence for:') print(" a:", str(type(a)), str(a)) print(" b:", str(type(b)), str(b)) raise + finally: + if catcher is not None: + catcher.__exit__(None, None, None) + t = type(e) if t is bool: return e diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 7ad3bf91..eff56635 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -1,5 +1,6 @@ import pyqtgraph as pg import numpy as np +import sys from numpy.testing import assert_array_almost_equal, assert_almost_equal import pytest @@ -293,6 +294,68 @@ def test_makeARGB(): with AssertExc(): # 3d levels not allowed 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__': test_interpolateArray() \ No newline at end of file From 16f0e3034c1667776ca008630628fd4f85c3eb0e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 2 Aug 2017 15:03:58 -0700 Subject: [PATCH 4/4] Add tests for inpute/output type on a few parameter types --- .../tests/test_parametertypes.py | 120 +++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/tests/test_parametertypes.py b/pyqtgraph/parametertree/tests/test_parametertypes.py index dc581019..a654a9ad 100644 --- a/pyqtgraph/parametertree/tests/test_parametertypes.py +++ b/pyqtgraph/parametertree/tests/test_parametertypes.py @@ -1,7 +1,19 @@ +# ~*~ coding: utf8 ~*~ +import sys +import pytest +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph.parametertree as pt import pyqtgraph as pg +from pyqtgraph.python2_3 import asUnicode +from pyqtgraph.functions import eq +import numpy as np + app = pg.mkQApp() +def _getWidget(param): + return list(param.items.keys())[0].widget + + def test_opts(): paramSpec = [ dict(name='bool', type='bool', readonly=True), @@ -12,7 +24,111 @@ def test_opts(): tree = pt.ParameterTree() tree.setParameters(param) - assert list(param.param('bool').items.keys())[0].widget.isEnabled() is False - assert list(param.param('color').items.keys())[0].widget.isEnabled() is False + assert _getWidget(param.param('bool')).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)) + + + + + + + \ No newline at end of file