diff --git a/mypy/checker.py b/mypy/checker.py index 09c58c2b2dc8..cb55cfc73dc2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2452,10 +2452,19 @@ def push_type_map(self, type_map: Optional[Dict[Expression, Type]]) -> None: TypeMap = Optional[Dict[Expression, Type]] +# An object that represents either a precise type or a type with an upper bound; +# it is important for correct type inference with isinstance. +TypeRange = NamedTuple( + 'TypeRange', + [ + ('item', Type), + ('is_upper_bound', bool), # False => precise type + ]) + def conditional_type_map(expr: Expression, current_type: Optional[Type], - proposed_type: Optional[Type], + proposed_type_ranges: Optional[List[TypeRange]], ) -> Tuple[TypeMap, TypeMap]: """Takes in an expression, the current type of the expression, and a proposed type of that expression. @@ -2463,17 +2472,26 @@ def conditional_type_map(expr: Expression, Returns a 2-tuple: The first element is a map from the expression to the proposed type, if the expression can be the proposed type. The second element is a map from the expression to the type it would hold - if it was not the proposed type, if any.""" - if proposed_type: + if it was not the proposed type, if any. None means bot, {} means top""" + if proposed_type_ranges: + if len(proposed_type_ranges) == 1: + proposed_type = proposed_type_ranges[0].item # Union with a single type breaks tests + else: + proposed_type = UnionType([type_range.item for type_range in proposed_type_ranges]) if current_type: - if is_proper_subtype(current_type, proposed_type): - # Expression is always of type proposed_type + if (not any(type_range.is_upper_bound for type_range in proposed_type_ranges) + and is_proper_subtype(current_type, proposed_type)): + # Expression is always of one of the types in proposed_type_ranges return {}, None elif not is_overlapping_types(current_type, proposed_type): - # Expression is never of type proposed_type + # Expression is never of any type in proposed_type_ranges return None, {} else: - remaining_type = restrict_subtype_away(current_type, proposed_type) + # we can only restrict when the type is precise, not bounded + proposed_precise_type = UnionType([type_range.item + for type_range in proposed_type_ranges + if not type_range.is_upper_bound]) + remaining_type = restrict_subtype_away(current_type, proposed_precise_type) return {expr: proposed_type}, {expr: remaining_type} else: return {expr: proposed_type}, {} @@ -2644,8 +2662,8 @@ def find_isinstance_check(node: Expression, expr = node.args[0] if expr.literal == LITERAL_TYPE: vartype = type_map[expr] - type = get_isinstance_type(node.args[1], type_map) - return conditional_type_map(expr, vartype, type) + types = get_isinstance_type(node.args[1], type_map) + return conditional_type_map(expr, vartype, types) elif refers_to_fullname(node.callee, 'builtins.callable'): expr = node.args[0] if expr.literal == LITERAL_TYPE: @@ -2663,7 +2681,8 @@ def find_isinstance_check(node: Expression, # two elements in node.operands, and at least one of them # should represent a None. vartype = type_map[expr] - if_vars, else_vars = conditional_type_map(expr, vartype, NoneTyp()) + none_typ = [TypeRange(NoneTyp(), is_upper_bound=False)] + if_vars, else_vars = conditional_type_map(expr, vartype, none_typ) break if is_not: @@ -2725,26 +2744,30 @@ def flatten(t: Expression) -> List[Expression]: return [t] -def get_isinstance_type(expr: Expression, type_map: Dict[Expression, Type]) -> Type: +def get_isinstance_type(expr: Expression, type_map: Dict[Expression, Type]) -> List[TypeRange]: all_types = [type_map[e] for e in flatten(expr)] - - types = [] # type: List[Type] - + types = [] # type: List[TypeRange] for type in all_types: - if isinstance(type, FunctionLike): - if type.is_type_obj(): - # Type variables may be present -- erase them, which is the best - # we can do (outside disallowing them here). - type = erase_typevars(type.items()[0].ret_type) - - types.append(type) - - if len(types) == 0: + if isinstance(type, FunctionLike) and type.is_type_obj(): + # Type variables may be present -- erase them, which is the best + # we can do (outside disallowing them here). + type = erase_typevars(type.items()[0].ret_type) + types.append(TypeRange(type, is_upper_bound=False)) + elif isinstance(type, TypeType): + # Type[A] means "any type that is a subtype of A" rather than "precisely type A" + # we indicate this by setting is_upper_bound flag + types.append(TypeRange(type.item, is_upper_bound=True)) + elif isinstance(type, Instance) and type.type.fullname() == 'builtins.type': + object_type = Instance(type.type.mro[-1], []) + types.append(TypeRange(object_type, is_upper_bound=True)) + else: # we didn't see an actual type, but rather a variable whose value is unknown to us + return None + if not types: + # this can happen if someone has empty tuple as 2nd argument to isinstance + # strictly speaking, we should return UninhabitedType but for simplicity we will simply + # refuse to do any type inference for now return None - elif len(types) == 1: - return types[0] - else: - return UnionType(types) + return types def expand_func(defn: FuncItem, map: Dict[TypeVarId, Type]) -> FuncItem: diff --git a/mypy/semanal.py b/mypy/semanal.py index 664d74800c07..f3b1eaec8568 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2679,8 +2679,7 @@ def visit_member_expr(self, expr: MemberExpr) -> None: # This branch handles the case foo.bar where foo is a module. # In this case base.node is the module's MypyFile and we look up # bar in its namespace. This must be done for all types of bar. - file = base.node - assert isinstance(file, (MypyFile, type(None))) + file = cast(Optional[MypyFile], base.node) # can't use isinstance due to issue #2999 n = file.names.get(expr.name, None) if file is not None else None if n: n = self.normalize_type_alias(n, expr) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 43dbfd8321d1..973d463d6085 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1360,3 +1360,55 @@ def f(x: object) -> None: reveal_type(b) # E: Revealed type is '__main__.A' [builtins fixtures/isinstance.pyi] [out] + + +[case testIsInstanceWithUnknownType] +from typing import Union +def f(x: Union[int, str], typ: type) -> None: + if isinstance(x, (typ, int)): + x + 1 # E: Unsupported operand types for + (likely involving Union) + reveal_type(x) # E: Revealed type is 'Union[builtins.int, builtins.str]' + else: + reveal_type(x) # E: Revealed type is 'builtins.str' +[builtins fixtures/isinstancelist.pyi] + + +[case testIsInstanceWithBoundedType] +from typing import Union, Type + +class A: pass +def f(x: Union[int, A], a: Type[A]) -> None: + if isinstance(x, (a, int)): + reveal_type(x) # E: Revealed type is 'Union[builtins.int, __main__.A]' + else: + reveal_type(x) # E: Revealed type is '__main__.A' + +[builtins fixtures/isinstancelist.pyi] + + +[case testIsInstanceWithEmtpy2ndArg] +from typing import Union +def f(x: Union[int, str]) -> None: + if isinstance(x, ()): + reveal_type(x) # E: Revealed type is 'Union[builtins.int, builtins.str]' + else: + reveal_type(x) # E: Revealed type is 'Union[builtins.int, builtins.str]' +[builtins fixtures/isinstancelist.pyi] + + +[case testIsInstanceWithTypeObject] +from typing import Union, Type + +class A: pass + +def f(x: Union[int, A], a: Type[A]) -> None: + if isinstance(x, a): + reveal_type(x) # E: Revealed type is '__main__.A' + elif isinstance(x, int): + reveal_type(x) # E: Revealed type is 'builtins.int' + else: + reveal_type(x) # E: Revealed type is '__main__.A' + reveal_type(x) # E: Revealed type is 'Union[builtins.int, __main__.A]' + +[builtins fixtures/isinstancelist.pyi] + diff --git a/typeshed b/typeshed index db0c106d2fe7..bd5b33f3b18f 160000 --- a/typeshed +++ b/typeshed @@ -1 +1 @@ -Subproject commit db0c106d2fe76204f6506f459edeff9cfde4d607 +Subproject commit bd5b33f3b18fc8811ad2403d35f21ad6aae94b62