From 4e34fecfe59d83aa3b9e04cbde1834aff7a47e5d Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Fri, 3 Dec 2021 16:20:30 +0300 Subject: [PATCH] Now `TypeInfo.get_method` also returns `Decorator` nodes (#11150) Support decorators properly in additional contexts. Closes #10409 --- mypy/checker.py | 30 ++++- mypy/checkmember.py | 109 +++++++++------- mypy/nodes.py | 4 +- mypy/subtypes.py | 24 ++-- test-data/unit/check-callable.test | 50 +++++++ test-data/unit/check-classes.test | 166 ++++++++++++++++++++++++ test-data/unit/check-custom-plugin.test | 75 ++++++++++- test-data/unit/check-flags.test | 61 +++++++++ test-data/unit/check-protocols.test | 51 ++++++++ test-data/unit/fixtures/bool.pyi | 1 + 10 files changed, 505 insertions(+), 66 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index ac4b905643ea..9dc8eaea2a99 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -46,7 +46,8 @@ ) import mypy.checkexpr from mypy.checkmember import ( - analyze_member_access, analyze_descriptor_access, type_object_type, + MemberContext, analyze_member_access, analyze_descriptor_access, analyze_var, + type_object_type, ) from mypy.typeops import ( map_type_from_supertype, bind_self, erase_to_bound, make_simplified_union, @@ -3205,9 +3206,12 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type, code=codes.ASSIGNMENT) return rvalue_type, attribute_type, True - get_type = analyze_descriptor_access( - instance_type, attribute_type, self.named_type, - self.msg, context, chk=self) + mx = MemberContext( + is_lvalue=False, is_super=False, is_operator=False, + original_type=instance_type, context=context, self_type=None, + msg=self.msg, chk=self, + ) + get_type = analyze_descriptor_access(attribute_type, mx) 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, @@ -3221,9 +3225,15 @@ def check_member_assignment(self, instance_type: Type, attribute_type: Type, if dunder_set is None: self.fail(message_registry.DESCRIPTOR_SET_NOT_CALLABLE.format(attribute_type), context) return AnyType(TypeOfAny.from_error), get_type, False - - function = function_type(dunder_set, self.named_type('builtins.function')) - bound_method = bind_self(function, attribute_type) + if isinstance(dunder_set, Decorator): + bound_method = analyze_var( + '__set__', dunder_set.var, attribute_type, attribute_type.type, mx, + ) + else: + bound_method = bind_self( + function_type(dunder_set, self.named_type('builtins.function')), + attribute_type, + ) typ = map_instance_to_supertype(attribute_type, dunder_set.info) dunder_set_type = expand_type_by_instance(bound_method, typ) @@ -6214,6 +6224,12 @@ def is_untyped_decorator(typ: Optional[Type]) -> bool: elif isinstance(typ, Instance): method = typ.type.get_method('__call__') if method: + if isinstance(method, Decorator): + return ( + is_untyped_decorator(method.func.type) + or is_untyped_decorator(method.var.type) + ) + if isinstance(method.type, Overloaded): return any(is_untyped_decorator(item) for item in method.type.items) else: diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 9db96bd87c7e..3ab85b850689 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -67,7 +67,8 @@ def not_ready_callback(self, name: str, context: Context) -> None: self.chk.handle_cannot_determine_type(name, context) def copy_modified(self, *, messages: Optional[MessageBuilder] = None, - self_type: Optional[Type] = None) -> 'MemberContext': + self_type: Optional[Type] = None, + is_lvalue: Optional[bool] = None) -> 'MemberContext': mx = MemberContext(self.is_lvalue, self.is_super, self.is_operator, self.original_type, self.context, self.msg, self.chk, self.self_type, self.module_symbol_table) @@ -75,6 +76,8 @@ def copy_modified(self, *, messages: Optional[MessageBuilder] = None, mx.msg = messages if self_type is not None: mx.self_type = self_type + if is_lvalue is not None: + mx.is_lvalue = is_lvalue return mx @@ -197,7 +200,7 @@ def analyze_instance_member_access(name: str, # Look up the member. First look up the method dictionary. method = info.get_method(name) - if method: + if method and not isinstance(method, Decorator): if method.is_property: assert isinstance(method, OverloadedFuncDef) first_item = cast(Decorator, method.items[0]) @@ -390,29 +393,46 @@ def analyze_member_var_access(name: str, if not mx.is_lvalue: for method_name in ('__getattribute__', '__getattr__'): method = info.get_method(method_name) + # __getattribute__ is defined on builtins.object and returns Any, so without # the guard this search will always find object.__getattribute__ and conclude # that the attribute exists if method and method.info.fullname != 'builtins.object': - function = function_type(method, mx.named_type('builtins.function')) - bound_method = bind_self(function, mx.self_type) + if isinstance(method, Decorator): + # https://github.com/python/mypy/issues/10409 + bound_method = analyze_var(method_name, method.var, itype, info, mx) + else: + bound_method = bind_self( + function_type(method, mx.named_type('builtins.function')), + mx.self_type, + ) typ = map_instance_to_supertype(itype, method.info) getattr_type = get_proper_type(expand_type_by_instance(bound_method, typ)) if isinstance(getattr_type, CallableType): result = getattr_type.ret_type - - # Call the attribute hook before returning. - fullname = '{}.{}'.format(method.info.fullname, name) - hook = mx.chk.plugin.get_attribute_hook(fullname) - if hook: - result = hook(AttributeContext(get_proper_type(mx.original_type), - result, mx.context, mx.chk)) - return result + else: + result = getattr_type + + # Call the attribute hook before returning. + fullname = '{}.{}'.format(method.info.fullname, name) + hook = mx.chk.plugin.get_attribute_hook(fullname) + if hook: + result = hook(AttributeContext(get_proper_type(mx.original_type), + result, mx.context, mx.chk)) + return result else: setattr_meth = info.get_method('__setattr__') if setattr_meth and setattr_meth.info.fullname != 'builtins.object': - setattr_func = function_type(setattr_meth, mx.named_type('builtins.function')) - bound_type = bind_self(setattr_func, mx.self_type) + if isinstance(setattr_meth, Decorator): + bound_type = analyze_var( + name, setattr_meth.var, itype, info, + mx.copy_modified(is_lvalue=False), + ) + else: + bound_type = bind_self( + function_type(setattr_meth, mx.named_type('builtins.function')), + mx.self_type, + ) typ = map_instance_to_supertype(itype, setattr_meth.info) setattr_type = get_proper_type(expand_type_by_instance(bound_type, typ)) if isinstance(setattr_type, CallableType) and len(setattr_type.arg_types) > 0: @@ -441,32 +461,24 @@ def check_final_member(name: str, info: TypeInfo, msg: MessageBuilder, ctx: Cont msg.cant_assign_to_final(name, attr_assign=True, ctx=ctx) -def analyze_descriptor_access(instance_type: Type, - descriptor_type: Type, - named_type: Callable[[str], Instance], - msg: MessageBuilder, - context: Context, *, - chk: 'mypy.checker.TypeChecker') -> Type: +def analyze_descriptor_access(descriptor_type: Type, + mx: MemberContext) -> 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. + mx: The current member access context. Return: The return type of the appropriate ``__get__`` overload for the descriptor. """ - instance_type = get_proper_type(instance_type) + instance_type = get_proper_type(mx.original_type) descriptor_type = get_proper_type(descriptor_type) if isinstance(descriptor_type, UnionType): # Map the access over union types return make_simplified_union([ - analyze_descriptor_access(instance_type, typ, named_type, - msg, context, chk=chk) + analyze_descriptor_access(typ, mx) for typ in descriptor_type.items ]) elif not isinstance(descriptor_type, Instance): @@ -476,13 +488,21 @@ def analyze_descriptor_access(instance_type: Type, return descriptor_type dunder_get = descriptor_type.type.get_method('__get__') - if dunder_get is None: - msg.fail(message_registry.DESCRIPTOR_GET_NOT_CALLABLE.format(descriptor_type), context) + mx.msg.fail(message_registry.DESCRIPTOR_GET_NOT_CALLABLE.format(descriptor_type), + mx.context) return AnyType(TypeOfAny.from_error) - function = function_type(dunder_get, named_type('builtins.function')) - bound_method = bind_self(function, descriptor_type) + if isinstance(dunder_get, Decorator): + bound_method = analyze_var( + '__set__', dunder_get.var, descriptor_type, descriptor_type.type, mx, + ) + else: + bound_method = bind_self( + function_type(dunder_get, mx.named_type('builtins.function')), + descriptor_type, + ) + typ = map_instance_to_supertype(descriptor_type, dunder_get.info) dunder_get_type = expand_type_by_instance(bound_method, typ) @@ -495,19 +515,19 @@ def analyze_descriptor_access(instance_type: Type, else: owner_type = instance_type - callable_name = chk.expr_checker.method_fullname(descriptor_type, "__get__") - dunder_get_type = chk.expr_checker.transform_callee_type( + callable_name = mx.chk.expr_checker.method_fullname(descriptor_type, "__get__") + dunder_get_type = mx.chk.expr_checker.transform_callee_type( callable_name, dunder_get_type, - [TempNode(instance_type, context=context), - TempNode(TypeType.make_normalized(owner_type), context=context)], - [ARG_POS, ARG_POS], context, object_type=descriptor_type, + [TempNode(instance_type, context=mx.context), + TempNode(TypeType.make_normalized(owner_type), context=mx.context)], + [ARG_POS, ARG_POS], mx.context, object_type=descriptor_type, ) - _, inferred_dunder_get_type = chk.expr_checker.check_call( + _, inferred_dunder_get_type = mx.chk.expr_checker.check_call( dunder_get_type, - [TempNode(instance_type, context=context), - TempNode(TypeType.make_normalized(owner_type), context=context)], - [ARG_POS, ARG_POS], context, object_type=descriptor_type, + [TempNode(instance_type, context=mx.context), + TempNode(TypeType.make_normalized(owner_type), context=mx.context)], + [ARG_POS, ARG_POS], mx.context, object_type=descriptor_type, callable_name=callable_name) inferred_dunder_get_type = get_proper_type(inferred_dunder_get_type) @@ -516,7 +536,8 @@ def analyze_descriptor_access(instance_type: Type, return inferred_dunder_get_type if not isinstance(inferred_dunder_get_type, CallableType): - msg.fail(message_registry.DESCRIPTOR_GET_NOT_CALLABLE.format(descriptor_type), context) + mx.msg.fail(message_registry.DESCRIPTOR_GET_NOT_CALLABLE.format(descriptor_type), + mx.context) return AnyType(TypeOfAny.from_error) return inferred_dunder_get_type.ret_type @@ -605,8 +626,7 @@ def analyze_var(name: str, fullname = '{}.{}'.format(var.info.fullname, name) hook = mx.chk.plugin.get_attribute_hook(fullname) if result and not mx.is_lvalue and not implicit: - result = analyze_descriptor_access(mx.original_type, result, mx.named_type, - mx.msg, mx.context, chk=mx.chk) + result = analyze_descriptor_access(result, mx) if hook: result = hook(AttributeContext(get_proper_type(mx.original_type), result, mx.context, mx.chk)) @@ -785,8 +805,7 @@ def analyze_class_attribute_access(itype: Instance, result = add_class_tvars(t, isuper, is_classmethod, mx.self_type, original_vars=original_vars) if not mx.is_lvalue: - result = analyze_descriptor_access(mx.original_type, result, mx.named_type, - mx.msg, mx.context, chk=mx.chk) + result = analyze_descriptor_access(result, mx) return result elif isinstance(node.node, Var): mx.not_ready_callback(name, mx.context) diff --git a/mypy/nodes.py b/mypy/nodes.py index ac2dbf336634..98c3582bb5c2 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2687,12 +2687,14 @@ def __bool__(self) -> bool: def has_readable_member(self, name: str) -> bool: return self.get(name) is not None - def get_method(self, name: str) -> Optional[FuncBase]: + def get_method(self, name: str) -> Union[FuncBase, Decorator, None]: for cls in self.mro: if name in cls.names: node = cls.names[name].node if isinstance(node, FuncBase): return node + elif isinstance(node, Decorator): # Two `if`s make `mypyc` happy + return node else: return None return None diff --git a/mypy/subtypes.py b/mypy/subtypes.py index ef117925a817..26054a057540 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -650,6 +650,8 @@ def find_member(name: str, info = itype.type method = info.get_method(name) if method: + if isinstance(method, Decorator): + return find_node_type(method.var, itype, subtype) if method.is_property: assert isinstance(method, OverloadedFuncDef) dec = method.items[0] @@ -659,12 +661,7 @@ def find_member(name: str, else: # don't have such method, maybe variable or decorator? node = info.get(name) - if not node: - v = None - else: - v = node.node - if isinstance(v, Decorator): - v = v.var + v = node.node if node else None if isinstance(v, Var): return find_node_type(v, itype, subtype) if (not v and name not in ['__getattr__', '__setattr__', '__getattribute__'] and @@ -676,9 +673,13 @@ def find_member(name: str, # structural subtyping. method = info.get_method(method_name) if method and method.info.fullname != 'builtins.object': - getattr_type = get_proper_type(find_node_type(method, itype, subtype)) + if isinstance(method, Decorator): + getattr_type = get_proper_type(find_node_type(method.var, itype, subtype)) + else: + getattr_type = get_proper_type(find_node_type(method, itype, subtype)) if isinstance(getattr_type, CallableType): return getattr_type.ret_type + return getattr_type if itype.type.fallback_to_any: return AnyType(TypeOfAny.special_form) return None @@ -698,8 +699,10 @@ def get_member_flags(name: str, info: TypeInfo) -> Set[int]: method = info.get_method(name) setattr_meth = info.get_method('__setattr__') if method: - # this could be settable property - if method.is_property: + if isinstance(method, Decorator): + if method.var.is_staticmethod or method.var.is_classmethod: + return {IS_CLASS_OR_STATIC} + elif method.is_property: # this could be settable property assert isinstance(method, OverloadedFuncDef) dec = method.items[0] assert isinstance(dec, Decorator) @@ -712,9 +715,6 @@ def get_member_flags(name: str, info: TypeInfo) -> Set[int]: return {IS_SETTABLE} return set() v = node.node - if isinstance(v, Decorator): - if v.var.is_staticmethod or v.var.is_classmethod: - return {IS_CLASS_OR_STATIC} # just a variable if isinstance(v, Var) and not v.is_property: flags = {IS_SETTABLE} diff --git a/test-data/unit/check-callable.test b/test-data/unit/check-callable.test index d2c7a49066bc..f19bf166a874 100644 --- a/test-data/unit/check-callable.test +++ b/test-data/unit/check-callable.test @@ -223,6 +223,56 @@ else: [builtins fixtures/callable.pyi] +[case testDecoratedCallMethods] +from typing import Any, Callable, Union, TypeVar + +F = TypeVar('F', bound=Callable) + +def decorator(f: F) -> F: + pass +def change(f: Callable) -> Callable[[Any], str]: + pass +def untyped(f): + pass + +class Some1: + @decorator + def __call__(self) -> int: + pass +class Some2: + @change + def __call__(self) -> int: + pass +class Some3: + @untyped + def __call__(self) -> int: + pass +class Some4: + __call__: Any + +s1: Some1 +s2: Some2 +s3: Some3 +s4: Some4 + +if callable(s1): + 1 + 'a' # E: Unsupported operand types for + ("int" and "str") +else: + 2 + 'b' +if callable(s2): + 1 + 'a' # E: Unsupported operand types for + ("int" and "str") +else: + 2 + 'b' +if callable(s3): + 1 + 'a' # E: Unsupported operand types for + ("int" and "str") +else: + 2 + 'b' # E: Unsupported operand types for + ("int" and "str") +if callable(s4): + 1 + 'a' # E: Unsupported operand types for + ("int" and "str") +else: + 2 + 'b' # E: Unsupported operand types for + ("int" and "str") +[builtins fixtures/callable.pyi] + [case testCallableNestedUnions] from typing import Callable, Union diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index b15f9f77f3c7..8f411735149a 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2654,6 +2654,35 @@ b = a.bar [out] main:9: error: Incompatible types in assignment (expression has type "A", variable has type "B") +[case testDecoratedGetAttribute] +from typing import Callable, TypeVar + +T = TypeVar('T', bound=Callable) + +def decorator(f: T) -> T: + return f + +def bad(f: Callable) -> Callable[..., int]: + return f + +class A: + @decorator + def __getattribute__(self, x: str) -> A: + return A() +class B: + @bad # We test that type will be taken from decorated type, not node itself + def __getattribute__(self, x: str) -> A: + return A() + +a: A +b: B + +a1: A = a.foo +b1: B = a.bar # E: Incompatible types in assignment (expression has type "A", variable has type "B") +a2: A = b.baz # E: Incompatible types in assignment (expression has type "int", variable has type "A") +b2: B = b.roo # E: Incompatible types in assignment (expression has type "int", variable has type "B") +[builtins fixtures/tuple.pyi] + [case testGetattributeSignature] class A: def __getattribute__(self, x: str) -> A: pass @@ -2681,6 +2710,35 @@ b = a.bar [out] main:9: error: Incompatible types in assignment (expression has type "A", variable has type "B") +[case testDecoratedGetattr] +from typing import Callable, TypeVar + +T = TypeVar('T', bound=Callable) + +def decorator(f: T) -> T: + return f + +def bad(f: Callable) -> Callable[..., int]: + return f + +class A: + @decorator + def __getattr__(self, x: str) -> A: + return A() +class B: + @bad # We test that type will be taken from decorated type, not node itself + def __getattr__(self, x: str) -> A: + return A() + +a: A +b: B + +a1: A = a.foo +b1: B = a.bar # E: Incompatible types in assignment (expression has type "A", variable has type "B") +a2: A = b.baz # E: Incompatible types in assignment (expression has type "int", variable has type "A") +b2: B = b.roo # E: Incompatible types in assignment (expression has type "int", variable has type "B") +[builtins fixtures/tuple.pyi] + [case testGetattrWithGetitem] class A: def __getattr__(self, x: str) -> 'A': @@ -2787,6 +2845,35 @@ s = Sub() s.success = 4 s.fail = 'fail' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +[case testDecoratedSetattr] +from typing import Any, Callable, TypeVar + +T = TypeVar('T', bound=Callable) + +def decorator(f: T) -> T: + return f + +def bad(f: Callable) -> Callable[[Any, str, int], None]: + return f + +class A: + @decorator + def __setattr__(self, k: str, v: str) -> None: + pass +class B: + @bad # We test that type will be taken from decorated type, not node itself + def __setattr__(self, k: str, v: str) -> None: + pass + +a: A +a.foo = 'a' +a.bar = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") + +b: B +b.good = 1 +b.bad = 'a' # E: Incompatible types in assignment (expression has type "str", variable has type "int") +[builtins fixtures/tuple.pyi] + [case testSetattrSignature] from typing import Any @@ -6116,6 +6203,85 @@ class C: reveal_type(self.spec) # N: Revealed type is "__main__.B" [builtins fixtures/bool.pyi] +[case testDecoratedDunderGet] +from typing import Any, Callable, TypeVar, Type + +F = TypeVar('F', bound=Callable) +T = TypeVar('T') + +def decorator(f: F) -> F: + return f + +def change(f: Callable) -> Callable[..., int]: + pass + +def untyped(f): + return f + +class A: ... + +class Descr1: + @decorator + def __get__(self, obj: T, typ: Type[T]) -> A: ... +class Descr2: + @change + def __get__(self, obj: T, typ: Type[T]) -> A: ... +class Descr3: + @untyped + def __get__(self, obj: T, typ: Type[T]) -> A: ... + +class C: + spec1 = Descr1() + spec2 = Descr2() + spec3 = Descr3() + +c: C +reveal_type(c.spec1) # N: Revealed type is "__main__.A" +reveal_type(c.spec2) # N: Revealed type is "builtins.int" +reveal_type(c.spec3) # N: Revealed type is "Any" +[builtins fixtures/bool.pyi] + +[case testDecoratedDunderSet] +from typing import Any, Callable, TypeVar, Type + +F = TypeVar('F', bound=Callable) +T = TypeVar('T') + +def decorator(f: F) -> F: + return f + +def change(f: Callable) -> Callable[[Any, Any, int], None]: + pass + +def untyped(f): + return f + +class A: ... + +class Descr1: + @decorator + def __set__(self, obj: T, value: A) -> None: ... +class Descr2: + @change + def __set__(self, obj: T, value: A) -> None: ... +class Descr3: + @untyped + def __set__(self, obj: T, value: A) -> None: ... + +class C: + spec1 = Descr1() + spec2 = Descr2() + spec3 = Descr3() + +c: C +c.spec1 = A() +c.spec1 = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "A") +c.spec2 = A() # E: Incompatible types in assignment (expression has type "A", variable has type "int") +c.spec2 = 1 +c.spec3 = A() +c.spec3 = 1 +[builtins fixtures/bool.pyi] + [case testClassLevelImport] # flags: --ignore-missing-imports class Test: diff --git a/test-data/unit/check-custom-plugin.test b/test-data/unit/check-custom-plugin.test index 8f02a13cfc67..2707d886d64e 100644 --- a/test-data/unit/check-custom-plugin.test +++ b/test-data/unit/check-custom-plugin.test @@ -213,6 +213,74 @@ class DerivedSignal(Signal[T]): ... \[mypy] plugins=/test-data/unit/plugins/attrhook.py +[case testAttributeTypeHookPluginUntypedDecoratedGetattr] +# flags: --config-file tmp/mypy.ini +from m import Magic, DerivedMagic + +magic = Magic() +reveal_type(magic.magic_field) # N: Revealed type is "builtins.str" +reveal_type(magic.non_magic_method()) # N: Revealed type is "builtins.int" +reveal_type(magic.non_magic_field) # N: Revealed type is "builtins.int" +magic.nonexistent_field # E: Field does not exist +reveal_type(magic.fallback_example) # N: Revealed type is "Any" + +derived = DerivedMagic() +reveal_type(derived.magic_field) # N: Revealed type is "builtins.str" +derived.nonexistent_field # E: Field does not exist +reveal_type(derived.fallback_example) # N: Revealed type is "Any" + +[file m.py] +from typing import Any, Callable + +def decorator(f): + pass + +class Magic: + # Triggers plugin infrastructure: + @decorator + def __getattr__(self, x: Any) -> Any: ... + def non_magic_method(self) -> int: ... + non_magic_field: int + +class DerivedMagic(Magic): ... +[file mypy.ini] +\[mypy] +plugins=/test-data/unit/plugins/attrhook2.py + +[case testAttributeTypeHookPluginDecoratedGetattr] +# flags: --config-file tmp/mypy.ini +from m import Magic, DerivedMagic + +magic = Magic() +reveal_type(magic.magic_field) # N: Revealed type is "builtins.str" +reveal_type(magic.non_magic_method()) # N: Revealed type is "builtins.int" +reveal_type(magic.non_magic_field) # N: Revealed type is "builtins.int" +magic.nonexistent_field # E: Field does not exist +reveal_type(magic.fallback_example) # N: Revealed type is "builtins.bool" + +derived = DerivedMagic() +reveal_type(derived.magic_field) # N: Revealed type is "builtins.str" +derived.nonexistent_field # E: Field does not exist +reveal_type(derived.fallback_example) # N: Revealed type is "builtins.bool" + +[file m.py] +from typing import Any, Callable + +def decorator(f: Callable) -> Callable[[Any, str], bool]: + pass + +class Magic: + # Triggers plugin infrastructure: + @decorator + def __getattr__(self, x: Any) -> Any: ... + def non_magic_method(self) -> int: ... + non_magic_field: int + +class DerivedMagic(Magic): ... +[file mypy.ini] +\[mypy] +plugins=/test-data/unit/plugins/attrhook2.py + [case testAttributeHookPluginForDynamicClass] # flags: --config-file tmp/mypy.ini from m import Magic, DerivedMagic @@ -223,7 +291,12 @@ reveal_type(magic.non_magic_method()) # N: Revealed type is "builtins.int" reveal_type(magic.non_magic_field) # N: Revealed type is "builtins.int" magic.nonexistent_field # E: Field does not exist reveal_type(magic.fallback_example) # N: Revealed type is "Any" -reveal_type(DerivedMagic().magic_field) # N: Revealed type is "builtins.str" + +derived = DerivedMagic() +reveal_type(derived.magic_field) # N: Revealed type is "builtins.str" +derived.nonexistent_field # E: Field does not exist +reveal_type(magic.fallback_example) # N: Revealed type is "Any" + [file m.py] from typing import Any class Magic: diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 21ed20f7bf8b..02f6b034c512 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -178,6 +178,67 @@ reveal_type(g) # N: Revealed type is "Any" reveal_type(h) # N: Revealed type is "def (*Any, **Any) -> Any" reveal_type(i) # N: Revealed type is "Any" +[case testDisallowUntypedDecoratorsCallableInstanceDecoratedCall] +# flags: --disallow-untyped-decorators +from typing import Callable, TypeVar + +C = TypeVar('C', bound=Callable) + +def typed_decorator(c: C) -> C: + return c + +def untyped_decorator(c): + return c + +class TypedDecorator: + @typed_decorator + def __call__(self, c: Callable) -> Callable: + return function + +class UntypedDecorator1: + @untyped_decorator + def __call__(self, c): + return function + +class UntypedDecorator2: + @untyped_decorator # E: Untyped decorator makes function "__call__" untyped + def __call__(self, c: Callable) -> Callable: + return function + +class UntypedDecorator3: + @typed_decorator + @untyped_decorator # E: Untyped decorator makes function "__call__" untyped + def __call__(self, c: Callable) -> Callable: + return function + +class UntypedDecorator4: + @untyped_decorator # E: Untyped decorator makes function "__call__" untyped + @typed_decorator + def __call__(self, c: Callable) -> Callable: + return function + +@TypedDecorator() +def f() -> None: pass + +@UntypedDecorator1() # E: Untyped decorator makes function "g1" untyped +def g1() -> None: pass + +@UntypedDecorator2() # E: Untyped decorator makes function "g2" untyped +def g2() -> None: pass + +@UntypedDecorator3() # E: Untyped decorator makes function "g3" untyped +def g3() -> None: pass + +@UntypedDecorator4() # E: Untyped decorator makes function "g4" untyped +def g4() -> None: pass + +reveal_type(f) # N: Revealed type is "def (*Any, **Any) -> Any" +reveal_type(g1) # N: Revealed type is "Any" +reveal_type(g2) # N: Revealed type is "Any" +reveal_type(g3) # N: Revealed type is "Any" +reveal_type(g4) # N: Revealed type is "Any" +[builtins fixtures/bool.pyi] + [case testDisallowUntypedDecoratorsNonCallableInstance] # flags: --disallow-untyped-decorators class Decorator: diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index b9c3376ad83a..6768263e9832 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -39,6 +39,57 @@ def fun2() -> P: def fun3() -> P: return B() # E: Incompatible return value type (got "B", expected "P") +[case testProtocolAttrAccessDecoratedGetAttrDunder] +from typing import Any, Protocol, Callable + +def typed_decorator(fun: Callable) -> Callable[[Any, str], str]: + pass + +def untyped_decorator(fun): + pass + +class P(Protocol): + @property + def x(self) -> int: + pass + +class A: + @untyped_decorator + def __getattr__(self, key: str) -> int: + pass + +class B: + @typed_decorator + def __getattr__(self, key: str) -> int: + pass + +class C: + def __getattr__(self, key: str) -> int: + pass + +def fun(x: P) -> None: + pass + +a: A +reveal_type(a.x) +fun(a) + +b: B +reveal_type(b.x) +fun(b) + +c: C +reveal_type(c.x) +fun(c) +[out] +main:32: note: Revealed type is "Any" +main:36: note: Revealed type is "builtins.str" +main:37: error: Argument 1 to "fun" has incompatible type "B"; expected "P" +main:37: note: Following member(s) of "B" have conflicts: +main:37: note: x: expected "int", got "str" +main:40: note: Revealed type is "builtins.int" +[builtins fixtures/bool.pyi] + [case testSimpleProtocolOneAbstractMethod] from typing import Protocol from abc import abstractmethod diff --git a/test-data/unit/fixtures/bool.pyi b/test-data/unit/fixtures/bool.pyi index ca2564dabafd..245526d78907 100644 --- a/test-data/unit/fixtures/bool.pyi +++ b/test-data/unit/fixtures/bool.pyi @@ -17,3 +17,4 @@ class str: pass class unicode: pass class ellipsis: pass class list: pass +class property: pass