Skip to content

Commit 673468c

Browse files
authored
Allow fancy self-types (#7860)
Fixes #3625 Fixes #5305 Fixes #5320 Fixes #5868 Fixes #7191 Fixes #7778 Fixes python/typing#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.
1 parent 3fc3823 commit 673468c

File tree

11 files changed

+606
-61
lines changed

11 files changed

+606
-61
lines changed

docs/source/generics.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,8 @@ or a deserialization method returns the actual type of self. Therefore
312312
you may need to silence mypy inside these methods (but not at the call site),
313313
possibly by making use of the ``Any`` type.
314314

315+
For some advanced uses of self-types see :ref:`additional examples <advanced_self>`.
316+
315317
.. _variance-of-generics:
316318

317319
Variance of generic types

docs/source/more_types.rst

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,144 @@ with ``Union[int, slice]`` and ``Union[T, Sequence]``.
561561
to returning ``Any`` only if the input arguments also contain ``Any``.
562562

563563

564+
.. _advanced_self:
565+
566+
Advanced uses of self-types
567+
***************************
568+
569+
Normally, mypy doesn't require annotations for the first arguments of instance and
570+
class methods. However, they may be needed to have more precise static typing
571+
for certain programming patterns.
572+
573+
Restricted methods in generic classes
574+
-------------------------------------
575+
576+
In generic classes some methods may be allowed to be called only
577+
for certain values of type arguments:
578+
579+
.. code-block:: python
580+
581+
T = TypeVar('T')
582+
583+
class Tag(Generic[T]):
584+
item: T
585+
def uppercase_item(self: C[str]) -> str:
586+
return self.item.upper()
587+
588+
def label(ti: Tag[int], ts: Tag[str]) -> None:
589+
ti.uppercase_item() # E: Invalid self argument "Tag[int]" to attribute function
590+
# "uppercase_item" with type "Callable[[Tag[str]], str]"
591+
ts.uppercase_item() # This is OK
592+
593+
This pattern also allows matching on nested types in situations where the type
594+
argument is itself generic:
595+
596+
.. code-block:: python
597+
598+
T = TypeVar('T')
599+
S = TypeVar('S')
600+
601+
class Storage(Generic[T]):
602+
def __init__(self, content: T) -> None:
603+
self.content = content
604+
def first_chunk(self: Storage[Sequence[S]]) -> S:
605+
return self.content[0]
606+
607+
page: Storage[List[str]]
608+
page.first_chunk() # OK, type is "str"
609+
610+
Storage(0).first_chunk() # Error: Invalid self argument "Storage[int]" to attribute function
611+
# "first_chunk" with type "Callable[[Storage[Sequence[S]]], S]"
612+
613+
Finally, one can use overloads on self-type to express precise types of
614+
some tricky methods:
615+
616+
.. code-block:: python
617+
618+
T = TypeVar('T')
619+
620+
class Tag(Generic[T]):
621+
@overload
622+
def export(self: Tag[str]) -> str: ...
623+
@overload
624+
def export(self, converter: Callable[[T], str]) -> T: ...
625+
626+
def export(self, converter=None):
627+
if isinstance(self.item, str):
628+
return self.item
629+
return converter(self.item)
630+
631+
In particular, an :py:meth:`~object.__init__` method overloaded on self-type
632+
may be useful to annotate generic class constructors where type arguments
633+
depend on constructor parameters in a non-trivial way, see e.g. :py:class:`~subprocess.Popen`.
634+
635+
Mixin classes
636+
-------------
637+
638+
Using host class protocol as a self-type in mixin methods allows
639+
more code re-usability for static typing of mixin classes. For example,
640+
one can define a protocol that defines common functionality for
641+
host classes instead of adding required abstract methods to every mixin:
642+
643+
.. code-block:: python
644+
645+
class Lockable(Protocol):
646+
@property
647+
def lock(self) -> Lock: ...
648+
649+
class AtomicCloseMixin:
650+
def atomic_close(self: Lockable) -> int:
651+
with self.lock:
652+
# perform actions
653+
654+
class AtomicOpenMixin:
655+
def atomic_open(self: Lockable) -> int:
656+
with self.lock:
657+
# perform actions
658+
659+
class File(AtomicCloseMixin, AtomicOpenMixin):
660+
def __init__(self) -> None:
661+
self.lock = Lock()
662+
663+
class Bad(AtomicCloseMixin):
664+
pass
665+
666+
f = File()
667+
b: Bad
668+
f.atomic_close() # OK
669+
b.atomic_close() # Error: Invalid self type for "atomic_close"
670+
671+
Note that the explicit self-type is *required* to be a protocol whenever it
672+
is not a supertype of the current class. In this case mypy will check the validity
673+
of the self-type only at the call site.
674+
675+
Precise typing of alternative constructors
676+
------------------------------------------
677+
678+
Some classes may define alternative constructors. If these
679+
classes are generic, self-type allows giving them precise signatures:
680+
681+
.. code-block:: python
682+
683+
T = TypeVar('T')
684+
685+
class Base(Generic[T]):
686+
Q = TypeVar('Q', bound='Base[T]')
687+
688+
def __init__(self, item: T) -> None:
689+
self.item = item
690+
691+
@classmethod
692+
def make_pair(cls: Type[Q], item: T) -> Tuple[Q, Q]:
693+
return cls(item), cls(item)
694+
695+
class Sub(Base[T]):
696+
...
697+
698+
pair = Sub.make_pair('yes') # Type is "Tuple[Sub[str], Sub[str]]"
699+
bad = Sub[int].make_pair('no') # Error: Argument 1 to "make_pair" of "Base"
700+
# has incompatible type "str"; expected "int"
701+
564702
.. _async-and-await:
565703

566704
Typing async/await

mypy/checker.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -906,10 +906,18 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
906906
isclass = defn.is_class or defn.name() in ('__new__', '__init_subclass__')
907907
if isclass:
908908
ref_type = mypy.types.TypeType.make_normalized(ref_type)
909-
erased = erase_to_bound(arg_type)
909+
erased = get_proper_type(erase_to_bound(arg_type))
910910
if not is_subtype_ignoring_tvars(ref_type, erased):
911911
note = None
912-
if typ.arg_names[i] in ['self', 'cls']:
912+
if (isinstance(erased, Instance) and erased.type.is_protocol or
913+
isinstance(erased, TypeType) and
914+
isinstance(erased.item, Instance) and
915+
erased.item.type.is_protocol):
916+
# We allow the explicit self-type to be not a supertype of
917+
# the current class if it is a protocol. For such cases
918+
# the consistency check will be performed at call sites.
919+
msg = None
920+
elif typ.arg_names[i] in {'self', 'cls'}:
913921
if (self.options.python_version[0] < 3
914922
and is_same_type(erased, arg_type) and not isclass):
915923
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])
919927
erased, ref_type)
920928
else:
921929
msg = message_registry.MISSING_OR_INVALID_SELF_TYPE
922-
self.fail(msg, defn)
923-
if note:
924-
self.note(note, defn)
930+
if msg:
931+
self.fail(msg, defn)
932+
if note:
933+
self.note(note, defn)
925934
elif isinstance(arg_type, TypeVarType):
926935
# Refuse covariant parameter type variables
927936
# TODO: check recursively for inner type variables

