Skip to content

Infer type of class variables from base classes #12022

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2267,13 +2267,6 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type
self.check_compatibility_all_supers(lvalue, lvalue_type, rvalue)):
# We hit an error on this line; don't check for any others
return
elif (is_literal_none(rvalue) and
isinstance(lvalue, NameExpr) and
isinstance(lvalue.node, Var) and
lvalue.node.is_initialized_in_class and
not new_syntax):
# Allow None's to be assigned to class variables with non-Optional types.
rvalue_type = lvalue_type
elif (isinstance(lvalue, MemberExpr) and
lvalue.kind is None): # Ignore member access to modules
instance_type = self.expr_checker.accept(lvalue.expr)
Expand Down Expand Up @@ -3045,8 +3038,40 @@ def check_lvalue(self, lvalue: Lvalue) -> Tuple[Optional[Type],
not isinstance(lvalue, NameExpr) or isinstance(lvalue.node, Var)
):
if isinstance(lvalue, NameExpr):
inferred = cast(Var, lvalue.node)
assert isinstance(inferred, Var)
# If this is a class variable without a type annotation and
# there's a base class with the same name then set the type to
# that variable's type.
#
# We need to check lvalue.node.info.__bool__() because it can
# be FakeInfo.
type_set = False
if (
isinstance(lvalue.node, Var) and
lvalue.node.type is None and
lvalue.node.info and
not (lvalue.name.startswith("__") and lvalue.name.endswith("__"))
):

for base in lvalue.node.info.mro[1:]:
base_type, _base_node = self.lvalue_type_from_base(lvalue.node, base)

if base_type:
# Give up on callables for now. MyPy does not deal with
# them properly.
# See https://github.com/microsoft/pyright/issues/2805
if isinstance(get_proper_type(base_type), CallableType):
break
lvalue_type = base_type
lvalue.node.type = lvalue_type
self.store_type(lvalue, lvalue_type)
type_set = True
break

# If the type still isn't set, infer it to be any var.
if not type_set:
inferred = cast(Var, lvalue.node)
assert isinstance(inferred, Var)
Comment on lines +3072 to +3073
Copy link
Collaborator

@A5rocks A5rocks Jan 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit suspicious -- does assert isinstance(lvalue.node, Var) and then inferred = lvalue.node not work here? I would have thought it would narrow.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot!


else:
assert isinstance(lvalue, MemberExpr)
self.expr_checker.accept(lvalue.expr)
Expand Down
6 changes: 5 additions & 1 deletion mypyc/test-data/irbuild-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -1067,7 +1067,7 @@ class B(A):
y = LOL
z: Optional[str] = None
b = True
bogus = None # type: int
bogus = None # type: Optional[int]
[out]
def A.lol(self):
self :: __main__.A
Expand All @@ -1091,6 +1091,8 @@ def B.__mypyc_defaults_setup(__mypyc_self__):
r5 :: bool
r6 :: object
r7, r8 :: bool
r9 :: object
r10 :: bool
L0:
__mypyc_self__.x = 20; r0 = is_error
r1 = __main__.globals :: static
Expand All @@ -1101,6 +1103,8 @@ L0:
r6 = box(None, 1)
__mypyc_self__.z = r6; r7 = is_error
__mypyc_self__.b = 1; r8 = is_error
r9 = box(None, 1)
__mypyc_self__.bogus = r9; r10 = is_error
return 1

[case testSubclassDictSpecalized]
Expand Down
26 changes: 25 additions & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -4339,7 +4339,7 @@ class C(B):
x = object()
[out]
main:4: error: Incompatible types in assignment (expression has type "str", base class "A" defined the type as "int")
main:6: error: Incompatible types in assignment (expression has type "object", base class "B" defined the type as "str")
main:6: error: Incompatible types in assignment (expression has type "object", variable has type "str")

