Repository: quora/pyanalyze (python)

Issue:
Accept IntEnum as float
```py
import enum

class A(enum.IntEnum):
    a = 1
    b = 2
    c = 3

def f(arg: int) -> None:
    ...

f(A.c)  # IntEnum -> int: ok


def g(arg: float) -> None:
    ...

a: int = 100
g(a)  # int -> float : ok
g(A.b)  # IntEnum -> float : incompatible_argument
```

```
Incompatible argument type for arg: expected float but got Literal[<A.b: 2>] (code: incompatible_argument)
  Cannot assign Literal[<A.b: 2>] to float
```

Should we accept IntEnum as float? mypy is fine with this.


Pull request description:
Apply implicit int/float promotion to subclasses
Fixes #736

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 5349901..480eaf5 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Treat subclasses of `int` as subclasses of `float` and `complex` too (#738)
 - Fix crash on encountering certain decorators in stubs (#734)
 - Fix inference of signature for certain secondary methods (#732)
 
diff --git a/pyanalyze/type_object.py b/pyanalyze/type_object.py
index edff8fa..4c06247 100644
--- a/pyanalyze/type_object.py
+++ b/pyanalyze/type_object.py
@@ -68,10 +68,10 @@ class TypeObject:
         self.base_classes |= set(get_mro(self.typ))
         # As a special case, the Python type system treats int as
         # a subtype of float, and both int and float as subtypes of complex.
-        if self.typ is int:
+        if self.typ is int or safe_in(int, self.base_classes):
             self.artificial_bases.add(float)
             self.artificial_bases.add(complex)
-        if self.typ is float:
+        if self.typ is float or safe_in(float, self.base_classes):
             self.artificial_bases.add(complex)
         if self.is_thrift_enum:
             self.artificial_bases.add(int)


Test patch:
diff --git a/pyanalyze/test_type_object.py b/pyanalyze/test_type_object.py
index 0489ba8..6cc4d6b 100644
--- a/pyanalyze/test_type_object.py
+++ b/pyanalyze/test_type_object.py
@@ -23,6 +23,9 @@ class TestNumerics(TestNameCheckVisitorBase):
         def take_float(x: float) -> None:
             pass
 
+        class IntSubclass(int):
+            pass
+
         def capybara(nt: NT, i: int, f: float) -> None:
             take_float(nt)
             take_float(i)
@@ -31,11 +34,9 @@ class TestNumerics(TestNameCheckVisitorBase):
             take_float(3)
             take_float(1 + 1j)  # E: incompatible_argument
             take_float("string")  # E: incompatible_argument
-            # Strictly speaking this should be allowed, but it doesn't
-            # seem useful and I don't know of a concrete use case that would
-            # require this to work. This can be revisited if we do find a use
-            # case.
-            take_float(True)  # E: incompatible_argument
+            # bool is a subclass of int, which is treated as a subclass of float
+            take_float(True)
+            take_float(IntSubclass(3))
 
     @assert_passes()
     def test_complex(self):
@@ -57,7 +58,7 @@ class TestNumerics(TestNameCheckVisitorBase):
             take_complex(3)
             take_complex(1 + 1j)
             take_complex("string")  # E: incompatible_argument
-            take_complex(True)  # E: incompatible_argument
+            take_complex(True)  # bool is an int, which is a float, which is a complex
 
 
 class TestSyntheticType(TestNameCheckVisitorBase):