mypy/checkmember.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ def analyze_instance_member_access(name: str,
200200
# the first argument.
201201
pass
202202
else:
203+
if isinstance(signature, FunctionLike) and name != '__call__':
204+
# TODO: use proper treatment of special methods on unions instead
205+
# of this hack here and below (i.e. mx.self_type).
206+
dispatched_type = meet.meet_types(mx.original_type, typ)
207+
signature = check_self_arg(signature, dispatched_type, False, mx.context,
208+
name, mx.msg)
203209
signature = bind_self(signature, mx.self_type)
204210
typ = map_instance_to_supertype(typ, method.info)
205211
member_type = expand_type_by_instance(signature, typ)
@@ -546,8 +552,8 @@ def analyze_var(name: str,
546552
# In `x.f`, when checking `x` against A1 we assume x is compatible with A
547553
# and similarly for B1 when checking agains B
548554
dispatched_type = meet.meet_types(mx.original_type, itype)
549-
check_self_arg(functype, dispatched_type, var.is_classmethod, mx.context, name,
550-
mx.msg)
555+
functype = check_self_arg(functype, dispatched_type, var.is_classmethod,
556+
mx.context, name, mx.msg)
551557
signature = bind_self(functype, mx.self_type, var.is_classmethod)
552558
if var.is_property:
553559
# A property cannot have an overloaded type => the cast is fine.
@@ -596,27 +602,45 @@ def check_self_arg(functype: FunctionLike,
596602
dispatched_arg_type: Type,
597603
is_classmethod: bool,
598604
context: Context, name: str,
599-
msg: MessageBuilder) -> None:
600-
"""For x.f where A.f: A1 -> T, check that meet(type(x), A) <: A1 for each overload.
605+
msg: MessageBuilder) -> FunctionLike:
606+
"""Check that an instance has a valid type for a method with annotated 'self'.
601607
602-
dispatched_arg_type is meet(B, A) in the following example
603-
604-
def g(x: B): x.f
608+
For example if the method is defined as:
605609
class A:
606-
f: Callable[[A1], None]
610+
def f(self: S) -> T: ...
611+
then for 'x.f' we check that meet(type(x), A) <: S. If the method is overloaded, we
612+
select only overloads items that satisfy this requirement. If there are no matching
613+
overloads, an error is generated.
614+
615+
Note: dispatched_arg_type uses a meet to select a relevant item in case if the
616+
original type of 'x' is a union. This is done because several special methods
617+
treat union types in ad-hoc manner, so we can't use MemberContext.self_type yet.
607618
"""
608-
# TODO: this is too strict. We can return filtered overloads for matching definitions
609-
for item in functype.items():
619+
items = functype.items()
620+
if not items:
621+
return functype
622+
new_items = []
623+
for item in items:
610624
if not item.arg_types or item.arg_kinds[0] not in (ARG_POS, ARG_STAR):
611625
# No positional first (self) argument (*args is okay).
612626
msg.no_formal_self(name, item, context)
627+
# This is pretty bad, so just return the original signature if
628+
# there is at least one such error.
629+
return functype
613630
else:
614631
selfarg = item.arg_types[0]
615632
if is_classmethod:
616633
dispatched_arg_type = TypeType.make_normalized(dispatched_arg_type)
617-
if not subtypes.is_subtype(dispatched_arg_type, erase_to_bound(selfarg)):
618-
msg.incompatible_self_argument(name, dispatched_arg_type, item,
619-
is_classmethod, context)
634+
if subtypes.is_subtype(dispatched_arg_type, erase_typevars(erase_to_bound(selfarg))):
635+
new_items.append(item)
636+
if not new_items:
637+
# Choose first item for the message (it may be not very helpful for overloads).
638+
msg.incompatible_self_argument(name, dispatched_arg_type, items[0],
639+
is_classmethod, context)
640+
return functype
641+
if len(new_items) == 1:
642+
return new_items[0]
643+
return Overloaded(new_items)
620644

621645

622646
def analyze_class_attribute_access(itype: Instance,
@@ -702,7 +726,10 @@ def analyze_class_attribute_access(itype: Instance,
702726

703727
is_classmethod = ((is_decorated and cast(Decorator, node.node).func.is_class)
704728
or (isinstance(node.node, FuncBase) and node.node.is_class))
705-
result = add_class_tvars(get_proper_type(t), itype, isuper, is_classmethod,
729+
t = get_proper_type(t)
730+
if isinstance(t, FunctionLike) and is_classmethod:
731+
t = check_self_arg(t, mx.self_type, False, mx.context, name, mx.msg)
732+
result = add_class_tvars(t, itype, isuper, is_classmethod,
706733
mx.builtin_type, mx.self_type)
707734
if not mx.is_lvalue:
708735
result = analyze_descriptor_access(mx.original_type, result, mx.builtin_type,

mypy/infer.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
from typing import List, Optional, Sequence
44

5-
from mypy.constraints import infer_constraints, infer_constraints_for_callable
5+
from mypy.constraints import (
6+
infer_constraints, infer_constraints_for_callable, SUBTYPE_OF, SUPERTYPE_OF
7+
)
68
from mypy.types import Type, TypeVarId, CallableType
79
from mypy.solve import solve_constraints
8-
from mypy.constraints import SUBTYPE_OF
910

1011

1112
def infer_function_type_arguments(callee_type: CallableType,
@@ -36,8 +37,10 @@ def infer_function_type_arguments(callee_type: CallableType,
3637

3738

3839
def infer_type_arguments(type_var_ids: List[TypeVarId],
39-
template: Type, actual: Type) -> List[Optional[Type]]:
40+
template: Type, actual: Type,
41+
is_supertype: bool = False) -> List[Optional[Type]]:
4042
# Like infer_function_type_arguments, but only match a single type
4143
# against a generic type.
42-
constraints = infer_constraints(template, actual, SUBTYPE_OF)
44+
constraints = infer_constraints(template, actual,
45+
SUPERTYPE_OF if is_supertype else SUBTYPE_OF)
4346
return solve_constraints(type_var_ids, constraints)

mypy/meet.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,10 @@ def visit_instance(self, t: Instance) -> ProperType:
493493
call = unpack_callback_protocol(t)
494494
if call:
495495
return meet_types(call, self.s)
496+
elif isinstance(self.s, FunctionLike) and self.s.is_type_obj() and t.type.is_metaclass():
497+
if is_subtype(self.s.fallback, t):
498+
return self.s
499+
return self.default(self.s)
496500
elif isinstance(self.s, TypeType):
497501
return meet_types(t, self.s)
498502
elif isinstance(self.s, TupleType):

0 commit comments

Comments
 (0)