Skip to content
Open
15 changes: 14 additions & 1 deletion mypy/plugins/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
deserialize_and_fixup_type,
)
from mypy.server.trigger import make_wildcard_trigger
from mypy.state import state
from mypy.typeops import get_type_vars, make_simplified_union, map_type_from_supertype
from mypy.types import (
AnyType,
Expand Down Expand Up @@ -123,10 +124,19 @@ def __init__(
self.context = context
self.init_type = init_type

def expand_type(self, typ: Type | None, ctx: mypy.plugin.ClassDefContext) -> Type | None:
if typ is not None and self.info.self_type is not None:
# In general, it is not safe to call `expand_type()` during semantic analyzis,
# however this plugin is called very late, so all types should be fully ready.
# Also, it is tricky to avoid eager expansion of Self types here (e.g. because
# we serialize attributes).
return expand_type(typ, {self.info.self_type.id: fill_typevars(ctx.cls.info)})
return typ

def argument(self, ctx: mypy.plugin.ClassDefContext) -> Argument:
"""Return this attribute as an argument to __init__."""
assert self.init
init_type: Type | None = None
init_type: Type | None
if self.converter:
if self.converter.init_type:
init_type = self.converter.init_type
Expand Down Expand Up @@ -171,6 +181,9 @@ def argument(self, ctx: mypy.plugin.ClassDefContext) -> Argument:
else:
arg_kind = ARG_OPT if self.has_default else ARG_POS

with state.strict_optional_set(ctx.api.options.strict_optional):
init_type = self.expand_type(init_type, ctx)

# Attrs removes leading underscores when creating the __init__ arguments.
return Argument(Var(self.name.lstrip("_"), init_type), init_type, None, arg_kind)

Expand Down
4 changes: 2 additions & 2 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import Iterator, Optional
from typing import Iterator
from typing_extensions import Final

from mypy import errorcodes, message_registry
Expand Down Expand Up @@ -120,7 +120,7 @@ def to_argument(self, current_info: TypeInfo) -> Argument:
kind=arg_kind,
)

def expand_type(self, current_info: TypeInfo) -> Optional[Type]:
def expand_type(self, current_info: TypeInfo) -> Type | None:
if self.type is not None and self.info.self_type is not None:
# In general, it is not safe to call `expand_type()` during semantic analyzis,
# however this plugin is called very late, so all types should be fully ready.
Expand Down
25 changes: 25 additions & 0 deletions test-data/unit/check-plugin-attrs.test
Original file line number Diff line number Diff line change
Expand Up @@ -1978,6 +1978,31 @@ D(1, "").b = "2" # E: Cannot assign to final attribute "b"

[builtins fixtures/property.pyi]

[case testSelfInClassInit]
# flags: --strict-optional
from attrs import define
from typing import Union, Self

@define
class C:
a: Union[Self, None] = None

reveal_type(C) # N: Revealed type is "def (a: Union[__main__.C, None] =) -> __main__.C"
C(C())
C(None)
reveal_type(C(C()).a) # N: Revealed type is "Union[__main__.C, None]"

@define
class S(C):
...

reveal_type(S) # N: Revealed type is "def (a: Union[__main__.S, None] =) -> __main__.S"
S(S())
S(None)
reveal_type(S(S()).a) # N: Revealed type is "Union[__main__.S, None]"

[builtins fixtures/property.pyi]

[case testEvolve]
import attr

Expand Down