[case testClassOneErrorPerLine]
class A:
Expand Down Expand Up @@ -7134,3 +7134,27 @@ class B(A): # E: Final class __main__.B has abstract attributes "foo"
[case testUndefinedBaseclassInNestedClass]
class C:
class C1(XX): pass # E: Name "XX" is not defined

[case testInheritClassVariableType]
# flags: --python-version 3.7
from __future__ import annotations
class Base:
X: list[int] = []
class Derived0(Base):
X = []
class Derived1(Base):
X: list[str] = [] # E: Incompatible types in assignment (expression has type "List[str]", base class "Base" defined the type as "List[int]")
class Derived2(Base):
X = ["a"] # E: List item 0 has incompatible type "str"; expected "int"
[builtins fixtures/tuple.pyi]

[case testInheritClassVariableTypeIncompatibility]
# flags: --python-version 3.7
from __future__ import annotations
class A:
x: list[int]
class B(A):
x: list[str] # E: Incompatible types in assignment (expression has type "List[str]", base class "A" defined the type as "List[int]")
class C(B):
x = ["hi"]
[builtins fixtures/tuple.pyi]
4 changes: 2 additions & 2 deletions test-data/unit/check-formatting.test
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ class D(bytes):

[case testFormatCallFormatTypesBytesNotPy2]
# flags: --py2
from typing import Union, TypeVar, NewType, Generic
from typing import Union, TypeVar, NewType, Generic, Optional

A = TypeVar('A', str, unicode)
B = TypeVar('B', bound=str)
Expand All @@ -547,7 +547,7 @@ u'{}'.format(x)
u'{}'.format(n)

class C(Generic[B]):
x = None # type: B
x = None # type: Optional[B]
def meth(self):
# type: () -> None
'{}'.format(self.x)
Expand Down
12 changes: 6 additions & 6 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -5205,7 +5205,7 @@ def test() -> None:
[out]
[out2]

[case testCannotDetermineTypeFromOtherModule]
[case testCanDetermineTypeFromOtherModule]

import aa

Expand Down Expand Up @@ -5233,15 +5233,15 @@ class Base:
def foo(self) -> int: ...

class Sub(Base):
foo = desc(42) # type: ignore
foo = desc(42)

[builtins fixtures/property.pyi]
[out]
tmp/a.py:3: error: Cannot determine type of "foo"
tmp/a.py:4: error: Cannot determine type of "foo"
tmp/b.py:12: error: Too many arguments for "desc"
tmp/b.py:12: error: Incompatible types in assignment (expression has type "desc", variable has type "int")
[out2]
tmp/a.py:3: error: Cannot determine type of "foo"
tmp/a.py:4: error: Cannot determine type of "foo"
tmp/b.py:12: error: Too many arguments for "desc"
tmp/b.py:12: error: Incompatible types in assignment (expression has type "desc", variable has type "int")

[case testRedefinitionClass]
import b
Expand Down
18 changes: 9 additions & 9 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -2575,15 +2575,15 @@ class A:
class B(A):
x = None

reveal_type(B.x) # N: Revealed type is "None"
reveal_type(B.x) # N: Revealed type is "Union[builtins.str, None]"

[case testLocalPartialTypesWithInheritance2]
# flags: --local-partial-types --strict-optional
class A:
x: str

class B(A):
x = None # E: Incompatible types in assignment (expression has type "None", base class "A" defined the type as "str")
x = None # E: Incompatible types in assignment (expression has type "None", variable has type "str")

[case testLocalPartialTypesWithAnyBaseClass]
# flags: --local-partial-types --strict-optional
Expand Down Expand Up @@ -2611,8 +2611,8 @@ class C(B):
x = None

# TODO: Inferring None below is unsafe (https://github.com/python/mypy/issues/3208)
reveal_type(B.x) # N: Revealed type is "None"
reveal_type(C.x) # N: Revealed type is "None"
reveal_type(B.x) # N: Revealed type is "Union[builtins.str, None]"
reveal_type(C.x) # N: Revealed type is "Union[builtins.str, None]"

