-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Infer types from issubclass() calls #3005
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
158c889
c5525bd
1776e90
3334b8e
3587549
81e1923
e4ffdc5
9440a52
c831401
a4a7214
f94ca45
a260b79
fbd0b91
e48d983
b0ce683
aca05c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2616,6 +2616,20 @@ def or_conditional_maps(m1: TypeMap, m2: TypeMap) -> TypeMap: | |
return result | ||
|
||
|
||
def convert_to_typetype(type_map: TypeMap) -> TypeMap: | ||
converted_type_map = {} # type: TypeMap | ||
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: | ||
# TODO: verify this is ok | ||
# unknown object, don't know how to convert | ||
return {} | ||
return converted_type_map | ||
|
||
|
||
def find_isinstance_check(node: Expression, | ||
type_map: Dict[Expression, Type], | ||
) -> Tuple[TypeMap, TypeMap]: | ||
|
@@ -2639,8 +2653,30 @@ 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 an error; should be caught earlier, so we should never be here | ||
return {}, {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you are sure this is an unreachable branch, then you could put |
||
vartype = UnionType(union_list) | ||
elif isinstance(vartype, TypeType): | ||
vartype = vartype.item | ||
else: | ||
# TODO: verify this is ok | ||
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: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2696,8 +2696,9 @@ def visit_member_expr(self, expr: MemberExpr) -> None: | |
# one type checker run. If we reported errors here, | ||
# the build would terminate after semantic analysis | ||
# and we wouldn't be able to report any type errors. | ||
full_name = '%s.%s' % (file.fullname() if file is not None else None, expr.name) | ||
mod_name = " '%s'" % file.fullname() if file is not None else '' | ||
full_name = '%s.%s' % (file.fullname() if file is not None # type: ignore | ||
else None, expr.name) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you don't need this formatting change, it only creates noise in the PR. Otherwise now it looks good to me |
||
mod_name = " '%s'" % file.fullname() if file is not None else '' # type: ignore | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did you add three There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is still not done. |
||
if full_name in obsolete_name_mapping: | ||
self.fail("Module%s has no attribute %r (it's now called %r)" % ( | ||
mod_name, expr.name, obsolete_name_mapping[full_name]), expr) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1412,3 +1412,171 @@ def f(x: Union[int, A], a: Type[A]) -> None: | |
|
||
[builtins fixtures/isinstancelist.pyi] | ||
|
||
|
||
[case testIssubclass] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to split this test into few smaller unit tests? |
||
from typing import Type, ClassVar | ||
|
||
class Goblin: | ||
aggressive: bool = True | ||
level: int | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you need both attributes here? Some test cases could be simplified if you have only one instance variable. |
||
|
||
class GoblinAmbusher(Goblin): | ||
job: ClassVar[str] = 'Ranger' | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One blanc line is enough here |
||
def test_issubclass(cls: Type[Goblin]) -> None: | ||
if issubclass(cls, GoblinAmbusher): | ||
cls.aggressive | ||
cls.level | ||
cls.job | ||
ga = cls() | ||
ga.level = 15 | ||
ga.job | ||
ga.job = "Warrior" # E: Cannot assign to class variable "job" via instance | ||
else: | ||
cls.aggressive | ||
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): | ||
aggressive: bool = True | ||
level: int | ||
|
||
class GoblinAmbusher(Goblin): | ||
job: ClassVar[str] = 'Ranger' | ||
|
||
def test_issubclass(cls: Type[Mob]) -> None: | ||
if issubclass(cls, Goblin): | ||
cls.aggressive | ||
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.aggressive | ||
cls.level | ||
cls.job | ||
g = cls() | ||
g.level = 15 | ||
g.job | ||
g.job = 'Warrior' # E: Cannot assign to class variable "job" via instance | ||
else: | ||
cls.aggressive # E: Type[Mob] has no attribute "aggressive" | ||
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): | ||
cls.aggressive | ||
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): | ||
cls.aggressive | ||
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): | ||
aggressive: bool = True | ||
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)): | ||
cls.aggressive | ||
cls.level | ||
cls.job # E: Some element of union has no attribute "job" | ||
g = cls() | ||
g.level = 15 | ||
g.job # E: "Goblin" has no attribute "job" | ||
if issubclass(cls, GoblinAmbusher): | ||
cls.aggressive | ||
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: | ||
cls.aggressive # E: Type[Mob] has no attribute "aggressive" | ||
cls.job # E: Type[Mob] has no attribute "job" | ||
cls.level # E: Type[Mob] has no attribute "level" | ||
< 10000 /td> | m = cls() | |
m.level = 15 # E: "Mob" has no attribute "level" | ||
m.job # E: "Mob" has no attribute "job" | ||
if issubclass(cls, GoblinAmbusher): | ||
cls.aggressive | ||
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)): | ||
cls.aggressive | ||
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): | ||
if issubclass(cls, MyList): | ||
cls()[0] | ||
else: | ||
cls()[0] # E: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the error message here? Why didn't you write it here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, no error message I just accidentally left There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But shouldn't this actually be an error? In the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it's my bad. I confused this test with another where I left The reason it doesn't complain is that Btw, I would prefer |
||
|
||
if issubclass(cls, MyIntList): | ||
cls()[0] + 1 | ||
|
||
[builtins fixtures/isinstancelist.pyi] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "this an error;" -> "this is an error;"