[red-knot] Type inference for comparisons between arbitrary instances#13903
Conversation
|
Next task topics?
from the old-style iteration protocol doc class A:
def __getitem__(self, key: str) -> str:
return key
reveal_type("never" in A()) # revealed: Never |
|
Overall this looks fantastic; you're definitely on the right track. Thanks!! |
|
carljm
left a comment
There was a problem hiding this comment.
Thank you, this is excellent!!
crates/red_knot_python_semantic/resources/mdtest/comparison/instances.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
|
Oh sorry for requesting changes, I somehow failed to notice this PR was still in draft state |
94b99c1 to
4415380
Compare
|
Although this is a draft, the reviews have been helpful! Thank you!! And, I’ve separated the commits so that you can compare them with the initial draft version :) |
| # TODO: should be Literal[False], once type-checking for rich comparison operands is implemented | ||
| reveal_type(a == b) # revealed: bool | ||
|
|
||
| # TODO: should be Literal[True] | ||
| reveal_type(a != b) # revealed: @Todo | ||
| # TODO: should be Literal[True], once type-checking for rich comparison operands is implemented | ||
| reveal_type(a != b) # revealed: bool |
There was a problem hiding this comment.
It seems that inferring Literal types based on only rich comparison and typeshed may not be feasible.
The __eq__ method in both builtin.str and builtin.int has an argument type of object.
https://github.com/astral-sh/ruff/blob/main/crates/red_knot_vendored/vendor/typeshed/stdlib/builtins.pyi#L590
https://github.com/astral-sh/ruff/blob/main/crates/red_knot_vendored/vendor/typeshed/stdlib/builtins.pyi#L319
There was a problem hiding this comment.
The
__eq__method in both builtin.str and builtin.int has an argument type of object.
Right. The convention when it comes to annotating parameters on __eq__ and __ne__ is different from the other four rich-comparison methods. As far as I'm aware, this is unspecified.
The convention for nearly all rich-comparison and binary-arithmetic dunders is to annotate them in such a way that any call that would result in NotImplemented being returned would lead to the type checker emitting an error. So although this "works" at runtime (no error is raised, just NotImplemented returned), typeshed's annotations disallow it:
>>> (2).__lt__(2.0)
NotImplementedBut for __eq__ and __ne__, the convention is to annotate parameters such that any calls that would succeed at runtime are allowed, even if those successful calls would return NotImplemented. I can think of two good reasons for this:
__eq__and__ne__really are different from all the others: unlike the other rich-comparison operators and the binary-arithmetic operators,x == ywill always result in abool(neverTypeError), even iftype(x).__eq__(y)returnsNotImplementedandtype(y).__eq__(x)returnsNotImplemented.- It's useful for type checkers to enforce the Liskov Substitution Principle for equality. If a class raises an exception during an equality check, that seriously violates some assumptions that are baked in across the language, standard library, and many third-party libraries. It's useful to be able to distinguish "this
__eq__method returnsNotImplementedif you pass it afloat" and "this__eq__method raisesTypeErrororAttributeErrorif you pass it afloat", because the first is fine but the second is very bad indeed. With the current convention, type checkers will warn you if you say that an__eq__method has to accept a non-objecttype, because that's an incompatible override ofobject.__eq__(they're able to detect the Liskov violation).
There was a problem hiding this comment.
In this particular case, I think we should be able to infer Literal False and True, without ever falling back to typeshed, because we should explicitly implement equality comparison for mismatched Literal types to return Literal[False]. (Currently we just don't have a match arm for that case, so we fall back to the typeshed definitions instead.)
AlexWaygood
left a comment
There was a problem hiding this comment.
Thanks! This review only reviews your tests ;)
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
carljm
left a comment
There was a problem hiding this comment.
Code here looks great to me!! Thank you for following up with all the review comments.
I do have a bunch more review comments, but they are all about tests and mostly about TODOs :)
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/unsupported.md
Outdated
Show resolved
Hide resolved
|
Thanks so much for the super detailed and helpful review..!! I’ve already made updates based on your reviews. Since the code’s mostly complete and I think I’ll be in a place where it’ll be hard to make code changes for a while 😢😢😢, feel free to commit any needed fixes directly! |
AlexWaygood
left a comment
There was a problem hiding this comment.
Some more docs nitpicks. I don't think any are controversial, so I'll just push them to your branch :-)
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/membership_test.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/comparison/instances/rich_comparison.md
Outdated
Show resolved
Hide resolved
| # TODO: it should be `bool`, need to check arg type and fall back to `is` and `is not` | ||
| reveal_type(A() == A()) # revealed: A | ||
| reveal_type(A() != A()) # revealed: A |
There was a problem hiding this comment.
Yes, just because we should emit a diagnostic telling the user off doesn't mean we should ignore what the annotation says! So this looks correct to me.
There was a problem hiding this comment.
This looks ready to land to me! @AlexWaygood since you just reviewed as well, I'll assume you're good with it? I'll go ahead and enable auto-merge. Feel free to cancel if there's more you want to look at first.
AlexWaygood
left a comment
There was a problem hiding this comment.
Fantastic work. Thanks @cake-monotone!
Summary
Closes #13872
Test Plan
mdtest included!
comparisons/instances/rich_comparison.mdcomparisons/instances/membership_test.md