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

Make isinstance/issubclass generate ad-hoc intersections #8305

Merged
merged 5 commits into from Jan 23, 2020

Conversation

Michael0x2a
Copy link
Collaborator

This diff makes isinstance(...) and issubclass(...) try generating ad-hoc intersections of Instances when possible.

For example, we previously concluded the if-branch is unreachable in the following program. This PR makes mypy infer an ad-hoc intersection instead.

class A: pass
class B: pass

x: A
if isinstance(x, B):
    reveal_type(x)  # N: Revealed type is 'test.<subclass of "A" and "B">'

If you try doing an isinstance(...) that legitimately is impossible due to conflicting method signatures or MRO issues, we continue to declare the branch unreachable. Passing in the --warn-unreachable flag will now also report an error about this:

# flags: --warn-unreachable
x: str

# E: Subclass of "str" and "bytes" cannot exist: would have
#    incompatible method signatures
if isinstance(x, bytes):
    reveal_type(x)  # E: Statement is unreachable

This error message has the same limitations as the other --warn-unreachable ones: we suppress them if the isinstance check is inside a function using TypeVars with multiple values.

However, we do end up always inferring an intersection type when possible -- that logic is never suppressed.

I initially thought we might have to suppress the new logic as well (see #3603 (comment)), but it turns out this is a non-issue in practice once you add in the check that disallows impossible intersections.

For example, when I tried running this PR on the larger of our two internal codebases, I found about 25 distinct errors, all of which were legitimate and unrelated to the problem discussed in the PR.

(And if we don't suppress the extra error message, we get about 100-120 errors, mostly due to tests repeatdly doing result = blah() followed by assert isinstance(result, X) where X keeps changing.)

@Michael0x2a
Copy link
Collaborator Author

Not really sure what's up with the tests though -- they seem to be crashing for unrelated reasons?

@ethanhs ethanhs mentioned this pull request Jan 21, 2020
@ethanhs
Copy link
Collaborator

ethanhs commented Jan 21, 2020

@Michael0x2a I think #8308 fixes the failing tests.

This diff makes `isinstance(...)` and `issubclass(...)` try
generating ad-hoc intersections of Instances when possible.

For example, we previously concluded the if-branch is unreachable
in the following program. This PR makes mypy infer an ad-hoc
intersection instead.

    class A: pass
    class B: pass

    x: A
    if isinstance(x, B):
        reveal_type(x)  # N: Revealed type is 'test.<subclass of "A" and "B">'

If you try doing an `isinstance(...)` that legitimately is impossible
due to conflicting method signatures or MRO issues, we continue to
declare the branch unreachable. Passing in the `--warn-unreachable`
flag will now also report an error about this:

    # flags: --warn-unreachable
    x: str

    # E: Subclass of "str" and "bytes" cannot exist: would have
    #    incompatible method signatures
    if isinstance(x, bytes):
        reveal_type(x)  # E: Statement is unreachable

This error message has the same limitations as the other
`--warn-unreachable` ones: we suppress them if the isinstance check
is inside a function using TypeVars with multiple values.

However, we *do* end up always inferring an intersection type when
possible -- that logic is never suppressed.

I initially thought we might have to suppress the new logic as well
(see python#3603 (comment)),
but it turns out this is a non-issue in practice once you add in
the check that disallows impossible intersections.

For example, when I tried running this PR on the larger of our two
internal codebases, I found about 25 distinct errors, all of which
were legitimate and unrelated to the problem discussed in the PR.

(And if we don't suppress the extra error message, we get about
100-120 errors, mostly due to tests repeatdly doing `result = blah()`
followed by `assert isinstance(result, X)` where X keeps changing.)
@ilevkivskyi
Copy link
Member

A quick question before I will do a full review: last time I was thinking about this my main worry was about how the fake intersections would interact with incremental mode. Did you try this in incremental mode? Maybe add few tests where generated TypeInfo "leaks" (for example via assignment to self) and therefore gets serialized and subsequently deserialized.

@msullivan
Copy link
Collaborator

I'm also interested in how this interacts with mypyc---though making it work with mypyc I don't think is actually a requirement to merge this, but would be great to tackle if you are interested.

(These intersection issues can cause runtime errors under mypyc, which is very frustrating.)

@Michael0x2a
Copy link
Collaborator Author

Ok, I added some incremental mode tests.

Just as a disclaimer though, I'm pretty rusty on anything to do with incremental mode, so I'm not actually sure if the new tests are high quality. In particular:

  1. I'm a little suspicious of the new test results in deps.test. If I'm understanding the results correctly, any change to either of the original classes should end up triggering a reanalysis of whatever scope happens to be creating the ad-hoc intersection -- but it's a little surprising to me that I don't see any triggers along the lines of <m.A> -> <m.<subclass of "A" and "B">>.
  2. I copied the incremental mode tests into the fine-grained mode tests, but I'm not actually sure if that's actually a useful thing to do.

If it's any consolation, I'm creating the ad-hoc intersection using the same code we were previously using in intersect_callable, which was added to mypy ~2 years ago. Basically, that method was generating a new classdef + typeinfo then adding the new typeinfo to the global scope.

I can try looking into adding support for this in mypyc, sure.

@msullivan
Copy link
Collaborator

msullivan commented Jan 22, 2020

I know that intersect_callable had some issue with astmerge where it wouldn't necessarily get merged correctly. Since it was pretty obscure anyway (it was my starter project on mypy! :P) I don't think we worried about it too much and I'm not sure if we ever figured out how to make a test that failed. But I think it did fail the astmerge consistency checker. (Which actually might just be completely broken because I don't think we've run it in a year and a half and it never passed on everything anyway)

Update: yeah the consistency checker is now completely broken

@msullivan
Copy link
Collaborator

I'm hacking on the consistency checker to get it back into shape and while there are some failures it looks like none of them are related to the callable test. So I think that the new semantic analyzer must have fixed that issue, which makes me a lot less nervous about these changes.

msullivan added a commit that referenced this pull request Jan 22, 2020
This fixes some stuff in the mergechecker that breaks with current
mypy's and also some bugs that the mergechecker caught. The entire
fine-grained test suite now passes with the merge checker on, which I
don't think has ever been true before.

This means that we could turn it on by default, but it doubles the
runtime of the fine-grained tests (from 90s CPU time to 180s CPU time
on my laptop), so I've left it off for now.

The motivation here is that I knew intersect_callable's creation of
types during typechecking used to run afoul of the consistency checker
and so I was nervous that #8305 would cause more problems by adding
more logic of that kind. It no longer does, probably as a result of
the semantic analyzer rewrite, so I think we are in the clear on that.
type_ranges: Optional[List[TypeRange]],
) -> Tuple[TypeMap, TypeMap]:
# For some reason, doing "yes_map, no_map = conditional_type_maps(...)"
# doesn't work: mypyc will decide that 'yes_map' is of type None if we try.
Copy link
Collaborator

Choose a reason for hiding this comment

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

weird

Copy link
Collaborator

@msullivan msullivan left a comment

Choose a reason for hiding this comment

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

I think this looks good and is a pretty exciting improvement.

A few doc nits but I'm happy with this.

I'd skip any mypyc stuff in this PR and do that in a follow-up if you want to tackle that.

mypy/checker.py Outdated Show resolved Hide resolved
mypy/checker.py Show resolved Hide resolved
@Michael0x2a Michael0x2a merged commit ad6c717 into python:master Jan 23, 2020
@Michael0x2a Michael0x2a deleted the add-ad-hoc-intersections-v2 branch January 23, 2020 19:04
msullivan added a commit that referenced this pull request Jan 23, 2020
This fixes some stuff in the mergechecker that breaks with current
mypy's and also some bugs that the mergechecker caught. The entire
fine-grained test suite now passes with the merge checker on, which I
don't think has ever been true before.

This means that we could turn it on by default, but it doubles the
runtime of the fine-grained tests (from 90s CPU time to 180s CPU time
on my laptop), so I've left it off for now.

The motivation here is that I knew intersect_callable's creation of
types during typechecking used to run afoul of the consistency checker
and so I was nervous that #8305 would cause more problems by adding
more logic of that kind. It no longer does, probably as a result of
the semantic analyzer rewrite, so I think we are in the clear on that.
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.

None yet

4 participants