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

Bug: false positive with --warn-unreachable #7214

Closed
jcugat opened this issue Jul 15, 2019 · 6 comments · Fixed by #7317
Closed

Bug: false positive with --warn-unreachable #7214

jcugat opened this issue Jul 15, 2019 · 6 comments · Fixed by #7317
Assignees
Labels
bug mypy got something wrong false-positive mypy gave an error on correct code priority-1-normal

Comments

@jcugat
Copy link

jcugat commented Jul 15, 2019

Please provide more information to help us understand the issue:

  • Are you reporting a bug, or opening a feature request?
    Bug

  • Please insert below the code you are checking with mypy,
    or a mock-up repro if the source is private. We would appreciate
    if you try to simplify your case to a minimal repro.

from contextlib import suppress
from typing import Dict


def foo(blah: Dict[str, str]):
    with suppress(KeyError):
        return blah["blah"]
    return blah["default"]


print(foo({"default": "default"}))
  • What is the actual behavior/output?
$ mypy --warn-unreachable test.py
test.py:8: error: Statement is unreachable
  • What is the behavior/output you expect?
    No error, since line 8 is reachable.

  • What are the versions of mypy and Python you are using?

$ pip freeze | grep mypy
mypy==0.720
mypy-extensions==0.4.1
$ python -V
Python 3.6.8
  • What are the mypy flags you are using? (For example --strict-optional)
    --warn-unreachable

Maybe related to #7204 and #7207

@JukkaL JukkaL added bug mypy got something wrong false-positive mypy gave an error on correct code priority-1-normal labels Jul 16, 2019
@JukkaL
Copy link
Collaborator

JukkaL commented Jul 16, 2019

This seems like a bug in how mypy calculates reachability of with statements.

cc @Michael0x2a

@Michael0x2a Michael0x2a self-assigned this Jul 21, 2019
@Michael0x2a
Copy link
Collaborator

@JukkaL -- I'm not sure if there's a simple solution for this one either. Basically, we could try adjusting mypy so it (correctly) treats all with statements as if they were the body of a try-finally block, which would mean statements after the with would start to be considered reachable.

But that ends up causing "Missing return statement" errors for code like the following:

def foo() -> int:
    with open("foobar.txt", "r") as f:
        return 3

More generally, there doesn't seem to be a clean way of distinguishing between context managers that suppress exceptions vs ones that don't.

It'd be nice to find some way of encoding this distinction at the type-system level (for example, by taking advantage of the proposed Annotated type?), but maybe for now we could get by just hard-coding a list of these exception-swallowing context managers within mypy?

@JelleZijlstra
Copy link
Member

You could in theory get this out of the return type of the context manager's __exit__: if it returns None, you know that it doesn't swallow exceptions, and if it returns bool or Optional[bool], you know that it may. Of course that's not sufficient, because you don't in general know the concrete contextmanager class involved (typing.ContextManager.__exit__ is annotated as returning Optional[bool]).

One possible pragmatic solution is to assume that contextmanagers don't swallow exceptions, unless they are annotated as returning bool (but not Optional[bool], because that's what typing.ContextManager returns and most general context managers don't swallow exceptions). We'd then add an explicit -> bool return type to contextlib.suppress.__exit__ in typeshed.

@JukkaL
Copy link
Collaborator

JukkaL commented Jul 22, 2019

@JelleZijlstra I like your idea! The implementation sounds pretty simple and it seems pretty logical (though it might be a little surprising). We'd probably need to document this in typeshed at least, and perhaps also in mypy docs.

@Michael0x2a What do you think about Jelle's proposal?

Michael0x2a added a commit to Michael0x2a/typeshed that referenced this issue Aug 10, 2019
This pull request is a follow-up to python/mypy#7214.

In short, within that mypy issue, we found it would be helpful to
determine between contextmanagers that can "swallow" exceptions vs ones
that can't. This helps prevent some false positive when using flags that
analyze control flow such as `--warn-unreachable`. To do this,
Jelle proposed assuming that only contextmanagers where the `__exit__`
returns `bool` are assumed to swallow exceptions.

This unfortunately required the following typeshed changes:

1. The typing.IO, threading.Lock, and concurrent.futures.Executor
   were all modified so `__exit__` returns `Optional[None]` instead
   of None -- along with all of their subclasses.

   I believe these three types are meant to be subclassed, so I felt
   picking the more general type was correct.

2. There were also a few concrete types (e.g. see socketserver,
   subprocess, ftplib...) that I modified to return `None` -- I checked
   the source code, and these all seem to return None (and don't appear
   to be meant to be subclassable).

3. contextlib.suppress was changed to return bool. I also double-checked
   the unittest modules and modified a subset of those contextmanagers,
   leaving ones like `_AssertRaisesContext` alone.
Michael0x2a added a commit to Michael0x2a/mypy that referenced this issue Aug 10, 2019
This pull request fixes python#7214:
it makes mypy treat any contextmanagers where the `__exit__` returns
`bool` or `Literal[True]` as ones that can potentially swallow
exceptions.

Contextmanagers that return `Optional[bool]`, None, or `Literal[False]`
continue to be treated as non-exception-swallowing ones.

This distinction helps the `--warn-unreachable` flag do the right thing
in this example program:

```python
from contextlib import suppress

def should_warn() -> str:
    with contextlib.suppress(IndexError):
        return ["a", "b", "c"][0]

def should_not_warn() -> str:
    with open("foo.txt") as f:
        return "blah"
```

