diff --git a/mypy/checker.py b/mypy/checker.py index 982ceca72576..488f3185e897 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2675,6 +2675,21 @@ def or_conditional_maps(m1: TypeMap, m2: TypeMap) -> TypeMap: return result +def convert_to_typetype(type_map: TypeMap) -> TypeMap: + converted_type_map = {} # type: TypeMap + if type_map is None: + return None + for expr, typ in type_map.items(): + if isinstance(typ, UnionType): + converted_type_map[expr] = UnionType([TypeType(t) for t in typ.items]) + elif isinstance(typ, Instance): + converted_type_map[expr] = TypeType(typ) + else: + # unknown type; error was likely reported earlier + return {} + return converted_type_map + + def find_isinstance_check(node: Expression, type_map: Dict[Expression, Type], ) -> Tuple[TypeMap, TypeMap]: @@ -2700,8 +2715,32 @@ def find_isinstance_check(node: Expression, expr = node.args[0] if expr.literal == LITERAL_TYPE: vartype = type_map[expr] - types = get_isinstance_type(node.args[1], type_map) - return conditional_type_map(expr, vartype, types) + type = get_isinstance_type(node.args[1], type_map) + return conditional_type_map(expr, vartype, type) + elif refers_to_fullname(node.callee, 'builtins.issubclass'): + expr = node.args[0] + if expr.literal == LITERAL_TYPE: + vartype = type_map[expr] + type = get_isinstance_type(node.args[1], type_map) + if isinstance(vartype, UnionType): + union_list = [] + for t in vartype.items: + if isinstance(t, TypeType): + union_list.append(t.item) + else: + # this is an error that should be reported earlier + # if we reach here, we refuse to do any type inference + return {}, {} + vartype = UnionType(union_list) + elif isinstance(vartype, TypeType): + vartype = vartype.item + else: + # any other object whose type we don't know precisely + # for example, Any or Instance of type type + return {}, {} # unknown type + yes_map, no_map = conditional_type_map(expr, vartype, type) + yes_map, no_map = map(convert_to_typetype, (yes_map, no_map)) + return yes_map, no_map elif refers_to_fullname(node.callee, 'builtins.callable'): expr = node.args[0] if expr.literal == LITERAL_TYPE: @@ -2793,18 +2832,18 @@ def flatten_types(t: Type) -> List[Type]: def get_isinstance_type(expr: Expression, type_map: Dict[Expression, Type]) -> List[TypeRange]: all_types = flatten_types(type_map[expr]) types = [] # type: List[TypeRange] - for type in all_types: - if isinstance(type, FunctionLike) and type.is_type_obj(): + for typ in all_types: + if isinstance(typ, FunctionLike) and typ.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): + typ = erase_typevars(typ.items()[0].ret_type) + types.append(TypeRange(typ, is_upper_bound=False)) + elif isinstance(typ, 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(typ.item, is_upper_bound=True)) + elif isinstance(typ, Instance) and typ.type.fullname() == 'builtins.type': + object_type = Instance(typ.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 @@ -2955,17 +2994,17 @@ def is_more_precise_signature(t: CallableType, s: CallableType) -> bool: return is_more_precise(t.ret_type, s.ret_type) -def infer_operator_assignment_method(type: Type, operator: str) -> Tuple[bool, str]: +def infer_operator_assignment_method(typ: Type, operator: str) -> Tuple[bool, str]: """Determine if operator assignment on given value type is in-place, and the method name. For example, if operator is '+', return (True, '__iadd__') or (False, '__add__') depending on which method is supported by the type. """ method = nodes.op_methods[operator] - if isinstance(type, Instance): + if isinstance(typ, Instance): if operator in nodes.ops_with_inplace_method: inplace_method = '__i' + method[2:] - if type.type.has_readable_member(inplace_method): + if typ.type.has_readable_member(inplace_method): return True, inplace_method return False, method diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 63874eb4f9ba..f3f4ea429f56 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1423,6 +1423,213 @@ def f(x: Union[int, A], a: Type[A]) -> None: [builtins fixtures/isinstancelist.pyi] +[case testIssubclassUnreachable] +from typing import Type, Sequence, Union +x: Type[str] +if issubclass(x, int): + reveal_type(x) # unreachable block + + +class X: pass +class Y(X): pass +class Z(X): pass + +a: Union[Type[Y], Type[Z]] +if issubclass(a, X): + reveal_type(a) # E: Revealed type is 'Union[Type[__main__.Y], Type[__main__.Z]]' +else: + reveal_type(a) # unreachable block + +[builtins fixtures/isinstancelist.pyi] + + +[case testIssubclasDestructuringUnions] +from typing import Union, List, Tuple, Dict, Type +def f(x: Union[Type[int], Type[str], Type[List]]) -> None: + if issubclass(x, (str, (int,))): + reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str]' + x()[1] # E: Value of type "Union[int, str]" is not indexable + else: + reveal_type(x) # E: Revealed type is 'Type[builtins.list]' + reveal_type(x()) # E: Revealed type is 'builtins.list[]' + x()[1] + reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' + if issubclass(x, (str, (list,))): + reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[]]' + x()[1] + reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' +[builtins fixtures/isinstancelist.pyi] + + +[case testIssubclass] +from typing import Type, ClassVar + +class Goblin: + level: int + +class GoblinAmbusher(Goblin): + job: ClassVar[str] = 'Ranger' + +def test_issubclass(cls: Type[Goblin]) -> None: + if issubclass(cls, GoblinAmbusher): + reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]' + cls.level + cls.job + ga = cls() + ga.level = 15 + ga.job + ga.job = "Warrior" # E: Cannot assign to class variable "job" via instance + else: + reveal_type(cls) # E: Revealed type is 'Type[__main__.Goblin]' + cls.level + cls.job # E: Type[Goblin] has no attribute "job" + g = cls() + g.level = 15 + g.job # E: "Goblin" has no attribute "job" + + +[builtins fixtures/isinstancelist.pyi] + + +[case testIssubclassDeepHierarchy] +from typing import Type, ClassVar + +class Mob: + pass + +class Goblin(Mob): + level: int + +class GoblinAmbusher(Goblin): + job: ClassVar[str] = 'Ranger' + +def test_issubclass(cls: Type[Mob]) -> None: + if issubclass(cls, Goblin): + reveal_type(cls) # E: Revealed type is 'Type[__main__.Goblin]' + cls.level + cls.job # E: Type[Goblin] has no attribute "job" + g = cls() + g.level = 15 + g.job # E: "Goblin" has no attribute "job" + if issubclass(cls, GoblinAmbusher): + reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]' + cls.level + cls.job + g = cls() + g.level = 15 + g.job + g.job = 'Warrior' # E: Cannot assign to class variable "job" via instance + else: + reveal_type(cls) # E: Revealed type is 'Type[__main__.Mob]' + cls.job # E: Type[Mob] has no attribute "job" + cls.level # E: Type[Mob] has no attribute "level" + m = cls() + m.level = 15 # E: "Mob" has no attribute "level" + m.job # E: "Mob" has no attribute "job" + if issubclass(cls, GoblinAmbusher): + reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]' + cls.job + cls.level + ga = cls() + ga.level = 15 + ga.job + ga.job = 'Warrior' # E: Cannot assign to class variable "job" via instance + + if issubclass(cls, GoblinAmbusher): + reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]' + cls.level + cls.job + ga = cls() + ga.level = 15 + ga.job + ga.job = "Warrior" # E: Cannot assign to class variable "job" via instance + +[builtins fixtures/isinstancelist.pyi] + + +[case testIssubclassTuple] +from typing import Type, ClassVar + +class Mob: + pass + +class Goblin(Mob): + level: int + +class GoblinAmbusher(Goblin): + job: ClassVar[str] = 'Ranger' + +class GoblinDigger(Goblin): + job: ClassVar[str] = 'Thief' + +def test_issubclass(cls: Type[Mob]) -> None: + if issubclass(cls, (Goblin, GoblinAmbusher)): + reveal_type(cls) # E: Revealed type is 'Type[__main__.Goblin]' + cls.level + cls.job # E: Type[Goblin] has no attribute "job" + g = cls() + g.level = 15 + g.job # E: "Goblin" has no attribute "job" + if issubclass(cls, GoblinAmbusher): + cls.level + reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]' + cls.job + ga = cls() + ga.level = 15 + ga.job + ga.job = "Warrior" # E: Cannot assign to class variable "job" via instance + else: + reveal_type(cls) # E: Revealed type is 'Type[__main__.Mob]' + cls.job # E: Type[Mob] has no attribute "job" + cls.level # E: Type[Mob] has no attribute "level" + m = cls() + m.level = 15 # E: "Mob" has no attribute "level" + m.job # E: "Mob" has no attribute "job" + if issubclass(cls, GoblinAmbusher): + reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]' + cls.job + cls.level + ga = cls() + ga.level = 15 + ga.job + ga.job = "Warrior" # E: Cannot assign to class variable "job" via instance + + if issubclass(cls, (GoblinDigger, GoblinAmbusher)): + reveal_type(cls) # E: Revealed type is 'Union[Type[__main__.GoblinDigger], Type[__main__.GoblinAmbusher]]' + cls.level + cls.job + g = cls() + g.level = 15 + g.job + g.job = "Warrior" # E: Cannot assign to class variable "job" via instance + +[builtins fixtures/isinstancelist.pyi] + + +[case testIssubclassBuiltins] +from typing import List, Type + +class MyList(List): pass +class MyIntList(List[int]): pass + +def f(cls: Type[object]) -> None: + if issubclass(cls, MyList): + reveal_type(cls) # E: Revealed type is 'Type[__main__.MyList]' + cls()[0] + else: + reveal_type(cls) # E: Revealed type is 'Type[builtins.object]' + cls()[0] # E: Value of type "object" is not indexable + + if issubclass(cls, MyIntList): + reveal_type(cls) # E: Revealed type is 'Type[__main__.MyIntList]' + cls()[0] + 1 + +[builtins fixtures/isinstancelist.pyi] + [case testIsinstanceTypeArgs] from typing import Iterable, TypeVar x = 1 diff --git a/test-data/unit/fixtures/isinstancelist.pyi b/test-data/unit/fixtures/isinstancelist.pyi index 9096961b06ab..7d0c8a02b1af 100644 --- a/test-data/unit/fixtures/isinstancelist.pyi +++ b/test-data/unit/fixtures/isinstancelist.pyi @@ -12,6 +12,7 @@ class tuple: pass class function: pass def isinstance(x: object, t: Union[type, Tuple]) -> bool: pass +def issubclass(x: object, t: Union[type, Tuple]) -> bool: pass @builtinclass class int: diff --git a/typeshed b/typeshed index 26360e821b42..8b835f95001b 160000 --- a/typeshed +++ b/typeshed @@ -1 +1 @@ -Subproject commit 26360e821b427c29ca27117061a3ba806f63e12e +Subproject commit 8b835f95001b61734c6b147d3aa6eb4fbe7bce03