[case testLocalPartialTypesWithInheritance3]
# flags: --local-partial-types
Expand All @@ -2628,7 +2628,7 @@ class B(A):
x = None
x = Y()

reveal_type(B.x) # N: Revealed type is "Union[__main__.Y, None]"
reveal_type(B.x) # N: Revealed type is "Union[__main__.X, None]"

[case testLocalPartialTypesBinderSpecialCase]
# flags: --local-partial-types
Expand Down Expand Up @@ -2813,8 +2813,8 @@ class C(A):
x = ['12']

reveal_type(A.x) # N: Revealed type is "builtins.list[Any]"
reveal_type(B.x) # N: Revealed type is "builtins.list[builtins.int]"
reveal_type(C.x) # N: Revealed type is "builtins.list[builtins.str]"
reveal_type(B.x) # N: Revealed type is "builtins.list[Any]"
reveal_type(C.x) # N: Revealed type is "builtins.list[Any]"

[builtins fixtures/list.pyi]

Expand Down Expand Up @@ -2920,15 +2920,15 @@ class A:

class B(A):
x = None
x = 2 # E: Incompatible types in assignment (expression has type "int", base class "A" defined the type as "str")
x = 2 # E: Incompatible types in assignment (expression has type "int", variable has type "str")

[case testInheritedAttributeStrictOptional]
# flags: --strict-optional
class A:
x: str

class B(A):
x = None # E: Incompatible types in assignment (expression has type "None", base class "A" defined the type as "str")
x = None # E: Incompatible types in assignment (expression has type "None", variable has type "str")
x = ''

[case testNeedAnnotationForCallable]
Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-optional.test
Original file line number Diff line number Diff line change
Expand Up @@ -215,22 +215,22 @@ x() # E: "Dict[str, int]" not callable
[case testNoneClassVariable]
from typing import Optional
class C:
x = None # type: int
x = None # type: Optional[int]
def __init__(self) -> None:
self.x = 0

[case testNoneClassVariableInInit]
from typing import Optional
class C:
x = None # type: int
x = 0 # type: int
def __init__(self) -> None:
self.x = None # E: Incompatible types in assignment (expression has type "None", variable has type "int")
[out]

[case testMultipleAssignmentNoneClassVariableInInit]
from typing import Optional
class C:
x, y = None, None # type: int, str
x, y = 0, "" # type: int, str
def __init__(self) -> None:
self.x = None # E: Incompatible types in assignment (expression has type "None", variable has type "int")
self.y = None # E: Incompatible types in assignment (expression has type "None", variable has type "str")
Expand Down
3 changes: 2 additions & 1 deletion test-data/unit/deps-types.test
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,9 @@ def f(arg):
# type: (Type[C]) -> None
arg.x
[file mod.py]
from typing import Optional
class M(type):
x = None # type: int
x = None # type: Optional[int]
class C:
__metaclass__ = M
[out]
Expand Down
16 changes: 10 additions & 6 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -2962,20 +2962,24 @@ import submod
class C:
__metaclass__ = submod.M
[file submod.py]
from typing import Optional
class M(type):
x = None # type: int
x = None # type: Optional[int]
[file submod.py.2]
from typing import Optional
class M(type):
x = None # type: str
x = None # type: Optional[str]
[file submod.py.3]
from typing import Optional
class M(type):
y = None # type: str
y = None # type: Optional[str]
[file submod.py.4]
from typing import Optional
class M(type):
x = None # type: int
x = None # type: Optional[int]
[out]
==
a.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "str")
a.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "Optional[str]")
==
a.py:4: error: "Type[C]" has no attribute "x"
==
Expand Down Expand Up @@ -3686,7 +3690,7 @@ class BaseS:
x: str
[out]
==
b.py:3: error: Incompatible types in assignment (expression has type "int", base class "BaseS" defined the type as "str")
b.py:3: error: Incompatible types in assignment (expression has type "int", variable has type "str")

[case testAliasFineGenericMod]
import b
Expand Down