Skip to content

Commit

Permalink
Rebind self-types in subclass methods without Self annotation (#15541)
Browse files Browse the repository at this point in the history
Fixes #15529

The fix is straightforward, hopefully there will be no fallout. (Note
that #14075 would also fix this, but I am still not sure we should do
that)

---------

Co-authored-by: Shantanu <[email protected]>
  • Loading branch information
ilevkivskyi and hauntsaninja authored Jun 29, 2023
1 parent 95dde9d commit b995e16
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 3 deletions.
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"

0 comments on commit b995e16

Please sign in to comment.