Repository: python/mypy

Issue:
mypy 0.730 no longer recognizes x**y literals to be ints
I noticed a regression when switching from 0.720 to 0.730. Here's the code:
```py
def f() -> int:
    size = 2 ** 20
    reveal_type(size)
    return size
```

**Expected behavior**

With mypy 0.720 (no matter whether the new or the old semantic analyzer is used), I get:
```
$ mypy --strict f.py
f.py:3: note: Revealed type is 'builtins.int'
```
**Actual behavior**

With mypy 0.730 (Python 3.6.8), I get:
```
$ mypy --strict f.py
f.py:3: note: Revealed type is 'Any'
f.py:4: error: Returning Any from function declared to return "int"
```

**Notes**

The behavior from 0.720 is what I'd expect.

One thing I noticed is that the typeshed indeed declares that `__pow__` for two `int`s returns `Any`. This makes sense because `2 ** -2` is a `float`. The only recent change which I can see in the typeshed though is the following: 
```diff
--- a/stdlib/2and3/builtins.pyi
+++ b/stdlib/2and3/builtins.pyi
@@ -168,7 +168,7 @@ class int:
     def __rtruediv__(self, x: int) -> float: ...
     def __rmod__(self, x: int) -> int: ...
     def __rdivmod__(self, x: int) -> Tuple[int, int]: ...
-    def __pow__(self, x: int) -> Any: ...  # Return type can be int or float, depending on x.
+    def __pow__(self, __x: int, __modulo: Optional[int] = ...) -> Any: ...  # Return type can be int or float, depending on x.
     def __rpow__(self, x: int) -> Any: ...
     def __and__(self, n: int) -> int: ...
```
(see: https://github.com/python/typeshed/commit/b2cd972b1760d850694772fe763e3027f131f223)

This means that the `Any` return type has always been there but `mypy` used to correctly recognize that `2 ** 20` is an `int`. It's difficult for me to tell whether the behavior in 0.730 is a regression compared to 0.720 or actually the desired behavior – it seems to me that `mypy` follows the typeshed more precisely here. However, maybe it's worth to handle literals like `2 ** 30` in a way that `mypy` knows that this is always `int`?


Discussion:
Investigating some, the issue is that mypy actually does already have a plugin to handle exponentiation by a constant as a special case, but adding a new argument to `__pow__` in typeshed broke it. Should be an easy fix.

Resolving patch:
diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py
--- a/mypy/plugins/default.py
+++ b/mypy/plugins/default.py
@@ -365,8 +365,10 @@ def typed_dict_update_signature_callback(ctx: MethodSigContext) -> CallableType:
 
 def int_pow_callback(ctx: MethodContext) -> Type:
     """Infer a more precise return type for int.__pow__."""
-    if (len(ctx.arg_types) == 1
-            and len(ctx.arg_types[0]) == 1):
+    # int.__pow__ has an optional modulo argument,
+    # so we expect 2 argument positions
+    if (len(ctx.arg_types) == 2
+            and len(ctx.arg_types[0]) == 1 and len(ctx.arg_types[1]) == 0):
         arg = ctx.args[0][0]
         if isinstance(arg, IntExpr):
             exponent = arg.value


Test patch:
diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py
--- a/mypy/test/testcmdline.py
+++ b/mypy/test/testcmdline.py
@@ -89,6 +89,8 @@ def test_python_cmdline(testcase: DataDrivenTestCase, step: int) -> None:
         if obvious_result != result:
             out.append('== Return code: {}'.format(result))
         expected_out = testcase.output if step == 1 else testcase.output2[step]
+        # Strip "tmp/" out of the test so that # E: works...
+        expected_out = [s.replace("tmp" + os.sep, "") for s in expected_out]
         assert_string_arrays_equal(expected_out, out,
                                    'Invalid output ({}, line {}){}'.format(
                                        testcase.file, testcase.line,
diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test
--- a/test-data/unit/check-expressions.test
+++ b/test-data/unit/check-expressions.test
@@ -2311,25 +2311,6 @@ class B: ...
 
 [builtins fixtures/dict.pyi]
 
--- Type checker default plugin
--- ---------------------------
-
-
-[case testIntPow]
-a = 1
-b = a + 2
-reveal_type(a**0) # N: Revealed type is 'builtins.int'
-reveal_type(a**1) # N: Revealed type is 'builtins.int'
-reveal_type(a**2) # N: Revealed type is 'builtins.int'
-reveal_type(a**-0) # N: Revealed type is 'builtins.int'
-reveal_type(a**-1) # N: Revealed type is 'builtins.float'
-reveal_type(a**(-2)) # N: Revealed type is 'builtins.float'
-reveal_type(a**b) # N: Revealed type is 'Any'
-reveal_type(a.__pow__(2)) # N: Revealed type is 'builtins.int'
-reveal_type(a.__pow__(a)) # N: Revealed type is 'Any'
-a.__pow__() # E: Too few arguments for "__pow__" of "int"
-[builtins fixtures/ops.pyi]
-
 [case testTypeAnnotationNeededMultipleAssignment]
 x, y = [], [] # E: Need type annotation for 'x' (hint: "x: List[<type>] = ...") \
             # E: Need type annotation for 'y' (hint: "y: List[<type>] = ...")
diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test
--- a/test-data/unit/cmdline.test
+++ b/test-data/unit/cmdline.test
@@ -578,6 +578,25 @@ python_version = 3.4
 python_version = 3.6
 [out]
 
+-- This should be a dumping ground for tests of plugins that are sensitive to
+-- typeshed changes.
+[case testTypeshedSensitivePlugins]
+# cmd: mypy int_pow.py
+
+[file int_pow.py]
+a = 1
+b = a + 2
+reveal_type(a**0) # N: Revealed type is 'builtins.int'
+reveal_type(a**1) # N: Revealed type is 'builtins.int'
+reveal_type(a**2) # N: Revealed type is 'builtins.int'
+reveal_type(a**-0) # N: Revealed type is 'builtins.int'
+reveal_type(a**-1) # N: Revealed type is 'builtins.float'
+reveal_type(a**(-2)) # N: Revealed type is 'builtins.float'
+reveal_type(a**b) # N: Revealed type is 'Any'
+reveal_type(a.__pow__(2)) # N: Revealed type is 'builtins.int'
+reveal_type(a.__pow__(a)) # N: Revealed type is 'Any'
+a.__pow__() # E: Too few arguments for "__pow__" of "int"
+
 [case testDisallowAnyUnimported]
 # cmd: mypy main.py
 [file mypy.ini]