From b995e1620789a3ab2fc5dbcf0698a9077b0f5731 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 29 Jun 2023 23:34:15 +0100 Subject: [PATCH] Rebind self-types in subclass methods without Self annotation (#15541) 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 <12621235+hauntsaninja@users.noreply.github.com> --- mypy/checkmember.py | 14 ++++++++++++-- mypy/plugins/dataclasses.py | 3 ++- mypy/semanal.py | 2 ++ test-data/unit/check-selftype.test | 17 +++++++++++++++++ test-data/unit/pythoneval.test | 17 +++++++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 7af61a532b7b..36fea9daa3e3 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -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: @@ -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 diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index bb3009dddf10..9e054493828f 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -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) diff --git a/mypy/semanal.py b/mypy/semanal.py index 1eb14e499e4d..25393096bc5f 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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() diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index 53c24584cb73..96d5b2306427 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -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] diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index b43dcbd7088f..1460002e1b65 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -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"