diff --git a/mypy/checker.py b/mypy/checker.py index 26614523538e..ce892abd2b95 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -36,14 +36,17 @@ from mypy.sametypes import is_same_type from mypy.messages import MessageBuilder import mypy.checkexpr -from mypy.checkmember import map_type_from_supertype, bind_self, erase_to_bound +from mypy.checkmember import ( + map_type_from_supertype, bind_self, erase_to_bound, find_type_from_bases +) from mypy import messages from mypy.subtypes import ( is_subtype, is_equivalent, is_proper_subtype, is_more_precise, restrict_subtype_away, is_subtype_ignoring_tvars ) from mypy.maptype import map_instance_to_supertype -from mypy.semanal import fill_typevars, set_callable_name, refers_to_fullname +from mypy.typevars import fill_typevars +from mypy.semanal import set_callable_name, refers_to_fullname from mypy.erasetype import erase_typevars from mypy.expandtype import expand_type from mypy.visitor import NodeVisitor @@ -1113,6 +1116,16 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type else: lvalue_type, index_lvalue, inferred = self.check_lvalue(lvalue) if lvalue_type: + if isinstance(lvalue, NameExpr): + base_type = find_type_from_bases(lvalue) + + # If a type is known, validate the type is a subtype of the base type + if base_type: + self.check_subtype(lvalue_type, base_type, lvalue, + messages.INCOMPATIBLE_TYPES_IN_ASSIGNMENT, + 'expression has type', + 'variable has type') + if isinstance(lvalue_type, PartialType) and lvalue_type.type is None: # Try to infer a proper type for a variable with a partial None type. rvalue_type = self.accept(rvalue) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 94db4701b6ff..2125f45b3d28 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -38,7 +38,7 @@ from mypy.checkstrformat import StringFormatterChecker from mypy.expandtype import expand_type from mypy.util import split_module_names -from mypy.semanal import fill_typevars +from mypy.typevars import fill_typevars from mypy import experiments diff --git a/mypy/checkmember.py b/mypy/checkmember.py index d1e9ab1cc777..4de316f40845 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -7,14 +7,17 @@ Overloaded, TypeVarType, UnionType, PartialType, DeletedType, NoneTyp, TypeType, function_type ) -from mypy.nodes import TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context, MypyFile +from mypy.nodes import ( + TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context, MypyFile, MDEF, + NameExpr +) from mypy.nodes import ARG_POS, ARG_STAR, ARG_STAR2 from mypy.nodes import Decorator, OverloadedFuncDef from mypy.messages import MessageBuilder from mypy.maptype import map_instance_to_supertype from mypy.expandtype import expand_type_by_instance, expand_type from mypy.infer import infer_type_arguments -from mypy.semanal import fill_typevars +from mypy.typevars import fill_typevars, has_no_typevars from mypy import messages from mypy import subtypes MYPY = False @@ -616,3 +619,34 @@ def erase_to_bound(t: Type): if isinstance(t.item, TypeVarType): return TypeType(t.item.upper_bound) return t + + +def find_type_from_bases(e: NameExpr): + """For a NameExpr that is part of a class, walk all base classes and try + to find the first class that defines a Type for the same name.""" + + expr_node = e.node + if not (isinstance(expr_node, Var) and e.kind == MDEF and + len(expr_node.info.bases) > 0): + return None + + expr_name = expr_node.name() + expr_base = expr_node.info.bases[0] + + for base in expr_node.info.mro[1:]: + base_var = base.names.get(expr_name) + if base_var and base_var.type: + if has_no_typevars(base_var.type): + base_type = base_var.type + else: + itype = map_instance_to_supertype(expr_base, base) + base_type = expand_type_by_instance(base_var.type, itype) + + if isinstance(base_type, CallableType): + # If we are a property, return the Type of the return value, not the Callable + if isinstance(base_var.node, Decorator) and base_var.node.func.is_property: + base_type = base_type.ret_type + elif isinstance(base_var.node, FuncDef) and base_var.node.is_property: + base_type = base_type.ret_type + + return base_type diff --git a/mypy/semanal.py b/mypy/semanal.py index e8609fdadb1b..89ea4e41a806 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -48,6 +48,7 @@ List, Dict, Set, Tuple, cast, TypeVar, Union, Optional, Callable ) +from mypy.checkmember import find_type_from_bases from mypy.nodes import ( MypyFile, TypeInfo, Node, AssignmentStmt, FuncDef, OverloadedFuncDef, ClassDef, Var, GDEF, MODULE_REF, FuncItem, Import, Expression, Lvalue, @@ -67,6 +68,7 @@ IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr, TempNode, COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES, ) +from mypy.typevars import has_no_typevars, fill_typevars from mypy.visitor import NodeVisitor from mypy.traverser import TraverserVisitor from mypy.errors import Errors, report_internal_error @@ -79,7 +81,6 @@ from mypy.typeanal import TypeAnalyser, TypeAnalyserPass3, analyze_type_alias from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.sametypes import is_same_type -from mypy.erasetype import erase_typevars from mypy.options import Options from mypy import join @@ -1162,11 +1163,15 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: s.type = self.anal_type(s.type, allow_tuple_literal) else: # For simple assignments, allow binding type aliases. - # Also set the type if the rvalue is a simple literal. + # Also use the type of the base class if available, or + # set the type if the rvalue is a simple literal. if (s.type is None and len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr)): if s.lvalues[0].is_def: - s.type = self.analyze_simple_literal_type(s.rvalue) + s.type = find_type_from_bases(s.lvalues[0]) + if s.type is None: + s.type = self.analyze_simple_literal_type(s.rvalue) + res = analyze_type_alias(s.rvalue, self.lookup_qualified, self.lookup_fully_qualified, @@ -3130,19 +3135,6 @@ def builtin_type(self, name: str, args: List[Type] = None) -> Instance: return Instance(sym.node, args or []) -def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]: - """For a non-generic type, return instance type representing the type. - For a generic G type with parameters T1, .., Tn, return G[T1, ..., Tn]. - """ - tv = [] # type: List[Type] - for i in range(len(typ.type_vars)): - tv.append(TypeVarType(typ.defn.type_vars[i])) - inst = Instance(typ, tv) - if typ.tuple_type is None: - return inst - return typ.tuple_type.copy_modified(fallback=inst) - - def replace_implicit_first_type(sig: FunctionLike, new: Type) -> FunctionLike: if isinstance(sig, CallableType): return sig.copy_modified(arg_types=[new] + sig.arg_types[1:]) @@ -3514,7 +3506,3 @@ def find_fixed_callable_return(expr: Expression) -> Optional[CallableType]: if isinstance(t.ret_type, CallableType): return t.ret_type return None - - -def has_no_typevars(typ: Type) -> bool: - return is_same_type(typ, erase_typevars(typ)) diff --git a/mypy/typevars.py b/mypy/typevars.py new file mode 100644 index 000000000000..1bdb1049ebed --- /dev/null +++ b/mypy/typevars.py @@ -0,0 +1,24 @@ +from typing import Union + +from mypy.nodes import TypeInfo + +from mypy.erasetype import erase_typevars +from mypy.sametypes import is_same_type +from mypy.types import Instance, TypeVarType, TupleType, Type + + +def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]: + """For a non-generic type, return instance type representing the type. + For a generic G type with parameters T1, .., Tn, return G[T1, ..., Tn]. + """ + tv = [] # type: List[Type] + for i in range(len(typ.type_vars)): + tv.append(TypeVarType(typ.defn.type_vars[i])) + inst = Instance(typ, tv) + if typ.tuple_type is None: + return inst + return typ.tuple_type.copy_modified(fallback=inst) + + +def has_no_typevars(typ: Type) -> bool: + return is_same_type(typ, erase_typevars(typ)) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index e6f097972372..6f5acccaa06e 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2123,3 +2123,153 @@ class B(object, A): # E: Cannot determine consistent method resolution order (MR # flags: --fast-parser class C(metaclass=int()): # E: Dynamic metaclass not supported for 'C' pass + +[case testVariableSubclass] +class A: + a = 1 # type: int +class B(A): + a = 1 +[out] + +[case testVariableSubclassAssignMismatch] +class A: + a = 1 # type: int +class B(A): + a = "a" +[out] +main: note: In class "B": +main:4: error: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testVariableSubclassAssignment] +class A: + a = None # type: int +class B(A): + def __init__(self) -> None: + self.a = "a" +[out] +main: note: In member "__init__" of class "B": +main:5: error: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testVariableSubclassTypeOverwrite] +class A: + a = None # type: int +class B(A): + a = None # type: str +class C(B): + a = "a" +[out] +main: note: In class "B": +main:4: error: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testVariableSubclassTypeOverwriteImplicit] +class A: + a = 1 +class B(A): + a = None # type: str +[out] +main: note: In class "B": +main:4: error: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testVariableSuperUsage] +class A: + a = [] # type: list +class B(A): + a = [1, 2] +class C(B): + a = B.a + [3] +[builtins fixtures/list.pyi] +[out] + +[case testVariableRvalue] +class A: + a = None +class B(A): + a = 1 +class C(B): + a = "a" +[out] +main: note: In class "A": +main:2: error: Need type annotation for variable +main: note: In class "C": +main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testVariableTypeVar] +from typing import TypeVar, Generic +T = TypeVar('T') +class A(Generic[T]): + a = None # type: T +class B(A[int]): + a = 1 + +[case testVariableTypeVarInvalid] +from typing import TypeVar, Generic +T = TypeVar('T') +class A(Generic[T]): + a = None # type: T +class B(A[int]): + a = "abc" +[out] +main: note: In class "B": +main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testVariableTypeVarIndirectly] +from typing import TypeVar, Generic +T = TypeVar('T') +class A(Generic[T]): + a = None # type: T +class B(A[int]): + pass +class C(B): + a = "a" +[out] +main: note: In class "C": +main:8: error: Incompatible types in assignment (expression has type "str", variable has type "int") + +[case testVariableTypeVarList] +from typing import List, TypeVar, Generic +T = TypeVar('T') +class A(Generic[T]): + a = None # type: List[T] + b = None # type: List[T] +class B(A[int]): + a = [1] + b = [''] +[builtins fixtures/list.pyi] +[out] +main: note: In class "B": +main:8: error: List item 0 has incompatible type "str" + +[case testVariableMethod] +class A: + def a(self) -> None: pass + b = 1 +class B(A): + a = 1 + def b(self) -> None: pass +[out] +main: note: In class "B": +main:5: error: Incompatible types in assignment (expression has type "int", variable has type Callable[[A], None]) +main:6: error: Signature of "b" incompatible with supertype "A" + +[case testVariableProperty] +class A: + @property + def a(self) -> bool: pass +class B(A): + a = None # type: bool +class C(A): + a = True +class D(A): + a = 1 +[builtins fixtures/property.pyi] +[out] +main: note: In class "D": +main:9: error: Incompatible types in assignment (expression has type "int", variable has type "bool") + +[case testVariableOverwriteAny] +from typing import Any +class A: + a = 1 +class B(A): + a = 'x' # type: Any +[out]