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

Rebind self-types in subclass methods without Self annotation #15541

Merged
merged 3 commits into from
Jun 29, 2023
Merged
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
14 changes: 12 additions & 2 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,12 +735,12 @@ def analyze_var(
"""Analyze access to an attribute via a Var node.

This is conceptually part of analyze_member_access and the arguments are similar.

itype is the class object in which var is defined
itype is the instance type in which attribute should be looked up
original_type is the type of E in the expression E.var
if implicit is True, the original Var was created as an assignment to self
"""
# Found a member variable.
original_itype = itype
itype = map_instance_to_supertype(itype, var.info)
typ = var.type
if typ:
Expand All @@ -756,6 +756,16 @@ def analyze_var(
get_proper_type(mx.original_type)
):
t = expand_self_type(var, t, mx.original_type)
elif (
mx.is_self
and original_itype.type != var.info
# If an attribute with Self-type was defined in a supertype, we need to
# rebind the Self type variable to Self type variable of current class...
and original_itype.type.self_type is not None
# ...unless `self` has an explicit non-trivial annotation.
and original_itype == mx.chk.scope.active_self_type()
):
t = expand_self_type(var, t, original_itype.type.self_type)
t = get_proper_type(expand_type_by_instance(t, itype))
freeze_all_type_vars(t)
result: Type = t
Expand Down
3 changes: 2 additions & 1 deletion mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,8 @@ def transform(self) -> bool:
self._add_dataclass_fields_magic_attribute()

if self._spec is _TRANSFORM_SPEC_FOR_DATACLASSES:
self._add_internal_replace_method(attributes)
with state.strict_optional_set(self._api.options.strict_optional):
self._add_internal_replace_method(attributes)
if "__post_init__" in info.names:
self._add_internal_post_init_method(attributes)

Expand Down
2 changes: 2 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1692,6 +1692,8 @@ def is_core_builtin_class(self, defn: ClassDef) -> bool:
def analyze_class_body_common(self, defn: ClassDef) -> None:
"""Parts of class body analysis that are common to all kinds of class defs."""
self.enter_class(defn.info)
if any(b.self_type is not None for b in defn.info.mro):
self.setup_self_type()
defn.defs.accept(self)
self.apply_class_plugin_hooks(defn)
self.leave_class()
Expand Down
17 changes: 17 additions & 0 deletions test-data/unit/check-selftype.test
Original file line number Diff line number Diff line change
Expand Up @@ -1867,3 +1867,20 @@ class B:
return B() # E: Incompatible return value type (got "B", expected "A")

[builtins fixtures/isinstancelist.pyi]

[case testAttributeOnSelfAttributeInSubclass]
from typing import List, Self

class A:
x: Self
xs: List[Self]

class B(A):
extra: int

def meth(self) -> None:
reveal_type(self.x) # N: Revealed type is "Self`0"
reveal_type(self.xs[0]) # N: Revealed type is "Self`0"
reveal_type(self.x.extra) # N: Revealed type is "builtins.int"
reveal_type(self.xs[0].extra) # N: Revealed type is "builtins.int"
[builtins fixtures/list.pyi]
17 changes: 17 additions & 0 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -2092,3 +2092,20 @@ def fst(kv: Tuple[K, V]) -> K:
pairs = [(len(s), s) for s in ["one", "two", "three"]]
grouped = groupby(pairs, key=fst)
[out]

[case testDataclassReplaceOptional]
# flags: --strict-optional
from dataclasses import dataclass, replace
from typing import Optional

@dataclass
class A:
x: Optional[int]

a = A(x=42)
reveal_type(a)
a2 = replace(a, x=None) # OK
reveal_type(a2)
[out]
_testDataclassReplaceOptional.py:10: note: Revealed type is "_testDataclassReplaceOptional.A"
_testDataclassReplaceOptional.py:12: note: Revealed type is "_testDataclassReplaceOptional.A"