Skip to content
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

SIM106: Handle error cases early #8

Closed
MartinThoma opened this issue Sep 26, 2020 · 13 comments · Fixed by #108
Closed

SIM106: Handle error cases early #8

MartinThoma opened this issue Sep 26, 2020 · 13 comments · Fixed by #108
Assignees
Labels
enhancement New feature or request

Comments

@MartinThoma
Copy link
Owner

Explanation

Exceptions and error cases should be handled early to prevent deeply-nested code.

Example

# Bad
if cond:
    a
    b
    c
else:
    raise Exception

# Good
if not cond:
    raise Exception

a
b
c
@MartinThoma MartinThoma added the enhancement New feature or request label Sep 26, 2020
@MartinThoma MartinThoma self-assigned this Sep 26, 2020
@MartinThoma MartinThoma changed the title [New Rule] [New Rule] Handle error cases early Sep 26, 2020
@MartinThoma
Copy link
Owner Author

    If(
        test=Name(id='cond', ctx=Load()),
        body=[
            Expr(
                value=Name(id='a', ctx=Load()),
            ),
            Expr(
                value=Name(id='b', ctx=Load()),
            ),
            Expr(
                value=Name(id='c', ctx=Load()),
            ),
        ],
        orelse=[
            Raise(
                exc=Name(id='Exception', ctx=Load()),
                cause=None,
            ),
        ],
    ),

@MartinThoma
Copy link
Owner Author

        If(
            test=Name(id='cond', ctx=Load()),
            body=[
                Expr(
                    value=Name(id='a', ctx=Load()),
                ),
                Expr(
                    value=Name(id='b', ctx=Load()),
                ),
                Expr(
                    value=Name(id='c', ctx=Load()),
                ),
            ],
            orelse=[
                Raise(
                    exc=Name(id='Exception', ctx=Load()),
                    cause=None,
                ),
            ],
        ),

@MartinThoma
Copy link
Owner Author

d998c5e

@MartinThoma MartinThoma changed the title [New Rule] Handle error cases early SIM106: Handle error cases early Jan 7, 2021
@GitRon
Copy link

GitRon commented Jan 7, 2021

Hi @MartinThoma! I just ran into another thing :)

I often use a "else Exception" when I want to make 110% sure to avoid any definition gaps. I will quite obscurify my code if I do the (never happens) case on top with all the things that my variable might not be.

if a == 1:
    do_stuff()
if a == 2:
    do_stuff()
if a == 3:
    do_stuff()
if a == 4:
    do_stuff()
else:
    raise RuntimeError('Invalid paramter')

Any ideas about that? 😃

@MartinThoma
Copy link
Owner Author

Oh, interesting! I had this with NotImplemented ( #14 ), but I can see that you might want to do this with parameters as well.

In this simple example, I would actually write it like this:

if a not in [1, 2, 3, 4]:
    raise ValueError(f'Invalid parameter a={a}')

if a == 1:
    do_stuff1()
if a == 2:
    do_stuff2()
if a == 3:
    do_stuff3()
if a == 4:
    do_stuff4()

But I guess your real case is more complicated? Could you maybe share an example so that I can get a better understanding?

@MartinThoma MartinThoma reopened this Jan 7, 2021
@GitRon
Copy link

GitRon commented Jan 15, 2021

Hi @MartinThoma - it's not more complicated I think but I really don't like creating a long list for the exit condition. Especially when the numbers are constants, which are very long to write down like MyModel.SomeValueChoice.MY_FANCY_CHOICE. If you have many of them, the exit condtion will be super unreadable. So IMHO I think it's perfectly ok to write it the way it is shown in my example. 😃

@MartinThoma
Copy link
Owner Author

I can see your point. As a general remark: Although I try to make this plugin "agreeable" in the sense that typically one can take the rules as they are, such cases will always come. Please take the rules as an opinionated recommendation. It is absolutely understandable if you disable rules that don't fit your general style.

Specifically, in this case, I have a hard time seeing how this could become hard to read if you also use the black formatter. You can give this a name and then do this:

valid_values = [
    MyModel.SomeValueChoice.MY_FANCY_CHOICE1,
    MyModel.SomeValueChoice.MY_FANCY_CHOICE2,
    MyModel.SomeValueChoice.MY_FANCY_CHOICE3,
    MyModel.SomeValueChoice.MY_FANCY_CHOICE4,
]

if a not in valid_values:
    raise ValueError(f"Invalid parameter a={a}")

if a == MyModel.SomeValueChoice.MY_FANCY_CHOICE1:
    do_stuff1()
if a == MyModel.SomeValueChoice.MY_FANCY_CHOICE2:
    do_stuff2()
if a == MyModel.SomeValueChoice.MY_FANCY_CHOICE3:
    do_stuff3()
if a == MyModel.SomeValueChoice.MY_FANCY_CHOICE3:
    do_stuff4()

You can even combine this with Literal values for type checking

@GitRon
Copy link

GitRon commented Jan 18, 2021

@MartinThoma True, this might be the way to go. Still, I'd duplicate all choices so I doesn't feel so DRY anymore. In a regular function, I'd agree that exit conditons should go first to avoid nested if-conditions. But in an if? Why you think that it's a bad pattern? 😃

@MartinThoma
Copy link
Owner Author

In a regular function, I'd agree that exit conditons should go first to avoid nested if-conditions.

Thank you! I was all the time feeling that something is missing in that rule - that was it! It needs a check that it's the first thing in a function (maybe allowing few exceptions 🤔 ... hm, not sure )

@GitRon
Copy link

GitRon commented Jan 20, 2021

Cool, glad I could help 😃

Another question: The other day I was trying out some new rules for the https://github.com/rocioar/flake8-django package. Do you maybe have experience with flaking django model attributes?

@alk-acezar
Copy link

Other solution:

handlers = {
    MyModel.SomeValueChoice.MY_FANCY_CHOICE1: do_stuff1,
    MyModel.SomeValueChoice.MY_FANCY_CHOICE2: do_stuff2,
    MyModel.SomeValueChoice.MY_FANCY_CHOICE3: do_stuff3,
    MyModel.SomeValueChoice.MY_FANCY_CHOICE4: do_stuff4,
}

handler = handlers.get(a, lambda: ValueError(f"Invalid parameter a={a}"))
handler()

I it's OK for you I'd like to contribute a pr where simple if/else raises SIM106 while if/elif/else doesn't. Does it looks like a good compromise?

@MartinThoma
Copy link
Owner Author

@alk-acezar I'm unhappy with SIM106. What do you think abou removing it altogether? Does it actually provide value?

@ghost
Copy link

ghost commented Feb 20, 2022

It's useful in some cases IMO, but I do find myself having to add # noqa: SIM106 many times in cases where I would have to repeat the list of possible values at the beginning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants