Repository: quora/pyanalyze
Pull request #480 (2022-02-12 05:08:30)

in and not in always return bool
The easy part of #472


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index a52b5fc..544e978 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,8 @@
 
 ## Unreleased
 
+- The `in` and `not in` operators always return 
+  booleans (#480)
 - Allow `NotImplemented` to be returned from special
   methods that support it (#479)
 - Fix bug affecting type compatibility between
diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py
index cc6c547..3c6a94a 100644
--- a/pyanalyze/name_check_visitor.py
+++ b/pyanalyze/name_check_visitor.py
@@ -2727,7 +2727,10 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
             )
         else:
             constraint = NULL_CONSTRAINT
-        if isinstance(op, (ast.Is, ast.IsNot)):
+        # is, is not, in, and not in always return a boolean, but the other
+        # comparisons may return arbitrary objects. We should get the
+        # return value out of the dunder methods, but we don't do that yet.
+        if isinstance(op, (ast.Is, ast.IsNot, ast.In, ast.NotIn)):
             val = TypedValue(bool)
         else:
             val = AnyValue(AnySource.inference)


Test patch:
diff --git a/pyanalyze/test_name_check_visitor.py b/pyanalyze/test_name_check_visitor.py
index 95d35c7..ca06084 100644
--- a/pyanalyze/test_name_check_visitor.py
+++ b/pyanalyze/test_name_check_visitor.py
@@ -448,44 +448,6 @@ class TestNameCheckVisitor(TestNameCheckVisitorBase):
         def run():
             tpl[1] = 1  # E: unsupported_operation
 
-    @assert_passes()
-    def test_binop(self):
-        from typing import Union
-
-        def tucotuco():
-            assert_is_value(2 + 3, KnownValue(5))
-
-        def capybara(x: Union[int, float], y: Union[int, float]) -> float:
-            return x + y
-
-    @assert_passes()
-    def test_inplace_binop(self):
-        class Capybara:
-            def __add__(self, x: int) -> str:
-                return ""
-
-            def __iadd__(self, x: str) -> int:
-                return 0
-
-        def tucotuco():
-            x = Capybara()
-            assert_is_value(x + 1, TypedValue(str))
-            x += "a"
-            assert_is_value(x, TypedValue(int))
-
-    @assert_passes()
-    def test_binop_notimplemented(self):
-        from pyanalyze.extensions import assert_type
-
-        class Capybara:
-            def __add__(self, x: str) -> bool:
-                if not isinstance(x, str):
-                    return NotImplemented
-                return len(x) > 3
-
-        def pacarana():
-            assert_type(Capybara() + "x", bool)
-
     @assert_passes()
     def test_global_sets_value(self):
         capybara = None
@@ -737,57 +699,6 @@ class TestSubclassValue(TestNameCheckVisitorBase):
             capybara(str)
 
 
-class TestBoolOp(TestNameCheckVisitorBase):
-    @assert_passes()
-    def test(self):
-        def capybara(x):
-            if x:
-                cond = str(x)
-                cond2 = True
-            else:
-                cond = None
-                cond2 = None
-            assert_is_value(cond, MultiValuedValue([TypedValue(str), KnownValue(None)]))
-            assert_is_value(
-                cond2, MultiValuedValue([KnownValue(True), KnownValue(None)])
-            )
-            assert_is_value(
-                cond and 1,
-                MultiValuedValue([TypedValue(str), KnownValue(None), KnownValue(1)]),
-                skip_annotated=True,
-            )
-            assert_is_value(
-                cond2 and 1,
-                MultiValuedValue([KnownValue(None), KnownValue(1)]),
-                skip_annotated=True,
-            )
-            assert_is_value(
-                cond or 1,
-                MultiValuedValue([TypedValue(str), KnownValue(1)]),
-                skip_annotated=True,
-            )
-            assert_is_value(
-                cond2 or 1,
-                MultiValuedValue([KnownValue(True), KnownValue(1)]),
-                skip_annotated=True,
-            )
-
-        def hutia(x=None):
-            assert_is_value(x, AnyValue(AnySource.unannotated) | KnownValue(None))
-            assert_is_value(
-                x or 1,
-                AnyValue(AnySource.unannotated) | KnownValue(1),
-                skip_annotated=True,
-            )
-            y = x or 1
-            assert_is_value(
-                y, AnyValue(AnySource.unannotated) | KnownValue(1), skip_annotated=True
-            )
-            assert_is_value(
-                (True if x else False) or None, KnownValue(True) | KnownValue(None)
-            )
-
-
 class TestReturn(TestNameCheckVisitorBase):
     @assert_passes()
     def test_type_inference(self):
@@ -1134,10 +1045,10 @@ class TestImports(TestNameCheckVisitorBase):
     def test_star_import(self):
         self.assert_passes(
             """
-from qcore.asserts import *
+            from qcore.asserts import *
 
-assert_eq(1, 1)
-"""
+            assert_eq(1, 1)
+            """
         )
 
     @assert_passes()
@@ -1574,73 +1485,6 @@ class TestPython3Compatibility(TestNameCheckVisitorBase):
             return "foo" + b"bar"  # E: unsupported_operation
 
 
-class TestOperators(TestNameCheckVisitorBase):
-    @assert_passes(settings={ErrorCode.value_always_true: False})
-    def test_not(self):
-        def capybara(x):
-            assert_is_value(not x, TypedValue(bool), skip_annotated=True)
-            assert_is_value(not True, KnownValue(False))
-
-    @assert_passes()
-    def test_unary_op(self):
-        def capybara(x):
-            assert_is_value(~x, AnyValue(AnySource.from_another))
-            assert_is_value(~3, KnownValue(-4))
-
-    @assert_passes()
-    def test_binop_type_inference(self):
-        def capybara(x):
-            assert_is_value(1 + int(x), TypedValue(int))
-            assert_is_value(3 * int(x), TypedValue(int))
-            assert_is_value("foo" + str(x), TypedValue(str))
-            assert_is_value(1 + float(x), TypedValue(float))
-            assert_is_value(1.0 + int(x), TypedValue(float))
-            assert_is_value(3 * 3.0 + 1, KnownValue(10.0))
-
-    @assert_passes()
-    def test_union(self):
-        from typing import Union
-
-        def capybara(x: Union[int, str]) -> None:
-            assert_is_value(x * 3, MultiValuedValue([TypedValue(int), TypedValue(str)]))
-
-    @assert_passes()
-    def test_rop(self):
-        class HasAdd:
-            def __add__(self, other: int) -> "HasAdd":
-                raise NotImplementedError
-
-        class HasRadd:
-            def __radd__(self, other: int) -> "HasRadd":
-                raise NotImplementedError
-
-        class HasBoth:
-            def __add__(self, other: "HasBoth") -> "HasBoth":
-                raise NotImplementedError
-
-            def __radd__(self, other: "HasBoth") -> int:
-                raise NotImplementedError
-
-        def capybara(x):
-            ha = HasAdd()
-            hr = HasRadd()
-            assert_is_value(1 + hr, TypedValue(HasRadd))
-            assert_is_value(x + hr, AnyValue(AnySource.from_another))
-            assert_is_value(ha + 1, TypedValue(HasAdd))
-            assert_is_value(ha + x, AnyValue(AnySource.from_another))
-            assert_is_value(HasBoth() + HasBoth(), TypedValue(HasBoth))
-
-    @assert_passes()
-    def test_unsupported_unary_op(self):
-        def capybara():
-            ~"capybara"  # E: unsupported_operation
-
-    @assert_passes()
-    def test_int_float_product(self):
-        def capybara(f: float, i: int):
-            assert_is_value(i * f, TypedValue(float))
-
-
 class TestTaskNeedsYield(TestNameCheckVisitorBase):
     @assert_fails(ErrorCode.task_needs_yield)
     def test_constfuture(self):
@@ -1801,15 +1645,6 @@ class TestBadAsyncYield(TestNameCheckVisitorBase):
             assert_is_value(val3, KnownValue(4) | AnyValue(AnySource.inference))
 
 
-class TestAugAssign(TestNameCheckVisitorBase):
-    @assert_passes()
-    def test_aug_assign(self):
-        def capybara(condition):
-            x = 1
-            x += 2
-            assert_is_value(x, KnownValue(3))
-
-
 class TestUnpacking(TestNameCheckVisitorBase):
     @assert_passes()
     def test_dict_unpacking(self):
@@ -2289,19 +2124,6 @@ def test_static_hasattr():
     assert not _static_hasattr(hgat, "random_attribute")
 
 
-class TestCompare(TestNameCheckVisitorBase):
-    @assert_passes()
-    def test_multi(self):
-        from typing_extensions import Literal
-
-        def capybara(i: Literal[1, 2, 3], x: Literal[3, 4]) -> None:
-            assert_is_value(i, KnownValue(1) | KnownValue(2) | KnownValue(3))
-            assert_is_value(x, KnownValue(3) | KnownValue(4))
-            if 1 < i < 3 != x:
-                assert_is_value(i, KnownValue(2))
-                assert_is_value(x, KnownValue(4))
-
-
 class TestIncompatibleOverride(TestNameCheckVisitorBase):
     @assert_passes()
     def test_simple(self):
diff --git a/pyanalyze/test_operations.py b/pyanalyze/test_operations.py
new file mode 100644
index 0000000..0fb7ae2
--- /dev/null
+++ b/pyanalyze/test_operations.py
@@ -0,0 +1,193 @@
+# static analysis: ignore
+from .implementation import assert_is_value
+from .value import AnySource, AnyValue, KnownValue, MultiValuedValue, TypedValue
+from .test_name_check_visitor import TestNameCheckVisitorBase
+from .test_node_visitor import assert_passes
+from .error_code import ErrorCode
+
+
+class TestBinOps(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test_binop(self):
+        from typing import Union
+
+        def tucotuco():
+            assert_is_value(2 + 3, KnownValue(5))
+
+        def capybara(x: Union[int, float], y: Union[int, float]) -> float:
+            return x + y
+
+    @assert_passes()
+    def test_inplace_binop(self):
+        class Capybara:
+            def __add__(self, x: int) -> str:
+                return ""
+
+            def __iadd__(self, x: str) -> int:
+                return 0
+
+        def tucotuco():
+            x = Capybara()
+            assert_is_value(x + 1, TypedValue(str))
+            x += "a"
+            assert_is_value(x, TypedValue(int))
+
+    @assert_passes()
+    def test_binop_notimplemented(self):
+        from pyanalyze.extensions import assert_type
+
+        class Capybara:
+            def __add__(self, x: str) -> bool:
+                if not isinstance(x, str):
+                    return NotImplemented
+                return len(x) > 3
+
+        def pacarana():
+            assert_type(Capybara() + "x", bool)
+
+
+class TestBoolOp(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test(self):
+        def capybara(x):
+            if x:
+                cond = str(x)
+                cond2 = True
+            else:
+                cond = None
+                cond2 = None
+            assert_is_value(cond, MultiValuedValue([TypedValue(str), KnownValue(None)]))
+            assert_is_value(
+                cond2, MultiValuedValue([KnownValue(True), KnownValue(None)])
+            )
+            assert_is_value(
+                cond and 1,
+                MultiValuedValue([TypedValue(str), KnownValue(None), KnownValue(1)]),
+                skip_annotated=True,
+            )
+            assert_is_value(
+                cond2 and 1,
+                MultiValuedValue([KnownValue(None), KnownValue(1)]),
+                skip_annotated=True,
+            )
+            assert_is_value(
+                cond or 1,
+                MultiValuedValue([TypedValue(str), KnownValue(1)]),
+                skip_annotated=True,
+            )
+            assert_is_value(
+                cond2 or 1,
+                MultiValuedValue([KnownValue(True), KnownValue(1)]),
+                skip_annotated=True,
+            )
+
+        def hutia(x=None):
+            assert_is_value(x, AnyValue(AnySource.unannotated) | KnownValue(None))
+            assert_is_value(
+                x or 1,
+                AnyValue(AnySource.unannotated) | KnownValue(1),
+                skip_annotated=True,
+            )
+            y = x or 1
+            assert_is_value(
+                y, AnyValue(AnySource.unannotated) | KnownValue(1), skip_annotated=True
+            )
+            assert_is_value(
+                (True if x else False) or None, KnownValue(True) | KnownValue(None)
+            )
+
+
+class TestOperators(TestNameCheckVisitorBase):
+    @assert_passes(settings={ErrorCode.value_always_true: False})
+    def test_not(self):
+        def capybara(x):
+            assert_is_value(not x, TypedValue(bool), skip_annotated=True)
+            assert_is_value(not True, KnownValue(False))
+
+    @assert_passes()
+    def test_unary_op(self):
+        def capybara(x):
+            assert_is_value(~x, AnyValue(AnySource.from_another))
+            assert_is_value(~3, KnownValue(-4))
+
+    @assert_passes()
+    def test_binop_type_inference(self):
+        def capybara(x):
+            assert_is_value(1 + int(x), TypedValue(int))
+            assert_is_value(3 * int(x), TypedValue(int))
+            assert_is_value("foo" + str(x), TypedValue(str))
+            assert_is_value(1 + float(x), TypedValue(float))
+            assert_is_value(1.0 + int(x), TypedValue(float))
+            assert_is_value(3 * 3.0 + 1, KnownValue(10.0))
+
+    @assert_passes()
+    def test_union(self):
+        from typing import Union
+
+        def capybara(x: Union[int, str]) -> None:
+            assert_is_value(x * 3, MultiValuedValue([TypedValue(int), TypedValue(str)]))
+
+    @assert_passes()
+    def test_rop(self):
+        class HasAdd:
+            def __add__(self, other: int) -> "HasAdd":
+                raise NotImplementedError
+
+        class HasRadd:
+            def __radd__(self, other: int) -> "HasRadd":
+                raise NotImplementedError
+
+        class HasBoth:
+            def __add__(self, other: "HasBoth") -> "HasBoth":
+                raise NotImplementedError
+
+            def __radd__(self, other: "HasBoth") -> int:
+                raise NotImplementedError
+
+        def capybara(x):
+            ha = HasAdd()
+            hr = HasRadd()
+            assert_is_value(1 + hr, TypedValue(HasRadd))
+            assert_is_value(x + hr, AnyValue(AnySource.from_another))
+            assert_is_value(ha + 1, TypedValue(HasAdd))
+            assert_is_value(ha + x, AnyValue(AnySource.from_another))
+            assert_is_value(HasBoth() + HasBoth(), TypedValue(HasBoth))
+
+    @assert_passes()
+    def test_unsupported_unary_op(self):
+        def capybara():
+            ~"capybara"  # E: unsupported_operation
+
+    @assert_passes()
+    def test_int_float_product(self):
+        def capybara(f: float, i: int):
+            assert_is_value(i * f, TypedValue(float))
+
+    @assert_passes()
+    def test_contains(self):
+        from pyanalyze.extensions import assert_type
+
+        def capybara(x, y):
+            assert_type(x in y, bool)
+
+
+class TestAugAssign(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test_aug_assign(self):
+        def capybara(condition):
+            x = 1
+            x += 2
+            assert_is_value(x, KnownValue(3))
+
+
+class TestCompare(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test_multi(self):
+        from typing_extensions import Literal
+
+        def capybara(i: Literal[1, 2, 3], x: Literal[3, 4]) -> None:
+            assert_is_value(i, KnownValue(1) | KnownValue(2) | KnownValue(3))
+            assert_is_value(x, KnownValue(3) | KnownValue(4))
+            if 1 < i < 3 != x:
+                assert_is_value(i, KnownValue(2))
+                assert_is_value(x, KnownValue(4))