Skip to content

Commit

Permalink
Use __truediv__ for python2 with __future__ import, refs #11740 (#…
Browse files Browse the repository at this point in the history
…11787)

Now `__future__` imports do not leak in semanal.py.

Closes #11740
Refs #11741
Refs #11276
Refs #11700
  • Loading branch information
sobolevn committed Feb 23, 2022
1 parent 115ac31 commit 14de8c6
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 10 deletions.
6 changes: 4 additions & 2 deletions mypy/build.py
Expand Up @@ -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]:
Expand Down
7 changes: 5 additions & 2 deletions mypy/checkexpr.py
Expand Up @@ -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]

Expand Down
11 changes: 10 additions & 1 deletion mypy/nodes.py
Expand Up @@ -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]
Expand Down Expand Up @@ -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],
Expand All @@ -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).
Expand All @@ -331,13 +335,17 @@ 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,
'names': self.names.serialize(self._fullname),
'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
Expand All @@ -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


Expand Down
9 changes: 4 additions & 5 deletions mypy/semanal.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]):
Expand Down
64 changes: 64 additions & 0 deletions test-data/unit/check-expressions.test
Expand Up @@ -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():
Expand Down

0 comments on commit 14de8c6

Please sign in to comment.