Skip to content

Conversation

@wmvanvliet
Copy link
Contributor

As @massich noted in #5946, the following snippet is a common occurrence in our codebase:

# Validate method parameter
if method not in ['foo', 'bar', 'baz']:
    raise ValueError("Invalid value for 'method' parameter. Valid options are ...")

Generating a good error message takes a bit more work. You can either do:

valid_options = ['foo', 'bar', 'baz']
if method not in valid_options:
    raise ValueError("Invalid value for 'method' parameter. Valid options are %s, "
                     "but got '%s' instead." % (valid_options, method))

or go just write it all out if you don't like the list-like formatting of the valid options:

if method not in ['foo', 'bar', 'baz']:
    raise ValueError("Invalid value for 'method' parameter. Valid options are "
                     "'foo', 'bar' and 'baz', but got '%s' instead." % method)

It's a little bit of a hassle and we're not very consistent across the code base in how we generate such errors.

This PR adds a private _check_option function that you can use to perform such a check and generate a friendly error message in a single line:

_check_option('method', method, valid_options=['foo', 'bar', 'baz'])

Small QOL improvement or unnecessary bloat? You be the judge :)
If we agree that we want this function, I'll open a follow-up PR that changes the bulk of the occurrences of this pattern in our codebase.

@larsoner
Copy link
Member

+1 from me



