Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
from __future__ import annotations

import re
from collections.abc import Callable, Collection, Iterable, Iterator
from collections.abc import Callable, Collection, Iterable, Iterator, Sequence
from contextlib import contextmanager
from typing import Any, Final, TypeAlias as _TypeAlias, TypeGuard, TypeVar, cast
from typing_extensions import assert_never
Expand All @@ -60,6 +60,7 @@
from mypy.constant_fold import constant_fold_expr
from mypy.errorcodes import PROPERTY_DECORATOR, ErrorCode
from mypy.errors import Errors, report_internal_error
from mypy.expandtype import expand_type
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
from mypy.message_registry import CANNOT_DECLARE_TYPE_OF_SPECIAL_FORM, ErrorMessage
from mypy.messages import (
Expand Down Expand Up @@ -1198,20 +1199,43 @@ def is_expected_self_type(self, typ: Type, is_classmethod: bool) -> bool:
return self.is_expected_self_type(typ.item, is_classmethod=False)
if isinstance(typ, UnboundType):
sym = self.lookup_qualified(typ.name, typ, suppress_errors=True)
if sym is not None and sym.fullname in TYPE_NAMES and typ.args:
if sym is None or not typ.args:
return False
if sym.fullname in TYPE_NAMES:
return self.is_expected_self_type(typ.args[0], is_classmethod=False)
if sym is not None and sym.fullname in ANNOTATED_TYPE_NAMES and typ.args:
if sym.fullname in ANNOTATED_TYPE_NAMES:
return self.is_expected_self_type(typ.args[0], is_classmethod=True)
expanded = self.expand_type_alias_for_self_check(sym.node, typ.args)
if expanded is not None:
return self.is_expected_self_type(expanded, is_classmethod=True)
return False

if isinstance(typ, TypeVarType):
return typ == self.type.self_type
if isinstance(typ, UnboundType):
sym = self.lookup_qualified(typ.name, typ, suppress_errors=True)
if sym is not None and sym.fullname in ANNOTATED_TYPE_NAMES and typ.args:
if sym is None:
return False
if sym.fullname in ANNOTATED_TYPE_NAMES and typ.args:
return self.is_expected_self_type(typ.args[0], is_classmethod=False)
return sym is not None and sym.fullname in SELF_TYPE_NAMES
expanded = self.expand_type_alias_for_self_check(sym.node, typ.args)
if expanded is not None:
return self.is_expected_self_type(expanded, is_classmethod=False)
return sym.fullname in SELF_TYPE_NAMES
return False

def expand_type_alias_for_self_check(
self, node: SymbolNode | None, args: Sequence[Type]
) -> Type | None:
"""Substitute a type alias's own type parameters with the given arguments
(e.g. "Ident[Self]" for "type Ident[T] = T" gives back "Self"), so the
result can be checked like any other type.
"""
if not isinstance(node, TypeAlias) or len(node.alias_tvars) != len(args):
return None
env = {tvar.id: arg for tvar, arg in zip(node.alias_tvars, args)}
return expand_type(node.target, env)

def set_original_def(self, previous: Node | None, new: FuncDef | Decorator) -> bool:
"""If 'new' conditionally redefine 'previous', set 'previous' as original

Expand Down
78 changes: 78 additions & 0 deletions test-data/unit/check-selftype.test
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,84 @@ reveal_type(C.foo) # N: Revealed type is "def [Self <: __main__.C] (self: Self`2
reveal_type(C.bar) # N: Revealed type is "def (x: builtins.int)"
[builtins fixtures/tuple.pyi]

[case testTypingSelfIdentityTypeAliasMinimal]
# flags: --python-version 3.12
type Ident[T] = T
def f(x: Ident[int]) -> Ident[int]:
return x
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-full.pyi]

[case testTypingSelfIdentityTypeAlias]
# flags: --python-version 3.12
from typing import Self

type Ident[T] = T

class C:
def foo(self: Ident[Self], x: int) -> None:
...

@classmethod
def bar(cls: Ident[type[Self]], x: int) -> None:
...

@classmethod
def baz(cls: type[Ident[Self]], x: int) -> None:
...

reveal_type(C.foo) # N: Revealed type is "def [Self <: __main__.C] (self: Self`10, x: builtins.int)"
reveal_type(C.bar) # N: Revealed type is "def (x: builtins.int)"
reveal_type(C.baz) # N: Revealed type is "def (x: builtins.int)"

class D:
def qux(self: Ident[int]) -> None: # E: The erased type of self "builtins.int" is not a supertype of its class "__main__.D"
...
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-full.pyi]

[case testTypingSelfIdentityTypeAliasSubstitution]
# flags: --python-version 3.12
# The identity-alias check works by substituting the alias's own arguments into
# its target (mypy.expandtype.expand_type) and checking the result, rather than
# only recognizing the literal shape "type X[T] = T". This covers, for free:
# extra unused type parameters, any parameter order, and a target that only
# collapses to Self once its members are substituted with the same type.
from typing import Self

type Extra[T, S] = T
type Extra2[S, T] = T
type UnionSame[T] = T | T

class C:
def union_collapses(self: UnionSame[Self]) -> None:
...

def extra_unused_first(self: Extra[Self, int]) -> None:
...

def extra_unused_second(self: Extra2[int, Self]) -> None:
...

@classmethod
def extra_classmethod(cls: Extra[type[Self], int]) -> None:
...

reveal_type(C.union_collapses) # N: Revealed type is "def [Self <: __main__.C] (self: Self`10 | Self`10)"
reveal_type(C.extra_unused_first) # N: Revealed type is "def [Self <: __main__.C] (self: Self`11)"
reveal_type(C.extra_unused_second) # N: Revealed type is "def [Self <: __main__.C] (self: Self`12)"
reveal_type(C.extra_classmethod) # N: Revealed type is "def ()"

class D:
# Self supplied for the alias's *unused* type parameter must not be mistaken
# for the one that actually reaches the target -- this must still be a real
# self-type error, not silently accepted.
def wrong_slot(self: Extra[int, Self]) -> None: # E: Method cannot have explicit self annotation and Self type \
# E: The erased type of self "builtins.int" is not a supertype of its class "__main__.D"
...
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-full.pyi]

[case testTypingSelfOverload]
from typing import Self, overload, Union

Expand Down
Loading