From 14de8c6f1f24648299528881561ddcd52172701d Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Wed, 23 Feb 2022 21:16:02 +0300 Subject: [PATCH] Use `__truediv__` for python2 with `__future__` import, refs #11740 (#11787) Now `__future__` imports do not leak in semanal.py. Closes #11740 Refs #11741 Refs #11276 Refs #11700 --- mypy/build.py | 6 ++- mypy/checkexpr.py | 7 ++- mypy/nodes.py | 11 ++++- mypy/semanal.py | 9 ++-- test-data/unit/check-expressions.test | 64 +++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 10 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 24cef0b83bd3..3ddbe40f1a90 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2186,8 +2186,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 index 36f92898edb7..4ece9ac4c94b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2416,8 +2416,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 index 49edd16a3085..f520280dce2d 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -258,7 +258,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] @@ -287,6 +288,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], @@ -309,6 +312,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). @@ -331,6 +335,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, @@ -338,6 +345,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 @@ -350,6 +358,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 index c83bc01908ad..02ce361ab5d3 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -226,7 +226,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 @@ -291,8 +290,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 @@ -5374,10 +5371,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]): diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 6c6a5d473f01..46f0cf02a125 100644 --- 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():