diff --git a/mypy/checker.py b/mypy/checker.py index f4f466bb6ba8..5ec156d8aacd 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3924,10 +3924,12 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM elif is_false_literal(node): return None, {} elif isinstance(node, CallExpr): + if len(node.args) == 0: + return {}, {} + expr = collapse_walrus(node.args[0]) if refers_to_fullname(node.callee, 'builtins.isinstance'): if len(node.args) != 2: # the error will be reported elsewhere return {}, {} - expr = node.args[0] if literal(expr) == LITERAL_TYPE: return self.conditional_type_map_with_intersection( expr, @@ -3937,13 +3939,11 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM elif refers_to_fullname(node.callee, 'builtins.issubclass'): if len(node.args) != 2: # the error will be reported elsewhere return {}, {} - expr = node.args[0] if literal(expr) == LITERAL_TYPE: return self.infer_issubclass_maps(node, expr, type_map) elif refers_to_fullname(node.callee, 'builtins.callable'): if len(node.args) != 1: # the error will be reported elsewhere return {}, {} - expr = node.args[0] if literal(expr) == LITERAL_TYPE: vartype = type_map[expr] return self.conditional_callable_type_map(expr, vartype) @@ -3952,7 +3952,7 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM # narrow their types. (For example, we shouldn't try narrowing the # types of literal string or enum expressions). - operands = node.operands + operands = [collapse_walrus(x) for x in node.operands] operand_types = [] narrowable_operand_index_to_hash = {} for i, expr in enumerate(operands): @@ -5742,3 +5742,14 @@ def has_bool_item(typ: ProperType) -> bool: return any(is_named_instance(item, 'builtins.bool') for item in typ.items) return False + + +def collapse_walrus(e: Expression) -> Expression: + """If an expression is an AssignmentExpr, pull out the assignment target. + + We don't make any attempt to pull out all the targets in code like `x := (y := z)`. + We could support narrowing those if that sort of code turns out to be common. + """ + if isinstance(e, AssignmentExpr): + return e.target + return e diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 0141271eccba..0118080c1c00 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2409,7 +2409,16 @@ def make_local_errors() -> MessageBuilder: def lookup_operator(op_name: str, base_type: Type) -> Optional[Type]: """Looks up the given operator and returns the corresponding type, if it exists.""" + + # This check is an important performance optimization, + # even though it is mostly a subset of + # analyze_member_access. + # TODO: Find a way to remove this call without performance implications. + if not self.has_member(base_type, op_name): + return None + local_errors = make_local_errors() + member = analyze_member_access( name=op_name, typ=base_type, @@ -3790,6 +3799,45 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool: [self.named_type('builtins.unicode'), AnyType(TypeOfAny.special_form)]))) + def has_member(self, typ: Type, member: str) -> bool: + """Does type have member with the given name?""" + # TODO: refactor this to use checkmember.analyze_member_access, otherwise + # these two should be carefully kept in sync. + # This is much faster than analyze_member_access, though, and so using + # it first as a filter is important for performance. + typ = get_proper_type(typ) + + if isinstance(typ, TypeVarType): + typ = get_proper_type(typ.upper_bound) + if isinstance(typ, TupleType): + typ = tuple_fallback(typ) + if isinstance(typ, LiteralType): + typ = typ.fallback + if isinstance(typ, Instance): + return typ.type.has_readable_member(member) + if isinstance(typ, CallableType) and typ.is_type_obj(): + return typ.fallback.type.has_readable_member(member) + elif isinstance(typ, AnyType): + return True + elif isinstance(typ, UnionType): + result = all(self.has_member(x, member) for x in typ.relevant_items()) + return result + elif isinstance(typ, TypeType): + # Type[Union[X, ...]] is always normalized to Union[Type[X], ...], + # so we don't need to care about unions here. + item = typ.item + if isinstance(item, TypeVarType): + item = get_proper_type(item.upper_bound) + if isinstance(item, TupleType): + item = tuple_fallback(item) + if isinstance(item, Instance) and item.type.metaclass_type is not None: + return self.has_member(item.type.metaclass_type, member) + if isinstance(item, AnyType): + return True + return False + else: + return False + def not_ready_callback(self, name: str, context: Context) -> None: """Called when we can't infer the type of a variable because it's not ready yet. diff --git a/mypy/typeops.py b/mypy/typeops.py index d143588aada3..828791333f36 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -719,5 +719,5 @@ def custom_special_method(typ: Type, name: str, check_all: bool = False) -> bool if isinstance(typ, AnyType): # Avoid false positives in uncertain cases. return True - # TODO: support other types (see analyze_member_access)? + # TODO: support other types (see ExpressionChecker.has_member())? return False diff --git a/mypy/version.py b/mypy/version.py index 81a8cfca378b..a75857d23827 100644 --- a/mypy/version.py +++ b/mypy/version.py @@ -5,7 +5,7 @@ # - Release versions have the form "0.NNN". # - Dev versions have the form "0.NNN+dev" (PLUS sign to conform to PEP 440). # - For 1.0 we'll switch back to 1.2.3 form. -__version__ = '0.770+dev' +__version__ = '0.770' base_version = __version__ mypy_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index e924402c8614..ed547510b46c 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -6650,7 +6650,7 @@ reveal_type(0.5 + C) # N: Revealed type is 'Any' reveal_type(0.5 + D()) # N: Revealed type is 'Any' reveal_type(D() + 0.5) # N: Revealed type is 'Any' -reveal_type("str" + D()) # N: Revealed type is 'Any' +reveal_type("str" + D()) # N: Revealed type is 'builtins.str' reveal_type(D() + "str") # N: Revealed type is 'Any' diff --git a/test-data/unit/check-python38.test b/test-data/unit/check-python38.test index 98eda306c731..12a060525820 100644 --- a/test-data/unit/check-python38.test +++ b/test-data/unit/check-python38.test @@ -189,7 +189,7 @@ def f(p1: bytes, p2: float, /) -> None: [case testWalrus] # flags: --strict-optional -from typing import NamedTuple, Optional +from typing import NamedTuple, Optional, List from typing_extensions import Final if a := 2: @@ -288,10 +288,23 @@ def check_partial() -> None: reveal_type(x) # N: Revealed type is 'Union[builtins.int, None]' -def check_narrow(x: Optional[int]) -> None: +def check_narrow(x: Optional[int], s: List[int]) -> None: if (y := x): reveal_type(y) # N: Revealed type is 'builtins.int' -[builtins fixtures/f_string.pyi] + + if (y := x) is not None: + reveal_type(y) # N: Revealed type is 'builtins.int' + + if (y := x) == 10: + reveal_type(y) # N: Revealed type is 'builtins.int' + + if (y := x) in s: + reveal_type(y) # N: Revealed type is 'builtins.int' + + if isinstance((y := x), int): + reveal_type(y) # N: Revealed type is 'builtins.int' + +[builtins fixtures/isinstancelist.pyi] [case testWalrusPartialTypes] from typing import List