diff --git a/mypy/checker.py b/mypy/checker.py index e1b844465c07..0d1068449e05 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -45,7 +45,7 @@ from mypy.maptype import map_instance_to_supertype from mypy.semanal import fill_typevars, set_callable_name, refers_to_fullname from mypy.erasetype import erase_typevars -from mypy.expandtype import expand_type +from mypy.expandtype import expand_type, expand_type_by_instance from mypy.visitor import NodeVisitor from mypy.join import join_types from mypy.treetransform import TransformVisitor @@ -1149,6 +1149,11 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type 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.accept(lvalue.expr) + rvalue_type, infer_lvalue_type = self.check_member_assignment( + instance_type, lvalue_type, rvalue, lvalue) else: rvalue_type = self.check_simple_assignment(lvalue_type, rvalue, lvalue) @@ -1478,6 +1483,60 @@ def check_simple_assignment(self, lvalue_type: Type, rvalue: Expression, '{} has type'.format(lvalue_name)) return rvalue_type + def check_member_assignment(self, instance_type: Type, attribute_type: Type, + rvalue: Expression, context: Context) -> Tuple[Type, bool]: + """Type member assigment. + + This is defers to check_simple_assignment, unless the member expression + is a descriptor, in which case this checks descriptor semantics as well. + + Return the inferred rvalue_type and whether to infer anything about the attribute type + """ + # Descriptors don't participate in class-attribute access + if ((isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or + isinstance(instance_type, TypeType)): + rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) + return rvalue_type, True + + if not isinstance(attribute_type, Instance): + rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) + return rvalue_type, True + + if not attribute_type.type.has_readable_member('__set__'): + # If there is no __set__, we type-check that the assigned value matches + # the return type of __get__. This doesn't match the python semantics, + # (which allow you to override the descriptor with any value), but preserves + # the type of accessing the attribute (even after the override). + if attribute_type.type.has_readable_member('__get__'): + attribute_type = self.expr_checker.analyze_descriptor_access( + instance_type, attribute_type, context) + rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) + return rvalue_type, True + + dunder_set = attribute_type.type.get_method('__set__') + if dunder_set is None: + self.msg.fail("{}.__set__ is not callable".format(attribute_type), context) + return AnyType(), False + + function = function_type(dunder_set, self.named_type('builtins.function')) + bound_method = bind_self(function, attribute_type) + typ = map_instance_to_supertype(attribute_type, dunder_set.info) + dunder_set_type = expand_type_by_instance(bound_method, typ) + + _, inferred_dunder_set_type = self.expr_checker.check_call( + dunder_set_type, [TempNode(instance_type), rvalue], + [nodes.ARG_POS, nodes.ARG_POS], context) + + if not isinstance(inferred_dunder_set_type, CallableType): + self.fail("__set__ is not callable", context) + return AnyType(), True + + if len(inferred_dunder_set_type.arg_types) < 2: + # A message already will have been recorded in check_call + return AnyType(), False + + return inferred_dunder_set_type.arg_types[1], False + def check_indexed_assignment(self, lvalue: IndexExpr, rvalue: Expression, context: Context) -> None: """Type check indexed assignment base[index] = rvalue. diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 94db4701b6ff..6c6f58ff185d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -7,7 +7,7 @@ Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef, TupleType, TypedDictType, Instance, TypeVarId, TypeVarType, ErasedType, UnionType, PartialType, DeletedType, UnboundType, UninhabitedType, TypeType, - true_only, false_only, is_named_instance, function_type, + true_only, false_only, is_named_instance, function_type, FunctionLike, get_typ_args, set_typ_args, ) from mypy.nodes import ( @@ -30,13 +30,14 @@ from mypy import messages from mypy.infer import infer_type_arguments, infer_function_type_arguments from mypy import join +from mypy.maptype import map_instance_to_supertype from mypy.subtypes import is_subtype, is_equivalent from mypy import applytype from mypy import erasetype -from mypy.checkmember import analyze_member_access, type_object_type +from mypy.checkmember import analyze_member_access, type_object_type, bind_self from mypy.constraints import get_actual_type from mypy.checkstrformat import StringFormatterChecker -from mypy.expandtype import expand_type +from mypy.expandtype import expand_type, expand_type_by_instance from mypy.util import split_module_names from mypy.semanal import fill_typevars @@ -983,10 +984,69 @@ def analyze_ordinary_member_access(self, e: MemberExpr, else: # This is a reference to a non-module attribute. original_type = self.accept(e.expr) - return analyze_member_access(e.name, original_type, e, - is_lvalue, False, False, - self.named_type, self.not_ready_callback, self.msg, - original_type=original_type, chk=self.chk) + member_type = analyze_member_access( + e.name, original_type, e, is_lvalue, False, False, + self.named_type, self.not_ready_callback, self.msg, + original_type=original_type, chk=self.chk) + if is_lvalue: + return member_type + else: + return self.analyze_descriptor_access(original_type, member_type, e) + + def analyze_descriptor_access(self, instance_type: Type, descriptor_type: Type, + context: Context) -> Type: + """Type check descriptor access. + + Arguments: + instance_type: The type of the instance on which the descriptor + attribute is being accessed (the type of ``a`` in ``a.f`` when + ``f`` is a descriptor). + descriptor_type: The type of the descriptor attribute being accessed + (the type of ``f`` in ``a.f`` when ``f`` is a descriptor). + context: The node defining the context of this inference. + Return: + The return type of the appropriate ``__get__`` overload for the descriptor. + """ + if not isinstance(descriptor_type, Instance): + return descriptor_type + + if not descriptor_type.type.has_readable_member('__get__'): + return descriptor_type + + dunder_get = descriptor_type.type.get_method('__get__') + + if dunder_get is None: + self.msg.fail("{}.__get__ is not callable".format(descriptor_type), context) + return AnyType() + + function = function_type(dunder_get, self.named_type('builtins.function')) + bound_method = bind_self(function, descriptor_type) + typ = map_instance_to_supertype(descriptor_type, dunder_get.info) + dunder_get_type = expand_type_by_instance(bound_method, typ) + owner_type = None # type: Type + + if isinstance(instance_type, FunctionLike) and instance_type.is_type_obj(): + owner_type = instance_type.items()[0].ret_type + instance_type = NoneTyp() + elif isinstance(instance_type, TypeType): + owner_type = instance_type.item + instance_type = NoneTyp() + else: + owner_type = instance_type + + _, inferred_dunder_get_type = self.check_call( + dunder_get_type, [TempNode(instance_type), TempNode(TypeType(owner_type))], + [nodes.ARG_POS, nodes.ARG_POS], context) + + if isinstance(inferred_dunder_get_type, AnyType): + # check_call failed, and will have reported an error + return inferred_dunder_get_type + + if not isinstance(inferred_dunder_get_type, CallableType): + self.msg.fail("{}.__get__ is not callable".format(descriptor_type), context) + return AnyType() + + return inferred_dunder_get_type.ret_type def analyze_external_member_access(self, member: str, base_type: Type, context: Context) -> Type: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index d1e9ab1cc777..4295d4e6ed2b 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -210,6 +210,7 @@ def analyze_member_var_access(name: str, itype: Instance, info: TypeInfo, if isinstance(vv, Decorator): # The associated Var node of a decorator contains the type. v = vv.var + if isinstance(v, Var): return analyze_var(name, v, itype, info, node, is_lvalue, msg, original_type, not_ready_callback) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 1434bb3c7caa..bdfa2de9b340 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -805,10 +805,8 @@ import typing class A: @property def f(self) -> str: pass -a = A() # type. A -s = '' # type: str -s = a.f -a = a.f # E: Incompatible types in assignment (expression has type "str", variable has type "A") +a = A() +reveal_type(a.f) # E: Revealed type is 'builtins.str' [builtins fixtures/property.pyi] [case testAssigningToReadOnlyProperty] @@ -834,7 +832,7 @@ import typing class A: @property def f(self): pass -a = A() # type. A +a = A() a.f.xx a.f = '' # E: Property "f" defined in "A" is read-only [builtins fixtures/property.pyi] @@ -868,6 +866,359 @@ a.f = a.f # E: Property "f" defined in "A" is read-only a.f.x # E: "int" has no attribute "x" [builtins fixtures/property.pyi] +-- Descriptors +-- ----------- + + +[case testAccessingNonDataDescriptor] +from typing import Any +class D: + def __get__(self, inst: Any, own: Any) -> str: return 's' +class A: + f = D() +a = A() +reveal_type(a.f) # E: Revealed type is 'builtins.str' + +[case testSettingNonDataDescriptor] +from typing import Any +class D: + def __get__(self, inst: Any, own: Any) -> str: return 's' +class A: + f = D() +a = A() +a.f = 'foo' +a.f = D() # E: Incompatible types in assignment (expression has type "D", variable has type "str") + +[case testSettingDataDescriptor] +from typing import Any +class D: + def __get__(self, inst: Any, own: Any) -> str: return 's' + def __set__(self, inst: Any, value: str) -> None: pass +class A: + f = D() +a = A() +a.f = '' +a.f = 1 # E: Argument 2 to "__set__" of "D" has incompatible type "int"; expected "str" + +[case testReadingDescriptorWithoutDunderGet] +from typing import Union, Any +class D: + def __set__(self, inst: Any, value: str) -> None: pass +class A: + f = D() + def __init__(self): self.f = 's' +a = A() +reveal_type(a.f) # E: Revealed type is '__main__.D' + +[case testAccessingDescriptorFromClass] +# flags: --strict-optional +from d import D, Base +class A(Base): + f = D() +reveal_type(A.f) # E: Revealed type is 'd.D' +reveal_type(A().f) # E: Revealed type is 'builtins.str' +[file d.pyi] +from typing import TypeVar, Type, Generic, overload +class Base: pass +class D: + def __init__(self) -> None: pass + @overload + def __get__(self, inst: None, own: Type[Base]) -> D: pass + @overload + def __get__(self, inst: Base, own: Type[Base]) -> str: pass +[builtins fixtures/bool.pyi] + +[case testAccessingDescriptorFromClassWrongBase] +# flags: --strict-optional +from d import D, Base +class A: + f = D() +reveal_type(A.f) +reveal_type(A().f) +[file d.pyi] +from typing import TypeVar, Type, Generic, overload +class Base: pass +class D: + def __init__(self) -> None: pass + @overload + def __get__(self, inst: None, own: Type[Base]) -> D: pass + @overload + def __get__(self, inst: Base, own: Type[Base]) -> str: pass +[builtins fixtures/bool.pyi] +[out] +main:5: error: Revealed type is 'Any' +main:5: error: No overload variant of "__get__" of "D" matches argument types [builtins.None, Type[__main__.A]] +main:6: error: Revealed type is 'Any' +main:6: error: No overload variant of "__get__" of "D" matches argument types [__main__.A, Type[__main__.A]] + + +[case testAccessingGenericNonDataDescriptor] +from typing import TypeVar, Type, Generic, Any +V = TypeVar('V') +class D(Generic[V]): + def __init__(self, v: V) -> None: self.v = v + def __get__(self, inst: Any, own: Type) -> V: return self.v +class A: + f = D(10) + g = D('10') +a = A() +reveal_type(a.f) # E: Revealed type is 'builtins.int*' +reveal_type(a.g) # E: Revealed type is 'builtins.str*' + +[case testSettingGenericDataDescriptor] +from typing import TypeVar, Type, Generic, Any +V = TypeVar('V') +class D(Generic[V]): + def __init__(self, v: V) -> None: self.v = v + def __get__(self, inst: Any, own: Type) -> V: return self.v + def __set__(self, inst: Any, v: V) -> None: pass +class A: + f = D(10) + g = D('10') +a = A() +a.f = 1 +a.f = '' # E: Argument 2 to "__set__" of "D" has incompatible type "str"; expected "int" +a.g = '' +a.g = 1 # E: Argument 2 to "__set__" of "D" has incompatible type "int"; expected "str" + +[case testAccessingGenericDescriptorFromClass] +# flags: --strict-optional +from d import D +class A: + f = D(10) # type: D[A, int] + g = D('10') # type: D[A, str] +reveal_type(A.f) # E: Revealed type is 'd.D[__main__.A*, builtins.int*]' +reveal_type(A.g) # E: Revealed type is 'd.D[__main__.A*, builtins.str*]' +reveal_type(A().f) # E: Revealed type is 'builtins.int*' +reveal_type(A().g) # E: Revealed type is 'builtins.str*' +[file d.pyi] +from typing import TypeVar, Type, Generic, overload +T = TypeVar('T') +V = TypeVar('V') +class D(Generic[T, V]): + def __init__(self, v: V) -> None: pass + @overload + def __get__(self, inst: None, own: Type[T]) -> 'D[T, V]': pass + @overload + def __get__(self, inst: T, own: Type[T]) -> V: pass +[builtins fixtures/bool.pyi] + +[case testAccessingGenericDescriptorFromInferredClass] +# flags: --strict-optional +from typing import Type +from d import D +class A: + f = D(10) # type: D[A, int] + g = D('10') # type: D[A, str] +def f(some_class: Type[A]): + reveal_type(some_class.f) + reveal_type(some_class.g) +[file d.pyi] +from typing import TypeVar, Type, Generic, overload +T = TypeVar('T') +V = TypeVar('V') +class D(Generic[T, V]): + def __init__(self, v: V) -> None: pass + @overload + def __get__(self, inst: None, own: Type[T]) -> 'D[T, V]': pass + @overload + def __get__(self, inst: T, own: Type[T]) -> V: pass +[builtins fixtures/bool.pyi] +[out] +main:8: error: Revealed type is 'd.D[__main__.A*, builtins.int*]' +main:9: error: Revealed type is 'd.D[__main__.A*, builtins.str*]' + +[case testAccessingGenericDescriptorFromClassBadOverload] +# flags: --strict-optional +from d import D +class A: + f = D(10) # type: D[A, int] +reveal_type(A.f) +[file d.pyi] +from typing import TypeVar, Type, Generic, overload +T = TypeVar('T') +V = TypeVar('V') +class D(Generic[T, V]): + def __init__(self, v: V) -> None: pass + @overload + def __get__(self, inst: None, own: None) -> 'D[T, V]': pass + @overload + def __get__(self, inst: T, own: Type[T]) -> V: pass +[builtins fixtures/bool.pyi] +[out] +main:5: error: Revealed type is 'Any' +main:5: error: No overload variant of "__get__" of "D" matches argument types [builtins.None, Type[__main__.A]] + +[case testAccessingNonDataDescriptorSubclass] +from typing import Any +class C: + def __get__(self, inst: Any, own: Any) -> str: return 's' +class D(C): pass +class A: + f = D() +a = A() +reveal_type(a.f) # E: Revealed type is 'builtins.str' + +[case testSettingDataDescriptorSubclass] +from typing import Any +class C: + def __get__(self, inst: Any, own: Any) -> str: return 's' + def __set__(self, inst: Any, v: str) -> None: pass +class D(C): pass +class A: + f = D() +a = A() +a.f = '' +a.f = 1 # E: Argument 2 to "__set__" of "C" has incompatible type "int"; expected "str" + +[case testReadingDescriptorSubclassWithoutDunderGet] +from typing import Union, Any +class C: + def __set__(self, inst: Any, v: str) -> None: pass +class D(C): pass +class A: + f = D() + def __init__(self): self.f = 's' +a = A() +reveal_type(a.f) # E: Revealed type is '__main__.D' + +[case testAccessingGenericNonDataDescriptorSubclass] +from typing import TypeVar, Type, Generic, Any +V = TypeVar('V') +class C(Generic[V]): + def __init__(self, v: V) -> None: self.v = v + def __get__(self, inst: Any, own: Type) -> V: return self.v +class D(C[V], Generic[V]): pass +class A: + f = D(10) + g = D('10') +a = A() +reveal_type(a.f) # E: Revealed type is 'builtins.int*' +reveal_type(a.g) # E: Revealed type is 'builtins.str*' + +[case testSettingGenericDataDescriptorSubclass] +from typing import TypeVar, Type, Generic +T = TypeVar('T') +V = TypeVar('V') +class C(Generic[T, V]): + def __init__(self, v: V) -> None: self.v = v + def __get__(self, inst: T, own: Type[T]) -> V: return self.v + def __set__(self, inst: T, v: V) -> None: pass +class D(C[T, V], Generic[T, V]): pass +class A: + f = D(10) # type: D[A, int] + g = D('10') # type: D[A, str] +a = A() +a.f = 1 +a.f = '' # E: Argument 2 to "__set__" of "C" has incompatible type "str"; expected "int" +a.g = '' +a.g = 1 # E: Argument 2 to "__set__" of "C" has incompatible type "int"; expected "str" + +[case testSetDescriptorOnClass] +from typing import TypeVar, Type, Generic +T = TypeVar('T') +V = TypeVar('V') +class D(Generic[T, V]): + def __init__(self, v: V) -> None: self.v = v + def __get__(self, inst: T, own: Type[T]) -> V: return self.v + def __set__(self, inst: T, v: V) -> None: pass +class A: + f = D(10) # type: D[A, int] +A.f = D(20) +A.f = D('some string') # E: Argument 1 to "D" has incompatible type "str"; expected "int" + +[case testSetDescriptorOnInferredClass] +from typing import TypeVar, Type, Generic, Any +V = TypeVar('V') +class D(Generic[V]): + def __init__(self, v: V) -> None: self.v = v + def __get__(self, inst: Any, own: Type) -> V: return self.v + def __set__(self, inst: Any, v: V) -> None: pass +class A: + f = D(10) +def f(some_class: Type[A]): + A.f = D(20) + A.f = D('some string') +[out] +main:11: error: Argument 1 to "D" has incompatible type "str"; expected "int" + +[case testDescriptorUncallableDunderSet] +class D: + __set__ = 's' +class A: + f = D() +A().f = 'x' # E: __main__.D.__set__ is not callable + +[case testDescriptorDunderSetTooFewArgs] +class D: + def __set__(self, inst): pass +class A: + f = D() +A().f = 'x' # E: Too many arguments for "__set__" + +[case testDescriptorDunderSetTooManyArgs] +class D: + def __set__(self, inst, v, other): pass +class A: + f = D() +A().f = 'x' # E: Too few arguments for "__set__" + +[case testDescriptorDunderSetWrongArgTypes] +class D: + def __set__(self, inst: str, v:str) -> None: pass +class A: + f = D() +A().f = 'x' # E: Argument 1 to "__set__" of "D" has incompatible type "A"; expected "str" + +[case testDescriptorUncallableDunderGet] +class D: + __get__ = 's' +class A: + f = D() +A().f # E: __main__.D.__get__ is not callable + +[case testDescriptorDunderGetTooFewArgs] +class D: + def __get__(self, inst): pass +class A: + f = D() +A().f # E: Too many arguments for "__get__" + +[case testDescriptorDunderGetTooManyArgs] +class D: + def __get__(self, inst, own, other): pass +class A: + f = D() +A().f = 'x' # E: Too few arguments for "__get__" + +[case testDescriptorDunderGetWrongArgTypeForInstance] +from typing import Any +class D: + def __get__(self, inst: str, own: Any) -> Any: pass +class A: + f = D() +A().f # E: Argument 1 to "__get__" of "D" has incompatible type "A"; expected "str" + +[case testDescriptorDunderGetWrongArgTypeForOwner] +from typing import Any +class D: + def __get__(self, inst: Any, own: str) -> Any: pass +class A: + f = D() +A().f # E: Argument 2 to "__get__" of "D" has incompatible type Type[A]; expected "str" + +[case testDescriptorGetSetDifferentTypes] +from typing import Any +class D: + def __get__(self, inst: Any, own: Any) -> str: return 's' + def __set__(self, inst: Any, v: int) -> None: pass +class A: + f = D() +a = A() +a.f = 1 +reveal_type(a.f) # E: Revealed type is 'builtins.str' + + -- _promote decorators -- -------------------