Repository: python/mypy
Pull request: Whitelist some equality checks under --strict-equality
Fixes https://github.com/python/mypy/issues/7275

As I mentioned in the issue, we can just whitelist some pairs of classes.

Problem statement:
Unexpected error comparing set and frozenset for equality with --strict-equality
I just upgraded a local mypy installation to 0.720 (python 3.6.5) and was exploring `--strict-equality`.

My interpretation is that sets and frozensets can be compared just fine; as such the following error was rather unexpected:
```bash
$mypy --strict-equality /tmp/blah.py
blah.py:3: error: Non-overlapping equality check (left operand type: "FrozenSet[int]", right operand type: "Set[int]")
```
for
```python3
# blah.py
x = frozenset([1,2,3])
y = set([1,2,3])
if x == y:
    print("equal")
```
As expected, `python3 blah.py` gives `equal`.

Hints from issue discussion:
That is cute and frustrating. The error trips because the types aren't considered overlapping since neither is a superclass of the other, but nevertheless `__eq__` returns true because they are specifically built to handle that.

A related failure mode is `{1: 2}.keys() == {1}`, which fails due to non-overlapping equality of `Set` and `KeysView`.

If this is relatively uncommon, we can maybe handle it with some special casing (of `AbstractSet`, perhaps) in `dangerous_comparison`. If this turns out to happen more than I would guess, we might need to do something smart. (Looking at `__eq__` types, for example)
@msullivan I think we can special-case both cases.

Commit message: Whitelist some equality checks under --strict-equality

Patch to review:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index e74960241d1e..f7b168b78e4a 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -71,6 +71,15 @@
 MAX_UNIONS = 5  # type: Final
 
 
+# Types considered safe for comparisons with --strict-equality due to known behaviour of __eq__.
+OVERLAPPING_TYPES_WHITELIST = [('builtins.set', 'builtins.frozenset'),
+                               ('typing.KeysView', 'builtins.set'),
+                               ('typing.KeysView', 'builtins.frozenset')]  # type: Final
+
+for l, r in OVERLAPPING_TYPES_WHITELIST.copy():
+    OVERLAPPING_TYPES_WHITELIST.append((r, l))
+
+
 class TooManyUnions(Exception):
     """Indicates that we need to stop splitting unions in an attempt
     to match an overload in order to save performance.
@@ -2038,6 +2047,10 @@ def dangerous_comparison(self, left: Type, right: Type,
             # We need to special case bytes, because both 97 in b'abc' and b'a' in b'abc'
             # return True (and we want to show the error only if the check can _never_ be True).
             return False
+        if isinstance(left, Instance) and isinstance(right, Instance):
+            if any(left.type.fullname() == l and right.type.fullname() == r
+                   for l, r in OVERLAPPING_TYPES_WHITELIST):
+                return False
         return not is_overlapping_types(left, right, ignore_promotions=False)
 
     def get_operator_method(self, op: str) -> str:
diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test
index ecd98aeb5e68..f1f5870ae910 100644
--- a/test-data/unit/pythoneval.test
+++ b/test-data/unit/pythoneval.test
@@ -1385,3 +1385,20 @@ def thing(stuff: StuffDict) -> int: ...
 
 [out]
 _testNewAnalyzerTypedDictInStub_newsemanal.py:2: note: Revealed type is 'def (stuff: TypedDict('stub.StuffDict', {'foo': builtins.str, 'bar': builtins.int})) -> builtins.int'
+
+[case testStrictEqualityWhitelist]
+# mypy: strict-equality
+{1} == frozenset({1})
+frozenset({1}) == {1}
+
+frozenset({1}) == [1]  # Error
+
+{1: 2}.keys() == {1}
+{1: 2}.keys() == frozenset({1})
+
+{1: 2}.values() == {2}  # Error
+{1: 2}.keys() == [1]  # Error
+[out]
+_testStrictEqualityWhitelist.py:5: error: Non-overlapping equality check (left operand type: "FrozenSet[int]", right operand type: "List[int]")
+_testStrictEqualityWhitelist.py:10: error: Non-overlapping equality check (left operand type: "ValuesView[int]", right operand type: "Set[int]")
+_testStrictEqualityWhitelist.py:11: error: Non-overlapping equality check (left operand type: "KeysView[int]", right operand type: "List[int]")


Code review comments:
diff_hunk: @@ -71,6 +71,15 @@
 MAX_UNIONS = 5  # type: Final
 
 
+# Types considered safe for comparisons with --strict-equality due to known behaviour of __eq__.
+OVERLAPPING_TYPES_WHITELIST = [('builtins.set', 'builtins.frozenset'),
+                               ('typing.KeysView', 'builtins.set'),
+                               ('typing.KeysView', 'builtins.frozenset')]  # type: Final
path: mypy/checkexpr.py
text: @user1:
Is it worth doing Values also? (And Items?) (Genuinely not sure.)

@author:
Interesting:
```
>>> {1: 2}.values() == {2}
False
>>> {1: 2}.items() == {(1, 2)}
True
```
So it works for keys and items, but not values.

@user2:
There was actually a discussion about this on the python-ideas mailing list recently. Even comparing `d.values() == d.values()` (where d is a dict) returns False.