def _check_option(parameter, value, allowed_values, error=ValueError):
"""Checks a the value of a parameter against a list of valid options.
Copy link
Member

Choose a reason for hiding this comment

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

a the -> the

@agramfort
Copy link
Member

can you use/integrate this code anywhere? Let's clean at the same time.

@massich
Copy link
Contributor

massich commented Feb 19, 2019

+1000

I found myself refactoring again here https://github.com/mne-tools/mne-python/pull/5836/files#diff-d8a578281e14fb43fe3a386a41036b8fR794

So if you had not opened this PR I would have done it.

I would put the word rise in the function name and maybe we could add the type of error to raise(this is already there). But if you have a bulk of examples, maybe we can just start by this and then check if we can refactor further.

assert list(range(7, 10)) == picks


def test_check_option():
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good usecase to use pytest.mark.parametrize

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing me to this function. However, using pytest.mark.parametrize ended up not being any shorter nor clearer than these four lines. A little too much changes across each line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

...or I just don't understand the parametrize function too well. If you have some clever refactoring in mind, please share :)

@wmvanvliet
Copy link
Contributor Author

I would put the word rise in the function name

(I think you meant raise)

I totally see how that is good practice. But in this case, all the functions in the utils.check module start with _check_ (and many raise errors). Hence I feel that in this particular case, consistency beats this otherwise excellent guideline.

@wmvanvliet
Copy link
Contributor Author

can you use/integrate this code anywhere? Let's clean at the same time.

I'll put in a few use cases in this PR. But shall we leave be big cleanup for a separate PR that will strictly be a search and replace?

@agramfort
Copy link
Member

git grep "Got "

should help find some places you can use this new check function

I would favor to do the clean up in this PR. But others can help by pushing to the PR directly.

Just saying :)

@wmvanvliet
Copy link
Contributor Author

I would favor to do the clean up in this PR.

Then you got it!

@agramfort
Copy link
Member

agramfort commented Feb 20, 2019 via email

@wmvanvliet
Copy link
Contributor Author

Lets both go for it! There are plenty of cases to cover I guess :) And I'm still on paternity leave so my working time is spotty.

@codecov
Copy link

codecov bot commented Feb 20, 2019

Codecov Report

Merging #5958 into master will increase coverage by 0.08%.
The diff coverage is 99.31%.

@@            Coverage Diff             @@
##           master    #5958      +/-   ##
==========================================
+ Coverage   88.83%   88.92%   +0.08%     
==========================================
  Files         401      401              
  Lines       72874    72810      -64     
  Branches    12179    12108      -71     
==========================================
+ Hits        64741    64745       +4     
+ Misses       5215     5180      -35     
+ Partials     2918     2885      -33

@massich
Copy link
Contributor

massich commented Feb 20, 2019

Looking at the API I would prefer something like this:

def _check_option(*, option, accepted_values, error):
       raise ...

my_var = None
_check_option(option=my_var, accepted_values=[Ture, False], error=ValueError) 

and that it prints "valid values for my_var are "True" or "False"; got None"
I comparing it to _check_option('my_var', my_var, [Ture, False]) I find that if someone knows nothing about the whatever does _check_option reads the first one it is clear what it does (and that it will raise an error).

(We can allow for not passing option name _check_option(my_var, accepted_values=[Ture, False], error=ValueError). Also I'm not suggesting that we should put * to force it. This is just to illustrate what I'm refering to and then the rest be a convetion. We can even inforce such convention with LGTM)


If you don't find it useful, we can let it for someone who wants to drill in a rainy day. (here's a stack overflow)

@larsoner
Copy link
Member

I prefer it the way it is currently. It's really an expert/dev option and I don't find all the extra chars in our repo will be helpful in the long run. Extra verbosity can hurt readability sometimes, it's a trade-off. Here I think the first is better for something that is meant to be a time-saver anyway.

Returns
-------
is_valid : bool
Whether the value was valid or not.
Copy link
Contributor

@massich massich Feb 20, 2019

Choose a reason for hiding this comment

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

if its not valid, you are not returning. I'm not sure if we should say it. or just say return True

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point.

Copy link
Member

Choose a reason for hiding this comment

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

I would remove the "Returns" section as it returns None or raises a ValueError.

besides LGTM

Copy link
Member

Choose a reason for hiding this comment

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

@wmvanvliet this last one please

@agramfort
Copy link
Member

API of private function is fine with me as is. Let's use it as much as possible everywhere now.

@wmvanvliet
Copy link
Contributor Author

That's filenames stating with A-E done. Lots more to do.

@larsoner
Copy link
Member

This pull request introduces 1 alert when merging 692fca1 into 5418605 - view on LGTM.com

new alerts:

  • 1 for Wrong number of arguments in a call

Comment posted by LGTM.com

options = 'Allowed values are '
options += ', '.join(['%r' % v for v in allowed_values[:-1]])
options += ' and %r' % allowed_values[-1]
raise ValueError(msg.format(parameter=parameter, options=options,
Copy link
Contributor

Choose a reason for hiding this comment

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

are you sure that it will always be a ValueError? I'm fine with the change and change it back if a TypeError appears.

Copy link
Contributor

Choose a reason for hiding this comment

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

Scratch that. Actually to check if an option is a subclass of the allowed_values the function should be quite different:

def _check_option(option_name, option_val, allowed_values, type_of_check='one of', ...):

and allow for type_of_check='sublcass', etc..

Copy link
Contributor Author

@wmvanvliet wmvanvliet Feb 25, 2019

Choose a reason for hiding this comment

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

Why would I want to check whether an option is a subclass of allowed_values?

I've gone through the entire codebase now. This function only has to deal with str, None, True and False. It also only ever needs to throw ValueError, hence I removed the code for throwing other types of errors.

The reason this function doesn't need to be more complicated than this, is that all it really does is form a very specific error message that just happens to be a very common one. When a different kind of check is required, the error message is likely to be different as well and you're better off not bending over just to use this function but just write the if-statement and throw yourself.

Copy link
Member

Choose a reason for hiding this comment

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

I agree. Let’s not be too smart here. @wmvanvliet can you just see why CIs complain ?

@wmvanvliet
Copy link
Contributor Author

I think I'm done here. Ready for merge!

Copy link
Member

@larsoner larsoner left a comment

Choose a reason for hiding this comment

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

LGTM, let's merge once the CIs come back happy

@larsoner larsoner merged commit 73ea718 into mne-tools:master Feb 27, 2019
@larsoner
Copy link
Member

Thanks @wmvanvliet !

@massich
Copy link
Contributor

massich commented Mar 4, 2019

THX @wmvanvliet

@wmvanvliet wmvanvliet deleted the check_option branch March 4, 2019 12:29
jeythekey pushed a commit to jeythekey/mne-python that referenced this pull request Apr 27, 2019
* Add _check_option function

* Start putting _check_option to use

* More usage of _check_option

* Fixes

* Simplify _check_option function

* More _check_option usage

* fix

* Last batch of _check_option usage

* fix

* Fixes

* Update _check_option doc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants