Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

ENH: _StringFuncParser to get numerical functions callables from strings #7464

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 15, 2016
Next Next commit
First commit
  • Loading branch information
alvarosg committed Nov 15, 2016
commit 0ad3710ca6f2c6b6052cbd8ccf347fa1dd11e945
78 changes: 78 additions & 0 deletions 78 lib/matplotlib/cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should raise KeyError if you're explicitly catching the KeyError, otherwise I don't see the point in differentiating the value errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, actually from the point of view of the dictionary it is a key error, but from the point of view of the user, it may be seen as a value error in one of the input arguments, as he does not need to know that this is stored in a dictionary and access with the string as a key. In any case, I am happy to switch to KeyError.

The reason I have two cases was to differentiate from TypeError, because in this case it may not be as safe to cast the input as a string for printing the error message. The way the code is now there is no chance it will not be a string, so yes, I am happy to just have a single except.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So yeah, then I vote for use one for now and split if a need arises.

"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()))
33 changes: 33 additions & 0 deletions 33 lib/matplotlib/tests/test_cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,3 +517,36 @@ def test_flatiter():

assert 0 == next(it)
assert 1 == next(it)


class TestFuncParser(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you insist on writing your own lambdas instead of using numpy, then please test that the lambda functions are correct (no stray typos or the like)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a parametric test for each possible case, and it tests both the direct and the inverse, so it should be ok.

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))
Morty Proxy This is a proxified and sanitized view of the page, visit original site.