This pull request needs the typeshed changes I made in
python/typeshed#3179. Once that one gets
merged, I'll update typeshed and rebase this pull request.
Michael0x2a added a commit to Michael0x2a/typeshed that referenced this issue Aug 10, 2019
This pull request is a follow-up to python/mypy#7214.

In short, within that mypy issue, we found it would be helpful to
determine between contextmanagers that can "swallow" exceptions vs ones
that can't. This helps prevent some false positive when using flags that
analyze control flow such as `--warn-unreachable`. To do this,
Jelle proposed assuming that only contextmanagers where the `__exit__`
returns `bool` are assumed to swallow exceptions.

This unfortunately required the following typeshed changes:

1. The typing.IO, threading.Lock, and concurrent.futures.Executor
   were all modified so `__exit__` returns `Optional[None]` instead
   of None -- along with all of their subclasses.

   I believe these three types are meant to be subclassed, so I felt
   picking the more general type was correct.

2. There were also a few concrete types (e.g. see socketserver,
   subprocess, ftplib...) that I modified to return `None` -- I checked
   the source code, and these all seem to return None (and don't appear
   to be meant to be subclassable).

3. contextlib.suppress was changed to return bool. I also double-checked
   the unittest modules and modified a subset of those contextmanagers,
   leaving ones like `_AssertRaisesContext` alone.
JelleZijlstra pushed a commit to python/typeshed that referenced this issue Aug 16, 2019
…3179)

This pull request is a follow-up to python/mypy#7214.

In short, within that mypy issue, we found it would be helpful to
determine between contextmanagers that can "swallow" exceptions vs ones
that can't. This helps prevent some false positive when using flags that
analyze control flow such as `--warn-unreachable`. To do this,
Jelle proposed assuming that only contextmanagers where the `__exit__`
returns `bool` are assumed to swallow exceptions.

This unfortunately required the following typeshed changes:

1. The typing.IO, threading.Lock, and concurrent.futures.Executor
   were all modified so `__exit__` returns `Optional[None]` instead
   of None -- along with all of their subclasses.

   I believe these three types are meant to be subclassed, so I felt
   picking the more general type was correct.

2. There were also a few concrete types (e.g. see socketserver,
   subprocess, ftplib...) that I modified to return `None` -- I checked
   the source code, and these all seem to return None (and don't appear
   to be meant to be subclassable).

3. contextlib.suppress was changed to return bool. I also double-checked
   the unittest modules and modified a subset of those contextmanagers,
   leaving ones like `_AssertRaisesContext` alone.
Michael0x2a added a commit that referenced this issue Aug 25, 2019
…#7317)

This pull request fixes #7214:
it makes mypy treat any context managers where the `__exit__` returns
`bool` or `Literal[True]` as ones that can potentially swallow
exceptions.

Context managers that return `Optional[bool]`, None, or `Literal[False]`
continue to be treated as non-exception-swallowing ones.

This distinction helps the `--warn-unreachable` flag do the right thing
in this example program:

```python
from contextlib import suppress

def should_warn() -> str:
    with contextlib.suppress(IndexError):
        return ["a", "b", "c"][0]

def should_not_warn() -> str:
    with open("foo.txt") as f:
        return "blah"
```

This behavior is partially disabled when strict-optional is disabled: we can't
necessarily distinguish between `Optional[bool]` vs `bool` in that mode, so
we conservatively treat the latter in the same way we treat the former.
tk0miya added a commit to tk0miya/sphinx that referenced this issue Oct 20, 2019
According to the mypy#7214, mypy-0.740 prefers a return value of
__exit__() method which does not swallow exceptions is None
instead of bool.

mypy#7214: python/mypy#7214
tk0miya added a commit to tk0miya/sphinx that referenced this issue Oct 20, 2019
According to the python/mypy#7214, mypy-0.740 prefers a return
value of __exit__() method which does not swallow exceptions is
None instead of bool.

mypy#7214: python/mypy#7214
@The-Compiler
Copy link
Contributor

I hope you don't mind me digging up an old issue, but it seems like this fits in nicely here - unless I'm missing something, there's no way to annotate this if using @contextlib.contextmanager, correct?

As an example:

import contextlib
from typing import Iterator

@contextlib.contextmanager
def swallow_exceptions() -> Iterator[None]:
    try:
        yield
    except Exception:
        # ...
        pass

def test() -> int:
    with swallow_exceptions():
        return 1
    return 2

results in:

unr.py:15: error: Statement is unreachable  [unreachable]
        return 2

The-Compiler added a commit to qutebrowser/qutebrowser that referenced this issue Mar 18, 2021
As for the mypy unreachable warning, see:
See python/mypy#7214
and python/mypy#8766

Includes cherry-pick of 27ad478
@tjstum
Copy link

tjstum commented Jun 17, 2022

A workaround that you could consider:

import contextlib
from typing import TYPE_CHECKING, Iterator

if TYPE_CHECKING:
    class swallow_exceptions:
        def __init__(self) -> None: ...
        def __enter__(self) -> None: ...
        def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: ...
else:
    @contextlib.contextmanager
    def swallow_exceptions() -> Iterator[None]:
        try:
            yield
        except Exception:
            # ...
            pass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong false-positive mypy gave an error on correct code priority-1-normal
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants