Repository: python/mypy

Issue:
Python 2 future import can leak between modules
This test case fails unexpectedly:

```
[case testDivPython2FutureImportNotLeaking]
# flags: --python-version 2.7
import m1

[file m1.py]
import m2

class A(object):
    def __div__(self, other):
        # type: (str) -> str
        return 'a'

reveal_type(A() / 'b')  # N: Revealed type is "builtins.str"

[file m2.py]
from __future__ import division
[builtins fixtures/bool.pyi]
```

If I comment out the `from __future__` import, the test case passes. It looks like the `__future__` import leaks from `m2` to `m1`.  This was introduced in python/mypy#11276.

Since we are close to creating a release brach, and the fix may be non-trivial, I'm going to revert python/mypy#11276 to avoid a regression. The PR can be merged again once the issue has been fixed.

cc @sobolevn 


Discussion:
Ok, I will take a look why future imports leak and re-create this PR with your test. Thanks!

Patch:
diff --git a/mypy/build.py b/mypy/build.py
--- a/mypy/build.py
+++ b/mypy/build.py
@@ -2188,8 +2188,10 @@ def type_checker(self) -> TypeChecker:
         if not self._type_checker:
             assert self.tree is not None, "Internal error: must be called on parsed file only"
             manager = self.manager
-            self._type_checker = TypeChecker(manager.errors, manager.modules, self.options,
-                                             self.tree, self.xpath, manager.plugin)
+            self._type_checker = TypeChecker(
+                manager.errors, manager.modules, self.options,
+                self.tree, self.xpath, manager.plugin,
+            )
         return self._type_checker
 
     def type_map(self) -> Dict[Expression, Type]:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -2414,8 +2414,11 @@ def dangerous_comparison(self, left: Type, right: Type,
 
     def get_operator_method(self, op: str) -> str:
         if op == '/' and self.chk.options.python_version[0] == 2:
-            # TODO also check for "from __future__ import division"
-            return '__div__'
+            return (
+                '__truediv__'
+                if self.chk.tree.is_future_flag_set('division')
+                else '__div__'
+            )
         else:
             return operators.op_methods[op]
 
diff --git a/mypy/nodes.py b/mypy/nodes.py
--- a/mypy/nodes.py
+++ b/mypy/nodes.py
@@ -255,7 +255,8 @@ class MypyFile(SymbolNode):
 
     __slots__ = ('_fullname', 'path', 'defs', 'alias_deps',
                  'is_bom', 'names', 'imports', 'ignored_lines', 'is_stub',
-                 'is_cache_skeleton', 'is_partial_stub_package', 'plugin_deps')
+                 'is_cache_skeleton', 'is_partial_stub_package', 'plugin_deps',
+                 'future_import_flags')
 
     # Fully qualified module name
     _fullname: Bogus[str]
@@ -284,6 +285,8 @@ class MypyFile(SymbolNode):
     is_partial_stub_package: bool
     # Plugin-created dependencies
     plugin_deps: Dict[str, Set[str]]
+    # Future imports defined in this file. Populated during semantic analysis.
+    future_import_flags: Set[str]
 
     def __init__(self,
                  defs: List[Statement],
@@ -306,6 +309,7 @@ def __init__(self,
         self.is_stub = False
         self.is_cache_skeleton = False
         self.is_partial_stub_package = False
+        self.future_import_flags = set()
 
     def local_definitions(self) -> Iterator[Definition]:
         """Return all definitions within the module (including nested).
@@ -328,6 +332,9 @@ def accept(self, visitor: NodeVisitor[T]) -> T:
     def is_package_init_file(self) -> bool:
         return len(self.path) != 0 and os.path.basename(self.path).startswith('__init__.')
 
+    def is_future_flag_set(self, flag: str) -> bool:
+        return flag in self.future_import_flags
+
     def serialize(self) -> JsonDict:
         return {'.class': 'MypyFile',
                 '_fullname': self._fullname,
@@ -335,6 +342,7 @@ def serialize(self) -> JsonDict:
                 'is_stub': self.is_stub,
                 'path': self.path,
                 'is_partial_stub_package': self.is_partial_stub_package,
+                'future_import_flags': list(self.future_import_flags),
                 }
 
     @classmethod
@@ -347,6 +355,7 @@ def deserialize(cls, data: JsonDict) -> 'MypyFile':
         tree.path = data['path']
         tree.is_partial_stub_package = data['is_partial_stub_package']
         tree.is_cache_skeleton = True
+        tree.future_import_flags = set(data['future_import_flags'])
         return tree
 
 
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -219,7 +219,6 @@ class SemanticAnalyzer(NodeVisitor[None],
     errors: Errors  # Keeps track of generated errors
     plugin: Plugin  # Mypy plugin for special casing of library features
     statement: Optional[Statement] = None  # Statement/definition being analyzed
-    future_import_flags: Set[str]
 
     # Mapping from 'async def' function definitions to their return type wrapped as a
     # 'Coroutine[Any, Any, T]'. Used to keep track of whether a function definition's
@@ -284,8 +283,6 @@ def __init__(self,
         # current SCC or top-level function.
         self.deferral_debug_context: List[Tuple[str, int]] = []
 
-        self.future_import_flags: Set[str] = set()
-
     # mypyc doesn't properly handle implementing an abstractproperty
     # with a regular attribute so we make them properties
     @property
@@ -5252,10 +5249,12 @@ def parse_bool(self, expr: Expression) -> Optional[bool]:
 
     def set_future_import_flags(self, module_name: str) -> None:
         if module_name in FUTURE_IMPORTS:
-            self.future_import_flags.add(FUTURE_IMPORTS[module_name])
+            self.modules[self.cur_mod_id].future_import_flags.add(
+                FUTURE_IMPORTS[module_name],
+            )
 
     def is_future_flag_set(self, flag: str) -> bool:
-        return flag in self.future_import_flags
+        return self.modules[self.cur_mod_id].is_future_flag_set(flag)
 
 
 class HasPlaceholders(TypeQuery[bool]):


Test patch:
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
@@ -202,6 +202,70 @@ class C:
     pass
 [builtins fixtures/tuple.pyi]
 
+[case testDivPython2]
+# flags: --python-version 2.7
+class A(object):
+    def __div__(self, other):
+        # type: (A, str) -> str
+        return 'a'
+
+a = A()
+reveal_type(a / 'b')  # N: Revealed type is "builtins.str"
+a / 1  # E: Unsupported operand types for / ("A" and "int")
+[builtins fixtures/bool.pyi]
+
+[case testDivPython2FutureImport]
+# flags: --python-version 2.7
+from __future__ import division
+
+class A(object):
+    def __truediv__(self, other):
+        # type: (A, str) -> str
+        return 'a'
+
+a = A()
+reveal_type(a / 'b')  # N: Revealed type is "builtins.str"
+a / 1  # E: Unsupported operand types for / ("A" and "int")
+[builtins fixtures/bool.pyi]
+
+[case testDivPython2FutureImportNotLeaking]
+# flags: --python-version 2.7
+import m1
+
+[file m1.py]
+import m2
+
+class A(object):
+    def __div__(self, other):
+        # type: (A, str) -> str
+        return 'a'
+
+a = A()
+reveal_type(a / 'b')  # N: Revealed type is "builtins.str"
+a / 1  # E: Unsupported operand types for / ("A" and "int")
+[file m2.py]
+from __future__ import division
+[builtins fixtures/bool.pyi]
+
+[case testDivPython2FutureImportNotLeaking2]
+# flags: --python-version 2.7
+import m1
+
+[file m1.py]
+import m2
+
+class A(object):
+    def __div__(self, other):
+        # type: (A, str) -> str
+        return 'a'
+
+a = A()
+reveal_type(a / 'b')  # N: Revealed type is "builtins.str"
+a / 1  # E: Unsupported operand types for / ("A" and "int")
+[file m2.py]
+# empty
+[builtins fixtures/bool.pyi]
+
 [case testIntDiv]
 a, b, c = None, None, None # type: (A, B, C)
 if int():