From 2f9d8affe23624185b561fdb804db5ce5fb57325 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 18 Dec 2021 15:52:50 +0300 Subject: [PATCH 1/3] Revert "Revert "Imporves `__div__` with `__future__` import on py2 (#11276)" (#11741)" This reverts commit 538e7e64fd32124f5df42f6ca1b045955892af14. --- mypy/build.py | 7 +++++-- mypy/checker.py | 8 +++++++- mypy/checkexpr.py | 3 +-- test-data/unit/check-expressions.test | 26 ++++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 5eebc0a22a85..d786922a4564 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2188,8 +2188,11 @@ 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, + self.manager.semantic_analyzer.future_import_flags, + ) return self._type_checker def type_map(self) -> Dict[Expression, Type]: diff --git a/mypy/checker.py b/mypy/checker.py index b90221a0a5a5..be175cf54d19 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -221,8 +221,12 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface): # functions such as open(), etc. plugin: Plugin + # Future flags that we get from semantic analyzer. + future_import_flags: Set[str] + def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Options, - tree: MypyFile, path: str, plugin: Plugin) -> None: + tree: MypyFile, path: str, plugin: Plugin, + future_import_flags: Set[str]) -> None: """Construct a type checker. Use errors to report type check errors. @@ -268,6 +272,8 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option # argument through various `checker` and `checkmember` functions. self._is_final_def = False + self.future_import_flags = future_import_flags + @property def type_context(self) -> List[Optional[Type]]: return self.expr_checker.type_context diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1647339ef217..93df9c612126 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2414,8 +2414,7 @@ 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 'division' in self.chk.future_import_flags else '__div__' else: return operators.op_methods[op] diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index a275de94c9be..70fda50d617e 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -202,6 +202,32 @@ 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 testIntDiv] a, b, c = None, None, None # type: (A, B, C) if int(): From e0953c05436f5fadea67e9c3fcaa2ae62c105581 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 18 Dec 2021 16:20:20 +0300 Subject: [PATCH 2/3] Use `__truediv__` for python2 with `__future__` import, refs #11740 --- mypy/build.py | 4 ++- mypy/checker.py | 2 +- mypy/semanal.py | 10 ++++--- test-data/unit/check-expressions.test | 38 +++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index d786922a4564..5642d13999b8 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2191,7 +2191,9 @@ def type_checker(self) -> TypeChecker: self._type_checker = TypeChecker( manager.errors, manager.modules, self.options, self.tree, self.xpath, manager.plugin, - self.manager.semantic_analyzer.future_import_flags, + self.manager.semantic_analyzer.future_import_flags.get( + self.tree.fullname, set(), + ), ) return self._type_checker diff --git a/mypy/checker.py b/mypy/checker.py index be175cf54d19..d10cba1ba1ab 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -221,7 +221,7 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface): # functions such as open(), etc. plugin: Plugin - # Future flags that we get from semantic analyzer. + # Future flags that we get from semantic analyzer for this module. future_import_flags: Set[str] def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Options, diff --git a/mypy/semanal.py b/mypy/semanal.py index 9a5076beb6f4..2bea7da971f2 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -219,7 +219,7 @@ 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] + future_import_flags: Dict[str, Set[str]] # Module name / future imports # 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,7 +284,7 @@ def __init__(self, # current SCC or top-level function. self.deferral_debug_context: List[Tuple[str, int]] = [] - self.future_import_flags: Set[str] = set() + self.future_import_flags: Dict[str, Set[str]] = {} # mypyc doesn't properly handle implementing an abstractproperty # with a regular attribute so we make them properties @@ -5252,10 +5252,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.future_import_flags.setdefault( + self.cur_mod_id, set(), + ).add(FUTURE_IMPORTS[module_name]) def is_future_flag_set(self, flag: str) -> bool: - return flag in self.future_import_flags + return flag in self.future_import_flags.setdefault(self.cur_mod_id, set()) class HasPlaceholders(TypeQuery[bool]): diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 70fda50d617e..75e3eb7d46d6 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -228,6 +228,44 @@ 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(): From 23978210ea6c758673977ae4ad673e0c1546a3b2 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 23 Feb 2022 13:08:31 +0300 Subject: [PATCH 3/3] Add `future_import_flags` property to `MypyFile` --- mypy/build.py | 3 --- mypy/checker.py | 8 +------- mypy/checkexpr.py | 6 +++++- mypy/nodes.py | 11 ++++++++++- mypy/semanal.py | 11 ++++------- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 5642d13999b8..9865f6e31c3c 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2191,9 +2191,6 @@ def type_checker(self) -> TypeChecker: self._type_checker = TypeChecker( manager.errors, manager.modules, self.options, self.tree, self.xpath, manager.plugin, - self.manager.semantic_analyzer.future_import_flags.get( - self.tree.fullname, set(), - ), ) return self._type_checker diff --git a/mypy/checker.py b/mypy/checker.py index d10cba1ba1ab..b90221a0a5a5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -221,12 +221,8 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface): # functions such as open(), etc. plugin: Plugin - # Future flags that we get from semantic analyzer for this module. - future_import_flags: Set[str] - def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Options, - tree: MypyFile, path: str, plugin: Plugin, - future_import_flags: Set[str]) -> None: + tree: MypyFile, path: str, plugin: Plugin) -> None: """Construct a type checker. Use errors to report type check errors. @@ -272,8 +268,6 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Option # argument through various `checker` and `checkmember` functions. self._is_final_def = False - self.future_import_flags = future_import_flags - @property def type_context(self) -> List[Optional[Type]]: return self.expr_checker.type_context diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 93df9c612126..d3ebfe1fe738 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2414,7 +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: - return '__truediv__' if 'division' in self.chk.future_import_flags else '__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 156d756030ae..1156762ca03c 100644 --- 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 index 2bea7da971f2..c599ffc6cf57 100644 --- 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: Dict[str, Set[str]] # Module name / future imports # 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: Dict[str, Set[str]] = {} - # mypyc doesn't properly handle implementing an abstractproperty # with a regular attribute so we make them properties @property @@ -5252,12 +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.setdefault( - self.cur_mod_id, set(), - ).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.setdefault(self.cur_mod_id, set()) + return self.modules[self.cur_mod_id].is_future_flag_set(flag) class HasPlaceholders(TypeQuery[bool]):