Skip to content

Commit c38a836

Browse files
pkchilevkivskyi
authored andcommitted
Infer types from issubclass() calls (#3005)
* Make no inference when variables present in second argument to isinstance * Add failing unit tests * Infer type from issubclass * Apply CR comments * Fix 3.3 syntax, updated typeshed * More CR fixes * Fix testr * Revert accidental format change * Address CR * Rename type into typ * Fix unreachable block crash * Add back integration tests * Add union destructuring test
1 parent 8c0001a commit c38a836

File tree

3 files changed

+260
-13
lines changed

3 files changed

+260
-13
lines changed

mypy/checker.py

+52-13
Original file line numberDiff line numberDiff line change
@@ -2675,6 +2675,21 @@ def or_conditional_maps(m1: TypeMap, m2: TypeMap) -> TypeMap:
26752675
return result
26762676

26772677

2678+
def convert_to_typetype(type_map: TypeMap) -> TypeMap:
2679+
converted_type_map = {} # type: TypeMap
2680+
if type_map is None:
2681+
return None
2682+
for expr, typ in type_map.items():
2683+
if isinstance(typ, UnionType):
2684+
converted_type_map[expr] = UnionType([TypeType(t) for t in typ.items])
2685+
elif isinstance(typ, Instance):
2686+
converted_type_map[expr] = TypeType(typ)
2687+
else:
2688+
# unknown type; error was likely reported earlier
2689+
return {}
2690+
return converted_type_map
2691+
2692+
26782693
def find_isinstance_check(node: Expression,
26792694
type_map: Dict[Expression, Type],
26802695
) -> Tuple[TypeMap, TypeMap]:
@@ -2700,8 +2715,32 @@ def find_isinstance_check(node: Expression,
27002715
expr = node.args[0]
27012716
if expr.literal == LITERAL_TYPE:
27022717
vartype = type_map[expr]
2703-
types = get_isinstance_type(node.args[1], type_map)
2704-
return conditional_type_map(expr, vartype, types)
2718+
type = get_isinstance_type(node.args[1], type_map)
2719+
return conditional_type_map(expr, vartype, type)
2720+
elif refers_to_fullname(node.callee, 'builtins.issubclass'):
2721+
expr = node.args[0]
2722+
if expr.literal == LITERAL_TYPE:
2723+
vartype = type_map[expr]
2724+
type = get_isinstance_type(node.args[1], type_map)
2725+
if isinstance(vartype, UnionType):
2726+
union_list = []
2727+
for t in vartype.items:
2728+
if isinstance(t, TypeType):
2729+
union_list.append(t.item)
2730+
else:
2731+
# this is an error that should be reported earlier
2732+
# if we reach here, we refuse to do any type inference
2733+
return {}, {}
2734+
vartype = UnionType(union_list)
2735+
elif isinstance(vartype, TypeType):
2736+
vartype = vartype.item
2737+
else:
2738+
# any other object whose type we don't know precisely
2739+
# for example, Any or Instance of type type
2740+
return {}, {} # unknown type
2741+
yes_map, no_map = conditional_type_map(expr, vartype, type)
2742+
yes_map, no_map = map(convert_to_typetype, (yes_map, no_map))
2743+
return yes_map, no_map
27052744
elif refers_to_fullname(node.callee, 'builtins.callable'):
27062745
expr = node.args[0]
27072746
if expr.literal == LITERAL_TYPE:
@@ -2793,18 +2832,18 @@ def flatten_types(t: Type) -> List[Type]:
27932832
def get_isinstance_type(expr: Expression, type_map: Dict[Expression, Type]) -> List[TypeRange]:
27942833
all_types = flatten_types(type_map[expr])
27952834
types = [] # type: List[TypeRange]
2796-
for type in all_types:
2797-
if isinstance(type, FunctionLike) and type.is_type_obj():
2835+
for typ in all_types:
2836+
if isinstance(typ, FunctionLike) and typ.is_type_obj():
27982837
# Type variables may be present -- erase them, which is the best
27992838
# we can do (outside disallowing them here).
2800-
type = erase_typevars(type.items()[0].ret_type)
2801-
types.append(TypeRange(type, is_upper_bound=False))
2802-
elif isinstance(type, TypeType):
2839+
typ = erase_typevars(typ.items()[0].ret_type)
2840+
types.append(TypeRange(typ, is_upper_bound=False))
2841+
elif isinstance(typ, TypeType):
28032842
# Type[A] means "any type that is a subtype of A" rather than "precisely type A"
28042843
# we indicate this by setting is_upper_bound flag
2805-
types.append(TypeRange(type.item, is_upper_bound=True))
2806-
elif isinstance(type, Instance) and type.type.fullname() == 'builtins.type':
2807-
object_type = Instance(type.type.mro[-1], [])
2844+
types.append(TypeRange(typ.item, is_upper_bound=True))
2845+
elif isinstance(typ, Instance) and typ.type.fullname() == 'builtins.type':
2846+
object_type = Instance(typ.type.mro[-1], [])
28082847
types.append(TypeRange(object_type, is_upper_bound=True))
28092848
else: # we didn't see an actual type, but rather a variable whose value is unknown to us
28102849
return None
@@ -2955,17 +2994,17 @@ def is_more_precise_signature(t: CallableType, s: CallableType) -> bool:
29552994
return is_more_precise(t.ret_type, s.ret_type)
29562995

29572996

2958-
def infer_operator_assignment_method(type: Type, operator: str) -> Tuple[bool, str]:
2997+
def infer_operator_assignment_method(typ: Type, operator: str) -> Tuple[bool, str]:
29592998
"""Determine if operator assignment on given value type is in-place, and the method name.
29602999
29613000
For example, if operator is '+', return (True, '__iadd__') or (False, '__add__')
29623001
depending on which method is supported by the type.
29633002
"""
29643003
method = nodes.op_methods[operator]
2965-
if isinstance(type, Instance):
3004+
if isinstance(typ, Instance):
29663005
if operator in nodes.ops_with_inplace_method:
29673006
inplace_method = '__i' + method[2:]
2968-
if type.type.has_readable_member(inplace_method):
3007+
if typ.type.has_readable_member(inplace_method):
29693008
return True, inplace_method
29703009
return False, method
29713010

test-data/unit/check-isinstance.test

+207
Original file line numberDiff line numberDiff line change
@@ -1423,6 +1423,213 @@ def f(x: Union[int, A], a: Type[A]) -> None:
14231423
[builtins fixtures/isinstancelist.pyi]
14241424

14251425

1426+
[case testIssubclassUnreachable]
1427+
from typing import Type, Sequence, Union
1428+
x: Type[str]
1429+
if issubclass(x, int):
1430+
reveal_type(x) # unreachable block
1431+
1432+
1433+
class X: pass
1434+
class Y(X): pass
1435+
class Z(X): pass
1436+
1437+
a: Union[Type[Y], Type[Z]]
1438+
if issubclass(a, X):
1439+
reveal_type(a) # E: Revealed type is 'Union[Type[__main__.Y], Type[__main__.Z]]'
1440+
else:
1441+
reveal_type(a) # unreachable block
1442+
1443+
[builtins fixtures/isinstancelist.pyi]
1444+
1445+
1446+
[case testIssubclasDestructuringUnions]
1447+
from typing import Union, List, Tuple, Dict, Type
1448+
def f(x: Union[Type[int], Type[str], Type[List]]) -> None:
1449+
if issubclass(x, (str, (int,))):
1450+
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str]]'
1451+
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str]'
1452+
x()[1] # E: Value of type "Union[int, str]" is not indexable
1453+
else:
1454+
reveal_type(x) # E: Revealed type is 'Type[builtins.list]'
1455+
reveal_type(x()) # E: Revealed type is 'builtins.list[<uninhabited>]'
1456+
x()[1]
1457+
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list]]'
1458+
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<uninhabited>]]'
1459+
if issubclass(x, (str, (list,))):
1460+
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
1461+
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[<uninhabited>]]'
1462+
x()[1]
1463+
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
1464+
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<uninhabited>]]'
1465+
[builtins fixtures/isinstancelist.pyi]
1466+
1467+
1468+
[case testIssubclass]
1469+
from typing import Type, ClassVar
1470+
1471+
class Goblin:
1472+
level: int
1473+
1474+
class GoblinAmbusher(Goblin):
1475+
job: ClassVar[str] = 'Ranger'
1476+
1477+
def test_issubclass(cls: Type[Goblin]) -> None:
1478+
if issubclass(cls, GoblinAmbusher):
1479+
reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
1480+
cls.level
1481+
cls.job
1482+
ga = cls()
1483+
ga.level = 15
1484+
ga.job
1485+
ga.job = "Warrior" # E: Cannot assign to class variable "job" via instance
1486+
else:
1487+
reveal_type(cls) # E: Revealed type is 'Type[__main__.Goblin]'
1488+
cls.level
1489+
cls.job # E: Type[Goblin] has no attribute "job"
1490+
g = cls()
1491+
g.level = 15
1492+
g.job # E: "Goblin" has no attribute "job"
1493+
1494+
1495+
[builtins fixtures/isinstancelist.pyi]
1496+
1497+
1498+
[case testIssubclassDeepHierarchy]
1499+
from typing import Type, ClassVar
1500+
1501+
class Mob:
1502+
pass
1503+
1504+
class Goblin(Mob):
1505+
level: int
1506+
1507+
class GoblinAmbusher(Goblin):
1508+
job: ClassVar[str] = 'Ranger'
1509+
1510+
def test_issubclass(cls: Type[Mob]) -> None:
1511+
if issubclass(cls, Goblin):
1512+
reveal_type(cls) # E: Revealed type is 'Type[__main__.Goblin]'
1513+
cls.level
1514+
cls.job # E: Type[Goblin] has no attribute "job"
1515+
g = cls()
1516+
g.level = 15
1517+
g.job # E: "Goblin" has no attribute "job"
1518+
if issubclass(cls, GoblinAmbusher):
1519+
reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
1520+
cls.level
1521+
cls.job
1522+
g = cls()
1523+
g.level = 15
1524+
g.job
1525+
g.job = 'Warrior' # E: Cannot assign to class variable "job" via instance
1526+
else:
1527+
reveal_type(cls) # E: Revealed type is 'Type[__main__.Mob]'
1528+
cls.job # E: Type[Mob] has no attribute "job"
1529+
cls.level # E: Type[Mob] has no attribute "level"
1530+
m = cls()
1531+
m.level = 15 # E: "Mob" has no attribute "level"
1532+
m.job # E: "Mob" has no attribute "job"
1533+
if issubclass(cls, GoblinAmbusher):
1534+
reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
1535+
cls.job
1536+
cls.level
1537+
ga = cls()
1538+
ga.level = 15
1539+
ga.job
1540+
ga.job = 'Warrior' # E: Cannot assign to class variable "job" via instance
1541+
1542+
if issubclass(cls, GoblinAmbusher):
1543+
reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
1544+
cls.level
1545+
cls.job
1546+
ga = cls()
1547+
ga.level = 15
1548+
ga.job
1549+
ga.job = "Warrior" # E: Cannot assign to class variable "job" via instance
1550+
1551+
[builtins fixtures/isinstancelist.pyi]
1552+
1553+
1554+
[case testIssubclassTuple]
1555+
from typing import Type, ClassVar
1556+
1557+
class Mob:
1558+
pass
1559+
1560+
class Goblin(Mob):
1561+
level: int
1562+
1563+
class GoblinAmbusher(Goblin):
1564+
job: ClassVar[str] = 'Ranger'
1565+
1566+
class GoblinDigger(Goblin):
1567+
job: ClassVar[str] = 'Thief'
1568+
1569+
def test_issubclass(cls: Type[Mob]) -> None:
1570+
if issubclass(cls, (Goblin, GoblinAmbusher)):
1571+
reveal_type(cls) # E: Revealed type is 'Type[__main__.Goblin]'
1572+
cls.level
1573+
cls.job # E: Type[Goblin] has no attribute "job"
1574+
g = cls()
1575+
g.level = 15
1576+
g.job # E: "Goblin" has no attribute "job"
1577+
if issubclass(cls, GoblinAmbusher):
1578+
cls.level
1579+
reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
1580+
cls.job
1581+
ga = cls()
1582+
ga.level = 15
1583+
ga.job
1584+
ga.job = "Warrior" # E: Cannot assign to class variable "job" via instance
1585+
else:
1586+
reveal_type(cls) # E: Revealed type is 'Type[__main__.Mob]'
1587+
cls.job # E: Type[Mob] has no attribute "job"
1588+
cls.level # E: Type[Mob] has no attribute "level"
1589+
m = cls()
1590+
m.level = 15 # E: "Mob" has no attribute "level"
1591+
m.job # E: "Mob" has no attribute "job"
1592+
if issubclass(cls, GoblinAmbusher):
1593+
reveal_type(cls) # E: Revealed type is 'Type[__main__.GoblinAmbusher]'
1594+
cls.job
1595+
cls.level
1596+
ga = cls()
1597+
ga.level = 15
1598+
ga.job
1599+
ga.job = "Warrior" # E: Cannot assign to class variable "job" via instance
1600+
1601+
if issubclass(cls, (GoblinDigger, GoblinAmbusher)):
1602+
reveal_type(cls) # E: Revealed type is 'Union[Type[__main__.GoblinDigger], Type[__main__.GoblinAmbusher]]'
1603+
cls.level
1604+
cls.job
1605+
g = cls()
1606+
g.level = 15
1607+
g.job
1608+
g.job = "Warrior" # E: Cannot assign to class variable "job" via instance
1609+
1610+
[builtins fixtures/isinstancelist.pyi]
1611+
1612+
1613+
[case testIssubclassBuiltins]
1614+
from typing import List, Type
1615+
1616+
class MyList(List): pass
1617+
class MyIntList(List[int]): pass
1618+
1619+
def f(cls: Type[object]) -> None:
1620+
if issubclass(cls, MyList):
1621+
reveal_type(cls) # E: Revealed type is 'Type[__main__.MyList]'
1622+
cls()[0]
1623+
else:
1624+
reveal_type(cls) # E: Revealed type is 'Type[builtins.object]'
1625+
cls()[0] # E: Value of type "object" is not indexable
1626+
1627+
if issubclass(cls, MyIntList):
1628+
reveal_type(cls) # E: Revealed type is 'Type[__main__.MyIntList]'
1629+
cls()[0] + 1
1630+
1631+
[builtins fixtures/isinstancelist.pyi]
1632+
14261633
[case testIsinstanceTypeArgs]
14271634
from typing import Iterable, TypeVar
14281635
x = 1

test-data/unit/fixtures/isinstancelist.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class tuple: pass
1212
class function: pass
1313

1414
def isinstance(x: object, t: Union[type, Tuple]) -> bool: pass
15+
def issubclass(x: object, t: Union[type, Tuple]) -> bool: pass
1516

1617
@builtinclass
1718
class int:

0 commit comments

Comments
 (0)