Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix mishandling of typing.Self in attrs generated inits #14689

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
8 changes: 5 additions & 3 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2492,9 +2492,9 @@ class C(B, A[int]): ... # this is unsafe because...
ok = is_subtype(first_sig, second_sig, ignore_pos_arg_names=True)
elif first_type and second_type:
if isinstance(first.node, Var):
first_type = expand_self_type(first.node, first_type, fill_typevars(ctx))
first_type = expand_self_type(first_type, first.node.info, fill_typevars(ctx))
if isinstance(second.node, Var):
second_type = expand_self_type(second.node, second_type, fill_typevars(ctx))
second_type = expand_self_type(second_type, second.node.info, fill_typevars(ctx))
ok = is_equivalent(first_type, second_type)
if not ok:
second_node = base2[name].node
Expand Down Expand Up @@ -3062,7 +3062,9 @@ def lvalue_type_from_base(
base_node = base_var.node
base_type = base_var.type
if isinstance(base_node, Var) and base_type is not None:
base_type = expand_self_type(base_node, base_type, fill_typevars(expr_node.info))
base_type = expand_self_type(
base_type, base_node.info, fill_typevars(expr_node.info)
)
if isinstance(base_node, Decorator):
base_node = base_node.func
base_type = base_node.type
Expand Down
6 changes: 3 additions & 3 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ def analyze_var(
if not (mx.is_self or mx.is_super) or supported_self_type(
get_proper_type(mx.original_type)
):
t = expand_self_type(var, t, mx.original_type)
t = expand_self_type(t, var.info, mx.original_type)
t = get_proper_type(expand_type_by_instance(t, itype))
freeze_all_type_vars(t)
result: Type = t
Expand Down Expand Up @@ -768,7 +768,7 @@ def analyze_var(
# and similarly for B1 when checking against B
dispatched_type = meet.meet_types(mx.original_type, itype)
signature = freshen_all_functions_type_vars(functype)
bound = get_proper_type(expand_self_type(var, signature, mx.original_type))
bound = get_proper_type(expand_self_type(signature, var.info, mx.original_type))
assert isinstance(bound, FunctionLike)
signature = bound
signature = check_self_arg(
Expand Down Expand Up @@ -986,7 +986,7 @@ def analyze_class_attribute_access(
# In the above example this means that we infer following types:
# C.x -> Any
# C[int].x -> int
t = get_proper_type(expand_self_type(node.node, t, itype))
t = get_proper_type(expand_self_type(t, node.node.info, itype))
t = erase_typevars(expand_type_by_instance(t, isuper), {tv.id for tv in def_vars})

is_classmethod = (is_decorated and cast(Decorator, node.node).func.is_class) or (
Expand Down
18 changes: 12 additions & 6 deletions mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Iterable, Mapping, Sequence, TypeVar, cast, overload
from typing_extensions import Final

from mypy.nodes import ARG_POS, ARG_STAR, ArgKind, Var
from mypy.nodes import ARG_POS, ARG_STAR, ArgKind, TypeInfo
from mypy.type_visitor import TypeTranslator
from mypy.types import (
ANY_STRATEGY,
Expand Down Expand Up @@ -508,17 +508,23 @@ def expand_unpack_with_variables(


@overload
def expand_self_type(var: Var, typ: ProperType, replacement: ProperType) -> ProperType:
def expand_self_type(typ: ProperType, info: TypeInfo, replacement: ProperType) -> ProperType:
...


@overload
def expand_self_type(var: Var, typ: Type, replacement: Type) -> Type:
def expand_self_type(typ: Type, info: TypeInfo, replacement: Type) -> Type:
...


def expand_self_type(var: Var, typ: Type, replacement: Type) -> Type:
@overload
def expand_self_type(typ: None, info: TypeInfo, replacement: Type) -> None:
...


def expand_self_type(typ: Type | None, info: TypeInfo, replacement: Type) -> Type | None:
"""Expand appearances of Self type in a variable type."""
if var.info.self_type is not None and not var.is_property:
return expand_type(typ, {var.info.self_type.id: replacement})
if typ is not None and info.self_type is not None:
davfsa marked this conversation as resolved.
Show resolved Hide resolved
return expand_type(typ, {info.self_type.id: replacement})

return typ
7 changes: 6 additions & 1 deletion mypy/plugins/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing_extensions import Final, Literal

import mypy.plugin # To avoid circular imports.
from mypy.expandtype import expand_self_type
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
from mypy.nodes import (
ARG_NAMED,
Expand Down Expand Up @@ -46,6 +47,7 @@
deserialize_and_fixup_type,
)
from mypy.server.trigger import make_wildcard_trigger
from mypy.state import state
from mypy.typeops import make_simplified_union, map_type_from_supertype
from mypy.types import (
AnyType,
Expand Down Expand Up @@ -112,7 +114,7 @@ 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 @@ -147,6 +149,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 = expand_self_type(init_type, self.info, fill_typevars(self.info))

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

Expand Down
16 changes: 3 additions & 13 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

from __future__ import annotations

from typing import Optional
from typing_extensions import Final

from mypy.expandtype import expand_type
from mypy.expandtype import expand_self_type
from mypy.nodes import (
ARG_NAMED,
ARG_NAMED_OPT,
Expand Down Expand Up @@ -106,22 +105,13 @@ def to_argument(self, current_info: TypeInfo) -> Argument:
arg_kind = ARG_OPT
return Argument(
variable=self.to_var(current_info),
type_annotation=self.expand_type(current_info),
type_annotation=expand_self_type(self.type, self.info, fill_typevars(current_info)),
initializer=None,
kind=arg_kind,
)

def expand_type(self, current_info: TypeInfo) -> Optional[Type]:
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.
# Also, it is tricky to avoid eager expansion of Self types here (e.g. because
# we serialize attributes).
return expand_type(self.type, {self.info.self_type.id: fill_typevars(current_info)})
return self.type

def to_var(self, current_info: TypeInfo) -> Var:
return Var(self.name, self.expand_type(current_info))
return Var(self.name, expand_self_type(self.type, self.info, fill_typevars(current_info)))
davfsa marked this conversation as resolved.
Show resolved Hide resolved

def serialize(self) -> JsonDict:
assert self.type
Expand Down
2 changes: 1 addition & 1 deletion mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1249,7 +1249,7 @@ def find_node_type(
else:
typ = node.type
if typ is not None:
typ = expand_self_type(node, typ, subtype)
typ = expand_self_type(typ, node.info, subtype)
p_typ = get_proper_type(typ)
if typ is None:
return AnyType(TypeOfAny.from_error)
Expand Down
17 changes: 16 additions & 1 deletion test-data/unit/check-attr.test
Original file line number Diff line number Diff line change
Expand Up @@ -1866,4 +1866,19 @@ reveal_type(D) # N: Revealed type is "def (a: builtins.int, b: builtins.str) ->
D(1, "").a = 2 # E: Cannot assign to final attribute "a"
D(1, "").b = "2" # E: Cannot assign to final attribute "b"

[builtins fixtures/property.pyi]
[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)
davfsa marked this conversation as resolved.
Show resolved Hide resolved

[builtins fixtures/property.pyi]