Skip to content

Conflicting assertion causes (some) evaluations to stop #6931

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

Closed
lint-ai opened this issue Jun 3, 2019 · 4 comments
Closed

Conflicting assertion causes (some) evaluations to stop #6931

lint-ai opened this issue Jun 3, 2019 · 4 comments

Comments

@lint-ai
Copy link

lint-ai commented Jun 3, 2019

The simple version of the problem may already be a known issue; I searched, but it's hard to know the right keywords. The more complex version at the end appears to be a novel bug AFAICT.

Conflicting assertions don't cause type reconsideration

This works:

A simple variable, asserted non-None, then changed to be None, correctly intuits its new type. (Nice!)

from typing import Optional

b: Optional[bytes] = b""
assert b
reveal_type(b)  # -> Revealed type is 'builtins.bytes'
b = None
# No assertion needed
reveal_type(b)  # -> Revealed type is 'None'

Not to belabor the point, but even adding a bit of indirection (now b is in a class) still works, too:

from typing import Optional


class Foo(object):
    b: Optional[bytes]


var: Foo = Foo()

reveal_type(var.b)  # Revealed type is 'Union[builtins.bytes, None]'
assert var.b, "Always non-None to start"
var.b = None
reveal_type(var.b)  # -> Revealed type is 'None'
assert var.b is None, "kill() makes it None"
reveal_type(var.b)  # -> Revealed type is 'None'

This does not work:

Indirectly modifying a variable and then re-asserting its new types is not picked up.

from typing import Optional


class Foo(object):
    b: Optional[bytes]

    def kill(self):
        # (do some stuff and then...)
        self._b = None


var: Foo = Foo()

reveal_type(var.b)  # -> Revealed type is 'Union[builtins.bytes, None]'
assert var.b, "Always non-None to start"
var.kill()
reveal_type(var.b)  # -> Revealed type is 'builtins.bytes'
assert var.b is None, "kill() makes it None"
reveal_type(var.b)  # BUG: -> Revealed type is 'builtins.bytes'

This also doesn't work if we try to hint that there's more going on under the hood by making b a @property:

from typing import Optional


class Foo(object):
    _b: Optional[bytes]

    def kill(self):
        # (do some stuff and then...)
        self._b = None

    @property
    def b(self) -> Optional[bytes]:
        return self._b


var: Foo = Foo()

reveal_type(var.b)  # -> Revealed type is 'Union[builtins.bytes, None]'
assert var.b, "Always non-None to start"
var.kill()
reveal_type(var.b)  # -> Revealed type is 'builtins.bytes'
assert var.b is None, "kill() makes it None"
reveal_type(var.b)  # BUG: -> Revealed type is 'builtins.bytes'

Even casting the variable (inline or in kill) doesn't make the typechecker pick up on its new type.

Conflicting assertions cause (some) evaluations to stop altogether

But where it gets bizarre is if the conflicting assertion is in a block of some kind:

from typing import Optional


class Foo(object):
    _b: Optional[bytes]

    def kill(self):
        # (do some stuff and then...)
        self._b = None

    @property
    def b(self) -> Optional[bytes]:
        return self._b


var: Foo = Foo()

reveal_type(var.b)  # -> Revealed type is 'Union[builtins.bytes, None]'
assert var.b, "Always non-None to start"
reveal_type(var.b)  # -> Revealed type is 'builtins.bytes'
if True:
    reveal_type(var.b)  # -> Revealed type is 'builtins.bytes'
    var.kill()
    reveal_type(var.b)  # (still wrong) -> Revealed type is 'builtins.bytes'
    assert var.b is None, "kill() makes it None"
    reveal_type(var.b)  # (BUG: no output!?)
    var.c()  # BUG: In fact, it doesn't even notice this line now; it's given up on `var`?
    hrmph.wtf()  # But confusingly, this DOES produce: -> Name 'hrmph' is not defined

This one is kind of terrifying. It means anything else within that block now silently has disabled (or at least hobbled) type-checking.

Version Info

$ python --version ; python3 --version; mypy --version
Python 2.7.14
Python 3.7.3
mypy 0.701
@lint-ai
Copy link
Author

lint-ai commented Jun 13, 2019

Is there more I should share here in order to kick off the triage process? Have others tried been unable to repro the behavior?

Happy to add more info or test cases, but this is a bug that results in silently stopping evaluation, which seems important to fix.

@ilevkivskyi
Copy link
Member

What you do here is pretty magic, so no surprise mypy doesn't like it. Also this is essentially just a duplicate of #2395

@CH-DanReif
Copy link

It seems like something deeper is going on here, though. The other issue suggests that it's being treated this way because it's seen as unreachable code? Then why do other evaluations (hrmph) continue in that block? Why do evaluations continue in all cases if they not in a block at all? Why does reveal_type never work at all?

@ilevkivskyi
Copy link
Member

It seems like something deeper is going on here, though.

No, nothing deeper. Some messages are emitted during semantic analysis pass (like undefined names), while other during type checking (including reveal_type()).

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

No branches or pull requests

3 participants