From 673468cfc3d9f85f026983b264ee5e71d8dae47e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 5 Nov 2019 13:52:35 +0000 Subject: [PATCH] Allow fancy self-types (#7860) Fixes https://github.com/python/mypy/issues/3625 Fixes https://github.com/python/mypy/issues/5305 Fixes https://github.com/python/mypy/issues/5320 Fixes https://github.com/python/mypy/issues/5868 Fixes https://github.com/python/mypy/issues/7191 Fixes https://github.com/python/mypy/issues/7778 Fixes https://github.com/python/typing/issues/680 So, lately I was noticing many issues that would be fixed by (partially) moving the check for self-type from definition site to call site. This morning I found that we actually have such function `check_self_arg()` that is applied at call site, but it is almost not used. After more reading of the code I found that all the patterns for self-types that I wanted to support should either already work, or work with minimal modifications. Finally, I discovered that the root cause of many of the problems is the fact that `bind_self()` uses wrong direction for type inference! All these years it expected actual argument type to be _supertype_ of the formal one. After fixing this bug, it turned out it was easy to support following patterns for explicit self-types: * Structured match on generic self-types * Restricted methods in generic classes (methods that one is allowed to call only for some values or type arguments) * Methods overloaded on self-type * (Important case of the above) overloaded `__init__` for generic classes * Mixin classes (using protocols) * Private class-level decorators (a bit hacky) * Precise types for alternative constructors (mostly already worked) This PR cuts few corners, but it is ready for review (I left some TODOs). Note I also add some docs, I am not sure this is really needed, but probably good to have. --- docs/source/generics.rst | 2 + docs/source/more_types.rst | 138 ++++++++++++ mypy/checker.py | 19 +- mypy/checkmember.py | 55 +++-- mypy/infer.py | 11 +- mypy/meet.py | 4 + mypy/typeops.py | 83 ++++--- test-data/unit/check-classes.test | 12 +- test-data/unit/check-generics.test | 17 ++ test-data/unit/check-newsemanal.test | 2 +- test-data/unit/check-selftype.test | 324 ++++++++++++++++++++++++++- 11 files changed, 606 insertions(+), 61 deletions(-) diff --git a/docs/source/generics.rst b/docs/source/generics.rst index e23433af59a1..937b6ae51ecc 100644 --- a/docs/source/generics.rst +++ b/docs/source/generics.rst @@ -312,6 +312,8 @@ or a deserialization method returns the actual type of self. Therefore you may need to silence mypy inside these methods (but not at the call site), possibly by making use of the ``Any`` type. +For some advanced uses of self-types see :ref:`additional examples `. + .. _variance-of-generics: Variance of generic types diff --git a/docs/source/more_types.rst b/docs/source/more_types.rst index 8494de1aefee..b5744fda3228 100644 --- a/docs/source/more_types.rst +++ b/docs/source/more_types.rst @@ -561,6 +561,144 @@ with ``Union[int, slice]`` and ``Union[T, Sequence]``. to returning ``Any`` only if the input arguments also contain ``Any``. +.. _advanced_self: + +Advanced uses of self-types +*************************** + +Normally, mypy doesn't require annotations for the first arguments of instance and +class methods. However, they may be needed to have more precise static typing +for certain programming patterns. + +Restricted methods in generic classes +------------------------------------- + +In generic classes some methods may be allowed to be called only +for certain values of type arguments: + +.. code-block:: python + + T = TypeVar('T') + + class Tag(Generic[T]): + item: T + def uppercase_item(self: C[str]) -> str: + return self.item.upper() + + def label(ti: Tag[int], ts: Tag[str]) -> None: + ti.uppercase_item() # E: Invalid self argument "Tag[int]" to attribute function + # "uppercase_item" with type "Callable[[Tag[str]], str]" + ts.uppercase_item() # This is OK + +This pattern also allows matching on nested types in situations where the type +argument is itself generic: + +.. code-block:: python + + T = TypeVar('T') + S = TypeVar('S') + + class Storage(Generic[T]): + def __init__(self, content: T) -> None: + self.content = content + def first_chunk(self: Storage[Sequence[S]]) -> S: + return self.content[0] + + page: Storage[List[str]] + page.first_chunk() # OK, type is "str" + + Storage(0).first_chunk() # Error: Invalid self argument "Storage[int]" to attribute function + # "first_chunk" with type "Callable[[Storage[Sequence[S]]], S]" + +Finally, one can use overloads on self-type to express precise types of +some tricky methods: + +.. code-block:: python + + T = TypeVar('T') + + class Tag(Generic[T]): + @overload + def export(self: Tag[str]) -> str: ... + @overload + def export(self, converter: Callable[[T], str]) -> T: ... + + def export(self, converter=None): + if isinstance(self.item, str): + return self.item + return converter(self.item) + +In particular, an :py:meth:`~object.__init__` method overloaded on self-type +may be useful to annotate generic class constructors where type arguments +depend on constructor parameters in a non-trivial way, see e.g. :py:class:`~subprocess.Popen`. + +Mixin classes +------------- + +Using host class protocol as a self-type in mixin methods allows +more code re-usability for static typing of mixin classes. For example, +one can define a protocol that defines common functionality for +host classes instead of adding required abstract methods to every mixin: + +.. code-block:: python + + class Lockable(Protocol): + @property + def lock(self) -> Lock: ... + + class AtomicCloseMixin: + def atomic_close(self: Lockable) -> int: + with self.lock: + # perform actions + + class AtomicOpenMixin: + def atomic_open(self: Lockable) -> int: + with self.lock: + # perform actions + + class File(AtomicCloseMixin, AtomicOpenMixin): + def __init__(self) -> None: + self.lock = Lock() + + class Bad(AtomicCloseMixin): + pass + + f = File() + b: Bad + f.atomic_close() # OK + b.atomic_close() # Error: Invalid self type for "atomic_close" + +Note that the explicit self-type is *required* to be a protocol whenever it +is not a supertype of the current class. In this case mypy will check the validity +of the self-type only at the call site. + +Precise typing of alternative constructors +------------------------------------------ + +Some classes may define alternative constructors. If these +classes are generic, self-type allows giving them precise signatures: + +.. code-block:: python + + T = TypeVar('T') + + class Base(Generic[T]): + Q = TypeVar('Q', bound='Base[T]') + + def __init__(self, item: T) -> None: + self.item = item + + @classmethod + def make_pair(cls: Type[Q], item: T) -> Tuple[Q, Q]: + return cls(item), cls(item) + + class Sub(Base[T]): + ... + + pair = Sub.make_pair('yes') # Type is "Tuple[Sub[str], Sub[str]]" + bad = Sub[int].make_pair('no') # Error: Argument 1 to "make_pair" of "Base" + # has incompatible type "str"; expected "int" + .. _async-and-await: Typing async/await diff --git a/mypy/checker.py b/mypy/checker.py index 7cc1b04b5d91..c0acc970902d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -906,10 +906,18 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str]) isclass = defn.is_class or defn.name() in ('__new__', '__init_subclass__') if isclass: ref_type = mypy.types.TypeType.make_normalized(ref_type) - erased = erase_to_bound(arg_type) + erased = get_proper_type(erase_to_bound(arg_type)) if not is_subtype_ignoring_tvars(ref_type, erased): note = None - if typ.arg_names[i] in ['self', 'cls']: + if (isinstance(erased, Instance) and erased.type.is_protocol or + isinstance(erased, TypeType) and + isinstance(erased.item, Instance) and + erased.item.type.is_protocol): + # We allow the explicit self-type to be not a supertype of + # the current class if it is a protocol. For such cases + # the consistency check will be performed at call sites. + msg = None + elif typ.arg_names[i] in {'self', 'cls'}: if (self.options.python_version[0] < 3 and is_same_type(erased, arg_type) and not isclass): msg = message_registry.INVALID_SELF_TYPE_OR_EXTRA_ARG @@ -919,9 +927,10 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str]) erased, ref_type) else: msg = message_registry.MISSING_OR_INVALID_SELF_TYPE - self.fail(msg, defn) - if note: - self.note(note, defn) + if msg: + self.fail(msg, defn) + if note: + self.note(note, defn) elif isinstance(arg_type, TypeVarType): # Refuse covariant parameter type variables # TODO: check recursively for inner type variables diff --git a/mypy/checkmember.py b/mypy/checkmember.py index d12bf3c5ca30..0927149deeac 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -200,6 +200,12 @@ def analyze_instance_member_access(name: str, # the first argument. pass else: + if isinstance(signature, FunctionLike) and name != '__call__': + # TODO: use proper treatment of special methods on unions instead + # of this hack here and below (i.e. mx.self_type). + dispatched_type = meet.meet_types(mx.original_type, typ) + signature = check_self_arg(signature, dispatched_type, False, mx.context, + name, mx.msg) signature = bind_self(signature, mx.self_type) typ = map_instance_to_supertype(typ, method.info) member_type = expand_type_by_instance(signature, typ) @@ -546,8 +552,8 @@ def analyze_var(name: str, # In `x.f`, when checking `x` against A1 we assume x is compatible with A # and similarly for B1 when checking agains B dispatched_type = meet.meet_types(mx.original_type, itype) - check_self_arg(functype, dispatched_type, var.is_classmethod, mx.context, name, - mx.msg) + functype = check_self_arg(functype, dispatched_type, var.is_classmethod, + mx.context, name, mx.msg) signature = bind_self(functype, mx.self_type, var.is_classmethod) if var.is_property: # A property cannot have an overloaded type => the cast is fine. @@ -596,27 +602,45 @@ def check_self_arg(functype: FunctionLike, dispatched_arg_type: Type, is_classmethod: bool, context: Context, name: str, - msg: MessageBuilder) -> None: - """For x.f where A.f: A1 -> T, check that meet(type(x), A) <: A1 for each overload. + msg: MessageBuilder) -> FunctionLike: + """Check that an instance has a valid type for a method with annotated 'self'. - dispatched_arg_type is meet(B, A) in the following example - - def g(x: B): x.f + For example if the method is defined as: class A: - f: Callable[[A1], None] + def f(self: S) -> T: ... + then for 'x.f' we check that meet(type(x), A) <: S. If the method is overloaded, we + select only overloads items that satisfy this requirement. If there are no matching + overloads, an error is generated. + + Note: dispatched_arg_type uses a meet to select a relevant item in case if the + original type of 'x' is a union. This is done because several special methods + treat union types in ad-hoc manner, so we can't use MemberContext.self_type yet. """ - # TODO: this is too strict. We can return filtered overloads for matching definitions - for item in functype.items(): + items = functype.items() + if not items: + return functype + new_items = [] + for item in items: if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR): # No positional first (self) argument (*args is okay). msg.no_formal_self(name, item, context) + # This is pretty bad, so just return the original signature if + # there is at least one such error. + return functype else: selfarg = item.arg_types[0] if is_classmethod: dispatched_arg_type = TypeType.make_normalized(dispatched_arg_type) - if not subtypes.is_subtype(dispatched_arg_type, erase_to_bound(selfarg)): - msg.incompatible_self_argument(name, dispatched_arg_type, item, - is_classmethod, context) + if subtypes.is_subtype(dispatched_arg_type, erase_typevars(erase_to_bound(selfarg))): + new_items.append(item) + if not new_items: + # Choose first item for the message (it may be not very helpful for overloads). + msg.incompatible_self_argument(name, dispatched_arg_type, items[0], + is_classmethod, context) + return functype + if len(new_items) == 1: + return new_items[0] + return Overloaded(new_items) def analyze_class_attribute_access(itype: Instance, @@ -702,7 +726,10 @@ def analyze_class_attribute_access(itype: Instance, is_classmethod = ((is_decorated and cast(Decorator, node.node).func.is_class) or (isinstance(node.node, FuncBase) and node.node.is_class)) - result = add_class_tvars(get_proper_type(t), itype, isuper, is_classmethod, + t = get_proper_type(t) + if isinstance(t, FunctionLike) and is_classmethod: + t = check_self_arg(t, mx.self_type, False, mx.context, name, mx.msg) + result = add_class_tvars(t, itype, isuper, is_classmethod, mx.builtin_type, mx.self_type) if not mx.is_lvalue: result = analyze_descriptor_access(mx.original_type, result, mx.builtin_type, diff --git a/mypy/infer.py b/mypy/infer.py index b7d0dca8b9b4..c2f7fbd35e72 100644 --- a/mypy/infer.py +++ b/mypy/infer.py @@ -2,10 +2,11 @@ from typing import List, Optional, Sequence -from mypy.constraints import infer_constraints, infer_constraints_for_callable +from mypy.constraints import ( + infer_constraints, infer_constraints_for_callable, SUBTYPE_OF, SUPERTYPE_OF +) from mypy.types import Type, TypeVarId, CallableType from mypy.solve import solve_constraints -from mypy.constraints import SUBTYPE_OF def infer_function_type_arguments(callee_type: CallableType, @@ -36,8 +37,10 @@ def infer_function_type_arguments(callee_type: CallableType, def infer_type_arguments(type_var_ids: List[TypeVarId], - template: Type, actual: Type) -> List[Optional[Type]]: + template: Type, actual: Type, + is_supertype: bool = False) -> List[Optional[Type]]: # Like infer_function_type_arguments, but only match a single type # against a generic type. - constraints = infer_constraints(template, actual, SUBTYPE_OF) + constraints = infer_constraints(template, actual, + SUPERTYPE_OF if is_supertype else SUBTYPE_OF) return solve_constraints(type_var_ids, constraints) diff --git a/mypy/meet.py b/mypy/meet.py index 192f79dde8af..517eb93a5c81 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -493,6 +493,10 @@ def visit_instance(self, t: Instance) -> ProperType: call = unpack_callback_protocol(t) if call: return meet_types(call, self.s) + elif isinstance(self.s, FunctionLike) and self.s.is_type_obj() and t.type.is_metaclass(): + if is_subtype(self.s.fallback, t): + return self.s + return self.default(self.s) elif isinstance(self.s, TypeType): return meet_types(t, self.s) elif isinstance(self.s, TupleType): diff --git a/mypy/typeops.py b/mypy/typeops.py index 39c81617c9ab..8db2158d809c 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -9,13 +9,13 @@ from mypy.types import ( TupleType, Instance, FunctionLike, Type, CallableType, TypeVarDef, Overloaded, - TypeVarType, TypeType, UninhabitedType, FormalArgument, UnionType, NoneType, + TypeVarType, UninhabitedType, FormalArgument, UnionType, NoneType, AnyType, TypeOfAny, TypeType, ProperType, LiteralType, get_proper_type, get_proper_types, copy_type ) from mypy.nodes import ( FuncBase, FuncItem, OverloadedFuncDef, TypeInfo, TypeVar, ARG_STAR, ARG_STAR2, Expression, - StrExpr + StrExpr, ARG_POS ) from mypy.maptype import map_instance_to_supertype from mypy.expandtype import expand_type_by_instance, expand_type @@ -41,37 +41,50 @@ def type_object_type_from_function(signature: FunctionLike, def_info: TypeInfo, fallback: Instance, is_new: bool) -> FunctionLike: - # The __init__ method might come from a generic superclass - # (init_or_new.info) with type variables that do not map - # identically to the type variables of the class being constructed - # (info). For example + # We first need to record all non-trivial (explicit) self types in __init__, + # since they will not be available after we bind them. Note, we use explicit + # self-types only in the defining class, similar to __new__ (but not exactly the same, + # see comment in class_callable below). This is mostly useful for annotating library + # classes such as subprocess.Popen. + default_self = fill_typevars(info) + if not is_new and def_info == info and not info.is_newtype: + orig_self_types = [(it.arg_types[0] if it.arg_types and it.arg_types[0] != default_self + and it.arg_kinds[0] == ARG_POS else None) for it in signature.items()] + else: + orig_self_types = [None] * len(signature.items()) + + # The __init__ method might come from a generic superclass 'def_info' + # with type variables that do not map identically to the type variables of + # the class 'info' being constructed. For example: # - # class A(Generic[T]): def __init__(self, x: T) -> None: pass - # class B(A[List[T]], Generic[T]): pass + # class A(Generic[T]): + # def __init__(self, x: T) -> None: ... + # class B(A[List[T]]): + # ... # - # We need to first map B's __init__ to the type (List[T]) -> None. - signature = bind_self(signature, original_type=fill_typevars(info), is_classmethod=is_new) - signature = cast(FunctionLike, - map_type_from_supertype(signature, info, def_info)) + # We need to map B's __init__ to the type (List[T]) -> None. + signature = bind_self(signature, original_type=default_self, is_classmethod=is_new) + signature = cast(FunctionLike, map_type_from_supertype(signature, info, def_info)) + special_sig = None # type: Optional[str] if def_info.fullname() == 'builtins.dict': # Special signature! special_sig = 'dict' if isinstance(signature, CallableType): - return class_callable(signature, info, fallback, special_sig, is_new) + return class_callable(signature, info, fallback, special_sig, is_new, orig_self_types[0]) else: # Overloaded __init__/__new__. assert isinstance(signature, Overloaded) items = [] # type: List[CallableType] - for item in signature.items(): - items.append(class_callable(item, info, fallback, special_sig, is_new)) + for item, orig_self in zip(signature.items(), orig_self_types): + items.append(class_callable(item, info, fallback, special_sig, is_new, orig_self)) return Overloaded(items) def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance, special_sig: Optional[str], - is_new: bool) -> CallableType: + is_new: bool, orig_self_type: Optional[Type] = None) -> CallableType: """Create a type object type based on the signature of __init__.""" variables = [] # type: List[TypeVarDef] variables.extend(info.defn.type_vars) @@ -80,6 +93,7 @@ def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance, from mypy.subtypes import is_subtype init_ret_type = get_proper_type(init_type.ret_type) + orig_self_type = get_proper_type(orig_self_type) default_ret_type = fill_typevars(info) if ( is_new @@ -89,6 +103,8 @@ def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance, and is_subtype(init_ret_type, default_ret_type, ignore_type_params=True) ): ret_type = init_ret_type # type: Type + elif isinstance(orig_self_type, (Instance, TupleType)): + ret_type = orig_self_type else: ret_type = default_ret_type @@ -108,8 +124,8 @@ def map_type_from_supertype(typ: Type, For example, assume - . class D(Generic[S]) ... - . class C(D[E[T]], Generic[T]) ... + class D(Generic[S]): ... + class C(D[E[T]], Generic[T]): ... Now S in the context of D would be mapped to E[T] in the context of C. """ @@ -130,6 +146,18 @@ def map_type_from_supertype(typ: Type, return expand_type_by_instance(typ, inst_type) +def supported_self_type(typ: ProperType) -> bool: + """Is this a supported kind of explicit self-types? + + Currently, this means a X or Type[X], where X is an instance or + a type variable with an instance upper bound. + """ + if isinstance(typ, TypeType): + return supported_self_type(typ.item) + return (isinstance(typ, TypeVarType) or + (isinstance(typ, Instance) and typ != fill_typevars(typ.type))) + + F = TypeVar('F', bound=FunctionLike) @@ -164,7 +192,7 @@ class B(A): pass assert isinstance(method, CallableType) func = method if not func.arg_types: - # invalid method. return something + # Invalid method, return something. return cast(F, func) if func.arg_kinds[0] == ARG_STAR: # The signature is of the form 'def foo(*args, ...)'. @@ -174,23 +202,21 @@ class B(A): pass # TODO: infer bounds on the type of *args? return cast(F, func) self_param_type = get_proper_type(func.arg_types[0]) - if func.variables and (isinstance(self_param_type, TypeVarType) or - (isinstance(self_param_type, TypeType) and - isinstance(self_param_type.item, TypeVarType))): + if func.variables and supported_self_type(self_param_type): if original_type is None: - # Type check method override - # XXX value restriction as union? + # TODO: type check method override (see #7861). original_type = erase_to_bound(self_param_type) original_type = get_proper_type(original_type) ids = [x.id for x in func.variables] - typearg = get_proper_type(infer_type_arguments(ids, self_param_type, original_type)[0]) + typearg = get_proper_type(infer_type_arguments(ids, self_param_type, + original_type, is_supertype=True)[0]) if (is_classmethod and isinstance(typearg, UninhabitedType) and isinstance(original_type, (Instance, TypeVarType, TupleType))): # In case we call a classmethod through an instance x, fallback to type(x) - # TODO: handle Union typearg = get_proper_type(infer_type_arguments(ids, self_param_type, - TypeType(original_type))[0]) + TypeType(original_type), + is_supertype=True)[0]) def expand(target: Type) -> Type: assert typearg is not None @@ -217,6 +243,7 @@ def expand(target: Type) -> Type: def erase_to_bound(t: Type) -> Type: + # TODO: use value restrictions to produce a union? t = get_proper_type(t) if isinstance(t, TypeVarType): return t.upper_bound @@ -270,8 +297,6 @@ def make_simplified_union(items: Sequence[Type], Note: This must NOT be used during semantic analysis, since TypeInfos may not be fully initialized. """ - # TODO: Make this a function living somewhere outside mypy.types. Most other non-trivial - # type operations are not static methods, so this is inconsistent. items = get_proper_types(items) while any(isinstance(typ, UnionType) for typ in items): all_items = [] # type: List[ProperType] diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index a208e9ef09e1..0764ee3e4903 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -4271,9 +4271,9 @@ reveal_type(A.g4) # N: Revealed type is 'def () -> def () -> __main__.A' class B(metaclass=M): def foo(self): pass -B.g1 # Should be error: Argument 0 to "g1" of "M" has incompatible type "B"; expected "Type[A]" -B.g2 # Should be error: Argument 0 to "g2" of "M" has incompatible type "B"; expected "Type[TA]" -B.g3 # Should be error: Argument 0 to "g3" of "M" has incompatible type "B"; expected "TTA" +B.g1 # E: Invalid self argument "Type[B]" to attribute function "g1" with type "Callable[[Type[A]], A]" +B.g2 # E: Invalid self argument "Type[B]" to attribute function "g2" with type "Callable[[Type[TA]], TA]" +B.g3 # E: Invalid self argument "Type[B]" to attribute function "g3" with type "Callable[[TTA], TTA]" reveal_type(B.g4) # N: Revealed type is 'def () -> def () -> __main__.B' # 4 examples of unsoundness - instantiation, classmethod, staticmethod and ClassVar: @@ -4286,9 +4286,9 @@ reveal_type(ta.g3) # N: Revealed type is 'def () -> Type[__main__.A]' reveal_type(ta.g4) # N: Revealed type is 'def () -> Type[__main__.A]' x: M = ta -x.g1 # should be error: Argument 0 to "g1" of "M" has incompatible type "M"; expected "Type[A]" -x.g2 # should be error: Argument 0 to "g2" of "M" has incompatible type "M"; expected "Type[TA]" -x.g3 # should be error: Argument 0 to "g3" of "M" has incompatible type "M"; expected "TTA" +x.g1 # E: Invalid self argument "M" to attribute function "g1" with type "Callable[[Type[A]], A]" +x.g2 # E: Invalid self argument "M" to attribute function "g2" with type "Callable[[Type[TA]], TA]" +x.g3 # E: Invalid self argument "M" to attribute function "g3" with type "Callable[[TTA], TTA]" reveal_type(x.g4) # N: Revealed type is 'def () -> __main__.M*' def r(ta: Type[TA], tta: TTA) -> None: diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 0441f26cee36..09a3616fb3a2 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -1948,6 +1948,23 @@ class B(A[T], Generic[T, S]): reveal_type(B.foo) # N: Revealed type is 'def [T, S] () -> Tuple[T`1, __main__.B*[T`1, S`2]]' [builtins fixtures/classmethod.pyi] +[case testGenericClassAlternativeConstructorPrecise] +from typing import Generic, TypeVar, Type, Tuple, Any + +T = TypeVar('T') +Q = TypeVar('Q') + +class Base(Generic[T]): + def __init__(self, item: T) -> None: ... + @classmethod + def make_pair(cls: Type[Q], item: T) -> Tuple[Q, Q]: ... +class Sub(Base[T]): + ... + +reveal_type(Sub.make_pair('yes')) # N: Revealed type is 'Tuple[__main__.Sub[builtins.str*], __main__.Sub[builtins.str*]]' +Sub[int].make_pair('no') # E: Argument 1 to "make_pair" of "Base" has incompatible type "str"; expected "int" +[builtins fixtures/classmethod.pyi] + [case testGenericClassAttrUnboundOnClass] from typing import Generic, TypeVar T = TypeVar('T') diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test index 8e2dcc72e8fd..664288c9ff20 100644 --- a/test-data/unit/check-newsemanal.test +++ b/test-data/unit/check-newsemanal.test @@ -3173,4 +3173,4 @@ class User: def __init__(self, name: str) -> None: self.name = name # E: Cannot assign to a method \ - # E: Incompatible types in assignment (expression has type "str", variable has type overloaded function) + # E: Incompatible types in assignment (expression has type "str", variable has type "Callable[..., Any]") diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index 80455141c82e..c4d1bb3d2e53 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -468,16 +468,336 @@ class B(A[Q]): a: A[int] b: B[str] reveal_type(a.g) # N: Revealed type is 'builtins.int' ---reveal_type(a.gt) # N: Revealed type is 'builtins.int' +reveal_type(a.gt) # N: Revealed type is 'builtins.int*' reveal_type(a.f()) # N: Revealed type is 'builtins.int' reveal_type(a.ft()) # N: Revealed type is '__main__.A*[builtins.int]' reveal_type(b.g) # N: Revealed type is 'builtins.int' ---reveal_type(b.gt) # N: Revealed type is '__main__.B*[builtins.str]' +reveal_type(b.gt) # N: Revealed type is 'builtins.str*' reveal_type(b.f()) # N: Revealed type is 'builtins.int' reveal_type(b.ft()) # N: Revealed type is '__main__.B*[builtins.str]' +[builtins fixtures/property.pyi] + +[case testSelfTypeRestrictedMethod] +from typing import TypeVar, Generic + +T = TypeVar('T') +class C(Generic[T]): + def from_item(self: C[str]) -> None: ... + +i: C[int] +s: C[str] + +i.from_item() # E: Invalid self argument "C[int]" to attribute function "from_item" with type "Callable[[C[str]], None]" +s.from_item() + +[case testSelfTypeRestrictedClassMethod] +from typing import TypeVar, Generic, Type + +T = TypeVar('T') +class C(Generic[T]): + @classmethod + def from_item(cls: Type[C[str]]) -> None: ... + +class DI(C[int]): ... +class DS(C[str]): ... + +DI().from_item() # E: Invalid self argument "Type[DI]" to class attribute function "from_item" with type "Callable[[Type[C[str]]], None]" +DS().from_item() +DI.from_item() # E: Invalid self argument "Type[DI]" to attribute function "from_item" with type "Callable[[Type[C[str]]], None]" +DS.from_item() +[builtins fixtures/classmethod.pyi] + +[case testSelfTypeRestrictedMethodOverload] +from typing import TypeVar, Generic, overload, Tuple + +T = TypeVar('T') +class C(Generic[T]): + @overload + def from_item(self: C[str], item: str) -> None: ... + @overload + def from_item(self: C[int], item: Tuple[int]) -> None: ... + def from_item(self, item): + ... + +ci: C[int] +cs: C[str] +reveal_type(ci.from_item) # N: Revealed type is 'def (item: Tuple[builtins.int])' +reveal_type(cs.from_item) # N: Revealed type is 'def (item: builtins.str)' + +[case testSelfTypeRestrictedMethodOverloadFallback] +from typing import TypeVar, Generic, overload, Callable + +T = TypeVar('T') +class C(Generic[T]): + @overload + def from_item(self: C[str]) -> str: ... + @overload + def from_item(self, converter: Callable[[T], str]) -> str: ... + def from_item(self, converter): + ... + +ci: C[int] +cs: C[str] +reveal_type(cs.from_item()) # N: Revealed type is 'builtins.str' +ci.from_item() # E: Too few arguments for "from_item" of "C" + +def conv(x: int) -> str: ... +def bad(x: str) -> str: ... +reveal_type(ci.from_item(conv)) # N: Revealed type is 'builtins.str' +ci.from_item(bad) # E: Argument 1 to "from_item" of "C" has incompatible type "Callable[[str], str]"; expected "Callable[[int], str]" + +[case testSelfTypeRestrictedMethodOverloadInit] +from typing import TypeVar +from lib import P, C + +reveal_type(P) # N: Revealed type is 'Overload(def [T] (use_str: Literal[True]) -> lib.P[builtins.str], def [T] (use_str: Literal[False]) -> lib.P[builtins.int])' +reveal_type(P(use_str=True)) # N: Revealed type is 'lib.P[builtins.str]' +reveal_type(P(use_str=False)) # N: Revealed type is 'lib.P[builtins.int]' + +reveal_type(C) # N: Revealed type is 'Overload(def [T] (item: T`1, use_tuple: Literal[False]) -> lib.C[T`1], def [T] (item: T`1, use_tuple: Literal[True]) -> lib.C[builtins.tuple[T`1]])' +reveal_type(C(0, use_tuple=False)) # N: Revealed type is 'lib.C[builtins.int*]' +reveal_type(C(0, use_tuple=True)) # N: Revealed type is 'lib.C[builtins.tuple[builtins.int*]]' + +T = TypeVar('T') +class SubP(P[T]): + pass + +SubP('no') # E: No overload variant of "SubP" matches argument type "str" \ + # N: Possible overload variants: \ + # N: def [T] __init__(self, use_str: Literal[True]) -> SubP[T] \ + # N: def [T] __init__(self, use_str: Literal[False]) -> SubP[T] + +# This is a bit unfortunate: we don't have a way to map the overloaded __init__ to subtype. +x = SubP(use_str=True) # E: Need type annotation for 'x' +reveal_type(x) # N: Revealed type is '__main__.SubP[Any]' +y: SubP[str] = SubP(use_str=True) + +[file lib.pyi] +from typing import TypeVar, Generic, overload, Tuple +from typing_extensions import Literal + +T = TypeVar('T') +class P(Generic[T]): + @overload + def __init__(self: P[str], use_str: Literal[True]) -> None: ... + @overload + def __init__(self: P[int], use_str: Literal[False]) -> None: ... + +class C(Generic[T]): + @overload + def __init__(self: C[T], item: T, use_tuple: Literal[False]) -> None: ... + @overload + def __init__(self: C[Tuple[T, ...]], item: T, use_tuple: Literal[True]) -> None: ... +[builtins fixtures/bool.pyi] + +[case testSelfTypeRestrictedMethodOverloadInitFallBacks] +from lib import PFallBack, PFallBackAny + +t: bool +xx = PFallBack(t) # E: Need type annotation for 'xx' +yy = PFallBackAny(t) # OK + +[file lib.pyi] +from typing import TypeVar, Generic, overload, Tuple, Any +from typing_extensions import Literal + +class PFallBack(Generic[T]): + @overload + def __init__(self: PFallBack[str], use_str: Literal[True]) -> None: ... + @overload + def __init__(self: PFallBack[int], use_str: Literal[False]) -> None: ... + @overload + def __init__(self, use_str: bool) -> None: ... + +class PFallBackAny(Generic[T]): + @overload + def __init__(self: PFallBackAny[str], use_str: Literal[True]) -> None: ... + @overload + def __init__(self: PFallBackAny[int], use_str: Literal[False]) -> None: ... + @overload + def __init__(self: PFallBackAny[Any], use_str: bool) -> None: ... +[builtins fixtures/bool.pyi] + +[case testSelfTypeRestrictedMethodOverloadInitBadTypeNoCrash] +from lib import P +P(0) +[file lib.pyi] +from typing import overload +class P: + @overload + def __init__(self: Bad, x: int) -> None: ... # E: Name 'Bad' is not defined + @overload + def __init__(self) -> None: ... + +[case testSelfTypeNarrowBinding] +from typing import TypeVar, List, Generic + +T = TypeVar('T') +S = TypeVar('S') + +class Base(Generic[T]): + def get_item(self: Base[List[S]]) -> S: ... + +class Sub(Base[List[int]]): ... +class BadSub(Base[int]): ... + +reveal_type(Sub().get_item()) # N: Revealed type is 'builtins.int' +BadSub().get_item() # E: Invalid self argument "BadSub" to attribute function "get_item" with type "Callable[[Base[List[S]]], S]" +[builtins fixtures/list.pyi] + +[case testMixinAllowedWithProtocol] +from typing import TypeVar +from typing_extensions import Protocol + +class Resource(Protocol): + def close(self) -> int: ... + +class AtomicClose: + def atomic_close(self: Resource) -> int: + return self.close() + +T = TypeVar('T', bound=Resource) +class Copyable: + def copy(self: T) -> T: ... + +class File(AtomicClose, Copyable): + def close(self) -> int: + ... + +class Bad(AtomicClose, Copyable): + ... + +f: File +b: Bad +f.atomic_close() # OK +b.atomic_close() # E: Invalid self argument "Bad" to attribute function "atomic_close" with type "Callable[[Resource], int]" + +reveal_type(f.copy()) # N: Revealed type is '__main__.File*' +b.copy() # E: Invalid self argument "Bad" to attribute function "copy" with type "Callable[[T], T]" + +[case testBadClassLevelDecoratorHack] +from typing_extensions import Protocol +from typing import TypeVar, Any + +class FuncLike(Protocol): + __call__: Any +F = TypeVar('F', bound=FuncLike) + +class Test: + def _deco(func: F) -> F: ... + + @_deco + def meth(self, x: str) -> int: ... + +reveal_type(Test().meth) # N: Revealed type is 'def (x: builtins.str) -> builtins.int' +Test()._deco # E: Invalid self argument "Test" to attribute function "_deco" with type "Callable[[F], F]" + +[case testSelfTypeTrickyExample] +from typing import * + +In = TypeVar('In') +Out = TypeVar('Out') +Mid = TypeVar('Mid') +NewOut = TypeVar('NewOut') + +class Lnk(Generic[In, Out]): + def test(self: Lnk[In, Mid], other: Lnk[Mid, NewOut]) -> Lnk[In, NewOut]: ... + +class X: pass +class Y: pass +class Z: pass + +a: Lnk[X, Y] = Lnk() +b: Lnk[Y, Z] = Lnk() + +a.test(b) +b.test(a) # E: Argument 1 to "test" of "Lnk" has incompatible type "Lnk[X, Y]"; expected "Lnk[Z, Y]" + +[case testSelfTypeReallyTrickyExample] +from typing import * + +In = TypeVar('In') +Out = TypeVar('Out') +Other = TypeVar('Other') + +_1 = TypeVar('_1') +_2 = TypeVar('_2') +__1 = TypeVar('__1') +__2 = TypeVar('__2') + +class Lnk(Generic[In, Out]): + @overload + def __rshift__(self, other: Lnk[Out, Other]) -> Lnk[In,Other]: ... + @overload + def __rshift__(self: Lnk[In, Tuple[_1, _2]], + other: Tuple[Lnk[_1, __1], Lnk[_2, __2]]) -> Lnk[In, Tuple[__1, __2]]: ... + def __rshift__(self: Any, other: Any) -> Any: + ... + +a: Lnk[str, Tuple[str, int]] = Lnk() +b: Lnk[str, int] = Lnk() +c: Lnk[int, float] = Lnk() + +d: Lnk[str, float] = b >> c # OK +e: Lnk[str, Tuple[int, float]] = a >> (b, c) # OK +f: Lnk[str, Tuple[float, int]] = a >> (c, b) # E: Unsupported operand types for >> ("Lnk[str, Tuple[str, int]]" and "Tuple[Lnk[int, float], Lnk[str, int]]") + +[case testSelfTypeMutuallyExclusiveRestrictions] +from typing import Generic, TypeVar + +T = TypeVar('T') + +class Foo(Generic[T]): + def f1(self: Foo[str]) -> None: + self.f2() # E: Invalid self argument "Foo[str]" to attribute function "f2" with type "Callable[[Foo[int]], None]" + def f2(self: Foo[int]) -> None: + self.f1() # E: Invalid self argument "Foo[int]" to attribute function "f1" with type "Callable[[Foo[str]], None]" + +[case testSelfTypeStructureMetaclassMatch] +from typing import TypeVar, Type, Generic, cast + +Cls = TypeVar('Cls') +T = TypeVar('T') + +class Manager(Generic[Cls]): + def create(self: Manager[Type[T]]) -> T: ... + +class ModelMeta(type): + @property + def objects(cls: T) -> Manager[T]: ... + +class Model(metaclass=ModelMeta): + pass + +class Dog(Model): ... +class Cat(Model): ... + +c: Cat = Dog.objects.create() # E: Incompatible types in assignment (expression has type "Dog", variable has type "Cat") +d: Dog = Dog.objects.create() [builtins fixtures/property.pyi] +[case testSelfTypeProtocolMetaclassMatch] +from typing import Type, TypeVar, Protocol + +class HasX(Protocol): + x: int + +T = TypeVar('T', bound=HasX) + +class Meta(type): + def do_x(cls: Type[T]) -> T: + cls.x + return cls() + +class Good(metaclass=Meta): + x: int +class Bad(metaclass=Meta): + pass + +Good.do_x() +Bad.do_x() # E: Invalid self argument "Type[Bad]" to attribute function "do_x" with type "Callable[[Type[T]], T]" + [case testSelfTypeNotSelfType] # Friendlier error messages for common mistakes. See #2950 class A: