From 0ad3710ca6f2c6b6052cbd8ccf347fa1dd11e945 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Tue, 15 Nov 2016 19:58:27 +0000 Subject: [PATCH 01/11] First commit --- lib/matplotlib/cbook.py | 78 ++++++++++++++++++++++++++++++ lib/matplotlib/tests/test_cbook.py | 33 +++++++++++++ 2 files changed, 111 insertions(+) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 32ddabbb5a73..67389c6651f3 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2661,3 +2661,81 @@ def __exit__(self, exc_type, exc_value, traceback): os.rmdir(path) except OSError: pass + + +class _StringFuncParser(object): + # Each element has: + # -The direct function, + # -The inverse function, + # -A boolean indicating whether the function + # is bounded in the interval 0-1 + + funcs = {'linear': (lambda x: x, lambda x: x, True), + 'quadratic': (lambda x: x**2, lambda x: x**(1. / 2), True), + 'cubic': (lambda x: x**3, lambda x: x**(1. / 3), True), + 'sqrt': (lambda x: x**(1. / 2), lambda x: x**2, True), + 'cbrt': (lambda x: x**(1. / 3), lambda x: x**3, True), + 'log10': (lambda x: np.log10(x), lambda x: (10**(x)), False), + 'log': (lambda x: np.log(x), lambda x: (np.exp(x)), False), + 'power{a}': (lambda x, a: x**a, + lambda x, a: x**(1. / a), True), + 'root{a}': (lambda x, a: x**(1. / a), + lambda x, a: x**a, True), + 'log10(x+{a})': (lambda x, a: np.log10(x + a), + lambda x, a: 10**x - a, True), + 'log(x+{a})': (lambda x, a: np.log(x + a), + lambda x, a: np.exp(x) - a, True)} + + def __init__(self, str_func): + self.str_func = str_func + + def is_string(self): + return not hasattr(self.str_func, '__call__') + + def get_func(self): + return self._get_element(0) + + def get_invfunc(self): + return self._get_element(1) + + def is_bounded_0_1(self): + return self._get_element(2) + + def _get_element(self, ind): + if not self.is_string(): + raise ValueError("The argument passed is not a string.") + + str_func = six.text_type(self.str_func) + # Checking if it comes with a parameter + param = None + regex = '\{(.*?)\}' + search = re.search(regex, str_func) + if search is not None: + parstring = search.group(1) + + try: + param = float(parstring) + except: + raise ValueError("'a' in parametric function strings must be " + "replaced by a number that is not " + "zero, e.g. 'log10(x+{0.1})'.") + if param == 0: + raise ValueError("'a' in parametric function strings must be " + "replaced by a number that is not " + "zero.") + str_func = re.sub(regex, '{a}', str_func) + + try: + output = self.funcs[str_func][ind] + if param is not None: + output = (lambda x, output=output: output(x, param)) + + return output + except KeyError: + raise ValueError("%s: invalid function. The only strings " + "recognized as functions are %s." % + (str_func, self.funcs.keys())) + except: + raise ValueError("Invalid function. The only strings recognized " + "as functions are %s." % + (self.funcs.keys())) diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 65136c77bbc6..fdbfeb552cd0 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -517,3 +517,36 @@ def test_flatiter(): assert 0 == next(it) assert 1 == next(it) + + +class TestFuncParser(object): + x_test = np.linspace(0.01, 0.5, 3) + validstrings = ['linear', 'quadratic', 'cubic', 'sqrt', 'cbrt', + 'log', 'log10', 'power{1.5}', 'root{2.5}', + 'log(x+{0.5})', 'log10(x+{0.1})'] + results = [(lambda x: x), + (lambda x: x**2), + (lambda x: x**3), + (lambda x: x**(1. / 2)), + (lambda x: x**(1. / 3)), + (lambda x: np.log(x)), + (lambda x: np.log10(x)), + (lambda x: x**1.5), + (lambda x: x**(1 / 2.5)), + (lambda x: np.log(x + 0.5)), + (lambda x: np.log10(x + 0.1))] + + @pytest.mark.parametrize("string", validstrings, ids=validstrings) + def test_inverse(self, string): + func_parser = cbook._StringFuncParser(string) + f = func_parser.get_func() + finv = func_parser.get_invfunc() + assert_array_almost_equal(finv(f(self.x_test)), self.x_test) + + @pytest.mark.parametrize("string, func", + zip(validstrings, results), + ids=validstrings) + def test_values(self, string, func): + func_parser = cbook._StringFuncParser(string) + f = func_parser.get_func() + assert_array_almost_equal(f(self.x_test), func(self.x_test)) From 509ed9f2ef2d0ff975ed58b090c979be0da1d687 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Tue, 15 Nov 2016 23:08:57 +0000 Subject: [PATCH 02/11] Creation of class _FuncInfo --- lib/matplotlib/cbook.py | 193 +++++++++++++++++++++-------- lib/matplotlib/tests/test_cbook.py | 41 ++++-- 2 files changed, 172 insertions(+), 62 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 67389c6651f3..1651064da882 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2663,74 +2663,154 @@ def __exit__(self, exc_type, exc_value, traceback): pass +class _FuncInfo(object): + """ + Class used to store a function + + Each object has: + * The direct function (direct) + * The inverse function (inverse) + * A boolean indicating whether the function + is bounded in the interval 0-1 (bounded_0_1) + + """ + def __init__(self, direct, inverse, bounded_0_1): + self.direct = direct + self.inverse = inverse + self.bounded_0_1 = bounded_0_1 + + def copy(self): + return _FuncInfo(self.direct, + self.inverse, + self.bounded_0_1) + + class _StringFuncParser(object): - # Each element has: - # -The direct function, - # -The inverse function, - # -A boolean indicating whether the function - # is bounded in the interval 0-1 - - funcs = {'linear': (lambda x: x, lambda x: x, True), - 'quadratic': (lambda x: x**2, lambda x: x**(1. / 2), True), - 'cubic': (lambda x: x**3, lambda x: x**(1. / 3), True), - 'sqrt': (lambda x: x**(1. / 2), lambda x: x**2, True), - 'cbrt': (lambda x: x**(1. / 3), lambda x: x**3, True), - 'log10': (lambda x: np.log10(x), lambda x: (10**(x)), False), - 'log': (lambda x: np.log(x), lambda x: (np.exp(x)), False), - 'power{a}': (lambda x, a: x**a, - lambda x, a: x**(1. / a), True), - 'root{a}': (lambda x, a: x**(1. / a), - lambda x, a: x**a, True), - 'log10(x+{a})': (lambda x, a: np.log10(x + a), - lambda x, a: 10**x - a, True), - 'log(x+{a})': (lambda x, a: np.log(x + a), - lambda x, a: np.exp(x) - a, True)} + """ + A class used to convert predefined strings into + _FuncInfo objects, or to directly obtain _FuncInfo + properties. + + """ + + _funcs = {} + _funcs['linear'] = _FuncInfo(lambda x: x, + lambda x: x, + True) + _funcs['quadratic'] = _FuncInfo(lambda x: x**2, + lambda x: x**(1. / 2), + True) + _funcs['cubic'] = _FuncInfo(lambda x: x**3, + lambda x: x**(1. / 3), + True) + _funcs['sqrt'] = _FuncInfo(lambda x: x**(1. / 2), + lambda x: x**2, + True) + _funcs['cbrt'] = _FuncInfo(lambda x: x**(1. / 3), + lambda x: x**3, + True) + _funcs['log10'] = _FuncInfo(lambda x: np.log10(x), + lambda x: (10**(x)), + False) + _funcs['log'] = _FuncInfo(lambda x: np.log(x), + lambda x: (np.exp(x)), + False) + _funcs['x**{p}'] = _FuncInfo(lambda x, p: x**p[0], + lambda x, p: x**(1. / p[0]), + True) + _funcs['root{p}(x)'] = _FuncInfo(lambda x, p: x**(1. / p[0]), + lambda x, p: x**p, + True) + _funcs['log10(x+{p})'] = _FuncInfo(lambda x, p: np.log10(x + p[0]), + lambda x, p: 10**x - p[0], + True) + _funcs['log(x+{p})'] = _FuncInfo(lambda x, p: np.log(x + p[0]), + lambda x, p: np.exp(x) - p[0], + True) + _funcs['log{p}(x+{p})'] = _FuncInfo(lambda x, p: (np.log(x + p[1]) / + np.log(p[0])), + lambda x, p: p[0]**(x) - p[1], + True) def __init__(self, str_func): - self.str_func = str_func + """ + Parameters + ---------- + str_func : string + String to be parsed. - def is_string(self): - return not hasattr(self.str_func, '__call__') + """ + try: # For python 2.7 and python 3+ compatibility + is_str = isinstance(str_func, basestring) + except NameError: + is_str = isinstance(str_func, str) + + if not is_str: + raise ValueError("The argument passed is not a string.") + self._str_func = str_func + self._key, self._params = self._get_key_params() + self._func = self.get_func() def get_func(self): - return self._get_element(0) + """ + Returns the _FuncInfo object, replacing the relevant parameters if + necessary in the lambda functions. + + """ + + func = self._funcs[self._key].copy() + if len(self._params) > 0: + m = func.direct + func.direct = (lambda x, m=m: m(x, self._params)) + m = func.inverse + func.inverse = (lambda x, m=m: m(x, self._params)) + return func + + def get_directfunc(self): + """ + Returns the callable for the direct function. + + """ + return self._func.direct def get_invfunc(self): - return self._get_element(1) + """ + Returns the callable for the inverse function. + + """ + return self._func.inverse def is_bounded_0_1(self): - return self._get_element(2) + """ + Returns a boolean indicating if the function is bounded + in the [0-1 interval]. - def _get_element(self, ind): - if not self.is_string(): - raise ValueError("The argument passed is not a string.") + """ + return self._func.bounded_0_1 - str_func = six.text_type(self.str_func) - # Checking if it comes with a parameter - param = None + def _get_key_params(self): + str_func = six.text_type(self._str_func) + # Checking if it comes with parameters regex = '\{(.*?)\}' - search = re.search(regex, str_func) - if search is not None: - parstring = search.group(1) + params = re.findall(regex, str_func) - try: - param = float(parstring) - except: - raise ValueError("'a' in parametric function strings must be " - "replaced by a number that is not " - "zero, e.g. 'log10(x+{0.1})'.") - if param == 0: - raise ValueError("'a' in parametric function strings must be " - "replaced by a number that is not " - "zero.") - str_func = re.sub(regex, '{a}', str_func) + if len(params) > 0: + for i in range(len(params)): + try: + params[i] = float(params[i]) + except: + raise ValueError("'p' in parametric function strings must" + " be replaced by a number that is not " + "zero, e.g. 'log10(x+{0.1})'.") + + if params[i] == 0: + raise ValueError("'p' in parametric function strings must" + " be replaced by a number that is not " + "zero.") + str_func = re.sub(regex, '{p}', str_func) try: - output = self.funcs[str_func][ind] - if param is not None: - output = (lambda x, output=output: output(x, param)) - - return output + func = self._funcs[str_func] except KeyError: raise ValueError("%s: invalid function. The only strings " "recognized as functions are %s." % @@ -2739,3 +2819,12 @@ def _get_element(self, ind): raise ValueError("Invalid function. The only strings recognized " "as functions are %s." % (self.funcs.keys())) + if len(params) > 0: + func.direct(0.5, params) + try: + func.direct(0.5, params) + except: + raise ValueError("Invalid parameters set for '%s'." % + (str_func)) + + return str_func, params diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index fdbfeb552cd0..9265eba50036 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -522,8 +522,8 @@ def test_flatiter(): class TestFuncParser(object): x_test = np.linspace(0.01, 0.5, 3) validstrings = ['linear', 'quadratic', 'cubic', 'sqrt', 'cbrt', - 'log', 'log10', 'power{1.5}', 'root{2.5}', - 'log(x+{0.5})', 'log10(x+{0.1})'] + 'log', 'log10', 'x**{1.5}', 'root{2.5}(x)', + 'log(x+{0.5})', 'log10(x+{0.1})', 'log{2}(x+{0.1})'] results = [(lambda x: x), (lambda x: x**2), (lambda x: x**3), @@ -534,19 +534,40 @@ class TestFuncParser(object): (lambda x: x**1.5), (lambda x: x**(1 / 2.5)), (lambda x: np.log(x + 0.5)), - (lambda x: np.log10(x + 0.1))] + (lambda x: np.log10(x + 0.1)), + (lambda x: np.log2(x + 0.1))] - @pytest.mark.parametrize("string", validstrings, ids=validstrings) - def test_inverse(self, string): - func_parser = cbook._StringFuncParser(string) - f = func_parser.get_func() - finv = func_parser.get_invfunc() - assert_array_almost_equal(finv(f(self.x_test)), self.x_test) + bounded_list = [True, True, True, True, True, + False, False, True, True, + True, True, True] @pytest.mark.parametrize("string, func", zip(validstrings, results), ids=validstrings) def test_values(self, string, func): func_parser = cbook._StringFuncParser(string) - f = func_parser.get_func() + f = func_parser.get_directfunc() assert_array_almost_equal(f(self.x_test), func(self.x_test)) + + @pytest.mark.parametrize("string", validstrings, ids=validstrings) + def test_inverse(self, string): + func_parser = cbook._StringFuncParser(string) + f = func_parser.get_func() + fdir = f.direct + finv = f.inverse + assert_array_almost_equal(finv(fdir(self.x_test)), self.x_test) + + @pytest.mark.parametrize("string", validstrings, ids=validstrings) + def test_get_invfunc(self, string): + func_parser = cbook._StringFuncParser(string) + finv1 = func_parser.get_invfunc() + finv2 = func_parser.get_func().inverse + assert_array_almost_equal(finv1(self.x_test), finv2(self.x_test)) + + @pytest.mark.parametrize("string, bounded", + zip(validstrings, bounded_list), + ids=validstrings) + def test_bounded(self, string, bounded): + func_parser = cbook._StringFuncParser(string) + b = func_parser.is_bounded_0_1() + assert_array_equal(b, bounded) From bb042f6407cc0ab9c90fd76cb4cf1df466f81212 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Wed, 16 Nov 2016 18:33:34 +0000 Subject: [PATCH 03/11] Added parameter check, and other feedback from the PR --- lib/matplotlib/cbook.py | 131 ++++++++++++++++++----------- lib/matplotlib/tests/test_cbook.py | 37 ++++---- 2 files changed, 104 insertions(+), 64 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 1651064da882..1567aeedb6de 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2671,18 +2671,36 @@ class _FuncInfo(object): * The direct function (direct) * The inverse function (inverse) * A boolean indicating whether the function - is bounded in the interval 0-1 (bounded_0_1) + is bounded in the interval 0-1 (bounded_0_1), or + a method that returns the information depending + on this + * A callable (check_params) that returns a bool specifying if a + certain combination of parameters is valid. """ - def __init__(self, direct, inverse, bounded_0_1): + def __init__(self, direct, inverse, bounded_0_1=True, check_params=None): self.direct = direct self.inverse = inverse - self.bounded_0_1 = bounded_0_1 - def copy(self): - return _FuncInfo(self.direct, - self.inverse, - self.bounded_0_1) + if (hasattr(bounded_0_1, '__call__')): + self._bounded_0_1 = bounded_0_1 + else: + self._bounded_0_1 = lambda x: bounded_0_1 + + if check_params is None: + self._check_params = lambda x: True + elif (hasattr(check_params, '__call__')): + self._check_params = check_params + else: + raise ValueError("Check params must be a callable, returning " + "a boolean with the validity of the passed " + "parameters or None.") + + def is_bounded_0_1(self, params=None): + return self._bounded_0_1(params) + + def check_params(self, params=None): + return self._check_params(params) class _StringFuncParser(object): @@ -2697,40 +2715,49 @@ class _StringFuncParser(object): _funcs['linear'] = _FuncInfo(lambda x: x, lambda x: x, True) - _funcs['quadratic'] = _FuncInfo(lambda x: x**2, - lambda x: x**(1. / 2), + _funcs['quadratic'] = _FuncInfo(np.square, + np.sqrt, True) _funcs['cubic'] = _FuncInfo(lambda x: x**3, - lambda x: x**(1. / 3), + np.cbrt, True) - _funcs['sqrt'] = _FuncInfo(lambda x: x**(1. / 2), - lambda x: x**2, + _funcs['sqrt'] = _FuncInfo(np.sqrt, + np.square, True) - _funcs['cbrt'] = _FuncInfo(lambda x: x**(1. / 3), + _funcs['cbrt'] = _FuncInfo(np.cbrt, lambda x: x**3, True) - _funcs['log10'] = _FuncInfo(lambda x: np.log10(x), + _funcs['log10'] = _FuncInfo(np.log10, lambda x: (10**(x)), False) - _funcs['log'] = _FuncInfo(lambda x: np.log(x), - lambda x: (np.exp(x)), + _funcs['log'] = _FuncInfo(np.log, + np.exp, False) + _funcs['log2'] = _FuncInfo(np.log2, + lambda x: (2**x), + False) _funcs['x**{p}'] = _FuncInfo(lambda x, p: x**p[0], lambda x, p: x**(1. / p[0]), True) _funcs['root{p}(x)'] = _FuncInfo(lambda x, p: x**(1. / p[0]), lambda x, p: x**p, True) + _funcs['log{p}(x)'] = _FuncInfo(lambda x, p: (np.log(x) / + np.log(p[0])), + lambda x, p: p[0]**(x), + False, + lambda p: p[0] > 0) _funcs['log10(x+{p})'] = _FuncInfo(lambda x, p: np.log10(x + p[0]), lambda x, p: 10**x - p[0], - True) + lambda p: p[0] > 0) _funcs['log(x+{p})'] = _FuncInfo(lambda x, p: np.log(x + p[0]), lambda x, p: np.exp(x) - p[0], - True) + lambda p: p[0] > 0) _funcs['log{p}(x+{p})'] = _FuncInfo(lambda x, p: (np.log(x + p[1]) / np.log(p[0])), lambda x, p: p[0]**(x) - p[1], - True) + lambda p: p[1] > 0, + lambda p: p[0] > 0) def __init__(self, str_func): """ @@ -2749,44 +2776,57 @@ def __init__(self, str_func): raise ValueError("The argument passed is not a string.") self._str_func = str_func self._key, self._params = self._get_key_params() - self._func = self.get_func() + self._func = self.func - def get_func(self): + @property + def func(self): """ Returns the _FuncInfo object, replacing the relevant parameters if necessary in the lambda functions. """ - func = self._funcs[self._key].copy() - if len(self._params) > 0: + func = self._funcs[self._key] + if self._params: m = func.direct - func.direct = (lambda x, m=m: m(x, self._params)) + direct = (lambda x, m=m: m(x, self._params)) + m = func.inverse - func.inverse = (lambda x, m=m: m(x, self._params)) + inverse = (lambda x, m=m: m(x, self._params)) + + is_bounded_0_1 = func.is_bounded_0_1(self._params) + + func = _FuncInfo(direct, inverse, + is_bounded_0_1) + else: + func = _FuncInfo(func.direct, func.inverse, + func.is_bounded_0_1()) return func - def get_directfunc(self): + @property + def directfunc(self): """ Returns the callable for the direct function. """ return self._func.direct - def get_invfunc(self): + @property + def invfunc(self): """ Returns the callable for the inverse function. """ return self._func.inverse + @property def is_bounded_0_1(self): """ Returns a boolean indicating if the function is bounded in the [0-1 interval]. """ - return self._func.bounded_0_1 + return self._func.is_bounded_0_1() def _get_key_params(self): str_func = six.text_type(self._str_func) @@ -2794,37 +2834,30 @@ def _get_key_params(self): regex = '\{(.*?)\}' params = re.findall(regex, str_func) - if len(params) > 0: + if params: for i in range(len(params)): try: params[i] = float(params[i]) except: - raise ValueError("'p' in parametric function strings must" + raise ValueError("Error with parameter number %i: '%s'. " + "'p' in parametric function strings must " " be replaced by a number that is not " - "zero, e.g. 'log10(x+{0.1})'.") + "zero, e.g. 'log10(x+{0.1})'." % + (i, params[i])) - if params[i] == 0: - raise ValueError("'p' in parametric function strings must" - " be replaced by a number that is not " - "zero.") str_func = re.sub(regex, '{p}', str_func) try: func = self._funcs[str_func] - except KeyError: - raise ValueError("%s: invalid function. The only strings " - "recognized as functions are %s." % - (str_func, self.funcs.keys())) except: - raise ValueError("Invalid function. The only strings recognized " - "as functions are %s." % - (self.funcs.keys())) - if len(params) > 0: - func.direct(0.5, params) - try: - func.direct(0.5, params) - except: - raise ValueError("Invalid parameters set for '%s'." % - (str_func)) + raise ValueError("%s: invalid string. The only strings " + "recognized as functions are %s." % + (str_func, self._funcs.keys())) + + # Checking that the parameters are valid + if not func.check_params(params): + raise ValueError("%s: are invalid values for the parameters " + "in %s." % + (params, str_func)) return str_func, params diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 9265eba50036..85fc9fec9743 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -522,37 +522,44 @@ def test_flatiter(): class TestFuncParser(object): x_test = np.linspace(0.01, 0.5, 3) validstrings = ['linear', 'quadratic', 'cubic', 'sqrt', 'cbrt', - 'log', 'log10', 'x**{1.5}', 'root{2.5}(x)', - 'log(x+{0.5})', 'log10(x+{0.1})', 'log{2}(x+{0.1})'] + 'log', 'log10', 'log2', 'x**{1.5}', 'root{2.5}(x)', + 'log{2}(x)', + 'log(x+{0.5})', 'log10(x+{0.1})', 'log{2}(x+{0.1})', + 'log{2}(x+{0})'] results = [(lambda x: x), - (lambda x: x**2), + np.square, (lambda x: x**3), - (lambda x: x**(1. / 2)), - (lambda x: x**(1. / 3)), - (lambda x: np.log(x)), - (lambda x: np.log10(x)), + np.sqrt, + np.cbrt, + np.log, + np.log10, + np.log2, (lambda x: x**1.5), (lambda x: x**(1 / 2.5)), + (lambda x: np.log2(x)), (lambda x: np.log(x + 0.5)), (lambda x: np.log10(x + 0.1)), - (lambda x: np.log2(x + 0.1))] + (lambda x: np.log2(x + 0.1)), + (lambda x: np.log2(x))] bounded_list = [True, True, True, True, True, - False, False, True, True, - True, True, True] + False, False, False, True, True, + False, + True, True, True, + False] @pytest.mark.parametrize("string, func", zip(validstrings, results), ids=validstrings) def test_values(self, string, func): func_parser = cbook._StringFuncParser(string) - f = func_parser.get_directfunc() + f = func_parser.directfunc assert_array_almost_equal(f(self.x_test), func(self.x_test)) @pytest.mark.parametrize("string", validstrings, ids=validstrings) def test_inverse(self, string): func_parser = cbook._StringFuncParser(string) - f = func_parser.get_func() + f = func_parser.func fdir = f.direct finv = f.inverse assert_array_almost_equal(finv(fdir(self.x_test)), self.x_test) @@ -560,8 +567,8 @@ def test_inverse(self, string): @pytest.mark.parametrize("string", validstrings, ids=validstrings) def test_get_invfunc(self, string): func_parser = cbook._StringFuncParser(string) - finv1 = func_parser.get_invfunc() - finv2 = func_parser.get_func().inverse + finv1 = func_parser.invfunc + finv2 = func_parser.func.inverse assert_array_almost_equal(finv1(self.x_test), finv2(self.x_test)) @pytest.mark.parametrize("string, bounded", @@ -569,5 +576,5 @@ def test_get_invfunc(self, string): ids=validstrings) def test_bounded(self, string, bounded): func_parser = cbook._StringFuncParser(string) - b = func_parser.is_bounded_0_1() + b = func_parser.is_bounded_0_1 assert_array_equal(b, bounded) From 4fa16f3cf011479ad1419fedc878f83b1146d4a8 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Wed, 16 Nov 2016 23:17:32 +0000 Subject: [PATCH 04/11] Removed np.cbrt for compatibility with older numpy versions --- lib/matplotlib/cbook.py | 4 ++-- lib/matplotlib/tests/test_cbook.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 1567aeedb6de..7a1d12942eaa 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2719,12 +2719,12 @@ class _StringFuncParser(object): np.sqrt, True) _funcs['cubic'] = _FuncInfo(lambda x: x**3, - np.cbrt, + lambda x: x**(1. / 3), True) _funcs['sqrt'] = _FuncInfo(np.sqrt, np.square, True) - _funcs['cbrt'] = _FuncInfo(np.cbrt, + _funcs['cbrt'] = _FuncInfo(lambda x: x**(1. / 3), lambda x: x**3, True) _funcs['log10'] = _FuncInfo(np.log10, diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 85fc9fec9743..2aa032c4da81 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -530,7 +530,7 @@ class TestFuncParser(object): np.square, (lambda x: x**3), np.sqrt, - np.cbrt, + (lambda x: x**(1. / 3)), np.log, np.log10, np.log2, From c5770fdcc7b1b95eb15012dd393e85cfbb5f932d Mon Sep 17 00:00:00 2001 From: alvarosg Date: Tue, 13 Dec 2016 00:32:29 +0000 Subject: [PATCH 05/11] Corrections from @efiring --- lib/matplotlib/cbook.py | 72 +++++++++++++++--------------- lib/matplotlib/tests/test_cbook.py | 12 ++--- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 7a1d12942eaa..b1ddedaa8d8c 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2668,7 +2668,7 @@ class _FuncInfo(object): Class used to store a function Each object has: - * The direct function (direct) + * The direct function (function) * The inverse function (inverse) * A boolean indicating whether the function is bounded in the interval 0-1 (bounded_0_1), or @@ -2678,23 +2678,23 @@ class _FuncInfo(object): certain combination of parameters is valid. """ - def __init__(self, direct, inverse, bounded_0_1=True, check_params=None): - self.direct = direct + def __init__(self, function, inverse, bounded_0_1=True, check_params=None): + self.function = function self.inverse = inverse - if (hasattr(bounded_0_1, '__call__')): + if callable(bounded_0_1): self._bounded_0_1 = bounded_0_1 else: self._bounded_0_1 = lambda x: bounded_0_1 if check_params is None: self._check_params = lambda x: True - elif (hasattr(check_params, '__call__')): + elif callable(check_params): self._check_params = check_params else: raise ValueError("Check params must be a callable, returning " "a boolean with the validity of the passed " - "parameters or None.") + "parameters, or None.") def is_bounded_0_1(self, params=None): return self._bounded_0_1(params) @@ -2767,52 +2767,56 @@ def __init__(self, str_func): String to be parsed. """ - try: # For python 2.7 and python 3+ compatibility - is_str = isinstance(str_func, basestring) - except NameError: - is_str = isinstance(str_func, str) - if not is_str: + if not isinstance(str_func, six.string_types): raise ValueError("The argument passed is not a string.") - self._str_func = str_func + self._str_func = six.text_type(str_func) self._key, self._params = self._get_key_params() - self._func = self.func + self._func = self._parse_func() - @property - def func(self): + def _parse_func(self): """ - Returns the _FuncInfo object, replacing the relevant parameters if - necessary in the lambda functions. + Parses the parameters to build a new _FuncInfo object, + replacing the relevant parameters if necessary in the lambda + functions. """ func = self._funcs[self._key] if self._params: - m = func.direct - direct = (lambda x, m=m: m(x, self._params)) + m = func.function + function = (lambda x, m=m: m(x, self._params)) m = func.inverse inverse = (lambda x, m=m: m(x, self._params)) is_bounded_0_1 = func.is_bounded_0_1(self._params) - func = _FuncInfo(direct, inverse, + func = _FuncInfo(function, inverse, is_bounded_0_1) else: - func = _FuncInfo(func.direct, func.inverse, + func = _FuncInfo(func.function, func.inverse, func.is_bounded_0_1()) return func @property - def directfunc(self): + def func_info(self): + """ + Returns the _FuncInfo object. + + """ + return self._func + + @property + def function(self): """ Returns the callable for the direct function. """ - return self._func.direct + return self._func.function @property - def invfunc(self): + def inverse(self): """ Returns the callable for the inverse function. @@ -2829,30 +2833,28 @@ def is_bounded_0_1(self): return self._func.is_bounded_0_1() def _get_key_params(self): - str_func = six.text_type(self._str_func) + str_func = self._str_func # Checking if it comes with parameters regex = '\{(.*?)\}' params = re.findall(regex, str_func) if params: - for i in range(len(params)): + for i, param in enumerate(params): try: - params[i] = float(params[i]) - except: - raise ValueError("Error with parameter number %i: '%s'. " - "'p' in parametric function strings must " - " be replaced by a number that is not " - "zero, e.g. 'log10(x+{0.1})'." % - (i, params[i])) + params[i] = float(param) + except ValueError: + raise ValueError("Parameter %i is '%s', which is " + "not a number." % + (i, param)) str_func = re.sub(regex, '{p}', str_func) try: func = self._funcs[str_func] - except: + except ValueError, KeyError: raise ValueError("%s: invalid string. The only strings " "recognized as functions are %s." % - (str_func, self._funcs.keys())) + (str_func, list(self._funcs))) # Checking that the parameters are valid if not func.check_params(params): diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 2aa032c4da81..bc69e7c1d3a9 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -553,22 +553,22 @@ class TestFuncParser(object): ids=validstrings) def test_values(self, string, func): func_parser = cbook._StringFuncParser(string) - f = func_parser.directfunc + f = func_parser.function assert_array_almost_equal(f(self.x_test), func(self.x_test)) @pytest.mark.parametrize("string", validstrings, ids=validstrings) def test_inverse(self, string): func_parser = cbook._StringFuncParser(string) - f = func_parser.func - fdir = f.direct + f = func_parser.func_info + fdir = f.function finv = f.inverse assert_array_almost_equal(finv(fdir(self.x_test)), self.x_test) @pytest.mark.parametrize("string", validstrings, ids=validstrings) - def test_get_invfunc(self, string): + def test_get_inverse(self, string): func_parser = cbook._StringFuncParser(string) - finv1 = func_parser.invfunc - finv2 = func_parser.func.inverse + finv1 = func_parser.inverse + finv2 = func_parser.func_info.inverse assert_array_almost_equal(finv1(self.x_test), finv2(self.x_test)) @pytest.mark.parametrize("string, bounded", From 69f2a0440b7aeed55d4999e36f4e8b80533130f5 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Tue, 13 Dec 2016 00:41:09 +0000 Subject: [PATCH 06/11] Solved compatibility error with python 3+ --- lib/matplotlib/cbook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index b1ddedaa8d8c..11527bb05a5a 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2851,7 +2851,7 @@ def _get_key_params(self): try: func = self._funcs[str_func] - except ValueError, KeyError: + except (ValueError, KeyError): raise ValueError("%s: invalid string. The only strings " "recognized as functions are %s." % (str_func, list(self._funcs))) From 94a1af0eafd310a15c9cf4aa673c410fa4f9a610 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Wed, 14 Dec 2016 11:55:04 +0000 Subject: [PATCH 07/11] Feedback from @story645 --- lib/matplotlib/cbook.py | 47 +++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 11527bb05a5a..d578411fcad9 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2674,8 +2674,10 @@ class _FuncInfo(object): is bounded in the interval 0-1 (bounded_0_1), or a method that returns the information depending on this - * A callable (check_params) that returns a bool specifying if a - certain combination of parameters is valid. + * A callable (check_params) that takes a list of the parameters + and returns a boolean specifying if a certain combination of + parameters is valid. It is only required if the function as + parameters and some of them are restricted. """ def __init__(self, function, inverse, bounded_0_1=True, check_params=None): @@ -2692,9 +2694,12 @@ def __init__(self, function, inverse, bounded_0_1=True, check_params=None): elif callable(check_params): self._check_params = check_params else: - raise ValueError("Check params must be a callable, returning " - "a boolean with the validity of the passed " - "parameters, or None.") + raise ValueError("Check params must be a callable taking a list " + "with function parameters and returning a " + "boolean indicating whether that combination of " + "parameters is valid. In case of validity for " + "any combination of parameters it may be set " + "to None.") def is_bounded_0_1(self, params=None): return self._bounded_0_1(params) @@ -2769,7 +2774,7 @@ def __init__(self, str_func): """ if not isinstance(str_func, six.string_types): - raise ValueError("The argument passed is not a string.") + raise ValueError("'%s' is not a string." % str_func) self._str_func = six.text_type(str_func) self._key, self._params = self._get_key_params() self._func = self._parse_func() @@ -2783,7 +2788,11 @@ def _parse_func(self): """ func = self._funcs[self._key] - if self._params: + + if not self._params: + func = _FuncInfo(func.function, func.inverse, + func.is_bounded_0_1()) + else: m = func.function function = (lambda x, m=m: m(x, self._params)) @@ -2794,9 +2803,6 @@ def _parse_func(self): func = _FuncInfo(function, inverse, is_bounded_0_1) - else: - func = _FuncInfo(func.function, func.inverse, - func.is_bounded_0_1()) return func @property @@ -2838,27 +2844,26 @@ def _get_key_params(self): regex = '\{(.*?)\}' params = re.findall(regex, str_func) - if params: - for i, param in enumerate(params): - try: - params[i] = float(param) - except ValueError: - raise ValueError("Parameter %i is '%s', which is " - "not a number." % - (i, param)) + for i, param in enumerate(params): + try: + params[i] = float(param) + except ValueError: + raise ValueError("Parameter %i is '%s', which is " + "not a number." % + (i, param)) - str_func = re.sub(regex, '{p}', str_func) + str_func = re.sub(regex, '{p}', str_func) try: func = self._funcs[str_func] except (ValueError, KeyError): - raise ValueError("%s: invalid string. The only strings " + raise ValueError("'%s' is an invalid string. The only strings " "recognized as functions are %s." % (str_func, list(self._funcs))) # Checking that the parameters are valid if not func.check_params(params): - raise ValueError("%s: are invalid values for the parameters " + raise ValueError("%s are invalid values for the parameters " "in %s." % (params, str_func)) From 331d5d34da8c9c03c3da6549747ac3499f2ff4ee Mon Sep 17 00:00:00 2001 From: alvarosg Date: Wed, 14 Dec 2016 11:57:04 +0000 Subject: [PATCH 08/11] Tiny change missing @story645 --- lib/matplotlib/cbook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index d578411fcad9..4c36407afac1 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2774,7 +2774,7 @@ def __init__(self, str_func): """ if not isinstance(str_func, six.string_types): - raise ValueError("'%s' is not a string." % str_func) + raise ValueError("'%s' must be a string." % str_func) self._str_func = six.text_type(str_func) self._key, self._params = self._get_key_params() self._func = self._parse_func() From a45f217988647ee49cf0ced78cb23f28dc295c03 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Wed, 14 Dec 2016 17:53:39 +0000 Subject: [PATCH 09/11] Changed exception message --- lib/matplotlib/cbook.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 4c36407afac1..88571428d54a 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2694,12 +2694,7 @@ def __init__(self, function, inverse, bounded_0_1=True, check_params=None): elif callable(check_params): self._check_params = check_params else: - raise ValueError("Check params must be a callable taking a list " - "with function parameters and returning a " - "boolean indicating whether that combination of " - "parameters is valid. In case of validity for " - "any combination of parameters it may be set " - "to None.") + raise ValueError("Invalid 'check_params' argument.") def is_bounded_0_1(self, params=None): return self._bounded_0_1(params) From 3bd901ea8268431642550fbb0a072604827d4065 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Wed, 14 Dec 2016 18:19:11 +0000 Subject: [PATCH 10/11] Fixed typo --- lib/matplotlib/cbook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 88571428d54a..3c0155836584 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2676,7 +2676,7 @@ class _FuncInfo(object): on this * A callable (check_params) that takes a list of the parameters and returns a boolean specifying if a certain combination of - parameters is valid. It is only required if the function as + parameters is valid. It is only required if the function has parameters and some of them are restricted. """ From c143e75d0852351feff8d260bffae5c506b35ff8 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Thu, 15 Dec 2016 13:28:40 +0000 Subject: [PATCH 11/11] Implemented feedback from @QuLogic --- lib/matplotlib/cbook.py | 80 ++++++++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 3c0155836584..be5d3151be8a 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2665,22 +2665,38 @@ def __exit__(self, exc_type, exc_value, traceback): class _FuncInfo(object): """ - Class used to store a function - - Each object has: - * The direct function (function) - * The inverse function (inverse) - * A boolean indicating whether the function - is bounded in the interval 0-1 (bounded_0_1), or - a method that returns the information depending - on this - * A callable (check_params) that takes a list of the parameters - and returns a boolean specifying if a certain combination of - parameters is valid. It is only required if the function has - parameters and some of them are restricted. + Class used to store a function. """ + def __init__(self, function, inverse, bounded_0_1=True, check_params=None): + """ + Parameters + ---------- + + function : callable + A callable implementing the function receiving the variable as + first argument and any additional parameters in a list as second + argument. + inverse : callable + A callable implementing the inverse function receiving the variable + as first argument and any additional parameters in a list as + second argument. It must satisfy 'inverse(function(x, p), p) == x'. + bounded_0_1: bool or callable + A boolean indicating whether the function is bounded in the [0,1] + interval, or a callable taking a list of values for the additional + parameters, and returning a boolean indicating whether the function + is bounded in the [0,1] interval for that combination of + parameters. Default True. + check_params: callable or None + A callable taking a list of values for the additional parameters + and returning a boolean indicating whether that combination of + parameters is valid. It is only required if the function has + additional parameters and some of them are restricted. + Default None. + + """ + self.function = function self.inverse = inverse @@ -2697,9 +2713,47 @@ def __init__(self, function, inverse, bounded_0_1=True, check_params=None): raise ValueError("Invalid 'check_params' argument.") def is_bounded_0_1(self, params=None): + """ + Returns a boolean indicating if the function is bounded in the [0,1] + interval for a particular set of additional parameters. + + Parameters + ---------- + + params : list + The list of additional parameters. Default None. + + Returns + ------- + + out : bool + True if the function is bounded in the [0,1] interval for + parameters 'params'. Otherwise False. + + """ + return self._bounded_0_1(params) def check_params(self, params=None): + """ + Returns a boolean indicating if the set of additional parameters is + valid. + + Parameters + ---------- + + params : list + The list of additional parameters. Default None. + + Returns + ------- + + out : bool + True if 'params' is a valid set of additional parameters for the + function. Otherwise False. + + """ + return self._check_params(params)