Skip to content

[red-knot] Type inference for comparisons between arbitrary instances#13903

Merged
AlexWaygood merged 14 commits intoastral-sh:mainfrom
cake-monotone:red-knot-implement-comparison-instances
Oct 26, 2024
Merged

[red-knot] Type inference for comparisons between arbitrary instances#13903
AlexWaygood merged 14 commits intoastral-sh:mainfrom
cake-monotone:red-knot-implement-comparison-instances

Conversation

@cake-monotone
Copy link
Contributor

@cake-monotone cake-monotone commented Oct 24, 2024

Summary

  • Implemented "rich comparison" for instance comparisons.
  • Implemented "membership test" for instance comparisons.

Closes #13872

Test Plan

mdtest included!

  • comparisons/instances/rich_comparison.md
  • comparisons/instances/membership_test.md
  • (Resolved some TODO in other md tests)

@cake-monotone cake-monotone marked this pull request as draft October 24, 2024 10:04
@cake-monotone
Copy link
Contributor Author

Next task topics?

  • We should infer Never from the membership test for some special cases.

from the old-style iteration protocol doc

class A:
    def __getitem__(self, key: str) -> str:
        return key

reveal_type("never" in A())  # revealed: Never

@AlexWaygood
Copy link
Member

Overall this looks fantastic; you're definitely on the right track. Thanks!!

@github-actions
Copy link
Contributor

github-actions bot commented Oct 24, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Thank you, this is excellent!!

@carljm
Copy link
Contributor

carljm commented Oct 24, 2024

Oh sorry for requesting changes, I somehow failed to notice this PR was still in draft state

@cake-monotone cake-monotone force-pushed the red-knot-implement-comparison-instances branch from 94b99c1 to 4415380 Compare October 25, 2024 09:43
@cake-monotone
Copy link
Contributor Author

Although this is a draft, the reviews have been helpful! Thank you!!
Next time, I'll make sure to clearly label the title with WIP. 🥲🥲

And, I’ve separated the commits so that you can compare them with the initial draft version :)

cake-monotone

This comment was marked as duplicate.

Comment on lines +91 to +95
# 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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

@AlexWaygood AlexWaygood Oct 25, 2024

Choose a reason for hiding this comment

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

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)
NotImplemented

But 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:

  1. __eq__ and __ne__ really are different from all the others: unlike the other rich-comparison operators and the binary-arithmetic operators, x == y will always result in a bool (never TypeError), even if type(x).__eq__(y) returns NotImplemented and type(y).__eq__(x) returns NotImplemented.
  2. 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 returns NotImplemented if you pass it a float" and "this __eq__ method raises TypeError or AttributeError if you pass it a float", 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-object type, because that's an incompatible override of object.__eq__ (they're able to detect the Liskov violation).

Copy link
Contributor

@carljm carljm Oct 25, 2024

Choose a reason for hiding this comment

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

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.)

@cake-monotone cake-monotone marked this pull request as ready for review October 25, 2024 10:13
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Thanks! This review only reviews your tests ;)

@MichaReiser MichaReiser added the ty Multi-file analysis & type inference label Oct 25, 2024
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

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 :)

@cake-monotone
Copy link
Contributor Author

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!

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Some more docs nitpicks. I don't think any are controversial, so I'll just push them to your branch :-)

Comment on lines +263 to +265
# 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
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

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.

@carljm carljm enabled auto-merge (squash) October 26, 2024 18:04
@AlexWaygood AlexWaygood disabled auto-merge October 26, 2024 18:06
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Fantastic work. Thanks @cake-monotone!

@AlexWaygood AlexWaygood changed the title [red-knot] Compare expression inference - Instance [red-knot] Type inference for comparisons between arbitrary instances Oct 26, 2024
@AlexWaygood AlexWaygood enabled auto-merge (squash) October 26, 2024 18:15
@AlexWaygood AlexWaygood merged commit b6ffa51 into astral-sh:main Oct 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[red-knot] Compare expression inference - Instance (+ Rich Comparison, Membership Test)

4 participants